"""
Actor YAML Schema Definitions for CleverAgents v3.

This module defines the Pydantic models and enums that validate actor configuration
files. Actors are the v3 replacement for v2's complex reactive stream configurations,
providing a simpler, more declarative way to define AI agent behaviors.

Core Components:
    Enums:
        - ActorType: LLM, TOOL, or GRAPH execution models
        - NodeType: AGENT, TOOL, CONDITIONAL, or SUBGRAPH node types
        - ContextView: STRATEGIST, EXECUTOR, REVIEWER, or FULL context filtering

    Tool Models:
        - ToolParameter: Parameter definitions for inline tools
        - ToolDefinition: Complete inline tool with Python code

    Graph Models:
        - EdgeDefinition: Connections between nodes with conditional routing
        - NodeDefinition: Node specifications (agent, tool, conditional, subgraph)
        - RouteDefinition: Complete graph topology with validation

    Configuration:
        - MemoryConfig: Conversation history settings
        - ContextConfigSchema: File inclusion and context window configuration
        - ActorConfigSchema: Main schema bringing all components together

Usage:
    >>> from cleveragents.actor.schema import ActorConfigSchema
    >>> config = ActorConfigSchema.from_yaml_file("path/to/actor.yaml")
    >>> print(config.name, config.type)

Author: CleverAgents Team
Version: 3.0.0
"""

from __future__ import annotations

from enum import StrEnum
from pathlib import Path
from typing import Any

import yaml
from pydantic import BaseModel, Field, field_validator, model_validator

from cleveragents.actor.role_validation import RoleHint, actor_role_warnings


class ActorType(StrEnum):
    """
    Type of actor determining execution behavior.

    Actors in v3 are compiled to LangGraph workflows. The type determines
    how the actor is compiled and executed:

    - LLM: Single LLM call with optional tools (simplest)
    - TOOL: Collection of callable tools with agent orchestration
    - GRAPH: Multi-node StateGraph with conditional routing (most flexible)

    Examples:
        >>> actor_config = {"type": ActorType.LLM, "model": "gpt-4"}
        >>> if actor_config["type"] == ActorType.GRAPH:
        ...     # Compile to multi-node graph
    """

    LLM = "llm"  # Single LLM agent with system prompt and optional tools
    TOOL = "tool"  # Collection of callable tools (file ops, API calls, etc.)
    GRAPH = "graph"  # Multi-node StateGraph with routing and conditionals


class NodeType(StrEnum):
    """
    Type of node in a graph actor.

    Graph actors (ActorType.GRAPH) are composed of multiple nodes connected
    by edges. Each node performs a specific operation in the workflow:

    - AGENT: Invokes an LLM to make decisions or generate content
    - TOOL: Executes a specific tool (file operations, API calls, etc.)
    - CONDITIONAL: Routes execution based on conditions (if/else logic)
    - SUBGRAPH: Embeds another actor as a nested workflow (composition)

    Node types enable complex workflows like:
        code_writer (AGENT) → run_tests (TOOL) → check_passed (CONDITIONAL)
        → if passed: style_check (SUBGRAPH), else: back to code_writer

    Examples:
        >>> node_def = {"type": NodeType.AGENT, "prompt": "Write Python code"}
        >>> if node_def["type"] == NodeType.SUBGRAPH:
        ...     # Load and compile referenced actor
    """

    AGENT = "agent"  # LLM agent node (makes AI-powered decisions)
    TOOL = "tool"  # Tool execution node (runs specific function)
    CONDITIONAL = "conditional"  # Routing node (evaluates conditions)
    SUBGRAPH = "subgraph"  # Nested actor reference (hierarchical composition)


class ContextView(StrEnum):
    """
    Role-based context filtering for actors.

    Different actor roles need different levels of context. Providing too much
    context wastes tokens and degrades performance; too little causes errors.
    ContextView allows actors to request only the information they need.

    - STRATEGIST: High-level planning view (project structure, goals, constraints)
    - EXECUTOR: Implementation view (code, files, specific tasks)
    - REVIEWER: Validation view (changes, diffs, test results)
    - FULL: Complete view (all available context, use sparingly)

    Examples:
        >>> strategist = {"context_view": ContextView.STRATEGIST}
        >>> executor = {"context_view": ContextView.EXECUTOR}
        >>> # Strategist sees project-level info, executor sees file-level details
    """

    STRATEGIST = "strategist"  # Planning and strategy context
    EXECUTOR = "executor"  # Implementation and execution context
    REVIEWER = "reviewer"  # Review and validation context
    FULL = "full"  # Complete context (use sparingly)


