"""Secret masking utilities for VERONICA Security Containment Layer."""

from __future__ import annotations

import re
from typing import Any


# Pattern registry: (label, compiled_regex)
_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
    # AWS access key
    ("AWS_KEY", re.compile(r"\b(AKIA[0-9A-Z]{16})\b")),
    # AWS secret key (40-char base64-ish after common prefixes)
    (
        "AWS_SECRET",
        re.compile(
            r"(?i)aws[_\-. ]?secret[_\-. ]?(?:access[_\-. ]?)?key\s*[=:]\s*([^\s,;\"']{20,})"
        ),
    ),
    # GitHub new fine-grained PAT format: github_pat_... (must be BEFORE GITHUB_TOKEN)
    ("GITHUB_FINE_GRAINED", re.compile(r"\b(github_pat_[A-Za-z0-9_]{82,})\b")),
    # GitHub CLI OAuth tokens: gho_... (must be BEFORE GITHUB_TOKEN to avoid gh[o] overlap)
    ("GITHUB_CLI_TOKEN", re.compile(r"\b(gho_[A-Za-z0-9]{36,})\b")),
    # GitHub tokens (classic PAT + server; excludes gho_ which is caught above)
    ("GITHUB_TOKEN", re.compile(r"\b(gh[psua]_[A-Za-z0-9]{36,})\b")),
    # Stripe live keys
    (
        "STRIPE_KEY",
        re.compile(r"\b(sk_live_[A-Za-z0-9]{24,}|pk_live_[A-Za-z0-9]{24,})\b"),
    ),
    # Firebase / Supabase service role JWTs (long JWT tokens with known header)
    (
        "SERVICE_ROLE_JWT",
        re.compile(
            r"\beyJ[A-Za-z0-9+/=]{40,}\.[A-Za-z0-9+/=]{40,}\.[A-Za-z0-9\-_]{20,}\b"
        ),
    ),
    # Resend API keys
    ("RESEND_KEY", re.compile(r"\b(re_[A-Za-z0-9]{20,})\b")),
    # bitbank API keys (32-char hex-ish)
    (
        "BITBANK_KEY",
        re.compile(
            r"(?i)bitbank[_\-. ]?(?:api[_\-. ]?)?(?:key|secret)\s*[=:]\s*([^\s,;\"']{20,})"
        ),
    ),
    # Anthropic API keys -- must come BEFORE OPENAI_KEY (sk-ant- is a subset of sk-)
    ("ANTHROPIC_KEY", re.compile(r"\b(sk-ant-[A-Za-z0-9\-_]{20,})\b")),
    # OpenAI API keys (sk-proj-* and legacy sk-*)
    ("OPENAI_KEY", re.compile(r"\b(sk-(?:proj-)?[A-Za-z0-9\-_]{20,})\b")),
    # Slack bot/user/webhook tokens
    ("SLACK_TOKEN", re.compile(r"\b(xox[bposa]-[A-Za-z0-9\-]{10,})\b")),
    # Slack webhook URLs
    (
        "SLACK_WEBHOOK",
        re.compile(r"(https://hooks\.slack\.com/services/[A-Za-z0-9/]+)"),
    ),
    # Discord bot tokens
    (
        "DISCORD_TOKEN",
        re.compile(
            r"\b([MNO][A-Za-z0-9_\-]{23})\.[A-Za-z0-9_\-]{6}\.[A-Za-z0-9_\-]{27,}\b"
        ),
    ),
    # Twilio account SIDs and auth tokens
    ("TWILIO_SID", re.compile(r"\b(AC[0-9a-f]{32})\b")),
    (
        "TWILIO_TOKEN",
        re.compile(
            r"(?i)twilio[_\-. ]?(?:auth[_\-. ]?)?token\s*[=:]\s*([^\s,;\"']{20,})"
        ),
    ),
    # SendGrid API keys
    ("SENDGRID_KEY", re.compile(r"\b(SG\.[A-Za-z0-9_\-]{22}\.[A-Za-z0-9_\-]{43})\b")),
    # Google API keys
    ("GOOGLE_API_KEY", re.compile(r"\b(AIza[0-9A-Za-z\-_]{35})\b")),
    # Google OAuth2 client secrets
    ("GOOGLE_OAUTH", re.compile(r"\b(GOCSPX-[A-Za-z0-9_\-]{28,})\b")),
    # Azure SAS tokens and connection strings
    (
        "AZURE_SAS",
        re.compile(r"(?i)(?:sig|SharedAccessSignature)[=&]([A-Za-z0-9%+/=]{20,})"),
    ),
    # PGP private key blocks
    ("PGP_PRIVATE_KEY", re.compile(r"-----BEGIN PGP PRIVATE KEY BLOCK-----")),
    # SSH / RSA / EC / DSA / OPENSSH private key blocks (expanded to cover all OpenSSH formats)
    (
        "SSH_PRIVATE_KEY",
        re.compile(r"-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----"),
    ),
    # .netrc password field -- "password <value>" on its own line or inline
    ("NETRC_PASSWORD", re.compile(r"(?i)\bpassword\s+([^\s\r\n]{4,})")),
    # npm tokens
    ("NPM_TOKEN", re.compile(r"\b(npm_[A-Za-z0-9]{36,})\b")),
    # PyPI API tokens (real tokens are 50+ chars after pypi-)
    ("PYPI_TOKEN", re.compile(r"\b(pypi-[A-Za-z0-9_\-]{50,})\b")),
    # Polymarket API keys (common in this project)
    (
        "POLYMARKET_KEY",
        re.compile(
            r"(?i)polymarket[_\-. ]?(?:api[_\-. ]?)?(?:key|secret|token)\s*[=:]\s*([^\s,;\"']{20,})"
        ),
    ),
    # Generic 40+ hex strings that appear after a key/token/secret label.
    # Minimum length 40 skips MD5 (32 chars) and most git short hashes.
    # Context requirement (key=/token=/secret= prefix) prevents false positives
    # on SHA-256/SHA-512 digests, git commit hashes, and other hex-encoded data.
    (
        "HEX_SECRET",
        re.compile(
            r"(?i)(?:key|token|secret|password|apikey|api_key|access_key|auth)[=:\s]+([0-9a-fA-F]{40,})"
        ),
    ),
    # password=value, passwd=value, secret=value patterns (unquoted values)
    (
        "PASSWORD_KV",
        re.compile(
            r"(?i)(?:password|passwd|secret|token|api_key|apikey|access_key)\s*[=:]\s*([^\s,;\"'&]{4,})"
        ),
    ),
    # Quoted YAML / JSON password values: password: "my_secret" or password: 'my_secret'
    # H-4 fix: the base PASSWORD_KV pattern excludes quote chars from the capture group,
    # so password: "my_api_key_12345" produces an empty capture and the secret leaks.
    (
        "PASSWORD_KV_QUOTED",
        re.compile(
            r"(?i)(?:password|passwd|secret|token|api_key|apikey|access_key)\s*[=:]\s*[\"']([^\"']{4,})[\"']"
        ),
    ),
]


