"""
Idempotencia de las operaciones que gastan cuota.

El caso que resuelve es concreto: un pipeline de despliegue dispara la
sincronización, la red se corta antes de que llegue la respuesta, y el pipeline
reintenta. Sin esto, el segundo intento encola un lote nuevo y el sitio gasta el
doble de cuota por un problema de red que no tuvo nada que ver con el sitio.

Se guarda la **respuesta**, no sólo la marca de que ya pasó: quien reintenta
necesita el mismo cuerpo que habría recibido la primera vez, o no tiene forma de
saber qué lote quedó encolado.
"""

import hashlib
import json
from dataclasses import dataclass

from django.core.cache import cache

#: Ventana en la que un reintento se considera el mismo pedido. Un día cubre con
#: holgura los reintentos automáticos de un pipeline y no tanto como para que
#: alguien que quiere volver a sincronizar mañana se encuentre con la respuesta
#: de ayer.
TTL_SECONDS = 24 * 60 * 60

MAX_KEY_LENGTH = 128


class IdempotencyConflict(Exception):
    """
    La misma clave llegó con un cuerpo distinto.

    No es un reintento: es otro pedido reusando la clave. Devolver la respuesta
    guardada sería contestar una pregunta que nadie hizo, así que se rechaza.
    """


@dataclass(frozen=True)
class StoredResponse:
    status_code: int
    body: dict


def _cache_key(account_id, key: str) -> str:
    return f'idempotency:{account_id}:{key}'


def _fingerprint(payload) -> str:
    serialized = json.dumps(payload, sort_keys=True, default=str)
    return hashlib.sha256(serialized.encode()).hexdigest()


def lookup(account, key: str, payload) -> StoredResponse | None:
    """
    Busca la respuesta guardada para esta clave.

    Devuelve nada si es la primera vez. Rompe si la clave ya se usó con otro
    cuerpo, que es el único caso en el que responder lo guardado sería mentir.
    """
    if not key:
        return None

    guardado = cache.get(_cache_key(account.id, key))
    if guardado is None:
        return None

    if guardado['fingerprint'] != _fingerprint(payload):
        raise IdempotencyConflict(
            f'La clave de idempotencia «{key}» ya se usó con otros datos. '
            f'Usá una clave distinta para un pedido distinto.'
        )

    return StoredResponse(status_code=guardado['status_code'], body=guardado['body'])


def remember(account, key: str, payload, *, status_code: int, body: dict) -> None:
    """Guarda la respuesta para que el reintento reciba exactamente esto."""
    if not key:
        return

    cache.set(
        _cache_key(account.id, key),
        {
            'fingerprint': _fingerprint(payload),
            'status_code': status_code,
            'body': body,
        },
        timeout=TTL_SECONDS,
    )


def validate_key(key: str | None) -> str:
    """Normaliza la clave recibida por cabecera y rechaza las que no sirven."""
    if not key:
        return ''
    key = key.strip()
    if len(key) > MAX_KEY_LENGTH:
        raise IdempotencyConflict(
            f'La clave de idempotencia no puede superar los {MAX_KEY_LENGTH} caracteres.'
        )
    return key
