"""Privacy Filter — strip PII from context before remote API calls.

Implements Vitalik's Pattern 2: "A local model can strip out private data
before passing the query along to a remote LLM."

When Hermes routes a request to a cloud provider (Anthropic, OpenRouter, etc.),
this module sanitizes the message context to remove personally identifiable
information before it leaves the user's machine.

Threat model (from Vitalik's secure LLM architecture):
- Privacy (other): Non-LLM data leakage via search queries, API calls
- LLM accidents: LLM accidentally leaking private data in prompts
- LLM jailbreaks: Remote content extracting private context

Usage:
    from agent.privacy_filter import PrivacyFilter, sanitize_messages

    pf = PrivacyFilter()
    safe_messages = pf.sanitize_messages(messages)
    # safe_messages has PII replaced with [REDACTED] tokens
"""

from __future__ import annotations

import logging
import re
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Any, Dict, List, Optional, Tuple

logger = logging.getLogger(__name__)


class Sensitivity(Enum):
    """Classification of content sensitivity."""
    PUBLIC = auto()       # No PII detected
    LOW = auto()          # Generic references (e.g., city names)
    MEDIUM = auto()       # Personal identifiers (name, email, phone)
    HIGH = auto()         # Secrets, keys, financial data, medical info
    CRITICAL = auto()     # Crypto keys, passwords, SSN patterns


@dataclass
class RedactionReport:
    """Summary of what was redacted from a message batch."""
    total_messages: int = 0
    redacted_messages: int = 0
    redactions: List[Dict[str, Any]] = field(default_factory=list)
    max_sensitivity: Sensitivity = Sensitivity.PUBLIC

    @property
    def had_redactions(self) -> bool:
        return self.redacted_messages > 0

    def summary(self) -> str:
        if not self.had_redactions:
            return "No PII detected — context is clean for remote query."
        parts = [f"Redacted {self.redacted_messages}/{self.total_messages} messages:"]
        for r in self.redactions[:10]:
            parts.append(f"  - {r['type']}: {r['count']} occurrence(s)")
        if len(self.redactions) > 10:
            parts.append(f"  ... and {len(self.redactions) - 10} more types")
        return "\n".join(parts)


# =========================================================================
# PII pattern definitions
# =========================================================================

# Each pattern is (compiled_regex, redaction_type, sensitivity_level, replacement)
_PII_PATTERNS: List[Tuple[re.Pattern, str, Sensitivity, str]] = []


