<?php

declare(strict_types=1);

namespace App\Framework\Queue;

use App\Framework\Queue\ValueObjects\JobPayload;
use App\Framework\Redis\RedisConnectionInterface;
use App\Framework\Serializer\Php\PhpSerializer;
use App\Framework\Serializer\Serializer;
use Redis;

/**
 * Redis Queue Implementation with Priority Support
 *
 * Uses Redis sorted sets for priority queues and lists for FIFO within same priority
 */
final class RedisQueue implements Queue
{
    private Redis $redis;

    private string $priorityKey;

    private string $delayedKey;

    private string $statsKey;

    public function __construct(
        private RedisConnectionInterface $connection,
        private string $queueName = 'queue',
        private readonly Serializer $serializer = new PhpSerializer()
    ) {
        $this->redis = $this->connection->getClient();
        $this->priorityKey = "{$queueName}:priority";
        $this->delayedKey = "{$queueName}:delayed";
        $this->statsKey = "{$queueName}:stats";
    }

    public function push(JobPayload $payload): void
    {
        $serializedPayload = $this->serializer->serialize($payload);
        $currentTime = time();
        $score = $this->calculateScore($payload, $currentTime);

        // If delayed, add to delayed queue
        if ($payload->isDelayed()) {
            $availableTime = $payload->getAvailableTime();
            $this->redis->zAdd($this->delayedKey, $availableTime, $serializedPayload);
        } else {
            // Add to priority queue with score (higher priority = lower score for Redis)
            $this->redis->zAdd($this->priorityKey, $score, $serializedPayload);
        }

        // Update statistics
        $this->updateStats('pushed', $payload->priority->toString());
    }

    public function pop(): ?JobPayload
    {
        // First, process any delayed jobs that are now ready
        $this->processDelayedJobs();

        // Pop highest priority job (lowest score in Redis sorted set)
        $result = $this->redis->zPopMin($this->priorityKey);

        if (! empty($result)) {
            $serializedPayload = array_key_first($result);
            $payload = $this->serializer->unserialize($serializedPayload);

            if ($payload instanceof JobPayload) {
                $this->updateStats('popped', $payload->priority->toString());

                return $payload;
            }
        }

        return null;
    }

    public function peek(): ?JobPayload
    {
        // Process delayed jobs first
        $this->processDelayedJobs();

        // Peek at highest priority job
        $result = $this->redis->zRange($this->priorityKey, 0, 0);

        if (! empty($result)) {
            $serializedPayload = $result[0];
            $payload = $this->serializer->unserialize($serializedPayload);

            if ($payload instanceof JobPayload) {
                return $payload;
            }
        }

        return null;
    }

    public function size(): int
    {
        $prioritySize = $this->redis->zCard($this->priorityKey);
        $delayedSize = $this->redis->zCard($this->delayedKey);

        return $prioritySize + $delayedSize;
    }

    public function clear(): int
    {
        $prioritySize = $this->redis->zCard($this->priorityKey);
        $delayedSize = $this->redis->zCard($this->delayedKey);
        $totalSize = $prioritySize + $delayedSize;

        // Clear all queues
        $this->redis->del($this->priorityKey);
        $this->redis->del($this->delayedKey);
        $this->redis->del($this->statsKey);

        return $totalSize;
    }

    public function getStats(): array
    {
        $stats = $this->redis->hGetAll($this->statsKey);

        // Convert string values back to integers
        $processedStats = [];
        foreach ($stats as $key => $value) {
            $processedStats[$key] = is_numeric($value) ? (int) $value : $value;
        }

        // Add real-time counts
        $priorityBreakdown = $this->getPriorityBreakdown();

        return [
            'total_size' => $this->size(),
            'priority_queue_size' => $this->redis->zCard($this->priorityKey),
            'delayed_queue_size' => $this->redis->zCard($this->delayedKey),
            'priority_breakdown' => $priorityBreakdown,
            'stats' => $processedStats,
        ];
    }

    /**
     * Calculate score for Redis sorted set (lower score = higher priority)
     */
    private function calculateScore(JobPayload $payload, int $currentTime): float
    {
        $priorityScore = 1000 - $payload->priority->value; // Invert for Redis (lower score = higher priority)
        $timeScore = $currentTime / 1000000; // Microsecond precision for FIFO within same priority

        return $priorityScore + $timeScore;
    }

    /**
     * Process delayed jobs that are now ready
     */
    private function processDelayedJobs(): void
    {
        $currentTime = time();

        // Get all delayed jobs that are ready (score <= current time)
        $readyJobs = $this->redis->zRangeByScore($this->delayedKey, 0, $currentTime);

        foreach ($readyJobs as $serializedPayload) {
            // Remove from delayed queue
            $this->redis->zRem($this->delayedKey, $serializedPayload);

            // Deserialize to get the payload
            $payload = $this->serializer->unserialize($serializedPayload);

            if ($payload instanceof JobPayload) {
                // Remove delay and add to priority queue
                $payloadWithoutDelay = $payload->withDelay(\App\Framework\Core\ValueObjects\Duration::zero());
                $score = $this->calculateScore($payloadWithoutDelay, $currentTime);
                $this->redis->zAdd($this->priorityKey, $score, $this->serializer->serialize($payloadWithoutDelay));
            }
        }
    }

    /**
     * Update queue statistics
     */
    private function updateStats(string $operation, string $priority): void
    {
        $this->redis->hIncrBy($this->statsKey, "total_{$operation}", 1);
        $this->redis->hIncrBy($this->statsKey, "{$priority}_{$operation}", 1);
        $this->redis->hSet($this->statsKey, 'last_activity', time());
    }

    /**
     * Get priority breakdown of current queue
     */
    private function getPriorityBreakdown(): array
    {
        $breakdown = [];

        // Get all jobs in priority queue
        $allJobs = $this->redis->zRange($this->priorityKey, 0, -1);

        foreach ($allJobs as $serializedPayload) {
            $payload = $this->serializer->unserialize($serializedPayload);

            if ($payload instanceof JobPayload) {
                $priority = $payload->priority->toString();
                $breakdown[$priority] = ($breakdown[$priority] ?? 0) + 1;
            }
        }

        // Add delayed jobs
        $delayedJobs = $this->redis->zRange($this->delayedKey, 0, -1);

        foreach ($delayedJobs as $serializedPayload) {
            $payload = $this->serializer->unserialize($serializedPayload);

            if ($payload instanceof JobPayload) {
                $priority = $payload->priority->toString();
                $breakdown[$priority] = ($breakdown[$priority] ?? 0) + 1;
            }
        }

        return $breakdown;
    }

    /**
     * Get number of delayed jobs ready for processing
     */
    public function getReadyDelayedCount(): int
    {
        $currentTime = time();

        return $this->redis->zCount($this->delayedKey, 0, $currentTime);
    }

    /**
     * Get delayed jobs scheduled for future processing
     */
    public function getScheduledCount(): int
    {
        $currentTime = time();

        return $this->redis->zCount($this->delayedKey, $currentTime + 1, '+inf');
    }

    /**
     * Cleanup expired or old jobs (maintenance operation)
     */
    public function cleanup(int $maxAge = 86400): int
    {
        $cutoffTime = time() - $maxAge;
        $removed = 0;

        // Remove old jobs from delayed queue that are too old
        $removed += $this->redis->zRemRangeByScore($this->delayedKey, 0, $cutoffTime - $maxAge);

        return $removed;
    }
}
