"""
BloomOS Kernel - Cell Wall
==========================

Minimal security boundary for syscall authorization.

The Cell Wall enforces:
1. Syscall allowlists per actor
2. Input size limits
3. Bash command allowlists
4. Capability-based auth with expiration

This is NOT a full sandbox - it's a policy enforcement layer.
"""

from __future__ import annotations

import hashlib
import re
import shlex
import time
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Dict, FrozenSet, List, Optional, Set, Tuple


@dataclass
class Capability:
    """
    Capability token for fine-grained syscall authorization.

    Capabilities are time-limited tokens that grant specific syscall
    permissions to an actor. They can be revoked at any time.

    Usage:
        cap = cell_wall.grant_capability(
            actor="worker_1",
            syscalls={"cdna.decode.v1", "scheduler.acquire.v1"},
            max_it=50.0,
            expires_in_s=3600,
        )

        # Use in syscall
        result = kernel.invoke("cdna.decode.v1", "worker_1", {
            ...,
            "_capability_token": cap.token_id
        })
    """
    token_id: str                    # Unique capability token
    actor: str                       # Who owns this capability
    syscalls: FrozenSet[str]         # Allowed syscalls
    max_it_per_call: float           # Per-call IT limit
    max_input_bytes: int             # Per-call input size limit
    expires_at: float                # Expiration timestamp (time.time())
    created_at: str                  # ISO timestamp

    def is_expired(self) -> bool:
        """Check if capability has expired."""
        return time.time() > self.expires_at

    def allows_syscall(self, syscall_id: str) -> bool:
        """Check if capability allows a specific syscall."""
        return syscall_id in self.syscalls

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dict for JSON serialization."""
        return {
            "token_id": self.token_id,
            "actor": self.actor,
            "syscalls": sorted(self.syscalls),
            "max_it_per_call": self.max_it_per_call,
            "max_input_bytes": self.max_input_bytes,
            "expires_at": self.expires_at,
            "created_at": self.created_at,
            "is_expired": self.is_expired(),
        }


@dataclass
class ActorPolicy:
    """Security policy for an actor."""
    actor: str

    # Syscall allowlist (empty = all allowed)
    allowed_syscalls: Set[str] = field(default_factory=set)

    # Bash command allowlist (empty = none allowed)
    allowed_bash_commands: Set[str] = field(default_factory=set)

    # Input size limits
    max_input_bytes: int = 100 * 1024 * 1024  # 100 MB default

    # Rate limiting (syscalls per minute, 0 = unlimited)
    rate_limit_per_minute: int = 0


class CellWall:
    """
    Minimal security boundary for syscall authorization.

    The CellWall sits between the SyscallRegistry and handlers,
    enforcing policy before execution.

    Default policy:
    - All syscalls allowed (no allowlist = allow all)
    - Limited bash commands (safe read-only commands)
    - 100 MB input size limit

    Usage:
        wall = CellWall()
        ok, err = wall.authorize_syscall("bridge", "cdna.decode.v1")
        if not ok:
            return EPERM, err
    """

    # Default safe bash commands (read-only, no network)
    DEFAULT_BASH_ALLOWLIST = {
        "ls", "pwd", "cat", "head", "tail", "wc", "du", "df",
        "find", "grep", "sha256sum", "md5sum", "file", "stat",
        "echo", "date", "uname", "which", "whoami", "hostname",
        "env", "printenv",
    }

    # Default input size limit (100 MB)
    DEFAULT_MAX_INPUT_BYTES = 100 * 1024 * 1024

    def __init__(self):
        """Initialize Cell Wall with default policies."""
        self._policies: Dict[str, ActorPolicy] = {}
        self._global_syscall_allowlist: Set[str] = set()  # Empty = all allowed
        self._global_bash_allowlist: Set[str] = self.DEFAULT_BASH_ALLOWLIST.copy()
        self._rate_tracker: Dict[str, List[float]] = {}  # actor -> list of timestamps
        self._capabilities: Dict[str, Capability] = {}   # token_id -> Capability

    # -------------------------------------------------------------------------
    # Capability-Based Auth
    # -------------------------------------------------------------------------

    def grant_capability(
        self,
        actor: str,
        syscalls: Set[str],
        max_it_per_call: float = 100.0,
        max_input_bytes: int = 100 * 1024 * 1024,
        expires_in_s: float = 3600.0,
    ) -> Capability:
        """
        Grant a capability token to an actor.

        Args:
            actor: Who gets this capability
            syscalls: Set of allowed syscalls
            max_it_per_call: Maximum IT tokens per syscall (default: 100)
            max_input_bytes: Maximum input size per call (default: 100MB)
            expires_in_s: Expiration time in seconds (default: 1 hour)

        Returns:
            Capability token
        """
        token_id = f"cap_{uuid.uuid4().hex[:16]}"
        capability = Capability(
            token_id=token_id,
            actor=actor,
            syscalls=frozenset(syscalls),
            max_it_per_call=max_it_per_call,
            max_input_bytes=max_input_bytes,
            expires_at=time.time() + expires_in_s,
            created_at=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
        )
        self._capabilities[token_id] = capability
        return capability

    def revoke_capability(self, token_id: str) -> bool:
        """
        Revoke a capability token.

        Args:
            token_id: Token to revoke

        Returns:
            True if token was found and revoked
        """
        if token_id in self._capabilities:
            del self._capabilities[token_id]
            return True
        return False

    def get_capability(self, token_id: str) -> Optional[Capability]:
        """Get a capability by token ID (returns None if not found or expired)."""
        cap = self._capabilities.get(token_id)
        if cap and not cap.is_expired():
            return cap
        return None

    def authorize_with_capability(
        self,
        token_id: str,
        syscall_id: str,
        estimated_cost: float = 0.0,
        input_size: int = 0,
    ) -> Tuple[bool, Optional[str]]:
        """
        Authorize a syscall using a capability token.

        Args:
            token_id: Capability token ID
            syscall_id: Syscall to authorize
            estimated_cost: Estimated IT cost for this call
            input_size: Size of input data in bytes

        Returns:
            (authorized: bool, error_message: Optional[str])
        """
        cap = self._capabilities.get(token_id)

        if not cap:
            return (False, f"EPERM: capability token '{token_id}' not found")

        if cap.is_expired():
            # Clean up expired capability
            del self._capabilities[token_id]
            return (False, f"EPERM: capability token '{token_id}' has expired")

        if not cap.allows_syscall(syscall_id):
            return (
                False,
                f"EPERM: capability does not allow syscall '{syscall_id}'",
            )

        if estimated_cost > cap.max_it_per_call:
            return (
                False,
                f"EPERM: estimated cost {estimated_cost:.3f} exceeds capability limit {cap.max_it_per_call:.3f}",
            )

        if input_size > cap.max_input_bytes:
            return (
                False,
                f"EPERM: input size {input_size} exceeds capability limit {cap.max_input_bytes}",
            )

        return (True, None)

    def cleanup_expired_capabilities(self) -> int:
        """Remove expired capability tokens. Returns count removed."""
        expired = [
            token_id for token_id, cap in self._capabilities.items()
            if cap.is_expired()
        ]
        for token_id in expired:
            del self._capabilities[token_id]
        return len(expired)

    def list_capabilities(self, actor: Optional[str] = None) -> List[Dict[str, Any]]:
        """List all capabilities, optionally filtered by actor."""
        caps = []
        for cap in self._capabilities.values():
            if actor is None or cap.actor == actor:
                caps.append(cap.to_dict())
        return caps

    def set_policy(self, actor: str, policy: ActorPolicy) -> None:
        """Set policy for an actor."""
        self._policies[actor] = policy

    def get_policy(self, actor: str) -> ActorPolicy:
        """Get policy for an actor (creates default if missing)."""
        if actor not in self._policies:
            self._policies[actor] = ActorPolicy(
                actor=actor,
                allowed_bash_commands=self.DEFAULT_BASH_ALLOWLIST.copy(),
                max_input_bytes=self.DEFAULT_MAX_INPUT_BYTES,
            )
        return self._policies[actor]

    def set_global_syscall_allowlist(self, syscalls: Set[str]) -> None:
        """Set global syscall allowlist (empty = all allowed)."""
        self._global_syscall_allowlist = syscalls

    def set_global_bash_allowlist(self, commands: Set[str]) -> None:
        """Set global bash command allowlist."""
        self._global_bash_allowlist = commands

    def authorize_syscall(
        self,
        actor: str,
        syscall_id: str,
        capability_token: Optional[str] = None,
        estimated_cost: float = 0.0,
        input_size: int = 0,
    ) -> Tuple[bool, Optional[str]]:
        """
        Authorize a syscall for an actor.

        Authorization flow:
        1. If capability_token provided, check capability first
        2. Check global allowlist (if set)
        3. Check actor-specific allowlist (if set)
        4. Check rate limit

        Args:
            actor: Who is invoking
            syscall_id: Which syscall (e.g., "cdna.decode.v1")
            capability_token: Optional capability token for fine-grained auth
            estimated_cost: Estimated IT cost (for capability limits)
            input_size: Input size in bytes (for capability limits)

        Returns:
            (authorized: bool, error_message: Optional[str])
        """
        # If capability token provided, use capability-based auth
        if capability_token:
            cap = self._capabilities.get(capability_token)
            if not cap:
                return (False, f"EPERM: capability token '{capability_token}' not found")

            if cap.actor != actor:
                return (
                    False,
                    f"EPERM: capability token belongs to '{cap.actor}', not '{actor}'",
                )

            return self.authorize_with_capability(
                capability_token, syscall_id, estimated_cost, input_size
            )

        # Standard authorization flow
        policy = self.get_policy(actor)

        # Check global allowlist (if set)
        if self._global_syscall_allowlist:
            if syscall_id not in self._global_syscall_allowlist:
                return (False, f"EPERM: syscall '{syscall_id}' not in global allowlist")

        # Check actor-specific allowlist (if set)
        if policy.allowed_syscalls:
            if syscall_id not in policy.allowed_syscalls:
                return (False, f"EPERM: syscall '{syscall_id}' not allowed for actor '{actor}'")

        # Check rate limit
        if policy.rate_limit_per_minute > 0:
            now = time.time()
            timestamps = self._rate_tracker.get(actor, [])

            # Remove old timestamps (> 60 seconds)
            timestamps = [t for t in timestamps if now - t < 60.0]
            self._rate_tracker[actor] = timestamps

            if len(timestamps) >= policy.rate_limit_per_minute:
                return (False, f"EPERM: rate limit exceeded for actor '{actor}'")

            timestamps.append(now)
            self._rate_tracker[actor] = timestamps

        return (True, None)

    def validate_input(
        self,
        actor: str,
        input_bytes: Optional[bytes],
        schema: Optional[Dict[str, Any]] = None,
        data: Optional[Dict[str, Any]] = None,
    ) -> Tuple[bool, Optional[str]]:
        """
        Validate input size and structure.

        Args:
            actor: Who is invoking
            input_bytes: Raw input bytes (optional)
            schema: Expected input schema (optional)
            data: Input data dict (optional)

        Returns:
            (valid: bool, error_message: Optional[str])
        """
        policy = self.get_policy(actor)

        # Check size if bytes provided
        if input_bytes is not None:
            if len(input_bytes) > policy.max_input_bytes:
                return (
                    False,
                    f"EINVAL: input size {len(input_bytes)} exceeds limit {policy.max_input_bytes}",
                )

        # Check size of serialized data if provided
        if data is not None:
            import json
            try:
                serialized = json.dumps(data)
                if len(serialized) > policy.max_input_bytes:
                    return (
                        False,
                        f"EINVAL: serialized input size {len(serialized)} exceeds limit {policy.max_input_bytes}",
                    )
            except (TypeError, ValueError) as e:
                return (False, f"EINVAL: input not JSON serializable: {e}")

        # Schema validation (basic type checking)
        if schema and data:
            result = self._validate_schema(data, schema)
            if not result[0]:
                return result

        return (True, None)

    def _validate_schema(
        self,
        data: Dict[str, Any],
        schema: Dict[str, Any],
    ) -> Tuple[bool, Optional[str]]:
        """
        Basic schema validation.

        Schema format:
            {
                "required": ["field1", "field2"],
                "types": {"field1": "str", "field2": "int"},
            }
        """
        required = schema.get("required", [])
        types = schema.get("types", {})

        # Check required fields
        for field_name in required:
            if field_name not in data:
                return (False, f"EINVAL: missing required field '{field_name}'")

        # Check types
        type_map = {
            "str": str,
            "int": int,
            "float": (int, float),
            "bool": bool,
            "list": list,
            "dict": dict,
        }

        for field_name, expected_type in types.items():
            if field_name in data:
                value = data[field_name]
                if expected_type in type_map:
                    if not isinstance(value, type_map[expected_type]):
                        return (
                            False,
                            f"EINVAL: field '{field_name}' must be {expected_type}, got {type(value).__name__}",
                        )

        return (True, None)

    def authorize_bash(
        self,
        actor: str,
        command: str,
    ) -> Tuple[bool, Optional[str]]:
        """
        Authorize a bash command for an actor.

        Only allows commands in the allowlist.

        Args:
            actor: Who is invoking
            command: Full bash command string

        Returns:
            (authorized: bool, error_message: Optional[str])
        """
        policy = self.get_policy(actor)

        # Use actor-specific allowlist if set, else global
        allowlist = policy.allowed_bash_commands or self._global_bash_allowlist

        try:
            # Parse command to get the base command
            parts = shlex.split(command)
            if not parts:
                return (False, "EPERM: empty command")

            base_command = parts[0]

            # Handle paths (e.g., /usr/bin/ls -> ls)
            base_command = base_command.split("/")[-1]

        except ValueError as e:
            return (False, f"EPERM: cannot parse command: {e}")

        if base_command not in allowlist:
            return (
                False,
                f"EPERM: command '{base_command}' not in allowlist for actor '{actor}'",
            )

        # Check for dangerous patterns even if command is allowed
        dangerous_patterns = [
            r";\s*rm\s",           # rm after semicolon
            r"\|\s*rm\s",          # rm after pipe
            r">\s*/dev/",          # redirect to /dev/
            r">\s*/etc/",          # redirect to /etc/
            r">\s*/proc/",         # redirect to /proc/
            r"\$\(",               # command substitution
            r"`",                  # backtick substitution
        ]

        for pattern in dangerous_patterns:
            if re.search(pattern, command):
                return (False, f"EPERM: dangerous pattern detected in command")

        return (True, None)

    def stats(self) -> Dict[str, Any]:
        """Get Cell Wall statistics."""
        # Count active vs expired capabilities
        active_caps = sum(1 for cap in self._capabilities.values() if not cap.is_expired())
        expired_caps = len(self._capabilities) - active_caps

        return {
            "actor_count": len(self._policies),
            "global_syscall_allowlist_size": len(self._global_syscall_allowlist),
            "global_bash_allowlist_size": len(self._global_bash_allowlist),
            "global_bash_allowlist": sorted(self._global_bash_allowlist),
            "capabilities": {
                "total": len(self._capabilities),
                "active": active_caps,
                "expired": expired_caps,
            },
        }
