"""
Verificacion de la firma del webhook de eventos de SendGrid.

El endpoint del webhook es publico: cualquiera en internet puede hacerle
POST. Sin verificar la firma, un tercero podria inyectar eventos falsos y
corromper la auditoria (marcar como entregados correos que rebotaron, o al
reves).

SendGrid firma cada peticion con ECDSA sobre la curva P-256 y SHA-256. Lo que
se firma es la concatenacion de la marca de tiempo y el cuerpo crudo:

    firma = ECDSA_P256_SHA256(timestamp || cuerpo_crudo)

Es imprescindible verificar sobre el cuerpo *tal cual llego*: si se
deserializa el JSON y se vuelve a serializar, el orden de las claves o los
espacios cambian y la firma deja de cuadrar.

La firma por si sola no impide la repeticion: quien capture una peticion
valida puede reenviarla. Por eso se rechaza tambien cualquier peticion cuya
marca de tiempo se aleje mas de `SENDGRID_WEBHOOK_TOLERANCE_SECONDS`.
"""

from __future__ import annotations

import base64
import binascii
import logging
import time

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from django.conf import settings

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

SIGNATURE_HEADER = "HTTP_X_TWILIO_EMAIL_EVENT_WEBHOOK_SIGNATURE"
TIMESTAMP_HEADER = "HTTP_X_TWILIO_EMAIL_EVENT_WEBHOOK_TIMESTAMP"


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


# La clave publica se convierte una sola vez por proceso: analizar el PEM en
# cada peticion es trabajo repetido sin motivo.
_public_key_cache: dict[str, ec.EllipticCurvePublicKey] = {}


def load_public_key(encoded_key: str) -> ec.EllipticCurvePublicKey:
    """
    Convierte la clave que muestra el panel de SendGrid en una clave usable.

    En el panel aparece como base64 sin las lineas de delimitacion PEM.
    """
    cached = _public_key_cache.get(encoded_key)
    if cached is not None:
        return cached

    pem = (
        "-----BEGIN PUBLIC KEY-----\n"
        f"{encoded_key.strip()}\n"
        "-----END PUBLIC KEY-----"
    )
    try:
        key = load_pem_public_key(pem.encode("utf-8"))
    except Exception as exc:  # noqa: BLE001 - clave mal copiada
        raise SignatureVerificationError(
            "SENDGRID_WEBHOOK_PUBLIC_KEY no es una clave publica valida."
        ) from exc

    if not isinstance(key, ec.EllipticCurvePublicKey):
        raise SignatureVerificationError(
            "La clave del webhook debe ser de curva eliptica (ECDSA)."
        )

    _public_key_cache[encoded_key] = key
    return key


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

    Lanza `SignatureVerificationError` con el motivo si no se puede
    autenticar. No devuelve nada: si retorna, la peticion es legitima.
    """
    encoded_key = settings.SENDGRID_WEBHOOK_PUBLIC_KEY
    if not encoded_key:
        # Sin clave configurada no se puede distinguir un evento real de uno
        # inventado. Aceptar la peticion "porque aun no esta configurado"
        # dejaria la auditoria abierta a cualquiera.
        raise SignatureVerificationError(
            "SENDGRID_WEBHOOK_PUBLIC_KEY no esta configurada; no se pueden "
            "aceptar eventos."
        )

    signature = request.META.get(SIGNATURE_HEADER, "")
    timestamp = request.META.get(TIMESTAMP_HEADER, "")

    if not signature or not timestamp:
        raise SignatureVerificationError(
            "Faltan las cabeceras de firma del webhook."
        )

    _check_timestamp(timestamp)

    try:
        decoded_signature = base64.b64decode(signature, validate=True)
    except (binascii.Error, ValueError) as exc:
        raise SignatureVerificationError("La firma no es base64 valido.") from exc

    key = load_public_key(encoded_key)
    signed_payload = timestamp.encode("utf-8") + request.body

    try:
        key.verify(decoded_signature, signed_payload, ec.ECDSA(hashes.SHA256()))
    except InvalidSignature as exc:
        raise SignatureVerificationError("La firma del webhook no es valida.") from exc


def _check_timestamp(raw_timestamp: str) -> None:
    """Rechaza peticiones demasiado antiguas o con fecha futura."""
    try:
        timestamp = int(raw_timestamp)
    except (TypeError, ValueError) as exc:
        raise SignatureVerificationError(
            "La marca de tiempo del webhook no es un numero."
        ) from exc

    tolerance = settings.SENDGRID_WEBHOOK_TOLERANCE_SECONDS
    drift = time.time() - timestamp

    if drift > tolerance:
        raise SignatureVerificationError(
            f"La peticion es demasiado antigua ({int(drift)}s); se rechaza por "
            "posible repeticion."
        )
    if drift < -tolerance:
        raise SignatureVerificationError(
            "La marca de tiempo de la peticion esta en el futuro."
        )
