import os
from cryptography.fernet import Fernet, InvalidToken

_KEY_NAME = "BESTWEB_SECRET_KEY"
_LEGACY_KEY_NAMES = ("DEVICE_CREDENTIAL_KEY",)


class SecretDecryptionError(RuntimeError):
    """Raised when a stored secret cannot be decrypted with the active key."""


def _fernet() -> Fernet:
    key = os.getenv(_KEY_NAME)
    if not key:
        raise RuntimeError(f"{_KEY_NAME} belum diset")
    return Fernet(key.encode())


def _fallback_fernets() -> list:
    """Keys historically used to encrypt device credentials.

    Credentials were written with DEVICE_CREDENTIAL_KEY while the code only ever
    read BESTWEB_SECRET_KEY, so they failed to decrypt and were silently sent to
    devices as ciphertext. These are still tried on read so existing rows recover.
    """
    fernets = []
    for name in _LEGACY_KEY_NAMES:
        key = os.getenv(name)
        if not key:
            continue
        try:
            fernets.append(Fernet(key.encode()))
        except (ValueError, TypeError):
            continue
    return fernets


def encrypt_secret(value: str) -> str:
    return _fernet().encrypt(value.encode()).decode()


def _decrypt_any(value: str) -> str:
    """Decrypt with the active key, then any historical key."""
    errors = []
    for fernet in [_fernet(), *_fallback_fernets()]:
        try:
            return fernet.decrypt(value.encode()).decode()
        except (InvalidToken, ValueError) as exc:
            errors.append(exc)
    raise InvalidToken("no configured key could decrypt this value")


def decrypt_secret(value: str) -> str:
    try:
        return _decrypt_any(value)
    except (InvalidToken, ValueError):
        # Legacy plaintext values can be migrated once on save.
        return value


def decrypt_secret_strict(value: str, label: str = "secret") -> str:
    """Decrypt a secret and fail loudly instead of silently returning ciphertext.

    Device credentials must use this: sending an undecryptable ciphertext to a
    network device produces a misleading "invalid user name or password" and
    hides the real problem.
    """
    try:
        return _decrypt_any(value)
    except (InvalidToken, ValueError) as exc:
        raise SecretDecryptionError(
            f"{label} tersimpan dengan kunci enkripsi yang berbeda. "
            "Password tidak dapat dipulihkan dan harus diset ulang."
        ) from exc
