"""런타임 비밀 마스킹 - 로그 및 출력에서 자격증명 자동 제거.

로그 메시지가 파일이나 콘솔에 기록되기 전 API 키, 토큰, 비밀번호 등의
민감 정보를 정규식 패턴으로 감지하여 마스킹합니다.

짧은 토큰(18자 미만)은 *** 로 완전 치환, 긴 토큰은 앞 6자 + ... + 뒤 4자 형식으로
일부 보존하여 디버깅 가능성을 유지합니다.

환경변수 DISABLE_REDACT_SECRETS=1 로 비활성화 가능합니다.

Usage:
    from utils.redact import setup_redacted_logging
    setup_redacted_logging()          # 루트 로거에 적용
    setup_redacted_logging(logger)    # 특정 로거에 적용
"""

import logging
import os
import re
from typing import Literal, cast

# ---------------------------------------------------------------------------
# 알려진 API 키 접두사 패턴
# ---------------------------------------------------------------------------
_PREFIX_PATTERNS = [
    r"sk-[A-Za-z0-9_-]{10,}",  # OpenAI / Anthropic (sk-ant-*)
    r"ghp_[A-Za-z0-9]{10,}",  # GitHub PAT classic
    r"github_pat_[A-Za-z0-9_]{10,}",  # GitHub PAT fine-grained
    r"xox[baprs]-[A-Za-z0-9-]{10,}",  # Slack 토큰
    r"AIza[A-Za-z0-9_-]{30,}",  # Google API 키
    r"pplx-[A-Za-z0-9]{10,}",  # Perplexity
    r"fal_[A-Za-z0-9_-]{10,}",  # Fal.ai
    r"fc-[A-Za-z0-9]{10,}",  # Firecrawl
    r"bb_live_[A-Za-z0-9_-]{10,}",  # BrowserBase
    r"gAAAA[A-Za-z0-9_=-]{20,}",  # Codex 암호화 토큰
    r"AKIA[A-Z0-9]{16}",  # AWS Access Key ID
    r"sk_live_[A-Za-z0-9]{10,}",  # Stripe 라이브 키
    r"sk_test_[A-Za-z0-9]{10,}",  # Stripe 테스트 키
    r"rk_live_[A-Za-z0-9]{10,}",  # Stripe 제한 키
    r"SG\.[A-Za-z0-9_-]{10,}",  # SendGrid API 키
    r"hf_[A-Za-z0-9]{10,}",  # HuggingFace 토큰
    r"r8_[A-Za-z0-9]{10,}",  # Replicate 토큰
    r"npm_[A-Za-z0-9]{10,}",  # npm 액세스 토큰
    r"pypi-[A-Za-z0-9_-]{10,}",  # PyPI API 토큰
    r"dop_v1_[A-Za-z0-9]{10,}",  # DigitalOcean PAT
    r"doo_v1_[A-Za-z0-9]{10,}",  # DigitalOcean OAuth
    r"am_[A-Za-z0-9_-]{10,}",  # AgentMail API 키
]

_PREFIX_RE = re.compile(r"(?<![A-Za-z0-9_-])(" + "|".join(_PREFIX_PATTERNS) + r")(?![A-Za-z0-9_-])")

# ENV 할당 패턴: SECRET_KEY=value, OPENAI_API_KEY='xxx' 등
_SECRET_ENV_NAMES = r"(?:API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)"
_ENV_ASSIGN_RE = re.compile(
    rf"([A-Z_]*{_SECRET_ENV_NAMES}[A-Z_]*)\s*=\s*(['\"]?)(\S+)\2",
    re.IGNORECASE,
)

# JSON 필드 패턴: "apiKey": "value", "token": "value"
_JSON_KEY_NAMES = (
    r"(?:api_?[Kk]ey|token|secret|password|access_token|refresh_token"
    r"|auth_token|bearer|secret_value|raw_secret|secret_input|key_material)"
)
_JSON_FIELD_RE = re.compile(
    rf'("{_JSON_KEY_NAMES}")\s*:\s*"([^"]+)"',
    re.IGNORECASE,
)

# Authorization 헤더: Authorization: Bearer <token>
_AUTH_HEADER_RE = re.compile(
    r"(Authorization:\s*Bearer\s+)(\S+)",
    re.IGNORECASE,
)

# Telegram 봇 토큰: bot<digits>:<token>
_TELEGRAM_RE = re.compile(
    r"(bot)?(\d{8,}):([-A-Za-z0-9_]{30,})",
)

# PEM Private Key 블록
_PRIVATE_KEY_RE = re.compile(r"-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z ]*PRIVATE KEY-----")

# DB 연결 문자열: protocol://user:PASSWORD@host
_DB_CONNSTR_RE = re.compile(
    r"((?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp)://[^:]+:)([^@]+)(@)",
    re.IGNORECASE,
)

