"""
security.py - Path validation and safety guardrails for the filesystem watcher.

Prevents watching dangerous or overly-broad system paths to protect the host OS
and avoid flooding the agent with irrelevant events.

Blocking strategy
-----------------
* **Exact-only roots** – drive roots (``C:\\``, ``/``) are blocked only when the
  requested path *is* that exact root.  Subdirectories on the same drive are
  allowed unless they fall under a subtree-blocked path.
* **Subtree-blocked paths** – well-known system directories and their entire
  subtrees are always blocked (e.g. ``C:\\Windows``, ``/etc``).
* **Over-broad user roots** – ``C:\\Users`` / ``/home`` themselves are blocked;
  individual user home directories (``C:\\Users\\alice``) are allowed.
"""

import platform
from pathlib import Path, PurePosixPath, PureWindowsPath

# ---------------------------------------------------------------------------
# Blocked path configuration
# ---------------------------------------------------------------------------

# Paths that are only blocked when matched *exactly* (drive/filesystem roots).
_WINDOWS_EXACT_BLOCKED: list[str] = ["C:\\"]
_LINUX_EXACT_BLOCKED: list[str] = ["/"]
_MACOS_EXACT_BLOCKED: list[str] = ["/"]

# Paths whose entire subtrees (including themselves) are blocked.
_WINDOWS_SUBTREE_BLOCKED: list[str] = [
    "C:\\Windows",
    "C:\\Program Files",
    "C:\\Program Files (x86)",
    "C:\\ProgramData",
]

_LINUX_SUBTREE_BLOCKED: list[str] = [
    "/etc",
    "/sys",
    "/proc",
    "/dev",
    "/boot",
    "/usr",
    "/bin",
    "/sbin",
    "/lib",
    "/lib64",
    "/snap",
]

_MACOS_SUBTREE_BLOCKED: list[str] = [
    "/System",
    "/Library",
    "/usr",
    "/bin",
    "/sbin",
    "/etc",
    "/dev",
    "/private",
]

# Maximum depth allowed for recursive watches (informational; enforced by caller).
MAX_RECURSIVE_DEPTH_HEURISTIC = 6


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _platform_config() -> tuple[list, list]:
    """Return (exact_blocked, subtree_blocked) lists for the current platform.

    Uses platform-specific *pure* path types so that path comparisons work
    correctly even when tests mock ``platform.system`` on a different OS
    (e.g. mocking Windows on a Linux CI runner).
    """
    system = platform.system()
    if system == "Windows":
        exact = [PureWindowsPath(p) for p in _WINDOWS_EXACT_BLOCKED]
        subtree = [PureWindowsPath(p) for p in _WINDOWS_SUBTREE_BLOCKED]
    elif system == "Darwin":
        exact = [PurePosixPath(p) for p in _MACOS_EXACT_BLOCKED]
        subtree = [PurePosixPath(p) for p in _MACOS_SUBTREE_BLOCKED]
    else:
        exact = [PurePosixPath(p) for p in _LINUX_EXACT_BLOCKED]
        subtree = [PurePosixPath(p) for p in _LINUX_SUBTREE_BLOCKED]
    return exact, subtree


def _pure_path_type() -> type:
    """Return the correct pure path class for the current (possibly mocked) platform."""
    return PureWindowsPath if platform.system() == "Windows" else PurePosixPath


def _is_blocked(path: Path) -> bool:
    """
    Return True if *path* is:
    - exactly equal to a drive/filesystem root, OR
    - equal to or a descendant of a subtree-blocked system path.

    Uses ``path.as_posix()`` for normalisation so that the path string
    is not mangled by the host OS path class before comparison (e.g. on
    Windows, ``Path('/lib').as_posix()`` is ``'/lib'``, not ``'\\lib'``).
    """
    exact_blocked, subtree_blocked = _platform_config()
    pure = _pure_path_type()
    # as_posix() always uses '/' separators and preserves a leading '/',
    # so cross-platform string conversion is lossless.
    path_str = path.as_posix()
    try:
        normalised = pure(path_str)
    except Exception:
        return False

    # Exact-match check for roots
    for root in exact_blocked:
        if normalised == root:
            return True

    # Subtree check for system directories
    for sys_dir in subtree_blocked:
        try:
            normalised.relative_to(sys_dir)
            return True  # normalised IS sys_dir or a descendant
        except ValueError:
            pass

    return False


def _is_broad_user_dir(path: Path) -> bool:
    """
    Block the top-level user home-parent directory itself (e.g. ``C:\\Users``,
    ``/home``) while allowing individual user home directories beneath it.

    Uses ``path.as_posix()`` for lossless cross-platform normalisation.
    """
    pure = _pure_path_type()
    path_str = path.as_posix()
    try:
        normalised = pure(path_str)
    except Exception:
        return False

    if platform.system() == "Windows":
        return normalised == PureWindowsPath("C:/Users")
    else:
        return normalised == PurePosixPath("/home")


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


class SecurityError(ValueError):
    """Raised when a watch request fails security validation."""


def validate_watch_request(path: str, recursive: bool = False) -> Path:
    """
    Validate and resolve *path*, raising :class:`SecurityError` if it is
    blocked or does not exist.

    Parameters
    ----------
    path:
        Absolute or relative file system path to watch.
    recursive:
        Whether the watch will be recursive (informational).

    Returns
    -------
    Path
        The fully resolved :class:`~pathlib.Path` object.

    Raises
    ------
    SecurityError
        If the path is blocked, does not exist, or is not a directory.
    """
    try:
        resolved = Path(path).resolve()
    except Exception as exc:
        raise SecurityError(f"Cannot resolve path '{path}': {exc}") from exc

    try:
        path_exists = resolved.exists()
        path_is_dir = resolved.is_dir()
    except OSError as exc:
        raise SecurityError(f"Cannot access path '{resolved}': {exc}") from exc

    if not path_exists:
        raise SecurityError(f"Path does not exist: '{resolved}'")

    if not path_is_dir:
        raise SecurityError(f"Path is not a directory: '{resolved}'")

    if _is_blocked(resolved):
        raise SecurityError(
            f"Watching '{resolved}' is not allowed: the path is a protected system location."
        )

    if _is_broad_user_dir(resolved):
        raise SecurityError(
            f"Watching '{resolved}' is not allowed: too broad — watching the entire users "
            "root directory could expose sensitive data."
        )

    return resolved
