<?php

declare(strict_types=1);

final class BRBroadcastJob extends Job
{
    /*
     * تعداد پیام در هر Batch
     */
    private const BATCH_SIZE = 25;

    /*
     * فاصله بین پیام‌ها
     *
     * 40ms ≈ 25 پیام در ثانیه
     */
    private const DELAY_US = 40000;

    /*
     * حداکثر تلاش برای هر پیام
     */
    private const MAX_ATTEMPTS = 3;

    public function getName(): string
    {
        return 'BRBroadcastJob';
    }

    public function handle(): void
    {
        $startedAt =
            microtime(true);

        $totalProcessed = 0;
        $totalSent = 0;
        $totalFailed = 0;
        $totalRateLimited = 0;

        /*
         * ==========================================
         * تا زمانی که Queue وجود دارد ادامه بده
         * ==========================================
         */
        while (true) {

            $result =
                $this->processBatch(
                    self::BATCH_SIZE
                );

            $totalProcessed +=
                $result['processed'];

            $totalSent +=
                $result['sent'];

            $totalFailed +=
                $result['failed'];

            $totalRateLimited +=
                $result['rate_limited'];

            /*
             * اگر Rate Limit خوردیم،
             * همین اجرای Cron را متوقف کن.
             *
             * next_attempt_at قبلاً تنظیم شده.
             */
            if (
                $result['rate_limited'] > 0
            ) {
                break;
            }

            /*
             * اگر دیگر Queue آماده‌ای نداریم،
             * کار تمام است.
             */
            if (
                $result['processed'] === 0
            ) {
                break;
            }

            /*
             * اگر این Batch کمتر از ظرفیت بود،
             * احتمالاً Queue تمام شده.
             */
            if (
                $result['processed']
                <
                self::BATCH_SIZE
            ) {
                break;
            }
        }

        /*
         * ==========================================
         * تکمیل Broadcastهای تمام‌شده
         * ==========================================
         */
        $this->completeFinishedBroadcasts();

        /*
         * ==========================================
         * Log
         * ==========================================
         */
        $duration =
            round(
                microtime(true) -
                $startedAt,
                3
            );

        error_log(
            '[BR BROADCAST JOB] '
            . json_encode(
                [
                    'processed' =>
                        $totalProcessed,

                    'sent' =>
                        $totalSent,

                    'failed' =>
                        $totalFailed,

                    'rate_limited' =>
                        $totalRateLimited,

                    'remaining' =>
                        $this->getRemainingCount(),

                    'duration_seconds' =>
                        $duration,
                ],
                JSON_UNESCAPED_UNICODE
            )
        );
    }

