"""
Utilidades de registro de eventos.

Contiene tres piezas que se enganchan desde `LOGGING` en settings:

* `request_id_var`  - contexto con el identificador de la peticion en curso.
* `RequestIDFilter` - inyecta ese identificador en cada registro.
* `SensitiveDataFilter` - censura credenciales antes de que se escriban.
* `JSONFormatter`   - serializa cada registro como una linea JSON.
"""

from __future__ import annotations

import datetime as dt
import json
import logging
import re
from contextvars import ContextVar
from typing import Any

# Identificador de la peticion HTTP (o de la tarea Celery) en curso. Lo fija
# `api.middleware.RequestIDMiddleware` y permite correlacionar todos los
# registros generados durante una misma peticion.
request_id_var: ContextVar[str] = ContextVar("request_id", default="-")


# Atributos que `logging` pone en todos los registros y que no son datos
# propios del evento.
_RESERVED_ATTRS = frozenset(
    {
        "args", "asctime", "created", "exc_info", "exc_text", "filename",
        "funcName", "levelname", "levelno", "lineno", "module", "msecs",
        "message", "msg", "name", "pathname", "process", "processName",
        "relativeCreated", "stack_info", "taskName", "thread", "threadName",
    }
)


# ---------------------------------------------------------------------------
# Censura de datos sensibles
# ---------------------------------------------------------------------------

# Cada patron conserva la parte que identifica el dato censurado y sustituye
# el secreto en si.
_REDACTION_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
    # Claves de API de SendGrid: SG.<id>.<secreto>
    (re.compile(r"SG\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}"), "SG.***"),
    # JSON Web Tokens
    (
        re.compile(r"\beyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+"),
        "<jwt-censurado>",
    ),
    # Claves de API propias: mk_<prefijo>_<secreto>
    (re.compile(r"\bmk_[0-9a-f]{8,}_[A-Za-z0-9_\-]{8,}"), "mk_***"),
    # Cabecera Authorization
    (
        re.compile(r"(?i)\b(bearer|token|basic)\s+[A-Za-z0-9\-._~+/=]{8,}"),
        r"\1 <censurado>",
    ),
    # Pares clave/valor en JSON, querystrings o repr de diccionarios
    (
        re.compile(
            r"(?i)(['\"]?(?:password|passwd|secret|client_secret|token|"
            r"access_token|refresh_token|api[_-]?key|authorization|"
            r"private_key|signature)['\"]?\s*[:=]\s*['\"]?)"
            r"([^'\"\s,;}&]{4,})"
        ),
        r"\1<censurado>",
    ),
)


def redact(text: str) -> str:
    """Devuelve `text` con las credenciales reconocibles sustituidas."""
    for pattern, replacement in _REDACTION_PATTERNS:
        text = pattern.sub(replacement, text)
    return text


class SensitiveDataFilter(logging.Filter):
    """
    Censura credenciales en el mensaje ya interpolado del registro.

    Se resuelve el mensaje aqui (`record.getMessage()`) y se vacian los
    argumentos para que el formateador no vuelva a interpolar el original sin
    censurar.
    """

    def filter(self, record: logging.LogRecord) -> bool:
        try:
            message = record.getMessage()
        except Exception:  # noqa: BLE001 - un log jamas debe tumbar la peticion
            return True

        redacted = redact(message)
        if redacted != message or record.args:
            record.msg = redacted
            record.args = ()

        # Los campos extra tambien pueden arrastrar secretos.
        for key, value in list(record.__dict__.items()):
            if isinstance(value, str) and key not in _RESERVED_ATTRS:
                record.__dict__[key] = redact(value)

        return True


class RequestIDFilter(logging.Filter):
    """Anade `request_id` a cada registro para poder correlacionarlos."""

    def filter(self, record: logging.LogRecord) -> bool:
        if not hasattr(record, "request_id"):
            record.request_id = request_id_var.get()
        return True


# ---------------------------------------------------------------------------
# Formateador JSON
# ---------------------------------------------------------------------------


class JSONFormatter(logging.Formatter):
    """Serializa cada registro como una unica linea JSON."""

    def format(self, record: logging.LogRecord) -> str:
        payload: dict[str, Any] = {
            "timestamp": dt.datetime.fromtimestamp(
                record.created, tz=dt.UTC
            ).isoformat(),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
            "request_id": getattr(record, "request_id", request_id_var.get()),
        }

        if record.exc_info:
            payload["exception"] = redact(self.formatException(record.exc_info))
        if record.stack_info:
            payload["stack"] = redact(self.formatStack(record.stack_info))

        # Cualquier campo pasado con `logger.info(..., extra={...})`.
        for key, value in record.__dict__.items():
            if key in _RESERVED_ATTRS or key in payload or key.startswith("_"):
                continue
            payload[key] = _coerce(value)

        return json.dumps(payload, ensure_ascii=False, default=str)


def _coerce(value: Any) -> Any:
    """Convierte valores no serializables a algo que JSON acepte."""
    if isinstance(value, (str, int, float, bool, type(None))):
        return value
    if isinstance(value, (list, tuple, set)):
        return [_coerce(v) for v in value]
    if isinstance(value, dict):
        return {str(k): _coerce(v) for k, v in value.items()}
    return str(value)