# ============================================================================
# Tool Models (for inline tool definitions in actors)
# ============================================================================


class ToolParameter(BaseModel):
    """
    Parameter definition for inline tool functions.

    Used in ToolDefinition to specify input parameters for Python code tools.
    Supports type hints and default values for tool inputs.

    Attributes:
        name: Parameter name (must be valid Python identifier)
        type: Python type annotation as string (e.g., "str", "int", "list[str]")
        description: Human-readable parameter description
        required: Whether parameter must be provided (default: True)
        default: Default value if not provided (only for optional params)

    Examples:
        >>> param = ToolParameter(
        ...     name="input_file",
        ...     type="str",
        ...     description="Path to input file",
        ...     required=True
        ... )
    """

    name: str = Field(..., description="Parameter name")
    type: str = Field(..., description="Python type annotation")
    description: str = Field(..., description="Parameter description")
    required: bool = Field(default=True, description="Whether required")
    default: Any | None = Field(default=None, description="Default value")

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Ensure parameter name is a valid Python identifier."""
        if not v.isidentifier():
            msg = f"Parameter name must be valid Python identifier: {v}"
            raise ValueError(msg)
        return v


class ToolDefinition(BaseModel):
    """
    Inline tool definition with Python code.

    Allows defining simple tools directly in actor YAML files without
    creating separate tool modules. Useful for actor-specific utilities.

    Attributes:
        name: Tool name (namespaced format: "namespace/tool_name")
        description: What the tool does (used in LLM tool selection)
        parameters: List of input parameters
        code: Python code implementing the tool (must define a function)

    Examples:
        >>> tool = ToolDefinition(
        ...     name="utils/count_lines",
        ...     description="Count lines in a file",
        ...     parameters=[
        ...         ToolParameter(name="file_path", type="str", description="File")
        ...     ],
        ...     code="def count_lines(file_path: str) -> int:\\n    ..."
        ... )
    """

    name: str = Field(..., description="Tool name (namespaced)")
    description: str = Field(..., description="Tool description")
    parameters: list[ToolParameter] = Field(
        default_factory=list, description="Tool parameters"
    )
    code: str = Field(..., description="Python code for tool")

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Ensure tool name follows namespace/name format."""
        if "/" not in v:
            msg = f"Tool name must be namespaced (namespace/name): {v}"
            raise ValueError(msg)
        return v


# ============================================================================
# Configuration Models (memory and context settings)
# ============================================================================


class MemoryConfig(BaseModel):
    """
    Conversation history and memory settings for actors.

    Controls how much conversation history is retained and passed to the LLM.
    Balances context quality with token usage.

    Attributes:
        enabled: Whether to maintain conversation history (default: True)
        max_messages: Maximum messages to retain (None = unlimited)
        max_tokens: Maximum tokens in history (None = unlimited)
        summarize_old: Whether to summarize old messages (default: False)

    Examples:
        >>> memory = MemoryConfig(
        ...     enabled=True,
        ...     max_messages=50,
        ...     max_tokens=4000
        ... )
    """

    enabled: bool = Field(default=True, description="Enable conversation memory")
    max_messages: int | None = Field(default=None, description="Max messages to retain")
    max_tokens: int | None = Field(default=None, description="Max tokens in history")
    summarize_old: bool = Field(default=False, description="Summarize old messages")


