"""Resource handler protocol for CleverAgents.

Defines the :class:`ResourceHandler` protocol that all resource type
handlers must satisfy.  A handler bridges a :class:`Resource` domain
object to sandbox provisioning **and** to content CRUD operations:

Sandbox provisioning (M1):
    1. Reading the resource's ``location`` (the original filesystem path).
    2. Determining the sandbox strategy (from resource override or type default).
    3. Calling :meth:`SandboxManager.get_or_create_sandbox` to provision an
       isolated working directory.
    4. Returning a :class:`BoundResource` with ``sandbox_path`` populated.

Content CRUD and discovery (M6):
    5. ``read``  — read resource content from its resolved location.
    6. ``write`` — write/update resource content.
    7. ``delete`` — remove a resource.
    8. ``list_children`` — list direct child resources.
    9. ``diff``  — compare two resource states.
   10. ``discover_children`` — auto-discover child resources.

Sandbox and lifecycle methods (M6, issue #836):
   11. ``create_sandbox``  — create an isolated sandbox for the resource
   12. ``create_checkpoint`` — snapshot the current resource state
   13. ``rollback_to``  — restore a resource to a prior checkpoint
   14. ``project_access`` — check access permissions within project scope

Based on:
    - implementation_plan.md group M1.resource-handlers (L2254-L2271)
    - docs/specification.md Resource Handler architecture
    - Issue #827 — ResourceHandler CRUD and discovery methods
    - Issue #836 — ResourceHandler sandbox and checkpoint methods
"""

from __future__ import annotations

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

from cleveragents.domain.models.core.resource import Resource
from cleveragents.infrastructure.sandbox.manager import SandboxManager
from cleveragents.tool.context import BoundResource

# ---------------------------------------------------------------------------
# Result types for CRUD and discovery operations
# ---------------------------------------------------------------------------


@dataclass(frozen=True, slots=True)
class Content:
    """Result of a ``read`` operation.

    Attributes:
        data: The raw content bytes.
        encoding: Character encoding (e.g. ``"utf-8"``).  ``None``
            for binary content.
        content_hash: Optional hash (e.g. SHA-256 hex digest) of *data*.
        metadata: Arbitrary handler-specific metadata.
    """

    data: bytes
    encoding: str | None = "utf-8"
    content_hash: str | None = None
    metadata: dict[str, str] = field(default_factory=dict)

    @property
    def text(self) -> str:
        """Decode *data* using *encoding*.

        Raises:
            UnicodeDecodeError: If *data* cannot be decoded.
            ValueError: If *encoding* is ``None`` (binary content).
        """
        if self.encoding is None:
            raise ValueError("Cannot decode binary content as text")
        return self.data.decode(self.encoding)


@dataclass(frozen=True, slots=True)
class WriteResult:
    """Result of a ``write`` operation.

    Attributes:
        success: Whether the write completed without error.
        bytes_written: Number of bytes written.
        content_hash: Hash of the written content.
        message: Human-readable status message.
    """

    success: bool
    bytes_written: int = 0
    content_hash: str | None = None
    message: str = ""


@dataclass(frozen=True, slots=True)
class DeleteResult:
    """Result of a ``delete`` operation.

    Attributes:
        success: Whether the deletion completed without error.
        message: Human-readable status message.
    """

    success: bool
    message: str = ""


@dataclass(frozen=True, slots=True)
class DiffResult:
    """Result of a ``diff`` operation.

    Attributes:
        has_changes: ``True`` if the two states differ.
        unified_diff: Unified diff string (empty if no changes).
        files_changed: Number of files with differences.
        insertions: Number of added lines.
        deletions: Number of removed lines.
    """

    has_changes: bool
    unified_diff: str = ""
    files_changed: int = 0
    insertions: int = 0
    deletions: int = 0


# ---------------------------------------------------------------------------
# Result types for sandbox, checkpoint, and access operations (#836)
# ---------------------------------------------------------------------------


@dataclass(frozen=True, slots=True)
class SandboxResult:
    """Result of a ``create_sandbox`` operation."""

    sandbox_id: str
    sandbox_path: str
    strategy: str
    created: bool = True
    message: str = ""


@dataclass(frozen=True, slots=True)
class CheckpointResult:
    """Result of a ``create_checkpoint`` operation."""

    checkpoint_id: str
    plan_id: str
    timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
    snapshot_path: str = ""
    message: str = ""


@dataclass(frozen=True, slots=True)
class RollbackResult:
    """Result of a ``rollback_to`` operation."""

    success: bool
    checkpoint_id: str
    restored_files: int = 0
    message: str = ""


@dataclass(frozen=True, slots=True)
class AccessResult:
    """Result of a ``project_access`` operation."""

    permitted: bool
    principal: str = ""
    action: str = ""
    scope: str = ""
    reason: str = ""


# ---------------------------------------------------------------------------
# Protocol
# ---------------------------------------------------------------------------


