"""Custom sandbox strategy Protocol and supporting models.

Defines the ``SandboxStrategyProtocol`` that custom sandbox strategies must
implement, along with ``SandboxRef`` and ``DiffView`` value objects used in
the Protocol method signatures.

The Protocol specifies 9 methods covering the full sandbox lifecycle:
create, read, write, diff, commit, rollback, checkpoint,
restore_checkpoint, cleanup.

Custom strategies are registered via configuration::

    sandbox.custom_strategies.<name>.module = "my_module.path"
    sandbox.custom_strategies.<name>.class  = "MySandboxStrategy"

See Also:
    - :class:`~cleveragents.infrastructure.sandbox.protocol.Sandbox`
      The existing infrastructure-level sandbox protocol.
    - :class:`~cleveragents.domain.models.core.resource.SandboxStrategy`
      The StrEnum defining built-in strategy names.
    - Specification § Custom Sandbox Strategies (lines 46171-46186)

Based on issue #586.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from typing import Protocol, runtime_checkable

from pydantic import BaseModel, ConfigDict, Field

from cleveragents.domain.models.core.resource import Resource

# ---------------------------------------------------------------------------
# SandboxRef — opaque reference to a sandbox instance
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class SandboxRef:
    """Opaque reference to a sandbox instance.

    Returned by :meth:`SandboxStrategyProtocol.create` and passed to
    all subsequent operations.  The ``sandbox_id`` is a unique
    identifier (typically a ULID); ``metadata`` allows strategy
    implementations to carry extra state across calls.

    Attributes:
        sandbox_id: Unique identifier for this sandbox instance.
        plan_id: The plan that owns this sandbox.
        resource_id: The resource being sandboxed.
        created_at: When the sandbox was created.
        metadata: Strategy-specific opaque data.
    """

    sandbox_id: str
    plan_id: str
    resource_id: str
    created_at: datetime
    metadata: dict[str, object] = field(default_factory=dict)


# ---------------------------------------------------------------------------
# DiffView — structured view of sandbox changes
# ---------------------------------------------------------------------------


class DiffEntry(BaseModel):
    """A single file-level diff entry within a :class:`DiffView`.

    Attributes:
        path: Relative path of the changed file.
        operation: Type of change (``added``, ``modified``, ``deleted``).
        before: Content before the change (``None`` for additions).
        after: Content after the change (``None`` for deletions).
    """

    path: str = Field(..., min_length=1, description="Relative file path")
    operation: str = Field(
        ...,
        description="Change type: 'added', 'modified', or 'deleted'",
    )
    before: bytes | None = Field(
        default=None,
        description="Content before change (None for additions)",
    )
    after: bytes | None = Field(
        default=None,
        description="Content after change (None for deletions)",
    )

    model_config = ConfigDict(frozen=True)


class DiffView(BaseModel):
    """Structured view of all changes in a sandbox.

    Produced by :meth:`SandboxStrategyProtocol.diff`.

    Attributes:
        sandbox_id: The sandbox these diffs belong to.
        entries: Individual file-level diff entries.
        summary: Human-readable summary of the changes.
    """

    sandbox_id: str = Field(..., description="Sandbox identifier")
    entries: list[DiffEntry] = Field(
        default_factory=list,
        description="Per-file diff entries",
    )
    summary: str = Field(
        default="",
        description="Human-readable change summary",
    )

    model_config = ConfigDict(frozen=True)


# ---------------------------------------------------------------------------
# SandboxStrategyProtocol — the 9-method custom strategy interface
# ---------------------------------------------------------------------------


@runtime_checkable
class SandboxStrategyProtocol(Protocol):
    """Protocol for custom sandbox isolation strategies.

    All custom sandbox strategies must implement these 9 methods.
    Strategies are registered via configuration and resolved at
    runtime by the :class:`SandboxStrategyRegistry`.

    Lifecycle::

        ref = strategy.create(plan_id, resource)   # create sandbox
        data = strategy.read(ref, "file.txt")       # read from sandbox
        change = strategy.write(ref, "f.txt", b"x") # write to sandbox
        view = strategy.diff(ref)                    # inspect changes
        strategy.checkpoint(ref, "cp-1")             # save checkpoint
        strategy.restore_checkpoint(ref, "cp-1")     # restore checkpoint
        strategy.commit(ref)                         # apply changes
        strategy.rollback(ref)                       # discard changes
        strategy.cleanup(ref)                        # release resources
    """

    def create(self, plan_id: str, resource: Resource) -> SandboxRef:
        """Create a new sandbox for the given plan and resource.

        Args:
            plan_id: Identifier of the plan requesting the sandbox.
            resource: The resource to sandbox.

        Returns:
            An opaque :class:`SandboxRef` for subsequent operations.
        """
        ...

    def read(self, ref: SandboxRef, path: str) -> bytes:
        """Read file content from the sandbox.

        Args:
            ref: Sandbox reference from :meth:`create`.
            path: Relative path within the sandbox.

        Returns:
            Raw file content as bytes.
        """
        ...

    def write(self, ref: SandboxRef, path: str, content: bytes) -> DiffEntry:
        """Write content to a file in the sandbox.

        Args:
            ref: Sandbox reference from :meth:`create`.
            path: Relative path within the sandbox.
            content: Raw content to write.

        Returns:
            A :class:`DiffEntry` describing the change.
        """
        ...

    def diff(self, ref: SandboxRef) -> DiffView:
        """Compute a structured diff of all sandbox changes.

        Args:
            ref: Sandbox reference from :meth:`create`.

        Returns:
            A :class:`DiffView` with per-file entries.
        """
        ...

    def commit(self, ref: SandboxRef) -> None:
        """Apply sandbox changes to the original resource.

        Args:
            ref: Sandbox reference from :meth:`create`.
        """
        ...

    def rollback(self, ref: SandboxRef) -> None:
        """Discard all sandbox changes.

        Args:
            ref: Sandbox reference from :meth:`create`.
        """
        ...

    def checkpoint(self, ref: SandboxRef, checkpoint_id: str) -> None:
        """Save a named checkpoint of the current sandbox state.

        Args:
            ref: Sandbox reference from :meth:`create`.
            checkpoint_id: Unique identifier for the checkpoint.
        """
        ...

    def restore_checkpoint(self, ref: SandboxRef, checkpoint_id: str) -> None:
        """Restore the sandbox to a previously saved checkpoint.

        Args:
            ref: Sandbox reference from :meth:`create`.
            checkpoint_id: Identifier of the checkpoint to restore.
        """
        ...

    def cleanup(self, ref: SandboxRef) -> None:
        """Release all sandbox resources and artefacts.

        Args:
            ref: Sandbox reference from :meth:`create`.
        """
        ...
