<?php
namespace EasyChat\Library\Queue;

use EasyChat\Library\Logger;

class QueueManager {

    private $db;
    private $prefix;

     
    const STALE_TIMEOUT = 300;

     
    const BATCH_SIZE = 5;
    const DONE_RETENTION_SECONDS = 604800;
    const FAILED_RETENTION_SECONDS = 2592000;

    public function __construct($registry) {
        $this->db     = $registry->get('db');
        $this->prefix = easychat_db_prefix();
        $this->ensureTable();
        $this->ensureColumns();
    }

    








    public function push(string $type, array $payload, int $delay = 0, int $priority = 0): int {
        $type_e       = $this->db->escape($type);
        $payload_e    = $this->db->escape(json_encode($payload, JSON_UNESCAPED_UNICODE));
        $available_at = time() + max(0, $delay);
        $created_at   = time();
        $priority     = max(0, $priority);

        $this->db->query(
            "INSERT INTO `{$this->prefix}queue`
                (job_type, payload, status, attempts, max_attempts, priority, available_at, created_at)
             VALUES
                ('$type_e', '$payload_e', 'pending', 0, 3, $priority, $available_at, $created_at)"
        );

        return (int) $this->db->getLastId();
    }

    






    public function pushUnique(string $type, array $payload, string $dedupe_key, int $delay = 0, int $priority = 0): int {
        $dedupe_key = trim($dedupe_key);
        if ($dedupe_key === '') {
            return $this->push($type, $payload, $delay, $priority);
        }

        $type_e       = $this->db->escape($type);
        $payload_e    = $this->db->escape(json_encode($payload, JSON_UNESCAPED_UNICODE));
        $dedupe_e     = $this->db->escape(mb_substr($dedupe_key, 0, 191, 'UTF-8'));
        $available_at = time() + max(0, $delay);
        $created_at   = time();
        $priority     = max(0, $priority);

         
         
         
         
         
         
         
        $this->db->query(
            "INSERT INTO `{$this->prefix}queue`
                (job_type, payload, dedupe_key, status, attempts, max_attempts, priority, available_at, created_at)
             VALUES
                ('$type_e', '$payload_e', '$dedupe_e', 'pending', 0, 3, $priority, $available_at, $created_at)
             ON DUPLICATE KEY UPDATE
                id = LAST_INSERT_ID(id),
                payload = IF(
                    status = 'processing' AND locked_at IS NOT NULL,
                    VALUES(payload),
                    payload
                ),
                available_at = IF(
                    status = 'processing' AND locked_at IS NOT NULL,
                    LEAST(
                        IF(available_at > locked_at, available_at, 4294967295),
                        GREATEST(locked_at + 1, VALUES(available_at))
                    ),
                    LEAST(available_at, VALUES(available_at))
                ),
                priority = GREATEST(priority, VALUES(priority))"
        );

        return (int)$this->db->getLastId();
    }

    










    public function pop(): ?array {
         
        $token   = md5(uniqid('q', true));
        $token_e = $this->db->escape($token);
        $now     = time();

        $this->db->query(
            "UPDATE `{$this->prefix}queue`
             SET    status = 'processing', locked_at = $now, lock_token = '$token_e'
             WHERE  status = 'pending' AND available_at <= $now
             ORDER  BY priority DESC, id ASC
             LIMIT  1"
        );

        if ((int) $this->db->countAffected() === 0) {
            return null;
        }

        $result = $this->db->query(
            "SELECT * FROM `{$this->prefix}queue` WHERE lock_token = '$token_e' LIMIT 1"
        );

        return ($result && $result->row) ? $result->row : null;
    }

     
    public function claimById(int $id): ?array {
        if ($id <= 0) return null;
        $token = md5(uniqid('q', true));
        $token_e = $this->db->escape($token);
        $now = time();
        $this->db->query(
            "UPDATE `{$this->prefix}queue`
             SET status = 'processing', locked_at = {$now}, lock_token = '{$token_e}'
             WHERE id = {$id} AND status = 'pending' AND available_at <= {$now}"
        );
        if ((int)$this->db->countAffected() !== 1) return null;
        $result = $this->db->query(
            "SELECT * FROM `{$this->prefix}queue`
             WHERE id = {$id} AND status = 'processing' AND lock_token = '{$token_e}' LIMIT 1"
        );
        return ($result && $result->row) ? $result->row : null;
    }

    


    public function markDone(int $id, string $lock_token = ''): bool {
        $owner = $this->ownerWhere($lock_token);
        $finished_at = time();
         
         
         
         
         
         
        $rerun = "dedupe_key IS NOT NULL AND locked_at IS NOT NULL AND available_at > locked_at";
        $this->db->query(
            "UPDATE `{$this->prefix}queue`
             SET status = IF({$rerun}, 'pending', 'done'),
                 dedupe_key = IF({$rerun}, dedupe_key, NULL),
                 locked_at = NULL, lock_token = NULL, error = NULL,
                 finished_at = IF({$rerun}, NULL, {$finished_at})
             WHERE id = $id{$owner}"
        );
        return (int)$this->db->countAffected() === 1;
    }

    





