"""
Cifrado reversible del material de las credenciales.

Es **cifrado**, no hash, y la diferencia no es de grado: la clave de la cuenta
de servicio tiene que poder usarse para firmar cada llamada a Google, así que
hay que poder recuperar el valor original. Lo irreversible se reserva para lo
que sólo se compara, que son las claves de nuestra propia API.

La clave de cifrado vive en el entorno, nunca en la base: si estuvieran juntas,
quien se lleva un volcado de la base se lleva también con qué descifrarlo.
"""

from cryptography.fernet import Fernet, InvalidToken, MultiFernet
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured


class DecryptionError(Exception):
    """
    El material no pudo descifrarse con ninguna clave conocida.

    Se distingue de un fallo genérico a propósito: casi siempre significa que
    la clave del entorno cambió sin haber rotado los valores guardados, y ese
    diagnóstico es lo que hace falta para resolverlo.
    """


def _fernet() -> MultiFernet:
    """
    Arma el cifrador con la clave vigente primero y las anteriores después.

    El orden importa: MultiFernet cifra siempre con la primera y prueba a
    descifrar con todas, que es exactamente lo que permite rotar sin detener
    el servicio ni migrar todo de golpe.
    """
    fallbacks = getattr(settings, 'SETTINGS_ENCRYPTION_KEY_FALLBACKS', [])
    keys = [settings.SETTINGS_ENCRYPTION_KEY, *fallbacks]
    try:
        return MultiFernet([Fernet(key.encode() if isinstance(key, str) else key) for key in keys])
    except (ValueError, TypeError) as exc:
        raise ImproperlyConfigured(
            'SETTINGS_ENCRYPTION_KEY no tiene el formato esperado. Generá una con '
            'Fernet.generate_key() del paquete cryptography; el valor va en base64 url-safe.'
        ) from exc


def encrypt(plaintext: str | bytes) -> bytes:
    """Cifra con la clave vigente y devuelve el material listo para guardar."""
    if isinstance(plaintext, str):
        plaintext = plaintext.encode()
    return _fernet().encrypt(plaintext)


def decrypt(token: bytes | memoryview) -> bytes:
    """Descifra con cualquiera de las claves conocidas."""
    if isinstance(token, memoryview):
        token = bytes(token)
    try:
        return _fernet().decrypt(token)
    except InvalidToken as exc:
        raise DecryptionError(
            'El material guardado no pudo descifrarse con ninguna clave conocida. '
            'Si SETTINGS_ENCRYPTION_KEY cambió, la anterior tiene que declararse en '
            'SETTINGS_ENCRYPTION_KEY_FALLBACKS para poder rotar los valores.'
        ) from exc


def rotate(token: bytes | memoryview) -> bytes:
    """
    Vuelve a cifrar con la clave vigente un material cifrado con una anterior.

    No descifra a texto plano en el camino visible del programa: MultiFernet lo
    hace en memoria y devuelve el nuevo token, de modo que el valor original no
    pasa por ninguna variable que pueda terminar en un registro.
    """
    if isinstance(token, memoryview):
        token = bytes(token)
    try:
        return _fernet().rotate(token)
    except InvalidToken as exc:
        raise DecryptionError(
            'No se pudo rotar: el material no corresponde a ninguna clave conocida.'
        ) from exc
