"""Capability-based command validation: operations and permissions model.

This module defines the capability system that replaces the token-based denylist.

Key concepts:
- Operation: Type of access (read_file, execute_binary, write_stdout, etc.)
- Capability: Allows a specific operation on resources matching a pattern
- CapabilitySet: Collection of capabilities defining what's allowed

Example capability set (read-only):
    CapabilitySet([
        Capability(OperationType.READ_FILE, "/var/log/*"),
        Capability(OperationType.LIST_DIR, "/var/log"),
        Capability(OperationType.EXECUTE_BINARY, "cat|grep|ls|tail|head"),
        Capability(OperationType.WRITE_STDOUT, None),  # All resources
        Capability(OperationType.WRITE_STDERR, None),
    ])
"""

from dataclasses import dataclass
from enum import Enum
from fnmatch import fnmatch
from pathlib import PurePath
from typing import List, Optional


class OperationType(Enum):
    """Types of operations that can be permitted or denied."""

    # File operations
    READ_FILE = "read_file"  # Read from file (cat, grep, head, tail, etc.)
    LIST_DIR = "list_dir"  # List directory contents (ls, find, etc.)
    WRITE_FILE = "write_file"  # Write to file (echo >, sed -i, tee, etc.)

    # Process execution
    EXECUTE_BINARY = "execute_binary"  # Execute binary by name (ls, cat, grep, etc.)

    # Stream operations
    READ_STDIN = "read_stdin"  # Read from stdin (pipe input)
    WRITE_STDOUT = "write_stdout"  # Write to stdout (normal output)
    WRITE_STDERR = "write_stderr"  # Write to stderr (error output)

    # System operations
    READ_ENV = "read_env"  # Read environment variables
    SYSTEM_INFO = "system_info"  # System info (hostname, date, uname, etc.)


@dataclass
class Capability:
    """
    Represents one permitted operation.

    A Capability is an (operation, resource_pattern) pair that grants permission
    for a specific operation on resources matching the pattern.

    Examples:
        Capability(OperationType.READ_FILE, "/var/log/*")
        → Allows reading any file in /var/log/

        Capability(OperationType.EXECUTE_BINARY, "ls|grep|cat")
        → Allows executing ls, grep, or cat

        Capability(OperationType.WRITE_STDOUT, None)
        → Allows writing to stdout for all commands

    Attributes:
        operation: Type of operation (from OperationType enum)
        resource_pattern: Glob pattern for resources, or None for all resources
    """

    operation: OperationType
    resource_pattern: Optional[str] = None

    def matches(self, operation: OperationType, resource: str) -> bool:
        """
        Check if this capability permits the given operation on resource.

        Args:
            operation: Type of operation to check
            resource: Resource name (file path, binary name, etc.)

        Returns:
            True if operation is permitted, False otherwise

        Examples:
            cap = Capability(OperationType.READ_FILE, "/var/log/*")
            cap.matches(OperationType.READ_FILE, "/var/log/syslog")  # → True
            cap.matches(OperationType.READ_FILE, "/etc/passwd")  # → False
            cap.matches(OperationType.WRITE_FILE, "/var/log/syslog")  # → False
        """
        # Operation must match
        if self.operation != operation:
            return False

        # If no pattern, allow all resources
        if self.resource_pattern is None:
            return True

        # Special handling for pipe-separated alternatives (for binaries)
        # e.g., "cat|grep|ls" should match any of those binaries
        if "|" in self.resource_pattern:
            alternatives = self.resource_pattern.split("|")
            return resource in alternatives

        # Path-aware glob matching (handles * and ** with path boundaries)
        return self._glob_match_path(resource, self.resource_pattern)

    def _match_recursive_glob(self, resource: str, pattern: str) -> bool:
        """
        Match resource against a pattern with ** recursive wildcards.

        Examples:
            /var/log/**/* matches:
            - /var/log/syslog
            - /var/log/apache2/access.log
            - /var/log/deep/nested/file.log
        """
        # Pattern like /var/log/**/* should match anything under /var/log/
        if "/**/*" in pattern:
            prefix = pattern.split("/**/*")[0]
            # Resource must start with the prefix and have more path
            if resource == prefix:
                return True
            if not resource.startswith(prefix + "/"):
                return False
            # Should match anything under prefix
            return True
        elif "**/*" in pattern:
            prefix = pattern.split("**/*")[0]
            return resource.startswith(prefix)
        else:
            # Fallback to normal fnmatch
            return fnmatch(resource, pattern)

    def _glob_match_path(self, resource: str, pattern: str) -> bool:
        """
        Match file paths with glob semantics.

        Respects that * doesn't cross directory boundaries,
        but ** does (recursive).

        Examples:
            /var/log/* matches /var/log/syslog but NOT /var/log/apache2/access.log
            /var/log/**/* matches anything under /var/log/ recursively
        """
        # If no wildcards, exact match
        if not any(c in pattern for c in "*?[]"):
            return resource == pattern

        # Convert to Path objects for path-aware matching
        resource_parts = PurePath(resource).parts
        pattern_parts = PurePath(pattern).parts

        # Match parts
        return self._match_path_parts(resource_parts, pattern_parts)

    @staticmethod
    def _match_path_parts(resource_parts: tuple, pattern_parts: tuple) -> bool:
        """Recursively match path parts."""
        if not pattern_parts:
            return not resource_parts

        if not resource_parts:
            # Pattern remaining - only ok if all are ** or *
            # (**/* pattern means match directory and everything under it)
            return all(p in ("**", "*") for p in pattern_parts)

        pattern_part = pattern_parts[0]

        # Handle ** (matches any number of directories, including zero)
        if pattern_part == "**":
            # ** can match zero or more directories
            # Try matching rest of pattern with rest of resource (match zero directories)
            if Capability._match_path_parts(resource_parts, pattern_parts[1:]):
                return True
            # Or skip one directory and try again (match one or more directories)
            return Capability._match_path_parts(resource_parts[1:], pattern_parts)

        # Handle single path component with glob
        if not fnmatch(resource_parts[0], pattern_part):
            return False

        return Capability._match_path_parts(resource_parts[1:], pattern_parts[1:])