    public function markFailed(int $id, string $error = '', string $lock_token = ''): void {
        $owner = $this->ownerWhere($lock_token);
        $result = $this->db->query(
            "SELECT attempts, max_attempts FROM `{$this->prefix}queue` WHERE id = $id{$owner} LIMIT 1"
        );

        if (!$result || !$result->row) {
            return;
        }

        $attempts = (int) $result->row['attempts'] + 1;
        $max      = (int) $result->row['max_attempts'];
        $error_e  = $this->db->escape(mb_substr($error, 0, 2000));
        $finished_at = time();

        if ($attempts >= $max) {
            $this->db->query(
                "UPDATE `{$this->prefix}queue`
                 SET    status = 'failed', attempts = $attempts,
                        error = '$error_e', locked_at = NULL, lock_token = NULL,
                        dedupe_key = NULL, finished_at = {$finished_at}
                 WHERE  id = $id{$owner}"
            );
        } else {
             
            $backoff      = min(60 * (int) pow(5, $attempts - 1), 3600);
            $available_at = time() + $backoff;

            $this->db->query(
                "UPDATE `{$this->prefix}queue`
                 SET    status = 'pending', attempts = $attempts,
                        available_at = IF(
                            locked_at IS NOT NULL AND available_at > locked_at,
                            LEAST(available_at, $available_at),
                            $available_at
                        ),
                        locked_at = NULL, lock_token = NULL, error = '$error_e', finished_at = NULL
                 WHERE  id = $id{$owner}"
            );
        }
    }

    






    public function reschedule(int $id, int $delay = 60, string $error = '', string $lock_token = ''): void {
        $id = (int)$id;
        if ($id <= 0) {
            return;
        }

        $delay = max(1, min(3600, (int)$delay));
        $available_at = time() + $delay;
        $error_e = $this->db->escape(mb_substr($error, 0, 2000));
        $owner = $this->ownerWhere($lock_token);

        $this->db->query(
            "UPDATE `{$this->prefix}queue`
             SET    status = 'pending',
                    available_at = IF(
                        locked_at IS NOT NULL AND available_at > locked_at,
                        LEAST(available_at, $available_at),
                        $available_at
                    ),
                    locked_at = NULL,
                    lock_token = NULL,
                    error = '$error_e',
                    finished_at = NULL
             WHERE  id = $id{$owner}"
        );
    }

    






    public function rescheduleWithPayload(int $id, array $payload, int $delay = 60, string $error = '', string $lock_token = ''): void {
        $id = (int)$id;
        if ($id <= 0) {
            return;
        }

        $delay = max(1, min(3600, (int)$delay));
        $available_at = time() + $delay;
        $payload_e = $this->db->escape(json_encode($payload, JSON_UNESCAPED_UNICODE));
        $error_e = $this->db->escape(mb_substr($error, 0, 2000));
        $owner = $this->ownerWhere($lock_token);

        $this->db->query(
            "UPDATE `{$this->prefix}queue`
             SET    `payload` = '{$payload_e}',
                    `status` = 'pending',
                    `available_at` = IF(
                        `locked_at` IS NOT NULL AND `available_at` > `locked_at`,
                        LEAST(`available_at`, {$available_at}),
                        {$available_at}
                    ),
                    `locked_at` = NULL,
                    `lock_token` = NULL,
                    `error` = '{$error_e}',
                    `finished_at` = NULL
             WHERE  `id` = {$id}{$owner}"
        );
    }

     
    private function ownerWhere(string $lock_token): string {
        $lock_token = trim($lock_token);
        if ($lock_token === '') return '';
        $token_e = $this->db->escape($lock_token);
        return " AND status = 'processing' AND lock_token = '{$token_e}'";
    }

    





    public function cancelPendingByDedupeKey(string $dedupe_key, string $reason = 'cancelled_by_admin'): int {
        $dedupe_key = trim($dedupe_key);
        if ($dedupe_key === '') {
            return 0;
        }
        $dedupe_e = $this->db->escape(mb_substr($dedupe_key, 0, 191, 'UTF-8'));
        $reason_e = $this->db->escape(mb_substr($reason, 0, 500, 'UTF-8'));
        $finished_at = time();
        $this->db->query(
            "UPDATE `{$this->prefix}queue`
             SET status = 'done', dedupe_key = NULL, locked_at = NULL,
                 lock_token = NULL, error = '{$reason_e}', finished_at = {$finished_at}
             WHERE status = 'pending' AND dedupe_key = '{$dedupe_e}'"
        );
        return (int)$this->db->countAffected();
    }

    





