"""Centralized secrets redaction utility.

Provides pattern-based detection and masking of sensitive values
such as API keys, tokens, and credentials. Integrated into CLI
output, structlog processors, and error detail formatting.

Based on specification section "Secret Management" (line 27427):
- Patterns: ``sk-``, ``sk-ant-``, ``tok_``, bearer tokens
- Replacement: ``***REDACTED***``
- Applied at structlog processor level and CLI output

See Also:
    - ``docs/reference/secrets_handling.md``
"""

from __future__ import annotations

import re
import threading
from collections.abc import MutableMapping
from typing import Any

REDACTED = "***REDACTED***"

# ── Sensitive key-name substrings ──────────────────────────────

_SENSITIVE_SUBSTRINGS: set[str] = {
    "api_key",
    "apikey",
    "api-key",
    "password",
    "passwd",
    "secret",
    "token",
    "credential",
    "private_key",
    "access_key",
    "auth",
}

# Keys whose *entire* name (case-insensitive) is non-sensitive even
# though they contain a sensitive substring (e.g. "token_count").
_FALSE_POSITIVE_KEYS: set[str] = {
    "token_count",
    "token_limit",
    "token_usage",
    "max_tokens",
    "total_tokens",
    "prompt_tokens",
    "completion_tokens",
    "token_estimate",
    "hot_max_tokens",
    "summary_max_tokens",
    "auth_method",
    "auth_type",
    "auth_enabled",
}

# ── Secret value patterns (compiled) ──────────────────────────

_SECRET_PATTERNS: list[re.Pattern[str]] = [
    # OpenAI keys: sk-proj-..., sk-...
    re.compile(r"sk-(?:proj-)?[A-Za-z0-9_-]{10,}"),
    # Anthropic keys: sk-ant-api03-...
    re.compile(r"sk-ant-[A-Za-z0-9_-]{10,}"),
    # Google / Gemini API keys: AIzaSy...
    re.compile(r"AIzaSy[A-Za-z0-9_-]{30,}"),
    # Token IDs: tok_...
    re.compile(r"tok_[A-Za-z0-9]{10,}"),
    # Bearer tokens
    re.compile(r"Bearer\s+[A-Za-z0-9._~+/=-]{20,}"),
    # Generic long hex/base64 keys (40+ chars)
    re.compile(r"(?:key|KEY)-[A-Za-z0-9]{20,}"),
]

# Lock for thread-safe custom pattern registration
_patterns_lock = threading.Lock()

# ── Global show_secrets flag ──────────────────────────────────

_show_secrets: bool = False
_flag_lock = threading.Lock()


def get_show_secrets() -> bool:
    """Return the current global show_secrets flag."""
    with _flag_lock:
        return _show_secrets


def set_show_secrets(value: bool) -> None:
    """Set the global show_secrets flag.

    Args:
        value: Whether to reveal secrets in output.

    Raises:
        TypeError: If value is not a bool.
    """
    if not isinstance(value, bool):
        raise TypeError(f"show_secrets must be bool, got {type(value).__name__}")
    global _show_secrets
    with _flag_lock:
        _show_secrets = value


# ── Public API ────────────────────────────────────────────────


def is_sensitive_key(key: str) -> bool:
    """Check whether a key name indicates a secret value.

    Args:
        key: The field or key name to check.

    Returns:
        True if the key name suggests a sensitive value.
    """
    if not key:
        return False
    lower = key.lower()
    if lower in _FALSE_POSITIVE_KEYS:
        return False
    return any(sub in lower for sub in _SENSITIVE_SUBSTRINGS)


def redact_value(value: str) -> str:
    """Scan a string for secret patterns and replace matches.

    Args:
        value: The string to scan and redact.

    Returns:
        The string with any detected secrets replaced by
        ``***REDACTED***``.
    """
    if not value:
        return value
    result = value
    with _patterns_lock:
        patterns = list(_SECRET_PATTERNS)
    for pattern in patterns:
        result = pattern.sub(REDACTED, result)
    return result


def redact_dict(
    data: dict[str, Any],
    *,
    show_secrets: bool | None = None,
) -> dict[str, Any]:
    """Recursively redact sensitive keys and values in a dict.

    Args:
        data: The dictionary to redact.
        show_secrets: Override for the global flag. When ``None``
            (default), uses the global ``get_show_secrets()`` value.

    Returns:
        A new dict with sensitive values replaced by
        ``***REDACTED***``.
    """
    if show_secrets is None:
        show_secrets = get_show_secrets()
    if show_secrets:
        return dict(data)
    return _redact_dict_inner(data)


def _redact_dict_inner(data: dict[str, Any]) -> dict[str, Any]:
    """Recursive implementation of dict redaction."""
    result: dict[str, Any] = {}
    for key, value in data.items():
        if is_sensitive_key(key):
            result[key] = REDACTED
        elif isinstance(value, dict):
            result[key] = _redact_dict_inner(value)
        elif isinstance(value, str):
            result[key] = redact_value(value)
        elif isinstance(value, list):
            result[key] = [
                _redact_dict_inner(item)
                if isinstance(item, dict)
                else (redact_value(item) if isinstance(item, str) else item)
                for item in value
            ]
        else:
            result[key] = value
    return result


def mask_database_url(url: str) -> str:
    """Mask credentials in a database URL.

    SQLite URLs (no credentials) are returned unchanged. URLs
    with ``user:password@host`` patterns have the password
    replaced.

    Args:
        url: A SQLAlchemy-style database URL.

    Returns:
        The URL with any embedded password masked.
    """
    if not url:
        return url
    if url.startswith("sqlite"):
        return url
    # Pattern: scheme://user:password@host...
    masked = re.sub(
        r"(://[^:]+:)([^@]+)(@)",
        r"\1***\3",
        url,
    )
    return masked


def register_pattern(pattern: str) -> None:
    """Register a custom secret detection regex pattern.

    Args:
        pattern: A regular expression string to match secrets.

    Raises:
        ValueError: If the pattern is empty.
        re.error: If the pattern is invalid regex.
    """
    if not pattern:
        raise ValueError("pattern cannot be empty")
    compiled = re.compile(pattern)
    with _patterns_lock:
        _SECRET_PATTERNS.append(compiled)


# ── structlog processor ───────────────────────────────────────


def secrets_masking_processor(
    logger: Any,
    method_name: str,
    event_dict: MutableMapping[str, Any],
) -> MutableMapping[str, Any]:
    """structlog processor that redacts secrets from log events.

    Walks all values in the event dict and applies pattern-based
    redaction. Intended to be inserted into the structlog
    processor chain.

    Args:
        logger: The wrapped logger object (unused).
        method_name: The name of the log method (unused).
        event_dict: The structured log event dictionary.

    Returns:
        The event dict with secrets masked.
    """
    if get_show_secrets():
        return event_dict

    result: dict[str, Any] = {}
    for key, value in event_dict.items():
        if is_sensitive_key(key):
            result[key] = REDACTED
        elif isinstance(value, str):
            result[key] = redact_value(value)
        elif isinstance(value, dict):
            result[key] = _redact_dict_inner(value)
        else:
            result[key] = value
    return result
