"""Agent orchestration for Captain Claw."""

import asyncio
import json
from pathlib import Path
from typing import Any
from typing import Callable

from captain_claw.agent_chunked_processing_mixin import AgentChunkedProcessingMixin
from captain_claw.agent_completion_mixin import AgentCompletionMixin
from captain_claw.agent_context_mixin import AgentContextMixin
from captain_claw.agent_file_ops_mixin import AgentFileOpsMixin
from captain_claw.agent_guard_mixin import AgentGuardMixin
from captain_claw.agent_model_mixin import AgentModelMixin
from captain_claw.agent_orchestration_mixin import AgentOrchestrationMixin
from captain_claw.agent_playbook_mixin import AgentPlaybookMixin
from captain_claw.agent_pipeline_mixin import AgentPipelineMixin
from captain_claw.agent_reasoning_mixin import AgentReasoningMixin
from captain_claw.agent_research_mixin import AgentResearchMixin
from captain_claw.agent_scale_detection_mixin import AgentScaleDetectionMixin
from captain_claw.agent_scale_loop_mixin import AgentScaleLoopMixin
from captain_claw.agent_session_mixin import AgentSessionMixin
from captain_claw.agent_skills_mixin import AgentSkillsMixin
from captain_claw.agent_tool_loop_mixin import AgentToolLoopMixin
from captain_claw.config import get_config
from captain_claw.file_registry import FileRegistry
from captain_claw.instructions import InstructionLoader
from captain_claw.llm import LLMProvider, Message
from captain_claw.llm_session_logger import get_llm_session_logger
from captain_claw.session import Session, get_session_manager
from captain_claw.tools import get_tool_registry


