"""
Circuit Breaker Service

Implements the circuit breaker pattern for fault tolerance.
Protects services from cascading failures by tracking error rates
and temporarily blocking requests to failing services.

States:
- CLOSED: Normal operation, requests flow through
- OPEN: Circuit tripped, requests blocked
- HALF_OPEN: Testing if service recovered
"""

import asyncio
import json
import time
from collections import deque
from datetime import datetime, timedelta, timezone
from typing import Any, Callable

import redis.asyncio as redis

from ..self_healing_types import (
    CircuitBreakerConfig,
    CircuitBreakerEvent,
    CircuitBreakerState,
    CircuitBreakerStats,
    CircuitState,
    TripReason,
)


class CircuitBreaker:
    """
    Circuit Breaker implementation with Redis-backed state.

    Usage:
        breaker = CircuitBreaker("api_service", config)
        await breaker.initialize()

        # Wrap a call
        result = await breaker.call(async_function, arg1, arg2)

        # Or manual tracking
        if await breaker.allow_request():
            try:
                result = await do_something()
                await breaker.record_success(latency_ms)
            except Exception as e:
                await breaker.record_failure(str(e))
    """

    def __init__(
        self,
        name: str,
        config: CircuitBreakerConfig | None = None,
        redis_client: redis.Redis | None = None,
    ):
        """Initialize circuit breaker"""
        self.name = name
        self.config = config or CircuitBreakerConfig(name=name)
        self._redis = redis_client

        # In-memory state for fast access
        self._state = CircuitState.CLOSED
        self._trip_reason: TripReason | None = None
        self._tripped_at: datetime | None = None
        self._next_retry_at: datetime | None = None

        # Rolling window for stats
        self._request_window: deque[dict[str, Any]] = deque(maxlen=1000)
        self._half_open_requests = 0

        # Event handlers
        self._event_handlers: list[Callable[[CircuitBreakerEvent], Any]] = []

    async def initialize(self) -> None:
        """Initialize and load state from Redis"""
        if self._redis:
            await self._load_state()

    async def shutdown(self) -> None:
        """Save state before shutdown"""
        if self._redis:
            await self._save_state()

    # =========================================================================
    # Public API
    # =========================================================================

    async def call(
        self,
        func: Callable,
        *args: Any,
        **kwargs: Any,
    ) -> Any:
        """
        Execute a function with circuit breaker protection.

        Args:
            func: Async function to execute
            *args: Positional arguments
            **kwargs: Keyword arguments

        Returns:
            Result of the function

        Raises:
            CircuitOpenError: If circuit is open
            Original exception: If function fails
        """
        if not await self.allow_request():
            raise CircuitOpenError(
                f"Circuit '{self.name}' is open. "
                f"Retry after: {self._next_retry_at}"
            )

        start_time = time.time()

        try:
            result = await func(*args, **kwargs)
            latency_ms = (time.time() - start_time) * 1000
            await self.record_success(latency_ms)
            return result

        except asyncio.TimeoutError:
            latency_ms = (time.time() - start_time) * 1000
            await self.record_timeout(latency_ms)
            raise

        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            await self.record_failure(str(e), latency_ms)
            raise

    async def allow_request(self) -> bool:
        """
        Check if a request should be allowed through.

        Returns:
            True if request allowed, False if blocked
        """
        now = datetime.now(timezone.utc)

        if self._state == CircuitState.CLOSED:
            return True

        elif self._state == CircuitState.OPEN:
            # Check if recovery timeout has passed
            if self._next_retry_at and now >= self._next_retry_at:
                await self._transition_to(CircuitState.HALF_OPEN)
                self._half_open_requests = 0
                return True
            return False

        elif self._state == CircuitState.HALF_OPEN:
            # Allow limited requests for testing
            if self._half_open_requests < self.config.half_open_max_requests:
                self._half_open_requests += 1
                return True
            return False

        return False

    async def record_success(self, latency_ms: float = 0.0) -> None:
        """Record a successful request"""
        now = datetime.now(timezone.utc)

        self._request_window.append({
            "timestamp": now,
            "success": True,
            "timeout": False,
            "latency_ms": latency_ms,
        })

        if self._state == CircuitState.HALF_OPEN:
            # Check if we should close the circuit
            recent_requests = self._get_recent_requests(
                seconds=self.config.recovery_timeout_seconds
            )
            successes = sum(1 for r in recent_requests if r["success"])

            if successes >= self.config.half_open_max_requests:
                await self._transition_to(CircuitState.CLOSED)
                self._trip_reason = None

        await self._save_state()

    async def record_failure(
        self,
        error: str = "",
        latency_ms: float = 0.0,
    ) -> None:
        """Record a failed request"""
        now = datetime.now(timezone.utc)

        self._request_window.append({
            "timestamp": now,
            "success": False,
            "timeout": False,
            "latency_ms": latency_ms,
            "error": error,
        })

        await self._evaluate_circuit()
        await self._save_state()

    async def record_timeout(self, latency_ms: float = 0.0) -> None:
        """Record a timeout"""
        now = datetime.now(timezone.utc)

        self._request_window.append({
            "timestamp": now,
            "success": False,
            "timeout": True,
            "latency_ms": latency_ms,
        })

        await self._evaluate_circuit()
        await self._save_state()

    async def trip(self, reason: TripReason = TripReason.MANUAL) -> None:
        """Manually trip the circuit breaker"""
        await self._trip_circuit(reason)

    async def reset(self) -> None:
        """Manually reset the circuit breaker"""
        await self._transition_to(CircuitState.CLOSED)
        self._trip_reason = None
        self._tripped_at = None
        self._next_retry_at = None
        await self._save_state()

    def get_state(self) -> CircuitBreakerState:
        """Get current circuit breaker state"""
        return CircuitBreakerState(
            name=self.name,
            state=self._state,
            trip_reason=self._trip_reason,
            tripped_at=self._tripped_at,
            stats=self._calculate_stats(),
            config=self.config,
            next_retry_at=self._next_retry_at,
        )

    def on_state_change(
        self,
        handler: Callable[[CircuitBreakerEvent], Any],
    ) -> None:
        """Register event handler for state changes"""
        self._event_handlers.append(handler)

    # =========================================================================
    # Internal Methods
    # =========================================================================

    async def _evaluate_circuit(self) -> None:
        """Evaluate if circuit should trip"""
        if self._state == CircuitState.OPEN:
            return

        if self._state == CircuitState.HALF_OPEN:
            # Any failure in half-open trips immediately
            await self._trip_circuit(TripReason.ERROR_RATE)
            return

        # Get recent requests in window
        recent = self._get_recent_requests(self.config.window_size_seconds)

        if len(recent) < self.config.min_requests:
            return  # Not enough data

        total = len(recent)
        failures = sum(1 for r in recent if not r["success"])
        timeouts = sum(1 for r in recent if r["timeout"])
        latencies = [r["latency_ms"] for r in recent if r["latency_ms"] > 0]

        # Check error rate
        error_rate = failures / total
        if error_rate >= self.config.error_threshold:
            await self._trip_circuit(TripReason.ERROR_RATE)
            return

        # Check timeout rate
        timeout_rate = timeouts / total
        if timeout_rate >= self.config.timeout_threshold:
            await self._trip_circuit(TripReason.TIMEOUT_RATE)
            return

        # Check latency
        if latencies:
            avg_latency = sum(latencies) / len(latencies)
            if avg_latency >= self.config.latency_threshold_ms:
                await self._trip_circuit(TripReason.LATENCY_SPIKE)
                return

    async def _trip_circuit(self, reason: TripReason) -> None:
        """Trip the circuit breaker"""
        old_state = self._state
        self._state = CircuitState.OPEN
        self._trip_reason = reason
        self._tripped_at = datetime.now(timezone.utc)
        self._next_retry_at = datetime.now(timezone.utc) + timedelta(
            seconds=self.config.recovery_timeout_seconds
        )

        await self._emit_event(CircuitBreakerEvent(
            circuit_name=self.name,
            event_type="trip",
            old_state=old_state,
            new_state=CircuitState.OPEN,
            reason=reason.value,
        ))

        await self._save_state()

    async def _transition_to(self, new_state: CircuitState) -> None:
        """Transition to a new state"""
        old_state = self._state
        self._state = new_state

        await self._emit_event(CircuitBreakerEvent(
            circuit_name=self.name,
            event_type="state_change",
            old_state=old_state,
            new_state=new_state,
        ))

    async def _emit_event(self, event: CircuitBreakerEvent) -> None:
        """Emit event to all handlers"""
        for handler in self._event_handlers:
            try:
                result = handler(event)
                if asyncio.iscoroutine(result):
                    await result
            except Exception:
                pass  # Don't let handler errors affect circuit

        # Also store in Redis for persistence
        if self._redis:
            event_key = f"snbg:circuit:events:{self.name}"
            await self._redis.lpush(event_key, event.model_dump_json())
            await self._redis.ltrim(event_key, 0, 999)  # Keep last 1000 events

    def _get_recent_requests(self, seconds: int) -> list[dict[str, Any]]:
        """Get requests within time window"""
        cutoff = datetime.now(timezone.utc) - timedelta(seconds=seconds)
        return [
            r for r in self._request_window
            if r["timestamp"] >= cutoff
        ]

    def _calculate_stats(self) -> CircuitBreakerStats:
        """Calculate current statistics"""
        recent = self._get_recent_requests(self.config.window_size_seconds)

        if not recent:
            return CircuitBreakerStats()

        total = len(recent)
        successes = sum(1 for r in recent if r["success"])
        failures = sum(1 for r in recent if not r["success"] and not r["timeout"])
        timeouts = sum(1 for r in recent if r["timeout"])
        latencies = [r["latency_ms"] for r in recent if r["latency_ms"] > 0]

        # Calculate consecutive counts
        consecutive_failures = 0
        consecutive_successes = 0

        for r in reversed(list(recent)):
            if r["success"]:
                if consecutive_failures == 0:
                    consecutive_successes += 1
                else:
                    break
            else:
                if consecutive_successes == 0:
                    consecutive_failures += 1
                else:
                    break

        return CircuitBreakerStats(
            total_requests=total,
            successful_requests=successes,
            failed_requests=failures,
            timeout_requests=timeouts,
            avg_latency_ms=sum(latencies) / len(latencies) if latencies else 0.0,
            p99_latency_ms=sorted(latencies)[int(len(latencies) * 0.99)] if len(latencies) > 1 else (latencies[0] if latencies else 0.0),
            last_failure_time=next(
                (r["timestamp"] for r in reversed(list(recent)) if not r["success"]),
                None
            ),
            last_success_time=next(
                (r["timestamp"] for r in reversed(list(recent)) if r["success"]),
                None
            ),
            consecutive_failures=consecutive_failures,
            consecutive_successes=consecutive_successes,
        )

    async def _load_state(self) -> None:
        """Load state from Redis"""
        if not self._redis:
            return

        key = f"snbg:circuit:state:{self.name}"
        data = await self._redis.get(key)

        if data:
            state_dict = json.loads(data)
            self._state = CircuitState(state_dict.get("state", "closed"))
            self._trip_reason = (
                TripReason(state_dict["trip_reason"])
                if state_dict.get("trip_reason")
                else None
            )
            self._tripped_at = (
                datetime.fromisoformat(state_dict["tripped_at"])
                if state_dict.get("tripped_at")
                else None
            )
            self._next_retry_at = (
                datetime.fromisoformat(state_dict["next_retry_at"])
                if state_dict.get("next_retry_at")
                else None
            )

    async def _save_state(self) -> None:
        """Save state to Redis"""
        if not self._redis:
            return

        key = f"snbg:circuit:state:{self.name}"
        state_dict = {
            "state": self._state.value,
            "trip_reason": self._trip_reason.value if self._trip_reason else None,
            "tripped_at": self._tripped_at.isoformat() if self._tripped_at else None,
            "next_retry_at": self._next_retry_at.isoformat() if self._next_retry_at else None,
        }
        await self._redis.set(key, json.dumps(state_dict), ex=86400 * 7)  # 7 days


