using System.Text.Json;
using System.Text.Json.Serialization;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;

namespace ZB.MOM.NatsNet.Server;

internal delegate void JetStreamApiHandler(ClientConnection c, Account acc, string subject, string reply, byte[] message);

internal sealed class JetStreamApiSubscription
{
    public required string Subject { get; init; }
    public required JetStreamApiHandler Handler { get; init; }
}

internal sealed class JsApiRoutedRequest
{
    public required JetStreamApiSubscription Subscription { get; init; }
    public required Account Account { get; init; }
    public required string Subject { get; init; }
    public required string Reply { get; init; }
    public required byte[] Message { get; init; }
}

internal sealed class DelayedApiResponse
{
    public ClientInfo? ClientInfo { get; init; }
    public required Account Account { get; init; }
    public required string Subject { get; init; }
    public required string Reply { get; init; }
    public string Request { get; init; } = string.Empty;
    public byte[]? Header { get; init; }
    public required string Response { get; init; }
    public DateTime DeadlineUtc { get; init; }
    public bool RawResponse { get; init; }
    public DelayedApiResponse? Next { get; set; }
}

internal static class JetStreamApi
{
    internal static readonly TimeSpan ErrorResponseDelay = TimeSpan.FromMilliseconds(500);

    internal static Dictionary<string, string> GenerateJSMappingTable(string domain)
    {
        var mappings = new Dictionary<string, string>(StringComparer.Ordinal)
        {
            ["INFO"] = JsApiSubjects.JsApiAccountInfo,
            ["STREAM.>"] = "$JS.API.STREAM.>",
            ["CONSUMER.>"] = "$JS.API.CONSUMER.>",
            ["DIRECT.>"] = "$JS.API.DIRECT.>",
            ["META.>"] = "$JS.API.META.>",
            ["SERVER.>"] = "$JS.API.SERVER.>",
            ["ACCOUNT.>"] = "$JS.API.ACCOUNT.>",
            ["$KV.>"] = "$KV.>",
            ["$OBJ.>"] = "$OBJ.>",
        };

        var table = new Dictionary<string, string>(StringComparer.Ordinal);
        foreach (var (suffix, target) in mappings)
            table[$"$JS.{domain}.API.{suffix}"] = target;

        return table;
    }

    internal static void AddDelayedResponse(ref DelayedApiResponse? head, ref DelayedApiResponse? tail, DelayedApiResponse response)
    {
        if (head is null)
        {
            head = response;
            tail = response;
            return;
        }

        if (tail is not null && response.DeadlineUtc >= tail.DeadlineUtc)
        {
            tail.Next = response;
            tail = response;
            return;
        }

        DelayedApiResponse? previous = null;
        for (var current = head; current is not null; current = current.Next)
        {
            if (response.DeadlineUtc < current.DeadlineUtc)
            {
                response.Next = current;
                if (previous is null)
                    head = response;
                else
                    previous.Next = response;
                return;
            }

            previous = current;
        }

        if (tail is not null)
            tail.Next = response;
        tail = response;
    }

    internal static string StreamNameFromSubject(string subject) =>
        SubscriptionIndex.TokenAt(subject, 5);

    internal static string ConsumerNameFromSubject(string subject) =>
        SubscriptionIndex.TokenAt(subject, 6);

    internal static bool IsJSONObjectOrArray(ReadOnlySpan<byte> payload)
    {
        for (var i = 0; i < payload.Length; i++)
        {
            var ch = payload[i];
            if (ch is (byte)' ' or (byte)'\t' or (byte)'\r' or (byte)'\n')
                continue;

            return ch is (byte)'{' or (byte)'[';
        }

        return false;
    }

    internal static bool IsEmptyRequest(ReadOnlySpan<byte> payload)
    {
        if (payload.IsEmpty)
            return true;

        var start = 0;
        var end = payload.Length - 1;
        while (start < payload.Length && payload[start] is (byte)' ' or (byte)'\t' or (byte)'\r' or (byte)'\n')
            start++;
        while (end >= start && payload[end] is (byte)' ' or (byte)'\t' or (byte)'\r' or (byte)'\n')
            end--;
        if (end < start)
            return true;

        var trimmed = payload.Slice(start, end - start + 1);
        if (trimmed.SequenceEqual("{}"u8))
            return true;

        try
        {
            using var document = JsonDocument.Parse(trimmed.ToArray());
            return document.RootElement.ValueKind == JsonValueKind.Object &&
                   !document.RootElement.EnumerateObject().Any();
        }
        catch
        {
            return false;
        }
    }

    internal static bool SubjectMatches(string pattern, string subject)
    {
        var p = pattern.Split('.', StringSplitOptions.None);
        var s = subject.Split('.', StringSplitOptions.None);

        for (var i = 0; i < p.Length; i++)
        {
            if (i >= s.Length)
                return p[i] == ">";

            var token = p[i];
            if (token == ">")
                return true;
            if (token == "*")
                continue;
            if (!string.Equals(token, s[i], StringComparison.Ordinal))
                return false;
        }

        return p.Length == s.Length;
    }

    internal static T? DeserializeStrict<T>(byte[] payload, bool strictMode)
    {
        var strictOptions = new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true,
            UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
        };

        try
        {
            return JsonSerializer.Deserialize<T>(payload, strictOptions);
        }
        catch when (!strictMode)
        {
            return JsonSerializer.Deserialize<T>(payload);
        }
    }
}