def _compile_patterns() -> None:
    """Compile PII detection patterns. Called once at module init."""
    global _PII_PATTERNS
    if _PII_PATTERNS:
        return

    raw_patterns = [
        # --- CRITICAL: secrets and credentials ---
        (
            r'(?:api[_-]?key|apikey|secret[_-]?key|access[_-]?token)\s*[:=]\s*["\']?([A-Za-z0-9_\-\.]{20,})["\']?',
            "api_key_or_token",
            Sensitivity.CRITICAL,
            "[REDACTED-API-KEY]",
        ),
        (
            r'\b(?:sk-|sk_|pk_|rk_|ak_)[A-Za-z0-9]{20,}\b',
            "prefixed_secret",
            Sensitivity.CRITICAL,
            "[REDACTED-SECRET]",
        ),
        (
            r'\b(?:ghp_|gho_|ghu_|ghs_|ghr_)[A-Za-z0-9]{36,}\b',
            "github_token",
            Sensitivity.CRITICAL,
            "[REDACTED-GITHUB-TOKEN]",
        ),
        (
            r'\b(?:xox[bposa]-[A-Za-z0-9\-]+)\b',
            "slack_token",
            Sensitivity.CRITICAL,
            "[REDACTED-SLACK-TOKEN]",
        ),
        (
            r'(?:password|passwd|pwd)\s*[:=]\s*["\']?([^\s"\']{4,})["\']?',
            "password",
            Sensitivity.CRITICAL,
            "[REDACTED-PASSWORD]",
        ),
        (
            r'(?:-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----)',
            "private_key_block",
            Sensitivity.CRITICAL,
            "[REDACTED-PRIVATE-KEY]",
        ),
        # Ethereum / crypto addresses (42-char hex starting with 0x)
        (
            r'\b0x[a-fA-F0-9]{40}\b',
            "ethereum_address",
            Sensitivity.HIGH,
            "[REDACTED-ETH-ADDR]",
        ),
        # Bitcoin addresses (base58, 25-34 chars starting with 1/3/bc1)
        (
            r'\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b',
            "bitcoin_address",
            Sensitivity.HIGH,
            "[REDACTED-BTC-ADDR]",
        ),
        (
            r'\bbc1[a-zA-HJ-NP-Z0-9]{39,59}\b',
            "bech32_address",
            Sensitivity.HIGH,
            "[REDACTED-BTC-ADDR]",
        ),
        # --- HIGH: financial ---
        (
            r'\b(?:\d{4}[-\s]?){3}\d{4}\b',
            "credit_card_number",
            Sensitivity.HIGH,
            "[REDACTED-CC]",
        ),
        (
            r'\b\d{3}-\d{2}-\d{4}\b',
            "us_ssn",
            Sensitivity.HIGH,
            "[REDACTED-SSN]",
        ),
        # --- MEDIUM: personal identifiers ---
        # Email addresses
        (
            r'\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b',
            "email_address",
            Sensitivity.MEDIUM,
            "[REDACTED-EMAIL]",
        ),
        # Phone numbers (US/international patterns)
        (
            r'\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b',
            "phone_number_us",
            Sensitivity.MEDIUM,
            "[REDACTED-PHONE]",
        ),
        (
            r'\b\+\d{1,3}[-.\s]?\d{4,14}\b',
            "phone_number_intl",
            Sensitivity.MEDIUM,
            "[REDACTED-PHONE]",
        ),
        # Filesystem paths that reveal user identity
        (
            r'(?:/Users/|/home/|C:\\Users\\)([A-Za-z0-9_\-]+)',
            "user_home_path",
            Sensitivity.MEDIUM,
            r"/Users/[REDACTED-USER]",
        ),
        # --- LOW: environment / system info ---
        # Internal IPs
        (
            r'\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b',
            "internal_ip",
            Sensitivity.LOW,
            "[REDACTED-IP]",
        ),
    ]

    _PII_PATTERNS = [
        (re.compile(pattern, re.IGNORECASE), rtype, sensitivity, replacement)
        for pattern, rtype, sensitivity, replacement in raw_patterns
    ]


_compile_patterns()


# =========================================================================
# Sensitive file path patterns (context-aware)
# =========================================================================

_SENSITIVE_PATH_PATTERNS = [
    re.compile(r'\.(?:env|pem|key|p12|pfx|jks|keystore)\b', re.IGNORECASE),
    re.compile(r'(?:\.ssh/|\.gnupg/|\.aws/|\.config/gcloud/)', re.IGNORECASE),
    re.compile(r'(?:wallet|keystore|seed|mnemonic)', re.IGNORECASE),
    re.compile(r'(?:\.hermes/\.env)', re.IGNORECASE),
]


def _classify_path_sensitivity(path: str) -> Sensitivity:
    """Check if a file path references sensitive material."""
    for pat in _SENSITIVE_PATH_PATTERNS:
        if pat.search(path):
            return Sensitivity.HIGH
    return Sensitivity.PUBLIC


# =========================================================================
# Core filtering
# =========================================================================

