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

namespace StellaOps.Cryptography;

/// <summary>
/// PBKDF2-SHA256 password hasher for legacy credentials.
/// </summary>
public sealed class Pbkdf2PasswordHasher : IPasswordHasher
{
    private const int SaltLengthBytes = 16;
    private const int HashLengthBytes = 32;
    private const string Prefix = "PBKDF2";

    public string Hash(string password, PasswordHashOptions options)
    {
        ArgumentException.ThrowIfNullOrEmpty(password);
        ArgumentNullException.ThrowIfNull(options);

        if (options.Algorithm != PasswordHashAlgorithm.Pbkdf2)
        {
            throw new InvalidOperationException("Pbkdf2PasswordHasher only supports the PBKDF2 algorithm.");
        }

        if (options.Iterations <= 0)
        {
            throw new InvalidOperationException("PBKDF2 requires a positive iteration count.");
        }

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

        var hash = Derive(password, salt, options.Iterations);

        var payload = new byte[1 + SaltLengthBytes + HashLengthBytes];
        payload[0] = 0x01;
        salt.CopyTo(payload.AsSpan(1));
        hash.CopyTo(payload.AsSpan(1 + SaltLengthBytes));

        return $"{Prefix}.{options.Iterations}.{Convert.ToBase64String(payload)}";
    }

    public bool Verify(string password, string encodedHash)
    {
        ArgumentException.ThrowIfNullOrEmpty(password);
        ArgumentException.ThrowIfNullOrEmpty(encodedHash);

        if (!TryParse(encodedHash, out var parsed))
        {
            return false;
        }

        var computed = Derive(password, parsed.Salt, parsed.Iterations);
        return CryptographicOperations.FixedTimeEquals(parsed.Hash, computed);
    }

    public bool NeedsRehash(string encodedHash, PasswordHashOptions desired)
    {
        ArgumentNullException.ThrowIfNull(desired);

        if (!TryParse(encodedHash, out var parsed))
        {
            return true;
        }

        if (desired.Algorithm != PasswordHashAlgorithm.Pbkdf2)
        {
            return true;
        }

        return parsed.Iterations != desired.Iterations;
    }

    private static byte[] Derive(string password, ReadOnlySpan<byte> salt, int iterations)
    {
        return Rfc2898DeriveBytes.Pbkdf2(
            Encoding.UTF8.GetBytes(password),
            salt.ToArray(),
            iterations,
            HashAlgorithmName.SHA256,
            HashLengthBytes);
    }

    private static bool TryParse(string encodedHash, out Pbkdf2Parameters parsed)
    {
        parsed = default;

        var parts = encodedHash.Split('.', StringSplitOptions.RemoveEmptyEntries);
        if (parts.Length != 3 || !string.Equals(parts[0], Prefix, StringComparison.Ordinal))
        {
            return false;
        }

        if (!int.TryParse(parts[1], out var iterations) || iterations <= 0)
        {
            return false;
        }

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

        if (payload.Length != 1 + SaltLengthBytes + HashLengthBytes || payload[0] != 0x01)
        {
            return false;
        }

        var salt = new byte[SaltLengthBytes];
        var hash = new byte[HashLengthBytes];
        Array.Copy(payload, 1, salt, 0, SaltLengthBytes);
        Array.Copy(payload, 1 + SaltLengthBytes, hash, 0, HashLengthBytes);

        parsed = new Pbkdf2Parameters(iterations, salt, hash);
        return true;
    }

    private readonly struct Pbkdf2Parameters
    {
        public Pbkdf2Parameters(int iterations, byte[] salt, byte[] hash)
        {
            Iterations = iterations;
            Salt = salt;
            Hash = hash;
        }

        public int Iterations { get; }
        public byte[] Salt { get; }
        public byte[] Hash { get; }
    }
}
