<?php

namespace WPSN\Core\Http;

defined('ABSPATH') || exit;

/**
 * Best-effort transient-backed circuit breaker for external HTTP calls.
 *
 * Goal: avoid retry storms / repeated 503 loops on shared hosting.
 *
 * This is intentionally simple:
 * - Track consecutive failures.
 * - Open the circuit for a short window after N failures or after Retry-After.
 * - Reset on success.
 */
final class CircuitBreaker
{
    private const TRANSIENT_PREFIX = 'wpsn_http_cb_';
    private const REGISTRY_OPTION = 'wpsn_http_cb_registry';

    private string $key;
    private int $failThreshold;
    private int $openSeconds;

    public function __construct(string $key, int $failThreshold = 3, int $openSeconds = 300)
    {
        $key = sanitize_key($key);
        if ($key === '') {
            $key = 'default';
        }
        $this->key = $key;
        $this->failThreshold = max(1, (int) $failThreshold);
        $this->openSeconds = max(10, (int) $openSeconds);

        $this->registerKey();
    }

    /**
     * @return array{open:bool,retry_after:int,state:array<string,mixed>}
     */
    public function status(): array
    {
        $state = $this->getState();
        $now = time();
        $openUntil = isset($state['open_until']) ? (int) $state['open_until'] : 0;
        $open = $openUntil > $now;
        $retryAfter = $open ? max(1, $openUntil - $now) : 0;

        return [
            'open' => $open,
            'retry_after' => $retryAfter,
            'state' => $state,
        ];
    }

    public function reportSuccess(): void
    {
        $this->setState([
            'fails' => 0,
            'open_until' => 0,
            'last_status' => 0,
            'last_error' => '',
            'last_ts' => time(),
        ], 3600);
    }

    public function reportFailure(int $httpStatus, string $errorCode = '', int $retryAfterSeconds = 0): void
    {
        $state = $this->getState();
        $now = time();

        $fails = isset($state['fails']) ? (int) $state['fails'] : 0;
        $lastTs = isset($state['last_ts']) ? (int) $state['last_ts'] : 0;

        // Decay failure counter if last failure was long ago.
        if ($lastTs > 0 && ($now - $lastTs) > 900) { // 15 minutes
            $fails = 0;
        }

        $fails++;

        $openUntil = 0;

        // If Retry-After is provided (429/503), open circuit for that window.
        $retryAfterSeconds = max(0, (int) $retryAfterSeconds);
        if ($retryAfterSeconds > 0) {
            $openUntil = $now + min(3600, $retryAfterSeconds);
        }

        // Trip circuit after N failures (only for transient-ish statuses).
        $transientStatus = ($httpStatus === 0) || ($httpStatus === 429) || ($httpStatus >= 500);
        if ($openUntil === 0 && $transientStatus && $fails >= $this->failThreshold) {
            $openUntil = $now + $this->openSeconds;
        }

        $this->setState([
            'fails' => $fails,
            'open_until' => $openUntil,
            'last_status' => (int) $httpStatus,
            'last_error' => sanitize_key($errorCode),
            'last_ts' => $now,
        ], max(3600, $this->openSeconds));
    }

    /**
     * @return array<string,mixed>
     */
    private function getState(): array
    {
        $state = get_transient(self::TRANSIENT_PREFIX . $this->key);
        if (!is_array($state)) {
            return [
                'fails' => 0,
                'open_until' => 0,
                'last_status' => 0,
                'last_error' => '',
                'last_ts' => 0,
            ];
        }
        return $state;
    }

    /**
     * @param array<string,mixed> $state
     */
    private function setState(array $state, int $ttlSeconds): void
    {
        set_transient(self::TRANSIENT_PREFIX . $this->key, $state, max(60, $ttlSeconds));
    }

    private function registerKey(): void
    {
        // Keep a small registry for diagnostics.
        $list = get_option(self::REGISTRY_OPTION);
        if (!is_array($list)) {
            $list = [];
        }

        if (!in_array($this->key, $list, true)) {
            $list[] = $this->key;
        }

        // Cap registry size.
        if (count($list) > 80) {
            $list = array_slice($list, -80);
        }

        update_option(self::REGISTRY_OPTION, array_values($list), false);
    }

    /**
     * @return array<int,string>
     */
    public static function registry(): array
    {
        $list = get_option(self::REGISTRY_OPTION);
        if (!is_array($list)) {
            return [];
        }
        $out = [];
        foreach ($list as $k) {
            $kk = sanitize_key((string) $k);
            if ($kk !== '') {
                $out[] = $kk;
            }
        }
        return array_values(array_unique($out));
    }

    /**
     * @return array<string,mixed>
     */
    public static function getStateForKey(string $key): array
    {
        $key = sanitize_key($key);
        if ($key === '') {
            return [];
        }
        $state = get_transient(self::TRANSIENT_PREFIX . $key);
        return is_array($state) ? $state : [];
    }

    public static function clear(string $key): void
    {
        $key = sanitize_key($key);
        if ($key === '') {
            return;
        }
        delete_transient(self::TRANSIENT_PREFIX . $key);
    }
}
