using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;

namespace e_suite.Nuget.PasswordHasher;

public class CustomPasswordHasher<TUser> : IPasswordHasher<TUser> where TUser : class
{
    // https://owasp.org/www-project-cheat-sheets/cheatsheets/Password_Storage_Cheat_Sheet.html
    // default password hasher code is https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/PasswordHasher.cs
    // it's already using SHA256 with properly salted passwords, so no need to reinvent the wheel here, and will be upgraded in future versions of .net core.

    /* =======================
     * HASHED PASSWORD FORMATS
     * =======================
     * 
     * FormulaOne:
     * PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
     * (See also: SDL crypto guidelines v5.1, Part III)
     * Format: { 0x00, salt, subkey }
     *
     * ForumulaTwo:
     * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
     * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
     * (All UInt32s are stored big-endian.)
     *
     * ForumulaThree:
     * PBKDF2 with HMAC-SHA512, 128-bit salt, 256-bit subkey, 10000 iterations.
     * Format: { 0x01, prf (UInt32), iter count (UInt32), pepperIndex (UInt32), salt length (UInt32), salt, subkey }
     * (All UInt32s are stored big-endian.)
     */

    private readonly CustomPasswordHasherMethod _compatibilityMode;
    private readonly int _iterCount;
    private readonly RandomNumberGenerator _rng;
    private readonly Dictionary<int, byte[]> _pepper;