class PrivacyFilter:
    """Strip PII from message context before remote API calls.

    Integrates with the agent's message pipeline. Call sanitize_messages()
    before sending context to any cloud LLM provider.
    """

    def __init__(
        self,
        min_sensitivity: Sensitivity = Sensitivity.MEDIUM,
        aggressive_mode: bool = False,
    ):
        """
        Args:
            min_sensitivity: Only redact PII at or above this level.
                Default MEDIUM — redacts emails, phones, paths but not IPs.
            aggressive_mode: If True, also redact file paths and internal IPs.
        """
        self.min_sensitivity = (
            Sensitivity.LOW if aggressive_mode else min_sensitivity
        )
        self.aggressive_mode = aggressive_mode

    def sanitize_text(self, text: str) -> Tuple[str, List[Dict[str, Any]]]:
        """Sanitize a single text string. Returns (cleaned_text, redaction_list)."""
        redactions = []
        cleaned = text

        for pattern, rtype, sensitivity, replacement in _PII_PATTERNS:
            if sensitivity.value < self.min_sensitivity.value:
                continue

            matches = pattern.findall(cleaned)
            if matches:
                count = len(matches) if isinstance(matches[0], str) else sum(
                    1 for m in matches if m
                )
                if count > 0:
                    cleaned = pattern.sub(replacement, cleaned)
                    redactions.append({
                        "type": rtype,
                        "sensitivity": sensitivity.name,
                        "count": count,
                    })

        return cleaned, redactions

    def sanitize_messages(
        self, messages: List[Dict[str, Any]]
    ) -> Tuple[List[Dict[str, Any]], RedactionReport]:
        """Sanitize a list of OpenAI-format messages.

        Returns (safe_messages, report). System messages are NOT sanitized
        (they're typically static prompts). Only user and assistant messages
        with string content are processed.

        Args:
            messages: List of {"role": ..., "content": ...} dicts.

        Returns:
            Tuple of (sanitized_messages, redaction_report).
        """
        report = RedactionReport(total_messages=len(messages))
        safe_messages = []

        for msg in messages:
            role = msg.get("role", "")
            content = msg.get("content", "")

            # Only sanitize user/assistant string content
            if role in ("user", "assistant") and isinstance(content, str) and content:
                cleaned, redactions = self.sanitize_text(content)
                if redactions:
                    report.redacted_messages += 1
                    report.redactions.extend(redactions)
                    # Track max sensitivity
                    for r in redactions:
                        s = Sensitivity[r["sensitivity"]]
                        if s.value > report.max_sensitivity.value:
                            report.max_sensitivity = s
                    safe_msg = {**msg, "content": cleaned}
                    safe_messages.append(safe_msg)
                    logger.info(
                        "Privacy filter: redacted %d PII type(s) from %s message",
                        len(redactions), role,
                    )
                else:
                    safe_messages.append(msg)
            else:
                safe_messages.append(msg)

        return safe_messages, report

    def should_use_local_only(self, text: str) -> Tuple[bool, str]:
        """Determine if content is too sensitive for any remote call.

        Returns (should_block, reason). If True, the content should only
        be processed by a local model.
        """
        _, redactions = self.sanitize_text(text)

        critical_count = sum(
            1 for r in redactions
            if Sensitivity[r["sensitivity"]] == Sensitivity.CRITICAL
        )
        high_count = sum(
            1 for r in redactions
            if Sensitivity[r["sensitivity"]] == Sensitivity.HIGH
        )

        if critical_count > 0:
            return True, f"Contains {critical_count} critical-secret pattern(s) — local-only"
        if high_count >= 3:
            return True, f"Contains {high_count} high-sensitivity pattern(s) — local-only"
        return False, ""


def sanitize_messages(
    messages: List[Dict[str, Any]],
    min_sensitivity: Sensitivity = Sensitivity.MEDIUM,
    aggressive: bool = False,
) -> Tuple[List[Dict[str, Any]], RedactionReport]:
    """Convenience function: sanitize messages with default settings."""
    pf = PrivacyFilter(min_sensitivity=min_sensitivity, aggressive_mode=aggressive)
    return pf.sanitize_messages(messages)


def quick_sanitize(text: str) -> str:
    """Quick sanitize a single string — returns cleaned text only."""
    pf = PrivacyFilter()
    cleaned, _ = pf.sanitize_text(text)
    return cleaned