class ContextConfigSchema(BaseModel):
    """
    File inclusion and context window configuration.

    Defines which files/directories to include in actor context and how
    to manage the context window size.

    Attributes:
        include_files: List of file paths to include in context
        include_dirs: List of directory paths to include in context
        exclude_patterns: Glob patterns to exclude from context
        max_context_tokens: Maximum context window size (None = model default)

    Examples:
        >>> context = ContextConfigSchema(
        ...     include_files=["README.md", "src/main.py"],
        ...     include_dirs=["src/", "tests/"],
        ...     exclude_patterns=["**/__pycache__/**", "*.pyc"],
        ...     max_context_tokens=8000
        ... )
    """

    include_files: list[str] = Field(
        default_factory=list, description="Files to include"
    )
    include_dirs: list[str] = Field(
        default_factory=list, description="Directories to include"
    )
    exclude_patterns: list[str] = Field(
        default_factory=list, description="Exclusion patterns"
    )
    max_context_tokens: int | None = Field(
        default=None, description="Max context tokens"
    )


# ============================================================================
# LSP and Tool-Source Models (for per-node bindings)
# ============================================================================


class NodeLspBinding(BaseModel):
    """Per-node LSP server binding configuration.

    Allows individual graph nodes to override or specify their own LSP
    server bindings independently of the actor-level ``lsp`` setting.

    Three binding modes:
        1. Explicit: set ``server`` to a namespaced server name.
        2. Language-based: set ``languages`` to match servers by language.
        3. Auto: set ``auto=True`` to let the runtime resolve automatically.

    Attributes:
        server: Namespaced LSP server name (e.g. ``local/pyright``).
        languages: Languages for language-based binding resolution.
        auto: If ``True``, runtime resolves servers from project resources.
        capabilities: Optional capability filter for this node.
    """

    server: str | None = Field(default=None, description="LSP server name")
    languages: list[str] = Field(
        default_factory=list, description="Languages for binding"
    )
    auto: bool = Field(default=False, description="Auto-resolve binding")
    capabilities: list[str] | None = Field(
        default=None, description="LSP capability filter"
    )

    @field_validator("server")
    @classmethod
    def validate_server_namespace(cls, v: str | None) -> str | None:
        if v is not None and "/" not in v:
            msg = f"lsp_binding.server must be namespaced (namespace/name): '{v}'"
            raise ValueError(msg)
        return v


class ToolSourceRef(BaseModel):
    """Reference to a tool source available to a graph node.

    Allows nodes to declare where their tools come from — skills,
    MCP servers, or builtin tool groups.

    Attributes:
        type: Source type (``skill``, ``mcp``, ``builtin``, ``custom``).
        name: Namespaced name for skill/mcp sources.
        group: Group identifier for builtin sources.
    """

    type: str = Field(..., description="Source type: skill, mcp, builtin, custom")
    name: str | None = Field(default=None, description="Namespaced source name")
    group: str | None = Field(default=None, description="Builtin group name")

    @field_validator("type")
    @classmethod
    def validate_source_type(cls, v: str) -> str:
        allowed = {"skill", "mcp", "builtin", "custom"}
        if v not in allowed:
            msg = f"tool_sources type must be one of {sorted(allowed)}, got '{v}'"
            raise ValueError(msg)
        return v


class LspContextEnrichment(BaseModel):
    """Controls automatic LSP context enrichment injected into ACMS.

    Attributes:
        diagnostics: Inject file diagnostics into context (default True).
        type_annotations: Inject type info into context (default False).
        max_diagnostics_per_file: Cap on diagnostics per file (default 50).
    """

    diagnostics: bool = Field(default=True, description="Inject diagnostics")
    type_annotations: bool = Field(default=False, description="Inject type annotations")
    max_diagnostics_per_file: int = Field(
        default=50, description="Max diagnostics per file"
    )


# ============================================================================
# Graph Models (for ActorType.GRAPH - multi-node workflows)
# ============================================================================


class EdgeDefinition(BaseModel):
    """
    Edge connection between nodes in a graph actor.

    Defines transitions between nodes with optional conditional routing.
    Edges determine the flow of execution through the graph.

    Attributes:
        from_node: Source node ID
        to_node: Target node ID
        condition: Optional Python expression for conditional routing
        priority: Edge priority for multiple outgoing edges (higher = first)

    Examples:
        >>> # Simple unconditional edge
        >>> edge = EdgeDefinition(from_node="agent1", to_node="tool1")
        >>>
        >>> # Conditional edge with routing logic
        >>> edge = EdgeDefinition(
        ...     from_node="checker",
        ...     to_node="fix_errors",
        ...     condition="state.get('has_errors') == True",
        ...     priority=10
        ... )
    """

    from_node: str = Field(..., description="Source node ID")
    to_node: str = Field(..., description="Target node ID")
    condition: str | None = Field(
        default=None, description="Conditional routing expression"
    )
    priority: int = Field(default=0, description="Edge priority")


