<?php
/**
 * ===========================================
 * FLOWBOT DCI - RETRY MANAGER SERVICE
 * ===========================================
 * BACK-004: Implements exponential backoff with jitter for retries
 * Intelligently handles retry delays for failed requests
 */

declare(strict_types=1);

namespace FlowbotDCI\Services;

class RetryManager
{
    // Base delay in milliseconds
    private int $baseDelay;

    // Maximum delay in milliseconds
    private int $maxDelay;

    // Exponential factor (default 2 = doubles each time)
    private float $factor;

    // Whether to add jitter (randomness) to delays
    private bool $useJitter;

    // Maximum number of retries
    private int $maxRetries;

    // Retry counts per URL
    private array $retryCounts = [];

    // Last retry time per URL
    private array $lastRetryTimes = [];

    // Persistent storage file
    private ?string $stateFile = null;

    public function __construct(
        int $baseDelay = 1000,      // 1 second
        int $maxDelay = 60000,       // 60 seconds
        float $factor = 2.0,
        bool $useJitter = true,
        int $maxRetries = 5
    ) {
        $this->baseDelay = $baseDelay;
        $this->maxDelay = $maxDelay;
        $this->factor = $factor;
        $this->useJitter = $useJitter;
        $this->maxRetries = $maxRetries;
    }

    /**
     * Set state file for persistent storage
     */
    public function setStateFile(string $path): self
    {
        $this->stateFile = $path;
        $this->loadState();
        return $this;
    }

    /**
     * Check if a URL can be retried
     * @param string $url URL to check
     * @return bool True if can retry
     */
    public function canRetry(string $url): bool
    {
        $count = $this->retryCounts[$url] ?? 0;
        return $count < $this->maxRetries;
    }

    /**
     * Check if a URL is ready to retry (delay has passed)
     * @param string $url URL to check
     * @return bool True if ready to retry
     */
    public function isReadyToRetry(string $url): bool
    {
        if (!$this->canRetry($url)) {
            return false;
        }

        $lastTime = $this->lastRetryTimes[$url] ?? 0;
        if ($lastTime === 0) {
            return true;
        }

        $delay = $this->getDelay($url);
        $elapsedMs = (microtime(true) - $lastTime) * 1000;

        return $elapsedMs >= $delay;
    }

    /**
     * Get the current delay for a URL based on retry count
     * @param string $url URL to get delay for
     * @return int Delay in milliseconds
     */
    public function getDelay(string $url): int
    {
        $retryCount = $this->retryCounts[$url] ?? 0;

        if ($retryCount === 0) {
            return 0;
        }

        // Calculate exponential delay
        $delay = $this->baseDelay * pow($this->factor, $retryCount - 1);

        // Apply jitter (random variation of ±25%)
        if ($this->useJitter) {
            $jitter = $delay * 0.25;
            $delay = $delay + mt_rand((int)(-$jitter), (int)$jitter);
        }

        // Cap at max delay
        return (int)min($delay, $this->maxDelay);
    }

    /**
     * Get delay in seconds (for usleep/sleep)
     * @param string $url URL to get delay for
     * @return float Delay in seconds
     */
    public function getDelaySeconds(string $url): float
    {
        return $this->getDelay($url) / 1000;
    }

    /**
     * Record a retry attempt
     * @param string $url URL being retried
     */
    public function recordRetry(string $url): void
    {
        $this->retryCounts[$url] = ($this->retryCounts[$url] ?? 0) + 1;
        $this->lastRetryTimes[$url] = microtime(true);
        $this->saveState();
    }

    /**
     * Record a successful request (resets retry count)
     * @param string $url URL that succeeded
     */
    public function recordSuccess(string $url): void
    {
        if (isset($this->retryCounts[$url])) {
            unset($this->retryCounts[$url]);
            unset($this->lastRetryTimes[$url]);
            $this->saveState();
        }
    }

    /**
     * Get retry count for a URL
     * @param string $url URL to check
     * @return int Number of retries
     */
    public function getRetryCount(string $url): int
    {
        return $this->retryCounts[$url] ?? 0;
    }

    /**
     * Get time until next retry is allowed
     * @param string $url URL to check
     * @return int Milliseconds until retry allowed (0 if ready)
     */
    public function getTimeUntilRetry(string $url): int
    {
        $lastTime = $this->lastRetryTimes[$url] ?? 0;
        if ($lastTime === 0) {
            return 0;
        }

        $delay = $this->getDelay($url);
        $elapsedMs = (microtime(true) - $lastTime) * 1000;
        $remaining = $delay - $elapsedMs;

        return max(0, (int)$remaining);
    }

