using System.Globalization;
using System.Text;

namespace Nexus.Api.Services;

internal readonly record struct TaskBoardCursorPosition(
    DateTimeOffset UpdatedAt,
    Guid Id);

internal static class TaskBoardCursorCodec
{
    private const string Version = "v1";
    private const int MaximumEncodedLength = 128;

    public static string Encode(DateTimeOffset updatedAt, Guid id)
    {
        var payload = string.Create(
            CultureInfo.InvariantCulture,
            $"{Version}|{updatedAt.UtcDateTime.Ticks}|{id:N}");

        return Convert.ToBase64String(Encoding.UTF8.GetBytes(payload))
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
    }

    public static bool TryDecode(string? cursor, out TaskBoardCursorPosition position)
    {
        position = default;
        if (string.IsNullOrWhiteSpace(cursor) || cursor.Length > MaximumEncodedLength)
            return false;

        try
        {
            var normalized = cursor
                .Replace('-', '+')
                .Replace('_', '/');

            normalized = (normalized.Length % 4) switch
            {
                0 => normalized,
                2 => normalized + "==",
                3 => normalized + "=",
                _ => throw new FormatException("Invalid Base64Url length.")
            };

            var payload = Encoding.UTF8.GetString(Convert.FromBase64String(normalized));
            var parts = payload.Split('|');
            if (parts.Length != 3
                || !string.Equals(parts[0], Version, StringComparison.Ordinal)
                || !long.TryParse(parts[1], NumberStyles.None, CultureInfo.InvariantCulture, out var utcTicks)
                || !Guid.TryParseExact(parts[2], "N", out var id))
            {
                return false;
            }

            var updatedAt = new DateTimeOffset(utcTicks, TimeSpan.Zero);
            position = new TaskBoardCursorPosition(updatedAt, id);
            return true;
        }
        catch (Exception exception) when (
            exception is FormatException
            or ArgumentOutOfRangeException
            or DecoderFallbackException)
        {
            return false;
        }
    }
}

public sealed class InvalidTaskBoardCursorException()
    : FormatException("The Done cursor is invalid or unsupported.");
