"""Хуки 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")


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 scrub_pii_event(event: Event, _hint: dict[str, Any]) -> Event | None:
    """Redact consumer-PII из error event перед отправкой. Возвращает 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"))
    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]
