"""
Cryptographic engine for SafeConfig.

Provides AES-GCM authenticated encryption via the ``cryptography`` library.
Each encrypted value is a self-contained blob that includes:

    <version:1B> <salt:16B> <nonce:12B> <tag:16B> <ciphertext:NB>

encoded as URL-safe base64 so it is safe to store in .env files and JSON.

Key derivation uses PBKDF2-HMAC-SHA256 with a random per-value salt so that
two encryptions of the same plaintext always produce different ciphertexts.
"""

from __future__ import annotations

import base64
import logging
import os
import struct
from typing import Final

from cryptography.exceptions import InvalidTag
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

from pysafeconfigx.core.exceptions import DecryptionError, EncryptionError, MasterKeyError

logger = logging.getLogger(__name__)

# ─── Constants ───────────────────────────────────────────────────────────────

_BLOB_VERSION: Final[int] = 1
_SALT_SIZE: Final[int] = 16        # bytes — random per encryption
_NONCE_SIZE: Final[int] = 12       # bytes — required by AES-GCM
_KEY_SIZE: Final[int] = 32         # bytes — AES-256
_PBKDF2_ITERATIONS: Final[int] = 480_000  # NIST 2023 recommendation for SHA-256
_HEADER_SIZE: Final[int] = 1 + _SALT_SIZE + _NONCE_SIZE  # 29 bytes


# ─── Key generation ──────────────────────────────────────────────────────────


def generate_master_key() -> str:
    """Generate a new cryptographically random master key.

    Returns:
        A URL-safe base64-encoded 32-byte random key (256 bits).

    Example::

        key = generate_master_key()
        print(key)  # e.g. 'abc123...'
    """
    raw = os.urandom(_KEY_SIZE)
    encoded = base64.urlsafe_b64encode(raw).decode()
    logger.debug("Generated new master key.")
    return encoded


def load_master_key(raw_key: str) -> bytes:
    """Decode and validate a base64-encoded master key.

    Args:
        raw_key: URL-safe base64-encoded key string (as returned by
            :func:`generate_master_key`).

    Returns:
        Raw 32-byte key material.

    Raises:
        MasterKeyError: If the key is not valid base64 or has wrong length.
    """
    try:
        key_bytes = base64.urlsafe_b64decode(raw_key.encode() + b"==")
    except Exception as exc:
        raise MasterKeyError("Master key is not valid base64.") from exc

    if len(key_bytes) != _KEY_SIZE:
        raise MasterKeyError(
            f"Master key must be {_KEY_SIZE} bytes ({_KEY_SIZE * 8} bits) "
            f"after base64 decoding; got {len(key_bytes)} bytes."
        )
    return key_bytes


# ─── Internal KDF ────────────────────────────────────────────────────────────


def _derive_key(master_key_bytes: bytes, salt: bytes) -> bytes:
    """Derive a per-value AES key using PBKDF2-HMAC-SHA256.

    Args:
        master_key_bytes: Raw 32-byte master key.
        salt: Random 16-byte salt.

    Returns:
        32-byte derived key suitable for AES-256-GCM.
    """
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=_KEY_SIZE,
        salt=salt,
        iterations=_PBKDF2_ITERATIONS,
    )
    return kdf.derive(master_key_bytes)


# ─── Public API ──────────────────────────────────────────────────────────────


def encrypt_value(plaintext: str, master_key: str, config_key: str = "unknown") -> str:
    """Encrypt *plaintext* and return a base64-encoded ciphertext blob.

    The returned string is safe to store in a .env file or JSON document.

    Args:
        plaintext: The secret value to encrypt.
        master_key: URL-safe base64-encoded master key (from
            :func:`generate_master_key`).
        config_key: Human-readable name of the configuration key (used only
            for error messages).

    Returns:
        A base64-encoded string containing version, salt, nonce, and
        authenticated ciphertext.

    Raises:
        EncryptionError: If encryption fails for any reason.

    Example::

        blob = encrypt_value("supersecret", master_key, "API_KEY")
        # Store `blob` in your .env or config file.
    """
    try:
        master_bytes = load_master_key(master_key)
        salt = os.urandom(_SALT_SIZE)
        nonce = os.urandom(_NONCE_SIZE)
        derived = _derive_key(master_bytes, salt)

        aesgcm = AESGCM(derived)
        ciphertext_with_tag = aesgcm.encrypt(nonce, plaintext.encode(), None)

        # Pack: version(1) | salt(16) | nonce(12) | ciphertext+tag
        header = struct.pack("B", _BLOB_VERSION) + salt + nonce
        blob = base64.urlsafe_b64encode(header + ciphertext_with_tag).decode()
        logger.debug("Encrypted value for key '%s'.", config_key)
        return blob

    except (MasterKeyError, ValueError) as exc:
        raise EncryptionError(config_key, str(exc)) from exc
    except Exception as exc:
        raise EncryptionError(config_key, f"Unexpected error: {exc}") from exc


def decrypt_value(blob: str, master_key: str, config_key: str = "unknown") -> str:
    """Decrypt a base64-encoded ciphertext blob produced by :func:`encrypt_value`.

    Args:
        blob: Base64-encoded ciphertext blob.
        master_key: URL-safe base64-encoded master key.
        config_key: Human-readable name of the configuration key (for error
            messages).

    Returns:
        The original plaintext string.

    Raises:
        DecryptionError: If the blob is malformed, the master key is wrong,
            or the ciphertext has been tampered with.

    Example::

        secret = decrypt_value(blob, master_key, "API_KEY")
    """
    try:
        master_bytes = load_master_key(master_key)
        raw = base64.urlsafe_b64decode(blob.encode() + b"==")
    except MasterKeyError:
        raise
    except Exception as exc:
        raise DecryptionError(config_key, "Blob is not valid base64.") from exc

    if len(raw) < _HEADER_SIZE + 1:
        raise DecryptionError(config_key, "Blob is too short to be valid.")

    version = struct.unpack("B", raw[:1])[0]
    if version != _BLOB_VERSION:
        raise DecryptionError(
            config_key, f"Unsupported blob version {version}; expected {_BLOB_VERSION}."
        )

    salt = raw[1 : 1 + _SALT_SIZE]
    nonce = raw[1 + _SALT_SIZE : _HEADER_SIZE]
    ciphertext_with_tag = raw[_HEADER_SIZE:]

    try:
        derived = _derive_key(master_bytes, salt)
        aesgcm = AESGCM(derived)
        plaintext_bytes = aesgcm.decrypt(nonce, ciphertext_with_tag, None)
        logger.debug("Decrypted value for key '%s'.", config_key)
        return plaintext_bytes.decode()
    except InvalidTag as exc:
        raise DecryptionError(
            config_key,
            "Authentication tag mismatch — master key may be wrong or ciphertext corrupted.",
        ) from exc
    except Exception as exc:
        raise DecryptionError(config_key, f"Decryption failed: {exc}") from exc


def is_encrypted_blob(value: str) -> bool:
    """Return *True* if *value* looks like a blob produced by :func:`encrypt_value`.

    This is a cheap heuristic check (version byte + minimum length), not a
    full decryption attempt.

    Args:
        value: String to inspect.

    Returns:
        ``True`` if the value appears to be an encrypted blob.
    """
    try:
        raw = base64.urlsafe_b64decode(value.encode() + b"==")
        if len(raw) < _HEADER_SIZE + 1:
            return False
        version = struct.unpack("B", raw[:1])[0]
        return version == _BLOB_VERSION
    except Exception:
        return False
