"""
Agent Control Loop - Main orchestration loop.

This module implements the control loop that:
1. Monitors agent health periodically
2. Detects stuck agents and decides actions
3. Executes control actions (auto-prompt, escalate, terminate)
4. Coordinates with Memory layer for context
5. Manages task lifecycle

Usage:
    from agent_orchestrator.control import AgentControlLoop

    loop = AgentControlLoop(db, ops_path)
    await loop.start()
"""

import asyncio
import logging
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from pathlib import Path
from typing import Any, Callable, Optional

from ..persistence.database import OrchestratorDB
from ..persistence.models import Agent, Task, Run, Approval
from ..adapters.base import BaseAdapter, AgentResponse, AgentStatus
from ..memory import OperationalMemory, WorkingMemory, KnowledgeMemory
from .health import AgentHealthCheck, HealthCheckResult, StuckDetectionConfig
from .actions import (
    ActionPolicy,
    ActionResult,
    ControlAction,
    ControlActionType,
    EscalationLevel,
)


logger = logging.getLogger(__name__)


class LoopState(Enum):
    """State of the control loop."""

    STOPPED = "stopped"
    STARTING = "starting"
    RUNNING = "running"
    PAUSED = "paused"
    STOPPING = "stopping"


@dataclass
class LoopConfig:
    """Configuration for the control loop."""

    # Health check interval
    health_check_interval_seconds: int = 30

    # Maximum concurrent agents
    max_concurrent_agents: int = 5

    # Task polling interval
    task_poll_interval_seconds: int = 10

    # Escalation handlers timeout
    escalation_timeout_seconds: int = 300

    # Auto-cleanup of completed tasks
    auto_cleanup_hours: int = 24


@dataclass
class AgentContext:
    """Runtime context for a managed agent."""

    agent_id: str
    adapter: BaseAdapter
    current_task_id: Optional[str] = None
    task_start_time: Optional[datetime] = None
    last_health_check: Optional[HealthCheckResult] = None
    consecutive_stuck_checks: int = 0
    working_session_id: Optional[str] = None

    # Generation number for detecting stale operations
    # Incremented on each significant state change (task assignment, cleanup)
    # Used to detect and reject operations from previous generations
    generation: int = 0

    # Cleanup flag to prevent operations on terminated contexts
    is_terminated: bool = False

    def increment_generation(self) -> int:
        """Increment and return the new generation number."""
        self.generation += 1
        return self.generation

    def is_stale(self, expected_generation: int) -> bool:
        """Check if an operation is from a stale generation."""
        return self.generation != expected_generation or self.is_terminated


