<?php
/**
 * ===========================================
 * FLOWBOT DCI - DOMAIN RATE LIMITER v1.0
 * ===========================================
 * Smart per-domain rate limiting to handle 429 errors
 * without slowing down the entire system.
 *
 * Features:
 * - Dynamic concurrency per domain (reduces on 429)
 * - Retry-After header parsing
 * - Domain cooldown periods
 * - Aggressive backoff for problematic domains
 * - Auto-recovery when domain responds well
 */

declare(strict_types=1);

namespace FlowbotDCI\Services;

class DomainRateLimiter
{
    // Rate limit config per domain
    private array $domainConfigs = [];

    // Domain cooldowns (timestamp when cooldown ends)
    private array $domainCooldowns = [];

    // 429 counter per domain (for pattern detection)
    private array $domain429Counts = [];

    // Success counter per domain (for recovery detection)
    private array $domainSuccessCounts = [];

    // Default configuration
    private array $defaults = [
        'max_concurrent' => 3,      // Default max concurrent per domain
        'delay_ms' => 100,          // Default delay between requests
        'cooldown_seconds' => 0,    // No cooldown by default
        'backoff_multiplier' => 1,  // No backoff by default
    ];

    // Persistent storage path
    private string $storagePath;

    public function __construct(?string $storagePath = null)
    {
        $this->storagePath = $storagePath
            ?? dirname(__DIR__, 2) . '/temp/domain_rate_limits.json';

        $this->loadState();
    }

    /**
     * Load persisted state
     */
    private function loadState(): void
    {
        if (file_exists($this->storagePath)) {
            $data = @file_get_contents($this->storagePath);
            if ($data) {
                $state = json_decode($data, true);
                if (is_array($state)) {
                    $this->domainConfigs = $state['configs'] ?? [];
                    $this->domainCooldowns = $state['cooldowns'] ?? [];
                    $this->domain429Counts = $state['counts_429'] ?? [];
                    $this->domainSuccessCounts = $state['counts_success'] ?? [];

                    // Clean expired cooldowns
                    $now = time();
                    foreach ($this->domainCooldowns as $domain => $endTime) {
                        if ($endTime < $now) {
                            unset($this->domainCooldowns[$domain]);
                        }
                    }
                }
            }
        }
    }

    /**
     * Save state to disk
     */
    public function saveState(): void
    {
        $state = [
            'configs' => $this->domainConfigs,
            'cooldowns' => $this->domainCooldowns,
            'counts_429' => $this->domain429Counts,
            'counts_success' => $this->domainSuccessCounts,
            'updated' => time(),
        ];

        $dir = dirname($this->storagePath);
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }

