"""Runtime — the single entry point for AlbusOS.

Owns all components: agent repo, state store, registries, pathway VM,
observability. Delegates turn execution to Agent instances.

Architecture:
    Transport → runtime.turn(agent_id, message, thread_id)
                     │
                     └── Agent.turn() → PathwayVM → LLMNode → provider
"""

from __future__ import annotations

import logging
import os
import time
import uuid
from typing import TYPE_CHECKING, Any

from pydantic import BaseModel, Field

from core.agent import AgentRepository
from core.observability import (
    DebugHandler,
    GraphTraceLogger,
    RunRecorder,
)
from core.observability.events import EventEmitter
from core.pathways.models.tool import ToolCallRecord  # noqa: TC001 (Pydantic field)
from core.pathways.service import PathwayService
from core.pathways.service_container import ServiceContainer
from core.protocols.services import PathwayVMLike  # noqa: TC001 (type used at runtime)
from core.registry import Registry, set_registry
from core.state import FileStateStore
from core.types import (
    AgentDefinition,
    Attachment,
    ExecutionBudget,
)
from server.loaders import (
    load_builtin_skills,
    load_pathways_from_config,
    load_registered_tools,
    load_skills_from_config,
    register_agents_from_config,
)

if TYPE_CHECKING:
    from persistence.store import StudioStore

logger = logging.getLogger(__name__)


# =============================================================================
# EXECUTION RESULT
# =============================================================================


class ExecutionResult(BaseModel):
    """Result of an agent turn."""

    success: bool
    response: str
    agent_id: str
    thread_id: str

    # Execution details
    steps_taken: int = 0
    tokens_used: int = 0
    duration_ms: int = 0
    tool_calls: list[ToolCallRecord] = Field(default_factory=list)
    pathway: str | None = None

    # Budget info
    budget_exceeded: bool = False
    budget_exceeded_reason: str | None = None

    # Errors
    error: str | None = None


# =============================================================================
# RUNTIME
# =============================================================================


