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

namespace PlotLine.Services;

public sealed class TwoFactorService(ILogger<TwoFactorService> logger) : ITwoFactorService
{
    private const string Issuer = "PlotDirector";
    private const int CodeDigits = 6;
    private const int TimeStepSeconds = 30;
    private const string Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

    public string GenerateSecretKey()
    {
        var bytes = RandomNumberGenerator.GetBytes(20);
        return ToBase32(bytes);
    }

    public string GenerateQrCodeUri(string email, string secretKey)
    {
        var issuer = Uri.EscapeDataString(Issuer);
        var account = Uri.EscapeDataString($"{Issuer}:{email}");
        return $"otpauth://totp/{account}?secret={secretKey}&issuer={issuer}";
    }

    public bool ValidateCode(string secretKey, string code)
    {
        return ValidateCode(secretKey, code, lastUsedTimeStep: null, out _);
    }

    public bool ValidateCode(string secretKey, string code, long? lastUsedTimeStep, out long matchedTimeStep)
    {
        matchedTimeStep = 0;
        var cleanCode = CleanCode(code);
        if (cleanCode.Length != CodeDigits || cleanCode.Any(c => !char.IsDigit(c)))
        {
            return false;
        }

        var secretBytes = FromBase32(secretKey);
        var currentStep = GetCurrentTimeStep();
        for (var offset = -1; offset <= 1; offset++)
        {
            var step = currentStep + offset;
            if (lastUsedTimeStep.HasValue && step <= lastUsedTimeStep.Value)
            {
                continue;
            }

            if (GenerateTotp(secretBytes, step) == cleanCode)
            {
                matchedTimeStep = step;
                return true;
            }
        }

        return false;
    }

    public string? GenerateQrCodeDataUri(string qrCodeUri)
    {
        try
        {
            using var generator = new QRCodeGenerator();
            using var data = generator.CreateQrCode(qrCodeUri, QRCodeGenerator.ECCLevel.Q);
            var qrCode = new PngByteQRCode(data);
            var bytes = qrCode.GetGraphic(8);
            return $"data:image/png;base64,{Convert.ToBase64String(bytes)}";
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "QR code generation failed for two-factor setup.");
            return null;
        }
    }

    public IReadOnlyList<string> GenerateRecoveryCodes(int count)
    {
        var codes = new List<string>(count);
        for (var i = 0; i < count; i++)
        {
            codes.Add(GenerateRecoveryCode());
        }

        return codes;
    }

    private static long GetCurrentTimeStep()
    {
        return DateTimeOffset.UtcNow.ToUnixTimeSeconds() / TimeStepSeconds;
    }

    private static string GenerateTotp(byte[] secretBytes, long timeStep)
    {
        Span<byte> counter = stackalloc byte[8];
        BinaryPrimitives.WriteInt64BigEndian(counter, timeStep);

        using var hmac = new HMACSHA1(secretBytes);
        var hash = hmac.ComputeHash(counter.ToArray());
        var offset = hash[^1] & 0x0f;
        var binaryCode = ((hash[offset] & 0x7f) << 24)
            | ((hash[offset + 1] & 0xff) << 16)
            | ((hash[offset + 2] & 0xff) << 8)
            | (hash[offset + 3] & 0xff);

        var otp = binaryCode % 1_000_000;
        return otp.ToString("D6");
    }

    private static string GenerateRecoveryCode()
    {
        const string alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
        var bytes = RandomNumberGenerator.GetBytes(10);
        var builder = new StringBuilder(10);
        for (var i = 0; i < 10; i++)
        {
            builder.Append(alphabet[bytes[i] % alphabet.Length]);
        }

        return builder.ToString();
    }

    private static string ToBase32(byte[] bytes)
    {
        var output = new StringBuilder((bytes.Length + 4) / 5 * 8);
        var buffer = 0;
        var bitsLeft = 0;

        foreach (var b in bytes)
        {
            buffer = (buffer << 8) | b;
            bitsLeft += 8;
            while (bitsLeft >= 5)
            {
                output.Append(Base32Alphabet[(buffer >> (bitsLeft - 5)) & 31]);
                bitsLeft -= 5;
            }
        }

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

        return output.ToString();
    }

    private static byte[] FromBase32(string value)
    {
        var clean = value.Trim().Replace(" ", string.Empty).TrimEnd('=').ToUpperInvariant();
        var bytes = new List<byte>();
        var buffer = 0;
        var bitsLeft = 0;

        foreach (var c in clean)
        {
            var index = Base32Alphabet.IndexOf(c);
            if (index < 0)
            {
                throw new FormatException("The two-factor secret is not valid Base32.");
            }

            buffer = (buffer << 5) | index;
            bitsLeft += 5;
            if (bitsLeft >= 8)
            {
                bytes.Add((byte)((buffer >> (bitsLeft - 8)) & 255));
                bitsLeft -= 8;
            }
        }

        return bytes.ToArray();
    }

    private static string CleanCode(string code)
    {
        return code.Trim().Replace(" ", string.Empty).Replace("-", string.Empty);
    }
}
