"""Хуки before_send для GlitchTip/Sentry SDK (tradein-local, #396, #tgsupport).

Redact-ит consumer-PII (client_name / client_phone / client_email и пр.)
из error events до отправки в GlitchTip — estimator/trade-in flow таскает
эти поля, а send_default_pii=False их не покрывает (это user-data в
request.data / extra / contexts, не PII-заголовки).

`redact_telegram_bot_token` — отдельный хук (#tgsupport review): Telegram Bot
API токен живёт в URL-пути (`https://api.telegram.org/bot<id>:<secret>/...`),
а не в query/userinfo, поэтому НЕ покрывается sentry_sdk `sanitize_url` (тот
режет только `user:pass@` и query-параметры). Токен утекает ДВУМЯ путями,
которые `_scrub`/`scrub_pii_event` (ключ-based, PII-словарь) не ловят:
  1. `include_local_variables=True` (sentry_sdk default) кладёт locals
     stack-фрейма (`self._base`, `url` в `TelegramClient._request`) в
     traceback → полный токен открытым текстом.
  2. `HttpxIntegration` кладёт полный request URL в span `data` (виден при
     любом ненулевом `traces_sample_rate`), а не только в traceback.
Поэтому редактор — НЕ ключ-based, а regex full-text по КАЖДОЙ строке во всём
event (глубокий обход dict/list/tuple) — токен может всплыть в любом поле.
"""

from __future__ import annotations

import re
from typing import Any

from sentry_sdk.types import Event

_REDACTED = "[REDACTED]"
# Ключи consumer-PII (нижний регистр; сверка case-insensitive).
_PII_KEYS = frozenset({"client_name", "client_phone", "client_email", "phone", "email", "name"})

# Telegram Bot API токен в пути URL: /bot<numeric_id>:<secret-part>/<method>.
# Матчим ровно этот сегмент (не весь URL) — сохраняет остальной путь/query
# читаемым для диагностики (метод API, error code и т.п.).
_TG_BOT_TOKEN_RE = re.compile(r"/bot\d+:[A-Za-z0-9_-]+")
_TG_BOT_TOKEN_REPLACEMENT = "/bot[REDACTED]"

# Тот же токен БЕЗ префикса `/bot` — форма `<numeric_id>:<secret>` сама по себе
# (напр. локаль `token` в конструкторе TelegramClient, или если его кто-то
# засунет в log-сообщение). Сейчас единственный путь такой формы в event —
# locals стек-фрейма, а они выключены через include_local_variables=False в
# tgbot_main. Но именно на отказ того флага этот редактор и страхует: без этой
# ветки рубеж был бы один, а не два. Формат токена BotFather: 8-12 цифр `:` 35
# символов base64url — нижние границы взяты с запасом, чтобы не промахнуться
# на нестандартных id, но остаться уже, чем `\d+:\S+` (тот бил бы по любым
# `id:value` в логах, напр. `chat_id:12345`).
_TG_BOT_TOKEN_BARE_RE = re.compile(r"\b\d{6,12}:[A-Za-z0-9_-]{30,}\b")

# Query-string секреты в исходящих URL сторонних API (аудит-фикс, #security-audit):
# mobileproxy changeip-ссылка (`AVITO_PROXY_ROTATE_URL` и др., admin.py
# rotate_proxy_ip) несёт провайдерский API-ключ в query (`?...&proxy_key=...`).
# Два независимых пути утечки в GlitchTip, зеркалящих TG-токен выше:
#   1. `HttpxIntegration.send()` парсит URL через `parse_url(str(request.url),
#      sanitize=False)` (ЯВНЫЙ opt-out из sentry_sdk `sanitize_url`, который иначе
#      сам вырезал бы query-параметры) и кладёт полный URL в span `data["url"]` —
#      сейчас неактивно (`traces_sample_rate=0.0` в app/main.py/scheduler_main.py →
#      span не сэмплится/не уходит), но молча перестанет спасать, если трейсинг
#      когда-нибудь включат.
#   2. `include_local_variables=True` (sentry_sdk default в app/main.py — в отличие
#      от tgbot_main.py, где явно False) кладёт stack-frame locals (`rotate_url`,
#      `exc` в rotate_proxy_ip) в traceback открытым текстом.
# Как и TG-токен — full-text regex по КАЖДОЙ строке event (не ключ-based): секрет
# может всплыть где угодно (frame locals, breadcrumb, exception message). НЕ
# завязано на конкретного провайдера — покрывает любой query-параметр из
# общеупотребимого набора секретных имён (api_key/proxy_key/token/secret/password/
# access_token/auth), т.к. cian/yandex у нас имеют СВОИ rotate-URL (потенциально
# другой провайдер, другое имя параметра).
_URL_SECRET_QUERY_RE = re.compile(
    r"(?i)([?&](?:api[_-]?key|proxy[_-]?key|token|secret|password|pwd|"
    r"access[_-]?token|auth)=)[^&\s\"'<>]+"
)
_URL_SECRET_QUERY_REPLACEMENT = r"\g<1>" + _REDACTED


