"""Encryption for third-party credentials held on a user's behalf.

A Spotify refresh token is a long-lived key to somebody else's account. It is
not this app's secret to hold casually: a database dump, a stray backup, or a
read-only SQL path must not hand out working Spotify credentials. So every
third-party token is encrypted before it reaches a column and decrypted only at
the moment of use.

Fernet (AES-128-CBC + HMAC-SHA256, from `cryptography`) rather than anything
hand-rolled: it is authenticated, so a tampered ciphertext fails loudly instead
of decrypting to garbage.

**This fails CLOSED.** If `TOKEN_ENCRYPTION_KEY` is missing or still a
placeholder, `encrypt()` raises rather than storing plaintext. Storing an
unencrypted refresh token because config was incomplete is exactly the silent
downgrade this module exists to prevent.

Key rotation: there is no re-encryption path yet. Changing the key makes every
stored token undecryptable, which surfaces as users needing to reconnect
Spotify. That is a safe failure, not a data-loss one — see the plan, §6.
"""

from __future__ import annotations

from functools import lru_cache

from cryptography.fernet import Fernet, InvalidToken

from ..config import get_settings, is_configured


class TokenCryptoError(RuntimeError):
    """Encryption is unavailable or a stored value could not be decrypted."""


@lru_cache
def _fernet() -> Fernet:
    key = get_settings().token_encryption_key
    if not is_configured(key):
        raise TokenCryptoError(
            "TOKEN_ENCRYPTION_KEY is not configured — refusing to handle "
            "third-party tokens without encryption"
        )
    try:
        return Fernet(key.encode())
    except (ValueError, TypeError) as exc:
        raise TokenCryptoError(
            "TOKEN_ENCRYPTION_KEY is not a valid Fernet key (expected "
            "url-safe base64 of 32 bytes)"
        ) from exc


def encryption_available() -> bool:
    """Shape-only check for the status endpoint. Never touches a token."""
    try:
        _fernet()
        return True
    except TokenCryptoError:
        return False


def encrypt(plaintext: str) -> str:
    if not plaintext:
        return ""
    return _fernet().encrypt(plaintext.encode()).decode()


def decrypt(ciphertext: str) -> str:
    """Decrypt, or raise. A failure here means the key changed or the row was
    tampered with — both are conditions to surface, never to paper over by
    returning an empty string that a caller would send to Spotify as a token."""
    if not ciphertext:
        return ""
    try:
        return _fernet().decrypt(ciphertext.encode()).decode()
    except InvalidToken as exc:
        raise TokenCryptoError(
            "stored token could not be decrypted — the encryption key has "
            "changed, or the value was altered"
        ) from exc