class CircuitOpenError(Exception):
    """Raised when circuit is open and request is blocked"""

    pass


class CircuitBreakerRegistry:
    """
    Registry for managing multiple circuit breakers.

    Usage:
        registry = CircuitBreakerRegistry(redis_client)
        await registry.initialize()

        breaker = await registry.get_or_create("api_service", config)
        result = await breaker.call(my_function)
    """

    def __init__(self, redis_client: redis.Redis | None = None):
        """Initialize registry"""
        self._redis = redis_client
        self._breakers: dict[str, CircuitBreaker] = {}

    async def initialize(self) -> None:
        """Initialize all breakers from Redis"""
        if not self._redis:
            return

        # Load all circuit breaker keys
        keys = await self._redis.keys("snbg:circuit:state:*")
        for key in keys:
            name = key.split(":")[-1]
            if name not in self._breakers:
                breaker = CircuitBreaker(name, redis_client=self._redis)
                await breaker.initialize()
                self._breakers[name] = breaker

    async def get_or_create(
        self,
        name: str,
        config: CircuitBreakerConfig | None = None,
    ) -> CircuitBreaker:
        """Get existing or create new circuit breaker"""
        if name not in self._breakers:
            breaker = CircuitBreaker(
                name,
                config=config or CircuitBreakerConfig(name=name),
                redis_client=self._redis,
            )
            await breaker.initialize()
            self._breakers[name] = breaker

        return self._breakers[name]

    def get(self, name: str) -> CircuitBreaker | None:
        """Get circuit breaker by name"""
        return self._breakers.get(name)

    def list_all(self) -> list[CircuitBreakerState]:
        """List all circuit breaker states"""
        return [b.get_state() for b in self._breakers.values()]

    def list_open(self) -> list[CircuitBreakerState]:
        """List all open circuit breakers"""
        return [
            b.get_state()
            for b in self._breakers.values()
            if b._state == CircuitState.OPEN
        ]

    async def reset_all(self) -> None:
        """Reset all circuit breakers"""
        for breaker in self._breakers.values():
            await breaker.reset()
