"""Agent-related types.

These types define agent blueprints (pure data, no internal dependencies).
"""

from typing import Any, Self

from pydantic import BaseModel, Field, computed_field


class ModelRouting(BaseModel):
    """Per-step model routing — different models for different cognitive tasks.

    When set on an agent, LLMNode uses:
    - planning: for initial reasoning (first round)
    - execution: for tool-calling rounds
    - evaluation: for synthesis (final round)

    When None / not set, the single ``model`` field is used for everything.
    """

    planning: str = "reasoning"
    execution: str = "fast"
    evaluation: str = "reasoning"


class AgentDefinition(BaseModel):
    """Agent definition — the single source of truth for what an agent is.

    Three concerns:
    1. Instructions — what it knows and how it behaves (system prompt)
    2. Tools — what actions it can take
    3. Execution config — how it thinks (model, pathway, budget)

    When loaded from YAML, ``name`` and ``instructions`` may be None (shorthand
    for "just an ID reference").  Use ``is_full_definition`` to distinguish.
    """

    id: str
    name: str | None = None
    instructions: str | None = None

    # What it can do
    tools: list[str] = Field(default_factory=lambda: ["workspace.*", "code.*"])
    skills: list[str] = Field(default_factory=list)
    handoff_description: str = Field(
        default="",
        description=(
            "Human-readable description of what this agent is good at. "
            "Used by orchestrators and the agent.turn tool for routing decisions. "
            "Aligned with OpenAI Agents SDK handoff_description convention."
        ),
    )

    # How it thinks
    model: str = "reasoning"
    pathway: str  # User-defined pathway ID (no built-in default)
    temperature: float = 0.7
    max_steps: int = 10
    model_routing: ModelRouting | None = Field(
        default=None,
        description="Per-step model routing. None = use single `model` for all steps.",
    )

    # Metadata
    created_at: str | None = None
    updated_at: str | None = None
    workspace_id: str | None = None

    @computed_field
    @property
    def is_full_definition(self) -> bool:
        """True when this is a full agent definition (not just an ID reference).

        Useful for YAML config loading where a bare string ``"host"`` yields
        an AgentDefinition with only ``id`` set.
        """
        return self.name is not None or self.instructions is not None

    # -- Convenience accessors that guarantee non-None for runtime usage ------

    @property
    def display_name(self) -> str:
        """Return the agent name, falling back to id."""
        return self.name or self.id

    @property
    def system_prompt(self) -> str:
        """Return instructions, defaulting to empty string."""
        return self.instructions or ""

    # -- Factory for YAML config loading --------------------------------------

    @classmethod
    def from_yaml_dict(cls, data: dict[str, Any]) -> Self:
        """Create an AgentDefinition from a raw YAML dictionary."""
        raw = dict(data)
        # YAML-loaded agents default to all tools (unlike programmatic default).
        raw.setdefault("tools", ["*"])
        return cls.model_validate(raw)


__all__ = [
    "AgentDefinition",
    "ModelRouting",
]