# E.164 전화번호: +<국가코드><번호>
_PHONE_RE = re.compile(r"(\+[1-9]\d{6,14})(?![A-Za-z0-9])")


def _mask_token(token: str) -> str:
    """토큰 마스킹: 18자 미만은 ***, 이상은 앞6 + ... + 뒤4 보존."""
    if len(token) < 18:
        return "***"
    return f"{token[:6]}...{token[-4:]}"


def redact_sensitive_text(text: str) -> str:  # type: ignore[return]
    """텍스트에서 민감 정보를 감지하여 마스킹합니다.

    DISABLE_REDACT_SECRETS=1 환경변수로 비활성화 가능합니다.
    None 입력은 None 반환, 비문자열은 str()로 변환합니다.
    """
    if text is None:
        return None  # type: ignore[return-value]
    if not isinstance(text, str):
        text = str(text)
    if not text:
        return text
    if os.getenv("DISABLE_REDACT_SECRETS", "").lower() in ("1", "true", "yes", "on"):
        return text

    # 알려진 접두사 패턴 (sk-, ghp_, AKIA* 등)
    text = _PREFIX_RE.sub(lambda m: _mask_token(m.group(1)), text)

    # ENV 할당: OPENAI_API_KEY=value
    def _redact_env(m: re.Match[str]) -> str:
        name, quote, value = m.group(1), m.group(2), m.group(3)
        return f"{name}={quote}{_mask_token(value)}{quote}"

    text = _ENV_ASSIGN_RE.sub(_redact_env, text)

    # JSON 필드: "apiKey": "value"
    def _redact_json(m: re.Match[str]) -> str:
        key, value = m.group(1), m.group(2)
        return f'{key}: "{_mask_token(value)}"'

    text = _JSON_FIELD_RE.sub(_redact_json, text)

    # Authorization 헤더
    text = _AUTH_HEADER_RE.sub(
        lambda m: m.group(1) + _mask_token(m.group(2)),
        text,
    )

    # Telegram 봇 토큰
    def _redact_telegram(m: re.Match[str]) -> str:
        prefix = m.group(1) or ""
        digits = m.group(2)
        return f"{prefix}{digits}:***"

    text = _TELEGRAM_RE.sub(_redact_telegram, text)

    # PEM Private Key 블록
    text = _PRIVATE_KEY_RE.sub("[REDACTED PRIVATE KEY]", text)

    # DB 연결 문자열 비밀번호
    text = _DB_CONNSTR_RE.sub(lambda m: f"{m.group(1)}***{m.group(3)}", text)

    # E.164 전화번호
    def _redact_phone(m: re.Match[str]) -> str:
        phone = m.group(1)
        if len(phone) <= 8:
            return phone[:2] + "****" + phone[-2:]
        return phone[:4] + "****" + phone[-4:]

    text = _PHONE_RE.sub(_redact_phone, text)

    return text


class RedactingFormatter(logging.Formatter):
    """민감 정보를 자동으로 마스킹하는 로그 포맷터.

    기존 Formatter 위에 래핑하여 format() 출력 결과를 redact_sensitive_text()로 후처리합니다.
    기존 format 문자열, datefmt, style 설정을 모두 그대로 유지합니다.
    """

    def format(self, record: logging.LogRecord) -> str:
        original = super().format(record)
        return redact_sensitive_text(original)


def setup_redacted_logging(logger: logging.Logger | None = None) -> None:
    """지정된 로거(또는 루트 로거)의 모든 핸들러에 RedactingFormatter를 적용합니다.

    기존 Formatter 설정(fmt, datefmt, style)을 그대로 유지하면서
    RedactingFormatter로 교체합니다. 핸들러에 formatter가 없는 경우
    기본 RedactingFormatter를 새로 생성하여 적용합니다.

    utils/logger.py의 get_logger()로 생성한 로거와 호환됩니다.

    Args:
        logger: RedactingFormatter를 적용할 로거. None이면 루트 로거 사용.
    """
    target = logger if logger is not None else logging.getLogger()
    for handler in target.handlers:
        existing = handler.formatter
        if existing is not None and not isinstance(existing, RedactingFormatter):
            # logging.Formatter 내부: _style은 PercentStyle / StrFormatStyle / StringTemplateStyle
            # 타입 이름으로 style 문자 복원
            style_map: dict[str, Literal["%", "{", "$"]] = {
                "PercentStyle": "%",
                "StrFormatStyle": "{",
                "StringTemplateStyle": "$",
            }
            style_type = type(existing._style).__name__
            style_char = style_map.get(style_type, "%")
            redacting = RedactingFormatter(
                fmt=existing._fmt,
                datefmt=existing.datefmt,
                style=style_char,
            )
            handler.setFormatter(redacting)
        elif existing is None:
            handler.setFormatter(RedactingFormatter())
