using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using Microsoft.AspNetCore.Identity;

namespace Baya.Infrastructure.Identity.Identity.DataProtection;

public class LookupProtector : ILookupProtector
{
    private readonly ProtectorAlgorithm _defaultAlgorithm = ProtectorAlgorithm.Aes256Hmac512;
    private readonly ILookupProtectorKeyRing _keyRing;

    public LookupProtector(ILookupProtectorKeyRing keyRing)
    {
        _keyRing = keyRing;
    }

    public string Protect(string keyId, string data)
    {
        // Get the default algorithms.
        // We does this so we can embed the algorithm details used in the cipher text so we can
        // change algorithms as yet another collision appears in a hashing algorithm.
        // See https://media.blackhat.com/bh-us-10/whitepapers/Sullivan/BlackHat-USA-2010-Sullivan-Cryptographic-Agility-wp.pdf
        ProtectorAlgorithmHelper.GetAlgorithms(
            _defaultAlgorithm,
            out SymmetricAlgorithm encryptingAlgorithm,
            out KeyedHashAlgorithm signingAlgorithm,
            out int keyDerivationIterationCount);

        var masterKey = MasterKey(keyId);

        // Convert the string to bytes, because encryption works on bytes, not strings.
        var plainText = Encoding.UTF8.GetBytes(data);
        byte[] cipherTextAndIV;

        // Derive a key for encryption from the master key
        encryptingAlgorithm.Key = DerivedEncryptionKey(
            masterKey,
            encryptingAlgorithm,
            keyDerivationIterationCount);

        // As we need this to be deterministic, we need to force an IV that is derived from the plain text.
        encryptingAlgorithm.IV = DerivedInitializationVector(
            masterKey,
            data,
            encryptingAlgorithm,
            keyDerivationIterationCount);

        // And encrypt
        using (var ms = new MemoryStream())
        using (var cs = new CryptoStream(
                   ms,
                   encryptingAlgorithm.CreateEncryptor(),
                   CryptoStreamMode.Write))
        {
            cs.Write(plainText);
            cs.FlushFinalBlock();
            var encryptedData = ms.ToArray();

            cipherTextAndIV = CombineByteArrays(encryptingAlgorithm.IV, encryptedData);
        }

        // Now get a signature for the data so we can detect tampering in situ.
        byte[] signature = SignData(
            cipherTextAndIV,
            masterKey,
            encryptingAlgorithm,
            signingAlgorithm,
            keyDerivationIterationCount);

        // Add the signature to the cipher text.
        var signedData = CombineByteArrays(signature, cipherTextAndIV);

        // Add our algorithm identifier to the combined signature and cipher text.
        var algorithmIdentifier = BitConverter.GetBytes((int)_defaultAlgorithm);
        byte[] output = CombineByteArrays(algorithmIdentifier, signedData);

        // Clean everything up.
        encryptingAlgorithm.Clear();
        signingAlgorithm.Clear();
        encryptingAlgorithm.Dispose();
        signingAlgorithm.Dispose();

        Array.Clear(plainText, 0, plainText.Length);

        // Return the results as a string.
        return Convert.ToBase64String(output);
    }

