"""Reliability primitives for retries, circuit breakers and health reporting."""
from __future__ import annotations

import asyncio
from dataclasses import dataclass
import time
from typing import Any, Awaitable, Callable

from logos_adk.errors import CircuitBreakerOpenError


class CircuitBreaker:
    """Basic circuit breaker with half-open recovery."""

    def __init__(self, *, failure_threshold: int = 5, recovery_timeout: float = 30.0) -> None:
        self.failure_threshold = max(1, failure_threshold)
        self.recovery_timeout = max(0.1, recovery_timeout)
        self.failures = 0
        self.last_failure_at = 0.0
        self.state = "closed"

    def allow_request(self) -> bool:
        if self.state == "closed":
            return True
        if self.state == "open" and (time.time() - self.last_failure_at) >= self.recovery_timeout:
            self.state = "half_open"
            return True
        return self.state == "half_open"

    def record_success(self) -> None:
        self.failures = 0
        self.state = "closed"

    def record_failure(self) -> None:
        self.failures += 1
        self.last_failure_at = time.time()
        if self.failures >= self.failure_threshold:
            self.state = "open"


@dataclass
class RetryPolicy:
    """Retry policy with fixed or exponential backoff."""

    max_attempts: int = 3
    base_delay: float = 0.25
    backoff: str = "exponential"
    retry_on: tuple[type[BaseException], ...] = (Exception,)

    async def call(self, operation: Callable[[], Awaitable[Any]]) -> Any:
        last_error: BaseException | None = None
        for attempt in range(1, self.max_attempts + 1):
            try:
                return await operation()
            except self.retry_on as exc:
                last_error = exc
                if attempt >= self.max_attempts:
                    break
                await asyncio.sleep(self._delay_for_attempt(attempt, exc))
        if last_error is None:
            raise RuntimeError("RetryPolicy failed without capturing an exception")
        raise last_error

    def _delay_for_attempt(self, attempt: int, exc: BaseException) -> float:
        retry_after = getattr(exc, "retry_after", None)
        if isinstance(retry_after, (int, float)) and retry_after > 0:
            return float(retry_after)
        if self.backoff == "fixed":
            return self.base_delay
        return self.base_delay * (2 ** (attempt - 1))


@dataclass
class ProviderHealth:
    """Current health snapshot for a provider."""

    provider: str
    circuit_state: str
    failures: int
    healthy: bool


def ensure_circuit_allows(circuit_breaker: CircuitBreaker, provider_name: str) -> None:
    """Raise if the circuit breaker is open."""
    if not circuit_breaker.allow_request():
        raise CircuitBreakerOpenError(f"Circuit breaker is open for provider {provider_name}")


__all__ = [
    "CircuitBreaker",
    "ProviderHealth",
    "RetryPolicy",
    "ensure_circuit_allows",
]