class AgentControlLoop:
    """
    Main control loop for agent orchestration.

    Responsibilities:
    - Monitor health of all active agents
    - Execute control actions when agents are stuck
    - Route tasks to appropriate agents
    - Maintain working memory sessions
    - Handle escalations and approvals
    """

    def __init__(
        self,
        db: OrchestratorDB,
        ops_path: Path,
        config: Optional[LoopConfig] = None,
        stuck_config: Optional[StuckDetectionConfig] = None,
        action_policy: Optional[ActionPolicy] = None,
    ):
        """
        Initialize the control loop.

        Args:
            db: Database for persistence
            ops_path: Path to /ops/ directory
            config: Loop configuration
            stuck_config: Stuck detection configuration
            action_policy: Policy for control actions
        """
        self.db = db
        self.ops_path = Path(ops_path)
        self.config = config or LoopConfig()

        # Health checker
        self.health_checker = AgentHealthCheck(
            db=db,
            config=stuck_config or StuckDetectionConfig(),
        )

        # Action policy
        self.action_policy = action_policy or ActionPolicy()

        # Memory layers
        self.operational_memory = OperationalMemory(db=db, ops_path=ops_path)
        self.working_memory = WorkingMemory()
        self.knowledge_memory = KnowledgeMemory(ops_path=ops_path)

        # Runtime state
        self._state = LoopState.STOPPED
        self._agents: dict[str, AgentContext] = {}
        self._adapters: dict[str, BaseAdapter] = {}
        self._loop_task: Optional[asyncio.Task] = None

        # Lock for thread-safe access to _agents dictionary
        # Use this lock when reading/writing _agents to prevent race conditions
        self._agents_lock = asyncio.Lock()

        # Set of agent IDs with pending actions to prevent duplicate actions
        self._pending_actions: set[str] = set()

        # Callbacks
        self._escalation_handler: Optional[Callable] = None
        self._notification_handler: Optional[Callable] = None

        # Event queue for async processing
        self._action_queue: asyncio.Queue[ControlAction] = asyncio.Queue()

    @property
    def state(self) -> LoopState:
        """Get current loop state."""
        return self._state

    @property
    def active_agents(self) -> list[str]:
        """Get list of active agent IDs."""
        return list(self._agents.keys())

    def register_adapter(self, agent_id: str, adapter: BaseAdapter) -> None:
        """
        Register an adapter for an agent.

        Args:
            agent_id: Agent identifier
            adapter: Adapter instance
        """
        self._adapters[agent_id] = adapter
        logger.info(f"Registered adapter for agent {agent_id}")

    def set_escalation_handler(self, handler: Callable) -> None:
        """Set callback for escalations."""
        self._escalation_handler = handler

    def set_notification_handler(self, handler: Callable) -> None:
        """Set callback for notifications."""
        self._notification_handler = handler

    async def start(self) -> None:
        """Start the control loop."""
        if self._state == LoopState.RUNNING:
            logger.warning("Control loop already running")
            return

        self._state = LoopState.STARTING
        logger.info("Starting agent control loop")

        # Index knowledge memory
        self.knowledge_memory.index_all()

        # Start main loop
        self._state = LoopState.RUNNING
        self._loop_task = asyncio.create_task(self._main_loop())

        # Start action processor
        asyncio.create_task(self._process_actions())

        logger.info("Control loop started")

    async def stop(self) -> None:
        """Stop the control loop."""
        if self._state == LoopState.STOPPED:
            return

        self._state = LoopState.STOPPING
        logger.info("Stopping control loop")

        # Cancel main loop
        if self._loop_task:
            self._loop_task.cancel()
            try:
                await self._loop_task
            except asyncio.CancelledError:
                pass

        # Cleanup working sessions
        for agent_id in list(self._agents.keys()):
            await self._cleanup_agent(agent_id)

        self._state = LoopState.STOPPED
        logger.info("Control loop stopped")

    async def pause(self) -> None:
        """Pause the control loop."""
        if self._state == LoopState.RUNNING:
            self._state = LoopState.PAUSED
            logger.info("Control loop paused")

    async def resume(self) -> None:
        """Resume the control loop."""
        if self._state == LoopState.PAUSED:
            self._state = LoopState.RUNNING
            logger.info("Control loop resumed")

    async def _main_loop(self) -> None:
        """Main control loop iteration."""
        while self._state == LoopState.RUNNING:
            try:
                # Poll for pending tasks
                await self._poll_tasks()

                # Health check all active agents
                await self._health_check_all()

                # Process any pending approvals that timed out
                await self._check_approval_timeouts()

                # Sleep until next iteration
                await asyncio.sleep(self.config.health_check_interval_seconds)

            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Error in control loop: {e}")
                await asyncio.sleep(5)  # Brief pause on error

    async def _poll_tasks(self) -> None:
        """Poll for pending tasks and assign to available agents."""
        if self._state != LoopState.RUNNING:
            return

        # First, check for retryable tasks and reset them
        await self._process_retryable_tasks()

        # Get pending tasks
        pending_tasks = self.db.get_pending_tasks(limit=10)

        for task in pending_tasks:
            # Find available agent
            agent_id = await self._find_available_agent(task)
            if agent_id:
                await self.assign_task(task.id, agent_id)

    async def _process_retryable_tasks(self) -> None:
        """
        Process failed tasks that have retry attempts remaining.

        Tasks with run_until_done=True and attempt_count < max_retries
        are reset to 'pending' status for reassignment.
        """
        retryable_tasks = self.db.get_retryable_tasks(limit=5)

        for task in retryable_tasks:
            logger.info(
                f"Retrying task {task.id}: attempt {task.attempt_count + 1}/{task.max_retries}"
            )
            self.db.reset_task_for_retry(task.id)

    async def _find_available_agent(self, task: Task) -> Optional[str]:
        """Find an available agent for a task."""
        # Check registered adapters
        for agent_id, adapter in self._adapters.items():
            # Skip if already running a task
            if agent_id in self._agents and self._agents[agent_id].current_task_id:
                continue

            # Check if agent is healthy
            if adapter.is_healthy():
                return agent_id

        return None

    async def _health_check_all(self) -> None:
        """Health check all active agents."""
        # Take a snapshot of agents under lock
        async with self._agents_lock:
            agents_snapshot = list(self._agents.items())

        for agent_id, context in agents_snapshot:
            # Skip if context is terminated or already has pending action
            if context.is_terminated:
                continue
            if agent_id in self._pending_actions:
                logger.debug(f"Skipping health check for {agent_id}: action already pending")
                continue

            try:
                result = await self._health_check_agent(context)

                if result.is_stuck:
                    # Mark as having pending action to prevent duplicates
                    self._pending_actions.add(agent_id)

                    action = self.action_policy.decide_action(
                        result,
                        task_id=context.current_task_id
                    )
                    await self._action_queue.put(action)

            except Exception as e:
                logger.error(f"Health check failed for {agent_id}: {e}")

    async def _health_check_agent(self, context: AgentContext) -> HealthCheckResult:
        """Health check a single agent."""
        adapter = self._adapters.get(context.agent_id)

        # Get recent output if available
        current_output = None
        tokens_used = 0

        if adapter:
            usage = adapter.get_usage()
            tokens_used = usage.total_tokens()

        result = self.health_checker.sample_agent(
            agent_id=context.agent_id,
            current_output=current_output,
            tokens_used=tokens_used,
            task_start_time=context.task_start_time,
        )

        context.last_health_check = result

        # Track consecutive stuck checks
        if result.is_stuck:
            context.consecutive_stuck_checks += 1
        else:
            context.consecutive_stuck_checks = 0

        return result

    async def _process_actions(self) -> None:
        """Process control actions from the queue."""
        while self._state in (LoopState.RUNNING, LoopState.PAUSED):
            try:
                action = await asyncio.wait_for(
                    self._action_queue.get(),
                    timeout=5.0
                )

                if self._state == LoopState.RUNNING:
                    result = await self._execute_action(action)
                    logger.info(f"Action result: {result.message}")

            except asyncio.TimeoutError:
                continue
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Error processing action: {e}")

    async def _execute_action(self, action: ControlAction) -> ActionResult:
        """Execute a control action."""
        logger.info(f"Executing action: {action}")

        try:
            result = await self._execute_action_internal(action)
            return result
        finally:
            # Clear pending action flag after execution completes
            self._pending_actions.discard(action.agent_id)

    async def _execute_action_internal(self, action: ControlAction) -> ActionResult:
        """Internal action execution logic."""
        try:
            if action.action_type == ControlActionType.CONTINUE:
                return ActionResult(
                    action=action,
                    success=True,
                    message="No action needed",
                )

            elif action.action_type == ControlActionType.AUTO_PROMPT:
                return await self._execute_auto_prompt(action)

            elif action.action_type == ControlActionType.ESCALATE:
                return await self._execute_escalation(action)

            elif action.action_type == ControlActionType.TERMINATE:
                return await self._execute_termination(action)

            elif action.action_type == ControlActionType.REASSIGN:
                return await self._execute_reassignment(action)

            elif action.action_type == ControlActionType.PAUSE:
                return await self._execute_pause(action)

            elif action.action_type == ControlActionType.RESUME:
                return await self._execute_resume(action)

            else:
                return ActionResult(
                    action=action,
                    success=False,
                    message=f"Unknown action type: {action.action_type}",
                )

        except Exception as e:
            return ActionResult(
                action=action,
                success=False,
                message=f"Action failed: {str(e)}",
                error=str(e),
            )

    async def _execute_auto_prompt(self, action: ControlAction) -> ActionResult:
        """Execute an auto-prompt action."""
        agent_id = action.agent_id
        adapter = self._adapters.get(agent_id)

        if not adapter or not action.prompt:
            return ActionResult(
                action=action,
                success=False,
                message=f"No adapter or prompt for agent {agent_id}",
            )

        # Record the auto-prompt attempt
        self.action_policy.record_auto_prompt(agent_id)

        # Add to working memory
        context = self._agents.get(agent_id)
        if context and context.working_session_id:
            session = self.working_memory.get_session(context.working_session_id)
            if session:
                session.add_note(f"Auto-prompt sent: {action.reason}", "attempt")

        # Send the prompt to the agent
        try:
            ctx = self._build_agent_context(agent_id)
            response = await adapter.execute(action.prompt, ctx)

            # Update health checker with output
            self.health_checker.sample_agent(
                agent_id=agent_id,
                current_output=response.content,
                tokens_used=response.tokens_used,
            )

            return ActionResult(
                action=action,
                success=True,
                message=f"Auto-prompt sent to {agent_id}",
            )
        except Exception as e:
            return ActionResult(
                action=action,
                success=False,
                message=f"Failed to send auto-prompt: {str(e)}",
                error=str(e),
            )

    async def _execute_escalation(self, action: ControlAction) -> ActionResult:
        """Execute an escalation action."""
        agent_id = action.agent_id

        # Check escalation cooldown to prevent flooding
        if not self.action_policy.can_escalate(agent_id):
            logger.debug(f"Escalation cooldown active for {agent_id}, skipping")
            return ActionResult(
                action=action,
                success=True,
                message=f"Escalation skipped - cooldown active for {agent_id}",
            )

        # Record escalation with level
        self.action_policy.record_escalation(agent_id, action.escalation_level)

        # Create approval request
        approval = Approval(
            id=self.db.generate_approval_id(),
            agent_id=agent_id,
            action_type="escalation",
            target=action.reason,
            risk_level=action.escalation_level.value if action.escalation_level else "warn",
            status="pending",
        )
        self.db.create_approval(approval)

        # Notify via handler
        if self._escalation_handler:
            try:
                await self._escalation_handler(action)
            except Exception as e:
                logger.error(f"Escalation handler error: {e}")

        # Update agent status
        self.db.update_agent_status(agent_id, "stuck")

        return ActionResult(
            action=action,
            success=True,
            message=f"Escalation created for {agent_id}: {action.reason}",
        )

    async def _execute_termination(self, action: ControlAction) -> ActionResult:
        """Execute a termination action."""
        agent_id = action.agent_id
        adapter = self._adapters.get(agent_id)

        if adapter:
            await adapter.cancel()

        # Cleanup
        await self._cleanup_agent(agent_id)

        # Update database
        self.db.update_agent_status(agent_id, "terminated")

        if action.task_id:
            self.db.update_task_status(
                action.task_id,
                "failed",
                error_message=f"Terminated: {action.reason}"
            )

        return ActionResult(
            action=action,
            success=True,
            message=f"Agent {agent_id} terminated: {action.reason}",
        )

    async def _execute_reassignment(self, action: ControlAction) -> ActionResult:
        """Execute a task reassignment."""
        if not action.task_id or not action.target_agent_id:
            return ActionResult(
                action=action,
                success=False,
                message="Missing task_id or target_agent_id for reassignment",
            )

        # Cancel current agent's work
        await self._cleanup_agent(action.agent_id)

        # Assign to new agent
        await self.assign_task(action.task_id, action.target_agent_id)

        return ActionResult(
            action=action,
            success=True,
            message=f"Task {action.task_id} reassigned from {action.agent_id} to {action.target_agent_id}",
        )

    async def _execute_pause(self, action: ControlAction) -> ActionResult:
        """Pause an agent."""
        agent_id = action.agent_id
        adapter = self._adapters.get(agent_id)

        if adapter:
            adapter._status = AgentStatus.PAUSED

        self.db.update_agent_status(agent_id, "paused")

        return ActionResult(
            action=action,
            success=True,
            message=f"Agent {agent_id} paused",
        )

    async def _execute_resume(self, action: ControlAction) -> ActionResult:
        """Resume a paused agent."""
        agent_id = action.agent_id
        adapter = self._adapters.get(agent_id)

        if adapter:
            adapter._status = AgentStatus.IDLE

        self.db.update_agent_status(agent_id, "idle")

        return ActionResult(
            action=action,
            success=True,
            message=f"Agent {agent_id} resumed",
        )

    async def _check_approval_timeouts(self) -> None:
        """Check for timed-out approval requests."""
        pending = self.db.get_pending_approvals()
        now = datetime.now()

        for approval in pending:
            if not approval.requested_at:
                continue

            wait_time = (now - approval.requested_at).total_seconds()
            if wait_time > self.config.escalation_timeout_seconds:
                # Auto-escalate timed out approvals
                logger.warning(f"Approval {approval.id} timed out after {wait_time}s")
                self.db.update_approval(
                    approval.id,
                    status="timeout",
                    decided_by="system",
                    decision_notes="Timed out waiting for response",
                )

    async def _cleanup_agent(self, agent_id: str) -> None:
        """
        Cleanup resources for an agent.

        Thread-safe cleanup that:
        1. Marks context as terminated (prevents new operations)
        2. Increments generation (invalidates in-flight operations)
        3. Removes from agents dict
        4. Cleans up associated resources
        """
        async with self._agents_lock:
            context = self._agents.get(agent_id)

            if context:
                # Mark as terminated first to prevent new operations
                context.is_terminated = True
                context.increment_generation()

                # Remove from dict
                self._agents.pop(agent_id, None)

        # Clear pending action flag
        self._pending_actions.discard(agent_id)

        if context:
            # Discard working session (outside lock to avoid blocking)
            if context.working_session_id:
                summary = self.working_memory.discard(context.working_session_id)
                if summary:
                    logger.debug(f"Working session summary for {agent_id}: {summary}")

            # Clear health observations
            self.health_checker.clear_observations(agent_id)

            # Reset action policy counts
            self.action_policy.reset_agent(agent_id)

            logger.info(f"Cleaned up agent {agent_id} (generation {context.generation})")

    def _build_agent_context(self, agent_id: str) -> dict[str, Any]:
        """Build context dictionary for agent invocation."""
        # Get operational context
        op_context = self.operational_memory.build_context_for_agent(
            agent_id=agent_id,
            task_type="general"
        )

        # Get relevant knowledge
        project_state = self.knowledge_memory.get_project_state_summary()

        context = {
            "project_state": op_context.get("project_state", {}),
            "constraints": op_context.get("constraints", []),
            "recent_decisions": op_context.get("recent_decisions", []),
        }

        # Add working memory if available
        agent_context = self._agents.get(agent_id)
        if agent_context and agent_context.working_session_id:
            session = self.working_memory.get_session(agent_context.working_session_id)
            if session:
                context["working_memory"] = session.get_context()

        return context

    # =========================================================================
    # Public Task Management API
    # =========================================================================

    async def assign_task(self, task_id: str, agent_id: str) -> bool:
        """
        Assign a task to an agent.

        Thread-safe assignment that creates an AgentContext with a generation
        number for tracking stale operations.

        Args:
            task_id: Task to assign
            agent_id: Agent to assign to

        Returns:
            True if assignment successful
        """
        adapter = self._adapters.get(agent_id)
        if not adapter:
            logger.error(f"No adapter for agent {agent_id}")
            return False

        task = self.db.get_task(task_id)
        if not task:
            logger.error(f"Task {task_id} not found")
            return False

        # Create working session
        session = self.working_memory.create_session(task_id, agent_id)

        # Create agent context with generation number
        context = AgentContext(
            agent_id=agent_id,
            adapter=adapter,
            current_task_id=task_id,
            task_start_time=datetime.now(),
            working_session_id=session.session_id,
            generation=1,  # Start at generation 1
        )

        # Thread-safe assignment
        async with self._agents_lock:
            # Check if agent already has a task
            existing = self._agents.get(agent_id)
            if existing and existing.current_task_id and not existing.is_terminated:
                logger.warning(
                    f"Agent {agent_id} already running task {existing.current_task_id}, "
                    f"cannot assign {task_id}"
                )
                return False

            self._agents[agent_id] = context

        # Update database
        self.db.assign_task(task_id, agent_id)
        self.db.update_agent_status(agent_id, "running")

        # Capture generation for the async task
        task_generation = context.generation

        # Start task execution with generation tracking
        asyncio.create_task(self._execute_task(context, task, task_generation))

        logger.info(f"Assigned task {task_id} to agent {agent_id} (generation {task_generation})")
        return True

    async def _execute_task(
        self,
        context: AgentContext,
        task: Task,
        expected_generation: int,
    ) -> None:
        """
        Execute a task with an agent.

        Uses generation tracking to detect and handle stale operations
        (e.g., if the context was cleaned up during execution).

        Args:
            context: Agent context for execution
            task: Task to execute
            expected_generation: Generation number when task was assigned
        """
        agent_id = context.agent_id
        adapter = context.adapter
        task_id = task.id

        # Check for stale context before starting
        if context.is_stale(expected_generation):
            logger.warning(
                f"Stale task execution detected for {agent_id}: "
                f"expected gen {expected_generation}, current gen {context.generation}"
            )
            return

        # Build context
        ctx = self._build_agent_context(agent_id)

        # Get relevant runbooks and knowledge
        runbooks = self.knowledge_memory.get_relevant_runbooks(task.description)
        if runbooks:
            ctx["runbooks"] = [r.snippet for r in runbooks[:2]]

        # Start run in database
        run_id = self.db.generate_run_id(agent_id)
        self.db.start_run(run_id, agent_id, task_id)

        # Record attempt for run-until-done tracking
        attempt = self.db.record_task_attempt(task_id)
        if task.run_until_done:
            logger.info(
                f"Task {task_id} attempt {attempt}/{task.max_retries}"
            )

        try:
            # Update task status
            self.db.update_task_status(task_id, "running")

            # Execute via adapter
            adapter.set_task(task_id)
            response = await adapter.execute(task.description, ctx)

            # Check for stale context after async operation
            if context.is_stale(expected_generation):
                logger.warning(f"Context became stale during execution for {agent_id}")
                # Still record the result but skip context updates
                self.db.complete_run(
                    run_id,
                    outcome="terminated",
                    error_message="Context invalidated during execution",
                )
                return

            # Update health checker with output
            self.health_checker.sample_agent(
                agent_id=agent_id,
                current_output=response.content,
                tokens_used=response.tokens_used,
                task_start_time=context.task_start_time,
            )

            # Record in working memory
            session = self.working_memory.get_session(context.working_session_id or "")
            if session and session.is_active:
                session.add_tool_output(
                    tool_name=adapter.get_name(),
                    output=response.content[:500],
                    exit_code=0 if response.success else 1,
                )

            # Complete run
            packet = adapter.write_status_packet()
            self.db.complete_run(
                run_id,
                outcome="success" if response.success else "failure",
                packet=packet,
                error_message=response.error,
            )

            # Update task status
            if response.success:
                self.db.update_task_status(task_id, "completed")
                if task.run_until_done:
                    logger.info(
                        f"Task {task_id} completed successfully on attempt {attempt}"
                    )
            else:
                # Check if retries exhausted for run-until-done tasks
                if task.run_until_done and attempt >= task.max_retries:
                    self.db.mark_task_exhausted(task_id, response.error or "Unknown error")
                    logger.warning(
                        f"Task {task_id} exhausted all {task.max_retries} retries"
                    )
                else:
                    self.db.update_task_status(task_id, "failed", error_message=response.error)

            # Update usage
            self.db.update_daily_usage(
                agent_id=agent_id,
                tokens_input=response.tokens_input,
                tokens_output=response.tokens_output,
                cost_usd=response.cost,
                is_error=not response.success,
            )

        except Exception as e:
            logger.error(f"Task execution failed: {e}")
            self.db.update_task_status(task_id, "failed", error_message=str(e))

        finally:
            adapter.clear_task()

            # Only update context if not stale
            if not context.is_stale(expected_generation):
                context.current_task_id = None

                # Cleanup if task is done
                task_status = self.db.get_task(task_id)
                if task_status and task_status.status in ("completed", "failed"):
                    await self._cleanup_agent(agent_id)

    async def cancel_task(self, task_id: str) -> bool:
        """Cancel a running task."""
        task = self.db.get_task(task_id)
        if not task or not task.assigned_agent_id:
            return False

        agent_id = task.assigned_agent_id
        adapter = self._adapters.get(agent_id)

        if adapter:
            await adapter.cancel()

        await self._cleanup_agent(agent_id)
        self.db.update_task_status(task_id, "failed", error_message="Cancelled")

        return True

    def get_agent_status(self, agent_id: str) -> dict[str, Any]:
        """Get status of an agent."""
        context = self._agents.get(agent_id)
        adapter = self._adapters.get(agent_id)

        status = {
            "agent_id": agent_id,
            "is_registered": agent_id in self._adapters,
            "is_active": agent_id in self._agents,
        }

        if adapter:
            status["adapter_status"] = adapter.status.value
            usage = adapter.get_usage()
            status["usage"] = {
                "tokens": usage.total_tokens(),
                "cost": usage.total_cost,
                "requests": usage.requests_count,
            }

        if context:
            status["current_task"] = context.current_task_id
            status["task_start"] = context.task_start_time.isoformat() if context.task_start_time else None
            if context.last_health_check:
                status["health"] = {
                    "is_healthy": context.last_health_check.is_healthy,
                    "is_stuck": context.last_health_check.is_stuck,
                    "stuck_reason": context.last_health_check.stuck_reason.value,
                }

        return status

    def get_loop_status(self) -> dict[str, Any]:
        """Get overall loop status."""
        return {
            "state": self._state.value,
            "active_agents": len(self._agents),
            "registered_adapters": len(self._adapters),
            "pending_actions": self._action_queue.qsize(),
            "config": {
                "health_check_interval": self.config.health_check_interval_seconds,
                "max_concurrent_agents": self.config.max_concurrent_agents,
            },
        }
