<?php

/**
 * JobService - Manages send_jobs for the scheduler
 *
 * Handles:
 * - Creating jobs for due dates/recurrences
 * - Claiming jobs (atomic lock)
 * - Updating job status
 * - Creating subsequent jobs after successful sends
 * - Cancelling jobs
 */

declare(strict_types=1);

namespace Notifier\Services;

use PDO;
use DateTime;
use DateTimeZone;

class JobService
{
    private PDO $pdo;

    private const BACKOFF_MINUTES = [
        1 => 1,
        2 => 5,
        3 => 15,
        4 => 60,
        5 => 240,
    ];
    private const DEFAULT_BACKOFF = 480; // 8 hours
    private const MAX_ATTEMPTS = 20;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    /**
     * Get due jobs ready to be processed.
     *
     * @param int $limit Max jobs to return
     * @return array List of jobs with notification data
     */
    public function getDueJobs(int $limit = 50): array
    {
        $stmt = $this->pdo->prepare('
            SELECT j.*, n.title, n.message, n.image_path, n.timezone,
                   ns.message as subsequent_message, ns.image_path as subsequent_image_path
            FROM send_jobs j
            JOIN notifications n ON j.notification_id = n.id
            LEFT JOIN notification_subsequents ns ON j.source_id = ns.id AND j.job_type = ?
            WHERE j.status IN (?, ?)
            AND j.next_attempt_at <= datetime(\'now\')
            AND n.status = ?
            ORDER BY j.next_attempt_at ASC
            LIMIT ?
        ');
        $stmt->execute(['subsequent', 'pending', 'retrying', 'active', $limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * Claim a job for processing (atomic).
     *
     * @param int $jobId Job ID
     * @return bool True if claimed successfully
     */
    public function claim(int $jobId): bool
    {
        $stmt = $this->pdo->prepare('
            UPDATE send_jobs
            SET status = ?, attempt_count = attempt_count + 1
            WHERE id = ? AND status IN (?, ?)
        ');
        $stmt->execute(['sending', $jobId, 'pending', 'retrying']);
        return $stmt->rowCount() > 0;
    }

    /**
     * Mark job as successfully sent.
     *
     * @param int $jobId Job ID
     * @param int $telegramMessageId Telegram message ID
     * @param int $historyId History record ID
     * @return bool Success
     */
    public function markSent(int $jobId, int $telegramMessageId, int $historyId): bool
    {
        $stmt = $this->pdo->prepare('
            UPDATE send_jobs
            SET status = ?, sent_at = datetime(\'now\'),
                telegram_message_id = ?, history_id = ?
            WHERE id = ?
        ');
        $stmt->execute(['sent', $telegramMessageId, $historyId, $jobId]);
        return $stmt->rowCount() > 0;
    }

    /**
     * Mark job as failed, schedule retry with backoff.
     *
     * @param int $jobId Job ID
     * @param string $error Error message
     * @param int $attemptCount Current attempt count
     * @return array ['status' => string, 'next_attempt' => string|null, 'give_up' => bool]
     */
    public function markFailed(int $jobId, string $error, int $attemptCount): array
    {
        $giveUp = $attemptCount >= self::MAX_ATTEMPTS;

        if ($giveUp) {
            // Give up permanently
            $stmt = $this->pdo->prepare('
                UPDATE send_jobs
                SET status = ?, last_error = ?
                WHERE id = ?
            ');
            $stmt->execute(['failed', $error, $jobId]);

            return [
                'status' => 'failed',
                'next_attempt' => null,
                'give_up' => true,
            ];
        }

        // Calculate backoff
        $backoffMinutes = self::BACKOFF_MINUTES[$attemptCount] ?? self::DEFAULT_BACKOFF;

        $stmt = $this->pdo->prepare("
            UPDATE send_jobs
            SET status = ?, last_error = ?,
                next_attempt_at = datetime('now', '+' || ? || ' minutes')
            WHERE id = ?
        ");
        $stmt->execute(['retrying', $error, $backoffMinutes, $jobId]);

        $nextAttempt = (new DateTime())->modify("+{$backoffMinutes} minutes")->format('Y-m-d H:i:s');

        return [
            'status' => 'retrying',
            'next_attempt' => $nextAttempt,
            'give_up' => false,
        ];
    }

    /**
     * Cancel a job.
     *
     * @param int $jobId Job ID
     * @param string $reason Cancellation reason
     * @return bool Success
     */
    public function cancel(int $jobId, string $reason): bool
    {
        $stmt = $this->pdo->prepare('
            UPDATE send_jobs
            SET status = ?, cancelled_reason = ?
            WHERE id = ? AND status IN (?, ?)
        ');
        $stmt->execute(['cancelled', $reason, $jobId, 'pending', 'retrying']);
        return $stmt->rowCount() > 0;
    }

    /**
     * Retry a job immediately (manual retry).
     *
     * @param int $jobId Job ID
     * @return array ['success' => bool, 'error' => string|null]
     */
    public function retryNow(int $jobId): array
    {
        $job = $this->getById($jobId);
        if (!$job) {
            return ['success' => false, 'error' => 'Job not found'];
        }

        $status = $job['status'] ?? '';
        if ($status === 'sending') {
            return ['success' => false, 'error' => 'Job is currently sending'];
        }
        if (\in_array($status, ['sent', 'cancelled'], true)) {
            return ['success' => false, 'error' => 'Job cannot be retried'];
        }

        try {
            if ($status === 'failed') {
                $stmt = $this->pdo->prepare("
                    UPDATE send_jobs
                    SET status = 'pending',
                        next_attempt_at = datetime('now'),
                        last_error = NULL,
                        attempt_count = 0
                    WHERE id = ?
                ");
                $stmt->execute([$jobId]);
            } else {
                $stmt = $this->pdo->prepare("
                    UPDATE send_jobs
                    SET status = 'pending',
                        next_attempt_at = datetime('now')
                    WHERE id = ?
                ");
                $stmt->execute([$jobId]);
            }

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

    /**
     * Cancel all pending subsequent jobs for a history record.
     *
     * @param int $historyId History record ID
     * @param string $reason Cancellation reason
     * @return int Number of jobs cancelled
     */
    public function cancelSubsequentsByHistory(int $historyId, string $reason): int
    {
        $stmt = $this->pdo->prepare('
            UPDATE send_jobs
            SET status = ?, cancelled_reason = ?
            WHERE source_history_id = ?
            AND job_type = ?
            AND status IN (?, ?)
        ');
        $stmt->execute(['cancelled', $reason, $historyId, 'subsequent', 'pending', 'retrying']);
        return $stmt->rowCount();
    }

    /**
     * Create a job for a specific date notification.
     *
     * @param int $notificationId Notification ID
     * @param int $dateId notification_dates.id
     * @param string $scheduledFor UTC datetime
     * @return int|null Job ID or null if already exists
     */
    public function createDateJob(int $notificationId, int $dateId, string $scheduledFor): ?int
    {
        try {
            $stmt = $this->pdo->prepare('
                INSERT INTO send_jobs (notification_id, job_type, source_id, scheduled_for, next_attempt_at)
                VALUES (?, ?, ?, ?, ?)
            ');
            $stmt->execute([$notificationId, 'main_date', $dateId, $scheduledFor, $scheduledFor]);
            return (int) $this->pdo->lastInsertId();
        } catch (\PDOException $e) {
            // Unique constraint violation - job already exists
            if (\strpos($e->getMessage(), 'UNIQUE constraint failed') !== false) {
                return null;
            }
            throw $e;
        }
    }

    /**
     * Create a job for a recurring notification.
     *
     * @param int $notificationId Notification ID
     * @param string $scheduledFor UTC datetime
     * @return int|null Job ID or null if already exists
     */
    public function createRecurrenceJob(int $notificationId, string $scheduledFor): ?int
    {
        try {
            $stmt = $this->pdo->prepare('
                INSERT INTO send_jobs (notification_id, job_type, scheduled_for, next_attempt_at)
                VALUES (?, ?, ?, ?)
            ');
            $stmt->execute([$notificationId, 'main_recurrence', $scheduledFor, $scheduledFor]);
            return (int) $this->pdo->lastInsertId();
        } catch (\PDOException $e) {
            if (\strpos($e->getMessage(), 'UNIQUE constraint failed') !== false) {
                return null;
            }
            throw $e;
        }
    }

    /**
     * Create subsequent (follow-up) jobs after a successful main send.
     *
     * @param int $notificationId Notification ID
     * @param int $historyId History record that triggered these
     * @param string $baseDatetime UTC datetime of the main send
     * @return int Number of jobs created
     */
    public function createSubsequentJobs(int $notificationId, int $historyId, string $baseDatetime): int
    {
        // Get subsequent definitions
        $stmt = $this->pdo->prepare('
            SELECT id, offset_minutes, offset_hours, offset_days
            FROM notification_subsequents
            WHERE notification_id = ?
            ORDER BY sort_order ASC
        ');
        $stmt->execute([$notificationId]);
        $subsequents = $stmt->fetchAll(PDO::FETCH_ASSOC);

        if (empty($subsequents)) {
            return 0;
        }

        $utcTz = new DateTimeZone('UTC');
        $baseDate = new DateTime($baseDatetime, $utcTz);
        $created = 0;

        $insertSql = '
            INSERT INTO send_jobs
            (notification_id, job_type, source_id, source_history_id, scheduled_for, next_attempt_at)
            VALUES (?, ?, ?, ?, ?, ?)
        ';

        foreach ($subsequents as $sub) {
            $offset = ($sub['offset_days'] * 24 * 60) + ($sub['offset_hours'] * 60) + $sub['offset_minutes'];
            $scheduledFor = clone $baseDate;
            $scheduledFor->modify("+{$offset} minutes");
            $scheduledForStr = $scheduledFor->format('Y-m-d H:i:s');

            try {
                $insertStmt = $this->pdo->prepare($insertSql);
                $insertStmt->execute([
                    $notificationId,
                    'subsequent',
                    $sub['id'],
                    $historyId,
                    $scheduledForStr,
                    $scheduledForStr,
                ]);
                $created++;
            } catch (\PDOException $e) {
                // Skip duplicates
                if (\strpos($e->getMessage(), 'UNIQUE constraint failed') === false) {
                    throw $e;
                }
            }
        }

        return $created;
    }

    /**
     * Create jobs for all due dates (scheduler calls this).
     *
     * @return int Number of jobs created
     */
    public function createJobsForDueDates(): int
    {
        $stmt = $this->pdo->query('
            SELECT d.id, d.notification_id, d.scheduled_at
            FROM notification_dates d
            JOIN notifications n ON d.notification_id = n.id
            LEFT JOIN send_jobs j ON j.job_type = \'main_date\'
                AND j.notification_id = d.notification_id
                AND j.source_id = d.id
            WHERE d.scheduled_at <= datetime(\'now\')
            AND n.status = \'active\'
            AND n.schedule_type = \'dates\'
            AND j.id IS NULL
        ');
        $dates = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $created = 0;
        foreach ($dates as $date) {
            $jobId = $this->createDateJob($date['notification_id'], $date['id'], $date['scheduled_at']);
            if ($jobId !== null) {
                $created++;
            }
        }

        return $created;
    }

    /**
     * Create jobs for all due recurrences (scheduler calls this).
     *
     * @return int Number of jobs created
     */
    public function createJobsForDueRecurrences(): int
    {
        $stmt = $this->pdo->query('
            SELECT r.notification_id, r.next_trigger_at
            FROM notification_recurrence r
            JOIN notifications n ON r.notification_id = n.id
            WHERE r.next_trigger_at IS NOT NULL
            AND r.next_trigger_at <= datetime(\'now\')
            AND n.status = \'active\'
            AND n.schedule_type = \'recurrence\'
        ');
        $recurrences = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $created = 0;
        foreach ($recurrences as $rec) {
            $jobId = $this->createRecurrenceJob($rec['notification_id'], $rec['next_trigger_at']);
            if ($jobId !== null) {
                $created++;
            }
        }

        return $created;
    }

    /**
     * Reset stuck jobs (sending status for too long).
     *
     * @param int $stuckMinutes Minutes after which a job is considered stuck
     * @return int Number of jobs reset
     */
    public function resetStuckJobs(int $stuckMinutes = 10): int
    {
        $stmt = $this->pdo->prepare("
            UPDATE send_jobs
            SET status = 'retrying',
                next_attempt_at = datetime('now', '+1 minute'),
                last_error = 'Reset: stuck in sending state'
            WHERE status = 'sending'
            AND updated_at < datetime('now', '-' || ? || ' minutes')
        ");
        $stmt->execute([$stuckMinutes]);
        return $stmt->rowCount();
    }

    /**
     * Get job statistics.
     *
     * @return array Statistics by status
     */
    public function getStats(): array
    {
        $stmt = $this->pdo->query('
            SELECT status, COUNT(*) as count
            FROM send_jobs
            GROUP BY status
        ');

        $stats = [];
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $stats[$row['status']] = (int) $row['count'];
        }

        return $stats;
    }

    /**
     * Get a job by ID.
     *
     * @param int $jobId Job ID
     * @return array|null Job data
     */
    public function getById(int $jobId): ?array
    {
        $stmt = $this->pdo->prepare('SELECT * FROM send_jobs WHERE id = ?');
        $stmt->execute([$jobId]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        return $result ?: null;
    }
}
