"""Tool definitions and invocation contracts for model capabilities.

This module defines the protocol for exposing external functions to models
and handling their invocation requests. It enforces strict validation to
prevent injection attacks, code execution vulnerabilities, and protocol
violations.

Security Guarantees:
    - Function names cannot be Python keywords (prevents eval() injection)
    - Arguments are strictly JSON-serializable (no arbitrary objects)
    - Recursion depth is bounded to prevent memory exhaustion
    - Tool call IDs are validated for traceability

Classes:
    ToolCall: Represents a model's request to execute a function
    FunctionDef: Defines a callable function exposed to the model
    ToolDefinition: Wraps a FunctionDef in a standardized container
    NamedToolChoice: Forces the model to call a specific function

Type Aliases:
    ToolChoice: Union of tool invocation strategies

Enums:
    ToolChoiceType: Defines tool invocation strategies for model guidance
"""

import json
import keyword
from typing import Any, Dict, Literal, Union
from enum import Enum

from pydantic import Field, field_validator

from xulcan.core import (
    CanonicalRecord,
    MachineID,
    SemanticText,
    ExternalID,
    SafePayload
)


# ═══════════════════════════════════════════════════════════════════════════
# DOMAIN ENUMERATIONS
# ═══════════════════════════════════════════════════════════════════════════

class ToolChoiceType(str, Enum):
    """Defines tool invocation strategies for model guidance.
    
    - AUTO: Model decides whether to use tools based on context.
    - NONE: Model must respond without tool calls.
    - REQUIRED: Model must call at least one tool (prevents direct responses).
    - FUNCTION: Explicit function selection (see NamedToolChoice).
    """
    AUTO = "auto"
    NONE = "none"
    REQUIRED = "required"
    FUNCTION = "function"


# ═══════════════════════════════════════════════════════════════════════════
# TOOL INFRASTRUCTURE
# ═══════════════════════════════════════════════════════════════════════════

class ToolCall(CanonicalRecord):
    """Represents a model's request to execute an external function.
    
    This class encapsulates the intent-to-execute, not the execution result.
    Tool calls must be traceable (via unique ID) and must carry serializable
    arguments to prevent injection attacks when passed to execution contexts.
    
    Security Guarantees:
        - IDs are validated to be non-empty for traceability.
        - Arguments are strictly JSON-serializable (no arbitrary objects).
        - Recursion depth is bounded to prevent memory exhaustion.
    
    Attributes:
        id: Provider-generated unique identifier for correlation with ToolMessage.
        name: Function identifier (validated as MachineID).
        arguments: JSON-serializable dictionary of function parameters.
    
    Example:
        >>> call = ToolCall(
        ...     id="call_abc123",
        ...     name="get_weather",
        ...     arguments={"location": "San Francisco", "units": "celsius"}
        ... )
    """
    id: ExternalID = Field(
        description="Unique identifier for tracing this tool call through the system"
    )
    name: MachineID = Field(
        description="Function name to execute (must be valid identifier)"
    )

    arguments: SafePayload = Field(
        description="Function arguments as JSON-serializable dictionary"
    )

    context: SafePayload | None = Field(
        default=None,
        description=(
            "Kernel-injected execution metadata (e.g., run_id, parent_run_id). "
            "Never generated by the LLM, used strictly for out-of-band IPC "
            "and StateStore access by the Executors."
        )
    )

    @field_validator("id")
    @classmethod
    def validate_id_not_empty(cls, v: str) -> str:
        """Ensures traceability by rejecting empty or whitespace-only IDs.
        
        Without a valid ID, we cannot correlate ToolMessage responses back
        to their originating ToolCall, breaking the request-response chain.
        """
        if not v or not v.strip():
            raise ValueError(
                "Tool call ID cannot be empty. IDs are required for "
                "correlating tool execution results with requests."
            )
        return v