class NodeDefinition(BaseModel):
    """
    Node definition in a graph actor.

    Represents a single step in a multi-node workflow. Nodes can be
    agents (LLM calls), tools (function execution), conditionals (routing),
    or subgraphs (nested actors).

    Attributes:
        id: Unique node identifier within the graph
        type: Node type (AGENT, TOOL, CONDITIONAL, SUBGRAPH)
        name: Human-readable node name
        description: Node purpose and behavior
        config: Type-specific configuration (depends on node type)

    Node Type Configurations:
        AGENT: {"model": "gpt-4", "prompt": "...", "tools": [...]}
        TOOL: {"tool_name": "namespace/tool", "parameters": {...}}
        CONDITIONAL: {"conditions": [{"check": "...", "route_to": "..."}]}
        SUBGRAPH: {"actor_path": "path/to/actor.yaml"}

    Examples:
        >>> # Agent node
        >>> agent = NodeDefinition(
        ...     id="code_writer",
        ...     type=NodeType.AGENT,
        ...     name="Code Writer",
        ...     description="Writes Python code",
        ...     config={"model": "gpt-4", "prompt": "Write clean Python code"}
        ... )
        >>>
        >>> # Tool node
        >>> tool = NodeDefinition(
        ...     id="run_tests",
        ...     type=NodeType.TOOL,
        ...     name="Test Runner",
        ...     description="Executes pytest",
        ...     config={"tool_name": "testing/run_pytest"}
        ... )
    """

    id: str = Field(..., description="Node ID")
    type: NodeType = Field(..., description="Node type")
    name: str = Field(..., description="Node name")
    description: str = Field(..., description="Node description")
    config: dict[str, Any] = Field(default_factory=dict, description="Node config")
    lsp_binding: NodeLspBinding | None = Field(
        default=None, description="Per-node LSP binding"
    )
    tool_sources: list[ToolSourceRef] = Field(
        default_factory=list, description="Tool source references"
    )
    actor_ref: str | None = Field(
        default=None,
        description="Namespaced actor reference for subgraph nodes",
    )

    @field_validator("id")
    @classmethod
    def validate_id(cls, v: str) -> str:
        """Ensure node ID is a valid identifier."""
        if not v.replace("_", "").replace("-", "").isalnum():
            msg = f"Node ID must be alphanumeric with underscores/hyphens: {v}"
            raise ValueError(msg)
        return v

    @field_validator("actor_ref")
    @classmethod
    def validate_actor_ref(cls, v: str | None) -> str | None:
        """Ensure actor_ref follows namespace/name format when set."""
        if v is not None and "/" not in v:
            msg = f"actor_ref must be namespaced (namespace/name): '{v}'"
            raise ValueError(msg)
        return v


