from __future__ import annotations

import asyncio
from datetime import datetime, timezone
from enum import Enum
from typing import TypeVar

from nexus.core.events.bus import EventBus
from nexus.core.events.types import CircuitCloseEvent, CircuitOpenEvent
from nexus.resilience.errors import ProviderError

T = TypeVar("T")


class CircuitState(str, Enum):
    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"


class CircuitBreaker:
    """Circuit breaker with CLOSED -> OPEN -> HALF_OPEN transitions."""

    def __init__(
        self,
        target_id: str,
        bus: EventBus,
        failure_threshold: int = 5,
        recovery_timeout: float = 30.0,
        half_open_max_calls: int = 1,
    ) -> None:
        self.target_id = target_id
        self.bus = bus
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.half_open_max_calls = half_open_max_calls
        self._failure_count = 0
        self._state = CircuitState.CLOSED
        self._last_failure_time: datetime | None = None
        self._half_open_calls = 0

    async def call(self, fn, *args, **kwargs) -> T:
        """Execute protected async call under circuit breaker policy."""
        now = datetime.now(timezone.utc)

        if self._state == CircuitState.OPEN:
            if self._last_failure_time is None:
                raise ProviderError("circuit open", provider_name=self.target_id)
            elapsed = (now - self._last_failure_time).total_seconds()
            if elapsed < self.recovery_timeout:
                raise ProviderError("circuit open", provider_name=self.target_id)
            self._state = CircuitState.HALF_OPEN
            self._half_open_calls = 0

        if self._state == CircuitState.HALF_OPEN:
            if self._half_open_calls >= self.half_open_max_calls:
                raise ProviderError("half-open call limit reached", provider_name=self.target_id)
            self._half_open_calls += 1

        try:
            result = await fn(*args, **kwargs)
            if self._state == CircuitState.HALF_OPEN:
                latency_ms = (datetime.now(timezone.utc) - (self._last_failure_time or now)).total_seconds() * 1000.0
                self._state = CircuitState.CLOSED
                self._failure_count = 0
                await self.bus.publish_and_wait(
                    CircuitCloseEvent(
                        source_id="circuit_breaker",
                        target_id=self.target_id,
                        recovery_latency_ms=latency_ms,
                    )
                )
            return result
        except Exception as exc:
            self._failure_count += 1
            self._last_failure_time = datetime.now(timezone.utc)
            if self._failure_count >= self.failure_threshold or self._state == CircuitState.HALF_OPEN:
                self._state = CircuitState.OPEN
                await self.bus.publish_and_wait(
                    CircuitOpenEvent(
                        source_id="circuit_breaker",
                        target_id=self.target_id,
                        target_type="provider_or_tool",
                        failure_count=self._failure_count,
                    )
                )
            raise exc

    def reset(self) -> None:
        """Force breaker to CLOSED state."""
        self._state = CircuitState.CLOSED
        self._failure_count = 0
        self._half_open_calls = 0
        self._last_failure_time = None

    @property
    def state(self) -> CircuitState:
        return self._state

    @property
    def failure_count(self) -> int:
        return self._failure_count
