<?php

/**
 * NotificationService - CRUD operations for notifications
 *
 * Handles notifications with:
 * - Specific dates (schedule_type = 'dates')
 * - Recurrence rules (schedule_type = 'recurrence')
 * - Subsequent (follow-up) definitions
 */

declare(strict_types=1);

namespace Notifier\Services;

use PDO;
use DateTime;
use DateTimeZone;

class NotificationService
{
    private PDO $pdo;
    private RecurrenceCalculator $recurrenceCalc;
    private string $displayTimezone;

    private const MAX_MESSAGE_LENGTH = 4096;
    private const VALID_STATUSES = ['active', 'paused', 'completed'];
    private const VALID_SCHEDULE_TYPES = ['dates', 'recurrence'];

    public function __construct(PDO $pdo, string $displayTimezone = 'America/Mexico_City')
    {
        $this->pdo = $pdo;
        $this->recurrenceCalc = new RecurrenceCalculator();
        $this->displayTimezone = $displayTimezone;
    }

    /**
     * Get all notifications with optional filtering.
     *
     * @param array $filters Optional filters: category_id, status, search
     * @return array List of notifications
     */
    public function getAll(array $filters = []): array
    {
        $sql = '
            SELECT n.*, c.name as category_name, c.color as category_color, c.icon as category_icon,
                   r.next_trigger_at
            FROM notifications n
            LEFT JOIN categories c ON n.category_id = c.id
            LEFT JOIN notification_recurrence r ON n.id = r.notification_id
            WHERE 1=1
        ';
        $params = [];

        if (!empty($filters['category_id'])) {
            $sql .= ' AND n.category_id = ?';
            $params[] = $filters['category_id'];
        }

        if (!empty($filters['status'])) {
            $sql .= ' AND n.status = ?';
            $params[] = $filters['status'];
        }

        if (!empty($filters['search'])) {
            $sql .= ' AND (n.title LIKE ? OR n.message LIKE ?)';
            $searchTerm = '%' . $filters['search'] . '%';
            $params[] = $searchTerm;
            $params[] = $searchTerm;
        }

        $sql .= ' ORDER BY n.created_at DESC';

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        $notifications = $stmt->fetchAll(PDO::FETCH_ASSOC);

        // For date-based notifications, get the next scheduled date
        foreach ($notifications as &$notification) {
            if ($notification['schedule_type'] === 'dates' && $notification['status'] === 'active') {
                $notification['next_trigger_at'] = $this->getNextDateTrigger($notification['id']);
            }
        }

        return $notifications;
    }

