using System.Security.Cryptography;

namespace AuthorBuddy.Web.Services;

public static class TotpHelper
{
    private const int TotpSize = 6;
    private const int TotpStepSeconds = 30;
    private const int WindowSize = 1;

    public static string GenerateSecret()
    {
        var secret = RandomNumberGenerator.GetBytes(20);
        return Base32Encode(secret);
    }

    public static string GetProvisioningUri(string secret, string username, string issuer = "AuthorBuddy")
    {
        return $"otpauth://totp/{Uri.EscapeDataString(issuer)}:{Uri.EscapeDataString(username)}" +
               $"?secret={secret}&issuer={Uri.EscapeDataString(issuer)}&algorithm=SHA1&digits={TotpSize}&period={TotpStepSeconds}";
    }

    public static bool VerifyCode(string secret, string code)
    {
        if (string.IsNullOrWhiteSpace(code) || code.Length != TotpSize) return false;
        if (!int.TryParse(code, out _)) return false;

        var secretBytes = Base32Decode(secret);
        var counter = GetCurrentCounter();

        for (int i = -WindowSize; i <= WindowSize; i++)
        {
            var expected = GenerateTotp(secretBytes, counter + i);
            if (expected == code) return true;
        }
        return false;
    }

    private static long GetCurrentCounter()
    {
        var unix = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        return unix / TotpStepSeconds;
    }

    private static string GenerateTotp(byte[] secret, long counter)
    {
        var counterBytes = BitConverter.GetBytes(counter);
        if (BitConverter.IsLittleEndian) Array.Reverse(counterBytes);

        byte[] hash;
        using (var hmac = new HMACSHA1(secret))
        {
            hash = hmac.ComputeHash(counterBytes);
        }

        int offset = hash[^1] & 0xf;
        int binary = ((hash[offset] & 0x7f) << 24) |
                     ((hash[offset + 1] & 0xff) << 16) |
                     ((hash[offset + 2] & 0xff) << 8) |
                     (hash[offset + 3] & 0xff);

        int otp = binary % (int)Math.Pow(10, TotpSize);
        return otp.ToString($"D{TotpSize}");
    }

    private static string Base32Encode(byte[] data)
    {
        const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
        int bitCount = 0;
        int currentByte = 0;
        var result = new System.Text.StringBuilder();

        foreach (var b in data)
        {
            currentByte = (currentByte << 8) | b;
            bitCount += 8;

            while (bitCount >= 5)
            {
                bitCount -= 5;
                int index = (currentByte >> bitCount) & 0x1f;
                result.Append(alphabet[index]);
            }
        }

        if (bitCount > 0)
        {
            int index = (currentByte << (5 - bitCount)) & 0x1f;
            result.Append(alphabet[index]);
        }

        return result.ToString();
    }

    private static byte[] Base32Decode(string base32)
    {
        const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
        base32 = base32.Trim().ToUpperInvariant()
            .Replace('0', 'O').Replace('1', 'I').Replace('8', 'B');

        int bitCount = 0;
        int currentByte = 0;
        var bytes = new System.Collections.Generic.List<byte>();

        foreach (var c in base32)
        {
            int index = alphabet.IndexOf(c);
            if (index < 0) continue;

            currentByte = (currentByte << 5) | index;
            bitCount += 5;

            if (bitCount >= 8)
            {
                bitCount -= 8;
                bytes.Add((byte)((currentByte >> bitCount) & 0xff));
            }
        }

        return [.. bytes];
    }

    public static string HashPassword(string password)
    {
        var salt = RandomNumberGenerator.GetBytes(16);
        var hash = Rfc2898DeriveBytes.Pbkdf2(
            password,
            salt,
            100_000,
            HashAlgorithmName.SHA256,
            32);
        return $"{Convert.ToBase64String(salt)}.{Convert.ToBase64String(hash)}";
    }

    public static bool VerifyPassword(string password, string hashedPassword)
    {
        if (string.IsNullOrWhiteSpace(hashedPassword)) return false;
        var parts = hashedPassword.Split('.');
        if (parts.Length != 2) return false;

        try
        {
            var salt = Convert.FromBase64String(parts[0]);
            var storedHash = Convert.FromBase64String(parts[1]);
            var computedHash = Rfc2898DeriveBytes.Pbkdf2(
                password,
                salt,
                100_000,
                HashAlgorithmName.SHA256,
                32);
            return CryptographicOperations.FixedTimeEquals(storedHash, computedHash);
        }
        catch
        {
            return false;
        }
    }
}