@runtime_checkable
class ResourceHandler(Protocol):
    """Protocol for resource type handlers.

    Each handler knows how to resolve a specific resource type into a
    sandboxed working path, and optionally provides content CRUD and
    discovery operations.

    Implementations are registered via the ``handler`` field on
    :class:`ResourceTypeSpec` using the ``module:ClassName`` string
    format.

    Lifecycle::

        handler = resolve_handler(
            "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler"
        )
        bound = handler.resolve(
            resource=resource,
            plan_id="01ARZ3...",
            slot_name="repo",
            sandbox_manager=sandbox_manager,
            access="read_write",
        )
        # bound.sandbox_path is now populated
    """

    def resolve(
        self,
        *,
        resource: Resource,
        plan_id: str,
        slot_name: str,
        sandbox_manager: SandboxManager,
        access: str = "read_only",
    ) -> BoundResource:
        """Resolve a resource into a sandbox-backed BoundResource.

        Args:
            resource: The resource domain object to resolve.
            plan_id: The plan requesting the sandbox.
            slot_name: Name of the tool resource slot being filled.
            sandbox_manager: The sandbox lifecycle manager.
            access: Access mode (``read_only`` or ``read_write``).

        Returns:
            A :class:`BoundResource` with ``sandbox_path`` populated
            from the provisioned sandbox.

        Raises:
            ValueError: If the resource lacks a location or has an
                incompatible type.
            SandboxError: If sandbox creation fails.
        """
        ...

    def read(self, *, resource: Resource, path: str = "") -> Content:
        """Read content from a resource.

        Args:
            resource: The resource to read from.
            path: Relative path within the resource (empty string for
                the resource root or a single-file resource).

        Returns:
            A :class:`Content` object with the raw bytes and metadata.

        Raises:
            FileNotFoundError: If *path* does not exist within the
                resource.
            PermissionError: If the resource is not readable.
        """
        ...

    def write(self, *, resource: Resource, path: str, data: bytes) -> WriteResult:
        """Write content to a resource.

        Args:
            resource: The resource to write to.
            path: Relative path within the resource.
            data: Raw bytes to write.

        Returns:
            A :class:`WriteResult` indicating success and bytes written.

        Raises:
            PermissionError: If the resource is read-only.
            OSError: If the write fails at the OS level.
        """
        ...

    def delete(self, *, resource: Resource, path: str = "") -> DeleteResult:
        """Delete a resource or a path within it.

        Args:
            resource: The resource to delete from.
            path: Relative path within the resource.  Empty string
                deletes the resource root.

        Returns:
            A :class:`DeleteResult` indicating success.

        Raises:
            PermissionError: If the resource is read-only.
            FileNotFoundError: If *path* does not exist.
        """
        ...

    def list_children(self, *, resource: Resource) -> list[str]:
        """List direct children of a resource.

        Args:
            resource: The parent resource.

        Returns:
            Sorted list of child paths relative to the resource root.
        """
        ...

    def diff(self, *, resource: Resource, other_location: str) -> DiffResult:
        """Compare a resource against another location.

        Args:
            resource: The resource whose current state to compare.
            other_location: Filesystem path to compare against.

        Returns:
            A :class:`DiffResult` summarising the differences.
        """
        ...

    def discover_children(self, *, resource: Resource) -> list[Resource]:
        """Auto-discover child resources beneath this resource."""
        ...

    # -- Sandbox lifecycle methods (issue #836) ----------------------------

    def create_sandbox(
        self,
        *,
        resource: Resource,
        plan_id: str,
        sandbox_manager: SandboxManager,
    ) -> SandboxResult:
        """Create an isolated sandbox for the resource (idempotent)."""
        ...

    def create_checkpoint(
        self,
        *,
        resource: Resource,
        plan_id: str,
        sandbox_manager: SandboxManager,
        phase: str = "execution",
    ) -> CheckpointResult:
        """Create a checkpoint of the current resource state."""
        ...

    def rollback_to(
        self,
        *,
        resource: Resource,
        plan_id: str,
        checkpoint_id: str,
        sandbox_manager: SandboxManager,
    ) -> RollbackResult:
        """Rollback a resource to a prior checkpoint state."""
        ...

    def project_access(
        self,
        *,
        resource: Resource,
        principal: str,
        action: str = "read",
        project_id: str = "",
    ) -> AccessResult:
        """Check access permissions for the resource."""
        ...

    def content_hash(
        self,
        resource: Resource,
        *,
        algorithm: str = "sha256",
    ) -> str:
        """Compute a deterministic hash of the resource's content.

        The hash is **content-only**: metadata changes (timestamps,
        permissions) must not affect the result.  Same content must
        always produce the same hash.

        Args:
            resource: The resource to hash.
            algorithm: Hash algorithm name (default ``sha256``).
                Must be accepted by :func:`hashlib.new`.

        Returns:
            Hex-encoded hash digest, or the sentinel value
            ``EMPTY_CONTENT_HASH`` for empty or missing resources.
        """
        ...
