using System.Security.Cryptography;
using System.Text;
using ZB.MOM.WW.Secrets.Abstractions;

namespace ZB.MOM.WW.Secrets.Crypto;

/// <summary>
/// The default <see cref="ISecretCipher"/>: AES-256-GCM envelope encryption.
/// </summary>
/// <remarks>
/// <para>
/// Each secret is sealed under a fresh, per-secret 32-byte data-encryption key (DEK). The
/// DEK encrypts the UTF-8 plaintext with AES-256-GCM (12-byte nonce, 16-byte tag), binding the
/// secret <see cref="SecretName.Value"/> as additional authenticated data (AAD) so ciphertext
/// cannot be silently relocated to a different name. The DEK is then <i>wrapped</i> with a second
/// AES-256-GCM operation under the master key (KEK) from an <see cref="IMasterKeyProvider"/>,
/// binding the provider's <see cref="IMasterKeyProvider.KekId"/> as AAD so a row can only be
/// unwrapped by the KEK that is claimed to have sealed it.
/// </para>
/// <para>
/// GCM's authentication tag provides constant-time integrity verification, so no additional
/// constant-time comparison is required. Every cryptographic failure path — a mismatched tag,
/// a wrong or unknown KEK, or any other <see cref="CryptographicException"/> — is surfaced as a
/// <see cref="SecretDecryptionException"/>; the cipher never returns a wrong plaintext and never
/// leaks a raw cryptographic exception. The transient DEK buffer is always zeroed with
/// <see cref="CryptographicOperations.ZeroMemory"/>.
/// </para>
/// </remarks>
public sealed class AesGcmEnvelopeCipher : ISecretCipher
{
    private const int KeySizeBytes = 32;   // AES-256
    private const int NonceSizeBytes = 12; // AES-GCM standard nonce
    private const int TagSizeBytes = 16;   // AES-GCM full tag

    private readonly IMasterKeyProvider _masterKeyProvider;

    /// <summary>
    /// Creates a cipher that wraps and unwraps DEKs with the master key supplied by
    /// <paramref name="masterKeyProvider"/>.
    /// </summary>
    /// <param name="masterKeyProvider">The source of the AES-256 master key (KEK) and its id.</param>
    public AesGcmEnvelopeCipher(IMasterKeyProvider masterKeyProvider)
    {
        _masterKeyProvider = masterKeyProvider ?? throw new ArgumentNullException(nameof(masterKeyProvider));
    }

    /// <inheritdoc />
    public StoredSecret Encrypt(SecretName name, string plaintext, SecretContentType contentType)
    {
        ArgumentNullException.ThrowIfNull(plaintext);

        string kekId = _masterKeyProvider.KekId;
        ReadOnlySpan<byte> masterKey = _masterKeyProvider.GetMasterKey().Span;

        byte[] dek = new byte[KeySizeBytes];
        try
        {
            RandomNumberGenerator.Fill(dek);

            // Layer 1: seal the plaintext under the DEK, binding the secret name as AAD.
            byte[] plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
            byte[] nonce = new byte[NonceSizeBytes];
            byte[] tag = new byte[TagSizeBytes];
            byte[] ciphertext = new byte[plaintextBytes.Length];
            byte[] nameAad = Encoding.UTF8.GetBytes(name.Value);

            RandomNumberGenerator.Fill(nonce);
            using (var bodyGcm = new AesGcm(dek, TagSizeBytes))
            {
                bodyGcm.Encrypt(nonce, plaintextBytes, ciphertext, tag, nameAad);
            }

            // Layer 2: wrap the DEK under the master key, binding the KEK id as AAD.
            byte[] wrapNonce = new byte[NonceSizeBytes];
            byte[] wrapTag = new byte[TagSizeBytes];
            byte[] wrappedDek = new byte[KeySizeBytes];
            byte[] kekAad = Encoding.UTF8.GetBytes(kekId);

            RandomNumberGenerator.Fill(wrapNonce);
            using (var wrapGcm = new AesGcm(masterKey, TagSizeBytes))
            {
                wrapGcm.Encrypt(wrapNonce, dek, wrappedDek, wrapTag, kekAad);
            }

            return new StoredSecret
            {
                Name = name,
                ContentType = contentType,
                Ciphertext = ciphertext,
                Nonce = nonce,
                Tag = tag,
                WrappedDek = wrappedDek,
                WrapNonce = wrapNonce,
                WrapTag = wrapTag,
                KekId = kekId,
                Revision = 0,
                IsDeleted = false,
                CreatedUtc = default,
                UpdatedUtc = default,
            };
        }
        finally
        {
            CryptographicOperations.ZeroMemory(dek);
        }
    }

