using System.Security.Cryptography;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;

namespace ZB.MOM.WW.ScadaBridge.Transport.Encryption;

/// <summary>
/// AES-256-GCM authenticated encryption with a PBKDF2-SHA256 derived key.
/// Output format is <c>ciphertext || tag</c> (tag is the GCM authentication tag).
/// Each encrypt call produces a fresh random salt + nonce so re-encrypting the
/// same plaintext yields a different ciphertext.
/// </summary>
public sealed class BundleSecretEncryptor
{
    private const int KeyBytes = 32;            // AES-256.
    private const int SaltBytes = 16;
    private const int NonceBytes = 12;          // GCM standard.
    private const int TagBytes = 16;

    /// <summary>
    /// Encrypts plaintext using AES-256-GCM with a PBKDF2-derived key and a fresh random salt and nonce.
    /// </summary>
    /// <param name="plaintext">The data to encrypt.</param>
    /// <param name="passphrase">The passphrase used to derive the encryption key.</param>
    /// <param name="iterations">PBKDF2 iteration count for key derivation.</param>
    /// <param name="associatedData">
    /// T-005: optional AES-GCM Associated Authenticated Data — typically
    /// <see cref="BundleManifestAad.Compute"/>(manifest). Binds the manifest's
    /// non-derivative fields (source environment, exporter, summary, …) to the
    /// ciphertext so any tampering of those fields yields an authentication-tag
    /// mismatch on decrypt. Pass <see cref="ReadOnlySpan{Byte}.Empty"/> for the
    /// legacy no-AAD format; the decrypter must mirror the choice.
    /// </param>
    /// <returns>The ciphertext (with appended GCM tag) and the encryption metadata needed to decrypt.</returns>
    public (byte[] Ciphertext, EncryptionMetadata Metadata) Encrypt(
        ReadOnlySpan<byte> plaintext,
        string passphrase,
        int iterations,
        ReadOnlySpan<byte> associatedData = default)
    {
        var salt = RandomNumberGenerator.GetBytes(SaltBytes);
        var nonce = RandomNumberGenerator.GetBytes(NonceBytes);
        var key = DeriveKey(passphrase, salt, iterations);

        var ciphertext = new byte[plaintext.Length];
        var tag = new byte[TagBytes];
        using var aes = new AesGcm(key, TagBytes);
        aes.Encrypt(nonce, plaintext, ciphertext, tag, associatedData);

        // Format: ciphertext || tag.
        var output = new byte[ciphertext.Length + TagBytes];
        Buffer.BlockCopy(ciphertext, 0, output, 0, ciphertext.Length);
        Buffer.BlockCopy(tag, 0, output, ciphertext.Length, TagBytes);

        return (output, new EncryptionMetadata(
            "AES-256-GCM", "PBKDF2-SHA256", iterations,
            Convert.ToBase64String(salt),
            Convert.ToBase64String(nonce)));
    }

    /// <summary>
    /// Decrypts a payload produced by <see cref="Encrypt"/> using the supplied metadata and passphrase.
    /// </summary>
    /// <param name="payload">The ciphertext with appended GCM tag.</param>
    /// <param name="metadata">Encryption metadata carrying the algorithm, KDF, salt, nonce, and iterations.</param>
    /// <param name="passphrase">The passphrase used to derive the decryption key.</param>
    /// <param name="associatedData">
    /// T-005: AAD that was passed to <see cref="Encrypt"/>. Must match exactly —
    /// any tampering of the manifest used to derive AAD yields an authentication-
    /// tag mismatch (surfaces as <see cref="CryptographicException"/>, specifically
    /// <c>AuthenticationTagMismatchException</c> on .NET 10).
    /// </param>
    /// <returns>The decrypted plaintext bytes.</returns>
    public byte[] Decrypt(
        ReadOnlySpan<byte> payload,
        EncryptionMetadata metadata,
        string passphrase,
        ReadOnlySpan<byte> associatedData = default)
    {
        ArgumentNullException.ThrowIfNull(metadata);

        if (metadata.Algorithm != "AES-256-GCM" || metadata.Kdf != "PBKDF2-SHA256")
        {
            throw new CryptographicException("Unsupported bundle encryption parameters.");
        }

        var salt = Convert.FromBase64String(metadata.SaltB64);
        var nonce = Convert.FromBase64String(metadata.IvB64);
        var key = DeriveKey(passphrase, salt, metadata.Iterations);

        if (payload.Length < TagBytes)
        {
            throw new CryptographicException("Bundle payload too short.");
        }

        var ctLen = payload.Length - TagBytes;
        var ciphertext = payload[..ctLen];
        var tag = payload[ctLen..];

        var plaintext = new byte[ctLen];
        using var aes = new AesGcm(key, TagBytes);
        aes.Decrypt(nonce, ciphertext, tag, plaintext, associatedData);
        return plaintext;
    }

    private static byte[] DeriveKey(string passphrase, byte[] salt, int iterations)
    {
        return Rfc2898DeriveBytes.Pbkdf2(passphrase, salt, iterations, HashAlgorithmName.SHA256, KeyBytes);
    }
}