    /**
     * پردازش یک Batch
     */
    private function processBatch(
        int $batchSize
    ): array {

        /*
         * ==========================================
         * فعال کردن Broadcastهای queued
         * ==========================================
         */
        $st =
            $this->pdo->prepare("
                UPDATE tournament_br_broadcasts

                SET
                    status = 'processing',

                    started_at = COALESCE(
                        started_at,
                        NOW()
                    )

                WHERE status = 'queued'

                  AND total_users >
                    (
                        sent_users +
                        failed_users
                    )
            ");

        $st->execute();

        /*
         * ==========================================
         * دریافت Queue
         * ==========================================
         */
        $st =
            $this->pdo->prepare("
                SELECT
                    q.id,
                    q.broadcast_id,
                    q.user_id,
                    q.message_text,
                    q.attempts

                FROM tournament_br_broadcast_queue q

                INNER JOIN tournament_br_broadcasts b
                    ON b.id = q.broadcast_id

                WHERE q.status = 'queued'

                  AND q.next_attempt_at <= NOW()

                  AND b.status IN (
                      'queued',
                      'processing'
                  )

                ORDER BY
                    q.id ASC

                LIMIT {$batchSize}
            ");

        $st->execute();

        $rows =
            $st->fetchAll(
                PDO::FETCH_ASSOC
            );

        /*
         * هیچ Queue آماده‌ای نداریم.
         */
        if (!$rows) {

            $this->completeFinishedBroadcasts();

            return [
                'processed' =>
                    0,

                'sent' =>
                    0,

                'failed' =>
                    0,

                'rate_limited' =>
                    0,

                'remaining' =>
                    $this->getRemainingCount(),
            ];
        }

        $processed = 0;
        $sent = 0;
        $failed = 0;
        $rateLimited = 0;

        foreach (
            $rows as $row
        ) {

            $queueId =
                (int) $row['id'];

            $broadcastId =
                (int) $row['broadcast_id'];

            $userId =
                (int) $row['user_id'];

            /*
             * ======================================
             * Claim
             * ======================================
             */
            $claim =
                $this->pdo->prepare("
                    UPDATE tournament_br_broadcast_queue

                    SET
                        status = 'processing',
                        attempts = attempts + 1

                    WHERE id = ?

                      AND status = 'queued'
                ");

            $claim->execute([
                $queueId
            ]);

            if (
                $claim->rowCount() !== 1
            ) {
                continue;
            }

            $processed++;

            try {

                /*
                 * ==================================
                 * ارسال Telegram
                 * ==================================
                 */
                $result =
                    $this->sendMessage(
                        $userId,
                        $row['message_text']
                    );

                /*
                 * برای اطمینان از اینکه
                 * Rate Limit بین پیام‌ها کمتر شود.
                 */
                if (
                    is_array($result)
                    && ($result['ok'] ?? false) === true
                ) {

                    /*
                     * ==================================
                     * موفق
                     * ==================================
                     */
                    $messageId = null;

                    if (
                        isset(
                            $result[
                                'result'
                            ]['message_id']
                        )
                    ) {
                        $messageId =
                            (int) $result[
                                'result'
                            ]['message_id'];
                    }

                    $update =
                        $this->pdo->prepare("
                            UPDATE tournament_br_broadcast_queue

                            SET
                                status = 'sent',
                                telegram_message_id = ?,
                                sent_at = NOW(),
                                last_error = NULL

                            WHERE id = ?
                        ");

                    $update->execute([
                        $messageId,
                        $queueId
                    ]);

                    $sent++;

                    $this->updateBroadcastCounters(
                        $broadcastId
                    );

                    /*
                     * حدود 25 پیام در ثانیه
                     */
                    usleep(
                        self::DELAY_US
                    );

                    continue;
                }

                /*
                 * ==================================
                 * Telegram Error
                 * ==================================
                 */
                $errorCode =
                    is_array($result)
                        ? (
                            (int) (
                                $result[
                                    'error_code'
                                ]
                                ?? 0
                            )
                        )
                        : 0;



                if ($errorCode === 403) {

                    $errorText =
                        is_array($result)
                            ? json_encode(
                                $result,
                                JSON_UNESCAPED_UNICODE
                            )
                            : '403 Forbidden';

                    $update =
                        $this->pdo->prepare("
                            UPDATE tournament_br_broadcast_queue
                            SET
                                status = 'failed',
                                last_error = ?
                            WHERE id = ?
                        ");

                    $update->execute([
                        $errorText,
                        $queueId
                    ]);

                    $failed++;

                    $this->updateBroadcastCounters(
                        $broadcastId
                    );

                    continue;
                }

                /*
                 * ==================================
                 * 429
                 * ==================================
                 */
                if (
                    $errorCode === 429
                ) {

                    $retryAfter =
                        (int) (
                            $result[
                                'parameters'
                            ]['retry_after']
                            ?? 30
                        );

                    $retryAfter =
                        max(
                            1,
                            $retryAfter
                        );

                    $nextAttemptAt =
                        date(
                            'Y-m-d H:i:s',
                            time()
                            + $retryAfter
                        );

                    $errorText =
                        is_array($result)
                            ? json_encode(
                                $result,
                                JSON_UNESCAPED_UNICODE
                            )
                            : '429 Too Many Requests';

                    /*
                     * برگرداندن به Queue
                     */
                    $update =
                        $this->pdo->prepare("
                            UPDATE tournament_br_broadcast_queue

                            SET
                                status = 'queued',
                                next_attempt_at = ?,
                                last_error = ?

                            WHERE id = ?
                        ");

                    $update->execute([
                        $nextAttemptAt,
                        $errorText,
                        $queueId
                    ]);

                    $rateLimited++;

                    /*
                     * مهم:
                     * در همین اجرای Job ادامه نمی‌دهیم.
                     */
                    break;
                }

                /*
                 * ==================================
                 * خطای معمولی
                 * ==================================
                 */
                $attempts =
                    (int) $row['attempts'] + 1;

                $errorText =
                    is_array($result)
                        ? json_encode(
                            $result,
                            JSON_UNESCAPED_UNICODE
                        )
                        : 'Telegram API failed.';

                /*
                 * ==================================
                 * Retry
                 * ==================================
                 */
                if (
                    $attempts <
                    self::MAX_ATTEMPTS
                ) {

                    $retryDelay =
                        30 *
                        $attempts;

                    $nextAttemptAt =
                        date(
                            'Y-m-d H:i:s',
                            time()
                            + $retryDelay
                        );

                    $update =
                        $this->pdo->prepare("
                            UPDATE tournament_br_broadcast_queue

                            SET
                                status = 'queued',
                                next_attempt_at = ?,
                                last_error = ?

                            WHERE id = ?
                        ");

                    $update->execute([
                        $nextAttemptAt,
                        $errorText,
                        $queueId
                    ]);

                } else {

                    /*
                     * شکست نهایی
                     */
                    $update =
                        $this->pdo->prepare("
                            UPDATE tournament_br_broadcast_queue

                            SET
                                status = 'failed',
                                last_error = ?

                            WHERE id = ?
                        ");

                    $update->execute([
                        $errorText,
                        $queueId
                    ]);

                    $failed++;

                    $this->updateBroadcastCounters(
                        $broadcastId
                    );
                }

            } catch (Throwable $e) {

                $attempts =
                    (int) $row['attempts'] + 1;

                /*
                 * ==================================
                 * Retry
                 * ==================================
                 */
                if (
                    $attempts <
                    self::MAX_ATTEMPTS
                ) {

                    $retryDelay =
                        30 *
                        $attempts;

                    $nextAttemptAt =
                        date(
                            'Y-m-d H:i:s',
                            time()
                            + $retryDelay
                        );

                    $update =
                        $this->pdo->prepare("
                            UPDATE tournament_br_broadcast_queue

                            SET
                                status = 'queued',
                                next_attempt_at = ?,
                                last_error = ?

                            WHERE id = ?
                        ");

                    $update->execute([
                        $nextAttemptAt,
                        $e->getMessage(),
                        $queueId
                    ]);

                } else {

                    /*
                     * شکست نهایی
                     */
                    $update =
                        $this->pdo->prepare("
                            UPDATE tournament_br_broadcast_queue

                            SET
                                status = 'failed',
                                last_error = ?

                            WHERE id = ?
                        ");

                    $update->execute([
                        $e->getMessage(),
                        $queueId
                    ]);

                    $failed++;

                    $this->updateBroadcastCounters(
                        $broadcastId
                    );
                }

                error_log(
                    '[BR BROADCAST JOB ERROR] '
                    . 'queue='
                    . $queueId
                    . ' user='
                    . $userId
                    . ' error='
                    . $e->getMessage()
                );
            }
        }

        /*
         * ==========================================
         * تکمیل احتمالی Broadcast
         * ==========================================
         */
        $this->completeFinishedBroadcasts();

        return [
            'processed' =>
                $processed,

            'sent' =>
                $sent,

            'failed' =>
                $failed,

            'rate_limited' =>
                $rateLimited,

            'remaining' =>
                $this->getRemainingCount(),
        ];
    }

    /*
     * ==========================================================
     * Telegram Send
     * ==========================================================
     */
    private function sendMessage(
        int|string $chat_id,
        string|array $data,
        ?array $reply_markup = null,
        ?PDO $pdo = null
    ): array {

        /*
         * ==========================================
         * JSON Keyboard
         * ==========================================
         */
        $encodeKeyboard =
            static function ($markup): string {

                return json_encode(
                    $markup,
                    JSON_UNESCAPED_UNICODE |
                    JSON_UNESCAPED_SLASHES
                );
            };

        /*
         * ==========================================
         * TEXT STRING
         * ==========================================
         */
        if (
            is_string($data)
        ) {

            $params = [
                'chat_id' =>
                    $chat_id,

                'text' =>
                    $data,

                'parse_mode' =>
                    'HTML',

                'disable_web_page_preview' =>
                    true
            ];

            if (
                $reply_markup !== null
            ) {

                $params['reply_markup'] =
                    $encodeKeyboard(
                        $reply_markup
                    );
            }

            return $this->apiRequest(
                'sendMessage',
                $params
            );
        }

        /*
         * ==========================================
         * ARRAY
         * ==========================================
         */
        if (
            is_array($data)
        ) {

            $mediaTypes = [
                'photo',
                'video',
                'animation',
                'document',
                'audio'
            ];

            $detectedType = null;
            $mediaValue = null;

            foreach (
                $mediaTypes as $type
            ) {

                if (
                    !empty(
                        $data[$type]
                    )
                ) {

                    $detectedType =
                        $type;

                    $mediaValue =
                        $data[$type];

                    break;
                }
            }

            /*
             * Media
             */
            if (
                $detectedType !== null
            ) {

                $map = [
                    'photo' =>
                        'sendPhoto',

                    'video' =>
                        'sendVideo',

                    'animation' =>
                        'sendAnimation',

                    'document' =>
                        'sendDocument',

                    'audio' =>
                        'sendAudio',
                ];

                $method =
                    $map[
                        $detectedType
                    ]
                    ?? 'sendMessage';

                $params = [
                    'chat_id' =>
                        $chat_id,

                    $detectedType =>
                        $mediaValue,

                    'parse_mode' =>
                        $data[
                            'parse_mode'
                        ]
                        ?? 'HTML'
                ];

                if (
                    isset(
                        $data['caption']
                    )
                ) {

                    $params['caption'] =
                        (string) $data[
                            'caption'
                        ];
                }

                if (
                    $reply_markup !== null
                ) {

                    $params['reply_markup'] =
                        $encodeKeyboard(
                            $reply_markup
                        );
                }

                return $this->apiRequest(
                    $method,
                    $params
                );
            }

            /*
             * Text داخل Array
             */
            if (
                isset(
                    $data['text']
                )
            ) {

                $params = [
                    'chat_id' =>
                        $chat_id,

                    'text' =>
                        $data['text'],

                    'parse_mode' =>
                        $data[
                            'parse_mode'
                        ]
                        ?? 'HTML',

                    'disable_web_page_preview' =>
                        true
                ];

                if (
                    $reply_markup !== null
                ) {

                    $params['reply_markup'] =
                        $encodeKeyboard(
                            $reply_markup
                        );
                }

                return $this->apiRequest(
                    'sendMessage',
                    $params
                );
            }
        }

        return [
            'ok' =>
                false,

            'error' =>
                'Invalid message format'
        ];
    }

    /*
     * ==========================================================
     * Telegram API
     * ==========================================================
     */
    private function apiRequest(
        string $method,
        array $params = []
    ): array {

        $botToken =
            $_ENV['BOT_TOKEN']
            ?? $_SERVER['BOT_TOKEN']
            ?? getenv('BOT_TOKEN')
            ?: null;

        if (
            !is_string($botToken)
            || $botToken === ''
        ) {

            return [
                'ok' =>
                    false,

                'error' =>
                    'BOT_TOKEN not found'
            ];
        }

        $apiUrl =
            'https://api.telegram.org/bot'
            . $botToken
            . '/';

        $ch =
            curl_init(
                $apiUrl . $method
            );

        if (
            $ch === false
        ) {

            return [
                'ok' =>
                    false,

                'error' =>
                    'curl_init failed'
            ];
        }

        curl_setopt(
            $ch,
            CURLOPT_RETURNTRANSFER,
            true
        );

        curl_setopt(
            $ch,
            CURLOPT_POST,
            true
        );

        curl_setopt(
            $ch,
            CURLOPT_POSTFIELDS,
            $params
        );

        curl_setopt(
            $ch,
            CURLOPT_TIMEOUT,
            20
        );

        $res =
            curl_exec($ch);

        if (
            $res === false
        ) {

            $error =
                curl_error($ch);

            curl_close($ch);

            return [
                'ok' =>
                    false,

                'error' =>
                    'curl_error',

                'description' =>
                    $error
            ];
        }

        curl_close($ch);

        $json =
            json_decode(
                $res,
                true
            );

        return is_array($json)
            ? $json
            : [
                'ok' =>
                    false,

                'raw' =>
                    $res
            ];
    }

    /*
     * ==========================================================
     * Broadcast Counters
     * ==========================================================
     */
    private function updateBroadcastCounters(
        int $broadcastId
    ): void {

        $st =
            $this->pdo->prepare("
                UPDATE tournament_br_broadcasts b

                LEFT JOIN (
                    SELECT
                        broadcast_id,

                        SUM(
                            status = 'sent'
                        ) AS sent_count,

                        SUM(
                            status = 'failed'
                        ) AS failed_count

                    FROM tournament_br_broadcast_queue

                    WHERE broadcast_id = ?

                    GROUP BY
                        broadcast_id
                ) q

                    ON q.broadcast_id =
                        b.id

                SET
                    b.sent_users =
                        COALESCE(
                            q.sent_count,
                            0
                        ),

                    b.failed_users =
                        COALESCE(
                            q.failed_count,
                            0
                        )

                WHERE b.id = ?
            ");

        $st->execute([
            $broadcastId,
            $broadcastId
        ]);
    }

    /*
     * ==========================================================
     * Complete Finished Broadcasts
     * ==========================================================
     */
    private function completeFinishedBroadcasts(): void
    {
        /*
        * ==========================================
        * Broadcastهای فعال
        * ==========================================
        */
        $st = $this->pdo->prepare("
            SELECT
                id,
                created_by,
                started_at,
                total_users
            FROM tournament_br_broadcasts
            WHERE status IN (
                'queued',
                'processing'
            )
            ORDER BY id ASC
        ");

        $st->execute();

        $broadcasts =
            $st->fetchAll(
                PDO::FETCH_ASSOC
            );

        foreach (
            $broadcasts as $broadcast
        ) {

            $broadcastId =
                (int) $broadcast['id'];

            /*
            * ======================================
            * بررسی Queue باقی‌مانده
            * ======================================
            */
            $check = $this->pdo->prepare("
                SELECT
                    COUNT(*)
                FROM tournament_br_broadcast_queue
                WHERE broadcast_id = ?
                AND status IN (
                    'queued',
                    'processing'
                )
            ");

            $check->execute([
                $broadcastId
            ]);

            $remaining =
                (int) $check->fetchColumn();

            if (
                $remaining > 0
            ) {
                continue;
            }

            /*
            * ======================================
            * بروزرسانی آمار
            * ======================================
            */
            $this->updateBroadcastCounters(
                $broadcastId
            );

            /*
            * ======================================
            * دریافت آمار نهایی
            * ======================================
            */
            $stats = $this->pdo->prepare("
                SELECT
                    total_users,
                    sent_users,
                    failed_users,
                    started_at,
                    created_by
                FROM tournament_br_broadcasts
                WHERE id = ?
                LIMIT 1
            ");

            $stats->execute([
                $broadcastId
            ]);

            $final =
                $stats->fetch(
                    PDO::FETCH_ASSOC
                );

            if (!$final) {
                continue;
            }

            /*
            * ======================================
            * Complete
            * ======================================
            */
            $update = $this->pdo->prepare("
                UPDATE tournament_br_broadcasts
                SET
                    status = 'completed',
                    completed_at = NOW()
                WHERE id = ?
                AND status IN (
                    'queued',
                    'processing'
                )
            ");

            $update->execute([
                $broadcastId
            ]);

            /*
            * اگر Complete نشده، ادامه نده.
            */
            if (
                $update->rowCount() !== 1
            ) {
                continue;
            }

            /*
            * ======================================
            * آمار نهایی
            * ======================================
            */
            $totalUsers =
                (int) (
                    $final['total_users']
                    ?? 0
                );

            $sentUsers =
                (int) (
                    $final['sent_users']
                    ?? 0
                );

            $failedUsers =
                (int) (
                    $final['failed_users']
                    ?? 0
                );

            $createdBy =
                (int) (
                    $final['created_by']
                    ?? 0
                );

            /*
            * ======================================
            * Duration
            * ======================================
            */
            $durationText = 'نامشخص';

            if (
                !empty($final['started_at'])
                && !empty($final['completed_at'])
            ) {

                $startedTimestamp =
                    strtotime(
                        $final['started_at']
                    );

                $completedTimestamp =
                    strtotime(
                        $final['completed_at']
                    );

                if (
                    $startedTimestamp !== false
                    && $completedTimestamp !== false
                ) {

                    $duration =
                        max(
                            0,
                            $completedTimestamp
                            - $startedTimestamp
                        );

                    $minutes =
                        intdiv(
                            $duration,
                            60
                        );

                    $seconds =
                        $duration % 60;

                    if ($minutes > 0) {

                        $durationText =
                            $minutes
                            . ' دقیقه'
                            . (
                                $seconds > 0
                                    ? ' و '
                                    . $seconds
                                    . ' ثانیه'
                                    : ''
                            );

                    } else {

                        $durationText =
                            $seconds
                            . ' ثانیه';
                    }
                }
            }

            /*
            * ======================================
            * Admin Notification
            * ======================================
            */
            if (
                $createdBy > 0
            ) {

                $message =
                    "✅ <b>ارسال زمان‌بندی تکمیل شد.</b>\n\n"

                    . "🆔 Broadcast: <code>#"
                    . $broadcastId
                    . "</code>\n\n"

                    . "👥 کل کاربران: <code>"
                    . $totalUsers
                    . "</code>\n"

                    . "📨 ارسال موفق: <code>"
                    . $sentUsers
                    . "</code>\n"

                    . "❌ ارسال ناموفق: <code>"
                    . $failedUsers
                    . "</code>\n"

                    . "⏱ مدت ارسال: <code>"
                    . htmlspecialchars(
                        $durationText,
                        ENT_QUOTES |
                        ENT_SUBSTITUTE,
                        'UTF-8'
                    )
                    . "</code>";

                try {

                    $result =
                        $this->sendMessage(
                            $createdBy,
                            $message
                        );

                    /*
                    * فقط اگر Telegram موفق بود
                    * admin_notified_at را ثبت کن.
                    */
                    if (
                        is_array($result)
                        &&
                        (
                            $result['ok']
                            ?? false
                        ) === true
                    ) {

                        $notify =
                            $this->pdo->prepare("
                                UPDATE tournament_br_broadcasts
                                SET
                                    admin_notified_at = NOW()
                                WHERE id = ?
                                AND admin_notified_at IS NULL
                            ");

                        $notify->execute([
                            $broadcastId
                        ]);

                    } else {

                        error_log(
                            '[BR BROADCAST ADMIN NOTIFY FAILED] '
                            . 'broadcast='
                            . $broadcastId
                            . ' admin='
                            . $createdBy
                            . ' result='
                            . print_r(
                                $result,
                                true
                            )
                        );
                    }

                } catch (Throwable $e) {

                    error_log(
                        '[BR BROADCAST ADMIN NOTIFY ERROR] '
                        . 'broadcast='
                        . $broadcastId
                        . ' admin='
                        . $createdBy
                        . ' error='
                        . $e->getMessage()
                    );
                }
            }

            error_log(
                '[BR BROADCAST COMPLETED] '
                . 'broadcast='
                . $broadcastId
                . ' total='
                . $totalUsers
                . ' sent='
                . $sentUsers
                . ' failed='
                . $failedUsers
            );
        }
    }

    /*
     * ==========================================================
     * Remaining Queue
     * ==========================================================
     */
    private function getRemainingCount(): int
    {
        $st =
            $this->pdo->query("
                SELECT
                    COUNT(*)

                FROM tournament_br_broadcast_queue

                WHERE status IN (
                    'queued',
                    'processing'
                )
            ");

        return (int)
            $st->fetchColumn();
    }
}