"""
SandboxedFS - Secure File System Operations

This service restricts all file operations to within a project's root directory,
preventing path traversal attacks and ensuring AI operations are safely scoped.

Security Features:
- Path resolution with escape detection
- Symlink attack prevention
- Read/write operation logging
"""

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

logger = logging.getLogger(__name__)


class SandboxedFS:
    """
    File system operations restricted to a project root.

    All paths are validated to ensure they don't escape the sandbox.
    This is critical for AI operations that may receive untrusted input.
    """

    def __init__(self, root_path: str):
        """
        Initialize sandboxed file system.

        Args:
            root_path: Absolute path to project root directory

        Raises:
            ValueError: If root_path doesn't exist or isn't a directory
        """
        self.root = Path(root_path).resolve()
        if not self.root.is_dir():
            raise ValueError(f"Invalid root path: {root_path}")
        logger.info(f"SandboxedFS initialized with root: {self.root}")

    def _validate_path(self, relative_path: str) -> Path:
        """
        Validate and resolve a path within the sandbox.

        Args:
            relative_path: Path relative to project root

        Returns:
            Resolved absolute Path within sandbox

        Raises:
            PermissionError: If path escapes sandbox
        """
        # Normalize the path
        clean_path = os.path.normpath(relative_path)

        # Resolve full path
        full_path = (self.root / clean_path).resolve()

        # Security check: must be within root
        try:
            full_path.relative_to(self.root)
        except ValueError:
            logger.warning(f"Path traversal attempt blocked: {relative_path}")
            raise PermissionError(f"Path escapes sandbox: {relative_path}")

        return full_path

    def read_file(self, relative_path: str, max_size_kb: int = 500) -> str:
        """
        Read file content within sandbox.

        Args:
            relative_path: Path relative to project root
            max_size_kb: Maximum file size in KB (default 500KB)

        Returns:
            File content as string

        Raises:
            FileNotFoundError: If file doesn't exist
            PermissionError: If path escapes sandbox
            ValueError: If file exceeds max size
        """
        path = self._validate_path(relative_path)

        if not path.is_file():
            raise FileNotFoundError(f"File not found: {relative_path}")

        # Check file size
        size_kb = path.stat().st_size / 1024
        if size_kb > max_size_kb:
            raise ValueError(f"File too large: {size_kb:.1f}KB > {max_size_kb}KB limit")

        content = path.read_text(encoding='utf-8')
        logger.debug(f"Read file: {relative_path} ({len(content)} chars)")
        return content

    def write_file(self, relative_path: str, content: str) -> None:
        """
        Write file content within sandbox.

        Args:
            relative_path: Path relative to project root
            content: Content to write

        Raises:
            PermissionError: If path escapes sandbox
        """
        path = self._validate_path(relative_path)

        # Create parent directories if needed
        path.parent.mkdir(parents=True, exist_ok=True)

        path.write_text(content, encoding='utf-8')
        logger.info(f"Wrote file: {relative_path} ({len(content)} chars)")

    def delete_file(self, relative_path: str) -> None:
        """
        Delete file within sandbox.

        Args:
            relative_path: Path relative to project root

        Raises:
            FileNotFoundError: If file doesn't exist
            PermissionError: If path escapes sandbox
        """
        path = self._validate_path(relative_path)

        if not path.is_file():
            raise FileNotFoundError(f"File not found: {relative_path}")

        path.unlink()
        logger.info(f"Deleted file: {relative_path}")

    def list_directory(self, relative_path: str = ".") -> List[Dict[str, any]]:
        """
        List directory contents within sandbox.

        Args:
            relative_path: Path relative to project root

        Returns:
            List of dicts with name, type, and size

        Raises:
            NotADirectoryError: If path isn't a directory
            PermissionError: If path escapes sandbox
        """
        path = self._validate_path(relative_path)

        if not path.is_dir():
            raise NotADirectoryError(f"Not a directory: {relative_path}")

        result = []
        for item in sorted(path.iterdir()):
            entry = {
                "name": item.name,
                "type": "directory" if item.is_dir() else "file",
            }
            if item.is_file():
                entry["size"] = item.stat().st_size
            result.append(entry)

        return result

    def file_exists(self, relative_path: str) -> bool:
        """
        Check if file exists within sandbox.

        Args:
            relative_path: Path relative to project root

        Returns:
            True if file exists, False otherwise
        """
        try:
            path = self._validate_path(relative_path)
            return path.exists()
        except PermissionError:
            return False

    def get_file_tree(self, max_depth: int = 3, include_hidden: bool = False) -> Dict:
        """
        Get hierarchical file tree for AI context injection.

        Args:
            max_depth: Maximum directory depth to traverse
            include_hidden: Include hidden files (starting with .)

        Returns:
            Nested dict representing file tree with sizes
        """
        def build_tree(path: Path, depth: int) -> Dict:
            if depth > max_depth:
                return {"...": "truncated"}

            result = {}
            try:
                items = sorted(path.iterdir())
            except PermissionError:
                return {"error": "permission denied"}

            for item in items:
                # Skip hidden files unless requested
                if not include_hidden and item.name.startswith('.'):
                    # Always include .dss config folder
                    if item.name != '.dss':
                        continue

                # Skip common non-essential directories
                if item.name in ('node_modules', '__pycache__', '.git', 'dist', 'build'):
                    result[item.name + "/"] = {"...": "skipped"}
                    continue

                if item.is_dir():
                    result[item.name + "/"] = build_tree(item, depth + 1)
                else:
                    result[item.name] = item.stat().st_size

            return result

        return build_tree(self.root, 0)

    def get_root_path(self) -> str:
        """Get the absolute root path of this sandbox."""
        return str(self.root)