class FunctionDef(CanonicalRecord):
    """Defines a callable function exposed to the model.
    
    This specification allows the model to understand what tools are available
    and how to invoke them. The parameters field must conform to JSON Schema
    to enable automatic validation and type coercion in execution contexts.
    
    Invariants:
        - Function names cannot be Python keywords (prevents eval() injection).
        - Parameters schema must be valid JSON (no circular references).
        - Schema recursion is bounded to prevent parsing attacks.
    
    Attributes:
        name: Function identifier (validated as safe identifier, not a keyword).
        description: Human-readable explanation of function behavior.
        parameters: JSON Schema (draft-07) defining expected arguments.
    
    Example:
        >>> func = FunctionDef(
        ...     name="search_web",
        ...     description="Searches the web for a given query",
        ...     parameters={
        ...         "type": "object",
        ...         "properties": {
        ...             "query": {"type": "string"},
        ...             "max_results": {"type": "integer", "default": 10}
        ...         },
        ...         "required": ["query"]
        ...     }
        ... )
    """
    name: MachineID = Field(
        description="Function name (must be valid Python identifier)"
    )
    description: SemanticText | None = Field(
        default=None,
        description="Human-readable description for model context"
    )
    parameters: SafePayload = Field(
        description="JSON Schema (draft-07) defining function parameters"
    )

    @field_validator("name")
    @classmethod
    def validate_python_keywords(cls, v: str) -> str:
        """Prevents usage of reserved keywords as function names.
        
        If we allowed keywords like 'def', 'class', or 'import' as function
        names, downstream code generation or eval() contexts could execute
        arbitrary Python code. This validator enforces safe naming.
        """
        if keyword.iskeyword(v):
            raise ValueError(
                f"Function name '{v}' is a reserved Python keyword. "
                f"This could enable code injection in execution contexts."
            )
        return v

    

class ToolDefinition(CanonicalRecord):
    """Wraps a FunctionDef in a standardized tool container.
    
    Provides a consistent interface for tool registration across providers.
    The 'type' field is currently fixed to "function" but allows future
    extension to other tool types (e.g., "code_interpreter", "retrieval").
    
    Attributes:
        type: Tool category (currently only "function" is supported).
        function: The function specification.
    """
    type: Literal[ToolChoiceType.FUNCTION] = ToolChoiceType.FUNCTION
    function: FunctionDef


class FunctionIdentity(CanonicalRecord):
    """Internal model for the 'function' field in NamedToolChoice.
    
    Strictly enforces that 'name' is a valid MachineID and forbids
    any other keys automatically (inherited from CanonicalRecord).
    """
    name: MachineID = Field(
        description="Function name (must be valid Python identifier)"
    )
    
    @field_validator("name")
    @classmethod
    def validate_python_keywords(cls, v: str) -> str:
        """Prevents usage of reserved keywords as function names.
        
        If we allowed keywords like 'def', 'class', or 'import' as function
        names, downstream code generation or eval() contexts could execute
        arbitrary Python code. This validator enforces safe naming.
        """
        if keyword.iskeyword(v):
            raise ValueError(
                f"Function name '{v}' is a reserved Python keyword. "
                f"This could enable code injection in execution contexts."
            )
        return v

class NamedToolChoice(CanonicalRecord):
    """Forces the model to call a specific function.
    
    Used when the conversation context requires a deterministic tool invocation
    (e.g., "always call validate_input before proceeding"). This class enforces
    strict validation to prevent injection of malicious function names.
    
    Strict Mode Rationale:
        Only the 'name' key is allowed in the function dictionary. Extra keys
        could carry metadata that downstream systems misinterpret as executable
        instructions, leading to privilege escalation.
    
    Attributes:
        type: Discriminator (always "function").
        function: Dictionary containing exactly one key: 'name'.
    
    Example:
        >>> choice = NamedToolChoice(
        ...     type="function",
        ...     function={"name": "validate_input"}
        ... )
    """
    type: Literal[ToolChoiceType.FUNCTION] = ToolChoiceType.FUNCTION
    function: FunctionIdentity = Field(
        description="Must contain exactly 'name' key with valid function identifier"
    )



# ═══════════════════════════════════════════════════════════════════════════
# TYPE ALIASES
# ═══════════════════════════════════════════════════════════════════════════

ToolChoice = Union[ToolChoiceType, NamedToolChoice]
# Union representing either:
#   - A mode string ("auto", "none", "required")
#   - A specific function selection (NamedToolChoice)