class Runtime:
    """AlbusOS runtime — thin infrastructure that delegates to Agent instances.

    Transport layers (HTTP, CLI, A2A) hold a reference to the Runtime and
    call ``turn()`` directly. The runtime routes to the appropriate Agent,
    which owns memory, persona, and skill composition.
    """

    def __init__(
        self,
        *,
        events: EventEmitter,
        agent_repo: AgentRepository,
        state_store: FileStateStore,
        pathway_service: PathwayService | None = None,
        pathway_vm: PathwayVMLike | None = None,
        registry: Registry | None = None,
        default_budget: ExecutionBudget | None = None,
    ):
        self.events = events
        self.agent_repo = agent_repo
        self.state_store = state_store
        self.pathway_service = pathway_service
        self.pathway_vm = pathway_vm
        self.registry = registry or Registry()
        self._default_budget = default_budget or ExecutionBudget()

    # Convenience accessors for the composed registries
    @property
    def tool_registry(self):
        """Access the tool registry."""
        return self.registry.tools

    @property
    def skill_registry(self):
        """Access the skill registry."""
        return self.registry.skills

    # =========================================================================
    # TURN EXECUTION
    # =========================================================================

    async def turn(
        self,
        agent_id: str,
        message: str,
        thread_id: str,
        *,
        budget: ExecutionBudget | None = None,
        context: dict[str, Any] | None = None,
        attachments: list[Attachment] | None = None,
        pathway_override: str | None = None,
    ) -> ExecutionResult:
        """Execute one agent turn — THE execution entry point.

        Delegates to Agent.turn() which owns memory, persona, and execution.
        """
        start_time = time.time()
        budget = budget or self._default_budget

        # Load agent
        agent = await self.agent_repo.get(agent_id)
        if not agent:
            return ExecutionResult(
                success=False,
                response="",
                agent_id=agent_id,
                thread_id=thread_id,
                error=f"Agent not found: {agent_id}",
            )

        if not self.pathway_service or not self.pathway_vm:
            return ExecutionResult(
                success=False,
                response="Pathway execution not available. "
                "Configure PathwayService and PathwayVM.",
                agent_id=agent_id,
                thread_id=thread_id,
            )

        try:
            result = await agent.turn(
                message,
                thread_id,
                pathway_vm=self.pathway_vm,
                pathway_service=self.pathway_service,
                budget=budget,
                pathway_override=pathway_override,
                attachments=attachments,
            )
            result.duration_ms = int((time.time() - start_time) * 1000)
            return result

        except Exception as e:
            logger.exception("Turn execution failed for %s", agent_id)
            return ExecutionResult(
                success=False,
                response="",
                agent_id=agent_id,
                thread_id=thread_id,
                error=str(e),
                duration_ms=int((time.time() - start_time) * 1000),
            )

    async def agent_turn(
        self, agent_id: str, message: str, thread_id: str | None = None
    ) -> ExecutionResult:
        """Execute an agent turn (convenience wrapper that auto-generates thread_id)."""
        if thread_id is None:
            thread_id = f"turn-{uuid.uuid4().hex[:8]}"
        return await self.turn(agent_id=agent_id, message=message, thread_id=thread_id)

    # =========================================================================
    # BOOTSTRAP HELPERS (called during server startup)
    # =========================================================================

    async def register_agents(
        self,
        agent_specs: list[AgentDefinition],
        *,
        skills_registry: dict[str, Any] | None = None,
    ) -> None:
        """Register agents from deployment config.

        Parameters
        ----------
        agent_specs
            Agent definitions from the deployment config.
        skills_registry
            Global skills registry so agents can scope skills at boot.
        """
        await register_agents_from_config(
            agent_specs,
            self.agent_repo,
            skills_registry=skills_registry,
        )

    async def load_skills(
        self,
        directories: list[str],
        auto_activate: list[str] | None = None,
    ) -> int:
        return await load_skills_from_config(
            directories,
            auto_activate=auto_activate,
            pathway_vm=self.pathway_vm,
        )

    async def load_pathways(
        self,
        directories: list[str],
        auto_deploy: list[str] | None = None,
    ) -> int:
        return await load_pathways_from_config(
            directories, self.pathway_service, auto_deploy=auto_deploy
        )

    # =========================================================================
    # FACTORY
    # =========================================================================

    @classmethod
    async def create(
        cls,
        *,
        pathway_vm: PathwayVMLike | None = None,
        persist_dir: str = ".albus_store",
        studio_store: StudioStore | None = None,
        debug: bool = False,
    ) -> Runtime:
        """Create Runtime with standard wiring."""
        # -- registries -------------------------------------------------------
        reg = Registry()
        set_registry(reg)

        # -- core components --------------------------------------------------
        state_store = FileStateStore(root=persist_dir)
        agent_repo = AgentRepository(state_store, store=studio_store)
        pathway_service = PathwayService(store=studio_store)

        # -- observability ----------------------------------------------------
        events = EventEmitter()
        is_tty = getattr(__import__("sys").stderr, "isatty", lambda: False)()
        log_format = os.getenv("ALBUS_LOG_FORMAT", "").strip().lower()
        json_mode = log_format == "json" or (log_format not in ("pretty", "json") and not is_tty)
        use_color = (
            not json_mode
            and is_tty
            and os.getenv("ALBUS_NO_COLOR", "").lower() not in ("1", "true", "yes")
        )
        events.on_all(GraphTraceLogger(use_color=use_color, json_mode=json_mode))

        if studio_store:
            events.on_all(RunRecorder(store=studio_store))
            logger.info("RunRecorder attached — runs will be persisted")

        if debug:
            events.on_all(DebugHandler(show_timestamps=True))

        await agent_repo.load_all()

        logger.info("Runtime created")

        runtime = cls(
            events=events,
            agent_repo=agent_repo,
            state_store=state_store,
            pathway_service=pathway_service,
            pathway_vm=pathway_vm,
            registry=reg,
        )

        # Build frozen service container and install on VM context
        svc = ServiceContainer(
            runtime=runtime,
            pathway_service=pathway_service,
            pathway_executor=pathway_vm,
            state_store=state_store,
            studio_store=studio_store,
        )
        if pathway_vm and hasattr(pathway_vm, "ctx"):
            pathway_vm.ctx.services = svc

        try:
            await load_builtin_skills()
        except Exception:
            logger.warning("Failed to load builtin skills", exc_info=True)

        try:
            await load_registered_tools(persist_dir)
        except Exception:
            logger.warning("Failed to load registered tools", exc_info=True)

        return runtime


__all__ = [
    "ExecutionResult",
    "Runtime",
]
