using System;
using StellaOps.Cryptography;

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 CryptoPasswordHasher : IPasswordHasher
{
    private readonly StandardPluginOptions options;
    private readonly ICryptoProvider cryptoProvider;

    public CryptoPasswordHasher(StandardPluginOptions options, ICryptoProvider cryptoProvider)
    {
        this.options = options ?? throw new ArgumentNullException(nameof(options));
        this.cryptoProvider = cryptoProvider ?? throw new ArgumentNullException(nameof(cryptoProvider));
    }

    public string Hash(string password)
    {
        ArgumentException.ThrowIfNullOrEmpty(password);

        var hashOptions = options.PasswordHashing;
        hashOptions.Validate();

        var hasher = cryptoProvider.GetPasswordHasher(hashOptions.Algorithm.ToAlgorithmId());
        return hasher.Hash(password, hashOptions);
    }

    public PasswordVerificationResult Verify(string password, string hashedPassword)
    {
        ArgumentException.ThrowIfNullOrEmpty(password);
        ArgumentException.ThrowIfNullOrEmpty(hashedPassword);

        var desired = options.PasswordHashing;
        desired.Validate();

        var primaryHasher = cryptoProvider.GetPasswordHasher(desired.Algorithm.ToAlgorithmId());

        if (IsArgon2Hash(hashedPassword))
        {
            if (!primaryHasher.Verify(password, hashedPassword))
            {
                return PasswordVerificationResult.Failed;
            }

            return primaryHasher.NeedsRehash(hashedPassword, desired)
                ? PasswordVerificationResult.SuccessRehashNeeded
                : PasswordVerificationResult.Success;
        }

        if (IsLegacyPbkdf2Hash(hashedPassword))
        {
            var legacyHasher = cryptoProvider.GetPasswordHasher(PasswordHashAlgorithm.Pbkdf2.ToAlgorithmId());
            if (!legacyHasher.Verify(password, hashedPassword))
            {
                return PasswordVerificationResult.Failed;
            }

            return desired.Algorithm == PasswordHashAlgorithm.Pbkdf2 &&
                   !legacyHasher.NeedsRehash(hashedPassword, desired)
                ? PasswordVerificationResult.Success
                : PasswordVerificationResult.SuccessRehashNeeded;
        }

        return PasswordVerificationResult.Failed;
    }

    private static bool IsArgon2Hash(string value) =>
        value.StartsWith("$argon2id$", StringComparison.Ordinal);

    private static bool IsLegacyPbkdf2Hash(string value) =>
        value.StartsWith("PBKDF2.", StringComparison.Ordinal);
}