    /// <summary>
    /// Creates a new instance of <see cref="PasswordHasher{TUser}"/>.
    /// </summary>
    /// <param name="options">The options for this instance.</param>
    public CustomPasswordHasher(IOptions<CustomPasswordHasherOptions> optionsAccessor = null!)
    {
        var options = optionsAccessor?.Value ?? new CustomPasswordHasherOptions();
        _pepper = options.PepperDictionary;

        _compatibilityMode = options.CompatibilityMode;
        switch (_compatibilityMode)
        {
            case CustomPasswordHasherMethod.formulaOne:
                // nothing else to do
                break;

            case CustomPasswordHasherMethod.formulaTwo:
                _iterCount = options.IterationCount;
                if (_iterCount < 1)
                {
                    throw new InvalidOperationException(Resources.InvalidPasswordHasherIterationCount);
                }
                break;

            case CustomPasswordHasherMethod.formulaThree:
                _iterCount = options.IterationCount;
                if (_iterCount < 1)
                {
                    throw new InvalidOperationException(Resources.InvalidPasswordHasherIterationCount);
                }

                break;
            default:
                throw new InvalidOperationException(Resources.InvalidPasswordHasherCompatibilityMode);
        }

        _rng = RandomNumberGenerator.Create();
    }

#if NETSTANDARD2_0
        // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized.
        [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
        private static bool ByteArraysEqual(byte[] a, byte[] b)
        {
            if (a == null && b == null)
            {
                return true;
            }
            if (a == null || b == null || a.Length != b.Length)
            {
                return false;
            }
            var areSame = true;
            for (var i = 0; i < a.Length; i++)
            {
                areSame &= (a[i] == b[i]);
            }
            return areSame;
        }
#endif

    /// <summary>
    /// Returns a hashed representation of the supplied <paramref name="password"/> for the specified <paramref name="user"/>.
    /// </summary>
    /// <param name="user">The user whose password is to be hashed.</param>
    /// <param name="password">The password to hash.</param>
    /// <returns>A hashed representation of the supplied <paramref name="password"/> for the specified <paramref name="user"/>.</returns>
    public virtual string HashPassword(TUser user, string password)
    {
        if (password == null)
        {
            throw new ArgumentNullException(nameof(password));
        }

        return _compatibilityMode switch
        {
            CustomPasswordHasherMethod.formulaOne => Convert.ToBase64String(HashPasswordV2(password)),
            CustomPasswordHasherMethod.formulaTwo => Convert.ToBase64String(HashPasswordV3(password)),
            CustomPasswordHasherMethod.formulaThree => Convert.ToBase64String(HashPasswordV4(password)),
            _ => throw new NotImplementedException()
        };
    }

    private byte[] HashPassword(CustomPasswordHasherMethod formatMarker, string password, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested)
    {
        var salt = new byte[saltSize];
        _rng.GetBytes(salt);

        var pepperIndex = 0;
        byte[] pepper = Array.Empty<byte>();
        if (formatMarker == CustomPasswordHasherMethod.formulaThree)
        {
            pepperIndex = _pepper.Keys.Max();
            pepper = _pepper[pepperIndex];
        }

        var saltAndPepper = salt.Concatenate(pepper);
        var subkey = KeyDerivation.Pbkdf2(password, saltAndPepper, prf, iterCount, numBytesRequested);

        var offset = sizeof(byte); //formatMarker;
        if (formatMarker != CustomPasswordHasherMethod.formulaOne)
        {
            offset += sizeof(uint); //prf
            offset += sizeof(uint); //Iterations
        }
        if (formatMarker == CustomPasswordHasherMethod.formulaThree)
        {
            offset += sizeof(uint); //Pepper Index
        }
        offset += sizeof(uint); //Salt Size
        offset += salt.Length; //Salt

        var outputBytes = new byte[offset + subkey.Length];
        outputBytes[0] = (byte)formatMarker;
        offset = sizeof(byte);
        if (formatMarker != CustomPasswordHasherMethod.formulaOne)
        {
            WriteNetworkByteOrder(outputBytes, offset, (uint)prf);
            offset += sizeof(uint);
            WriteNetworkByteOrder(outputBytes, offset, (uint)iterCount);
            offset += sizeof(uint);
        }
        if (formatMarker == CustomPasswordHasherMethod.formulaThree)
        {
            WriteNetworkByteOrder(outputBytes, offset, (uint)pepperIndex);
            offset += sizeof(uint);
        }
        WriteNetworkByteOrder(outputBytes, offset, (uint)saltSize);
        offset += sizeof(uint);

        Buffer.BlockCopy(salt, 0, outputBytes, offset, salt.Length);
        offset += salt.Length;

        Buffer.BlockCopy(subkey, 0, outputBytes, offset, subkey.Length);
        return outputBytes;
    }

    private byte[] HashPasswordV2(string password)
    {
        return HashPassword(CustomPasswordHasherMethod.formulaOne,
            password,
            prf: KeyDerivationPrf.HMACSHA1,
            iterCount: 1000,
            saltSize: 128 / 8,
            numBytesRequested: 256 / 8);
    }

    private byte[] HashPasswordV3(string password)
    {
        return HashPassword(CustomPasswordHasherMethod.formulaTwo,
            password,
            prf: KeyDerivationPrf.HMACSHA256,
            iterCount: _iterCount,
            saltSize: 128 / 8,
            numBytesRequested: 256 / 8);
    }

    private byte[] HashPasswordV4(string password)
    {
        return HashPassword(CustomPasswordHasherMethod.formulaThree,
            password,
            prf: KeyDerivationPrf.HMACSHA512,
            iterCount: _iterCount,
            saltSize: 128 / 8,
            numBytesRequested: 512 / 8);
    }

    private static uint ReadNetworkByteOrder(byte[] buffer, int offset)
    {
        return ((uint)(buffer[offset + 0]) << 24)
               | ((uint)(buffer[offset + 1]) << 16)
               | ((uint)(buffer[offset + 2]) << 8)
               | ((uint)(buffer[offset + 3]));
    }

    /// <summary>
    /// Returns a <see cref="PasswordVerificationResult"/> indicating the result of a password hash comparison.
    /// </summary>
    /// <param name="user">The user whose password should be verified.</param>
    /// <param name="hashedPassword">The hash value for a user's stored password.</param>
    /// <param name="providedPassword">The password supplied for comparison.</param>
    /// <returns>A <see cref="PasswordVerificationResult"/> indicating the result of a password hash comparison.</returns>
    /// <remarks>Implementations of this method should be time consistent.</remarks>
    public virtual PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword)
    {
        if (hashedPassword == null)
        {
            throw new ArgumentNullException(nameof(hashedPassword));
        }
        if (providedPassword == null)
        {
            throw new ArgumentNullException(nameof(providedPassword));
        }

        var decodedHashedPassword = Convert.FromBase64String(hashedPassword);

        // read the format marker from the hashed password
        if (decodedHashedPassword.Length == 0)
        {
            return PasswordVerificationResult.Failed;
        }

        var customPasswordHasherMethod = (CustomPasswordHasherMethod)decodedHashedPassword[0];
        int embeddedIterCount;

        switch (customPasswordHasherMethod)
        {
            case CustomPasswordHasherMethod.formulaOne:
                if (VerifyHashedPasswordV2(decodedHashedPassword, providedPassword))
                {
                    // This is an old password hash format - the caller needs to rehash if we're not running in an older compat mode.
                    return _compatibilityMode != customPasswordHasherMethod
                        ? PasswordVerificationResult.SuccessRehashNeeded
                        : PasswordVerificationResult.Success;
                }
                else
                {
                    return PasswordVerificationResult.Failed;
                }

            case CustomPasswordHasherMethod.formulaTwo:
                if (VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, out embeddedIterCount))
                {
                    // If this hasher was configured with a higher iteration count, change the entry now.
                    return embeddedIterCount < _iterCount || _compatibilityMode != customPasswordHasherMethod
                        ? PasswordVerificationResult.SuccessRehashNeeded
                        : PasswordVerificationResult.Success;
                }
                else
                {
                    return PasswordVerificationResult.Failed;
                }

            case CustomPasswordHasherMethod.formulaThree:
                if (VerifyHashedPasswordV4(decodedHashedPassword, providedPassword, out embeddedIterCount))
                {
                    //todo upgrade this code to require rehash if new pepper available.
                    // If this hasher was configured with a higher iteration count, change the entry now.
                    return (embeddedIterCount < _iterCount) || _compatibilityMode != customPasswordHasherMethod
                        ? PasswordVerificationResult.SuccessRehashNeeded
                        : PasswordVerificationResult.Success;
                }

                return PasswordVerificationResult.Failed;

            default:
                return PasswordVerificationResult.Failed; // unknown format marker
        }
    }

    private static bool VerifyHashedPasswordV2(byte[] hashedPassword, string password)
    {
        const KeyDerivationPrf Pbkdf2Prf = KeyDerivationPrf.HMACSHA1; // default for Rfc2898DeriveBytes
        const int Pbkdf2IterCount = 1000; // default for Rfc2898DeriveBytes
        const int Pbkdf2SubkeyLength = 256 / 8; // 256 bits
        const int SaltSize = 128 / 8; // 128 bits

        // We know ahead of time the exact length of a valid hashed password payload.
        if (hashedPassword.Length != 1 + SaltSize + Pbkdf2SubkeyLength)
        {
            return false; // bad size
        }

        var salt = new byte[SaltSize];
        Buffer.BlockCopy(hashedPassword, 1, salt, 0, salt.Length);

        var expectedSubkey = new byte[Pbkdf2SubkeyLength];
        Buffer.BlockCopy(hashedPassword, 1 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);

        // Hash the incoming password and verify it
        var actualSubkey = KeyDerivation.Pbkdf2(password, salt, Pbkdf2Prf, Pbkdf2IterCount, Pbkdf2SubkeyLength);
#if NETSTANDARD2_0
            return ByteArraysEqual(actualSubkey, expectedSubkey);
#elif NETCOREAPP
        return CryptographicOperations.FixedTimeEquals(actualSubkey, expectedSubkey);
#else
#error Update target frameworks
#endif
    }

    private static bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, out int iterCount)
    {
        iterCount = default;

        try
        {
            // Read header information
            var prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1);
            iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5);
            var saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9);

            // Read the salt: must be >= 128 bits
            if (saltLength < 128 / 8)
            {
                return false;
            }
            var salt = new byte[saltLength];
            Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length);

            // Read the subkey (the rest of the payload): must be >= 128 bits
            var subkeyLength = hashedPassword.Length - 13 - salt.Length;
            if (subkeyLength < 128 / 8)
            {
                return false;
            }
            var expectedSubkey = new byte[subkeyLength];
            Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);

            // Hash the incoming password and verify it
            var actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength);
