"""Skill execution context for CleverAgents v3.

Provides the runtime context for skill execution, including plan and
project identifiers, sandbox path resolution, resource bindings, change
tracking, and read-only enforcement.

## Overview

- **SkillContext** -- Mutable runtime context that accumulates state
  during skill execution.  Carries plan/project identifiers, a sandbox
  root ``Path``, bound resources, and a change tracker for recording
  tool invocations.

## Read-Only Enforcement

When ``read_only=True``, the ``enforce_write_guard`` method raises a
``SkillExecutionError`` with error type ``PERMISSION_DENIED`` for any
tool that attempts to write.  This is the primary mechanism for
enforcing plan-level read restrictions.

Based on ``docs/specification.md`` and ``implementation_plan.md`` task
C3.context.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

from cleveragents.skills.protocol import SkillError, SkillErrorType

# ---------------------------------------------------------------------------
# Exception wrapper
# ---------------------------------------------------------------------------


class SkillExecutionError(Exception):
    """Exception raised for skill execution failures.

    Wraps a ``SkillError`` payload so it can be raised and caught
    while still carrying structured error data.
    """

    def __init__(self, skill_error: SkillError) -> None:
        self.skill_error = skill_error
        super().__init__(skill_error.message)

    @property
    def error_type(self) -> SkillErrorType:
        """Return the error type from the wrapped error."""
        return self.skill_error.error_type

    @property
    def tool_name(self) -> str | None:
        """Return the tool name from the wrapped error."""
        return self.skill_error.tool_name

    @property
    def details(self) -> dict[str, Any]:
        """Return the details from the wrapped error."""
        return self.skill_error.details


# ---------------------------------------------------------------------------
# Change tracker type alias
# ---------------------------------------------------------------------------

ToolInvocationRecord = dict[str, Any]
"""A single tool invocation record stored in the change tracker."""


# ---------------------------------------------------------------------------
# SkillContext
# ---------------------------------------------------------------------------


class SkillContext:
    """Runtime context for skill execution.

    Accumulates state during a skill's lifecycle.  **Not frozen** --
    mutable by design so that tool invocations can be recorded and
    resources can be resolved lazily.

    Attributes:
        plan_id: Identifier for the current plan.
        project_id: Identifier for the current project.
        sandbox_path: Root path to the execution sandbox.
        change_tracker: List of recorded tool invocation records.
        resource_bindings: Mapping of resource names to bound values.
        read_only: Whether the context forbids write operations.
        metadata: Arbitrary plan/project metadata dict.
    """

    def __init__(
        self,
        plan_id: str,
        project_id: str,
        sandbox_path: Path,
        *,
        change_tracker: list[ToolInvocationRecord] | None = None,
        resource_bindings: dict[str, Any] | None = None,
        read_only: bool = False,
        metadata: dict[str, Any] | None = None,
    ) -> None:
        self.plan_id = plan_id
        self.project_id = project_id
        self.sandbox_path = sandbox_path
        self.change_tracker: list[ToolInvocationRecord] = (
            change_tracker if change_tracker is not None else []
        )
        self.resource_bindings: dict[str, Any] = (
            resource_bindings if resource_bindings is not None else {}
        )
        self.read_only = read_only
        self.metadata: dict[str, Any] = metadata if metadata is not None else {}

    # -- Resource resolution ---------------------------------------------------

    def resolve_resource(self, name: str) -> Any:
        """Look up a bound resource by name.

        Args:
            name: The resource binding key.

        Returns:
            The bound resource value.

        Raises:
            SkillExecutionError: With ``RESOLUTION_FAILURE`` if the
                resource name is not found in the bindings.
        """
        if name not in self.resource_bindings:
            raise SkillExecutionError(
                SkillError(
                    error_type=SkillErrorType.RESOLUTION_FAILURE,
                    message=f"Resource '{name}' not found in context bindings",
                    skill_name=f"context/{self.plan_id}",
                    details={"resource_name": name},
                )
            )
        return self.resource_bindings[name]

    # -- Plan metadata ---------------------------------------------------------

    def get_plan_metadata(self) -> dict[str, Any]:
        """Return plan-related metadata dict.

        Returns:
            A dict containing ``plan_id``, ``project_id``, and any
            additional metadata stored on the context.
        """
        return {
            "plan_id": self.plan_id,
            "project_id": self.project_id,
            **self.metadata,
        }

    # -- Sandbox path ----------------------------------------------------------

    def get_sandbox_path(self) -> Path:
        """Return the sandbox root path.

        Returns:
            The ``Path`` to the sandbox root directory.
        """
        return self.sandbox_path

    # -- Read-only check -------------------------------------------------------

    def is_read_only(self) -> bool:
        """Check whether this context forbids write operations.

        Returns:
            ``True`` if the context is read-only.
        """
        return self.read_only

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

    def register_tool_invocation(
        self,
        tool_name: str,
        input_data: Any,
        output_data: Any,
        duration_ms: float,
    ) -> None:
        """Record a tool invocation in the change tracker.

        Args:
            tool_name: Name of the invoked tool.
            input_data: Input data passed to the tool.
            output_data: Output data returned by the tool.
            duration_ms: Execution duration in milliseconds.
        """
        record: ToolInvocationRecord = {
            "tool_name": tool_name,
            "input_data": input_data,
            "output_data": output_data,
            "duration_ms": duration_ms,
            "plan_id": self.plan_id,
            "project_id": self.project_id,
        }
        self.change_tracker.append(record)

    # -- Write guard -----------------------------------------------------------

    def enforce_write_guard(self, tool_name: str) -> None:
        """Raise ``SkillExecutionError(PERMISSION_DENIED)`` if read-only.

        Should be called before any tool that performs write operations.

        Args:
            tool_name: The name of the tool attempting to write.

        Raises:
            SkillExecutionError: With ``PERMISSION_DENIED`` if the
                context is read-only.
        """
        if self.read_only:
            raise SkillExecutionError(
                SkillError(
                    error_type=SkillErrorType.PERMISSION_DENIED,
                    message=(
                        f"Write operation denied: tool '{tool_name}' cannot "
                        f"write in read-only context (plan={self.plan_id})"
                    ),
                    skill_name=f"context/{self.plan_id}",
                    tool_name=tool_name,
                    details={
                        "plan_id": self.plan_id,
                        "project_id": self.project_id,
                        "read_only": True,
                    },
                )
            )
