"""Tool and Validation domain models for CleverAgents v3.

A **Tool** is the atomic unit of execution in CleverAgents -- a namespaced,
independently registered callable operation. Defined by JSON Schema
inputs/outputs, capability metadata (``read_only``, ``writes``,
``checkpointable``), and typed resource slots.

A **Validation** extends Tool with pass/fail semantics: always read-only,
always ``writes=False`` and ``checkpointable=False``. May wrap an existing
tool via ``wraps`` + ``transform`` to reuse its implementation.

Tools and Validations share the same naming namespace -- no collisions
allowed.

## Tool Name

Tools use a ``namespace/short_name`` naming pattern:

```
local/line-counter
devops/docker-build
```

## Source Types

| Source | Description | Required Fields |
|--------|-------------|-----------------|
| ``mcp`` | Delegates to MCP server | ``mcp_server``, ``mcp_tool_name`` |
| ``agent_skill`` | Agent Skills folder | ``agent_skill_path`` |
| ``builtin`` | Provided by runtime | (none) |
| ``custom`` | Inline Python code | ``code`` |
| ``wrapped`` | Validation-only wrapper | (set via Validation) |

## Validation Modes

| Mode            | Behavior                                        |
|-----------------|-------------------------------------------------|
| ``required``    | Failure blocks the operation                    |
| ``informational``| Failure is reported but does not block          |

## Validation Wraps Semantics

A Validation may wrap an existing Tool to reuse its implementation:

- When ``wraps`` is set, ``source`` must be ``wrapped``
- When ``wraps`` is set, ``code`` must not be set (mutually exclusive)
- When ``wraps`` is set, ``transform`` is required
- ``argument_mapping`` is only valid when ``wraps`` is set

## Forced Validation Constraints

When constructing a Validation, the following are always enforced:

- ``tool_type`` is forced to ``ToolType.VALIDATION``
- ``capability.read_only`` is forced to ``True``
- ``capability.writes`` is forced to ``False``
- ``capability.checkpointable`` is forced to ``False``

Based on ``docs/specification.md`` and ADR-004 (Pydantic Validation).
"""

from __future__ import annotations

import re
from collections import OrderedDict
from enum import StrEnum
from typing import Any

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

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

# ---------------------------------------------------------------------------
# Regex patterns
# ---------------------------------------------------------------------------

_TOOL_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$")

# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------


class ToolSource(StrEnum):
    """Source of a tool implementation.

    Determines which fields are required on the Tool model:
    - ``mcp``: requires ``mcp_server`` and ``mcp_tool_name``
    - ``agent_skill``: requires ``agent_skill_path``
    - ``builtin``: provided by the runtime
    - ``custom``: requires inline ``code``
    - ``wrapped``: validation-only; set implicitly when a Validation wraps
      an existing tool
    """

    MCP = "mcp"
    AGENT_SKILL = "agent_skill"
    BUILTIN = "builtin"
    CUSTOM = "custom"
    WRAPPED = "wrapped"


class ToolType(StrEnum):
    """Discriminator for the tool registry.

    Tools and Validations share the same namespace but are distinguished by
    this field for filtering and listing.
    """

    TOOL = "tool"
    VALIDATION = "validation"


class ValidationMode(StrEnum):
    """Controls whether validation failure blocks execution.

    - ``required``: failure blocks the operation
    - ``informational``: failure is reported but does not block
    """

    REQUIRED = "required"
    INFORMATIONAL = "informational"


class ResourceAccessMode(StrEnum):
    """Access level a tool has on a bound resource."""

    READ_ONLY = "read_only"
    READ_WRITE = "read_write"


class BindingMode(StrEnum):
    """How a resource slot is resolved to a concrete resource.

    - ``contextual``: resolved from the plan's project at invocation time
    - ``static``: hardcoded at registration via ``static_resource``
    - ``parameter``: passed as a parameter at invocation time
    """

    CONTEXTUAL = "contextual"
    STATIC = "static"
    PARAMETER = "parameter"


class CheckpointScope(StrEnum):
    """Granularity of checkpoint support for a tool."""

    FILE = "file"
    TRANSACTION = "transaction"
    SNAPSHOT = "snapshot"
    COMPOSITE = "composite"


# ---------------------------------------------------------------------------
# ResourceSlot
# ---------------------------------------------------------------------------