class RouteDefinition(BaseModel):
    """
    Complete graph topology for ActorType.GRAPH actors.

    Defines the full multi-node workflow including all nodes, edges,
    and entry/exit points. Includes validation for cycle detection
    and reachability.

    Attributes:
        nodes: List of all nodes in the graph
        edges: List of all edges connecting nodes
        entry_node: ID of the starting node
        exit_nodes: List of IDs for terminal nodes

    Validation:
        - All nodes must have unique IDs
        - Entry node must exist in nodes
        - All exit nodes must exist in nodes
        - All edge references must point to valid nodes
        - Graph must be acyclic (no cycles allowed)
        - All nodes must be reachable from entry_node

    Examples:
        >>> route = RouteDefinition(
        ...     nodes=[
        ...         NodeDefinition(
        ...             id="start",
        ...             type=NodeType.AGENT,
        ...             name="Planner",
        ...             description="Plans tasks",
        ...             config={"model": "gpt-4"}
        ...         ),
        ...         NodeDefinition(
        ...             id="execute",
        ...             type=NodeType.TOOL,
        ...             name="Executor",
        ...             description="Runs tasks",
        ...             config={"tool_name": "exec/run"}
        ...         )
        ...     ],
        ...     edges=[
        ...         EdgeDefinition(from_node="start", to_node="execute")
        ...     ],
        ...     entry_node="start",
        ...     exit_nodes=["execute"]
        ... )
    """

    nodes: list[NodeDefinition] = Field(..., description="Graph nodes")
    edges: list[EdgeDefinition] = Field(..., description="Graph edges")
    entry_node: str = Field(..., description="Entry node ID")
    exit_nodes: list[str] = Field(..., description="Exit node IDs")

    @field_validator("nodes")
    @classmethod
    def validate_unique_ids(cls, v: list[NodeDefinition]) -> list[NodeDefinition]:
        """Ensure all node IDs are unique."""
        ids = [node.id for node in v]
        if len(ids) != len(set(ids)):
            seen: set[str] = set()
            duplicates: list[str] = []
            for nid in ids:
                if nid in seen and nid not in duplicates:
                    duplicates.append(nid)
                seen.add(nid)
            msg = (
                f"route.nodes: Duplicate node IDs found: "
                f"{duplicates}. Each node ID must be unique."
            )
            raise ValueError(msg)
        return v

    def validate_references(self) -> None:
        """
        Validate all node references in edges and entry/exit points.

        Raises:
            ValueError: If any reference points to non-existent node.
                Messages include field paths for precise debugging.
        """
        node_ids = {node.id for node in self.nodes}
        valid_list = ", ".join(sorted(node_ids)) if node_ids else "(none)"

        if self.entry_node not in node_ids:
            msg = (
                f"Entry node '{self.entry_node}' not found in "
                f"route.entry_node. Valid node IDs: [{valid_list}]"
            )
            raise ValueError(msg)

        for exit_node in self.exit_nodes:
            if exit_node not in node_ids:
                msg = (
                    f"Exit node '{exit_node}' not found in "
                    f"route.exit_nodes. Valid node IDs: [{valid_list}]"
                )
                raise ValueError(msg)

        for idx, edge in enumerate(self.edges):
            if edge.from_node not in node_ids:
                msg = (
                    f"Edge from_node '{edge.from_node}' not found at "
                    f"route.edges[{idx}]. Valid node IDs: [{valid_list}]"
                )
                raise ValueError(msg)
            if edge.to_node not in node_ids:
                msg = (
                    f"Edge to_node '{edge.to_node}' not found at "
                    f"route.edges[{idx}]. Valid node IDs: [{valid_list}]"
                )
                raise ValueError(msg)

        # Check all nodes are reachable from entry_node via BFS.
        # Adjacency is built from both explicit edges and implicit routing
        # targets embedded in conditional node configs (route_to keys).
        adj: dict[str, list[str]] = {node.id: [] for node in self.nodes}
        for edge in self.edges:
            adj[edge.from_node].append(edge.to_node)
        for node in self.nodes:
            if node.type == NodeType.CONDITIONAL and isinstance(node.config, dict):
                for cond in node.config.get("conditions", []):
                    if isinstance(cond, dict) and "route_to" in cond:
                        target = str(cond["route_to"])
                        if target in node_ids:
                            adj[node.id].append(target)

        reachable: set[str] = set()
        queue: list[str] = [self.entry_node]
        while queue:
            current = queue.pop()
            if current in reachable:
                continue
            reachable.add(current)
            queue.extend(adj.get(current, []))

        unreachable = sorted(node_ids - reachable)
        if unreachable:
            nodes_str = ", ".join(f"'{n}'" for n in unreachable)
            msg = (
                f"route: nodes {nodes_str} are unreachable from entry node "
                f"'{self.entry_node}' at route.nodes. "
                "Hint: add edges connecting these nodes to the graph "
                "or remove them."
            )
            raise ValueError(msg)

    def detect_cycles(self) -> list[str]:
        """
        Detect cycles in the graph using DFS.

        Returns:
            List of node IDs involved in cycles (empty if acyclic)
        """
        # Build adjacency list
        graph: dict[str, list[str]] = {node.id: [] for node in self.nodes}
        for edge in self.edges:
            graph[edge.from_node].append(edge.to_node)

        # DFS cycle detection
        visited: set[str] = set()
        rec_stack: set[str] = set()
        cycle_nodes: list[str] = []

        def dfs(node: str) -> bool:
            visited.add(node)
            rec_stack.add(node)

            for neighbor in graph.get(node, []):
                if neighbor not in visited:
                    if dfs(neighbor):
                        return True
                elif neighbor in rec_stack:
                    cycle_nodes.append(neighbor)
                    return True

            rec_stack.remove(node)
            return False

        for node in graph:
            if node not in visited and dfs(node):
                break

        return cycle_nodes


