<?php
declare(strict_types=1);

namespace Celena\Core\Queue;

use Celena\Core\Container;
use Celena\Core\Database\Connection;
use Celena\Core\Logger;
use Throwable;

/**
 * Очередь задач на таблице celena_jobs.
 * Worker запускается по cron: `php bin/celena queue:work --max=50s`.
 */
final class QueueManager
{
    public function __construct(
        private readonly Connection $conn,
        private readonly Logger $logger,
        private readonly Container $container,
    ) {
    }

    public function dispatch(string $handler, array $payload, string $queue = 'default', int $delaySeconds = 0, int $maxAttempts = 3): int
    {
        $now = date('Y-m-d H:i:s');
        $availableAt = date('Y-m-d H:i:s', time() + $delaySeconds);
        $id = $this->conn->builder('celena_jobs')->insert([
            'queue' => $queue,
            'handler' => $handler,
            'payload' => json_encode($payload, JSON_UNESCAPED_UNICODE),
            'attempts' => 0,
            'max_attempts' => $maxAttempts,
            'available_at' => $availableAt,
            'reserved_at' => null,
            'completed_at' => null,
            'last_error' => null,
            'created_at' => $now,
        ]);
        return (int) $id;
    }

    /**
     * Запустить воркер на $maxSeconds секунд.
     * @return int количество обработанных задач
     */
    public function work(int $maxSeconds = 50, string $queue = 'default'): int
    {
        $deadline = time() + $maxSeconds;
        $processed = 0;
        while (time() < $deadline) {
            $job = $this->reserveNext($queue);
            if ($job === null) {
                // Пусто — выходим раньше deadline.
                break;
            }
            $this->process($job);
            $processed++;
        }
        return $processed;
    }

    /** @return array<string, mixed>|null */
    private function reserveNext(string $queue): ?array
    {
        // Выбираем доступную задачу (available_at <= now, не reserved, не completed).
        $now = date('Y-m-d H:i:s');
        $this->conn->transaction(function () use ($queue, $now, &$job) {
            $driver = $this->conn->driver()->name();
            // Пессимистическая блокировка — FOR UPDATE для PG и MySQL.
            $sql = 'SELECT * FROM ' . $this->conn->table_q('celena_jobs')
                . ' WHERE ' . $this->conn->quoteId('queue') . ' = ?'
                . '  AND ' . $this->conn->quoteId('reserved_at') . ' IS NULL'
                . '  AND ' . $this->conn->quoteId('completed_at') . ' IS NULL'
                . '  AND ' . $this->conn->quoteId('available_at') . ' <= ?'
                . ' ORDER BY ' . $this->conn->quoteId('id') . ' ASC'
                . ' LIMIT 1 FOR UPDATE';
            $row = $this->conn->run($sql, [$queue, $now])->fetch();
            if ($row === false || $row === null) {
                $job = null;
                return;
            }
            $this->conn->builder('celena_jobs')
                ->where('id', '=', (int) $row['id'])
                ->update(['reserved_at' => $now, 'attempts' => (int) $row['attempts'] + 1]);
            $row['attempts'] = (int) $row['attempts'] + 1;
            $row['reserved_at'] = $now;
            $job = $row;
        });
        return $job ?? null;
    }

    private function process(array $job): void
    {
        $handler = (string) $job['handler'];
        $payload = json_decode((string) ($job['payload'] ?? ''), true) ?: [];
        try {
            if (!class_exists($handler)) {
                throw new \RuntimeException("Job handler not found: $handler");
            }
            $instance = $this->container->make($handler);
            if (!$instance instanceof JobInterface) {
                throw new \RuntimeException("Job handler must implement JobInterface: $handler");
            }
            $instance->handle($this->container, $payload);
            $this->conn->builder('celena_jobs')
                ->where('id', '=', (int) $job['id'])
                ->update(['completed_at' => date('Y-m-d H:i:s'), 'last_error' => null]);
            $this->logger->info('Job done: ' . $handler, ['id' => $job['id']]);
        } catch (Throwable $e) {
            $attempts = (int) $job['attempts'];
            $max = (int) $job['max_attempts'];
            if ($attempts >= $max) {
                $this->conn->builder('celena_jobs')
                    ->where('id', '=', (int) $job['id'])
                    ->update([
                        'completed_at' => date('Y-m-d H:i:s'),
                        'last_error' => 'FAILED: ' . $e->getMessage(),
                    ]);
                $this->logger->error('Job failed: ' . $handler, ['id' => $job['id'], 'err' => $e->getMessage()]);
            } else {
                // Вернём в очередь с задержкой 60 * attempts секунд.
                $delay = 60 * $attempts;
                $this->conn->builder('celena_jobs')
                    ->where('id', '=', (int) $job['id'])
                    ->update([
                        'reserved_at' => null,
                        'available_at' => date('Y-m-d H:i:s', time() + $delay),
                        'last_error' => $e->getMessage(),
                    ]);
                $this->logger->warning('Job retry: ' . $handler, ['id' => $job['id'], 'err' => $e->getMessage()]);
            }
        }
    }
}
