"""
Verificacion de la firma de los callbacks de Twilio.

El endpoint de estados es publico: lo llama Twilio, no un cliente nuestro.
Sin verificar la firma, cualquiera podria marcar como entregados mensajes que
en realidad rebotaron, o al reves, y corromper la auditoria.

Twilio firma con HMAC-SHA1 sobre la URL completa a la que llamo, concatenada
con los parametros del formulario ordenados por nombre. Que la URL forme parte
de lo firmado tiene una consecuencia practica: hay que reconstruirla
exactamente como la vio Twilio. Detras de un proxy que termina el TLS, Django
puede creer que la peticion llego por http y la firma no cuadraria, por eso se
permite fijar la base publica en la configuracion.
"""

from __future__ import annotations

import logging

from django.conf import settings
from twilio.request_validator import RequestValidator

logger = logging.getLogger("notifications.webhooks.twilio")

SIGNATURE_HEADER = "HTTP_X_TWILIO_SIGNATURE"


class SignatureVerificationError(Exception):
    """La peticion del callback no se pudo autenticar."""


def build_public_url(request) -> str:
    """
    Reconstruye la URL publica exacta a la que llamo Twilio.

    Si `TWILIO_STATUS_CALLBACK_BASE_URL` esta configurada se usa como base, que
    es lo fiable detras de un proxy. Si no, se deduce de la peticion.
    """
    base = (settings.TWILIO_STATUS_CALLBACK_BASE_URL or "").rstrip("/")
    if base:
        return f"{base}{request.get_full_path()}"
    return request.build_absolute_uri()


def verify_request(request) -> None:
    """
    Comprueba que la peticion viene de Twilio.

    Lanza `SignatureVerificationError` con el motivo si no se puede
    autenticar. Si retorna, la peticion es legitima.
    """
    token = settings.TWILIO_AUTH_TOKEN
    if not token:
        # Sin el token no hay forma de distinguir un aviso real de uno
        # inventado. Aceptarlo "porque aun no esta configurado" dejaria la
        # auditoria abierta a cualquiera.
        raise SignatureVerificationError(
            "TWILIO_AUTH_TOKEN no esta configurado; no se pueden aceptar avisos."
        )

    firma = request.META.get(SIGNATURE_HEADER, "")
    if not firma:
        raise SignatureVerificationError("Falta la cabecera X-Twilio-Signature.")

    # Twilio firma los parametros del formulario, no el cuerpo crudo.
    parametros = request.POST.dict() if hasattr(request, "POST") else {}

    # Defensa en profundidad: un callback de estado de Twilio nunca lleva
    # ficheros. Una parte multipart con `filename` acaba en `request.FILES`, y
    # eso queda fuera de `request.POST` y, por tanto, fuera de lo que se firma.
    # Rechazarla de plano evita que la firma cubra un subconjunto de lo que
    # llega, aunque quien procese despues mire la fuente equivocada.
    if getattr(request, "FILES", None):
        raise SignatureVerificationError(
            "El callback no puede llevar ficheros adjuntos."
        )

    validador = RequestValidator(token)
    if not validador.validate(build_public_url(request), parametros, firma):
        raise SignatureVerificationError("La firma del callback no es valida.")