    public function cleanup(): int {
        $threshold = time() - self::STALE_TIMEOUT;

        $this->db->query(
            "UPDATE `{$this->prefix}queue`
             SET    status = 'pending', locked_at = NULL, lock_token = NULL
             WHERE  status = 'processing'
               AND  locked_at IS NOT NULL
               AND  locked_at < $threshold"
        );

        $reset = (int) $this->db->countAffected();

         
         
        $done_before = time() - self::DONE_RETENTION_SECONDS;
        $failed_before = time() - self::FAILED_RETENTION_SECONDS;
        $this->db->query(
            "DELETE FROM `{$this->prefix}queue`
             WHERE status = 'done' AND finished_at IS NOT NULL AND finished_at < {$done_before}
             ORDER BY finished_at ASC LIMIT 200"
        );
        $this->db->query(
            "DELETE FROM `{$this->prefix}queue`
             WHERE status = 'failed' AND finished_at IS NOT NULL AND finished_at < {$failed_before}
             ORDER BY finished_at ASC LIMIT 200"
        );

        return $reset;
    }

    


    public function log(string $message): void {
        Logger::write($message, 'queue.log');
    }

    





    private function ensureTable(): void {
        $this->db->query(
            "CREATE TABLE IF NOT EXISTS `{$this->prefix}queue` (
                `id`           INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
                `job_type`     VARCHAR(64) NOT NULL,
                `payload`      TEXT NOT NULL,
                `dedupe_key`   VARCHAR(191) NULL DEFAULT NULL,
                `status`       ENUM('pending','processing','done','failed') NOT NULL DEFAULT 'pending',
                `attempts`     TINYINT UNSIGNED NOT NULL DEFAULT 0,
                `max_attempts` TINYINT UNSIGNED NOT NULL DEFAULT 3,
                `priority`     TINYINT UNSIGNED NOT NULL DEFAULT 0,
                `available_at` INT UNSIGNED NOT NULL,
                `locked_at`    INT UNSIGNED NULL DEFAULT NULL,
                `lock_token`   CHAR(32) NULL DEFAULT NULL,
                `created_at`   INT UNSIGNED NOT NULL,
                `finished_at`  INT UNSIGNED NULL DEFAULT NULL,
                `error`        TEXT NULL DEFAULT NULL,
                INDEX `idx_status_priority` (`status`, `priority`, `available_at`),
                INDEX `idx_status_finished` (`status`, `finished_at`),
                UNIQUE KEY `uq_active_dedupe` (`dedupe_key`)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
        );
    }

    



    private function ensureColumns(): void {
        $db_name = defined('DB_DATABASE') ? $this->db->escape(DB_DATABASE) : '';
        $table   = $this->db->escape($this->prefix . 'queue');

        try {
            $priority = $this->db->query(
                "SELECT COUNT(*) AS cnt
                 FROM information_schema.columns
                 WHERE table_schema = '{$db_name}'
                   AND table_name = '{$table}' AND column_name = 'priority'"
            );
            if ((int)($priority->row['cnt'] ?? 0) === 0) {
                $this->db->query(
                    "ALTER TABLE `{$this->prefix}queue`
                     ADD COLUMN `priority` TINYINT UNSIGNED NOT NULL DEFAULT 0 AFTER `max_attempts`"
                );
            }

            $dedupe = $this->db->query(
                "SELECT COUNT(*) AS cnt
                 FROM information_schema.columns
                 WHERE table_schema = '{$db_name}'
                   AND table_name = '{$table}' AND column_name = 'dedupe_key'"
            );
            if ((int)($dedupe->row['cnt'] ?? 0) === 0) {
                $this->db->query(
                    "ALTER TABLE `{$this->prefix}queue`
                     ADD COLUMN `dedupe_key` VARCHAR(191) NULL DEFAULT NULL AFTER `payload`"
                );
            }

            $finished = $this->db->query(
                "SELECT COUNT(*) AS cnt
                 FROM information_schema.columns
                 WHERE table_schema = '{$db_name}'
                   AND table_name = '{$table}' AND column_name = 'finished_at'"
            );
            if ((int)($finished->row['cnt'] ?? 0) === 0) {
                $this->db->query(
                    "ALTER TABLE `{$this->prefix}queue`
                     ADD COLUMN `finished_at` INT UNSIGNED NULL DEFAULT NULL AFTER `created_at`"
                );
            }

            $unique = $this->db->query(
                "SELECT COUNT(*) AS cnt
                 FROM information_schema.statistics
                 WHERE table_schema = '{$db_name}'
                   AND table_name = '{$table}' AND index_name = 'uq_active_dedupe'"
            );
            if ((int)($unique->row['cnt'] ?? 0) === 0) {
                $this->db->query(
                    "ALTER TABLE `{$this->prefix}queue`
                     ADD UNIQUE KEY `uq_active_dedupe` (`dedupe_key`)"
                );
            }

            $finished_index = $this->db->query(
                "SELECT COUNT(*) AS cnt
                 FROM information_schema.statistics
                 WHERE table_schema = '{$db_name}'
                   AND table_name = '{$table}' AND index_name = 'idx_status_finished'"
            );
            if ((int)($finished_index->row['cnt'] ?? 0) === 0) {
                $this->db->query(
                    "ALTER TABLE `{$this->prefix}queue`
                     ADD INDEX `idx_status_finished` (`status`, `finished_at`)"
                );
            }
        } catch (\Throwable $e) {
             
             
        }
    }
}
