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

namespace StellaOps.Authority.Plugin.Standard.Security;

internal interface IPasswordHasher
{
    string Hash(string password);

    PasswordVerificationResult Verify(string password, string hashedPassword);
}

internal enum PasswordVerificationResult
{
    Failed,
    Success,
    SuccessRehashNeeded
}

internal sealed class Pbkdf2PasswordHasher : IPasswordHasher
{
    private const int SaltSize = 16;
    private const int HashSize = 32;
    private const int Iterations = 210_000;
    private const string Header = "PBKDF2";

    public string Hash(string password)
    {
        if (string.IsNullOrEmpty(password))
        {
            throw new ArgumentException("Password is required.", nameof(password));
        }

        Span<byte> salt = stackalloc byte[SaltSize];
        RandomNumberGenerator.Fill(salt);

        Span<byte> hash = stackalloc byte[HashSize];
        var derived = Rfc2898DeriveBytes.Pbkdf2(password, salt.ToArray(), Iterations, HashAlgorithmName.SHA256, HashSize);
        derived.CopyTo(hash);

        var payload = new byte[1 + SaltSize + HashSize];
        payload[0] = 0x01; // version
        salt.CopyTo(payload.AsSpan(1));
        hash.CopyTo(payload.AsSpan(1 + SaltSize));

        var builder = new StringBuilder();
        builder.Append(Header);
        builder.Append('.');
        builder.Append(Iterations);
        builder.Append('.');
        builder.Append(Convert.ToBase64String(payload));
        return builder.ToString();
    }

    public PasswordVerificationResult Verify(string password, string hashedPassword)
    {
        if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(hashedPassword))
        {
            return PasswordVerificationResult.Failed;
        }

        var parts = hashedPassword.Split('.', StringSplitOptions.RemoveEmptyEntries);
        if (parts.Length != 3 || !string.Equals(parts[0], Header, StringComparison.Ordinal))
        {
            return PasswordVerificationResult.Failed;
        }

        if (!int.TryParse(parts[1], out var iterations))
        {
            return PasswordVerificationResult.Failed;
        }

        byte[] payload;
        try
        {
            payload = Convert.FromBase64String(parts[2]);
        }
        catch (FormatException)
        {
            return PasswordVerificationResult.Failed;
        }

        if (payload.Length != 1 + SaltSize + HashSize)
        {
            return PasswordVerificationResult.Failed;
        }

        var version = payload[0];
        if (version != 0x01)
        {
            return PasswordVerificationResult.Failed;
        }

        var salt = new byte[SaltSize];
        Array.Copy(payload, 1, salt, 0, SaltSize);

        var expectedHash = new byte[HashSize];
        Array.Copy(payload, 1 + SaltSize, expectedHash, 0, HashSize);

        var actualHash = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, HashSize);

        var success = CryptographicOperations.FixedTimeEquals(expectedHash, actualHash);
        if (!success)
        {
            return PasswordVerificationResult.Failed;
        }

        return iterations < Iterations
            ? PasswordVerificationResult.SuccessRehashNeeded
            : PasswordVerificationResult.Success;
    }
}
