"""
Filtro que saca credenciales de los registros.

Es la última línea de defensa, no la primera: el código no debería registrar una
clave nunca. Pero un registro es el lugar más fácil donde algo secreto se filtra
sin que nadie lo note —una excepción que arrastra el cuerpo de la petición, un
`repr` de más durante una depuración— y a diferencia de una respuesta HTTP, un
registro queda escrito y se copia a donde sea que se agreguen los registros.

Actúa por forma y no por nombre de campo: busca lo que *parece* material de
clave. Filtrar por nombre sólo protege de los casos que ya se anticiparon.
"""

import logging
import re

REDACTED = '«dato omitido»'

#: Patrones de lo que nunca puede quedar escrito. El bloque PEM se ataja
#: completo, incluidas sus líneas intermedias, porque en un JSON de cuenta de
#: servicio llega en una sola línea con saltos escapados.
_PATTERNS: tuple[re.Pattern, ...] = (
    # Bloque de clave privada en PEM, en una línea o en varias.
    re.compile(r'-----BEGIN[^-]*PRIVATE KEY-----.*?-----END[^-]*PRIVATE KEY-----', re.DOTALL),
    # Campos de un JSON de cuenta de servicio que sí son secretos.
    re.compile(r'"private_key"\s*:\s*"(?:[^"\\]|\\.)*"'),
    re.compile(r'"private_key_id"\s*:\s*"(?:[^"\\]|\\.)*"'),
    # Claves de nuestra propia API, en cualquier contexto.
    re.compile(r'\birk_[0-9a-f]{8}_[A-Za-z0-9_-]{10,}'),
    # Cabecera de autorización, sea del tipo que sea. Se come el resto de la
    # línea y no una sola palabra: el valor viene en dos partes —el esquema y la
    # clave— y cortar en el primer espacio deja justo la mitad que importa.
    re.compile(r'(?i)\bauthorization\b\s*[:=]\s*[^\r\n]*'),
    # Material cifrado con Fernet, por si un token guardado sale en un rastreo.
    re.compile(r'\bgAAAAA[A-Za-z0-9_-]{20,}'),
)


def scrub(text: str) -> str:
    """Reemplaza todo lo que parezca material de credencial."""
    for pattern in _PATTERNS:
        text = pattern.sub(REDACTED, text)
    return text


class CredentialScrubFilter(logging.Filter):
    """
    Limpia el mensaje y sus argumentos antes de que el registro se escriba.

    Devuelve siempre verdadero: el filtro no descarta registros, los desinfecta.
    Descartar escondería que algo pasó, que es peor que registrarlo sin el
    secreto.
    """

    def filter(self, record: logging.LogRecord) -> bool:
        if isinstance(record.msg, str):
            record.msg = scrub(record.msg)

        if record.args:
            if isinstance(record.args, dict):
                record.args = {k: _scrub_value(v) for k, v in record.args.items()}
            else:
                record.args = tuple(_scrub_value(a) for a in record.args)

        return True


def _scrub_value(value):
    return scrub(value) if isinstance(value, str) else value