    /**
     * Get a notification by ID with full details.
     *
     * @param int $id Notification ID
     * @return array|null Full notification data or null if not found
     */
    public function getById(int $id): ?array
    {
        $stmt = $this->pdo->prepare('
            SELECT n.*, c.name as category_name, c.color as category_color, c.icon as category_icon
            FROM notifications n
            LEFT JOIN categories c ON n.category_id = c.id
            WHERE n.id = ?
        ');
        $stmt->execute([$id]);
        $notification = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$notification) {
            return null;
        }

        // Get dates if schedule_type = 'dates'
        if ($notification['schedule_type'] === 'dates') {
            $stmt = $this->pdo->prepare('
                SELECT id, scheduled_at
                FROM notification_dates
                WHERE notification_id = ?
                ORDER BY scheduled_at ASC
            ');
            $stmt->execute([$id]);
            $notification['dates'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
        }

        // Get recurrence if schedule_type = 'recurrence'
        if ($notification['schedule_type'] === 'recurrence') {
            $stmt = $this->pdo->prepare('
                SELECT *
                FROM notification_recurrence
                WHERE notification_id = ?
            ');
            $stmt->execute([$id]);
            $recurrence = $stmt->fetch(PDO::FETCH_ASSOC);

            // Decode JSON arrays for months and weekdays
            if ($recurrence) {
                if (!empty($recurrence['months']) && \is_string($recurrence['months'])) {
                    $recurrence['months'] = \json_decode($recurrence['months'], true) ?? [];
                }
                if (!empty($recurrence['weekdays']) && \is_string($recurrence['weekdays'])) {
                    $recurrence['weekdays'] = \json_decode($recurrence['weekdays'], true) ?? [];
                }
            }

            $notification['recurrence'] = $recurrence ?: null;
        }

        // Get subsequents
        $stmt = $this->pdo->prepare('
            SELECT id, offset_minutes, offset_hours, offset_days, message, sort_order, image_path
            FROM notification_subsequents
            WHERE notification_id = ?
            ORDER BY sort_order ASC
        ');
        $stmt->execute([$id]);
        $notification['subsequents'] = $stmt->fetchAll(PDO::FETCH_ASSOC);

        return $notification;
    }

    /**
     * Create a new notification.
     *
     * @param array $data Notification data
     * @return array ['success' => bool, 'id' => int|null, 'error' => string|null]
     */
    public function create(array $data): array
    {
        // Validate
        $validation = $this->validate($data);
        if (!$validation['valid']) {
            return ['success' => false, 'id' => null, 'error' => $validation['error']];
        }

        try {
            $this->pdo->beginTransaction();

            // Insert notification
            $stmt = $this->pdo->prepare('
                INSERT INTO notifications (title, message, context, category_id, timezone, image_path, status, schedule_type, yearly_repeat)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
            ');
            $stmt->execute([
                $data['title'],
                $data['message'],
                $data['context'] ?? null,
                $data['category_id'] ?? null,
                $data['timezone'] ?? $this->displayTimezone,
                $data['image_path'] ?? null,
                'active',
                $data['schedule_type'],
                ($data['schedule_type'] === 'dates' && !empty($data['yearly_repeat'])) ? 1 : 0,
            ]);
            $notificationId = (int) $this->pdo->lastInsertId();

            // Insert dates or recurrence
            if ($data['schedule_type'] === 'dates') {
                $this->insertDates($notificationId, $data['dates'], $data['timezone'] ?? $this->displayTimezone);
            } else {
                $this->insertRecurrence($notificationId, $data['recurrence'], $data['timezone'] ?? $this->displayTimezone);
            }

            // Insert subsequents
            if (!empty($data['subsequents'])) {
                $this->insertSubsequents($notificationId, $data['subsequents']);
            }

            $this->pdo->commit();

            return ['success' => true, 'id' => $notificationId, 'error' => null];
        } catch (\Exception $e) {
            $this->pdo->rollBack();
            return ['success' => false, 'id' => null, 'error' => 'Database error: ' . $e->getMessage()];
        }
    }

    /**
     * Update a notification.
     *
     * @param int $id Notification ID
     * @param array $data Updated notification data
     * @return array ['success' => bool, 'error' => string|null]
     */
    public function update(int $id, array $data): array
    {
        // Check if notification exists
        $existing = $this->getById($id);
        if (!$existing) {
            return ['success' => false, 'error' => 'Notification not found'];
        }

        // Validate
        $validation = $this->validate($data, $id);
        if (!$validation['valid']) {
            return ['success' => false, 'error' => $validation['error']];
        }

        try {
            $this->pdo->beginTransaction();

            // Update notification
            $stmt = $this->pdo->prepare('
                UPDATE notifications
                SET title = ?, message = ?, context = ?, category_id = ?,
                    timezone = ?, image_path = ?, schedule_type = ?, yearly_repeat = ?
                WHERE id = ?
            ');
            $stmt->execute([
                $data['title'],
                $data['message'],
                $data['context'] ?? null,
                $data['category_id'] ?? null,
                $data['timezone'] ?? $this->displayTimezone,
                $data['image_path'] ?? $existing['image_path'],
                $data['schedule_type'],
                ($data['schedule_type'] === 'dates' && !empty($data['yearly_repeat'])) ? 1 : 0,
                $id,
            ]);

            // Delete existing dates/recurrence
            $this->pdo->prepare('DELETE FROM notification_dates WHERE notification_id = ?')->execute([$id]);
            $this->pdo->prepare('DELETE FROM notification_recurrence WHERE notification_id = ?')->execute([$id]);

            // Insert new dates or recurrence
            if ($data['schedule_type'] === 'dates') {
                $this->insertDates($id, $data['dates'], $data['timezone'] ?? $this->displayTimezone);
            } else {
                $this->insertRecurrence($id, $data['recurrence'], $data['timezone'] ?? $this->displayTimezone);
            }

            // Delete and re-insert subsequents
            $this->pdo->prepare('DELETE FROM notification_subsequents WHERE notification_id = ?')->execute([$id]);
            if (!empty($data['subsequents'])) {
                $this->insertSubsequents($id, $data['subsequents']);
            }

            // Cancel pending jobs (they'll be recreated by scheduler)
            $this->cancelPendingJobs($id, 'notification_edited');

            $this->pdo->commit();

            return ['success' => true, 'error' => null];
        } catch (\Exception $e) {
            $this->pdo->rollBack();
            return ['success' => false, 'error' => 'Database error: ' . $e->getMessage()];
        }
    }

    /**
     * Delete a notification.
     *
     * @param int $id Notification ID
     * @return array ['success' => bool, 'error' => string|null, 'image_path' => string|null]
     */
    public function delete(int $id): array
    {
        $existing = $this->getById($id);
        if (!$existing) {
            return ['success' => false, 'error' => 'Notification not found', 'image_path' => null];
        }

        try {
            // CASCADE delete handles related tables
            $stmt = $this->pdo->prepare('DELETE FROM notifications WHERE id = ?');
            $stmt->execute([$id]);

            return [
                'success' => true,
                'error' => null,
                'image_path' => $existing['image_path'], // Caller should delete this file
            ];
        } catch (\PDOException $e) {
            return ['success' => false, 'error' => 'Database error: ' . $e->getMessage(), 'image_path' => null];
        }
    }

    /**
     * Pause a notification.
     *
     * @param int $id Notification ID
     * @return array ['success' => bool, 'error' => string|null]
     */
    public function pause(int $id): array
    {
        $existing = $this->getById($id);
        if (!$existing) {
            return ['success' => false, 'error' => 'Notification not found'];
        }

        if ($existing['status'] !== 'active') {
            return ['success' => false, 'error' => 'Only active notifications can be paused'];
        }

        try {
            $this->pdo->beginTransaction();

            $stmt = $this->pdo->prepare('UPDATE notifications SET status = ? WHERE id = ?');
            $stmt->execute(['paused', $id]);

            // Cancel pending jobs
            $this->cancelPendingJobs($id, 'notification_paused');

            $this->pdo->commit();

            return ['success' => true, 'error' => null];
        } catch (\Exception $e) {
            $this->pdo->rollBack();
            return ['success' => false, 'error' => 'Database error: ' . $e->getMessage()];
        }
    }

    /**
     * Resume a paused notification.
     *
     * @param int $id Notification ID
     * @return array ['success' => bool, 'error' => string|null]
     */
    public function resume(int $id): array
    {
        $existing = $this->getById($id);
        if (!$existing) {
            return ['success' => false, 'error' => 'Notification not found'];
        }

        if ($existing['status'] !== 'paused') {
            return ['success' => false, 'error' => 'Only paused notifications can be resumed'];
        }

        try {
            $this->pdo->beginTransaction();

            $stmt = $this->pdo->prepare('UPDATE notifications SET status = ? WHERE id = ?');
            $stmt->execute(['active', $id]);

            // Recalculate next_trigger_at for recurrence
            if ($existing['schedule_type'] === 'recurrence' && $existing['recurrence']) {
                $this->updateNextTriggerAt($id, $existing['recurrence'], $existing['timezone']);
            }

            $this->pdo->commit();

            return ['success' => true, 'error' => null];
        } catch (\Exception $e) {
            $this->pdo->rollBack();
            return ['success' => false, 'error' => 'Database error: ' . $e->getMessage()];
        }
    }

    /**
     * Get preview of next occurrences for a notification.
     *
     * @param int $id Notification ID
     * @param int $count Number of occurrences to preview
     * @return array ['success' => bool, 'dates' => array, 'dates_display' => array, 'error' => string|null]
     */
    public function getPreview(int $id, int $count = 4): array
    {
        $notification = $this->getById($id);
        if (!$notification) {
            return ['success' => false, 'dates' => [], 'dates_display' => [], 'error' => 'Notification not found'];
        }

        return $this->calculatePreview($notification, $count);
    }

    /**
     * Calculate preview from notification data (can be used before saving).
     *
     * @param array $data Notification data
     * @param int $count Number of occurrences
     * @return array ['success' => bool, 'dates' => array, 'dates_display' => array, 'error' => string|null]
     */
    public function calculatePreview(array $data, int $count = 4): array
    {
        $timezone = $data['timezone'] ?? $this->displayTimezone;
        $tz = new DateTimeZone($timezone);
        $utcTz = new DateTimeZone('UTC');

        $dates = [];
        $datesDisplay = [];

        try {
            if (($data['schedule_type'] ?? '') === 'dates') {
                // For specific dates, just return them sorted
                $rawDates = $data['dates'] ?? [];
                $now = new DateTime('now', $utcTz);

                foreach ($rawDates as $dateRow) {
                    $scheduledAt = $dateRow['scheduled_at'] ?? $dateRow;
                    $dtLocal = new DateTime($scheduledAt, $tz);
                    $dtUtc = clone $dtLocal;
                    $dtUtc->setTimezone($utcTz);
                    if ($dtUtc > $now) {
                        $dates[] = $dtUtc->format('Y-m-d\TH:i:s\Z');
                        $localDt = clone $dtUtc;
                        $localDt->setTimezone($tz);
                        $datesDisplay[] = $localDt->format('D, M j, Y H:i');
                    }
                }

                // Sort and limit
                \array_multisort($dates, SORT_ASC, $datesDisplay);
                $dates = \array_slice($dates, 0, $count);
                $datesDisplay = \array_slice($datesDisplay, 0, $count);
            } else {
                // For recurrence, use the calculator
                $recurrence = $data['recurrence'] ?? [];
                if (empty($recurrence)) {
                    return ['success' => false, 'dates' => [], 'dates_display' => [], 'error' => 'No recurrence rule'];
                }

                $occurrences = $this->recurrenceCalc->getNextOccurrences($recurrence, $timezone, $count);

                foreach ($occurrences as $utcDatetime) {
                    $dt = new DateTime($utcDatetime, $utcTz);
                    $dates[] = $dt->format('Y-m-d\TH:i:s\Z');

                    $localDt = clone $dt;
                    $localDt->setTimezone($tz);
                    $datesDisplay[] = $localDt->format('D, M j, Y H:i');
                }
            }

            return [
                'success' => true,
                'dates' => $dates,
                'dates_display' => $datesDisplay,
                'error' => null,
            ];
        } catch (\Exception $e) {
            return [
                'success' => false,
                'dates' => [],
                'dates_display' => [],
                'error' => 'Preview error: ' . $e->getMessage(),
            ];
        }
    }

    // ==================== PRIVATE HELPERS ====================

    /**
     * Validate notification data.
     */
    private function validate(array $data, ?int $excludeId = null): array
    {
        // Required fields
        if (empty($data['title'])) {
            return ['valid' => false, 'error' => 'Title is required'];
        }

        if (empty($data['message'])) {
            return ['valid' => false, 'error' => 'Message is required'];
        }

        if (\strlen($data['message']) > self::MAX_MESSAGE_LENGTH) {
            return ['valid' => false, 'error' => 'Message exceeds ' . self::MAX_MESSAGE_LENGTH . ' characters'];
        }

        if (empty($data['schedule_type']) || !\in_array($data['schedule_type'], self::VALID_SCHEDULE_TYPES)) {
            return ['valid' => false, 'error' => 'Invalid schedule_type. Must be: dates or recurrence'];
        }

        // Validate timezone
        $timezone = $data['timezone'] ?? $this->displayTimezone;
        if (!\in_array($timezone, \timezone_identifiers_list())) {
            return ['valid' => false, 'error' => 'Invalid timezone'];
        }

        // Validate category exists if provided
        if (!empty($data['category_id'])) {
            $stmt = $this->pdo->prepare('SELECT id FROM categories WHERE id = ?');
            $stmt->execute([$data['category_id']]);
            if (!$stmt->fetch()) {
                return ['valid' => false, 'error' => 'Category not found'];
            }
        }

        // Validate dates
        if ($data['schedule_type'] === 'dates') {
            if (empty($data['dates']) || !\is_array($data['dates'])) {
                return ['valid' => false, 'error' => 'At least one date is required'];
            }
            foreach ($data['dates'] as $date) {
                $dateStr = \is_array($date) ? ($date['scheduled_at'] ?? '') : $date;
                if (empty($dateStr) || !\strtotime($dateStr)) {
                    return ['valid' => false, 'error' => 'Invalid date format'];
                }
            }
        }

        // Validate recurrence
        if ($data['schedule_type'] === 'recurrence') {
            if (empty($data['recurrence']) || !\is_array($data['recurrence'])) {
                return ['valid' => false, 'error' => 'Recurrence rule is required'];
            }
            try {
                $this->recurrenceCalc->validateRecurrence($data['recurrence']);
            } catch (\InvalidArgumentException $e) {
                return ['valid' => false, 'error' => 'Recurrence error: ' . $e->getMessage()];
            }
        }

        // Validate subsequents
        if (!empty($data['subsequents'])) {
            foreach ($data['subsequents'] as $i => $sub) {
                if (empty($sub['message'])) {
                    return ['valid' => false, 'error' => "Subsequent #" . ($i + 1) . " message is required"];
                }
                if (\strlen($sub['message']) > self::MAX_MESSAGE_LENGTH) {
                    return ['valid' => false, 'error' => "Subsequent #" . ($i + 1) . " message exceeds limit"];
                }

                $offsetDaysRaw = $sub['offset_days'] ?? 0;
                $offsetHoursRaw = $sub['offset_hours'] ?? 0;
                $offsetMinutesRaw = $sub['offset_minutes'] ?? 0;

                if (\filter_var($offsetDaysRaw, FILTER_VALIDATE_INT) === false || (int) $offsetDaysRaw < 0) {
                    return ['valid' => false, 'error' => "Subsequent #" . ($i + 1) . " offset_days must be >= 0"];
                }
                if (\filter_var($offsetHoursRaw, FILTER_VALIDATE_INT) === false || (int) $offsetHoursRaw < 0) {
                    return ['valid' => false, 'error' => "Subsequent #" . ($i + 1) . " offset_hours must be >= 0"];
                }
                if (\filter_var($offsetMinutesRaw, FILTER_VALIDATE_INT) === false || (int) $offsetMinutesRaw < 0) {
                    return ['valid' => false, 'error' => "Subsequent #" . ($i + 1) . " offset_minutes must be >= 0"];
                }

                $offsetDays = (int) $offsetDaysRaw;
                $offsetHours = (int) $offsetHoursRaw;
                $offsetMinutes = (int) $offsetMinutesRaw;

                $totalMinutes = $offsetDays * 24 * 60
                              + $offsetHours * 60
                              + $offsetMinutes;
                if ($totalMinutes <= 0) {
                    return ['valid' => false, 'error' => "Subsequent #" . ($i + 1) . " must have positive offset"];
                }
                if ($offsetDays > 30) {
                    return ['valid' => false, 'error' => "Subsequent #" . ($i + 1) . " offset_days max is 30"];
                }

                if (array_key_exists('image_path', $sub) && $sub['image_path'] !== null && !\is_string($sub['image_path'])) {
                    return ['valid' => false, 'error' => "Subsequent #" . ($i + 1) . " image_path must be a string"];
                }
            }
        }

        return ['valid' => true, 'error' => null];
    }

    /**
     * Insert specific dates (with deduplication).
     */
    private function insertDates(int $notificationId, array $dates, string $timezone): void
    {
        $stmt = $this->pdo->prepare('
            INSERT INTO notification_dates (notification_id, scheduled_at)
            VALUES (?, ?)
        ');

        $tz = new DateTimeZone($timezone);
        $utcTz = new DateTimeZone('UTC');
        $seen = []; // Track unique dates

        foreach ($dates as $date) {
            $dateStr = \is_array($date) ? ($date['scheduled_at'] ?? $date) : $date;
            // Interpret dates in the notification timezone, then store as UTC
            $dt = new DateTime($dateStr, $tz);
            $dt->setTimezone($utcTz);
            $formatted = $dt->format('Y-m-d H:i:s');

            // Skip duplicates
            if (isset($seen[$formatted])) {
                continue;
            }
            $seen[$formatted] = true;

            $stmt->execute([$notificationId, $formatted]);
        }
    }

    /**
     * Insert recurrence rule and calculate next_trigger_at.
     */
    private function insertRecurrence(int $notificationId, array $recurrence, string $timezone): void
    {
        // Calculate next trigger
        $nextTrigger = $this->recurrenceCalc->calculateNext($recurrence, $timezone);

        $stmt = $this->pdo->prepare('
            INSERT INTO notification_recurrence
            (notification_id, type, months, day_of_month, week_number, weekday, weekdays,
             interval_days, anchor_date, time_of_day, end_date, weekend_adjust, next_trigger_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        ');

        $stmt->execute([
            $notificationId,
            $recurrence['type'],
            isset($recurrence['months']) ? (\is_array($recurrence['months']) ? \json_encode($recurrence['months']) : $recurrence['months']) : null,
            $recurrence['day_of_month'] ?? null,
            $recurrence['week_number'] ?? null,
            $recurrence['weekday'] ?? null,
            isset($recurrence['weekdays']) ? (\is_array($recurrence['weekdays']) ? \json_encode($recurrence['weekdays']) : $recurrence['weekdays']) : null,
            $recurrence['interval_days'] ?? null,
            $recurrence['anchor_date'] ?? null,
            $recurrence['time_of_day'],
            $recurrence['end_date'] ?? null,
            $recurrence['weekend_adjust'] ?? null,
            $nextTrigger,
        ]);
    }

    /**
     * Insert subsequent definitions.
     */
    private function insertSubsequents(int $notificationId, array $subsequents): void
    {
        $stmt = $this->pdo->prepare('
            INSERT INTO notification_subsequents
            (notification_id, offset_minutes, offset_hours, offset_days, message, sort_order, image_path)
            VALUES (?, ?, ?, ?, ?, ?, ?)
        ');

        foreach ($subsequents as $i => $sub) {
            $imagePath = isset($sub['image_path']) ? \trim((string) $sub['image_path']) : null;
            if ($imagePath === '') {
                $imagePath = null;
            }
            $stmt->execute([
                $notificationId,
                $sub['offset_minutes'] ?? 0,
                $sub['offset_hours'] ?? 0,
                $sub['offset_days'] ?? 0,
                $sub['message'],
                $sub['sort_order'] ?? $i,
                $imagePath,
            ]);
        }
    }

    /**
     * Cancel pending jobs for a notification.
     */
    private function cancelPendingJobs(int $notificationId, string $reason): void
    {
        $stmt = $this->pdo->prepare('
            UPDATE send_jobs
            SET status = ?, cancelled_reason = ?
            WHERE notification_id = ?
            AND status IN (?, ?)
        ');
        $stmt->execute(['cancelled', $reason, $notificationId, 'pending', 'retrying']);
    }

    /**
     * Update next_trigger_at for recurrence.
     */
    private function updateNextTriggerAt(int $notificationId, array $recurrence, string $timezone): void
    {
        $nextTrigger = $this->recurrenceCalc->calculateNext($recurrence, $timezone);

        $stmt = $this->pdo->prepare('
            UPDATE notification_recurrence
            SET next_trigger_at = ?
            WHERE notification_id = ?
        ');
        $stmt->execute([$nextTrigger, $notificationId]);
    }

    /**
     * Get next scheduled date for date-based notifications.
     */
    private function getNextDateTrigger(int $notificationId): ?string
    {
        $stmt = $this->pdo->prepare('
            SELECT MIN(scheduled_at) as next_date
            FROM notification_dates
            WHERE notification_id = ?
            AND scheduled_at > datetime(\'now\')
        ');
        $stmt->execute([$notificationId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row['next_date'] ?? null;
    }
}
