"""Tool execution context for CleverAgents v3.

The ``ToolExecutionContext`` is provided to every tool execution, regardless of
source.  It carries the plan metadata, sandbox reference, resolved resource
bindings, a change recorder, and a cancellation token so that long-running
tools can be interrupted when a plan is cancelled.

Based on docs/specification.md Tool Interface and Architecture section.
"""

from __future__ import annotations

import threading
from collections import OrderedDict
from datetime import UTC, datetime
from enum import StrEnum
from typing import Any

from pydantic import BaseModel, ConfigDict, Field

from cleveragents.domain.models.core.safety_profile import SafetyProfile

# ---------------------------------------------------------------------------
# Bound resource placeholder
# ---------------------------------------------------------------------------


class BoundResource(BaseModel):
    """A resolved resource binding for a tool slot.

    At execution time each ``ResourceSlot`` declared by the tool is resolved to
    a ``BoundResource`` that carries the concrete resource identity, the sandbox
    path (if applicable), and the access mode.
    """

    slot_name: str = Field(..., min_length=1, description="Slot name from the tool")
    resource_id: str = Field(..., min_length=1, description="Resource ULID or name")
    resource_type: str = Field(..., min_length=1, description="Resource type name")
    sandbox_path: str | None = Field(
        default=None,
        description="Sandbox-mapped path (None for non-file resources)",
    )
    access: str = Field(
        "read_only", description="Access mode: 'read_only' or 'read_write'"
    )

    model_config = ConfigDict(str_strip_whitespace=True)


# ---------------------------------------------------------------------------
# Change tracking
# ---------------------------------------------------------------------------


class ChangeOperation(StrEnum):
    """Operation type for a recorded change."""

    CREATE = "create"
    MODIFY = "modify"
    DELETE = "delete"
    MOVE = "move"


class Change(BaseModel):
    """A single recorded change produced by a tool execution.

    Every resource modification is explicit and tracked via the execution
    context.  The ChangeSet is built from these entries, enabling precise
    rollback (replay inverse of recorded changes) and audit.
    """

    operation: ChangeOperation = Field(..., description="Type of change")
    resource_id: str = Field(..., description="Resource ID affected")
    path: str | None = Field(default=None, description="File path (if applicable)")
    previous_content: str | None = Field(
        default=None, description="Content before change"
    )
    new_content: str | None = Field(default=None, description="Content after change")
    metadata: dict[str, Any] = Field(
        default_factory=dict, description="Additional change metadata"
    )
    timestamp: str = Field(
        default_factory=lambda: datetime.now(UTC).isoformat(),
        description="ISO-8601 timestamp of the change",
    )

    model_config = ConfigDict(str_strip_whitespace=True)


# ---------------------------------------------------------------------------
# Cancellation token
# ---------------------------------------------------------------------------


class CancellationToken:
    """Thread-safe cancellation token for tool execution.

    The plan lifecycle can set the token to signal cancellation, and tools
    check ``is_cancelled`` to abort long-running operations early.
    """

    def __init__(self) -> None:
        self._event = threading.Event()

    def cancel(self) -> None:
        """Signal cancellation."""
        self._event.set()

    @property
    def is_cancelled(self) -> bool:
        """Return True if cancellation has been requested."""
        return self._event.is_set()

    def check(self) -> None:
        """Raise ``ToolCancelledError`` if cancellation has been requested."""
        if self._event.is_set():
            raise ToolCancelledError("Tool execution was cancelled")


class ToolCancelledError(Exception):
    """Raised when a tool execution is cancelled."""


# ---------------------------------------------------------------------------
# Execution trace
# ---------------------------------------------------------------------------


class ToolExecutionTrace(BaseModel):
    """Diagnostic trace for a single tool execution.

    Captures start/end timestamps, duration, result size, and any error
    information for debugging and performance analysis.
    """

    tool_name: str = Field(..., description="Namespaced tool name")
    started_at: str = Field(..., description="ISO-8601 start timestamp")
    ended_at: str | None = Field(default=None, description="ISO-8601 end timestamp")
    duration_ms: float | None = Field(
        default=None, description="Duration in milliseconds"
    )
    result_size_bytes: int | None = Field(
        default=None, description="Size of the result payload"
    )
    error: str | None = Field(default=None, description="Error message if failed")
    success: bool = Field(default=True, description="Whether execution succeeded")

    model_config = ConfigDict(str_strip_whitespace=True)