class ResourceSlot(BaseModel):
    """Declaration of a resource binding for a tool.

    Each slot names a typed resource the tool operates on.  The binding mode
    determines how the concrete resource is resolved at invocation time.
    """

    name: str = Field(
        ..., min_length=1, description="Slot name (valid Python identifier)"
    )
    resource_type: str = Field(
        ..., min_length=1, description="Resource type name (e.g. 'git-checkout')"
    )
    access: ResourceAccessMode = Field(..., description="Access level on the resource")
    description: str = Field("", description="Human-readable description of the slot")
    binding: BindingMode = Field(
        BindingMode.CONTEXTUAL, description="How the resource is resolved"
    )
    static_resource: str | None = Field(
        default=None, description="Resource name when binding=static"
    )
    required: bool = Field(True, description="Whether the resource must be present")

    @field_validator("name")
    @classmethod
    def validate_name(cls: type[ResourceSlot], v: str) -> str:
        """Validate slot name is a valid Python identifier."""
        if not v.isidentifier():
            raise ValueError(
                "ResourceSlot name must be a valid Python identifier "
                "(alphanumeric and underscores, not starting with a digit)"
            )
        return v

    @model_validator(mode="after")
    def _validate_static_binding(self) -> ResourceSlot:
        """Enforce static_resource rules based on binding mode."""
        if self.binding == BindingMode.STATIC and not self.static_resource:
            raise ValueError("static_resource is required when binding is 'static'")
        if self.binding != BindingMode.STATIC and self.static_resource is not None:
            raise ValueError(
                "static_resource must not be set when binding is not 'static'"
            )
        return self

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


# ---------------------------------------------------------------------------
# ToolCapability
# ---------------------------------------------------------------------------


class ToolCapability(BaseModel):
    """Capability metadata describing what a tool can and cannot do.

    Used by the scheduler, sandbox, and safety layer to make decisions
    about checkpoint management, human approval, and cost tracking.
    """

    read_only: bool = Field(default=False, description="Tool only reads, never writes")
    writes: bool = Field(default=False, description="Tool can write to resources")
    write_scope: str | None = Field(
        default=None, description="Scope of writes (e.g. 'file', 'database')"
    )
    checkpointable: bool = Field(
        default=False,
        description="Tool supports checkpoint/rollback",
    )
    checkpoint_scope: CheckpointScope | None = Field(
        default=None, description="Granularity of checkpoint support"
    )
    side_effects: list[str] = Field(
        default_factory=list,
        description="Known side effects (e.g. 'network', 'filesystem')",
    )
    idempotent: bool = Field(
        default=False,
        description="Safe to re-run without side effects",
    )
    unsafe: bool = Field(default=False, description="Requires extra safety checks")
    human_approval_required: bool = Field(
        default=False,
        description="Requires explicit human approval before execution",
    )
    cost_profile: str | None = Field(
        default=None, description="Named cost profile for budgeting"
    )

    @model_validator(mode="after")
    def _enforce_read_only(self) -> ToolCapability:
        """If read_only is True, writes and checkpointable must be False."""
        if self.read_only:
            if self.writes:
                raise ValueError("read_only tool cannot have writes=True")
            if self.checkpointable:
                raise ValueError("read_only tool cannot have checkpointable=True")
        return self

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


# ---------------------------------------------------------------------------
# ToolLifecycle
# ---------------------------------------------------------------------------


class ToolLifecycle(BaseModel):
    """Optional lifecycle hooks for a tool.

    Each hook is a dotted Python path or shell command string that the
    runtime invokes at the corresponding lifecycle stage.

    The four-stage lifecycle matches the spec contract:
    ``discover`` → ``activate`` → ``execute`` → ``deactivate``.
    """

    discover: str | None = Field(default=None, description="Hook for tool discovery")
    activate: str | None = Field(default=None, description="Hook for tool activation")
    execute: str | None = Field(default=None, description="Hook for tool execution")
    deactivate: str | None = Field(
        default=None, description="Hook for tool deactivation"
    )

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


# ---------------------------------------------------------------------------
# Tool
# ---------------------------------------------------------------------------