        @file_put_contents($this->storagePath, json_encode($state, JSON_PRETTY_PRINT));
    }

    /**
     * Get domain from URL (extract base domain)
     */
    public function getDomain(string $url): string
    {
        $host = parse_url($url, PHP_URL_HOST) ?? '';
        $parts = explode('.', $host);
        $count = count($parts);

        // Get last 2 parts (e.g., erome.com from www.erome.com)
        if ($count >= 2) {
            return $parts[$count - 2] . '.' . $parts[$count - 1];
        }
        return $host;
    }

    /**
     * Check if domain is in cooldown
     */
    public function isInCooldown(string $domain): bool
    {
        $domain = $this->normalizeDomain($domain);

        if (!isset($this->domainCooldowns[$domain])) {
            return false;
        }

        if ($this->domainCooldowns[$domain] < time()) {
            unset($this->domainCooldowns[$domain]);
            return false;
        }

        return true;
    }

    /**
     * Get remaining cooldown time in seconds
     */
    public function getCooldownRemaining(string $domain): int
    {
        $domain = $this->normalizeDomain($domain);

        if (!isset($this->domainCooldowns[$domain])) {
            return 0;
        }

        $remaining = $this->domainCooldowns[$domain] - time();
        return max(0, $remaining);
    }

    /**
     * Set domain cooldown
     */
    public function setCooldown(string $domain, int $seconds): void
    {
        $domain = $this->normalizeDomain($domain);
        $this->domainCooldowns[$domain] = time() + $seconds;
        $this->saveState();
    }

    /**
     * Get max concurrent requests for domain
     */
    public function getMaxConcurrent(string $domain): int
    {
        $domain = $this->normalizeDomain($domain);
        return $this->domainConfigs[$domain]['max_concurrent']
            ?? $this->defaults['max_concurrent'];
    }

    /**
     * Get delay for domain in milliseconds
     */
    public function getDelay(string $domain): int
    {
        $domain = $this->normalizeDomain($domain);
        $config = $this->domainConfigs[$domain] ?? $this->defaults;

        $baseDelay = $config['delay_ms'] ?? $this->defaults['delay_ms'];
        $multiplier = $config['backoff_multiplier'] ?? 1;

        // Add jitter (±20%)
        $jitter = (int)($baseDelay * (rand(-20, 20) / 100));

        return (int)(($baseDelay + $jitter) * $multiplier);
    }

    /**
     * Record a 429 response - adjust rate limits
     *
     * @param string $domain Domain that returned 429
     * @param int|null $retryAfter Retry-After header value (seconds)
     */
    public function record429(string $domain, ?int $retryAfter = null): void
    {
        $domain = $this->normalizeDomain($domain);

        // Increment 429 counter
        $this->domain429Counts[$domain] = ($this->domain429Counts[$domain] ?? 0) + 1;
        $count = $this->domain429Counts[$domain];

        // Reset success counter
        $this->domainSuccessCounts[$domain] = 0;

        // Get current config
        $config = $this->domainConfigs[$domain] ?? $this->defaults;

        // AGGRESSIVE ADJUSTMENTS based on 429 count
        if ($count === 1) {
            // First 429: reduce concurrency to 2, increase delay 2x
            $config['max_concurrent'] = min(2, $config['max_concurrent'] ?? 3);
            $config['delay_ms'] = max(500, ($config['delay_ms'] ?? 100) * 2);
            $config['backoff_multiplier'] = 2;
        } elseif ($count === 2) {
            // Second 429: reduce to 1 concurrent, increase delay 3x
            $config['max_concurrent'] = 1;
            $config['delay_ms'] = max(1000, ($config['delay_ms'] ?? 100) * 3);
            $config['backoff_multiplier'] = 3;
        } elseif ($count >= 3) {
            // Third+ 429: single request, long delay, add cooldown
            $config['max_concurrent'] = 1;
            $config['delay_ms'] = max(2000, ($config['delay_ms'] ?? 100) * 5);
            $config['backoff_multiplier'] = 5;

            // Add cooldown based on Retry-After or default
            $cooldownSeconds = $retryAfter ?? ($count * 10); // 10s * count
            $this->setCooldown($domain, min(120, $cooldownSeconds)); // Max 2 min
        }

        $this->domainConfigs[$domain] = $config;
        $this->saveState();

        error_log("DomainRateLimiter: 429 #{$count} for {$domain} - " .
                  "concurrent={$config['max_concurrent']}, delay={$config['delay_ms']}ms");
    }

    /**
     * Record successful response - gradually recover
     */
    public function recordSuccess(string $domain): void
    {
        $domain = $this->normalizeDomain($domain);

        // Increment success counter
        $this->domainSuccessCounts[$domain] = ($this->domainSuccessCounts[$domain] ?? 0) + 1;
        $count = $this->domainSuccessCounts[$domain];

        // If no 429s recorded, nothing to recover
        if (!isset($this->domainConfigs[$domain])) {
            return;
        }

        // Gradual recovery after consecutive successes
        if ($count >= 10) {
            $config = $this->domainConfigs[$domain];

            // Reduce backoff multiplier
            if (isset($config['backoff_multiplier']) && $config['backoff_multiplier'] > 1) {
                $config['backoff_multiplier'] = max(1, $config['backoff_multiplier'] - 0.5);
            }

            // Reduce delay (min 100ms)
            if (isset($config['delay_ms']) && $config['delay_ms'] > 100) {
                $config['delay_ms'] = max(100, (int)($config['delay_ms'] * 0.9));
            }

            // Increase concurrency gradually
            if ($count >= 20 && isset($config['max_concurrent']) && $config['max_concurrent'] < 2) {
                $config['max_concurrent'] = 2;
            }

            if ($count >= 50 && isset($config['max_concurrent']) && $config['max_concurrent'] < 3) {
                $config['max_concurrent'] = 3;
            }

            $this->domainConfigs[$domain] = $config;

            // Reset 429 count after recovery
            if ($count >= 100) {
                $this->domain429Counts[$domain] = 0;
                $this->domainSuccessCounts[$domain] = 0;
            }

            $this->saveState();
        }
    }

    /**
     * Record any error (not 429 specific)
     */
    public function recordError(string $domain, int $httpCode): void
    {
        // Only special handling for 429
        if ($httpCode === 429) {
            $this->record429($domain);
        }
    }

    /**
     * Parse Retry-After header
     *
     * @param string|null $headerValue The Retry-After header value
     * @return int|null Seconds to wait, or null if invalid
     */
    public function parseRetryAfter(?string $headerValue): ?int
    {
        if (empty($headerValue)) {
            return null;
        }

        // If numeric, it's seconds
        if (is_numeric($headerValue)) {
            return max(1, (int)$headerValue);
        }

        // Otherwise it's an HTTP date
        $timestamp = strtotime($headerValue);
        if ($timestamp !== false) {
            $wait = $timestamp - time();
            return max(1, $wait);
        }

        return null;
    }

    /**
     * Get statistics for a domain
     */
    public function getDomainStats(string $domain): array
    {
        $domain = $this->normalizeDomain($domain);

        return [
            'domain' => $domain,
            'config' => $this->domainConfigs[$domain] ?? $this->defaults,
            'count_429' => $this->domain429Counts[$domain] ?? 0,
            'count_success' => $this->domainSuccessCounts[$domain] ?? 0,
            'in_cooldown' => $this->isInCooldown($domain),
            'cooldown_remaining' => $this->getCooldownRemaining($domain),
        ];
    }

    /**
     * Get all problematic domains (with high 429 counts)
     */
    public function getProblematicDomains(): array
    {
        $problematic = [];

        foreach ($this->domain429Counts as $domain => $count) {
            if ($count >= 2) {
                $problematic[$domain] = $this->getDomainStats($domain);
            }
        }

        // Sort by 429 count descending
        uasort($problematic, fn($a, $b) => $b['count_429'] <=> $a['count_429']);

        return $problematic;
    }

    /**
     * Reset all rate limits for a domain
     */
    public function resetDomain(string $domain): void
    {
        $domain = $this->normalizeDomain($domain);

        unset($this->domainConfigs[$domain]);
        unset($this->domainCooldowns[$domain]);
        unset($this->domain429Counts[$domain]);
        unset($this->domainSuccessCounts[$domain]);

        $this->saveState();
    }

    /**
     * Reset all rate limits
     */
    public function resetAll(): void
    {
        $this->domainConfigs = [];
        $this->domainCooldowns = [];
        $this->domain429Counts = [];
        $this->domainSuccessCounts = [];

        $this->saveState();
    }

    /**
     * Normalize domain name
     */
    private function normalizeDomain(string $domain): string
    {
        // Remove protocol if present
        if (str_contains($domain, '://')) {
            $domain = parse_url($domain, PHP_URL_HOST) ?? $domain;
        }

        // Remove www.
        if (str_starts_with($domain, 'www.')) {
            $domain = substr($domain, 4);
        }

        return strtolower(trim($domain));
    }

    /**
     * Get ordered URLs - deprioritize domains in cooldown or with high 429 rates
     *
     * @param array $urls List of URLs to process
     * @return array Reordered URLs (non-problematic first)
     */
    public function prioritizeUrls(array $urls): array
    {
        $normal = [];
        $deferred = [];
        $blocked = [];

        foreach ($urls as $url) {
            $domain = $this->getDomain($url);

            if ($this->isInCooldown($domain)) {
                $blocked[] = $url;
            } elseif (($this->domain429Counts[$domain] ?? 0) >= 3) {
                $deferred[] = $url;
            } else {
                $normal[] = $url;
            }
        }

        // Return: normal first, then deferred, blocked last
        return array_merge($normal, $deferred, $blocked);
    }

    /**
     * Check if should process URL now or defer
     *
     * @param string $url URL to check
     * @return array ['process' => bool, 'reason' => string, 'wait_seconds' => int]
     */
    public function shouldProcess(string $url): array
    {
        $domain = $this->getDomain($url);

        if ($this->isInCooldown($domain)) {
            return [
                'process' => false,
                'reason' => 'Domain in cooldown',
                'wait_seconds' => $this->getCooldownRemaining($domain),
            ];
        }

        $count429 = $this->domain429Counts[$domain] ?? 0;
        if ($count429 >= 5) {
            return [
                'process' => false,
                'reason' => 'Domain has too many 429 errors',
                'wait_seconds' => 60,
            ];
        }

        return [
            'process' => true,
            'reason' => 'OK',
            'wait_seconds' => 0,
        ];
    }

    /**
     * PERF-003: Cleanup old/stale domain data to prevent state file growth
     *
     * Removes domain entries that:
     * - Have no recent activity (older than $maxAge seconds)
     * - Have been fully recovered (0 errors, default config)
     *
     * @param int $maxAge Maximum age in seconds (default: 24 hours)
     * @return array Statistics about cleanup ['removed' => int, 'kept' => int]
     */
    public function cleanup(int $maxAge = 86400): array
    {
        $removed = 0;
        $kept = 0;
        $now = time();

        // Get last update time from state file
        $stateFileTime = file_exists($this->storagePath)
            ? filemtime($this->storagePath)
            : $now;

        // If state file hasn't been updated in $maxAge, consider all entries stale
        $isStale = ($now - $stateFileTime) > $maxAge;

        // Track domains to remove
        $domainsToRemove = [];

        // Check each domain in configs
        foreach ($this->domainConfigs as $domain => $config) {
            $has429 = ($this->domain429Counts[$domain] ?? 0) > 0;
            $hasCustomConfig = $config !== $this->defaults;
            $inCooldown = $this->isInCooldown($domain);

            // Remove if: no 429s, default config, not in cooldown, and state is old
            if (!$has429 && !$hasCustomConfig && !$inCooldown && $isStale) {
                $domainsToRemove[] = $domain;
            } else {
                $kept++;
            }
        }

        // Check domains with 429 counts but no config
        foreach ($this->domain429Counts as $domain => $count) {
            if (!isset($this->domainConfigs[$domain])) {
                // If count is 0 and no activity, remove
                if ($count === 0 && $isStale) {
                    $domainsToRemove[] = $domain;
                }
            }
        }

        // Check domains with success counts
        foreach ($this->domainSuccessCounts as $domain => $count) {
            if (!isset($this->domainConfigs[$domain]) &&
                !isset($this->domain429Counts[$domain])) {
                // Orphan success count, safe to remove
                $domainsToRemove[] = $domain;
            }
        }

        // Clean expired cooldowns
        foreach ($this->domainCooldowns as $domain => $endTime) {
            if ($endTime < $now) {
                unset($this->domainCooldowns[$domain]);
            }
        }

        // Remove marked domains (deduplicate first)
        $domainsToRemove = array_unique($domainsToRemove);
        foreach ($domainsToRemove as $domain) {
            unset($this->domainConfigs[$domain]);
            unset($this->domain429Counts[$domain]);
            unset($this->domainSuccessCounts[$domain]);
            unset($this->domainCooldowns[$domain]);
            $removed++;
        }

        // Save state if changes were made
        if ($removed > 0) {
            $this->saveState();
            error_log("PERF-003: DomainRateLimiter cleanup - removed {$removed} stale entries, kept {$kept}");
        }

        return [
            'removed' => $removed,
            'kept' => $kept,
            'total_before' => $removed + $kept,
            'total_after' => $kept,
        ];
    }

    /**
     * PERF-003: Get statistics about current state file
     *
     * @return array State file statistics
     */
    public function getStateStats(): array
    {
        $configCount = count($this->domainConfigs);
        $cooldownCount = count($this->domainCooldowns);
        $count429Total = array_sum($this->domain429Counts);
        $successTotal = array_sum($this->domainSuccessCounts);

        $fileSize = file_exists($this->storagePath)
            ? filesize($this->storagePath)
            : 0;

        $lastModified = file_exists($this->storagePath)
            ? filemtime($this->storagePath)
            : null;

        return [
            'domains_tracked' => $configCount,
            'domains_in_cooldown' => $cooldownCount,
            'total_429_errors' => $count429Total,
            'total_successes' => $successTotal,
            'state_file_size_bytes' => $fileSize,
            'state_file_size_kb' => round($fileSize / 1024, 2),
            'last_modified' => $lastModified ? date('Y-m-d H:i:s', $lastModified) : null,
            'age_seconds' => $lastModified ? (time() - $lastModified) : null,
        ];
    }
}