class PolicyError(Exception):
    """Command violates policy (operation not permitted by capabilities)."""

    pass


class CapabilitySet:
    """
    Collection of capabilities that define what operations are allowed.

    This replaces the old Policy class and denylist-based validation.

    A CapabilitySet is a whitelist: only operations explicitly granted
    via capabilities are permitted. All other operations are denied.

    Attributes:
        capabilities: List of Capability objects
    """

    def __init__(self, capabilities: List[Capability]):
        """
        Initialize capability set.

        Args:
            capabilities: List of Capability objects
        """
        self.capabilities = capabilities

    def permits(self, operation: OperationType, resource: str = "") -> bool:
        """
        Check if any capability permits this operation on resource.

        Args:
            operation: Type of operation to check
            resource: Resource name (empty string means "any resource")

        Returns:
            True if permitted, False otherwise

        Examples:
            cap_set = CapabilitySet([
                Capability(OperationType.READ_FILE, "/var/log/*"),
                Capability(OperationType.EXECUTE_BINARY, "cat|grep"),
            ])

            cap_set.permits(OperationType.READ_FILE, "/var/log/syslog")  # → True
            cap_set.permits(OperationType.READ_FILE, "/etc/passwd")  # → False
            cap_set.permits(OperationType.EXECUTE_BINARY, "cat")  # → True
            cap_set.permits(OperationType.EXECUTE_BINARY, "rm")  # → False
        """
        for capability in self.capabilities:
            if capability.matches(operation, resource):
                return True
        return False

    def check_allowed(
        self, operation: OperationType, resource: str = ""
    ) -> None:
        """
        Check if operation is permitted. Raise PolicyError if not.

        Args:
            operation: Type of operation to check
            resource: Resource name (file path, binary name, etc.)

        Raises:
            PolicyError: If operation is not permitted

        Examples:
            cap_set = CapabilitySet([
                Capability(OperationType.READ_FILE, "/var/log/*"),
            ])

            cap_set.check_allowed(OperationType.READ_FILE, "/var/log/syslog")
            # → OK, no exception

            cap_set.check_allowed(OperationType.WRITE_FILE, "/tmp/file")
            # → Raises PolicyError: "write_file on '/tmp/file' not permitted"
        """
        if not self.permits(operation, resource):
            message = (
                f"Operation {operation.value} on {repr(resource)} not permitted.\n"
                f"Available capabilities:\n"
                f"{self._describe_capabilities()}"
            )
            raise PolicyError(message)

    def _describe_capabilities(self) -> str:
        """Return human-readable list of capabilities for error messages."""
        if not self.capabilities:
            return "  (no capabilities configured)"

        lines = []
        for cap in self.capabilities:
            if cap.resource_pattern is None:
                pattern_str = "(all resources)"
            else:
                pattern_str = cap.resource_pattern
            lines.append(f"  - {cap.operation.value}: {pattern_str}")

        return "\n".join(lines)

    def __repr__(self) -> str:
        """String representation for debugging."""
        return (
            f"CapabilitySet({len(self.capabilities)} capabilities):\n"
            f"{self._describe_capabilities()}"
        )


# Preset capability sets for common scenarios

DEFAULT_READ_ONLY_CAPABILITIES = [
    # File operations - allow reading anywhere
    Capability(OperationType.READ_FILE, "/**/*"),  # Read any file recursively
    Capability(OperationType.LIST_DIR, "/**/*"),  # List any directory recursively
    # Binaries
    Capability(OperationType.EXECUTE_BINARY, "cat|grep|ls|head|tail|wc|sort|uniq|find|pwd|whoami|date|hostname"),
    # Streams
    Capability(OperationType.WRITE_STDOUT, None),
    Capability(OperationType.WRITE_STDERR, None),
]

DEFAULT_WRITE_CAPABLE_CAPABILITIES = [
    # File operations - allow reading anywhere, write only to /tmp
    Capability(OperationType.READ_FILE, "/**/*"),
    Capability(OperationType.LIST_DIR, "/**/*"),
    Capability(OperationType.WRITE_FILE, "/tmp/**/*"),  # Write anywhere in /tmp
    # Binaries
    Capability(
        OperationType.EXECUTE_BINARY,
        "cat|grep|ls|head|tail|wc|sort|uniq|find|sed|awk|cut|echo|printf"
    ),
    # Streams
    Capability(OperationType.WRITE_STDOUT, None),
    Capability(OperationType.WRITE_STDERR, None),
]

DEFAULT_RESTRICTIVE_CAPABILITIES = [
    # Very limited: only safe system info commands
    Capability(OperationType.EXECUTE_BINARY, "echo|pwd|hostname|date|whoami|uname"),
    Capability(OperationType.WRITE_STDOUT, None),
    Capability(OperationType.WRITE_STDERR, None),
]
