#!/usr/bin/env python3
"""
sandbox.py - Sandbox Filesystem (WO#GINNIE_05)
===============================================

Safe filesystem operations within sandbox boundaries.

Prevents:
- Access outside sandbox roots
- Path traversal attacks (../)
- Dangerous operations
"""

import os
from pathlib import Path
from typing import List, Optional

from helix_cdc.ginnie.config import GinnieConfig


class SandboxError(Exception):
    """Raised when a sandbox violation is detected."""
    pass


class Sandbox:
    """
    Safe filesystem wrapper.

    All operations are restricted to configured sandbox roots.
    """

    def __init__(self, config: GinnieConfig):
        self.config = config
        self.roots = [r.resolve() for r in config.sandbox_roots]
        self.scratch = config.scratch_dir.resolve()

        # Ensure scratch exists
        self.scratch.mkdir(parents=True, exist_ok=True)

    def _validate_path(self, path: str | Path) -> Path:
        """
        Validate and resolve a path.

        Args:
            path: Path to validate (can be relative or absolute)

        Returns:
            Resolved absolute path

        Raises:
            SandboxError: If path is outside sandbox
        """
        # Handle relative paths
        if isinstance(path, str):
            path = Path(path)

        if not path.is_absolute():
            # Relative to first sandbox root
            path = self.roots[0] / path

        resolved = path.resolve()

        # Check if within any allowed root
        allowed = False
        for root in self.roots:
            try:
                resolved.relative_to(root)
                allowed = True
                break
            except ValueError:
                continue

        # Also check scratch
        if not allowed:
            try:
                resolved.relative_to(self.scratch)
                allowed = True
            except ValueError:
                pass

        if not allowed:
            raise SandboxError(
                f"Path outside sandbox: {resolved}\n"
                f"Allowed roots: {self.roots}"
            )

        return resolved

    def read_file(self, path: str | Path) -> str:
        """
        Read a file within the sandbox.

        Args:
            path: Path to file (relative or absolute)

        Returns:
            File contents as string
        """
        validated = self._validate_path(path)

        if not validated.exists():
            raise SandboxError(f"File not found: {validated}")

        if not validated.is_file():
            raise SandboxError(f"Not a file: {validated}")

        return validated.read_text()

    def write_file(
        self,
        path: str | Path,
        content: str,
        mode: str = "create_or_overwrite",
    ) -> Path:
        """
        Write a file within the sandbox.

        Args:
            path: Path to file (relative or absolute)
            content: Content to write
            mode: Write mode (create_or_overwrite, create_only, append)

        Returns:
            Path to written file
        """
        validated = self._validate_path(path)

        if mode == "create_only" and validated.exists():
            raise SandboxError(f"File already exists: {validated}")

        # Ensure parent directory exists
        validated.parent.mkdir(parents=True, exist_ok=True)

        if mode == "append":
            with open(validated, "a") as f:
                f.write(content)
        else:
            validated.write_text(content)

        return validated

    def list_files(
        self,
        pattern: str = "*",
        root: Optional[str | Path] = None,
    ) -> List[Path]:
        """
        List files matching a pattern within the sandbox.

        Args:
            pattern: Glob pattern (e.g., "*.py", "**/*.json")
            root: Root directory for search (default: first sandbox root)

        Returns:
            List of matching paths
        """
        if root is None:
            search_root = self.roots[0]
        else:
            search_root = self._validate_path(root)

        matches = list(search_root.glob(pattern))

        # Filter to only files within sandbox
        return [m for m in matches if self.config.is_path_allowed(m)]

    def exists(self, path: str | Path) -> bool:
        """Check if a path exists within the sandbox."""
        try:
            validated = self._validate_path(path)
            return validated.exists()
        except SandboxError:
            return False

    def is_file(self, path: str | Path) -> bool:
        """Check if a path is a file within the sandbox."""
        try:
            validated = self._validate_path(path)
            return validated.is_file()
        except SandboxError:
            return False

    def is_dir(self, path: str | Path) -> bool:
        """Check if a path is a directory within the sandbox."""
        try:
            validated = self._validate_path(path)
            return validated.is_dir()
        except SandboxError:
            return False

    def delete_file(self, path: str | Path) -> None:
        """
        Delete a file within the sandbox.

        Args:
            path: Path to file
        """
        validated = self._validate_path(path)

        if not validated.exists():
            raise SandboxError(f"File not found: {validated}")

        if not validated.is_file():
            raise SandboxError(f"Not a file (won't delete directories): {validated}")

        validated.unlink()

    def make_dir(self, path: str | Path) -> Path:
        """
        Create a directory within the sandbox.

        Args:
            path: Path to directory

        Returns:
            Created directory path
        """
        validated = self._validate_path(path)
        validated.mkdir(parents=True, exist_ok=True)
        return validated
