"""Cryptographic operations for the secrets module.

Uses AES-256-GCM for authenticated encryption with unique nonces.
"""

from __future__ import annotations

import base64
import os

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

from gaol.secrets.exceptions import SecretDecryptionError

# Constants
NONCE_SIZE = 12  # 96 bits for GCM (NIST recommendation)
KEY_SIZE = 32  # 256 bits
SALT_SIZE = 32  # 256 bits
DEFAULT_KDF_ITERATIONS = 600_000  # OWASP 2023 recommendation for PBKDF2-SHA256


class SecretCrypto:
    """Handles encryption and decryption of secrets using AES-256-GCM."""

    def __init__(self, master_key: bytes) -> None:
        """Initialize with a master key.

        Args:
            master_key: The encryption key (32 bytes for AES-256)
        """
        if len(master_key) < KEY_SIZE:
            raise ValueError(f"Master key must be at least {KEY_SIZE} bytes")
        self._key = master_key[:KEY_SIZE]
        self._aesgcm = AESGCM(self._key)

    def encrypt(self, plaintext: str) -> str:
        """Encrypt plaintext and return base64-encoded ciphertext with nonce.

        Args:
            plaintext: The secret value to encrypt

        Returns:
            Base64-encoded string containing nonce + ciphertext
        """
        nonce = os.urandom(NONCE_SIZE)
        ciphertext = self._aesgcm.encrypt(nonce, plaintext.encode("utf-8"), None)
        # Combine nonce and ciphertext, then base64 encode
        return base64.b64encode(nonce + ciphertext).decode("ascii")

    def decrypt(self, encrypted: str) -> str:
        """Decrypt base64-encoded ciphertext with embedded nonce.

        Args:
            encrypted: Base64-encoded string containing nonce + ciphertext

        Returns:
            Decrypted plaintext string

        Raises:
            SecretDecryptionError: If decryption fails
        """
        try:
            data = base64.b64decode(encrypted)
            if len(data) < NONCE_SIZE + 16:  # Minimum: nonce + auth tag
                raise SecretDecryptionError("Encrypted data too short")

            nonce = data[:NONCE_SIZE]
            ciphertext = data[NONCE_SIZE:]
            plaintext = self._aesgcm.decrypt(nonce, ciphertext, None)
            return plaintext.decode("utf-8")
        except Exception as e:
            if isinstance(e, SecretDecryptionError):
                raise
            raise SecretDecryptionError(f"Decryption failed: {e}") from e


def derive_key_from_password(
    password: str,
    salt: bytes,
    iterations: int = DEFAULT_KDF_ITERATIONS,
) -> bytes:
    """Derive an encryption key from a password using PBKDF2.

    Args:
        password: The user's password
        salt: Random salt (should be stored alongside encrypted data)
        iterations: Number of PBKDF2 iterations

    Returns:
        Derived key (32 bytes)
    """
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=KEY_SIZE,
        salt=salt,
        iterations=iterations,
    )
    return kdf.derive(password.encode("utf-8"))


def generate_salt() -> bytes:
    """Generate a cryptographically secure random salt.

    Returns:
        Random salt (32 bytes)
    """
    return os.urandom(SALT_SIZE)


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

    Returns:
        Random master key (32 bytes)
    """
    return os.urandom(KEY_SIZE)


def encode_key(key: bytes) -> str:
    """Encode a key as base64 for storage.

    Args:
        key: The key bytes to encode

    Returns:
        Base64-encoded string
    """
    return base64.b64encode(key).decode("ascii")


def decode_key(encoded: str) -> bytes:
    """Decode a base64-encoded key.

    Args:
        encoded: Base64-encoded key string

    Returns:
        Decoded key bytes
    """
    return base64.b64decode(encoded)
