"""HandoffNode — true agent handoff (relay race, not nesting).

The core orchestration primitive for multi-agent pathways.
Evaluates candidate conditions, selects the best-matching agent,
and returns a SIGNAL to the engine. The node does NOT call the agent.

The engine's handoff loop sees the signal and does a flat follow-up
call — the parent's turn is COMPLETE before the child starts.

    Host pathway runs → HandoffNode returns signal → Host turn ends
    → Engine sees handoff → Engine.turn(target) → Target runs flat
    → Result returned to caller

This avoids:
- Nested call stacks (Stack Trace from Hell)
- Resource holding (parent is done before child starts)
- Unbounded recursion (engine enforces depth limit)

Usage in YAML:
    route:
      type: handoff
      candidates:
        - agent_id: system.researcher
          description: "Information gathering"
          when: "{{understand.output.approach}} == 'research'"
        - agent_id: system.coder
          description: "Code tasks"
          when: "{{understand.output.approach}} == 'code'"
      fallback: direct_answer
      message: "{{input.message}}"
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Literal

from pydantic import BaseModel, Field

from core.pathways.expressions import safe_eval
from core.pathways.models.io import NodeInputs, NodeOutputs
from core.pathways.nodes.base import NodeBase
from core.pathways.nodes.models import HandoffOutput

if TYPE_CHECKING:
    from core.pathways.context import Context

logger = logging.getLogger(__name__)


class HandoffCandidate(BaseModel):
    """A candidate agent for handoff routing.

    Attributes
    ----------
    agent_id : str
        Registered agent ID to delegate to.
    description : str
        Human-readable description of what this agent handles.
    when : str | None
        Condition expression. If None, always matches (catch-all).
    """

    model_config = {"extra": "forbid"}

    agent_id: str
    description: str = ""
    when: str | None = None


class HandoffNode(NodeBase):
    """Agent handoff — pure routing signal, no execution.

    Evaluates candidate conditions against upstream data, selects the
    first matching agent, and returns a HandoffOutput signal. The node
    does NOT call the agent — the engine's handoff loop handles that.

    If no candidate matches and a ``fallback`` node is specified,
    the node outputs ``selected_route`` pointing to the fallback
    so the DAG scheduler routes to the fallback node instead.

    Attributes
    ----------
    candidates : list[HandoffCandidate]
        Ordered list of agents to consider. First match wins.
    message : str
        Template for the message to send to the selected agent.
    fallback : str | None
        Node ID to route to if no candidate matches.
    include_context : bool
        Whether to pass upstream data as context to the agent.
    """

    type: Literal["handoff"] = "handoff"
    candidates: list[HandoffCandidate] = Field(default_factory=list)
    message: str = "{{input.message}}"
    fallback: str | None = None
    include_context: bool = True

    async def compute(self, inputs: NodeInputs, _ctx: Context) -> NodeOutputs:
        """Evaluate candidates, return a handoff signal. Does NOT call any agent."""
        template_data = inputs.to_dict()
        formatted_message = self._format_template(self.message, template_data)

        # --- Evaluate candidates ---
        selected: HandoffCandidate | None = None
        for candidate in self.candidates:
            if candidate.when is None:
                # No condition = catch-all
                selected = candidate
                break

            formatted_cond = self._format_template(candidate.when, template_data)
            try:
                if safe_eval(formatted_cond, env=template_data):
                    selected = candidate
                    break
            except Exception:
                logger.warning(
                    "HandoffNode %s: condition eval failed for %s: %s",
                    self.id,
                    candidate.agent_id,
                    candidate.when,
                )

        # --- No match: route to fallback ---
        if selected is None:
            if self.fallback:
                logger.info(
                    "HandoffNode %s: no candidate matched, routing to fallback '%s'",
                    self.id,
                    self.fallback,
                )
                output = HandoffOutput(
                    success=True,
                    agent_id="",
                    message=formatted_message,
                    is_handoff=False,
                    routed_to_fallback=True,
                    fallback_node=self.fallback,
                )
                return NodeOutputs(
                    output=output.model_dump(),
                    selected_route=self.fallback,
                )

            output = HandoffOutput(
                success=False,
                agent_id="",
                message=formatted_message,
                is_handoff=False,
                error="no_match",
            )
            return NodeOutputs(output=output.model_dump())

        # --- Return handoff signal (engine does the actual call) ---
        logger.info(
            "HandoffNode %s: signaling handoff to agent '%s' (%s)",
            self.id,
            selected.agent_id,
            selected.description,
        )

        output = HandoffOutput(
            success=True,
            agent_id=selected.agent_id,
            message=formatted_message,
            description=selected.description,
            is_handoff=True,
        )
        return NodeOutputs(output=output.model_dump())


__all__ = ["HandoffCandidate", "HandoffNode"]
