"""Encryption-at-rest for service secrets.

Service API keys / tokens are stored encrypted in the ``services.secrets_json``
column. Encryption uses Fernet (symmetric authenticated encryption) with a single
master key provided via the ``MANAGE_ENCRYPTION_KEY`` environment variable.

* The key **must** be a urlsafe base64-encoded 32-byte value (Fernet format).
* The key is **always required** — there is no development fallback, so secrets
  are never accidentally stored in plaintext.
* Secrets are encrypted field-by-field; the ``"which secrets are set"`` metadata
  can be derived from the ciphertext blob without decrypting.
"""

from __future__ import annotations

import os
from functools import lru_cache

from cryptography.fernet import Fernet, InvalidToken

ENCRYPTION_KEY_ENV = "MANAGE_ENCRYPTION_KEY"


class EncryptionKeyError(RuntimeError):
    """Raised when the encryption key is missing or invalid."""


@lru_cache(maxsize=1)
def get_encryption_key() -> bytes:
    """Return the raw Fernet key, or raise if missing/invalid.

    The result is cached for the process lifetime. Tests should call
    :func:`reset_encryption_key_cache` after changing the environment.
    """
    raw = os.environ.get(ENCRYPTION_KEY_ENV)
    if not raw:
        raise EncryptionKeyError(f"{ENCRYPTION_KEY_ENV} is required to store service secrets")
    key = raw.strip().encode()
    try:
        Fernet(key)
    except (ValueError, TypeError) as exc:  # pragma: no cover - validated by tests
        raise EncryptionKeyError(f"{ENCRYPTION_KEY_ENV} must be a valid Fernet key") from exc
    return key


def reset_encryption_key_cache() -> None:
    """Drop the cached encryption key (used by tests that swap keys)."""
    get_encryption_key.cache_clear()


def _fernet() -> Fernet:
    return Fernet(get_encryption_key())


def encrypt_value(plaintext: str) -> str:
    """Encrypt a single secret value and return the ciphertext string."""
    return _fernet().encrypt(plaintext.encode()).decode()


def decrypt_value(ciphertext: str) -> str:
    """Decrypt a single ciphertext value."""
    try:
        return _fernet().decrypt(ciphertext.encode()).decode()
    except InvalidToken as exc:
        raise EncryptionKeyError("Service secret could not be decrypted") from exc


def encrypt_secrets(values: dict[str, str]) -> dict[str, str]:
    """Encrypt every provided secret value."""
    fernet = _fernet()
    return {key: fernet.encrypt(value.encode()).decode() for key, value in values.items()}


def decrypt_secrets(blob: dict[str, str]) -> dict[str, str]:
    """Decrypt every secret value in a blob."""
    fernet = _fernet()
    result: dict[str, str] = {}
    for key, ciphertext in blob.items():
        try:
            result[key] = fernet.decrypt(ciphertext.encode()).decode()
        except InvalidToken as exc:
            raise EncryptionKeyError(f"Service secret '{key}' could not be decrypted") from exc
    return result


def generate_development_key() -> str:
    """Return a freshly generated Fernet key (helper for operators/docs)."""
    return Fernet.generate_key().decode()


def validate_encryption_key() -> None:
    """Eagerly validate that the encryption key is present and well-formed."""
    get_encryption_key()  # raises EncryptionKeyError on failure
