using System.Buffers.Binary;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;

namespace Eis.Infrastructure.Authentication;

internal sealed class TotpCompatibilityService(AuthenticationOptions options)
{
    private const string Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    private const string RecoveryAlphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
    private const long PeriodSeconds = 30;
    private readonly byte[] _encryptionKey = SHA256.HashData(Encoding.UTF8.GetBytes(options.TotpEncryptionMaterial));

    public string CreateSecret() => EncodeBase32(RandomNumberGenerator.GetBytes(20));

    public long? Verify(string? code, string secret, long? lastUsedStep = null, DateTimeOffset? now = null)
    {
        var normalized = string.Concat((code ?? string.Empty).Where(character => !char.IsWhiteSpace(character)));
        if (normalized.Length != 6 || normalized.Any(character => !char.IsAsciiDigit(character)))
        {
            return null;
        }

        var currentStep = (now ?? DateTimeOffset.UtcNow).ToUnixTimeSeconds() / PeriodSeconds;
        for (var offset = -1; offset <= 1; offset++)
        {
            var step = currentStep + offset;
            if (lastUsedStep is not null && step <= lastUsedStep.Value)
            {
                continue;
            }

            var expected = Encoding.ASCII.GetBytes(AtStep(secret, step));
            var supplied = Encoding.ASCII.GetBytes(normalized);
            if (CryptographicOperations.FixedTimeEquals(expected, supplied))
            {
                return step;
            }
        }

        return null;
    }

    public string AtStep(string secret, long step)
    {
        Span<byte> counter = stackalloc byte[8];
        BinaryPrimitives.WriteInt64BigEndian(counter, step);
        var digest = HMACSHA1.HashData(DecodeBase32(secret), counter);
        var offset = digest[^1] & 0x0f;
        var binary = (BinaryPrimitives.ReadInt32BigEndian(digest.AsSpan(offset, 4)) & 0x7fffffff) % 1_000_000;
        return binary.ToString("D6", CultureInfo.InvariantCulture);
    }

    public string BuildOtpAuthUri(string secret, string account, string issuer)
    {
        var label = Uri.EscapeDataString($"{issuer}:{account}");
        return $"otpauth://totp/{label}?secret={FormEncode(secret)}&issuer={FormEncode(issuer)}&algorithm=SHA1&digits=6&period=30";
    }

    public string EncryptSecret(string secret)
    {
        var nonce = RandomNumberGenerator.GetBytes(12);
        var plaintext = Encoding.UTF8.GetBytes(secret);
        var ciphertext = new byte[plaintext.Length];
        var tag = new byte[16];
        using var aes = new AesGcm(_encryptionKey, tag.Length);
        aes.Encrypt(nonce, plaintext, ciphertext, tag);
        return $"v1.{Base64UrlEncode(nonce)}.{Base64UrlEncode(tag)}.{Base64UrlEncode(ciphertext)}";
    }

    public string DecryptSecret(string value)
    {
        var parts = value.Split('.');
        if (parts.Length != 4 || parts[0] != "v1" || parts.Skip(1).Any(string.IsNullOrEmpty))
        {
            throw new CryptographicException("TOTP 密钥数据无效");
        }

        var nonce = Base64UrlDecode(parts[1]);
        var tag = Base64UrlDecode(parts[2]);
        var ciphertext = Base64UrlDecode(parts[3]);
        var plaintext = new byte[ciphertext.Length];
        using var aes = new AesGcm(_encryptionKey, tag.Length);
        aes.Decrypt(nonce, ciphertext, tag, plaintext);
        return Encoding.UTF8.GetString(plaintext);
    }

    public IReadOnlyList<string> CreateRecoveryCodes(int count = 8)
    {
        var output = new List<string>(count);
        for (var item = 0; item < count; item++)
        {
            var bytes = RandomNumberGenerator.GetBytes(10);
            var value = string.Concat(bytes.Select(value => RecoveryAlphabet[value % RecoveryAlphabet.Length]));
            output.Add($"{value[..5]}-{value[5..]}");
        }

        return output;
    }

    public string HashRecoveryCode(string? code)
    {
        var normalized = string.Concat((code ?? string.Empty)
            .ToUpperInvariant()
            .Where(char.IsAsciiLetterOrDigit));
        return Convert.ToHexStringLower(HMACSHA256.HashData(_encryptionKey, Encoding.UTF8.GetBytes(normalized)));
    }

    public IReadOnlyList<string>? ConsumeRecoveryCode(string? code, IReadOnlyList<string> hashes)
    {
        var candidate = Encoding.ASCII.GetBytes(HashRecoveryCode(code));
        for (var index = 0; index < hashes.Count; index++)
        {
            var stored = Encoding.ASCII.GetBytes(hashes[index] ?? string.Empty);
            if (stored.Length == candidate.Length && CryptographicOperations.FixedTimeEquals(stored, candidate))
            {
                return hashes.Where((_, itemIndex) => itemIndex != index).ToArray();
            }
        }

        return null;
    }

    private static string FormEncode(string value) => Uri.EscapeDataString(value).Replace("%20", "+", StringComparison.Ordinal);

    private static string EncodeBase32(ReadOnlySpan<byte> bytes)
    {
        var output = new StringBuilder((bytes.Length * 8 + 4) / 5);
        var buffer = 0;
        var bits = 0;
        foreach (var value in bytes)
        {
            buffer = (buffer << 8) | value;
            bits += 8;
            while (bits >= 5)
            {
                bits -= 5;
                output.Append(Base32Alphabet[(buffer >> bits) & 31]);
            }
        }

        if (bits > 0)
        {
            output.Append(Base32Alphabet[(buffer << (5 - bits)) & 31]);
        }

        return output.ToString();
    }

    private static byte[] DecodeBase32(string value)
    {
        var normalized = string.Concat(value.ToUpperInvariant().Where(character => Base32Alphabet.Contains(character)));
        var output = new List<byte>();
        var buffer = 0;
        var bits = 0;
        foreach (var character in normalized)
        {
            var index = Base32Alphabet.IndexOf(character, StringComparison.Ordinal);
            if (index < 0)
            {
                throw new FormatException("TOTP 密钥格式无效");
            }

            buffer = (buffer << 5) | index;
            bits += 5;
            if (bits >= 8)
            {
                bits -= 8;
                output.Add((byte)((buffer >> bits) & 0xff));
            }
        }

        return output.ToArray();
    }

    private static string Base64UrlEncode(byte[] value) => Convert.ToBase64String(value)
        .TrimEnd('=')
        .Replace('+', '-')
        .Replace('/', '_');

    private static byte[] Base64UrlDecode(string value)
    {
        var padded = value.Replace('-', '+').Replace('_', '/');
        padded += new string('=', (4 - padded.Length % 4) % 4);
        return Convert.FromBase64String(padded);
    }
}
