<?php
/**
 * ===========================================
 * FLOWBOT DCI - CIRCUIT BREAKER SERVICE
 * ===========================================
 * BACK-003: Implements circuit breaker pattern for domain requests
 * Prevents hammering domains that are failing
 */

declare(strict_types=1);

namespace FlowbotDCI\Services;

class CircuitBreaker
{
    // Circuit states
    const STATE_CLOSED = 'closed';     // Normal operation
    const STATE_OPEN = 'open';         // Failing, reject requests
    const STATE_HALF_OPEN = 'half_open'; // Testing if recovered

    // Configuration
    private int $failureThreshold;      // Failures before opening
    private int $successThreshold;      // Successes needed to close
    private int $cooldownPeriod;        // Seconds before half-open
    private int $halfOpenMaxRequests;   // Max requests in half-open

    // State storage per domain
    private array $circuits = [];

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

    public function __construct(
        int $failureThreshold = 5,
        int $successThreshold = 2,
        int $cooldownPeriod = 60,
        int $halfOpenMaxRequests = 3
    ) {
        $this->failureThreshold = $failureThreshold;
        $this->successThreshold = $successThreshold;
        $this->cooldownPeriod = $cooldownPeriod;
        $this->halfOpenMaxRequests = $halfOpenMaxRequests;
    }

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

    /**
     * Check if a request to a domain is allowed
     * @param string $domain Domain to check
     * @return bool True if request is allowed
     */
    public function isAllowed(string $domain): bool
    {
        $domain = $this->normalizeDomain($domain);
        $circuit = $this->getCircuit($domain);

        switch ($circuit['state']) {
            case self::STATE_CLOSED:
                return true;

            case self::STATE_OPEN:
                // Check if cooldown period has passed
                if (time() - $circuit['last_failure_time'] >= $this->cooldownPeriod) {
                    $this->transitionToHalfOpen($domain);
                    return true;
                }
                return false;

            case self::STATE_HALF_OPEN:
                // Allow limited requests in half-open state
                return $circuit['half_open_requests'] < $this->halfOpenMaxRequests;

            default:
                return true;
        }
    }

    /**
     * Record a successful request
     * @param string $domain Domain that succeeded
     */
    public function recordSuccess(string $domain): void
    {
        $domain = $this->normalizeDomain($domain);
        $circuit = $this->getCircuit($domain);

        if ($circuit['state'] === self::STATE_HALF_OPEN) {
            $circuit['half_open_successes']++;

            // If enough successes, close the circuit
            if ($circuit['half_open_successes'] >= $this->successThreshold) {
                $this->transitionToClosed($domain);
            } else {
                $this->circuits[$domain] = $circuit;
            }
        } elseif ($circuit['state'] === self::STATE_CLOSED) {
            // Reset failure count on success
            $circuit['failure_count'] = 0;
            $this->circuits[$domain] = $circuit;
        }

        $this->saveState();
    }

    /**
     * Record a failed request
     * @param string $domain Domain that failed
     * @param string|null $errorType Type of error (optional)
     */
    public function recordFailure(string $domain, ?string $errorType = null): void
    {
        $domain = $this->normalizeDomain($domain);
        $circuit = $this->getCircuit($domain);

        $circuit['failure_count']++;
        $circuit['last_failure_time'] = time();
        $circuit['last_error_type'] = $errorType;

        if ($circuit['state'] === self::STATE_HALF_OPEN) {
            // Any failure in half-open state opens the circuit again
            $this->transitionToOpen($domain);
        } elseif ($circuit['state'] === self::STATE_CLOSED) {
            // Check if threshold reached
            if ($circuit['failure_count'] >= $this->failureThreshold) {
                $this->transitionToOpen($domain);
            } else {
                $this->circuits[$domain] = $circuit;
            }
        }

        $this->saveState();
    }

    /**
     * Get the current state of a circuit
     * @param string $domain Domain to check
     * @return string Circuit state
     */
    public function getState(string $domain): string
    {
        $domain = $this->normalizeDomain($domain);
        $circuit = $this->getCircuit($domain);
        return $circuit['state'];
    }

