using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.WebUtilities;

namespace Jiaowu.Api.Infrastructure.Teaching;

public static class AttendanceCheckInChallenge
{
    public const int LifetimeSeconds = 20;
    public const int RefreshSeconds = 10;
    private const int MaximumClockSkewSeconds = 2;
    private const string Base36Digits = "0123456789abcdefghijklmnopqrstuvwxyz";

    public static AttendanceCheckInChallengeResult Create(
        Guid attendanceSheetId,
        string secret,
        DateTime nowUtc)
    {
        var issuedAt = new DateTimeOffset(
            DateTime.SpecifyKind(nowUtc, DateTimeKind.Utc));
        var payload = CreatePayload(
            attendanceSheetId,
            ToBase36(issuedAt.ToUnixTimeSeconds()));
        var signature = CreateSignature(payload, secret);
        return new AttendanceCheckInChallengeResult(
            $"{payload}.{signature}",
            issuedAt.UtcDateTime,
            issuedAt.AddSeconds(LifetimeSeconds).UtcDateTime,
            issuedAt.AddSeconds(RefreshSeconds).UtcDateTime);
    }

    public static bool TryReadSheetId(string? token, out Guid attendanceSheetId)
    {
        attendanceSheetId = Guid.Empty;
        if (!TryParse(token, out var sheetIdText, out _, out _))
            return false;
        return Guid.TryParseExact(sheetIdText, "N", out attendanceSheetId);
    }

    public static bool IsValid(
        string? token,
        Guid attendanceSheetId,
        string? secret,
        DateTime nowUtc)
    {
        if (string.IsNullOrWhiteSpace(secret) ||
            !TryParse(token, out var sheetIdText, out var issuedAtText, out var signature) ||
            !Guid.TryParseExact(sheetIdText, "N", out var tokenSheetId) ||
            tokenSheetId != attendanceSheetId ||
            !TryParseBase36(issuedAtText, out var issuedAtUnixSeconds))
        {
            return false;
        }

        var nowUnixSeconds = new DateTimeOffset(
            DateTime.SpecifyKind(nowUtc, DateTimeKind.Utc)).ToUnixTimeSeconds();
        var ageSeconds = nowUnixSeconds - issuedAtUnixSeconds;
        if (ageSeconds < -MaximumClockSkewSeconds || ageSeconds > LifetimeSeconds)
            return false;

        byte[] providedSignature;
        try
        {
            providedSignature = WebEncoders.Base64UrlDecode(signature);
        }
        catch (FormatException)
        {
            return false;
        }

        var expectedSignature = WebEncoders.Base64UrlDecode(
            CreateSignature(
                CreatePayload(attendanceSheetId, issuedAtText),
                secret));
        return CryptographicOperations.FixedTimeEquals(
            providedSignature,
            expectedSignature);
    }

    private static string CreatePayload(Guid attendanceSheetId, string issuedAtText) =>
        $"{attendanceSheetId:N}.{issuedAtText}";

    private static string CreateSignature(string payload, string secret)
    {
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        var digest = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
        return WebEncoders.Base64UrlEncode(digest[..16]);
    }

    private static bool TryParse(
        string? token,
        out string sheetIdText,
        out string issuedAtText,
        out string signature)
    {
        sheetIdText = string.Empty;
        issuedAtText = string.Empty;
        signature = string.Empty;
        if (string.IsNullOrWhiteSpace(token) || token.Length > 64)
            return false;

        var parts = token.Split('.');
        if (parts.Length != 3 ||
            parts[0].Length != 32 ||
            parts[1].Length is < 1 or > 12 ||
            parts[2].Length != 22)
        {
            return false;
        }

        sheetIdText = parts[0];
        issuedAtText = parts[1];
        signature = parts[2];
        return true;
    }

    private static string ToBase36(long value)
    {
        if (value == 0) return "0";
        Span<char> buffer = stackalloc char[13];
        var index = buffer.Length;
        while (value > 0)
        {
            buffer[--index] = Base36Digits[(int)(value % 36)];
            value /= 36;
        }
        return new string(buffer[index..]);
    }

    private static bool TryParseBase36(string value, out long result)
    {
        result = 0;
        foreach (var character in value)
        {
            var digit = Base36Digits.IndexOf(
                char.ToLower(character, CultureInfo.InvariantCulture));
            if (digit < 0) return false;
            try
            {
                result = checked(result * 36 + digit);
            }
            catch (OverflowException)
            {
                return false;
            }
        }
        return true;
    }
}

public sealed record AttendanceCheckInChallengeResult(
    string Token,
    DateTime IssuedAt,
    DateTime ExpiresAt,
    DateTime RefreshAt);
