using System.Security.Cryptography;
using System.Text;

namespace Journal.Core.Services.Vault;

public class VaultCryptoService : IVaultCryptoService
{
    public const int SaltSize = 16;
    public const int KeySize = 32;
    public const int NonceSize = 12;
    public const int TagSize = 16;
    public const int Iterations = 600_000;

    public byte[] DeriveKey(string password, byte[] salt)
    {
        if (string.IsNullOrWhiteSpace(password))
            throw new ArgumentException("Password cannot be empty.", nameof(password));
        ArgumentNullException.ThrowIfNull(salt);
        if (salt.Length != SaltSize)
            throw new ArgumentException($"Salt must be {SaltSize} bytes.", nameof(salt));

        return Rfc2898DeriveBytes.Pbkdf2(
            Encoding.UTF8.GetBytes(password),
            salt,
            Iterations,
            HashAlgorithmName.SHA256,
            KeySize);
    }

    public byte[] EncryptData(byte[] data, string password)
    {
        ArgumentNullException.ThrowIfNull(data);

        var salt = RandomNumberGenerator.GetBytes(SaltSize);
        var nonce = RandomNumberGenerator.GetBytes(NonceSize);
        return EncryptData(data, password, salt, nonce);
    }

    public byte[] DecryptData(byte[] encryptedData, string password)
    {
        ArgumentNullException.ThrowIfNull(encryptedData);

        var minLength = SaltSize + NonceSize + TagSize;
        if (encryptedData.Length < minLength)
            throw new ArgumentException("Encrypted payload is too short.", nameof(encryptedData));

        var salt = encryptedData.AsSpan(0, SaltSize).ToArray();
        var nonce = encryptedData.AsSpan(SaltSize, NonceSize).ToArray();
        var tag = encryptedData.AsSpan(SaltSize + NonceSize, TagSize).ToArray();
        var ciphertext = encryptedData.AsSpan(SaltSize + NonceSize + TagSize).ToArray();

        var key = DeriveKey(password, salt);
        var plaintext = new byte[ciphertext.Length];
        using var aes = new AesGcm(key, TagSize);
        aes.Decrypt(nonce, ciphertext, tag, plaintext);
        return plaintext;
    }

    public byte[] EncryptData(byte[] data, string password, byte[] salt, byte[] nonce)
    {
        ArgumentNullException.ThrowIfNull(data);
        ArgumentNullException.ThrowIfNull(salt);
        ArgumentNullException.ThrowIfNull(nonce);
        if (salt.Length != SaltSize)
            throw new ArgumentException($"Salt must be {SaltSize} bytes.", nameof(salt));
        if (nonce.Length != NonceSize)
            throw new ArgumentException($"Nonce must be {NonceSize} bytes.", nameof(nonce));

        var key = DeriveKey(password, salt);
        var ciphertext = new byte[data.Length];
        var tag = new byte[TagSize];

        using var aes = new AesGcm(key, TagSize);
        aes.Encrypt(nonce, data, ciphertext, tag);

        var payload = new byte[SaltSize + NonceSize + TagSize + ciphertext.Length];
        Buffer.BlockCopy(salt, 0, payload, 0, SaltSize);
        Buffer.BlockCopy(nonce, 0, payload, SaltSize, NonceSize);
        Buffer.BlockCopy(tag, 0, payload, SaltSize + NonceSize, TagSize);
        Buffer.BlockCopy(ciphertext, 0, payload, SaltSize + NonceSize + TagSize, ciphertext.Length);
        return payload;
    }
}