#if NETSTANDARD2_0
                return ByteArraysEqual(actualSubkey, expectedSubkey);
#elif NETCOREAPP
            return CryptographicOperations.FixedTimeEquals(actualSubkey, expectedSubkey);
#else
#error Update target frameworks
#endif
        }
        catch
        {
            // This should never occur except in the case of a malformed payload, where
            // we might go off the end of the array. Regardless, a malformed payload
            // implies verification failed.
            return false;
        }
    }

    private bool VerifyHashedPasswordV4(byte[] hashedPassword, string password, out int iterationCount)
    {
        iterationCount = default;

        try
        {
            // Read header information
            var prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1);
            iterationCount = (int)ReadNetworkByteOrder(hashedPassword, 5);
            var pepperIndex = (int)ReadNetworkByteOrder(hashedPassword, 9);
            var saltLength = (int)ReadNetworkByteOrder(hashedPassword, 13);

            // Read the salt: must be >= 128 bits
            if (saltLength < 128 / 8)
            {
                return false;
            }
            var salt = new byte[saltLength];
            Buffer.BlockCopy(hashedPassword, 17, salt, 0, salt.Length);

            // Read the subkey (the rest of the payload): must be >= 128 bits
            var subkeyLength = hashedPassword.Length - 17 - salt.Length;
            if (subkeyLength < 128 / 8)
            {
                return false;
            }

            var expectedSubkey = new byte[subkeyLength];
            Buffer.BlockCopy(hashedPassword, 17 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);

            var pepper = _pepper[pepperIndex];

            var saltAndPepper = salt.Concatenate(pepper);

            // Hash the incoming password and verify it
            var actualSubKey = KeyDerivation.Pbkdf2(password, saltAndPepper, prf, iterationCount, subkeyLength);
#if NETSTANDARD2_0
                return ByteArraysEqual(actualSubkey, expectedSubkey);
#elif NETCOREAPP
            return CryptographicOperations.FixedTimeEquals(actualSubKey, expectedSubkey);
#else
#error Update target frameworks
#endif
        }
        catch
        {
            // This should never occur except in the case of a malformed payload, where
            // we might go off the end of the array. Regardless, a malformed payload
            // implies verification failed.
            return false;
        }
    }

    private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
    {
        buffer[offset + 0] = (byte)(value >> 24);
        buffer[offset + 1] = (byte)(value >> 16);
        buffer[offset + 2] = (byte)(value >> 8);
        buffer[offset + 3] = (byte)(value >> 0);
    }
}