"""Sandbox directories cache for CleverAgents.

Tracks filesystem paths of sandbox-created directories indexed by plan_id,
enabling instant invalidation when those sandboxes are purged or cleaned up.
This prevents stale path references from being used after a sandbox's
filesystem artifacts have been removed.

Stage B3.7 supplementary -- introduced for #7527 (PR #10989).
"""

from __future__ import annotations

import logging
import threading

logger = logging.getLogger(__name__)


class SandboxDirsCache:
    """Thread-safe cache of sandbox directory paths, keyed by plan_id.

    When a sandbox is created, its filesystem path is recorded here.
    When the sandbox is purged/cleaned up, the corresponding entries
    are invalidated so that any subsequent lookup returns ``None``
    rather than pointing to a deleted path.

    Thread-safe: all mutable state is protected by ``_lock`` (an RLock).

    Usage::

        cache = SandboxDirsCache()
        cache.record("plan-001", "/tmp/sandboxes/worktree-abc")
        # ... sandbox is purged ...
        cache.purge("plan-001")  # clears all dirs for this plan
        cache.get("plan-001", "worktree-abc")  # -> None (invalidated)
    """

    def __init__(self) -> None:
        """Initialise the sandbox directories cache.

        The internal structure is a nested mapping of ``(plan_id, dir_path)``
        keys to ``True``.  All access is serialised by ``_lock``.
        """
        self._dirs: dict[str, set[str]] = {}
        self._lock: threading.RLock = threading.RLock()

    def record(self, plan_id: str, dir_path: str) -> None:
        """Record a sandbox directory path for a plan.

        Args:
            plan_id: Identifier of the plan that owns this sandbox.
            dir_path: Absolute filesystem path to the sandbox directory.

        Raises:
            ValueError: If ``plan_id`` or ``dir_path`` is empty.
        """
        if not plan_id:
            raise ValueError("plan_id cannot be empty")
        if not dir_path:
            raise ValueError("dir_path cannot be empty")

        with self._lock:
            self._dirs.setdefault(plan_id, set()).add(dir_path)
            logger.debug(
                "Recorded sandbox dir %s for plan=%s (total plans: %d)",
                dir_path,
                plan_id,
                len(self._dirs),
            )

    def purge(self, plan_id: str) -> list[str]:
        """Remove all recorded directories for a given plan.

        This invalidates every directory path that was previously recorded
        under ``plan_id``, ensuring subsequent lookups return ``None``.

        Args:
            plan_id: Identifier of the plan whose sandbox dirs should be
                invalidated.

        Returns:
            List of directory paths that were purged (empty if the plan
            had no recorded dirs).
        """
        with self._lock:
            removed = list(self._dirs.pop(plan_id, set()))

        if removed:
            logger.info("Purged %d sandbox dir(s) for plan=%s", len(removed), plan_id)

        return removed

    def get(self, plan_id: str, dir_path: str) -> bool:
        """Check whether a sandbox directory is still recorded for *plan_id*.

        Args:
            plan_id: Identifier of the plan to check.
            dir_path: Sandbox directory path to look up.

        Returns:
            ``True`` if the directory is still recorded (not yet purged).

        Raises:
            ValueError: If ``plan_id`` or ``dir_path`` is empty.
        """
        if not plan_id:
            raise ValueError("plan_id cannot be empty")
        if not dir_path:
            raise ValueError("dir_path cannot be empty")

        with self._lock:
            return dir_path in self._dirs.get(plan_id, set())

    @property
    def plan_count(self) -> int:
        """Return the number of distinct plans tracked."""
        with self._lock:
            return len(self._dirs)

    @property
    def dir_count(self) -> int:
        """Return the total number of recorded directory paths."""
        with self._lock:
            return sum(len(paths) for paths in self._dirs.values())

    def clear(self) -> None:
        """Remove all tracked plans and their directories."""
        with self._lock:
            count = len(self._dirs)
            self._dirs.clear()
        if count > 0:
            logger.info("Cleared %d tracked plan(s) from sandbox dirs cache", count)

    def __contains__(self, args: tuple[str, str]) -> bool:
        """Support ``"plan_id", "dir_path"`` tuple membership checks.

        Args:
            args: ``(plan_id, dir_path)`` tuple.

        Returns:
            ``True`` if recorded as valid for the given plan.
        """
        plan_id, dir_path = args
        return self.get(plan_id, dir_path)
