<?php

declare(strict_types=1);

namespace App\Framework\CircuitBreaker\FailurePredicate;

use App\Framework\HttpClient\Exception\ClientErrorException;
use App\Framework\HttpClient\Exception\ServerErrorException;
use Throwable;

/**
 * Predicate that matches HTTP status codes
 */
final readonly class HttpStatusFailurePredicate implements FailurePredicate
{
    /**
     * @param array<int> $includedStatusCodes
     * @param array<int> $excludedStatusCodes
     */
    public function __construct(
        private array $includedStatusCodes = [],
        private array $excludedStatusCodes = [],
        private bool $includeServerErrors = true,
        private bool $includeClientErrors = false,
    ) {
    }

    public function shouldTrigger(Throwable $exception, string $service): bool
    {
        $statusCode = $this->extractStatusCode($exception);

        if ($statusCode === null) {
            // Not an HTTP exception, use default behavior
            return false;
        }

        // Check excluded status codes first
        if (in_array($statusCode, $this->excludedStatusCodes, true)) {
            return false;
        }

        // Check included status codes
        if (! empty($this->includedStatusCodes)) {
            return in_array($statusCode, $this->includedStatusCodes, true);
        }

        // Default behavior based on status code ranges
        if ($statusCode >= 500 && $statusCode < 600) {
            return $this->includeServerErrors;
        }

        if ($statusCode >= 400 && $statusCode < 500) {
            return $this->includeClientErrors;
        }

        return false;
    }

    public function getIdentifier(): string
    {
        return 'http_status';
    }

    public function getDescription(): string
    {
        $parts = [];

        if (! empty($this->includedStatusCodes)) {
            $parts[] = 'Include: ' . implode(', ', $this->includedStatusCodes);
        } else {
            $ranges = [];
            if ($this->includeServerErrors) {
                $ranges[] = '5xx';
            }
            if ($this->includeClientErrors) {
                $ranges[] = '4xx';
            }
            if (! empty($ranges)) {
                $parts[] = 'Include: ' . implode(', ', $ranges);
            }
        }

        if (! empty($this->excludedStatusCodes)) {
            $parts[] = 'Exclude: ' . implode(', ', $this->excludedStatusCodes);
        }

        return empty($parts) ? 'No HTTP status matching' : implode(', ', $parts);
    }

    /**
     * Create predicate for specific status codes
     *
     * @param array<int> $statusCodes
     */
    public static function forStatusCodes(array $statusCodes): self
    {
        return new self(includedStatusCodes: $statusCodes);
    }

    /**
     * Create predicate for server errors (5xx)
     */
    public static function serverErrors(): self
    {
        return new self(includeServerErrors: true, includeClientErrors: false);
    }

    /**
     * Create predicate for client errors (4xx)
     */
    public static function clientErrors(): self
    {
        return new self(includeServerErrors: false, includeClientErrors: true);
    }

    /**
     * Create predicate for both client and server errors
     */
    public static function allHttpErrors(): self
    {
        return new self(includeServerErrors: true, includeClientErrors: true);
    }

    /**
     * Create predicate excluding specific status codes
     *
     * @param array<int> $statusCodes
     */
    public static function except(array $statusCodes): self
    {
        return new self(
            excludedStatusCodes: $statusCodes,
            includeServerErrors: true,
            includeClientErrors: true
        );
    }

    private function extractStatusCode(Throwable $exception): ?int
    {
        if ($exception instanceof ServerErrorException || $exception instanceof ClientErrorException) {
            return method_exists($exception, 'getStatusCode') ? $exception->getStatusCode() : null;
        }

        // Try to extract from exception message (fallback)
        if (preg_match('/HTTP (\d{3})/', $exception->getMessage(), $matches)) {
            return (int) $matches[1];
        }

        return null;
    }
}