    /**
     * Filter URLs that are ready to retry
     * @param array $urls URLs to filter
     * @return array URLs ready for retry
     */
    public function filterReadyUrls(array $urls): array
    {
        $ready = [];
        foreach ($urls as $url) {
            if ($this->isReadyToRetry($url)) {
                $ready[] = $url;
            }
        }
        return $ready;
    }

    /**
     * Get URLs that have exceeded max retries
     * @param array $urls URLs to check
     * @return array URLs that have exceeded retries
     */
    public function getExhaustedUrls(array $urls): array
    {
        $exhausted = [];
        foreach ($urls as $url) {
            if (!$this->canRetry($url)) {
                $exhausted[] = $url;
            }
        }
        return $exhausted;
    }

    /**
     * Wait for the delay period before retrying
     * @param string $url URL to wait for
     * @return bool True if waited, false if no wait needed
     */
    public function waitForRetry(string $url): bool
    {
        $timeUntil = $this->getTimeUntilRetry($url);
        if ($timeUntil > 0) {
            usleep($timeUntil * 1000); // Convert ms to microseconds
            return true;
        }
        return false;
    }

    /**
     * Reset retry count for a URL
     * @param string $url URL to reset
     */
    public function reset(string $url): void
    {
        unset($this->retryCounts[$url]);
        unset($this->lastRetryTimes[$url]);
        $this->saveState();
    }

    /**
     * Reset all retry counts
     */
    public function resetAll(): void
    {
        $this->retryCounts = [];
        $this->lastRetryTimes = [];
        $this->saveState();
    }

    /**
     * Get statistics about retries
     * @return array Retry statistics
     */
    public function getStats(): array
    {
        $stats = [
            'total_tracked' => count($this->retryCounts),
            'total_retries' => array_sum($this->retryCounts),
            'max_retries_reached' => 0,
            'pending_retries' => 0,
            'by_retry_count' => [],
        ];

        foreach ($this->retryCounts as $url => $count) {
            if ($count >= $this->maxRetries) {
                $stats['max_retries_reached']++;
            } else {
                $stats['pending_retries']++;
            }

            $stats['by_retry_count'][$count] = ($stats['by_retry_count'][$count] ?? 0) + 1;
        }

        return $stats;
    }

    /**
     * Get all tracked URLs with their retry info
     * @return array URL retry information
     */
    public function getAll(): array
    {
        $data = [];
        foreach ($this->retryCounts as $url => $count) {
            $data[$url] = [
                'retry_count' => $count,
                'can_retry' => $count < $this->maxRetries,
                'delay_ms' => $this->getDelay($url),
                'time_until_retry_ms' => $this->getTimeUntilRetry($url),
                'last_retry_time' => $this->lastRetryTimes[$url] ?? null,
            ];
        }
        return $data;
    }

    // =========================================
    // Configuration Methods
    // =========================================

    /**
     * Set base delay
     */
    public function setBaseDelay(int $ms): self
    {
        $this->baseDelay = max(100, $ms);
        return $this;
    }

    /**
     * Set max delay
     */
    public function setMaxDelay(int $ms): self
    {
        $this->maxDelay = max($this->baseDelay, $ms);
        return $this;
    }

    /**
     * Set exponential factor
     */
    public function setFactor(float $factor): self
    {
        $this->factor = max(1.0, $factor);
        return $this;
    }

    /**
     * Set max retries
     */
    public function setMaxRetries(int $max): self
    {
        $this->maxRetries = max(1, $max);
        return $this;
    }

    /**
     * Enable/disable jitter
     */
    public function setUseJitter(bool $use): self
    {
        $this->useJitter = $use;
        return $this;
    }

    // =========================================
    // Private Helper Methods
    // =========================================

    /**
     * Load state from file
     */
    private function loadState(): void
    {
        if ($this->stateFile && file_exists($this->stateFile)) {
            $data = @file_get_contents($this->stateFile);
            if ($data) {
                $state = @json_decode($data, true);
                if (is_array($state)) {
                    $this->retryCounts = $state['counts'] ?? [];
                    $this->lastRetryTimes = $state['times'] ?? [];
                }
            }
        }
    }

    /**
     * Save state to file
     */
    private function saveState(): void
    {
        if ($this->stateFile) {
            $dir = dirname($this->stateFile);
            if (!is_dir($dir)) {
                @mkdir($dir, 0755, true);
            }
            @file_put_contents(
                $this->stateFile,
                json_encode([
                    'counts' => $this->retryCounts,
                    'times' => $this->lastRetryTimes,
                ], JSON_PRETTY_PRINT),
                LOCK_EX
            );
        }
    }
}