class SecretMasker:
    """Masks sensitive secrets in strings, dicts, and argument lists."""

    def __init__(self) -> None:
        # Pre-bind one replacement callable per pattern so that mask() never
        # allocates new function objects on every call.
        def _make_replacer(
            tag: str, has_group: bool
        ):  # -> Callable[[re.Match[str]], str]
            if has_group:

                def _replace_group(m: re.Match[str]) -> str:
                    if m.lastindex:
                        return m.group(0).replace(m.group(1), tag, 1)
                    return tag

                return _replace_group
            else:

                def _replace_full(m: re.Match[str]) -> str:  # noqa: ARG001
                    return tag

                return _replace_full

        self._compiled: list[tuple[re.Pattern[str], object]] = [
            (pattern, _make_replacer(f"[REDACTED:{label}]", pattern.groups > 0))
            for label, pattern in _PATTERNS
        ]

    def mask(self, text: str) -> str:
        """Replace detected secrets in *text* with ``[REDACTED:<type>]``."""
        for pattern, replacer in self._compiled:
            text = pattern.sub(replacer, text)  # type: ignore[arg-type]
        return text

    # Hard recursion depth limit to prevent infinite loops on self-referential
    # containers (DoS protection).
    _MAX_DEPTH: int = 50

    def _mask_value(self, key: str, value: Any, _depth: int = 0) -> Any:
        """Dispatch masking for a single value based on its type.

        - str → apply secret patterns
        - dict → recurse via mask_dict
        - list | tuple → iterate elements, recurse on each
        - other → pass through unchanged

        Args:
            key: The dict key associated with this value (for logging context).
            value: The value to mask.
            _depth: Internal recursion depth counter; passed automatically.
        """
        if _depth >= self._MAX_DEPTH:
            # At max depth, still mask string/bytes values (non-recursive) to
            # prevent secrets hidden in deeply nested structures from leaking.
            # For containers, apply a shallow single-pass mask to immediate
            # string/bytes children so that secrets one level deeper are caught.
            if isinstance(value, str):
                return self.mask(value)
            if isinstance(value, bytes):
                return self.mask(value.decode("utf-8", errors="replace"))
            if isinstance(value, dict):
                return {
                    k: self.mask(v)
                    if isinstance(v, str)
                    else self.mask(v.decode("utf-8", errors="replace"))
                    if isinstance(v, bytes)
                    else v
                    for k, v in value.items()
                }
            if isinstance(value, (list, tuple)):
                masked = [
                    self.mask(item)
                    if isinstance(item, str)
                    else self.mask(item.decode("utf-8", errors="replace"))
                    if isinstance(item, bytes)
                    else item
                    for item in value
                ]
                return type(value)(masked) if isinstance(value, tuple) else masked
            return value
        if isinstance(value, bytes):
            return self._mask_value(
                key, value.decode("utf-8", errors="replace"), _depth
            )
        if isinstance(value, str):
            return self.mask(value)
        if isinstance(value, dict):
            return {k: self._mask_value(k, v, _depth + 1) for k, v in value.items()}
        if isinstance(value, (list, tuple)):
            masked = [self._mask_value(key, item, _depth + 1) for item in value]
            # Reconstruct as the same concrete type where possible.
            # namedtuples and custom sequence subclasses may not accept a
            # single-iterable constructor (TypeError) or may raise other
            # exceptions -- fall back to plain list/tuple in all error cases.
            try:
                return type(value)(masked)
            except Exception:
                return list(masked) if isinstance(value, list) else tuple(masked)
        return value

    def mask_dict(self, d: dict[str, Any], _depth: int = 0) -> dict[str, Any]:
        """Recursively mask secrets in all string values inside a dict.

        Handles arbitrarily nested structures: dicts, lists of dicts,
        tuples, and mixed-type containers are all traversed fully.
        Recursion is bounded by ``_MAX_DEPTH`` to prevent DoS via self-referential
        containers.
        """
        return {key: self._mask_value(key, value, _depth) for key, value in d.items()}

    def mask_args(self, args: list[str]) -> list[str]:
        """Mask secrets in each element of a list of strings."""
        return [self.mask(arg) for arg in args]
