import re
from typing import Any, Dict

# Exact key blocklist (always PII)
PII_EXACT_KEYS = {
    "user_email",
    "email",
    "org_id",
    "account_id",
    "sheet_id",
    "doc_id",
    "sfid",
    "ip_address",
    "ip",
    "phone",
    "address",
}

# Substring patterns that indicate PII
PII_KEY_PATTERNS = [
    "email",
    "domain",
    "sheet",
    "doc",
    "sfid",
    "record",
    "account",
    "org",
    "url",
    "prompt",
    "text",
    "password",
    "secret",
    "token",
    "key",
    "credential",
]

# Regex patterns for scrubbing PII from values
PII_VALUE_PATTERNS = [
    re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"),
    re.compile(r"https?://[^\s<>\"{}|\\^`\\[\\]]+"),
    re.compile(r"\b[a-z0-9-]+\.(com|org|net|io|co|ai|app)\b", re.I),
]

MAX_STRING_LENGTH = 256
MAX_ARRAY_LENGTH = 10
MAX_DICT_KEYS = 50
MAX_DEPTH = 5


def sanitize_context(raw: Dict[str, Any]) -> Dict[str, Any]:
    """
    Remove PII fields from context dict before storage.

    Protection layers:
    1. Key blocklist (exact + substring patterns)
    2. Value pattern scrubbing (emails, URLs, domains)
    3. Size limits (string length, array size, depth)
    """

    def _is_blocked_key(key: str) -> bool:
        key_lower = key.lower()
        if key_lower in PII_EXACT_KEYS:
            return True
        return any(pattern in key_lower for pattern in PII_KEY_PATTERNS)

    def _scrub_string(value: str) -> str:
        result = value
        for pattern in PII_VALUE_PATTERNS:
            result = pattern.sub("[REDACTED]", result)
        return result[:MAX_STRING_LENGTH]

    def _clean(obj: Any, depth: int = 0) -> Any:
        if depth > MAX_DEPTH:
            return "[MAX_DEPTH_EXCEEDED]"

        if isinstance(obj, dict):
            cleaned: Dict[str, Any] = {}
            for i, (k, v) in enumerate(obj.items()):
                if i >= MAX_DICT_KEYS:
                    break
                key = str(k)
                if _is_blocked_key(key):
                    continue
                cleaned[key] = _clean(v, depth + 1)
            return cleaned

        if isinstance(obj, list):
            return [_clean(item, depth + 1) for item in obj[:MAX_ARRAY_LENGTH]]

        if isinstance(obj, str):
            return _scrub_string(obj)

        if isinstance(obj, (int, float, bool)) or obj is None:
            return obj

        return _scrub_string(str(obj))

    if not isinstance(raw, dict):
        return {}

    return _clean(raw)