    public string Unprotect(string keyId, string data)
    {
        var masterKey = MasterKey(keyId);
        byte[] plainText;

        // Take our string and convert it back to bytes.
        var payload = Convert.FromBase64String(data);

        // Read the saved algorithm details and create instances of those algorithms.
        byte[] algorithmIdentifierAsBytes = new byte[4];
        Buffer.BlockCopy(payload, 0, algorithmIdentifierAsBytes, 0, 4);
        var algorithmIdentifier = (ProtectorAlgorithm)(BitConverter.ToInt32(algorithmIdentifierAsBytes, 0));
        ProtectorAlgorithmHelper.GetAlgorithms(
            _defaultAlgorithm,
            out SymmetricAlgorithm encryptingAlgorithm,
            out KeyedHashAlgorithm signingAlgorithm,
            out int keyDerivationIterationCount);

        // Now extract the signature
        byte[] signature = new byte[signingAlgorithm.HashSize / 8];
        Buffer.BlockCopy(payload, 4, signature, 0, signingAlgorithm.HashSize / 8);

        // And finally grab the rest of the data
        var dataLength = payload.Length - 4 - signature.Length;
        byte[] cipherTextAndIV = new byte[dataLength];
        Buffer.BlockCopy(payload, 4 + signature.Length, cipherTextAndIV, 0, dataLength);

        // Check the signature before anything else is done to detect tampering and avoid
        // oracles.
        byte[] computedSignature = SignData(
            cipherTextAndIV,
            masterKey,
            encryptingAlgorithm,
            signingAlgorithm,
            keyDerivationIterationCount);
        if (!ByteArraysEqual(computedSignature, signature))
        {
            throw new CryptographicException(@"Invalid Signature.");
        }
        signingAlgorithm.Clear();
        signingAlgorithm.Dispose();

        // The signature is valid, so now we can work on decrypting the data.
        var ivLength = encryptingAlgorithm.BlockSize / 8;
        byte[] initializationVector = new byte[ivLength];
        byte[] cipherText = new byte[cipherTextAndIV.Length - ivLength];
        // The IV is embedded in the cipher text, so we extract it out.
        Buffer.BlockCopy(cipherTextAndIV, 0, initializationVector, 0, ivLength);
        // Then we get the encrypted data.
        Buffer.BlockCopy(cipherTextAndIV, ivLength, cipherText, 0, cipherTextAndIV.Length - ivLength);

        encryptingAlgorithm.Key = DerivedEncryptionKey(
            masterKey,
            encryptingAlgorithm,
            keyDerivationIterationCount);
        encryptingAlgorithm.IV = initializationVector;

        // Decrypt
        using (var ms = new MemoryStream())
        using (var cs = new CryptoStream(ms, encryptingAlgorithm.CreateDecryptor(), CryptoStreamMode.Write))
        {
            cs.Write(cipherText);
            cs.FlushFinalBlock();
            plainText = ms.ToArray();
        }
        encryptingAlgorithm.Clear();
        encryptingAlgorithm.Dispose();

        // And convert from the bytes back to a string.
        return Encoding.UTF8.GetString(plainText);
    }

    public byte[] MasterKey(string keyId)
    {
        return Convert.FromBase64String(_keyRing[keyId]);
    }

    private byte[] SignData(byte[] cipherText, byte[] masterKey, SymmetricAlgorithm symmetricAlgorithm, KeyedHashAlgorithm hashAlgorithm, int keyDerivationIterationCount)
    {
        hashAlgorithm.Key = DerivedSigningKey(masterKey, symmetricAlgorithm, keyDerivationIterationCount);
        byte[] signature = hashAlgorithm.ComputeHash(cipherText);
        hashAlgorithm.Clear();
        return signature;
    }

    private byte[] DerivedSigningKey(byte[] key, SymmetricAlgorithm algorithm, int keyDerivationIterationCount)
    {
        return KeyDerivation.Pbkdf2(
            @"IdentityLookupDataSigning",
            key,
            KeyDerivationPrf.HMACSHA512,
            keyDerivationIterationCount,
            algorithm.KeySize / 8);
    }

    private byte[] DerivedEncryptionKey(byte[] key, SymmetricAlgorithm algorithm, int keyDerivationIterationCount)
    {
        return KeyDerivation.Pbkdf2(
            @"IdentityLookupEncryption",
            key,
            KeyDerivationPrf.HMACSHA512,
            keyDerivationIterationCount,
            algorithm.KeySize / 8);
    }

    private byte[] DerivedInitializationVector(byte[] key, string plainText, SymmetricAlgorithm algorithm, int keyDerivationIterationCount)
    {
        return KeyDerivation.Pbkdf2(
            plainText,
            key,
            KeyDerivationPrf.HMACSHA512,
            keyDerivationIterationCount,
            algorithm.BlockSize / 8);
    }

    private byte[] CombineByteArrays(byte[] left, byte[] right)
    {
        byte[] output = new byte[left.Length + right.Length];
        Buffer.BlockCopy(left, 0, output, 0, left.Length);
        Buffer.BlockCopy(right, 0, output, left.Length, right.Length);

        return output;
    }

    [MethodImpl(MethodImplOptions.NoOptimization)]
    private static bool ByteArraysEqual(byte[] a, byte[] b)
    {
        if (ReferenceEquals(a, b))
        {
            return true;
        }

        if (a == null || b == null || a.Length != b.Length)
        {
            return false;
        }

        bool areSame = true;
        for (int i = 0; i < a.Length; i++)
        {
            areSame &= (a[i] == b[i]);
        }
        return areSame;
    }

}