"""
Brynq Runtime Security — Agent Sandboxing & Resource Isolation.

Security primitives for in-process agents. Prevents:
- File access outside project directory (path traversal, symlink escape)
- API key leakage through broker messages or environment
- Resource exhaustion (CPU, memory, API quota)
- Cross-agent interference (session hijack, task modification)

Built from RUNTIME_SECURITY_PLAN.md.
"""

import os
import re
import time
import threading
from pathlib import Path
from typing import Optional


# ── File Access Sandbox ──────────────────────────────────────────────────


class FileSandbox:
    """Restricts file operations to a root directory.

    All paths are resolved to absolute paths and checked against the
    allowed root. Symlinks are resolved before checking to prevent
    symlink-based escapes.
    """

    def __init__(self, root: str, read_extra: Optional[list[str]] = None):
        self.root = Path(root).resolve()
        self.read_extra = [Path(p).resolve() for p in (read_extra or [])]
        self._deny_read = [
            Path.home() / ".brynq" / "vault.json",
            Path.home() / ".ssh",
            Path.home() / ".aws",
            Path.home() / ".env",
        ]

    def check_read(self, path: str) -> bool:
        """Return True if the agent may read this path."""
        resolved = Path(path).resolve()
        for denied in self._deny_read:
            if resolved == denied or self._is_under(resolved, denied):
                return False
        if self._is_under(resolved, self.root):
            return True
        for extra in self.read_extra:
            if self._is_under(resolved, extra):
                return True
        return False

    def check_write(self, path: str) -> bool:
        """Return True if the agent may write this path."""
        resolved = Path(path).resolve()
        for denied in self._deny_read:
            if resolved == denied or self._is_under(resolved, denied):
                return False
        return self._is_under(resolved, self.root)

    def check_delete(self, path: str) -> bool:
        """Same as write, but also blocks root dir, .git, and broker state."""
        resolved = Path(path).resolve()
        if resolved == self.root:
            return False
        if ".git" in resolved.parts:
            return False
        if "state" in resolved.parts and "broker" in resolved.parts:
            return False
        return self.check_write(path)

    def safe_open(self, path: str, mode: str = "r"):
        """Open a file with sandbox enforcement. Raises PermissionError if blocked."""
        is_write = any(c in mode for c in "wxa+")
        if is_write:
            if not self.check_write(path):
                raise PermissionError(f"Write denied: {path} outside sandbox {self.root}")
        else:
            if not self.check_read(path):
                raise PermissionError(f"Read denied: {path} outside sandbox")
        return open(path, mode)

    @staticmethod
    def _is_under(child: Path, parent: Path) -> bool:
        try:
            child.relative_to(parent)
            return True
        except ValueError:
            return False


# ── Credential Scanner ───────────────────────────────────────────────────

# Patterns that indicate leaked credentials
_CREDENTIAL_PATTERNS = [
    re.compile(r"sk-[a-zA-Z0-9]{20,}"),           # OpenAI / Stripe secret keys
    re.compile(r"sk_live_[a-zA-Z0-9]{20,}"),       # Stripe live keys
    re.compile(r"sk_test_[a-zA-Z0-9]{20,}"),       # Stripe test keys
    re.compile(r"AKIA[0-9A-Z]{16}"),               # AWS access keys
    re.compile(r"AIza[0-9A-Za-z\-_]{35}"),         # Google API keys
    re.compile(r"ghp_[a-zA-Z0-9]{36}"),            # GitHub personal tokens
    re.compile(r"gho_[a-zA-Z0-9]{36}"),            # GitHub OAuth tokens
    re.compile(r"brynq_[a-zA-Z0-9_\-]{32,}"),     # Brynq API keys
    re.compile(r"xox[bporas]-[0-9a-zA-Z\-]{10,}"), # Slack tokens
    re.compile(r"-----BEGIN (RSA |EC )?PRIVATE KEY-----"),  # Private keys
]


def scan_for_credentials(text: str) -> list[str]:
    """Scan text for credential patterns. Returns list of matched pattern descriptions."""
    found = []
    for pattern in _CREDENTIAL_PATTERNS:
        if pattern.search(text):
            found.append(pattern.pattern[:40])
    return found


def sanitize_message(text: str) -> str:
    """Redact detected credentials from a message."""
    result = text
    for pattern in _CREDENTIAL_PATTERNS:
        result = pattern.sub("[REDACTED]", result)
    return result


# ── Environment Isolation ────────────────────────────────────────────────

_SENSITIVE_ENV_VARS = {
    "STRIPE_API_KEY", "SQUARE_ACCESS_TOKEN", "ANTHROPIC_API_KEY",
    "GEMINI_API_KEY", "OPENAI_API_KEY", "AWS_SECRET_ACCESS_KEY",
    "GITHUB_TOKEN", "BRYNQ_JWT_SECRET", "DATABASE_URL",
}


def get_safe_env(key: str) -> Optional[str]:
    """Get an environment variable, blocking access to sensitive keys."""
    if key.upper() in _SENSITIVE_ENV_VARS:
        return None
    return os.environ.get(key)


def get_sanitized_env() -> dict[str, str]:
    """Return a copy of os.environ with sensitive values redacted."""
    env = {}
    for key, value in os.environ.items():
        if key.upper() in _SENSITIVE_ENV_VARS:
            env[key] = "[REDACTED]"
        else:
            env[key] = value
    return env


# ── Resource Limiter ─────────────────────────────────────────────────────


