"""No-op sandbox for resources that cannot be sandboxed.

Used for API endpoints, some cloud resources, and any resource where
``SandboxStrategy.NONE`` is configured.  Changes are applied immediately
and rollback is not possible.

Stage B3.5 of the implementation plan.
"""

from __future__ import annotations

import logging
from datetime import datetime

from ulid import ULID

from cleveragents.infrastructure.sandbox.protocol import (
    CommitResult,
    SandboxContext,
    SandboxRollbackError,
    SandboxStateError,
    SandboxStatus,
)

logger = logging.getLogger(__name__)


class NoSandbox:
    """Passthrough sandbox for non-sandboxable resources.

    Changes are applied immediately to the resource -- there is no isolation.
    ``rollback`` always raises because there is nothing to undo.

    Implements the :class:`~cleveragents.infrastructure.sandbox.protocol.Sandbox`
    protocol.
    """

    def __init__(self, resource_id: str, original_path: str) -> None:
        """Initialise a no-op sandbox.

        Args:
            resource_id: Identifier of the resource being wrapped.
            original_path: The original resource location / path.

        Raises:
            ValueError: If *resource_id* is empty.
            ValueError: If *original_path* is empty.
        """
        if not resource_id:
            raise ValueError("resource_id cannot be empty")
        if not original_path:
            raise ValueError("original_path cannot be empty")

        self._sandbox_id: str = str(ULID())
        self._resource_id: str = resource_id
        self._original_path: str = original_path
        self._status: SandboxStatus = SandboxStatus.PENDING
        self._context: SandboxContext | None = None

    # -- protocol properties -------------------------------------------------

    @property
    def sandbox_id(self) -> str:
        """Unique identifier for this sandbox instance."""
        return self._sandbox_id

    @property
    def status(self) -> SandboxStatus:
        """Current lifecycle status."""
        return self._status

    @property
    def context(self) -> SandboxContext | None:
        """Context after creation, ``None`` before ``create``."""
        return self._context

    # -- protocol methods ----------------------------------------------------

    def create(self, plan_id: str) -> SandboxContext:
        """Register the sandbox (no actual isolation is created).

        Args:
            plan_id: The plan that owns this sandbox.

        Returns:
            A :class:`SandboxContext` pointing at the *original* path.

        Raises:
            ValueError: If *plan_id* is empty.
            SandboxStateError: If not in ``PENDING`` status.
        """
        if not plan_id:
            raise ValueError("plan_id cannot be empty")

        SandboxStatus.assert_transition(self._status, SandboxStatus.CREATED)

        logger.warning(
            "Resource %s is not sandboxed -- changes are immediate and "
            "irreversible (plan_id=%s, sandbox_id=%s)",
            self._resource_id,
            plan_id,
            self._sandbox_id,
        )

        self._context = SandboxContext(
            sandbox_id=self._sandbox_id,
            sandbox_path=self._original_path,
            original_path=self._original_path,
            resource_id=self._resource_id,
            plan_id=plan_id,
            created_at=datetime.now(),
            metadata={"strategy": "none"},
        )
        self._status = SandboxStatus.CREATED
        return self._context

    def get_path(self, resource_path: str) -> str:
        """Return the original resource path unchanged.

        Args:
            resource_path: Path relative to the resource root.

        Returns:
            The *original_path* joined with *resource_path*.

        Raises:
            SandboxStateError: If sandbox has not been created or has been
                cleaned up.
            ValueError: If *resource_path* attempts directory traversal.
        """
        if self._status not in (
            SandboxStatus.CREATED,
            SandboxStatus.ACTIVE,
        ):
            raise SandboxStateError(
                f"Cannot resolve path in status {self._status.value}"
            )

        # Guard against path traversal
        if ".." in resource_path.split("/"):
            raise ValueError(f"Path traversal not allowed: {resource_path}")

        # Mark as active on first path resolution
        if self._status == SandboxStatus.CREATED:
            self._status = SandboxStatus.ACTIVE

        import os

        return os.path.join(self._original_path, resource_path)

    def commit(self, message: str | None = None) -> CommitResult:
        """Return a successful result (changes were already applied in-place).

        Args:
            message: Ignored for ``NoSandbox``.

        Returns:
            A :class:`CommitResult` with ``success=True`` and empty file lists
            (tracking is not possible without real sandboxing).

        Raises:
            SandboxStateError: If sandbox is not in a committable status.
        """
        if self._status not in (
            SandboxStatus.CREATED,
            SandboxStatus.ACTIVE,
        ):
            raise SandboxStateError(f"Cannot commit from status {self._status.value}")

        SandboxStatus.assert_transition(self._status, SandboxStatus.COMMITTED)

        logger.info(
            "NoSandbox commit (no-op) for resource %s (sandbox_id=%s)",
            self._resource_id,
            self._sandbox_id,
        )

        self._status = SandboxStatus.COMMITTED
        return CommitResult(
            sandbox_id=self._sandbox_id,
            success=True,
            commit_ref=None,
            changed_files=[],
            added_files=[],
            deleted_files=[],
            error=None,
            timestamp=datetime.now(),
        )

    def rollback(self) -> None:
        """Raise because rollback is not possible without sandboxing.

        Raises:
            SandboxRollbackError: Always, since changes are immediate.
        """
        raise SandboxRollbackError(
            f"Rollback is not possible for non-sandboxed resource "
            f"{self._resource_id} (sandbox_id={self._sandbox_id}). "
            f"Changes have been applied immediately."
        )

    def cleanup(self) -> None:
        """Transition to ``CLEANED_UP`` (no artefacts to remove).

        This method is idempotent -- calling it multiple times is safe.

        Raises:
            SandboxError: On unexpected errors during cleanup.
        """
        if self._status == SandboxStatus.CLEANED_UP:
            return

        logger.debug(
            "NoSandbox cleanup (no-op) for resource %s (sandbox_id=%s)",
            self._resource_id,
            self._sandbox_id,
        )
        self._status = SandboxStatus.CLEANED_UP