# ---------------------------------------------------------------------------
# Tool execution context
# ---------------------------------------------------------------------------


class ToolExecutionContext:
    """Context provided to every tool execution, regardless of source.

    Carries plan metadata, sandbox reference, resolved resource bindings,
    a change list, cancellation token, and execution traces.

    Parameters
    ----------
    plan_id:
        The ULID of the plan that owns this execution.
    plan_read_only:
        Whether the plan is read-only (restricts tool access).
    require_checkpoints:
        Whether the plan requires all tools to be checkpointable.
    safety_profile:
        Optional resolved safety profile for the plan.  When provided,
        the tool runtime enforces constraints such as
        ``allow_unsafe_tools``, ``allowed_skill_categories``,
        ``require_checkpoints``, ``require_sandbox``,
        ``require_human_approval``, cost limits, and retry limits
        from this profile.
    sandbox_id:
        Optional sandbox identifier for the execution.
    resources:
        Resolved resource bindings keyed by slot name.
    cancellation_token:
        Token for cancelling long-running tools.
    metadata:
        Additional plan/execution metadata.  The key
        ``human_approved`` (bool) is checked when the safety profile
        has ``require_human_approval=True``.
    accumulated_cost:
        Running total of cost (USD) for the current plan.  Updated
        by the caller after each tool execution.  Compared against
        the safety profile's ``max_cost_per_plan``.
    total_accumulated_cost:
        Running total of cost (USD) across all plans.  Updated by
        the caller.  Compared against ``max_total_cost``.
    step_retry_count:
        Number of retry attempts for the current action step.
        Updated by the caller.  Compared against
        ``max_retries_per_step``.
    """

    def __init__(
        self,
        *,
        plan_id: str,
        plan_read_only: bool = False,
        require_checkpoints: bool = False,
        safety_profile: SafetyProfile | None = None,
        sandbox_id: str | None = None,
        resources: dict[str, BoundResource] | None = None,
        cancellation_token: CancellationToken | None = None,
        metadata: dict[str, Any] | None = None,
        accumulated_cost: float = 0.0,
        total_accumulated_cost: float = 0.0,
        step_retry_count: int = 0,
    ) -> None:
        self.plan_id = plan_id
        self.plan_read_only = plan_read_only
        self.require_checkpoints = require_checkpoints
        self.safety_profile = safety_profile
        self.sandbox_id = sandbox_id
        self.resources: dict[str, BoundResource] = resources or {}
        self.cancellation_token = cancellation_token or CancellationToken()
        self.metadata: dict[str, Any] = metadata or {}
        self.accumulated_cost = accumulated_cost
        self.total_accumulated_cost = total_accumulated_cost
        self.step_retry_count = step_retry_count
        self.changes: list[Change] = []
        self.traces: list[ToolExecutionTrace] = []

    def record_change(self, change: Change) -> None:
        """Record a change made by a tool."""
        self.changes.append(change)

    def add_trace(self, trace: ToolExecutionTrace) -> None:
        """Add an execution trace entry."""
        self.traces.append(trace)

    def get_resource(self, slot_name: str) -> BoundResource:
        """Get a bound resource by slot name.

        Raises ``KeyError`` if the slot is not bound.
        """
        if slot_name not in self.resources:
            raise KeyError(
                f"No resource bound to slot '{slot_name}'. "
                f"Available slots: {sorted(self.resources)}"
            )
        return self.resources[slot_name]

    def as_summary(self) -> OrderedDict[str, Any]:
        """Return a summary dict for diagnostics and CLI rendering."""
        result: OrderedDict[str, Any] = OrderedDict()
        result["plan_id"] = self.plan_id
        result["plan_read_only"] = self.plan_read_only
        result["require_checkpoints"] = self.require_checkpoints
        result["has_safety_profile"] = self.safety_profile is not None
        result["sandbox_id"] = self.sandbox_id
        result["resource_count"] = len(self.resources)
        result["change_count"] = len(self.changes)
        result["trace_count"] = len(self.traces)
        result["accumulated_cost"] = self.accumulated_cost
        result["total_accumulated_cost"] = self.total_accumulated_cost
        result["step_retry_count"] = self.step_retry_count
        return result