def _scrub(obj: Any) -> None:
    """Рекурсивно заменить значения PII-ключей в dict на [REDACTED] (in-place)."""
    if isinstance(obj, dict):
        for key, value in obj.items():
            if isinstance(key, str) and key.lower() in _PII_KEYS:
                obj[key] = _REDACTED
            else:
                _scrub(value)
    elif isinstance(obj, list):
        for item in obj:
            _scrub(item)


def _redact_url_secrets_inplace(obj: Any) -> None:
    """Рекурсивно (IN-PLACE, как `_scrub`) заменяет значения секрет-подобных
    query-параметров (`?token=...`, `?proxy_key=...` и т.п.) на [REDACTED] в
    КАЖДОЙ строке event — не ключ-based: секрет утекает через httpx span
    `url`/`query` data и через текст исключений (`str(exc)` httpx содержит полный
    request URL), а не только через известные PII-поля формы. Мутирует dict/list
    на месте (НЕ пересоздаёт структуру, в отличие от `_redact_strings`) —
    сохраняет identity верхнеуровневого `event`, на что опирается контракт
    `scrub_pii_event`/`before_send` и существующие тесты (`out is event`).
    """
    if isinstance(obj, dict):
        for key, value in obj.items():
            if isinstance(value, str):
                redacted = _URL_SECRET_QUERY_RE.sub(_URL_SECRET_QUERY_REPLACEMENT, value)
                if redacted != value:
                    obj[key] = redacted
            else:
                _redact_url_secrets_inplace(value)
    elif isinstance(obj, list):
        for i, value in enumerate(obj):
            if isinstance(value, str):
                redacted = _URL_SECRET_QUERY_RE.sub(_URL_SECRET_QUERY_REPLACEMENT, value)
                if redacted != value:
                    obj[i] = redacted
            else:
                _redact_url_secrets_inplace(value)
    # tuple намеренно не обрабатываем: sentry_sdk event — это JSON-совместимая
    # структура (dict/list/str/int/...), tuple там не встречается, а даже если бы
    # встретился — он immutable, in-place правка невозможна (см. `_scrub`, тот же
    # выбор для dict/list).


def scrub_pii_event(event: Event, _hint: dict[str, Any]) -> Event | None:
    """Redact consumer-PII + URL query-string секретов из error event перед отправкой.

    Композиция (обе — in-place, сохраняют identity `event`): (1) ключ-based
    dict-scrub consumer-PII полей формы (как раньше), (2) full-text regex-проход
    по ВСЕМУ event, вырезающий значения секрет-подобных query-параметров в любой
    строке (proxy/API-ключи в исходящих URL сторонних сервисов, напр. mobileproxy
    changeip — #security-audit). Второй шаг не завязан на конкретные ключи полей —
    ловит секрет в frame locals, breadcrumb, exception message и т.д., где он может
    оказаться независимо от include_local_variables/traces_sample_rate. Возвращает
    event (не None).
    """
    if not isinstance(event, dict):
        return event
    request = event.get("request")
    if isinstance(request, dict):
        _scrub(request.get("data"))
    _scrub(event.get("extra"))
    _scrub(event.get("contexts"))
    _redact_url_secrets_inplace(event)
    return event


def _redact_strings(obj: Any) -> Any:
    """Рекурсивно проходит dict/list/tuple и прогоняет обе токен-регулярки по КАЖДОЙ
    строке (не только по конкретным ключам) — токен может оказаться в locals
    stack-фрейма, span data, breadcrumb message, request.url и т.д. Возвращает
    НОВУЮ структуру (не мутирует `obj` — в отличие от `_scrub`, чтобы не зависеть
    от того, какие контейнеры sentry_sdk считает mutable в своём event dict)."""
    if isinstance(obj, str):
        redacted = _TG_BOT_TOKEN_RE.sub(_TG_BOT_TOKEN_REPLACEMENT, obj)
        return _TG_BOT_TOKEN_BARE_RE.sub(_REDACTED, redacted)
    if isinstance(obj, dict):
        return {k: _redact_strings(v) for k, v in obj.items()}
    if isinstance(obj, list):
        return [_redact_strings(v) for v in obj]
    if isinstance(obj, tuple):
        return tuple(_redact_strings(v) for v in obj)
    return obj


def redact_telegram_bot_token(event: Event, _hint: dict[str, Any]) -> Event | None:
    """Full-text regex redaction Telegram Bot API токена по ВСЕМУ event (#tgsupport).

    Ловит оба вектора утечки токена в GlitchTip, которые ключ-based `scrub_pii_event`
    не покрывает: locals stack-фреймов (`include_local_variables=True`) и httpx-span
    `data` (полный request URL). Композировать с `scrub_pii_event`, не вместо него —
    разные классы секретов (PII полей формы vs bot-токен в URL).
    """
    if not isinstance(event, dict):
        return event
    return _redact_strings(event)  # type: ignore[return-value]