# ============================================================================
# Main Actor Configuration Schema
# ============================================================================


class ActorConfigSchema(BaseModel):
    """
    Main actor configuration schema bringing all components together.

    This is the top-level model for actor YAML files. Actors are the v3
    replacement for v2's reactive streams, providing a simpler declarative
    way to define AI agent behaviors.

    Attributes:
        name: Actor name (namespaced format: "namespace/actor_name")
        type: Actor type (LLM, TOOL, or GRAPH)
        description: What the actor does
        version: Schema version (default: "1.0")
        model: LLM model name (required for LLM/GRAPH types)
        system_prompt: System prompt for LLM actors
        tools: List of tool references or inline definitions
        context_view: Role-based context filtering
        memory: Memory and conversation history settings
        context: File inclusion and context window settings
        route: Graph topology (required for GRAPH type)
        env_vars: Environment variable mappings

    Type-Specific Requirements:
        ActorType.LLM: Requires model, optional system_prompt and tools
        ActorType.TOOL: Requires tools list
        ActorType.GRAPH: Requires model and route

    Examples:
        >>> # Simple LLM actor
        >>> actor = ActorConfigSchema(
        ...     name="assistants/code_reviewer",
        ...     type=ActorType.LLM,
        ...     description="Reviews Python code for best practices",
        ...     model="gpt-4",
        ...     system_prompt="You are an expert Python code reviewer"
        ... )
        >>>
        >>> # Graph actor with multiple nodes
        >>> actor = ActorConfigSchema(
        ...     name="workflows/test_driven_dev",
        ...     type=ActorType.GRAPH,
        ...     description="Test-driven development workflow",
        ...     model="gpt-4",
        ...     route=RouteDefinition(...)
        ... )
    """

    name: str = Field(..., description="Actor name (namespaced)")
    type: ActorType = Field(..., description="Actor type")
    description: str = Field(..., description="Actor description")
    version: str = Field(default="1.0", description="Schema version")

    # LLM configuration
    model: str | None = Field(default=None, description="LLM model name")
    system_prompt: str | None = Field(default=None, description="System prompt")
    response_format: dict[str, Any] | None = Field(
        default=None,
        description="Optional JSON schema for structured LLM output",
    )

    # Tool configuration
    tools: list[str | ToolDefinition] = Field(
        default_factory=list, description="Tool references or definitions"
    )

    # Context and memory
    context_view: ContextView | None = Field(
        default=None, description="Context filtering view"
    )
    role_hint: RoleHint | None = Field(
        default=None,
        description="Optional role hint for role-aware validation",
    )
    memory: MemoryConfig = Field(
        default_factory=MemoryConfig, description="Memory settings"
    )
    context: ContextConfigSchema = Field(
        default_factory=ContextConfigSchema, description="Context settings"
    )

    # Graph configuration
    route: RouteDefinition | None = Field(
        default=None, description="Graph topology (for GRAPH type)"
    )

    # Skills
    skills: list[str] = Field(
        default_factory=list,
        description="Namespaced skill references (e.g. local/file-ops)",
    )

    # LSP configuration
    lsp: list[str] | dict[str, Any] | None = Field(
        default=None,
        description="LSP server bindings: list of server names, "
        "language-based object, or {auto: true}",
    )
    lsp_capabilities: list[str] | str | None = Field(
        default=None,
        description="LSP capability filter: list or 'all'",
    )
    lsp_context_enrichment: LspContextEnrichment | None = Field(
        default=None,
        description="Automatic LSP context enrichment settings",
    )

    # Environment variables
    env_vars: dict[str, str] = Field(
        default_factory=dict, description="Environment variable mappings"
    )

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Ensure actor name follows namespace/name format."""
        if "/" not in v:
            msg = f"Actor name must be namespaced (namespace/name): {v}"
            raise ValueError(msg)

        # Check for exactly one slash
        parts = v.split("/")
        if len(parts) != 2:
            msg = (
                f"Actor name must be namespaced with exactly one slash "
                f"(namespace/name): {v}"
            )
            raise ValueError(msg)

        namespace, name = parts
        if not namespace or not name:
            msg = (
                f"Actor name must be namespaced with non-empty namespace and name: {v}"
            )
            raise ValueError(msg)

        return v

    @field_validator("response_format")
    @classmethod
    def validate_response_format(
        cls, value: dict[str, Any] | None
    ) -> dict[str, Any] | None:
        """Validate minimal response_format structure for structured outputs."""
        if value is None:
            return value

        rf_type = value.get("type")
        if not isinstance(rf_type, str):
            msg = "response_format.type is required and must be a string"
            raise ValueError(msg)
        if rf_type not in {"object", "json_schema"}:
            msg = "response_format.type must be 'object' or 'json_schema'"
            raise ValueError(msg)

        return value

    @model_validator(mode="after")
    def validate_type_requirements(self) -> ActorConfigSchema:
        """Validate type-specific requirements."""
        # LLM actors require model
        if self.type == ActorType.LLM and not self.model:
            msg = "LLM actors require 'model' field"
            raise ValueError(msg)

        # TOOL actors require tools
        if self.type == ActorType.TOOL and not self.tools:
            msg = "TOOL actors require at least one tool"
            raise ValueError(msg)

        # GRAPH actors require model and route
        if self.type == ActorType.GRAPH:
            if not self.model:
                msg = "GRAPH actors require 'model' field"
                raise ValueError(msg)
            if not self.route:
                msg = "GRAPH actors require 'route' field"
                raise ValueError(msg)

            # Validate route references and cycles
            self.route.validate_references()
            cycles = self.route.detect_cycles()
            if cycles:
                nodes_str = " → ".join(cycles)
                msg = (
                    f"route: graph contains a cycle involving "
                    f"nodes: [{nodes_str}]. "
                    f"Hint: remove or redirect edges to break the cycle."
                )
                raise ValueError(msg)

        return self

    @classmethod
    def from_yaml_file(cls, file_path: str | Path) -> ActorConfigSchema:
        """
        Load actor configuration from YAML file.

        Args:
            file_path: Path to YAML file

        Returns:
            Parsed and validated ActorConfigSchema

        Raises:
            FileNotFoundError: If file doesn't exist
            yaml.YAMLError: If YAML is invalid
            ValidationError: If schema validation fails

        Examples:
            >>> actor = ActorConfigSchema.from_yaml_file("actors/reviewer.yaml")
            >>> print(actor.name, actor.type)
        """
        path = Path(file_path)
        if not path.exists():
            msg = f"Actor file not found: {file_path}"
            raise FileNotFoundError(msg)

        with path.open("r", encoding="utf-8") as f:
            data = yaml.safe_load(f)

        return cls.model_validate(data)

    def to_yaml_file(self, file_path: str | Path) -> None:
        """
        Save actor configuration to YAML file.

        Args:
            file_path: Path to save YAML file

        Examples:
            >>> actor.to_yaml_file("actors/new_actor.yaml")
        """
        path = Path(file_path)
        path.parent.mkdir(parents=True, exist_ok=True)

        with path.open("w", encoding="utf-8") as f:
            yaml.safe_dump(
                self.model_dump(mode="json", exclude_none=True),
                f,
                default_flow_style=False,
                sort_keys=False,
            )


__all__ = [
    "ActorConfigSchema",
    "ActorType",
    "ContextConfigSchema",
    "ContextView",
    "EdgeDefinition",
    "LspContextEnrichment",
    "MemoryConfig",
    "NodeDefinition",
    "NodeLspBinding",
    "NodeType",
    "RoleHint",
    "RouteDefinition",
    "ToolDefinition",
    "ToolParameter",
    "ToolSourceRef",
    "actor_role_warnings",
]