class Tool(BaseModel):
    """Domain model for a Tool.

    A Tool is the atomic unit of execution in CleverAgents -- a namespaced,
    independently registered callable operation.  Created via
    ``agents tool add --config <file>``.

    The **namespaced name** (``namespace/short_name``) is the unique identifier.
    Tools and Validations share the same namespace; no collisions are allowed.
    """

    # Identity -- namespace/name format
    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",
    )

    # Source
    source: ToolSource = Field(
        ..., description="Where the tool implementation comes from"
    )
    tool_type: ToolType = Field(
        ToolType.TOOL, description="Discriminator: tool or validation"
    )

    # Source-specific fields
    code: str | None = Field(
        default=None, description="Inline Python code (required for source=custom)"
    )
    mcp_server: str | None = Field(
        default=None, description="MCP server name (required for source=mcp)"
    )
    mcp_tool_name: str | None = Field(
        default=None,
        description="Tool name on the MCP server (required for source=mcp)",
    )
    agent_skill_path: str | None = Field(
        default=None,
        description=(
            "Path to Agent Skills Standard folder (required for source=agent_skill)"
        ),
    )

    # Schemas
    input_schema: dict[str, Any] | None = Field(
        default=None, description="JSON Schema for tool inputs"
    )
    output_schema: dict[str, Any] | None = Field(
        default=None, description="JSON Schema for tool outputs"
    )

    # Capability & resource bindings
    capability: ToolCapability = Field(
        default_factory=lambda: ToolCapability(),
        description="Capability metadata for the tool",
    )
    resource_slots: list[ResourceSlot] = Field(
        default_factory=list,
        description="Resource slot declarations for the tool",
    )

    # Lifecycle
    lifecycle: ToolLifecycle | None = Field(
        default=None, description="Optional lifecycle hooks"
    )

    # Execution
    timeout: int = Field(300, ge=1, description="Execution timeout in seconds")

    # Execution environment preference (intrinsic to tool definition)
    execution_environment: ExecutionEnvironmentPreference = Field(
        default_factory=ExecutionEnvironmentPreference,
        description="Intrinsic execution environment preference",
    )

    # -- Name format validation ------------------------------------------------

    @field_validator("name")
    @classmethod
    def validate_name_format(cls: type[Tool], v: str) -> str:
        """Validate tool name matches namespace/name pattern."""
        if not _TOOL_NAME_PATTERN.match(v):
            raise ValueError(
                f"Tool name must match 'namespace/name' pattern "
                f"(alphanumeric, hyphens, underscores): got '{v}'"
            )
        return v

    # -- Source-conditional field validation ------------------------------------

    @model_validator(mode="after")
    def _validate_source_fields(self) -> Tool:
        """Enforce source-conditional field requirements."""
        if self.source == ToolSource.CUSTOM and not self.code:
            raise ValueError("code is required when source is 'custom'")
        if self.source == ToolSource.MCP:
            if not self.mcp_server:
                raise ValueError("mcp_server is required when source is 'mcp'")
            if not self.mcp_tool_name:
                raise ValueError("mcp_tool_name is required when source is 'mcp'")
        if self.source == ToolSource.AGENT_SKILL and not self.agent_skill_path:
            raise ValueError(
                "agent_skill_path is required when source is 'agent_skill'"
            )
        return self

    # -- Properties ------------------------------------------------------------

    @property
    def namespace(self) -> str:
        """Extract the namespace portion from the name."""
        return self.name.split("/", 1)[0]

    @property
    def short_name(self) -> str:
        """Extract the short name portion from the name."""
        return self.name.split("/", 1)[1]

    # -- Factories & serialization --------------------------------------------

    @classmethod
    def from_config(cls, config: dict[str, Any]) -> Tool:
        """Create a Tool from a YAML configuration dict.

        The config follows the spec schema (see ``docs/specification.md``
        Tool Configuration Files section).
        """
        if "name" not in config:
            raise ValueError("Tool config must include 'name'")
        if "description" not in config:
            raise ValueError("Tool config must include 'description'")
        if "source" not in config:
            raise ValueError("Tool config must include 'source'")

        # Build capability from nested dict
        cap_data = config.get("capability")
        capability = (
            ToolCapability.model_validate(cap_data) if cap_data else ToolCapability()
        )

        # Build resource slots
        slots_data: list[dict[str, Any]] = config.get("resource_slots", [])
        resource_slots = [ResourceSlot.model_validate(s) for s in slots_data]

        # Build lifecycle
        lc_data = config.get("lifecycle")
        lifecycle = ToolLifecycle.model_validate(lc_data) if lc_data else None

        # Build execution environment preference
        ee_data = config.get("execution_environment")
        exec_env = (
            ExecutionEnvironmentPreference.model_validate(ee_data)
            if ee_data
            else ExecutionEnvironmentPreference()
        )

        return cls(
            name=config["name"],
            description=config["description"],
            source=ToolSource(config["source"]),
            tool_type=ToolType(config.get("tool_type", "tool")),
            code=config.get("code"),
            mcp_server=config.get("mcp_server"),
            mcp_tool_name=config.get("mcp_tool_name"),
            agent_skill_path=config.get("agent_skill_path"),
            input_schema=config.get("input_schema"),
            output_schema=config.get("output_schema"),
            capability=capability,
            resource_slots=resource_slots,
            lifecycle=lifecycle,
            timeout=config.get("timeout", 300),
            execution_environment=exec_env,
        )

    def as_cli_dict(self) -> OrderedDict[str, Any]:
        """Return a stable-ordered dictionary for CLI rendering.

        Keys are ordered for consistent display across output formats.
        """
        result: OrderedDict[str, Any] = OrderedDict()
        result["name"] = self.name
        result["description"] = self.description
        result["source"] = self.source.value
        result["tool_type"] = self.tool_type.value
        result["namespace"] = self.namespace
        result["short_name"] = self.short_name

        if self.code:
            result["code"] = self.code
        if self.mcp_server:
            result["mcp_server"] = self.mcp_server
        if self.mcp_tool_name:
            result["mcp_tool_name"] = self.mcp_tool_name
        if self.agent_skill_path:
            result["agent_skill_path"] = self.agent_skill_path
        if self.input_schema:
            result["input_schema"] = self.input_schema
        if self.output_schema:
            result["output_schema"] = self.output_schema

        cap_dict: dict[str, Any] = {
            "read_only": self.capability.read_only,
            "writes": self.capability.writes,
            "checkpointable": self.capability.checkpointable,
            "idempotent": self.capability.idempotent,
            "unsafe": self.capability.unsafe,
            "human_approval_required": self.capability.human_approval_required,
        }
        if self.capability.side_effects:
            cap_dict["side_effects"] = list(self.capability.side_effects)
        if self.capability.cost_profile:
            cap_dict["cost_profile"] = self.capability.cost_profile
        result["capability"] = cap_dict

        if self.resource_slots:
            result["resource_slots"] = [
                {
                    "name": slot.name,
                    "resource_type": slot.resource_type,
                    "access": slot.access.value,
                    "binding": slot.binding.value,
                    "required": slot.required,
                }
                for slot in self.resource_slots
            ]

        result["timeout"] = self.timeout

        if self.lifecycle is not None:
            lc = self.lifecycle
            lc_dict: dict[str, str | None] = {}
            if lc.discover is not None:
                lc_dict["discover"] = lc.discover
            if lc.activate is not None:
                lc_dict["activate"] = lc.activate
            if lc.execute is not None:
                lc_dict["execute"] = lc.execute
            if lc.deactivate is not None:
                lc_dict["deactivate"] = lc.deactivate
            if lc_dict:
                result["lifecycle"] = lc_dict

        ee = self.execution_environment
        if ee.mode != EnvironmentPreferenceMode.NONE:
            ee_dict: dict[str, str] = {"mode": ee.mode.value}
            if ee.target_resource:
                ee_dict["target_resource"] = ee.target_resource
            result["execution_environment"] = ee_dict

        return result

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


