"""Encryption backend for the mesh credential vault.

Uses AES-256-GCM via the ``cryptography`` library with PBKDF2-SHA256 key
derivation (100 000 iterations).  Falls back to Fernet if AES-GCM primitives
are unavailable.
"""
from __future__ import annotations

import base64
import hashlib
import hmac
import logging
import os
from typing import Dict, List

logger = logging.getLogger(__name__)

_KDF_ITERATIONS = 100_000
_SALT_LEN = 32
_KEY_LEN = 32  # 256 bits

# Try importing AES-GCM; fall back to Fernet
_USE_AESGCM = False
try:
    from cryptography.hazmat.primitives.ciphers.aead import AESGCM
    _USE_AESGCM = True
except ImportError:
    try:
        from cryptography.fernet import Fernet
    except ImportError:
        raise ImportError(
            "The 'cryptography' package is required.  "
            "Install with: pip install cryptography"
        )


def _derive_key(passphrase: str, salt: bytes) -> bytes:
    """Derive a 256-bit key from *passphrase* using PBKDF2-HMAC-SHA256."""
    return hashlib.pbkdf2_hmac(
        "sha256",
        passphrase.encode("utf-8"),
        salt,
        iterations=_KDF_ITERATIONS,
        dklen=_KEY_LEN,
    )


class VaultCrypto:
    """Symmetric encryption engine backed by a passphrase-derived key.

    Parameters
    ----------
    master_passphrase:
        Human-provided passphrase used to derive the master encryption key.
    """

    def __init__(self, master_passphrase: str) -> None:
        if not master_passphrase:
            raise ValueError("Master passphrase must not be empty")
        self._passphrase = master_passphrase

    # ── public helpers ────────────────────────────────────────────

    @staticmethod
    def derive_key(passphrase: str, salt: bytes) -> bytes:
        """Expose key derivation for external callers."""
        return _derive_key(passphrase, salt)

    # ── encrypt / decrypt ─────────────────────────────────────────

    def encrypt(self, plaintext: str) -> str:
        """Encrypt *plaintext* and return ``base64(salt + nonce + tag + ciphertext)``."""
        salt = os.urandom(_SALT_LEN)
        key = _derive_key(self._passphrase, salt)

        if _USE_AESGCM:
            nonce = os.urandom(12)  # 96-bit nonce for GCM
            aesgcm = AESGCM(key)
            # AESGCM.encrypt returns nonce‖ciphertext‖tag combined
            ct_with_tag = aesgcm.encrypt(nonce, plaintext.encode("utf-8"), None)
            # GCM tag is the last 16 bytes
            tag = ct_with_tag[-16:]
            ciphertext = ct_with_tag[:-16]
            blob = salt + nonce + tag + ciphertext
        else:
            # Fernet fallback — key must be 32 url-safe base64 bytes
            from cryptography.fernet import Fernet
            fernet_key = base64.urlsafe_b64encode(key)
            f = Fernet(fernet_key)
            token = f.encrypt(plaintext.encode("utf-8"))
            # Store salt + fernet token (nonce/tag embedded in fernet token)
            blob = salt + token

        return base64.b64encode(blob).decode("ascii")

    def decrypt(self, encrypted: str) -> str:
        """Decrypt a value previously returned by :meth:`encrypt`."""
        blob = base64.b64decode(encrypted)
        salt = blob[:_SALT_LEN]
        key = _derive_key(self._passphrase, salt)

        if _USE_AESGCM:
            nonce = blob[_SALT_LEN : _SALT_LEN + 12]
            tag = blob[_SALT_LEN + 12 : _SALT_LEN + 12 + 16]
            ciphertext = blob[_SALT_LEN + 12 + 16 :]
            aesgcm = AESGCM(key)
            plaintext_bytes = aesgcm.decrypt(nonce, ciphertext + tag, None)
        else:
            from cryptography.fernet import Fernet
            fernet_key = base64.urlsafe_b64encode(key)
            f = Fernet(fernet_key)
            token = blob[_SALT_LEN:]
            plaintext_bytes = f.decrypt(token)

        return plaintext_bytes.decode("utf-8")

    # ── key rotation ──────────────────────────────────────────────

    def rotate_key(
        self,
        old_passphrase: str,
        new_passphrase: str,
        encrypted_entries: Dict[str, str],
    ) -> Dict[str, str]:
        """Re-encrypt all *encrypted_entries* from *old_passphrase* to *new_passphrase*.

        Parameters
        ----------
        old_passphrase:
            The current passphrase used to decrypt existing entries.
        new_passphrase:
            The new passphrase to re-encrypt entries with.
        encrypted_entries:
            Mapping of ``field_name -> encrypted_value``.

        Returns
        -------
        dict
            New mapping with the same keys, values re-encrypted under the new key.
        """
        old_crypto = VaultCrypto(old_passphrase)
        new_crypto = VaultCrypto(new_passphrase)
        result: Dict[str, str] = {}
        for field_name, enc_value in encrypted_entries.items():
            plaintext = old_crypto.decrypt(enc_value)
            result[field_name] = new_crypto.encrypt(plaintext)
        return result
