using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace InABox.Core
{
    public static class Encryption
    {
        // This constant is used to determine the keysize of the encryption algorithm in bits.
        // We divide this by 8 within the code below to get the equivalent number of bytes.
        private const int Keysize = 256;

        // This constant determines the number of iterations for the password bytes generation function.
        private const int DerivationIterations = 1000;


        public static string Encrypt(string plainText, string passPhrase, bool simple = false)
        {
            if (simple)
            {
                var encrypted = new byte[plainText.Length];
                for (var i = 0; i < plainText.Length; i++)
                {
                    var ch = plainText[i];
                    encrypted[i] = (byte)(ch + passPhrase[i % passPhrase.Length]);
                }

                return Convert.ToBase64String(encrypted);
            }

            // Salt and IV is randomly generated each time, but is preprended to encrypted cipher text
            // so that the same Salt and IV values can be used when decrypting.  
            var saltStringBytes = Generate256BitsOfRandomEntropy();
            var ivStringBytes = Generate256BitsOfRandomEntropy();
            var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
            using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
            {
                var keyBytes = password.GetBytes(Keysize / 8);
                using (var symmetricKey = new RijndaelManaged())
                {
                    symmetricKey.BlockSize = 256;
                    symmetricKey.Mode = CipherMode.CBC;
                    symmetricKey.Padding = PaddingMode.PKCS7;
                    using (var encryptor = symmetricKey.CreateEncryptor(keyBytes, ivStringBytes))
                    {
                        using (var memoryStream = new MemoryStream())
                        {
                            using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
                            {
                                cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
                                cryptoStream.FlushFinalBlock();
                                // Create the final bytes as a concatenation of the random salt bytes, the random iv bytes and the cipher bytes.
                                var cipherTextBytes = saltStringBytes;
                                cipherTextBytes = cipherTextBytes.Concat(ivStringBytes).ToArray();
                                cipherTextBytes = cipherTextBytes.Concat(memoryStream.ToArray()).ToArray();
                                memoryStream.Close();
                                cryptoStream.Close();
                                return Convert.ToBase64String(cipherTextBytes);
                            }
                        }
                    }
                }
            }
        }

        public static string Decrypt(string cipherText, string passPhrase, bool simpleEncryption = true)
        {
            if (string.IsNullOrWhiteSpace(cipherText))
                throw new Exception("Text may not be blank!");

            // New Simplified Decryption to handle .Net6 clients
            // Kenric needs this to get the GenHTTP server running
            // Needs to be revisited (stronger encryption?  TLS/SSL?) when moving to .Net6 overall
            if (cipherText.Length < 64 || simpleEncryption == true)
            {
                var encrypted = Convert.FromBase64String(cipherText);
                var decrypted = new byte[encrypted.Length];
                for (var i = 0; i < encrypted.Length; i++)
                {
                    var ch = encrypted[i];
                    decrypted[i] = (byte)(ch - passPhrase[i % passPhrase.Length]);
                }

                return Encoding.UTF8.GetString(decrypted);
            }

            // Get the complete stream of bytes that represent:
            // [32 bytes of Salt] + [32 bytes of IV] + [n bytes of CipherText]
            var cipherTextBytesWithSaltAndIv = Convert.FromBase64String(cipherText);
            // Get the saltbytes by extracting the first 32 bytes from the supplied cipherText bytes.
            var saltStringBytes = cipherTextBytesWithSaltAndIv.Take(Keysize / 8).ToArray();
            // Get the IV bytes by extracting the next 32 bytes from the supplied cipherText bytes.
            var ivStringBytes = cipherTextBytesWithSaltAndIv.Skip(Keysize / 8).Take(Keysize / 8).ToArray();
            // Get the actual cipher text bytes by removing the first 64 bytes from the cipherText string.
            var cipherTextBytes = cipherTextBytesWithSaltAndIv.Skip(Keysize / 8 * 2).Take(cipherTextBytesWithSaltAndIv.Length - Keysize / 8 * 2)
                .ToArray();

            using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
            {
                var keyBytes = password.GetBytes(Keysize / 8);
                using (var symmetricKey = new RijndaelManaged())
                {
                    symmetricKey.BlockSize = 256;
                    symmetricKey.Mode = CipherMode.CBC;
                    symmetricKey.Padding = PaddingMode.PKCS7;
                    using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, ivStringBytes))
                    {
                        using (var memoryStream = new MemoryStream(cipherTextBytes))
                        {
                            using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
                            {
                                var plainTextBytes = new byte[cipherTextBytes.Length];
                                var decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length);
                                memoryStream.Close();
                                cryptoStream.Close();
                                return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount);
                            }
                        }
                    }
                }
            }
        }

        public static bool Decrypt(string cipherText, string passPhrase, out string decrypted)
        {
            decrypted = "";
            var result = false;
            try
            {
                decrypted = Decrypt(cipherText, passPhrase);
                result = true;
            }
            catch (Exception e)
            {
            }

            return result;
        }

        public static byte[] Generate256BitsOfRandomEntropy()
        {
            var randomBytes = new byte[32]; // 32 Bytes will give us 256 bits.
            using (var rngCsp = new RNGCryptoServiceProvider())
            {
                // Fill the array with cryptographically secure random bytes.
                rngCsp.GetBytes(randomBytes);
            }

            return randomBytes;
        }

        public static string EncryptV2(string data, byte[] key)
        {
            if (TryEncryptV2(data, key, out var result, out var error))
            {
                return result;
            }
            throw new Exception(error);
        }
        public static bool TryEncryptV2(string data, byte[] key, [NotNullWhen(true)] out string? result, [NotNullWhen(false)] out string? error)
        {
            try
            {
                using var stream = new MemoryStream();
                using (var aes = Aes.Create())
                {
                    aes.Key = key;
                    var iv = aes.IV;
                    stream.Write(iv, 0, iv.Length);
                    using var cryptoStream = new CryptoStream(stream, aes.CreateEncryptor(), CryptoStreamMode.Write);
                    using var writer = new StreamWriter(cryptoStream);
                    writer.Write(data);
                }
                result = Convert.ToBase64String(stream.ToArray());
                error = null;
                return true;
            }
            catch(Exception e)
            {
                error = $"Encryption failed! {CoreUtils.FormatException(e)}";
                result = null;
                return false;
            }
        }
        public static string DecryptV2(string data, byte[] key)
        {
            if(TryDecryptV2(data, key, out var result, out var error))
            {
                return result;
            }
            throw new Exception(error);
        }
        public static bool TryDecryptV2(string data, byte[] key, [NotNullWhen(true)] out string? result, [NotNullWhen(false)] out string? error)
        {
            try
            {
                using var stream = new MemoryStream(Convert.FromBase64String(data));
                using var aes = Aes.Create();

                var iv = new byte[aes.IV.Length];
                if (stream.Read(iv, 0, iv.Length) != iv.Length)
                {
                    error = $"Decryption failed! Data is too short for IV.";
                    result = null;
                    return false;
                }

                using var cryptoStream = new CryptoStream(stream, aes.CreateDecryptor(key, iv), CryptoStreamMode.Read);
                using var decryptReader = new StreamReader(cryptoStream);
                var decrypted = decryptReader.ReadToEnd();
                result = decrypted;
                error = null;
                return true;
            }
            catch(Exception e)
            {
                error = $"Decryption failed! {CoreUtils.FormatException(e)}";
                result = null;
                return false;
            }
        }
    }
}