<?php

declare(strict_types=1);

namespace ApiModel\Infrastructure\Queue;

use ApiModel\Infrastructure\Logging\StructuredLog;
use ApiModel\Infrastructure\Persistence\Database;
use DateTimeImmutable;
use PDO;
use Throwable;

final class AsyncJobQueue
{
    private bool $schemaReady = false;

    public function __construct(private readonly PDO $pdo) {}

    /**
     * @param array<string,mixed> $payload
     */
    public function enqueue(string $type, array $payload, int $maxAttempts = 5, int $delaySeconds = 0): string
    {
        $this->ensureSchema();
        $id = Database::uuid();
        $availableAt = (new DateTimeImmutable())->modify('+' . max(0, $delaySeconds) . ' seconds')->format('Y-m-d H:i:s');
        $encoded = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);

        $this->pdo->prepare(
            'INSERT INTO async_jobs (id, type, payload, status, attempts, max_attempts, available_at, last_error)
             VALUES (?, ?, ?, ?, 0, ?, ?, NULL)',
        )->execute([$id, $type, $encoded, 'pending', max(1, $maxAttempts), $availableAt]);

        return $id;
    }

    /**
     * @param callable(string,array<string,mixed>):void $handler
     * @return array{processed:int,succeeded:int,retried:int,failed:int}
     */
    public function processDue(callable $handler, int $limit = 20): array
    {
        $this->ensureSchema();
        $stats = ['processed' => 0, 'succeeded' => 0, 'retried' => 0, 'failed' => 0];

        $jobs = $this->pdo->prepare(
            "SELECT id, type, payload, attempts, max_attempts
             FROM async_jobs
             WHERE status = 'pending' AND available_at <= CURRENT_TIMESTAMP
             ORDER BY created_at ASC
             LIMIT ?",
        );
        $jobs->execute([max(1, $limit)]);

        foreach ($jobs->fetchAll() as $job) {
            $stats['processed']++;
            $attempt = ((int) $job['attempts']) + 1;
            $id = (string) $job['id'];

            $this->pdo->prepare(
                "UPDATE async_jobs
                 SET status = 'processing', attempts = ?, updated_at = CURRENT_TIMESTAMP
                 WHERE id = ?",
            )->execute([$attempt, $id]);

            try {
                $payload = json_decode((string) $job['payload'], true, 512, JSON_THROW_ON_ERROR);
                if (! is_array($payload)) {
                    throw new \RuntimeException('Payload de job inválido');
                }

                $handler((string) $job['type'], $payload);
                $this->pdo->prepare(
                    "UPDATE async_jobs
                     SET status = 'done', updated_at = CURRENT_TIMESTAMP
                     WHERE id = ?",
                )->execute([$id]);
                $stats['succeeded']++;
            } catch (Throwable $e) {
                $maxAttempts = (int) $job['max_attempts'];
                if ($attempt >= $maxAttempts) {
                    $this->pdo->prepare(
                        "UPDATE async_jobs
                         SET status = 'failed', last_error = ?, updated_at = CURRENT_TIMESTAMP
                         WHERE id = ?",
                    )->execute([$e->getMessage(), $id]);
                    $stats['failed']++;
                } else {
                    $backoff = min(300, 5 * $attempt);
                    $nextRun = (new DateTimeImmutable())->modify('+' . $backoff . ' seconds')->format('Y-m-d H:i:s');
                    $this->pdo->prepare(
                        "UPDATE async_jobs
                         SET status = 'pending', available_at = ?, last_error = ?, updated_at = CURRENT_TIMESTAMP
                         WHERE id = ?",
                    )->execute([$nextRun, $e->getMessage(), $id]);
                    $stats['retried']++;
                }

                StructuredLog::emit('warning', 'queue.job_failed', [
                    'job_id' => $id,
                    'job_type' => (string) $job['type'],
                    'attempt' => $attempt,
                    'max_attempts' => (int) $job['max_attempts'],
                    'error' => $e->getMessage(),
                ]);
            }
        }

        return $stats;
    }

    private function ensureSchema(): void
    {
        if ($this->schemaReady) {
            return;
        }

        $this->pdo->exec(
            "CREATE TABLE IF NOT EXISTS async_jobs (
                id TEXT PRIMARY KEY,
                type TEXT NOT NULL,
                payload TEXT NOT NULL,
                status TEXT NOT NULL DEFAULT 'pending',
                attempts INTEGER NOT NULL DEFAULT 0,
                max_attempts INTEGER NOT NULL DEFAULT 5,
                available_at TEXT NOT NULL,
                last_error TEXT NULL,
                created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
            )",
        );

        $this->pdo->exec(
            "CREATE INDEX IF NOT EXISTS idx_async_jobs_due
             ON async_jobs (status, available_at, created_at)",
        );

        $this->schemaReady = true;
    }
}
