"""Context monitoring for agent token usage tracking."""

import asyncio
import logging
from collections import defaultdict
from collections.abc import Callable
from typing import Any

from src.circuit_breaker import CircuitBreaker
from src.context_compaction import CompactionResult, ContextCompactor, SessionRotation
from src.models import ContextAction, ContextUsage

logger = logging.getLogger(__name__)


class ContextMonitor:
    """Monitor agent context usage and trigger threshold-based actions.

    Tracks agent token usage in real-time by polling the Claude API.
    Triggers appropriate actions based on defined thresholds:
    - 80% (COMPACT_THRESHOLD): Trigger context compaction
    - 95% (ROTATE_THRESHOLD): Trigger session rotation

    Circuit Breaker (SEC-ORCH-7):
    - Per-agent circuit breakers prevent infinite retry loops on API failures
    - After failure_threshold consecutive failures, backs off for cooldown_seconds
    """

    COMPACT_THRESHOLD = 0.80  # 80% triggers compaction
    ROTATE_THRESHOLD = 0.95  # 95% triggers rotation

    def __init__(
        self,
        api_client: Any,
        poll_interval: float = 10.0,
        circuit_breaker_threshold: int = 3,
        circuit_breaker_cooldown: float = 60.0,
    ) -> None:
        """Initialize context monitor.

        Args:
            api_client: Claude API client for fetching context usage
            poll_interval: Seconds between polls (default: 10s)
            circuit_breaker_threshold: Consecutive failures before opening circuit (default: 3)
            circuit_breaker_cooldown: Seconds to wait before retry after circuit opens (default: 60)
        """
        self.api_client = api_client
        self.poll_interval = poll_interval
        self._usage_history: dict[str, list[ContextUsage]] = defaultdict(list)
        self._monitoring_tasks: dict[str, bool] = {}
        self._compactor = ContextCompactor(api_client=api_client)

        # Circuit breaker settings for per-agent monitoring loops (SEC-ORCH-7)
        self._circuit_breaker_threshold = circuit_breaker_threshold
        self._circuit_breaker_cooldown = circuit_breaker_cooldown
        self._circuit_breakers: dict[str, CircuitBreaker] = {}

    async def get_context_usage(self, agent_id: str) -> ContextUsage:
        """Get current context usage for an agent.

        Args:
            agent_id: Unique identifier for the agent

        Returns:
            ContextUsage object with current token usage

        Raises:
            Exception: If API call fails
        """
        response = await self.api_client.get_context_usage(agent_id)
        usage = ContextUsage(
            agent_id=agent_id,
            used_tokens=response["used_tokens"],
            total_tokens=response["total_tokens"],
        )

        # Log usage to history
        self._usage_history[agent_id].append(usage)
        logger.debug(f"Context usage for {agent_id}: {usage.usage_percent:.1f}%")

        return usage

    async def determine_action(self, agent_id: str) -> ContextAction:
        """Determine appropriate action based on current context usage.

        Args:
            agent_id: Unique identifier for the agent

        Returns:
            ContextAction based on threshold crossings
        """
        usage = await self.get_context_usage(agent_id)

        if usage.usage_ratio >= self.ROTATE_THRESHOLD:
            logger.warning(
                f"Agent {agent_id} hit ROTATE threshold: {usage.usage_percent:.1f}%"
            )
            return ContextAction.ROTATE_SESSION
        elif usage.usage_ratio >= self.COMPACT_THRESHOLD:
            logger.info(
                f"Agent {agent_id} hit COMPACT threshold: {usage.usage_percent:.1f}%"
            )
            return ContextAction.COMPACT
        else:
            logger.debug(f"Agent {agent_id} continuing: {usage.usage_percent:.1f}%")
            return ContextAction.CONTINUE

    def get_usage_history(self, agent_id: str) -> list[ContextUsage]:
        """Get historical context usage for an agent.

        Args:
            agent_id: Unique identifier for the agent

        Returns:
            List of ContextUsage objects in chronological order
        """
        return self._usage_history[agent_id]

    def _get_circuit_breaker(self, agent_id: str) -> CircuitBreaker:
        """Get or create circuit breaker for an agent.

        Args:
            agent_id: Unique identifier for the agent

        Returns:
            CircuitBreaker instance for this agent
        """
        if agent_id not in self._circuit_breakers:
            self._circuit_breakers[agent_id] = CircuitBreaker(
                name=f"context_monitor_{agent_id}",
                failure_threshold=self._circuit_breaker_threshold,
                cooldown_seconds=self._circuit_breaker_cooldown,
            )
        return self._circuit_breakers[agent_id]

    def get_circuit_breaker_stats(self, agent_id: str) -> dict[str, Any]:
        """Get circuit breaker statistics for an agent.

        Args:
            agent_id: Unique identifier for the agent

        Returns:
            Dictionary with circuit breaker stats, or empty dict if no breaker exists
        """
        if agent_id in self._circuit_breakers:
            return self._circuit_breakers[agent_id].get_stats()
        return {}

    async def start_monitoring(
        self, agent_id: str, callback: Callable[[str, ContextAction], None]
    ) -> None:
        """Start background monitoring loop for an agent.

        Polls context usage at regular intervals and calls callback with
        appropriate actions when thresholds are crossed.

        Uses circuit breaker to prevent infinite retry loops on repeated failures.

        Args:
            agent_id: Unique identifier for the agent
            callback: Function to call with (agent_id, action) on each poll
        """
        self._monitoring_tasks[agent_id] = True
        circuit_breaker = self._get_circuit_breaker(agent_id)

        logger.info(
            f"Started monitoring agent {agent_id} (poll interval: {self.poll_interval}s)"
        )

        while self._monitoring_tasks.get(agent_id, False):
            # Check circuit breaker state before polling
            if not circuit_breaker.can_execute():
                wait_time = circuit_breaker.time_until_retry
                logger.warning(
                    f"Circuit breaker OPEN for agent {agent_id} - "
                    f"backing off for {wait_time:.1f}s"
                )
                try:
                    await asyncio.sleep(wait_time)
                except asyncio.CancelledError:
                    break
                continue

            try:
                action = await self.determine_action(agent_id)
                callback(agent_id, action)
                # Successful poll - record success
                circuit_breaker.record_success()
            except Exception as e:
                # Record failure in circuit breaker
                circuit_breaker.record_failure()
                logger.error(
                    f"Error monitoring agent {agent_id}: {e} "
                    f"(circuit breaker: {circuit_breaker.state.value}, "
                    f"failures: {circuit_breaker.failure_count}"
                    f"/{circuit_breaker.failure_threshold})"
                )

            # Wait for next poll (or until stopped)
            try:
                await asyncio.sleep(self.poll_interval)
            except asyncio.CancelledError:
                break

        # Clean up circuit breaker when monitoring stops
        if agent_id in self._circuit_breakers:
            stats = self._circuit_breakers[agent_id].get_stats()
            del self._circuit_breakers[agent_id]
            logger.info(
                f"Stopped monitoring agent {agent_id} (circuit breaker stats: {stats})"
            )
        else:
            logger.info(f"Stopped monitoring agent {agent_id}")

    def stop_monitoring(self, agent_id: str) -> None:
        """Stop background monitoring for an agent.

        Args:
            agent_id: Unique identifier for the agent
        """
        self._monitoring_tasks[agent_id] = False
        logger.info(f"Requested stop for agent {agent_id} monitoring")

    async def trigger_compaction(self, agent_id: str) -> CompactionResult:
        """Trigger context compaction for an agent.

        Replaces conversation history with a concise summary to free memory.
        Target: 40-50% context reduction.

        Args:
            agent_id: Unique identifier for the agent

        Returns:
            CompactionResult with before/after metrics
        """
        logger.info(f"Triggering context compaction for agent {agent_id}")
        result = await self._compactor.compact(agent_id)

        if result.success:
            logger.info(
                f"Compaction successful for {agent_id}: "
                f"freed {result.tokens_freed} tokens ({result.reduction_percent:.1f}% reduction)"
            )
        else:
            logger.error(f"Compaction failed for {agent_id}: {result.error_message}")

        return result

    async def trigger_rotation(
        self,
        agent_id: str,
        agent_type: str,
        next_issue_number: int,
    ) -> SessionRotation:
        """Trigger session rotation for an agent.

        Spawns fresh agent when context reaches 95% threshold.

        Rotation process:
        1. Get current context usage metrics
        2. Close current agent session
        3. Spawn new agent with same type
        4. Transfer next issue to new agent
        5. Log rotation event with metrics

        Args:
            agent_id: Unique identifier for the current agent
            agent_type: Type of agent (sonnet, haiku, opus, glm)
            next_issue_number: Issue number to transfer to new agent

        Returns:
            SessionRotation with rotation details and metrics
        """
        logger.warning(
            f"Triggering session rotation for agent {agent_id} "
            f"(type: {agent_type}, next issue: #{next_issue_number})"
        )

        try:
            # Get context usage before rotation
            usage = await self.get_context_usage(agent_id)
            context_before_tokens = usage.used_tokens
            context_before_percent = usage.usage_percent

            logger.info(
                f"Agent {agent_id} context before rotation: "
                f"{context_before_tokens}/{usage.total_tokens} ({context_before_percent:.1f}%)"
            )

            # Close current session
            await self.api_client.close_session(agent_id)
            logger.info(f"Closed session for agent {agent_id}")

            # Spawn new agent with same type
            spawn_response = await self.api_client.spawn_agent(
                agent_type=agent_type,
                issue_number=next_issue_number,
            )
            new_agent_id = spawn_response["agent_id"]

            logger.info(
                f"Session rotation successful: {agent_id} -> {new_agent_id} "
                f"(issue #{next_issue_number})"
            )

            return SessionRotation(
                old_agent_id=agent_id,
                new_agent_id=new_agent_id,
                agent_type=agent_type,
                next_issue_number=next_issue_number,
                context_before_tokens=context_before_tokens,
                context_before_percent=context_before_percent,
                success=True,
            )

        except Exception as e:
            logger.error(f"Session rotation failed for agent {agent_id}: {e}")
            return SessionRotation(
                old_agent_id=agent_id,
                new_agent_id="",
                agent_type=agent_type,
                next_issue_number=next_issue_number,
                context_before_tokens=0,
                context_before_percent=0.0,
                success=False,
                error_message=str(e),
            )