class Agent(
    AgentOrchestrationMixin,
    AgentCompletionMixin,
    AgentFileOpsMixin,
    AgentContextMixin,
    AgentGuardMixin,
    AgentModelMixin,
    AgentPipelineMixin,
    AgentReasoningMixin,
    AgentResearchMixin,
    AgentScaleDetectionMixin,
    AgentScaleLoopMixin,
    AgentChunkedProcessingMixin,
    AgentSessionMixin,
    AgentSkillsMixin,
    AgentPlaybookMixin,
    AgentToolLoopMixin,
):
    """Main agent orchestrator."""

    def __init__(
        self,
        provider: LLMProvider | None = None,
        status_callback: Callable[[str], None] | None = None,
        tool_output_callback: Callable[[str, dict[str, Any], str], None] | None = None,
        approval_callback: Callable[[str], bool] | None = None,
        thinking_callback: Callable[[str, str, str], None] | None = None,
        tool_stream_callback: Callable[[str], None] | None = None,
    ):
        """Initialize the agent.

        Args:
            provider: Optional LLM provider override
            status_callback: Optional runtime status callback
            approval_callback: Optional callback for guard approval prompts
            thinking_callback: Optional callback for inline thinking/reasoning updates
            tool_stream_callback: Optional callback for streaming tool output chunks
        """
        self.provider = provider
        self.status_callback = status_callback
        self.tool_output_callback = tool_output_callback
        self.approval_callback = approval_callback
        self.thinking_callback = thinking_callback
        self._tool_stream_callback = tool_stream_callback
        self.tools = get_tool_registry()
        self.tools.set_approval_callback(self.approval_callback)
        self.runtime_base_path = Path.cwd().resolve()
        cfg = get_config()
        self.workspace_base_path = cfg.resolved_workspace_path(self.runtime_base_path)
        self.tools.set_runtime_base_path(self.workspace_base_path)
        self.session_manager = get_session_manager()
        self.session: Session | None = None
        self._initialized = False
        self.max_iterations = 40  # Max tool calls per message
        self.last_usage: dict[str, int] = self._empty_usage()
        self.total_usage: dict[str, int] = self._empty_usage()
        self.last_context_window: dict[str, int | float] = {}
        self._last_memory_debug_signature: str | None = None
        self.pipeline_mode: str = "loop"  # "loop" (fast/simple) | "contracts" (planner+critic)
        self.planning_enabled: bool = False
        self._is_worker: bool = False  # Set True for orchestrator worker agents
        self._last_complete_success: bool = True  # Updated by finish() in complete()
        self.monitor_trace_llm: bool = bool(getattr(cfg.ui, "monitor_trace_llm", False))
        self.monitor_trace_pipeline: bool = bool(getattr(cfg.ui, "monitor_trace_pipeline", True))
        self.instructions = InstructionLoader()
        self._provider_override = provider is not None
        self._runtime_model_details: dict[str, Any] = {}
        self._skills_snapshot_cache = None
        self.memory = None
        self._last_semantic_memory_debug_signature: str | None = None
        # File registry for cross-task/cross-session file path resolution.
        # Set per orchestration run or per session for single-agent mode.
        self._file_registry: FileRegistry | None = None
        # External cancellation event — set by the UI layer (TUI/web) when
        # the user presses Ctrl+C / ESC or sends a cancel WS message.
        # The main iteration loop checks this at the top of every iteration
        # and breaks cleanly rather than running to completion.
        self.cancel_event: asyncio.Event = asyncio.Event()
        # LLM session logger — writes every LLM call to logs/<session>/session_log.md
        self.llm_session_logging: bool = bool(getattr(cfg.logging, "llm_session_logging", False))
        self._llm_logger = get_llm_session_logger(self.workspace_base_path / "logs")
        self._refresh_runtime_model_details(source="startup")

    @staticmethod
    def _empty_usage() -> dict[str, int]:
        """Create an empty usage bucket."""
        return {
            "prompt_tokens": 0,
            "completion_tokens": 0,
            "total_tokens": 0,
            "cache_creation_input_tokens": 0,
            "cache_read_input_tokens": 0,
        }

    @staticmethod
    def _is_monitor_only_tool_name(tool_name: str) -> bool:
        """Whether tool output is monitor-only and should not feed model context."""
        normalized = str(tool_name or "").strip().lower()
        return normalized in {
            "llm_trace",
            "planning",
            "task_contract",
            "task_rephrase",
            "completion_gate",
            "pipeline_trace",
            "telegram",
        }

    @staticmethod
    def _accumulate_usage(target: dict[str, int], usage: dict[str, int] | None) -> None:
        """Add usage values into target totals."""
        if not usage:
            return
        prompt = int(usage.get("prompt_tokens", 0))
        completion = int(usage.get("completion_tokens", 0))
        total = int(usage.get("total_tokens", prompt + completion))
        target["prompt_tokens"] += prompt
        target["completion_tokens"] += completion
        target["total_tokens"] += total
        # Cache tokens (Anthropic via LiteLLM)
        target.setdefault("cache_creation_input_tokens", 0)
        target.setdefault("cache_read_input_tokens", 0)
        target["cache_creation_input_tokens"] += int(usage.get("cache_creation_input_tokens", 0))
        target["cache_read_input_tokens"] += int(usage.get("cache_read_input_tokens", 0))

    def _finalize_turn_usage(self, turn_usage: dict[str, int]) -> None:
        """Persist usage for the last turn and aggregate global totals."""
        self.last_usage = turn_usage
        self._accumulate_usage(self.total_usage, turn_usage)

    def _set_runtime_status(self, status: str) -> None:
        """Forward runtime status updates when callback is configured."""
        if self.status_callback:
            try:
                self.status_callback(status)
            except Exception:
                pass

    def _emit_thinking(self, text: str, tool: str = "", phase: str = "tool") -> None:
        """Forward inline thinking/reasoning updates to the UI."""
        if self.thinking_callback:
            try:
                self.thinking_callback(text, tool, phase)
            except Exception:
                pass

    def _emit_tool_output(self, tool_name: str, arguments: dict[str, Any], output: str) -> None:
        """Forward raw tool output to UI callback when configured."""
        if self.session and tool_name in {"planning", "task_contract", "task_rephrase", "completion_gate", "scale_micro_loop"}:
            self._add_session_message(
                role="tool",
                content=str(output or ""),
                tool_name=tool_name,
                tool_arguments=arguments if isinstance(arguments, dict) else {},
            )
            if self.monitor_trace_pipeline:
                trace_payload = self._build_pipeline_trace_payload(
                    source_tool=tool_name,
                    arguments=arguments if isinstance(arguments, dict) else {},
                )
                trace_text = json.dumps(trace_payload, ensure_ascii=True, sort_keys=True)
                self._add_session_message(
                    role="tool",
                    content=trace_text,
                    tool_name="pipeline_trace",
                    tool_arguments=trace_payload,
                )
        if not self.tool_output_callback:
            return
        try:
            self.tool_output_callback(tool_name, arguments, output)
        except Exception:
            pass

    def _log_llm_call(
        self,
        interaction_label: str,
        messages: list[Message],
        response: Any,
        tools_enabled: bool = False,
        max_tokens: int | None = None,
        instruction_files: list[str] | None = None,
    ) -> None:
        """Write an LLM call entry to the file-based session log."""
        if not self.llm_session_logging:
            return
        try:
            # Ensure logger is pointed at the current session
            slug = self._current_session_slug()
            self._llm_logger.set_session(slug)

            # Drain recently-loaded instruction files if not explicitly provided.
            if not instruction_files:
                instruction_files = self.instructions.drain_recent_files()

            model = str(getattr(self.provider, "model", "") or "")
            self._llm_logger.log_call(
                interaction_label=interaction_label,
                model=model,
                messages=messages,
                response=response,
                instruction_files=instruction_files,
                tools_enabled=tools_enabled,
                max_tokens=max_tokens,
            )
        except Exception:
            pass
