"""Actor compiler: translate hierarchical YAML actor configs to LangGraph.

Compiles ``ActorConfigSchema`` (type=GRAPH) definitions into LangGraph
``GraphConfig`` objects with validated node/edge wiring, subgraph
resolution, cycle detection, and LSP binding metadata.

Public API:
    - :func:`compile_actor` — compile a single actor config.
    - :class:`CompilationMetadata` — diagnostic output.
    - :class:`CompiledActor` — compiled result bundle.
    - :class:`ActorCompilationError` — raised on compile failures.
    - :class:`SubgraphCycleError` — raised when subgraph refs form a cycle.
    - :class:`MissingNodeError` — raised when a referenced node is absent.

Usage::

    from cleveragents.actor.compiler import compile_actor
    compiled = compile_actor(config)
    print(compiled.metadata.node_ids)
"""

from __future__ import annotations

from typing import Any

from pydantic import BaseModel, ConfigDict, Field

from cleveragents.actor.schema import (
    ActorConfigSchema,
    ActorType,
    EdgeDefinition,
    NodeDefinition,
    NodeType,
)
from cleveragents.core.exceptions import ValidationError
from cleveragents.langgraph import nodes as lg_nodes
from cleveragents.lsp.models import LspBinding

# ---------------------------------------------------------------------------
# Errors
# ---------------------------------------------------------------------------


class ActorCompilationError(ValidationError):
    """Raised when actor compilation fails."""


class SubgraphCycleError(ActorCompilationError):
    """Raised when subgraph references form a cycle."""


class MissingNodeError(ActorCompilationError):
    """Raised when a referenced node does not exist."""


class InvalidEntryExitError(ActorCompilationError):
    """Raised when entry/exit node mapping is invalid."""


# ---------------------------------------------------------------------------
# Compilation metadata & result models
# ---------------------------------------------------------------------------

_NODE_TYPE_MAP: dict[NodeType, lg_nodes.NodeType] = {
    NodeType.AGENT: lg_nodes.NodeType.AGENT,
    NodeType.TOOL: lg_nodes.NodeType.TOOL,
    NodeType.CONDITIONAL: lg_nodes.NodeType.CONDITIONAL,
    NodeType.SUBGRAPH: lg_nodes.NodeType.SUBGRAPH,
}


class CompilationMetadata(BaseModel):
    """Diagnostic metadata produced by the actor compiler.

    Attributes:
        node_ids: All compiled node IDs.
        tool_nodes: Node IDs that are tool-type nodes.
        lsp_bindings: Per-node LSP binding records.
        subgraph_refs: Mapping of subgraph node ID → referenced actor name.
        entry_node: The validated entry-point node ID.
        exit_nodes: The validated exit-point node IDs.
    """

    node_ids: list[str] = Field(default_factory=list)
    tool_nodes: list[str] = Field(default_factory=list)
    lsp_bindings: list[LspBinding] = Field(default_factory=list)
    subgraph_refs: dict[str, str] = Field(default_factory=dict)
    entry_node: str = Field(default="")
    exit_nodes: list[str] = Field(default_factory=list)

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )


class CompiledActor(BaseModel):
    """Result bundle returned by :func:`compile_actor`.

    Attributes:
        name: Actor name from the source config.
        nodes: Compiled LangGraph ``NodeConfig`` mapping.
        edges: Compiled LangGraph ``Edge`` list.
        entry_point: Entry node ID.
        metadata: Diagnostic compilation metadata.
    """

    name: str = Field(..., description="Actor name")
    nodes: dict[str, lg_nodes.NodeConfig] = Field(default_factory=dict)
    edges: list[lg_nodes.Edge] = Field(default_factory=list)
    entry_point: str = Field(default="start")
    metadata: CompilationMetadata = Field(default_factory=CompilationMetadata)

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
        arbitrary_types_allowed=True,
    )


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

ActorResolver = Any  # callable(name: str) -> ActorConfigSchema | None


def _map_node(node: NodeDefinition) -> lg_nodes.NodeConfig:
    """Map an actor schema node to a LangGraph ``NodeConfig``."""
    lg_type = _NODE_TYPE_MAP.get(node.type, lg_nodes.NodeType.FUNCTION)
    config = node.config
    return lg_nodes.NodeConfig(
        name=node.id,
        type=lg_type,
        agent=config.get("agent") if node.type == NodeType.AGENT else None,
        function=(
            config.get("function") if node.type == NodeType.CONDITIONAL else None
        ),
        tools=config.get("tools", []) if node.type == NodeType.TOOL else [],
        subgraph=(config.get("actor_ref") if node.type == NodeType.SUBGRAPH else None),
        metadata=dict(config),
    )


def _map_edge(edge: EdgeDefinition) -> lg_nodes.Edge:
    """Map an actor schema edge to a LangGraph ``Edge``."""
    condition: dict[str, Any] | None = None
    if edge.condition is not None:
        condition = {"expression": edge.condition}
    return lg_nodes.Edge(
        source=edge.from_node,
        target=edge.to_node,
        condition=condition,
        metadata={"priority": edge.priority},
    )


