<?php

namespace Gestoreasy\Core\Queue;

use Redis;
use Exception;
use Gestoreasy\Core\Cache\CacheService;

/**
 * Serviço de Filas para Processamento Assíncrono
 * 
 * Usa Redis para gerenciar filas de jobs.
 * Suporta retry, dead letter queue e prioridades.
 */
class QueueService {
    private ?Redis $redis = null;
    private bool $enabled = false;
    private string $prefix = 'queue:';
    private int $defaultRetryAttempts = 3;
    private int $defaultRetryDelay = 60; // segundos

    public function __construct() {
        $this->enabled = $this->initRedis();
    }

    /**
     * Inicializa conexão com Redis
     */
    private function initRedis(): bool {
        try {
            $host = $_ENV['REDIS_HOST'] ?? '127.0.0.1';
            $port = (int)($_ENV['REDIS_PORT'] ?? 6379);
            $password = $_ENV['REDIS_PASSWORD'] ?? null;
            $database = (int)($_ENV['REDIS_DATABASE'] ?? 1); // DB diferente do cache

            if (!extension_loaded('redis')) {
                error_log('Redis extension not loaded. Queue will be disabled.');
                return false;
            }

            $this->redis = new Redis();
            $connected = $this->redis->connect($host, $port, 2.0);
            
            if (!$connected) {
                error_log('Failed to connect to Redis. Queue will be disabled.');
                return false;
            }

            if ($password !== null) {
                $this->redis->auth($password);
            }

            $this->redis->select($database);
            $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_JSON);

            return true;
        } catch (Exception $e) {
            error_log('Redis Queue initialization error: ' . $e->getMessage());
            return false;
        }
    }

    /**
     * Verifica se as filas estão habilitadas
     */
    public function isEnabled(): bool {
        return $this->enabled && $this->redis !== null;
    }

    /**
     * Adiciona job à fila
     * 
     * @param string $queue Nome da fila
     * @param string $jobClass Classe do job
     * @param array $data Dados do job
     * @param int $priority Prioridade (maior = mais prioritário, padrão: 5)
     * @param int|null $delay Atraso em segundos antes de processar
     * @return string ID do job
     */
    public function push(
        string $queue,
        string $jobClass,
        array $data = [],
        int $priority = 5,
        ?int $delay = null
    ): string {
        if (!$this->isEnabled()) {
            throw new Exception('Queue service is not enabled');
        }

        $jobId = uniqid('job_', true);
        $job = [
            'id' => $jobId,
            'class' => $jobClass,
            'data' => $data,
            'priority' => $priority,
            'attempts' => 0,
            'max_attempts' => $this->defaultRetryAttempts,
            'created_at' => time(),
            'processed_at' => null,
            'failed_at' => null,
            'error' => null,
        ];

        $queueKey = $this->prefix . $queue;

        if ($delay !== null && $delay > 0) {
            // Job com delay vai para sorted set (score = timestamp de execução)
            $this->redis->zAdd($queueKey . ':delayed', time() + $delay, $jobId);
            $this->redis->hSet($queueKey . ':jobs', $jobId, json_encode($job));
        } else {
            // Job imediato vai para lista ordenada por prioridade
            $score = $priority * 1000000 + time(); // Prioridade + timestamp
            $this->redis->zAdd($queueKey, $score, $jobId);
            $this->redis->hSet($queueKey . ':jobs', $jobId, json_encode($job));
        }

        return $jobId;
    }

    /**
     * Processa próximo job da fila
     * 
     * @param string $queue Nome da fila
     * @param int $timeout Timeout em segundos (padrão: 5)
     * @return array|null Job processado ou null se não houver jobs
     */
    public function pop(string $queue, int $timeout = 5): ?array {
        if (!$this->isEnabled()) {
            return null;
        }

        $queueKey = $this->prefix . $queue;

        // Primeiro, mover jobs com delay expirado para a fila principal
        $this->moveDelayedJobs($queue);

        // Buscar próximo job (maior prioridade)
        $result = $this->redis->zRevRange($queueKey, 0, 0, true);
        
        if (empty($result)) {
            return null;
        }

        $jobId = array_key_first($result);
        $jobData = $this->redis->hGet($queueKey . ':jobs', $jobId);

        if ($jobData === false) {
            // Job não encontrado, remover da fila
            $this->redis->zRem($queueKey, $jobId);
            return null;
        }

        $job = json_decode($jobData, true);
        
        // Remover da fila antes de processar
        $this->redis->zRem($queueKey, $jobId);

        return $job;
    }

    /**
     * Move jobs com delay expirado para a fila principal
     */
    private function moveDelayedJobs(string $queue): void {
        $queueKey = $this->prefix . $queue;
        $delayedKey = $queueKey . ':delayed';
        $now = time();

        // Buscar jobs com delay expirado
        $expired = $this->redis->zRangeByScore($delayedKey, 0, $now);

        foreach ($expired as $jobId) {
            $jobData = $this->redis->hGet($queueKey . ':jobs', $jobId);
            
            if ($jobData !== false) {
                $job = json_decode($jobData, true);
                $priority = $job['priority'] ?? 5;
                $score = $priority * 1000000 + time();
                
                // Mover para fila principal
                $this->redis->zAdd($queueKey, $score, $jobId);
                $this->redis->zRem($delayedKey, $jobId);
            }
        }
    }

    /**
     * Marca job como processado com sucesso
     * 
     * @param string $queue Nome da fila
     * @param string $jobId ID do job
     */
    public function markAsProcessed(string $queue, string $jobId): void {
        if (!$this->isEnabled()) {
            return;
        }

        $queueKey = $this->prefix . $queue;
        $jobData = $this->redis->hGet($queueKey . ':jobs', $jobId);

        if ($jobData !== false) {
            $job = json_decode($jobData, true);
            $job['processed_at'] = time();
            $this->redis->hSet($queueKey . ':jobs', $jobId, json_encode($job));
        }

        // Remover job dos metadados
        $this->redis->hDel($queueKey . ':jobs', $jobId);
    }

    /**
     * Marca job como falhado e agenda retry ou move para dead letter
     * 
     * @param string $queue Nome da fila
     * @param string $jobId ID do job
     * @param string $error Mensagem de erro
     */
    public function markAsFailed(string $queue, string $jobId, string $error): void {
        if (!$this->isEnabled()) {
            return;
        }

        $queueKey = $this->prefix . $queue;
        $jobData = $this->redis->hGet($queueKey . ':jobs', $jobId);

        if ($jobData === false) {
            return;
        }

        $job = json_decode($jobData, true);
        $job['attempts'] = ($job['attempts'] ?? 0) + 1;
        $job['error'] = $error;
        $job['failed_at'] = time();

        $maxAttempts = $job['max_attempts'] ?? $this->defaultRetryAttempts;

        if ($job['attempts'] >= $maxAttempts) {
            // Mover para dead letter queue
            $this->redis->lPush($queueKey . ':failed', json_encode($job));
            $this->redis->hDel($queueKey . ':jobs', $jobId);
        } else {
            // Agendar retry com delay exponencial
            $delay = $this->defaultRetryDelay * pow(2, $job['attempts'] - 1);
            $this->redis->zAdd($queueKey . ':delayed', time() + $delay, $jobId);
            $this->redis->hSet($queueKey . ':jobs', $jobId, json_encode($job));
        }
    }

    /**
     * Obtém estatísticas da fila
     * 
     * @param string $queue Nome da fila
     * @return array
     */
    public function getQueueStats(string $queue): array {
        if (!$this->isEnabled()) {
            return ['enabled' => false];
        }

        $queueKey = $this->prefix . $queue;

        return [
            'enabled' => true,
            'pending' => $this->redis->zCard($queueKey),
            'delayed' => $this->redis->zCard($queueKey . ':delayed'),
            'failed' => $this->redis->lLen($queueKey . ':failed'),
        ];
    }

    /**
     * Limpa fila (use com cuidado!)
     * 
     * @param string $queue Nome da fila
     * @param bool $includeFailed Incluir dead letter queue
     */
    public function clear(string $queue, bool $includeFailed = false): void {
        if (!$this->isEnabled()) {
            return;
        }

        $queueKey = $this->prefix . $queue;
        $this->redis->del($queueKey);
        $this->redis->del($queueKey . ':delayed');
        $this->redis->del($queueKey . ':jobs');

        if ($includeFailed) {
            $this->redis->del($queueKey . ':failed');
        }
    }
}

