"""Cifrado simétrico de secretos que hay que guardar y volver a leer.

Se usa hoy para la contraseña de la llave privada del CSD: el timbrado necesita abrir el
``.key`` sin que haya nadie tecleando, así que la contraseña tiene que estar en reposo. Un
hash no sirve — habría que recuperar el valor original, no compararlo.

**La clave maestra vive en el entorno** (``CSD_ENCRYPTION_KEY``), nunca en la base ni en el
repositorio. Quien tenga a la vez la base de datos y esa variable puede descifrar los secretos:
esa es la propiedad que da el diseño y conviene tenerla presente al decidir quién ve qué.

Para generar una clave nueva::

    python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
"""

from cryptography.fernet import Fernet, InvalidToken

from core.config import settings


class SecretsNotConfigured(Exception):
    """No hay clave maestra configurada, así que no se puede cifrar ni descifrar."""


class SecretDecryptionError(Exception):
    """El valor guardado no se pudo descifrar con la clave maestra actual."""


def _fernet() -> Fernet:
    clave = (settings.CSD_ENCRYPTION_KEY or "").strip()
    if not clave:
        raise SecretsNotConfigured(
            "Falta CSD_ENCRYPTION_KEY. Genérala con "
            '`python -c "from cryptography.fernet import Fernet; '
            'print(Fernet.generate_key().decode())"` y ponla en el .env.'
        )
    try:
        return Fernet(clave.encode("utf-8"))
    except (ValueError, TypeError) as exc:
        raise SecretsNotConfigured(
            "CSD_ENCRYPTION_KEY no es una clave Fernet válida (32 bytes en base64 url-safe)."
        ) from exc


def encrypt_secret(value: str) -> str:
    """Cifra un secreto. Devuelve el token en texto, listo para guardar en una columna."""
    if not value:
        raise ValueError("no se cifra un secreto vacío")
    return _fernet().encrypt(value.encode("utf-8")).decode("ascii")


def decrypt_secret(token: str) -> str:
    """Recupera el secreto original.

    Falla con ``SecretDecryptionError`` si la clave maestra cambió o el dato está corrupto.
    Se distingue de ``SecretsNotConfigured`` a propósito: una dice "configura la variable" y la
    otra "la variable no es la que cifró este dato", y confundirlas manda a buscar al lugar
    equivocado.
    """
    if not token:
        raise SecretDecryptionError("no hay secreto guardado")
    try:
        return _fernet().decrypt(token.encode("ascii")).decode("utf-8")
    except InvalidToken as exc:
        raise SecretDecryptionError(
            "No pude descifrar el secreto guardado: la clave maestra no corresponde con la que "
            "se usó al guardarlo, o el dato está dañado. Hay que volver a capturarlo."
        ) from exc


def secrets_available() -> bool:
    """``True`` si hay clave maestra utilizable. Para avisar en la UI antes de pedir el dato."""
    try:
        _fernet()
        return True
    except SecretsNotConfigured:
        return False