# ---------------------------------------------------------------------------
# Validation
# ---------------------------------------------------------------------------


class Validation(Tool):
    """Domain model for a Validation.

    A Validation extends Tool with pass/fail semantics: always read-only,
    ``writes=False``, ``checkpointable=False``.  May wrap an existing tool
    via ``wraps`` + ``transform`` to reuse its implementation.

    Shares the Tool naming namespace -- no collisions allowed.
    Managed via ``agents validation add/attach/detach``.
    """

    mode: ValidationMode = Field(
        ValidationMode.REQUIRED,
        description="Whether failure blocks execution",
    )
    wraps: str | None = Field(
        default=None,
        description="Name of existing tool this validation wraps",
    )
    transform: str | None = Field(
        default=None,
        description="Python function code to transform wrapped tool output",
    )
    argument_mapping: dict[str, Any] | None = Field(
        default=None,
        description="Argument mapping for the wrapped tool (only valid with wraps)",
    )

    @model_validator(mode="after")
    def _validate_wraps_fields(self) -> Validation:
        """Enforce wraps-related constraints."""
        if self.wraps is not None:
            # When wraps is set, source should be wrapped
            if self.source != ToolSource.WRAPPED:
                raise ValueError("source must be 'wrapped' when wraps is set")
            if self.code is not None:
                raise ValueError(
                    "code must not be set when wraps is set "
                    "(wraps and custom code are mutually exclusive)"
                )
            if self.transform is None:
                raise ValueError("transform is required when wraps is set")
        else:
            # No wraps: argument_mapping is forbidden
            if self.argument_mapping is not None:
                raise ValueError("argument_mapping is only valid when wraps is set")
        return self

    @model_validator(mode="after")
    def _enforce_validation_constraints(self) -> Validation:
        """Force validation-specific constraints on capability and tool_type."""
        if self.tool_type != ToolType.VALIDATION:
            object.__setattr__(self, "tool_type", ToolType.VALIDATION)
        cap = self.capability
        if cap.writes or cap.checkpointable or not cap.read_only:
            forced = ToolCapability(
                read_only=True,
                writes=False,
                write_scope=cap.write_scope,
                checkpointable=False,
                checkpoint_scope=cap.checkpoint_scope,
                side_effects=cap.side_effects,
                idempotent=cap.idempotent,
                unsafe=cap.unsafe,
                human_approval_required=cap.human_approval_required,
                cost_profile=cap.cost_profile,
            )
            object.__setattr__(self, "capability", forced)
        return self

    @classmethod
    def from_config(cls, config: dict[str, Any]) -> Validation:
        """Create a Validation from a YAML configuration dict.

        Handles validation-specific fields (mode, wraps, transform,
        argument_mapping) on top of the base Tool config.
        """
        if "name" not in config:
            raise ValueError("Validation config must include 'name'")
        if "description" not in config:
            raise ValueError("Validation config must include 'description'")

        # Determine source: if wraps is set, source is implicitly 'wrapped'
        if config.get("wraps"):
            source = ToolSource.WRAPPED
        elif "source" not in config:
            raise ValueError("Validation config must include 'source' (or 'wraps')")
        else:
            source = ToolSource(config["source"])

        # Build capability from nested dict
        cap_data = config.get("capability", {})
        capability = ToolCapability(**cap_data) if cap_data else ToolCapability()

        # Build resource slots
        slots_data = config.get("resource_slots", [])
        resource_slots = [ResourceSlot(**s) for s in slots_data]

        # Build lifecycle
        lc_data = config.get("lifecycle")
        lifecycle = ToolLifecycle(**lc_data) if lc_data else None

        return cls(
            name=config["name"],
            description=config["description"],
            source=source,
            tool_type=ToolType.VALIDATION,
            code=config.get("code"),
            mcp_server=config.get("mcp_server"),
            mcp_tool_name=config.get("mcp_tool_name"),
            agent_skill_path=config.get("agent_skill_path"),
            input_schema=config.get("input_schema"),
            output_schema=config.get("output_schema"),
            capability=capability,
            resource_slots=resource_slots,
            lifecycle=lifecycle,
            timeout=config.get("timeout", 300),
            mode=ValidationMode(config.get("mode", "required")),
            wraps=config.get("wraps"),
            transform=config.get("transform"),
            argument_mapping=config.get("argument_mapping"),
        )

    def as_cli_dict(self) -> OrderedDict[str, Any]:
        """Return a stable-ordered dictionary for CLI rendering.

        Extends the base Tool CLI dict with validation-specific fields.
        """
        result = super().as_cli_dict()
        result["mode"] = self.mode.value
        if self.wraps:
            result["wraps"] = self.wraps
        if self.transform:
            result["transform"] = self.transform
        if self.argument_mapping:
            result["argument_mapping"] = dict(self.argument_mapping)
        return result

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