"""
Security & Agent Limits Enforcement

Validates tool execution requests against per-agent limits.
Checks filesystem paths, commands, tool permissions, and rate limits.
"""

import fnmatch
import logging
import time
from collections import defaultdict
from dataclasses import dataclass, field
from pathlib import Path, PurePosixPath
from typing import Dict, List, Optional

from .protocol import AgentLimits, ToolRequest

logger = logging.getLogger(__name__)


class RateLimiter:
    """Simple sliding-window rate limiter."""

    def __init__(self):
        self._windows: Dict[str, list] = defaultdict(list)

    def check(self, key: str, max_per_minute: int) -> bool:
        """Return True if request is within limits."""
        now = time.time()
        window = self._windows[key]
        # Prune entries older than 60s
        window[:] = [t for t in window if now - t < 60]
        if len(window) >= max_per_minute:
            return False
        window.append(now)
        return True


class SecurityEngine:
    """
    Enforces per-agent security limits on tool execution.
    
    Limits are sent from the backend with each tool call.
    This engine validates before the tool executes.
    """

    def __init__(self, workspace_root: str = "/workspace"):
        self._workspace_root = Path(workspace_root).resolve()
        self._rate_limiter = RateLimiter()
        self._active_shells: Dict[str, int] = defaultdict(int)  # agent_id → count
        self._active_browsers: Dict[str, int] = defaultdict(int)

    def validate_request(self, request: ToolRequest) -> Optional[str]:
        """
        Validate a tool request against the agent's limits.
        
        Returns None if OK, or an error string if denied.
        """
        limits = request.limits
        if not limits:
            return None  # No limits configured — allow all

        # 1. Tool permission check
        if limits.enabled_tools and request.tool not in limits.enabled_tools:
            return f"Tool '{request.tool}' is not enabled for agent '{limits.agent_name}'"

        # 2. Rate limit
        rate_key = f"{request.agent_id}:{request.tool}"
        if not self._rate_limiter.check(rate_key, limits.max_requests_per_minute):
            return f"Rate limit exceeded for agent '{limits.agent_name}': {limits.max_requests_per_minute}/min"

        # 3. Tool-specific checks
        tool_lower = request.tool.lower()

        if tool_lower in ("read_file", "write_file", "append_file", "delete_file", "list_directory"):
            return self._validate_filesystem(request, limits)

        if tool_lower in ("run_command", "persistent_shell"):
            return self._validate_command(request, limits)

        if tool_lower == "browser":
            return self._validate_browser(request, limits)

        if request.tool_type.value == "service":
            return self._validate_service(request, limits)

        return None

    def _validate_filesystem(self, request: ToolRequest, limits: AgentLimits) -> Optional[str]:
        """Check file path against allowed/blocked lists."""
        file_path = request.arguments.get("file_path") or request.arguments.get("path") or ""
        if not file_path:
            return None

        # Normalize to absolute path
        try:
            resolved = Path(file_path).resolve()
        except (ValueError, OSError):
            return f"Invalid file path: {file_path}"

        # Check blocked paths (glob patterns)
        for pattern in limits.blocked_paths:
            if fnmatch.fnmatch(str(resolved), pattern) or fnmatch.fnmatch(resolved.name, pattern):
                return f"Access denied: path '{file_path}' matches blocked pattern '{pattern}'"

        # Check allowed paths
        if limits.allowed_paths:
            allowed = False
            for allowed_path in limits.allowed_paths:
                try:
                    allowed_resolved = Path(allowed_path).resolve()
                    if str(resolved).startswith(str(allowed_resolved)):
                        allowed = True
                        break
                except (ValueError, OSError):
                    continue
            if not allowed:
                return f"Access denied: path '{file_path}' is outside allowed directories"

        # Check file size for write operations
        if request.tool.lower() in ("write_file", "append_file"):
            content = request.arguments.get("content", "")
            if len(content.encode("utf-8", errors="replace")) > limits.max_file_size_bytes:
                return f"File content exceeds max size ({limits.max_file_size_bytes} bytes)"

        return None

    def _validate_command(self, request: ToolRequest, limits: AgentLimits) -> Optional[str]:
        """Check shell command against allowed/blocked lists."""
        command = request.arguments.get("command", "")
        if not command:
            return None

        # Check blocked commands
        cmd_lower = command.lower().strip()
        for blocked in limits.blocked_commands:
            if blocked.lower() in cmd_lower:
                return f"Command blocked: '{command}' matches blocked pattern '{blocked}'"

        # Check allowed commands (match by first word / binary name)
        if limits.allowed_commands:
            first_word = cmd_lower.split()[0] if cmd_lower.split() else ""
            # Strip path prefix (e.g., /usr/bin/npm → npm)
            binary = first_word.rsplit("/", 1)[-1].rsplit("\\", 1)[-1]
            if binary not in [c.lower() for c in limits.allowed_commands]:
                return f"Command not allowed: '{binary}' is not in the allowed list"

        # Check timeout
        timeout = request.arguments.get("timeout", limits.max_shell_timeout)
        if timeout > limits.max_shell_timeout:
            return f"Shell timeout {timeout}s exceeds max ({limits.max_shell_timeout}s)"

        # Check concurrent shells
        if self._active_shells.get(request.agent_id, 0) >= limits.max_concurrent_shells:
            return f"Max concurrent shells ({limits.max_concurrent_shells}) reached"

        return None

    def _validate_browser(self, request: ToolRequest, limits: AgentLimits) -> Optional[str]:
        """Check browser session limits."""
        action = request.arguments.get("action", "")
        if action == "launch":
            if self._active_browsers.get(request.agent_id, 0) >= limits.max_browser_sessions:
                return f"Max browser sessions ({limits.max_browser_sessions}) reached"
        return None

    def _validate_service(self, request: ToolRequest, limits: AgentLimits) -> Optional[str]:
        """Check service execution permissions."""
        if not request.execution_spec or not request.execution_spec.auth:
            return None

        service_slug = request.execution_spec.auth.service_slug
        if limits.allowed_services and service_slug not in limits.allowed_services:
            return f"Service '{service_slug}' is not allowed for agent '{limits.agent_name}'"

        return None

    # --- Resource tracking ---

    def track_shell_start(self, agent_id: str):
        self._active_shells[agent_id] = self._active_shells.get(agent_id, 0) + 1

    def track_shell_end(self, agent_id: str):
        self._active_shells[agent_id] = max(0, self._active_shells.get(agent_id, 0) - 1)

    def track_browser_start(self, agent_id: str):
        self._active_browsers[agent_id] = self._active_browsers.get(agent_id, 0) + 1

    def track_browser_end(self, agent_id: str):
        self._active_browsers[agent_id] = max(0, self._active_browsers.get(agent_id, 0) - 1)