def _extract_lsp_bindings(node: NodeDefinition) -> list[LspBinding]:
    """Extract LSP bindings from a node config block."""
    bindings: list[LspBinding] = []
    raw_bindings = node.config.get("lsp_bindings", [])
    if not isinstance(raw_bindings, list):
        return bindings
    for entry in raw_bindings:
        if not isinstance(entry, dict):
            continue
        server = entry.get("lsp_server_name", "")
        if not server or not isinstance(server, str):
            continue
        bindings.append(
            LspBinding(
                node_name=node.id,
                lsp_server_name=server,
                languages=entry.get("languages", []),
                auto_detect=entry.get("auto_detect", True),
            )
        )
    return bindings


def _detect_subgraph_cycles(
    actor_name: str,
    route_nodes: list[NodeDefinition],
    resolver: ActorResolver | None,
    visited: frozenset[str],
) -> list[str]:
    """Detect cycles in subgraph references across actors.

    Returns list of actor names forming a cycle, or empty list.
    """
    if resolver is None:
        return []

    for node in route_nodes:
        if node.type != NodeType.SUBGRAPH:
            continue
        ref_name = node.config.get("actor_ref", "")
        if not ref_name:
            continue
        if ref_name in visited:
            return [*list(visited), ref_name]

        referenced: ActorConfigSchema | None = resolver(ref_name)
        if referenced is None:
            continue
        if referenced.type != ActorType.GRAPH or referenced.route is None:
            continue

        deeper = _detect_subgraph_cycles(
            ref_name,
            referenced.route.nodes,
            resolver,
            visited | {ref_name},
        )
        if deeper:
            return deeper
    return []


# ---------------------------------------------------------------------------
# Public compile function
# ---------------------------------------------------------------------------


def compile_actor(
    config: ActorConfigSchema,
    *,
    actor_resolver: ActorResolver | None = None,
) -> CompiledActor:
    """Compile an ``ActorConfigSchema`` into a LangGraph-ready bundle.

    Args:
        config: The actor configuration to compile.  Must have
            ``type == ActorType.GRAPH`` and a valid ``route``.
        actor_resolver: Optional callable ``(name: str) -> ActorConfigSchema | None``
            used to resolve subgraph actor references.

    Returns:
        A :class:`CompiledActor` with nodes, edges, and metadata.

    Raises:
        ActorCompilationError: If the config type is not GRAPH.
        MissingNodeError: If a referenced node is not found.
        InvalidEntryExitError: If entry/exit mapping is invalid.
        SubgraphCycleError: If subgraph references form a cycle.
    """
    if config.type != ActorType.GRAPH:
        raise ActorCompilationError(
            f"Only GRAPH actors can be compiled; got type={config.type.value}"
        )

    route = config.route
    if route is None:
        raise ActorCompilationError(
            f"GRAPH actor '{config.name}' has no route definition"
        )

    # Validate references first
    route.validate_references()

    # Detect intra-graph cycles (already done in schema validation,
    # but repeated here for compile-time safety)
    intra_cycles = route.detect_cycles()
    if intra_cycles:
        raise SubgraphCycleError(f"Graph contains intra-graph cycles: {intra_cycles}")

    # Detect cross-actor subgraph cycles
    cross_cycles = _detect_subgraph_cycles(
        config.name, route.nodes, actor_resolver, frozenset({config.name})
    )
    if cross_cycles:
        raise SubgraphCycleError(
            f"Subgraph references form a cycle: {' -> '.join(cross_cycles)}"
        )

    # Build node map
    node_ids = {n.id for n in route.nodes}
    nodes: dict[str, lg_nodes.NodeConfig] = {}
    tool_node_ids: list[str] = []
    all_lsp_bindings: list[LspBinding] = []
    subgraph_refs: dict[str, str] = {}

    for node_def in route.nodes:
        nodes[node_def.id] = _map_node(node_def)

        if node_def.type == NodeType.TOOL:
            tool_node_ids.append(node_def.id)

        bindings = _extract_lsp_bindings(node_def)
        all_lsp_bindings.extend(bindings)

        if node_def.type == NodeType.SUBGRAPH:
            ref = node_def.config.get("actor_ref", "")
            if ref:
                subgraph_refs[node_def.id] = ref

    # Build edges
    edges: list[lg_nodes.Edge] = []
    for edge_def in route.edges:
        if edge_def.from_node not in node_ids:
            raise MissingNodeError(
                f"Edge from_node '{edge_def.from_node}' not in compiled nodes"
            )
        if edge_def.to_node not in node_ids:
            raise MissingNodeError(
                f"Edge to_node '{edge_def.to_node}' not in compiled nodes"
            )
        edges.append(_map_edge(edge_def))

    # Validate entry/exit
    if route.entry_node not in node_ids:
        raise InvalidEntryExitError(
            f"Entry node '{route.entry_node}' not found in nodes"
        )
    for exit_id in route.exit_nodes:
        if exit_id not in node_ids:
            raise InvalidEntryExitError(f"Exit node '{exit_id}' not found in nodes")

    metadata = CompilationMetadata(
        node_ids=sorted(node_ids),
        tool_nodes=tool_node_ids,
        lsp_bindings=all_lsp_bindings,
        subgraph_refs=subgraph_refs,
        entry_node=route.entry_node,
        exit_nodes=list(route.exit_nodes),
    )

    return CompiledActor(
        name=config.name,
        nodes=nodes,
        edges=edges,
        entry_point=route.entry_node,
        metadata=metadata,
    )


__all__ = [
    "ActorCompilationError",
    "CompilationMetadata",
    "CompiledActor",
    "InvalidEntryExitError",
    "MissingNodeError",
    "SubgraphCycleError",
    "compile_actor",
]
