"""Agent runner — wires an ``AgentConfig`` to core components and runs the agent."""

from __future__ import annotations

import json
import os
import tempfile
import threading
from pathlib import Path
from typing import Any

from chatixia.config import AgentConfig


class AgentRunner:
    """Create and run an agent described by an :class:`AgentConfig`."""

    def __init__(self, config: AgentConfig) -> None:
        self.config = config
        self._initialized = False

        # Core components (populated by _initialize)
        self._heartbeat: Any = None
        self._client: Any = None
        self._deployment: str = ""
        self._skills: dict[str, Any] = {}
        self._executor: Any = None
        self._db: Any = None
        self._session_store: Any = None
        self._memory_store: Any = None
        self._prompt_store: Any = None
        self._context_manager: Any = None
        self._agent_logger: Any = None
        self._mcp_manager: Any = None
        self._task_thread: threading.Thread | None = None
        self._task_stop = threading.Event()

    # ------------------------------------------------------------------
    # Initialization
    # ------------------------------------------------------------------

    def _initialize(self) -> None:
        """Bootstrap all core components from the config (idempotent)."""
        if self._initialized:
            return

        from dotenv import load_dotenv

        # 1. Environment -------------------------------------------------
        env_path = self.config.resolve_path(self.config.env_file)
        if env_path.exists():
            load_dotenv(env_path)

        if self.config.provider:
            os.environ.setdefault("LLM_PROVIDER", self.config.provider)
        if self.config.model:
            _model_env = {
                "azure": "AZURE_OPENAI_DEPLOYMENT",
                "openai": "OPENAI_MODEL",
                "ollama": "OLLAMA_MODEL",
            }
            env_key = _model_env.get(self.config.provider)
            if env_key:
                os.environ.setdefault(env_key, self.config.model)

        missing = self.config.check_env()
        if missing:
            raise RuntimeError(
                f"Missing required environment variables: {', '.join(missing)}\n"
                f"Set them in {self.config.env_file} or export them in your shell."
            )

        # 2. Logging -----------------------------------------------------
        from logging_config import configure_logging

        configure_logging(
            level=os.getenv("LOG_LEVEL", "WARNING"),
            json_logs=os.getenv("LOG_FORMAT") == "json",
        )

        # 3. LLM client --------------------------------------------------
        from llm import create_llm_client

        self._client, self._deployment = create_llm_client()

        # 4. Skills -------------------------------------------------------
        from skills import SkillLoader

        self._skills = {}

        builtin_dir = self._find_builtin_skills_dir()
        if builtin_dir:
            all_builtin = SkillLoader(builtin_dir).load_all()
            if self.config.skills.builtin:
                for name in self.config.skills.builtin:
                    if name in all_builtin:
                        self._skills[name] = all_builtin[name]
            else:
                # No filter → load all builtins
                self._skills.update(all_builtin)

        for rel_dir in self.config.skills.dirs:
            resolved = self.config.resolve_path(rel_dir)
            if resolved.is_dir():
                self._skills.update(SkillLoader(resolved).load_all())

        for name in self.config.skills.disabled:
            self._skills.pop(name, None)

        # 5. MCP servers --------------------------------------------------
        from mcp_client import MCPManager

        self._mcp_manager = MCPManager()
        if self.config.mcp_servers:
            mcp_json = {
                "mcpServers": {
                    name: {
                        "command": srv.command,
                        "args": srv.args,
                        "env": srv.env,
                        "description": srv.description,
                        "enabled": srv.enabled,
                    }
                    for name, srv in self.config.mcp_servers.items()
                }
            }
            tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
            try:
                json.dump(mcp_json, tmp)
                tmp.close()
                self._mcp_manager.connect_from_config(Path(tmp.name))
            finally:
                Path(tmp.name).unlink(missing_ok=True)

            mcp_skills = self._mcp_manager.get_skills()
            if mcp_skills:
                self._skills.update(mcp_skills)

        # 6. Database & stores --------------------------------------------
        from agent_logger import AgentLogger
        from database import Database
        from memory import MemoryStore
        from prompts import PromptStore
        from session import ContextManager, SessionStore

        data_dir = self.config.resolve_path(self.config.data_dir)
        data_dir.mkdir(parents=True, exist_ok=True)

        self._db = Database(data_dir / "db" / "chatixia.db")
        self._session_store = SessionStore(self._db)
        self._memory_store = MemoryStore(self._db)
        self._prompt_store = PromptStore(self._db)
        self._agent_logger = AgentLogger(self._db)
        self._context_manager = ContextManager(max_tokens=self.config.context_window)

        # 7. Executor -----------------------------------------------------
        from skills import SkillExecutor

        self._executor = SkillExecutor(
            self._client,
            self._deployment,
            agent_logger=self._agent_logger,
        )

        # 8. LAN discovery (announce this agent) ----------------------------
        self._announcer: Any = None
        self._hub_listener: Any = None

        if self.config.hub.discovery:
            from chatixia.discovery import MSG_AGENT_ANNOUNCE, Announcer

            self._announcer = Announcer(
                msg_type=MSG_AGENT_ANNOUNCE,
                payload={
                    "agent_id": self.config.name,
                    "port": 8000,
                    "status": "running",
                    "mode": "interactive",
                    "skills_count": len(self._skills),
                    "goals_count": len(self.config.goals),
                },
                interval=self.config.hub.interval,
            )
            self._announcer.start()

        # 9. Heartbeat (hub reporting) --------------------------------------
        hub_url = self.config.hub.url
        if hub_url == "auto":
            hub_url = self._discover_hub()

        self._hub_url = hub_url or ""
        # Expose hub info to skill handlers via env vars
        os.environ["CHATIXIA_AGENT_ID"] = self.config.name
        if self._hub_url:
            os.environ["CHATIXIA_HUB_URL"] = self._hub_url
        if self._hub_url:
            from chatixia.heartbeat import HeartbeatReporter

            self._heartbeat = HeartbeatReporter(
                hub_url=self._hub_url,
                agent_id=self.config.name,
                interval=self.config.hub.interval,
                skills_count=len(self._skills),
                goals_count=len(self.config.goals),
                skill_names=list(self._skills.keys()),
            )
            self._heartbeat.start()

        self._initialized = True

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def _discover_hub(self, timeout: float = 5.0) -> str:
        """Listen for a hub announcement on the LAN. Returns URL or empty string."""
        import time as _time

        from chatixia.discovery import MSG_HUB_ANNOUNCE, Listener

        result: list[str] = []

        def on_message(msg: dict) -> None:
            if msg.get("type") == MSG_HUB_ANNOUNCE and not result:
                ip = msg.get("ip", "")
                port = msg.get("port", 8002)
                result.append(f"http://{ip}:{port}")

        listener = Listener(on_message)
        listener.start()
        deadline = _time.monotonic() + timeout
        while not result and _time.monotonic() < deadline:
            _time.sleep(0.2)
        listener.stop()

        if result:
            return result[0]
        return ""

    def _find_builtin_skills_dir(self) -> Path | None:
        """Locate the built-in skills directory."""
        # Vendored inside the wheel
        vendored = Path(__file__).resolve().parent / "_builtins" / "skills"
        if vendored.is_dir():
            return vendored
        # Development mode — monorepo sibling
        dev = Path(__file__).resolve().parent.parent.parent.parent / "skills"
        if dev.is_dir():
            return dev
        return None

    def _build_system_prompt(self) -> str:
        """Assemble the system prompt from config, skill guidance, and memory."""
        from skills import build_skill_guidance

        parts: list[str] = []

        # Base prompt
        if self.config.prompt:
            parts.append(self.config.prompt)
        else:
            active = self._prompt_store.get_active()
            if active:
                parts.append(active.content)
            else:
                parts.append("You are a helpful assistant. Use the available tools to help the user.")

        # Skill guidance
        if self._skills:
            guidance = build_skill_guidance(self._skills)
            if guidance:
                parts.append(
                    "## Available Tools\n\n"
                    "Use the tools below to help the user. "
                    "Call the appropriate tool based on the user's request.\n\n" + guidance
                )

        # Network context (peer agents)
        if self._hub_url:
            parts.append(
                "## Network\n\n"
                f"You are agent **{self.config.name}** connected to a hub at {self._hub_url}.\n"
                "You are part of a multi-agent network. Use `list_agents` to discover "
                "other agents, and `delegate` to send tasks to them.\n"
                "When the user asks you to talk to or interact with another agent, "
                "use these tools."
            )

        # Memory context
        memory_ctx = self._memory_store.get_context_string()
        if memory_ctx:
            parts.append(memory_ctx)

        return "\n\n".join(parts)

    def _start_task_worker(self) -> None:
        """Start a background thread that processes delegated tasks."""
        if self._task_thread is not None:
            return

        def _worker() -> None:
            while not self._task_stop.is_set():
                try:
                    self._process_delegated_tasks()
                except Exception:
                    pass
                self._task_stop.wait(5)

        self._task_stop.clear()
        self._task_thread = threading.Thread(target=_worker, daemon=True, name="task-worker")
        self._task_thread.start()

    def _stop_task_worker(self) -> None:
        self._task_stop.set()
        if self._task_thread is not None:
            self._task_thread.join(timeout=2)
            self._task_thread = None

    def _cleanup(self) -> None:
        self._stop_task_worker()
        if self._announcer:
            self._announcer.stop()
        if self._heartbeat:
            self._heartbeat.stop()
        if self._mcp_manager:
            self._mcp_manager.close()

    def _process_delegated_tasks(self) -> None:
        """Execute any tasks received from the hub via heartbeat."""
        if not self._heartbeat or not self.config.hub.accept_tasks:
            return

        tasks = self._heartbeat.pending_tasks()
        if not tasks:
            return

        from chatixia.tasks import COMPLETED, FAILED, TaskClient

        client = TaskClient(self._hub_url)

        for task in tasks:
            task_id = task.get("id", "")
            payload = task.get("payload", {})
            skill_name = task.get("skill", "")

            try:
                # Direct skill invocation if args provided and skill exists
                if skill_name and skill_name in self._skills:
                    skill = self._skills[skill_name]
                    args = payload.get("args", {})
                    if skill.handler and args:
                        result = skill.handler(**args)
                        client.update_status(task_id, COMPLETED, result=str(result))
                        continue

                # Fall back to LLM-mediated execution
                message = payload.get("message", "")
                if message:
                    from session import Session

                    session = Session.new(self._build_system_prompt())
                    prepared = self._context_manager.prepare_messages(session.messages)
                    response = self._executor.run(
                        message,
                        self._skills,
                        messages=prepared,
                        session_id=session.id,
                    )
                    client.update_status(task_id, COMPLETED, result=str(response))
                else:
                    client.update_status(task_id, FAILED, error="Empty payload")
            except Exception as exc:
                client.update_status(task_id, FAILED, error=str(exc))

    # ------------------------------------------------------------------
    # Run modes
    # ------------------------------------------------------------------

    def run_interactive(self) -> int:
        """Run the agent as an interactive REPL."""
        self._initialize()

        from prompt_toolkit import prompt as pt_prompt
        from prompt_toolkit.styles import Style as PTStyle
        from session import Session

        BOLD, CYAN, GREEN, DIM, RESET = "\033[1m", "\033[96m", "\033[92m", "\033[2m", "\033[0m"

        print(f"\n{BOLD}{CYAN}🤖 {self.config.name}{RESET}")
        if self.config.description:
            print(f"{DIM}{self.config.description}{RESET}")
        print(f"{DIM}{'━' * 60}{RESET}")
        print(f"{GREEN}  ✓{RESET} Provider: {self.config.provider} ({self._deployment})")
        print(f"{GREEN}  ✓{RESET} Skills: {len(self._skills)}")
        if self.config.goals:
            print(f"{GREEN}  ✓{RESET} Goals: {len(self.config.goals)} defined (use --daemon to activate)")
        print(f"\n{DIM}Type your message, or 'quit' to exit{RESET}")
        print(f"{DIM}{'━' * 60}{RESET}")

        session = Session.new(self._build_system_prompt())
        style = PTStyle.from_dict({"completion-menu": "bg:default #888888"})

        # Start background task worker for hub-delegated tasks
        self._start_task_worker()

        while True:
            try:
                user_input = pt_prompt("\n> ", style=style).strip()
            except (EOFError, KeyboardInterrupt):
                break

            if not user_input:
                continue
            if user_input.lower() in ("quit", "exit", "q"):
                break

            prepared = self._context_manager.prepare_messages(session.messages)

            stream = self._executor.run(
                user_input,
                self._skills,
                messages=prepared,
                session_id=session.id,
                stream=True,
            )

            # Print chunks as they arrive
            print()
            for chunk in stream:
                print(chunk, end="", flush=True)
            print()

            session.messages = prepared
            session.touch()
            session.auto_title()
            self._session_store.save(session)

        # Save on exit
        if len(session.messages) > 1:
            session.auto_title()
            self._session_store.save(session)
            print(f"\n💾 Session saved: {session.id}")

        self._cleanup()
        print("👋 Goodbye!")
        return 0

    def run_daemon(self, tick_interval: int = 30) -> int:
        """Run the agent in autonomous (daemon) mode."""
        self._initialize()

        from autonomous import AutonomousLoop
        from goals import GoalStore

        goal_store = GoalStore(self._db)

        # Sync goals from manifest → database
        for gcfg in self.config.goals:
            existing = [g for g in goal_store.list_all() if g.name == gcfg.name]
            kwargs: dict[str, Any] = dict(
                description=gcfg.sensor,
                sensor_prompt=gcfg.sensor,
                action_prompt=gcfg.action,
                check_interval=gcfg.interval,
                autonomy_level=gcfg.autonomy,
                skills_allowed=gcfg.skills,
            )
            if existing:
                goal_store.update(existing[0].id, enabled=True, **kwargs)
            else:
                goal_store.add(
                    name=gcfg.name,
                    metadata={"always_act": gcfg.always_act},
                    **kwargs,
                )

        BOLD, CYAN, GREEN, DIM, RESET = "\033[1m", "\033[96m", "\033[92m", "\033[2m", "\033[0m"

        print(f"\n{BOLD}{CYAN}🤖 {self.config.name} — Autonomous Mode{RESET}")
        print(f"{DIM}{'━' * 60}{RESET}")

        enabled = goal_store.list_enabled()
        if not enabled:
            print("No enabled goals. Add goals to agent.yaml and try again.")
            self._cleanup()
            return 1

        print(f"{GREEN}  ✓{RESET} Goals: {len(enabled)} enabled")
        for g in enabled:
            icon = {"auto": "⚡", "notify": "📣", "approve": "🔒"}.get(g.autonomy_level, "❓")
            print(f"    {icon} {g.name} (every {g.check_interval}s)")

        print(f"\n{BOLD}🚀 Running{RESET} — tick every {tick_interval}s, Ctrl+C to stop\n")

        def _notify(msg: str) -> None:
            import datetime

            ts = datetime.datetime.now().strftime("%H:%M:%S")
            print(f"  [{ts}] {msg}")

        loop = AutonomousLoop(
            executor=self._executor,
            skills=self._skills,
            goal_store=goal_store,
            session_store=self._session_store,
            context_manager=self._context_manager,
            agent_logger=self._agent_logger,
            system_prompt=self._build_system_prompt(),
            notify_fn=_notify,
        )

        # Wrap autonomous loop to also process delegated tasks each tick
        original_tick = loop.tick if hasattr(loop, "tick") else None

        def _tick_with_tasks() -> None:
            self._process_delegated_tasks()
            if original_tick:
                original_tick()

        if original_tick:
            loop.tick = _tick_with_tasks  # type: ignore[attr-defined]

        try:
            loop.run_forever(tick_interval=tick_interval)
        except KeyboardInterrupt:
            pass
        finally:
            self._cleanup()
            print("\n👋 Autonomous mode stopped.")

        return 0

    def run_once(self, message: str) -> int:
        """Send a single message and print the response."""
        self._initialize()

        from session import Session

        session = Session.new(self._build_system_prompt())
        prepared = self._context_manager.prepare_messages(session.messages)

        response = self._executor.run(
            message,
            self._skills,
            messages=prepared,
            session_id=session.id,
        )

        print(response)
        self._cleanup()
        return 0
