"""Tool runtime models for CleverAgents.

Provides the execution-layer data structures that sit on top of the domain
[Tool][cleveragents.domain.models.core.tool.Tool] model:

- ``ToolSpec`` -- wraps a domain tool with an executable handler
- ``ToolResult`` -- captures execution outcomes (success/failure, duration)
- ``ToolError`` -- structured exception for tool failures

## ToolResult Fields

| Field          | Type           | Description                     |
|----------------|----------------|---------------------------------|
| ``success``    | ``bool``       | Whether execution succeeded     |
| ``output``     | ``dict``       | JSON-serialisable output payload|
| ``error``      | ``str | None`` | Error message on failure        |
| ``duration_ms``| ``float``      | Execution wall-clock time (ms)  |
| ``metadata``   | ``dict``       | Arbitrary execution metadata    |

## ToolError Attributes

| Attribute      | Type   | Description                              |
|----------------|--------|------------------------------------------|
| ``tool_name``  | ``str``| Namespaced tool name                     |
| ``error_type`` | ``str``| Category (e.g. ``RegistrationError``)    |
| ``details``    | ``str``| Human-readable explanation               |
"""

from __future__ import annotations

from collections.abc import Callable
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field, model_validator

from cleveragents.domain.models.core.execution_environment_preference import (
    ExecutionEnvironmentPreference,
)
from cleveragents.domain.models.core.tool import ToolCapability


class ToolSpec(BaseModel):
    """Specification of a registered, executable tool.

    Combines the domain-level metadata (name, schemas, capabilities) with
    an actual callable *handler* that the ``ToolRunner`` invokes.

    The optional ``source`` field tracks where the tool originated:

    - ``"builtin"`` — shipped with the CleverAgents runtime.
    - ``"agent_skills"`` — discovered from an Agent Skills Standard folder.
    - ``"mcp"`` — imported from an MCP server.
    - ``"custom"`` — user-defined inline tool.

    When ``source_metadata`` is populated it carries provenance details
    such as the filesystem path or MCP server URI that the tool came from.
    """

    name: str = Field(
        ...,
        min_length=1,
        description="Namespaced tool name (namespace/short_name)",
    )
    description: str = Field(
        ...,
        min_length=1,
        description="Short description of what the tool does",
    )
    input_schema: dict[str, Any] = Field(
        default_factory=dict,
        description="JSON Schema for tool inputs",
    )
    output_schema: dict[str, Any] = Field(
        default_factory=dict,
        description="JSON Schema for tool outputs",
    )
    capabilities: ToolCapability = Field(
        default_factory=ToolCapability,
        description="Capability metadata for the tool",
    )
    handler: Callable[..., Any] = Field(
        ...,
        description="Callable that executes the tool logic",
    )
    tool_type: Literal["tool", "validation"] = Field(
        default="tool",
        description="Discriminator: tool or validation",
    )
    source: str = Field(
        default="builtin",
        description=("Origin of the tool: builtin, agent_skills, mcp, or custom"),
    )
    source_metadata: dict[str, Any] = Field(
        default_factory=dict,
        description=("Provenance details for the tool (e.g. path, server URI)"),
    )
    execution_environment: ExecutionEnvironmentPreference = Field(
        default_factory=ExecutionEnvironmentPreference,
        description="Intrinsic execution environment preference for the tool",
    )

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


class ToolResult(BaseModel):
    """Outcome of a single tool execution."""

    success: bool = Field(..., description="Whether execution succeeded")
    output: dict[str, Any] = Field(
        default_factory=dict,
        description="Tool output payload (JSON-serialisable)",
    )
    error: str | None = Field(
        default=None,
        description="Error message when success is False",
    )
    duration_ms: float = Field(
        default=0.0,
        description="Wall-clock execution time in milliseconds",
    )
    metadata: dict[str, Any] = Field(
        default_factory=dict,
        description="Arbitrary metadata from the execution",
    )

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

    @model_validator(mode="after")
    def _validate_success_error_consistency(self) -> ToolResult:
        """Ensure success and error fields are logically consistent.

        A result marked as ``success=True`` must not carry an error
        message, and a result marked as ``success=False`` must have
        an error message explaining the failure.
        """
        if self.success and self.error is not None:
            raise ValueError(
                "ToolResult with success=True must not have an error message"
            )
        if not self.success and self.error is None:
            raise ValueError(
                "ToolResult with success=False must include an error message"
            )
        return self


class ToolError(Exception):
    """Exception raised for tool execution failures.

    Carries structured context so callers can inspect the failure without
    parsing a message string.
    """

    def __init__(
        self,
        tool_name: str,
        error_type: str,
        details: str,
    ) -> None:
        self.tool_name = tool_name
        self.error_type = error_type
        self.details = details
        super().__init__(f"[{error_type}] {tool_name}: {details}")
