using System;
using System.Globalization;
using System.Security.Cryptography;

namespace StellaOps.Cryptography;

/// <summary>
/// Argon2id password hasher that emits PHC-compliant encoded strings.
/// </summary>
public sealed partial class Argon2idPasswordHasher : IPasswordHasher
{
    private const int SaltLengthBytes = 16;
    private const int HashLengthBytes = 32;

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

        options.Validate();

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

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

        var hash = DeriveHash(password, salt, options);

        return BuildEncodedHash(salt, hash, options);
    }

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

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

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

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

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

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

        if (!parsed.Options.Algorithm.Equals(desired.Algorithm))
        {
            return true;
        }

        return parsed.Options.MemorySizeInKib != desired.MemorySizeInKib
            || parsed.Options.Iterations != desired.Iterations
            || parsed.Options.Parallelism != desired.Parallelism;
    }

    private static byte[] DeriveHash(string password, ReadOnlySpan<byte> salt, PasswordHashOptions options)
        => DeriveHashCore(password, salt, options);

    private static partial byte[] DeriveHashCore(string password, ReadOnlySpan<byte> salt, PasswordHashOptions options);

    private static string BuildEncodedHash(ReadOnlySpan<byte> salt, ReadOnlySpan<byte> hash, PasswordHashOptions options)
    {
        var saltEncoded = Convert.ToBase64String(salt);
        var hashEncoded = Convert.ToBase64String(hash);

        return $"$argon2id$v=19$m={options.MemorySizeInKib},t={options.Iterations},p={options.Parallelism}${saltEncoded}${hashEncoded}";
    }

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

        if (!encodedHash.StartsWith("$argon2id$", StringComparison.Ordinal))
        {
            return false;
        }

        var segments = encodedHash.Split('$', StringSplitOptions.RemoveEmptyEntries);
        if (segments.Length != 5)
        {
            return false;
        }

        // segments: 0=argon2id, 1=v=19, 2=m=...,t=...,p=..., 3=salt, 4=hash
        if (!segments[1].StartsWith("v=19", StringComparison.Ordinal))
        {
            return false;
        }

        var parameterParts = segments[2].Split(',', StringSplitOptions.RemoveEmptyEntries);
        if (parameterParts.Length != 3)
        {
            return false;
        }

        if (!TryParseInt(parameterParts[0], "m", out var memory) ||
            !TryParseInt(parameterParts[1], "t", out var iterations) ||
            !TryParseInt(parameterParts[2], "p", out var parallelism))
        {
            return false;
        }

        byte[] saltBytes;
        byte[] hashBytes;
        try
        {
            saltBytes = Convert.FromBase64String(segments[3]);
            hashBytes = Convert.FromBase64String(segments[4]);
        }
        catch (FormatException)
        {
            return false;
        }

        if (saltBytes.Length != SaltLengthBytes || hashBytes.Length != HashLengthBytes)
        {
            return false;
        }

        var options = new PasswordHashOptions
        {
            Algorithm = PasswordHashAlgorithm.Argon2id,
            MemorySizeInKib = memory,
            Iterations = iterations,
            Parallelism = parallelism
        };

        parsed = new Argon2HashParameters(options, saltBytes, hashBytes);
        return true;
    }

    private static bool TryParseInt(string component, string key, out int value)
    {
        value = 0;
        if (!component.StartsWith(key + "=", StringComparison.Ordinal))
        {
            return false;
        }

        return int.TryParse(component.AsSpan(key.Length + 1), NumberStyles.None, CultureInfo.InvariantCulture, out value);
    }

    private readonly struct Argon2HashParameters
    {
        public Argon2HashParameters(PasswordHashOptions options, byte[] salt, byte[] hash)
        {
            Options = options;
            Salt = salt;
            Hash = hash;
        }

        public PasswordHashOptions Options { get; }
        public byte[] Salt { get; }
        public byte[] Hash { get; }
    }
}
