"""
InsAIts SDK - Agent Circuit Breaker
=====================================
Prevents cascading failures by temporarily blocking agents that
produce too many anomalies within a sliding window.

State machine:
    CLOSED  -- Normal operation. Recording anomaly rate.
      |
      v  (anomaly_rate > threshold)
    OPEN    -- Agent blocked. All messages rejected.
      |
      v  (recovery_seconds elapsed)
    HALF_OPEN -- Allow one trial message through.
      |
      v  (trial clean -> CLOSED, trial anomaly -> OPEN)

Parameters:
    window_size: Sliding window of recent messages (default 20)
    failure_threshold: Anomaly rate to trip breaker (default 0.4 = 40%)
    recovery_seconds: Wait time before HALF_OPEN trial (default 60)
"""

import time
from collections import defaultdict, deque
from typing import Any, Dict


# Circuit breaker states
STATE_CLOSED = "closed"
STATE_OPEN = "open"
STATE_HALF_OPEN = "half_open"


class AgentCircuitBreaker:
    """Sliding-window circuit breaker per agent.

    Monitors the anomaly rate for each agent and blocks agents
    that exceed the failure threshold, allowing recovery after
    a cooldown period.

    Usage:
        cb = AgentCircuitBreaker()

        # Before sending message
        if cb.is_open("agent1"):
            # Reject message -- agent is blocked
            ...

        # After processing message
        cb.record("agent1", had_anomaly=True)
    """

    def __init__(
        self,
        window_size: int = 20,
        failure_threshold: float = 0.4,
        recovery_seconds: float = 60.0,
    ):
        """Initialize the circuit breaker.

        Args:
            window_size: Number of recent messages to track per agent
            failure_threshold: Anomaly rate to trip the breaker (0.0-1.0)
            recovery_seconds: Seconds to wait before allowing a trial message
        """
        if window_size < 1:
            raise ValueError("window_size must be at least 1")
        if not 0.0 < failure_threshold <= 1.0:
            raise ValueError("failure_threshold must be in (0.0, 1.0]")
        if recovery_seconds < 0:
            raise ValueError("recovery_seconds must be non-negative")

        self._window_size = window_size
        self._failure_threshold = failure_threshold
        self._recovery_seconds = recovery_seconds

        # Per-agent sliding windows: deque of bools (True = anomaly)
        self._windows: Dict[str, deque] = defaultdict(
            lambda: deque(maxlen=self._window_size)
        )
        # Per-agent state
        self._states: Dict[str, str] = defaultdict(lambda: STATE_CLOSED)
        # Timestamp when breaker was tripped (for recovery timing)
        self._tripped_at: Dict[str, float] = {}

    def is_open(self, agent_id: str) -> bool:
        """Check if an agent is currently blocked.

        If the agent is in OPEN state and the recovery time has elapsed,
        transitions to HALF_OPEN to allow one trial message.

        Args:
            agent_id: The agent to check

        Returns:
            True if the agent is blocked (OPEN state)
        """
        state = self._states[agent_id]

        if state == STATE_CLOSED:
            return False

        if state == STATE_HALF_OPEN:
            return False

        # STATE_OPEN: check if recovery time has elapsed
        tripped_time = self._tripped_at.get(agent_id, 0)
        elapsed = time.time() - tripped_time
        if elapsed >= self._recovery_seconds:
            self._states[agent_id] = STATE_HALF_OPEN
            return False

        return True

    def record(self, agent_id: str, had_anomaly: bool) -> None:
        """Record a message result for an agent.

        Updates the sliding window and transitions state as needed.

        Args:
            agent_id: The agent that sent the message
            had_anomaly: Whether anomalies were detected
        """
        state = self._states[agent_id]

        if state == STATE_HALF_OPEN:
            if had_anomaly:
                # Trial failed: re-open
                self._states[agent_id] = STATE_OPEN
                self._tripped_at[agent_id] = time.time()
            else:
                # Trial succeeded: close
                self._states[agent_id] = STATE_CLOSED
                self._windows[agent_id].clear()
            return

        # CLOSED state: record and check threshold
        self._windows[agent_id].append(had_anomaly)
        rate = self._anomaly_rate(agent_id)

        if (
            state == STATE_CLOSED
            and len(self._windows[agent_id]) >= self._window_size
            and rate > self._failure_threshold
        ):
            self._states[agent_id] = STATE_OPEN
            self._tripped_at[agent_id] = time.time()

    def get_state(self, agent_id: str) -> Dict[str, Any]:
        """Get full state info for an agent.

        Args:
            agent_id: The agent to query

        Returns:
            Dict with state, anomaly_rate, window_size, etc.
        """
        window = self._windows.get(agent_id, deque())
        state = self._states[agent_id]
        rate = self._anomaly_rate(agent_id)

        result: Dict[str, Any] = {
            "agent_id": agent_id,
            "state": state,
            "anomaly_rate": round(rate, 3),
            "window_fill": len(window),
            "window_size": self._window_size,
            "failure_threshold": self._failure_threshold,
        }

        if state == STATE_OPEN:
            tripped = self._tripped_at.get(agent_id, 0)
            remaining = max(
                0.0,
                self._recovery_seconds - (time.time() - tripped),
            )
            result["recovery_remaining_seconds"] = round(remaining, 1)

        return result

    def reset(self, agent_id: str) -> None:
        """Manually reset an agent's circuit breaker to CLOSED.

        Args:
            agent_id: The agent to reset
        """
        self._states[agent_id] = STATE_CLOSED
        self._windows[agent_id].clear()
        self._tripped_at.pop(agent_id, None)

    def reset_all(self) -> None:
        """Reset all agent circuit breakers."""
        self._windows.clear()
        self._states.clear()
        self._tripped_at.clear()

    def _anomaly_rate(self, agent_id: str) -> float:
        """Calculate the anomaly rate for an agent's sliding window."""
        window = self._windows.get(agent_id)
        if not window:
            return 0.0
        return sum(1 for x in window if x) / len(window)