    /**
     * Get all circuits and their states
     * @return array All circuit states
     */
    public function getAllCircuits(): array
    {
        return $this->circuits;
    }

    /**
     * Get domains with open circuits
     * @return array List of blocked domains
     */
    public function getOpenCircuits(): array
    {
        $open = [];
        foreach ($this->circuits as $domain => $circuit) {
            if ($circuit['state'] === self::STATE_OPEN) {
                $open[$domain] = [
                    'failure_count' => $circuit['failure_count'],
                    'last_failure_time' => $circuit['last_failure_time'],
                    'last_error_type' => $circuit['last_error_type'],
                    'time_until_retry' => max(0, $this->cooldownPeriod - (time() - $circuit['last_failure_time'])),
                ];
            }
        }
        return $open;
    }

    /**
     * Manually reset a circuit
     * @param string $domain Domain to reset
     */
    public function reset(string $domain): void
    {
        $domain = $this->normalizeDomain($domain);
        $this->transitionToClosed($domain);
        $this->saveState();
    }

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

    /**
     * Get circuit statistics
     * @return array Statistics about all circuits
     */
    public function getStats(): array
    {
        $stats = [
            'total' => count($this->circuits),
            'closed' => 0,
            'open' => 0,
            'half_open' => 0,
            'most_failures' => null,
        ];

        $maxFailures = 0;
        foreach ($this->circuits as $domain => $circuit) {
            $stats[$circuit['state']]++;
            if ($circuit['failure_count'] > $maxFailures) {
                $maxFailures = $circuit['failure_count'];
                $stats['most_failures'] = [
                    'domain' => $domain,
                    'failures' => $circuit['failure_count'],
                ];
            }
        }

        return $stats;
    }

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

    /**
     * Get or create circuit for domain
     */
    private function getCircuit(string $domain): array
    {
        if (!isset($this->circuits[$domain])) {
            $this->circuits[$domain] = [
                'state' => self::STATE_CLOSED,
                'failure_count' => 0,
                'last_failure_time' => 0,
                'last_error_type' => null,
                'half_open_requests' => 0,
                'half_open_successes' => 0,
            ];
        }
        return $this->circuits[$domain];
    }

    /**
     * Transition circuit to open state
     */
    private function transitionToOpen(string $domain): void
    {
        $circuit = $this->getCircuit($domain);
        $circuit['state'] = self::STATE_OPEN;
        $circuit['last_failure_time'] = time();
        $this->circuits[$domain] = $circuit;

        error_log("CircuitBreaker: Circuit OPENED for {$domain} after {$circuit['failure_count']} failures");
    }

    /**
     * Transition circuit to half-open state
     */
    private function transitionToHalfOpen(string $domain): void
    {
        $circuit = $this->getCircuit($domain);
        $circuit['state'] = self::STATE_HALF_OPEN;
        $circuit['half_open_requests'] = 0;
        $circuit['half_open_successes'] = 0;
        $this->circuits[$domain] = $circuit;

        error_log("CircuitBreaker: Circuit HALF-OPEN for {$domain} - testing recovery");
    }

    /**
     * Transition circuit to closed state
     */
    private function transitionToClosed(string $domain): void
    {
        $this->circuits[$domain] = [
            'state' => self::STATE_CLOSED,
            'failure_count' => 0,
            'last_failure_time' => 0,
            'last_error_type' => null,
            'half_open_requests' => 0,
            'half_open_successes' => 0,
        ];

        error_log("CircuitBreaker: Circuit CLOSED for {$domain} - back to normal");
    }

    /**
     * Normalize domain name
     */
    private function normalizeDomain(string $domain): string
    {
        // Remove protocol if present
        $domain = preg_replace('/^https?:\/\//', '', $domain);
        // Remove path if present
        $domain = explode('/', $domain)[0];
        // Remove www prefix
        $domain = preg_replace('/^www\./', '', $domain);
        // Lowercase
        return strtolower($domain);
    }

    /**
     * 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->circuits = $state;
                }
            }
        }
    }

    /**
     * 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($this->circuits, JSON_PRETTY_PRINT),
                LOCK_EX
            );
        }
    }
}
