"""Adapter that wraps existing Sandbox implementations as SandboxStrategyProtocol.

Bridges the existing :class:`Sandbox` protocol (infrastructure layer) to the
new :class:`SandboxStrategyProtocol` (domain layer).  This allows built-in
sandbox implementations (NoSandbox, CopyOnWriteSandbox, GitWorktreeSandbox,
TransactionSandbox) to be used through the unified custom-strategy API.

Based on issue #586.
"""

from __future__ import annotations

import logging
import os
from datetime import datetime

from cleveragents.domain.models.core.resource import Resource
from cleveragents.domain.models.core.sandbox_strategy import (
    DiffEntry,
    DiffView,
    SandboxRef,
)
from cleveragents.infrastructure.sandbox.factory import (
    SandboxFactory,
    SandboxStrategyStr,
)
from cleveragents.infrastructure.sandbox.protocol import (
    Sandbox,
)

logger = logging.getLogger(__name__)


class BuiltInSandboxStrategyAdapter:
    """Adapts a built-in :class:`Sandbox` implementation to the 9-method Protocol.

    Wraps the existing ``Sandbox`` lifecycle (create, get_path, commit,
    rollback, cleanup) and adds read/write/diff/checkpoint/restore
    operations on top.  Checkpoint storage is in-memory per adapter
    instance.

    This adapter satisfies :class:`SandboxStrategyProtocol` structurally.

    Args:
        strategy_name: The built-in strategy name (e.g. ``"none"``).
        factory: Optional :class:`SandboxFactory` to create sandbox instances.
    """

    def __init__(
        self,
        strategy_name: SandboxStrategyStr = "none",
        factory: SandboxFactory | None = None,
    ) -> None:
        self._strategy_name: SandboxStrategyStr = strategy_name
        self._factory = factory or SandboxFactory()
        self._sandboxes: dict[str, Sandbox] = {}
        self._checkpoints: dict[str, dict[str, bytes]] = {}

    def create(self, plan_id: str, resource: Resource) -> SandboxRef:
        """Create a sandbox using the built-in factory and return a ref.

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

        Returns:
            A :class:`SandboxRef` wrapping the internal sandbox instance.
        """
        location = resource.location or ""
        sandbox = self._factory.create_sandbox(
            resource_id=resource.resource_id,
            original_path=location,
            sandbox_strategy=self._strategy_name,
        )
        ctx = sandbox.create(plan_id)
        self._sandboxes[ctx.sandbox_id] = sandbox

        return SandboxRef(
            sandbox_id=ctx.sandbox_id,
            plan_id=plan_id,
            resource_id=resource.resource_id,
            created_at=datetime.now(),
            metadata={"strategy": self._strategy_name},
        )

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

        Args:
            ref: Sandbox reference.
            path: Relative file path.

        Returns:
            File content as bytes.

        Raises:
            FileNotFoundError: If the file does not exist.
            KeyError: If the sandbox ref is unknown.
        """
        sandbox = self._sandboxes[ref.sandbox_id]
        full_path = sandbox.get_path(path)
        with open(full_path, "rb") as fh:
            return fh.read()

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

        Args:
            ref: Sandbox reference.
            path: Relative file path.
            content: Content to write.

        Returns:
            A :class:`DiffEntry` describing the change.
        """
        sandbox = self._sandboxes[ref.sandbox_id]
        full_path = sandbox.get_path(path)

        existed = os.path.exists(full_path)
        before: bytes | None = None
        if existed:
            with open(full_path, "rb") as fh:
                before = fh.read()

        parent_dir = os.path.dirname(full_path)
        if parent_dir:
            os.makedirs(parent_dir, exist_ok=True)

        with open(full_path, "wb") as fh:
            fh.write(content)

        operation = "modified" if existed else "added"
        return DiffEntry(
            path=path,
            operation=operation,
            before=before,
            after=content,
        )

    def diff(self, ref: SandboxRef) -> DiffView:
        """Return a diff view of sandbox changes.

        For built-in strategies, returns a minimal summary since the
        underlying Sandbox protocol does not expose diff information
        until commit.

        Args:
            ref: Sandbox reference.

        Returns:
            A :class:`DiffView` (may be empty for built-in strategies).
        """
        return DiffView(
            sandbox_id=ref.sandbox_id,
            entries=[],
            summary="Built-in strategy; diff computed at commit time.",
        )

    def commit(self, ref: SandboxRef) -> None:
        """Commit sandbox changes via the underlying Sandbox.

        Args:
            ref: Sandbox reference.
        """
        sandbox = self._sandboxes[ref.sandbox_id]
        sandbox.commit()

    def rollback(self, ref: SandboxRef) -> None:
        """Rollback sandbox changes via the underlying Sandbox.

        Args:
            ref: Sandbox reference.
        """
        sandbox = self._sandboxes[ref.sandbox_id]
        sandbox.rollback()

    def checkpoint(self, ref: SandboxRef, checkpoint_id: str) -> None:
        """Save a named checkpoint (in-memory snapshot for adapter).

        Args:
            ref: Sandbox reference.
            checkpoint_id: Unique checkpoint identifier.
        """
        key = f"{ref.sandbox_id}:{checkpoint_id}"
        sandbox = self._sandboxes[ref.sandbox_id]

        # Snapshot current sandbox state if sandbox is filesystem-based
        ctx = sandbox.context
        snapshot: dict[str, bytes] = {}
        if ctx is not None and os.path.isdir(ctx.sandbox_path):
            for dirpath, _dirs, files in os.walk(ctx.sandbox_path):
                for fname in files:
                    full = os.path.join(dirpath, fname)
                    rel = os.path.relpath(full, ctx.sandbox_path)
                    with open(full, "rb") as fh:
                        snapshot[rel] = fh.read()

        self._checkpoints[key] = snapshot
        logger.debug(
            "Checkpoint saved: sandbox=%s checkpoint=%s files=%d",
            ref.sandbox_id,
            checkpoint_id,
            len(snapshot),
        )

    def restore_checkpoint(self, ref: SandboxRef, checkpoint_id: str) -> None:
        """Restore sandbox state from a named checkpoint.

        Args:
            ref: Sandbox reference.
            checkpoint_id: Identifier of the checkpoint to restore.

        Raises:
            KeyError: If the checkpoint does not exist.
        """
        key = f"{ref.sandbox_id}:{checkpoint_id}"
        snapshot = self._checkpoints[key]

        sandbox = self._sandboxes[ref.sandbox_id]
        ctx = sandbox.context
        if ctx is not None and os.path.isdir(ctx.sandbox_path):
            # Restore files from snapshot
            for rel_path, content in snapshot.items():
                full = os.path.join(ctx.sandbox_path, rel_path)
                parent = os.path.dirname(full)
                if parent:
                    os.makedirs(parent, exist_ok=True)
                with open(full, "wb") as fh:
                    fh.write(content)

        logger.debug(
            "Checkpoint restored: sandbox=%s checkpoint=%s",
            ref.sandbox_id,
            checkpoint_id,
        )

    def cleanup(self, ref: SandboxRef) -> None:
        """Cleanup sandbox resources.

        Args:
            ref: Sandbox reference.
        """
        sandbox = self._sandboxes.pop(ref.sandbox_id, None)
        if sandbox is not None:
            sandbox.cleanup()

        # Remove associated checkpoints
        keys_to_remove = [
            k for k in self._checkpoints if k.startswith(f"{ref.sandbox_id}:")
        ]
        for k in keys_to_remove:
            del self._checkpoints[k]