class ResourceLimiter:
    """Track and enforce resource limits per agent.

    Enforces:
    - Max messages per minute (rate limiting)
    - Max concurrent tasks
    - Max API calls per hour
    - Max file writes per minute
    """

    def __init__(
        self,
        max_messages_per_min: int = 30,
        max_concurrent_tasks: int = 5,
        max_api_calls_per_hour: int = 500,
        max_file_writes_per_min: int = 50,
    ):
        self.limits = {
            "messages": max_messages_per_min,
            "tasks": max_concurrent_tasks,
            "api_calls": max_api_calls_per_hour,
            "file_writes": max_file_writes_per_min,
        }
        self._counters: dict[str, list[float]] = {k: [] for k in self.limits}
        self._active_tasks = 0
        self._lock = threading.Lock()

    def check(self, resource: str) -> bool:
        """Check if an action is within limits. Returns True if allowed."""
        if resource not in self.limits:
            return True

        with self._lock:
            if resource == "tasks":
                return self._active_tasks < self.limits["tasks"]

            now = time.time()
            window = 60 if resource != "api_calls" else 3600
            cutoff = now - window

            # Prune old entries
            self._counters[resource] = [t for t in self._counters[resource] if t > cutoff]

            return len(self._counters[resource]) < self.limits[resource]

    def record(self, resource: str) -> bool:
        """Record a resource usage. Returns True if allowed, False if rate-limited."""
        if not self.check(resource):
            return False

        with self._lock:
            if resource == "tasks":
                self._active_tasks += 1
            else:
                self._counters[resource].append(time.time())
        return True

    def release_task(self):
        """Mark a task as complete, freeing a slot."""
        with self._lock:
            self._active_tasks = max(0, self._active_tasks - 1)

    def get_usage(self) -> dict:
        """Get current resource usage."""
        with self._lock:
            now = time.time()
            return {
                "messages_per_min": len([t for t in self._counters.get("messages", []) if t > now - 60]),
                "api_calls_per_hour": len([t for t in self._counters.get("api_calls", []) if t > now - 3600]),
                "file_writes_per_min": len([t for t in self._counters.get("file_writes", []) if t > now - 60]),
                "active_tasks": self._active_tasks,
                "limits": self.limits,
            }


# ── Agent Security Context ───────────────────────────────────────────────


class AgentSecurityContext:
    """Combined security context for an in-process agent.

    Bundles file sandbox, credential scanning, environment isolation,
    and resource limiting into a single object passed to each agent.
    """

    def __init__(
        self,
        agent_id: str,
        project_root: str,
        trust_level: str = "contributor",
        read_extra: Optional[list[str]] = None,
    ):
        self.agent_id = agent_id
        self.trust_level = trust_level
        self.sandbox = FileSandbox(project_root, read_extra=read_extra)

        # Adjust resource limits by trust level
        limits = {
            "intern": {"messages": 10, "tasks": 1, "api_calls": 50, "file_writes": 10},
            "contributor": {"messages": 30, "tasks": 3, "api_calls": 200, "file_writes": 50},
            "trusted": {"messages": 60, "tasks": 5, "api_calls": 500, "file_writes": 100},
            "core": {"messages": 120, "tasks": 10, "api_calls": 1000, "file_writes": 200},
        }
        lim = limits.get(trust_level, limits["contributor"])
        self.limiter = ResourceLimiter(
            max_messages_per_min=lim["messages"],
            max_concurrent_tasks=lim["tasks"],
            max_api_calls_per_hour=lim["api_calls"],
            max_file_writes_per_min=lim["file_writes"],
        )
        self.violations: list[dict] = []

    def check_message(self, text: str) -> dict:
        """Check if a message is safe to send. Scans for credentials and rate limits."""
        creds = scan_for_credentials(text)
        if creds:
            self.violations.append({
                "type": "credential_leak",
                "patterns": creds,
                "timestamp": time.time(),
                "agent_id": self.agent_id,
            })
            return {"allowed": False, "reason": "Message contains detected credentials", "sanitized": sanitize_message(text)}

        if not self.limiter.check("messages"):
            return {"allowed": False, "reason": "Rate limit exceeded"}

        self.limiter.record("messages")
        return {"allowed": True}

    def check_file_access(self, path: str, mode: str = "r") -> dict:
        """Check if file access is allowed."""
        is_write = any(c in mode for c in "wxa+")
        if is_write:
            if not self.sandbox.check_write(path):
                return {"allowed": False, "reason": f"Write denied: outside sandbox {self.sandbox.root}"}
            if not self.limiter.record("file_writes"):
                return {"allowed": False, "reason": "File write rate limit exceeded"}
        else:
            if not self.sandbox.check_read(path):
                return {"allowed": False, "reason": "Read denied: outside sandbox"}
        return {"allowed": True}

    def check_task_accept(self) -> dict:
        """Check if agent can accept another task."""
        if not self.limiter.check("tasks"):
            return {"allowed": False, "reason": f"Max concurrent tasks reached ({self.limiter.limits['tasks']})"}
        return {"allowed": True}

    def get_status(self) -> dict:
        """Get security context status for monitoring."""
        return {
            "agent_id": self.agent_id,
            "trust_level": self.trust_level,
            "sandbox_root": str(self.sandbox.root),
            "resource_usage": self.limiter.get_usage(),
            "violation_count": len(self.violations),
            "recent_violations": self.violations[-5:],
        }