    /// <inheritdoc />
    public string Decrypt(StoredSecret secret)
    {
        ArgumentNullException.ThrowIfNull(secret);

        string kekId = _masterKeyProvider.KekId;
        if (!string.Equals(kekId, secret.KekId, StringComparison.Ordinal))
        {
            throw new SecretDecryptionException($"Row wrapped by unknown KEK '{secret.KekId}'.");
        }

        ReadOnlySpan<byte> masterKey = _masterKeyProvider.GetMasterKey().Span;

        byte[] dek = new byte[KeySizeBytes];
        try
        {
            // Unwrap the DEK under the master key, verifying the KEK-id AAD binding.
            byte[] kekAad = Encoding.UTF8.GetBytes(secret.KekId);
            using (var wrapGcm = new AesGcm(masterKey, TagSizeBytes))
            {
                wrapGcm.Decrypt(secret.WrapNonce, secret.WrappedDek, secret.WrapTag, dek, kekAad);
            }

            // Decrypt the body under the DEK, verifying the name AAD binding.
            byte[] nameAad = Encoding.UTF8.GetBytes(secret.Name.Value);
            byte[] plaintextBytes = new byte[secret.Ciphertext.Length];
            using (var bodyGcm = new AesGcm(dek, TagSizeBytes))
            {
                bodyGcm.Decrypt(secret.Nonce, secret.Ciphertext, secret.Tag, plaintextBytes, nameAad);
            }

            return Encoding.UTF8.GetString(plaintextBytes);
        }
        catch (AuthenticationTagMismatchException ex)
        {
            throw new SecretDecryptionException(
                $"Failed to decrypt secret '{secret.Name.Value}': authentication tag mismatch.", ex);
        }
        catch (CryptographicException ex)
        {
            throw new SecretDecryptionException(
                $"Failed to decrypt secret '{secret.Name.Value}'.", ex);
        }
        finally
        {
            CryptographicOperations.ZeroMemory(dek);
        }
    }

    /// <inheritdoc />
    public StoredSecret Rewrap(StoredSecret secret, IMasterKeyProvider oldKek, IMasterKeyProvider newKek)
    {
        ArgumentNullException.ThrowIfNull(secret);
        ArgumentNullException.ThrowIfNull(oldKek);
        ArgumentNullException.ThrowIfNull(newKek);

        // Fail closed if the row is not actually wrapped by the supplied old KEK — a caller must
        // not silently "rewrap" a row that is already on a different key.
        if (!string.Equals(secret.KekId, oldKek.KekId, StringComparison.Ordinal))
        {
            throw new SecretDecryptionException(
                $"Cannot rewrap secret '{secret.Name.Value}': row is wrapped by KEK '{secret.KekId}', " +
                $"not the supplied old KEK '{oldKek.KekId}'.");
        }

        ReadOnlySpan<byte> oldMasterKey = oldKek.GetMasterKey().Span;
        ReadOnlySpan<byte> newMasterKey = newKek.GetMasterKey().Span;
        string newKekId = newKek.KekId;

        byte[] dek = new byte[KeySizeBytes];
        try
        {
            // Unwrap the DEK under the OLD master key, verifying the old KEK-id AAD binding. A wrong
            // old key or tampered wrap surfaces as SecretDecryptionException (never a raw crypto leak).
            byte[] oldKekAad = Encoding.UTF8.GetBytes(secret.KekId);
            try
            {
                using var unwrap = new AesGcm(oldMasterKey, TagSizeBytes);
                unwrap.Decrypt(secret.WrapNonce, secret.WrappedDek, secret.WrapTag, dek, oldKekAad);
            }
            // CryptographicException = wrong key / tampered wrap; ArgumentException = a malformed
            // (wrong-length) wrap nonce/tag on a corrupt row. Both fail closed as a decryption error
            // rather than leaking a raw crypto/arg exception (which the caller would misclassify).
            catch (Exception ex) when (ex is CryptographicException or ArgumentException)
            {
                throw new SecretDecryptionException(
                    $"Failed to rewrap secret '{secret.Name.Value}': the DEK could not be unwrapped " +
                    "under the supplied old KEK.", ex);
            }

            // Re-wrap the SAME DEK under the NEW master key with a fresh nonce, binding the new KEK id
            // as AAD. The body ciphertext is left untouched — rotation re-wraps DEKs only.
            byte[] wrapNonce = new byte[NonceSizeBytes];
            byte[] wrapTag = new byte[TagSizeBytes];
            byte[] wrappedDek = new byte[KeySizeBytes];
            byte[] newKekAad = Encoding.UTF8.GetBytes(newKekId);

            RandomNumberGenerator.Fill(wrapNonce);
            using (var wrapGcm = new AesGcm(newMasterKey, TagSizeBytes))
            {
                wrapGcm.Encrypt(wrapNonce, dek, wrappedDek, wrapTag, newKekAad);
            }

            // Only the KEK-wrap envelope + kek_id change; body, revision, timestamps, tombstone,
            // content-type, and every other field are preserved so a rewrap is invisible to the
            // (updated_utc, revision) last-writer-wins ordering used by cluster anti-entropy.
            return secret with
            {
                WrappedDek = wrappedDek,
                WrapNonce = wrapNonce,
                WrapTag = wrapTag,
                KekId = newKekId,
            };
        }
        finally
        {
            CryptographicOperations.ZeroMemory(dek);
        }
    }
}
