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

namespace ZB.MOM.NatsNet.Server;

public sealed partial class NatsServer
{
    private readonly Lock _jsApiSubsLock = new();
    private readonly List<JetStreamApiSubscription> _jsApiSubscriptions = [];
    private IpQueue<JsApiRoutedRequest>? _jsApiRoutedReqs;
    private IpQueue<DelayedApiResponse>? _delayedApiResponses;

    internal void ProcessJSAPIRoutedRequests()
    {
        var queue = _jsApiRoutedReqs;
        if (queue is null)
            return;

        var client = CreateInternalJetStreamClient();
        var token = _quitCts.Token;

        while (!token.IsCancellationRequested)
        {
            try
            {
                if (!queue.Ch.WaitToReadAsync(token).AsTask().GetAwaiter().GetResult())
                    break;

                while (true)
                {
                    var (request, ok) = queue.PopOne();
                    if (!ok)
                        break;

                    var start = DateTime.UtcNow;
                    request.Subscription.Handler(client, request.Account, request.Subject, request.Reply, request.Message);
                    var elapsed = DateTime.UtcNow - start;
                    if (elapsed >= TimeSpan.FromSeconds(1))
                        Warnf("Internal subscription on '{0}' took too long: {1}", request.Subject, elapsed);

                    var js = GetJetStreamState();
                    if (js is not null)
                        Interlocked.Add(ref js.ApiInflight, -1);
                }
            }
            catch (OperationCanceledException)
            {
                return;
            }
            catch (Exception ex)
            {
                Warnf("JetStream routed API worker failed: {0}", ex.Message);
            }
        }
    }

    internal Exception? SetJetStreamExportSubs()
    {
        var js = GetJetStreamState();
        if (js is null)
            return new InvalidOperationException(JsApiErrors.NewJSNotEnabledError().Description ?? "jetstream not enabled");

        var workers = Math.Min(Math.Max(Environment.ProcessorCount, 1), 16);

        _jsApiRoutedReqs = IpQueue<JsApiRoutedRequest>.NewIPQueue("Routed JS API Requests");
        _delayedApiResponses = IpQueue<DelayedApiResponse>.NewIPQueue("Delayed JS API Responses");
        Interlocked.Exchange(ref js.QueueLimit, JsApiSubjects.JsDefaultRequestQueueLimit);

        for (var i = 0; i < workers; i++)
            StartGoRoutine(ProcessJSAPIRoutedRequests);

        StartGoRoutine(DelayedAPIResponder);

        lock (_jsApiSubsLock)
        {
            _jsApiSubscriptions.Clear();
            _jsApiSubscriptions.AddRange(
            [
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiAccountInfo, Handler = JsAccountInfoRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiStreamCreate, Handler = JsStreamCreateRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiStreamUpdate, Handler = JsStreamUpdateRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiStreams, Handler = JsStreamNamesRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiStreamList, Handler = JsStreamListRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiStreamInfo, Handler = JsStreamInfoRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiStreamDelete, Handler = JsStreamDeleteRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiMsgDelete, Handler = JsMsgDeleteRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiMsgGet, Handler = JsMsgGetRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiStreamPurge, Handler = JsStreamPurgeRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiStreamRestore, Handler = JsStreamRestoreRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiStreamSnapshot, Handler = JsStreamSnapshotRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiConsumerCreateEx, Handler = JsConsumerCreateRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiConsumerCreate, Handler = JsConsumerCreateRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiDurableCreate, Handler = JsConsumerCreateRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiConsumers, Handler = JsConsumerNamesRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiConsumerList, Handler = JsConsumerListRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiConsumerInfo, Handler = JsConsumerInfoRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiConsumerDelete, Handler = JsConsumerDeleteRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiConsumerPause, Handler = JsConsumerPauseRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiConsumerUnpin, Handler = JsConsumerUnpinRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiStreamLeaderStepDown, Handler = JsStreamLeaderStepDownRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiConsumerLeaderStepDown, Handler = JsConsumerLeaderStepDownRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiStreamRemovePeer, Handler = JsStreamRemovePeerRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiRemoveServer, Handler = JsLeaderServerRemoveRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiServerStreamMove, Handler = JsLeaderServerStreamMoveRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiServerStreamCancelMove, Handler = JsLeaderServerStreamCancelMoveRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiAccountPurge, Handler = JsLeaderAccountPurgeRequest },
                new JetStreamApiSubscription { Subject = JsApiSubjects.JsApiLeaderStepDown, Handler = JsLeaderStepDownRequest },
            ]);
        }

        var sys = SystemAccount();
        if (sys is null)
            return null;

        var err = sys.AddServiceExport(JsApiSubjects.JsAllApi, null);
        if (err is not null)
            Warnf("Error setting up jetstream service exports: {0}", err.Message);
        return err;
    }

    internal JetStreamApiSubscription? MatchJetStreamApiSubscription(string subject)
    {
        lock (_jsApiSubsLock)
        {
            foreach (var sub in _jsApiSubscriptions)
            {
                if (JetStreamApi.SubjectMatches(sub.Subject, subject))
                    return sub;
            }
        }

        return null;
    }

    internal (bool Queued, long Dropped) EnqueueJSApiRequest(JetStream js, JsApiRoutedRequest request)
    {
        var queue = _jsApiRoutedReqs;
        if (queue is null)
            return (false, 0);

        var (pending, error) = queue.Push(request);
        var limit = Interlocked.Read(ref js.QueueLimit);
        if (limit <= 0)
            limit = JsApiSubjects.JsDefaultRequestQueueLimit;

        if (error is null && pending < limit)
            return (true, 0);

        RateLimitFormatWarnf("JetStream API queue limit reached, dropping {0} requests", pending);
        var drained = queue.Drain();
        if (drained > 0)
            Interlocked.Add(ref js.ApiInflight, -drained);

        return (false, drained);
    }

    internal void SendAPIResponse(ClientInfo? ci, Account acc, string subject, string reply, string request, string response)
    {
        acc.TrackAPI();
        if (!string.IsNullOrWhiteSpace(reply))
            _ = SendInternalAccountMsgWithReply(acc, reply, string.Empty, null, response, false);

        SendJetStreamAPIAuditAdvisory(ci, acc, subject, request, response);
    }

    internal void SendAPIErrResponse(ClientInfo? ci, Account acc, string subject, string reply, string request, string response)
    {
        acc.TrackAPIErr();
        if (!string.IsNullOrWhiteSpace(reply))
            _ = SendInternalAccountMsgWithReply(acc, reply, string.Empty, null, response, false);

        SendJetStreamAPIAuditAdvisory(ci, acc, subject, request, response);
    }

    internal void DelayedAPIResponder()
    {
        var queue = _delayedApiResponses;
        if (queue is null)
            return;

        var token = _quitCts.Token;
        DelayedApiResponse? head = null;
        DelayedApiResponse? tail = null;

        while (!token.IsCancellationRequested)
        {
            try
            {
                while (true)
                {
                    var (entry, ok) = queue.PopOne();
                    if (!ok)
                        break;
                    JetStreamApi.AddDelayedResponse(ref head, ref tail, entry);
                }

                var now = DateTime.UtcNow;
                if (head is not null && head.DeadlineUtc <= now)
                {
                    var entry = head;
                    head = head.Next;
                    if (head is null)
                        tail = null;

                    if (entry.RawResponse)
                        _ = SendInternalAccountMsgWithReply(entry.Account, entry.Subject, string.Empty, entry.Header, entry.Response, false);
                    else
                        SendAPIErrResponse(entry.ClientInfo, entry.Account, entry.Subject, entry.Reply, entry.Request, entry.Response);

                    continue;
                }

                var wait = head is null
                    ? TimeSpan.FromSeconds(1)
                    : head.DeadlineUtc - now;

                if (wait < TimeSpan.Zero)
                    wait = TimeSpan.Zero;

                _ = Task.WhenAny(
                    queue.Ch.WaitToReadAsync(token).AsTask(),
                    Task.Delay(wait, token)).GetAwaiter().GetResult();
            }
            catch (OperationCanceledException)
            {
                return;
            }
            catch (Exception ex)
            {
                Warnf("Delayed JetStream API responder failed: {0}", ex.Message);
            }
        }
    }

    internal void SendDelayedAPIErrResponse(
        ClientInfo? ci,
        Account acc,
        string subject,
        string reply,
        string request,
        string response,
        TimeSpan duration)
    {
        _delayedApiResponses?.Push(new DelayedApiResponse
        {
            ClientInfo = ci,
            Account = acc,
            Subject = subject,
            Reply = reply,
            Request = request,
            Response = response,
            DeadlineUtc = DateTime.UtcNow.Add(duration),
            RawResponse = false,
        });
    }

    internal void SendDelayedErrResponse(Account acc, string subject, byte[]? header, string response, TimeSpan duration)
    {
        _delayedApiResponses?.Push(new DelayedApiResponse
        {
            Account = acc,
            Subject = subject,
            Reply = string.Empty,
            Header = header,
            Response = response,
            DeadlineUtc = DateTime.UtcNow.Add(duration),
            RawResponse = true,
        });
    }

    internal (ClientInfo? ClientInfo, Account? Account, byte[] Header, byte[] Message, Exception? Error) GetRequestInfo(ClientConnection c, byte[] raw)
    {
        var (hdr, msg) = c.MsgParts(raw);

        ClientInfo? clientInfo = null;
        var clientInfoBytes = NatsMessageHeaders.GetHeader(AccountEventConstants.ClientInfoHeader, hdr);
        if (clientInfoBytes is { Length: > 0 })
        {
            try
            {
                clientInfo = JsonSerializer.Deserialize<ClientInfo>(clientInfoBytes);
            }
            catch (Exception ex)
            {
                return (null, null, hdr, msg, ex);
            }
        }

        Account? acc = null;
        var serviceAccount = clientInfo?.ServiceAccount();
        if (!string.IsNullOrWhiteSpace(serviceAccount))
            (acc, _) = LookupAccount(serviceAccount);

        if (acc is null)
        {
            acc = c.Account() as Account;
            acc ??= SystemAccount();
        }

        if (acc is null)
            return (clientInfo, null, hdr, msg, ServerErrors.ErrMissingAccount);

        return (clientInfo, acc, hdr, msg, null);
    }

    internal Exception? UnmarshalRequest<T>(ClientConnection c, Account acc, string subject, byte[] message, out T? value)
    {
        var strictMode = JetStreamConfig()?.Strict ?? true;
        value = JetStreamApi.DeserializeStrict<T>(message, strictMode);
        if (value is not null)
            return null;

        var err = new InvalidOperationException("unable to deserialize JetStream API request");
        c.RateLimitWarnf("Invalid JetStream request '{0} > {1}': {2}", acc.Name, subject, err.Message);
        return err;
    }

    internal Exception? UnmarshalRequest(ClientConnection c, Account acc, string subject, byte[] message, object destination)
    {
        var strictMode = JetStreamConfig()?.Strict ?? true;
        object? parsed;

        try
        {
            parsed = JsonSerializer.Deserialize(message, destination.GetType(), new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true,
                UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
            });
        }
        catch (Exception ex) when (!strictMode)
        {
            try
            {
                parsed = JsonSerializer.Deserialize(message, destination.GetType());
            }
            catch
            {
                c.RateLimitWarnf("Invalid JetStream request '{0} > {1}': {2}", acc.Name, subject, ex.Message);
                return ex;
            }
        }
        catch (Exception ex)
        {
            c.RateLimitWarnf("Invalid JetStream request '{0} > {1}': {2}", acc.Name, subject, ex.Message);
            return ex;
        }

        if (parsed is null)
            return new InvalidOperationException("empty JetStream API request body");

        foreach (var property in destination.GetType().GetProperties())
        {
            if (!property.CanRead || !property.CanWrite)
                continue;
            property.SetValue(destination, property.GetValue(parsed));
        }

        return null;
    }

    internal void JsAccountInfoRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        if (!JetStreamEnabled())
            return;

        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
        {
            Warnf("Malformed JetStream API Request: {0}", Encoding.UTF8.GetString(msg));
            return;
        }

        var response = new JsApiAccountInfoResponse
        {
            Type = JsApiSubjects.JsApiAccountInfoResponseType,
        };

        var requiredApiLevel = NatsMessageHeaders.GetHeader(JsApiSubjects.JsRequiredApiLevel, hdr);
        var reqApiHeader = requiredApiLevel is null ? null : Encoding.ASCII.GetString(requiredApiLevel);
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(reqApiHeader))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var (hasJetStream, shouldError) = requestAcc.CheckJetStream();
        if (!hasJetStream)
        {
            if (!shouldError)
                return;

            response.Error = JsApiErrors.NewJSNotEnabledForAccountError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var stats = requestAcc.JetStreamUsage();
        response.Memory = stats.Memory;
        response.Store = stats.Store;
        response.ReservedMemory = stats.ReservedMemory;
        response.ReservedStore = stats.ReservedStore;
        response.Streams = stats.Streams;
        response.Consumers = stats.Consumers;
        response.Limits = stats.Limits;
        response.Domain = stats.Domain;
        response.Api = stats.Api;
        response.Tiers = stats.Tiers;

        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsStreamCreateRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiStreamCreateResponse { Type = JsApiSubjects.JsApiStreamCreateResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var (hasJetStream, shouldError) = requestAcc.CheckJetStream();
        if (!hasJetStream)
        {
            if (shouldError)
            {
                response.Error = JsApiErrors.NewJSNotEnabledForAccountError();
                SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            }

            return;
        }

        var request = new StreamConfigRequest();
        if (UnmarshalRequest(c, requestAcc, subject, msg, request) is Exception decodeErr)
        {
            response.Error = JsApiErrors.NewJSInvalidJSONError(decodeErr);
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var streamName = StreamNameFromSubject(subject);
        if (!string.Equals(streamName, request.Config.Name, StringComparison.Ordinal))
        {
            response.Error = JsApiErrors.NewJSStreamMismatchError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        if (streamName.IndexOfAny(['\\', '/']) >= 0)
        {
            response.Error = JsApiErrors.NewJSStreamNameContainsPathSeparatorsError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var jsa = requestAcc.JetStream;
        if (jsa is null)
            return;

        jsa.Lock.EnterWriteLock();
        try
        {
            if (jsa.Streams.ContainsKey(streamName))
            {
                response.Error = JsApiErrors.NewJSStreamNameExistError();
                SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
                return;
            }

            var stream = NatsStream.Create(requestAcc, request.Config, jsa, null, null, this);
            if (stream is null)
            {
                response.Error = JsApiErrors.NewJSStreamCreateError(new InvalidOperationException("stream creation failed"));
                SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
                return;
            }

            jsa.Streams[streamName] = stream;
            response.Config = stream.GetConfig();
            response.State = stream.State();
            response.DidCreate = true;
        }
        finally
        {
            jsa.Lock.ExitWriteLock();
        }

        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsStreamUpdateRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiStreamUpdateResponse { Type = JsApiSubjects.JsApiStreamUpdateResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var (hasJetStream, shouldError) = requestAcc.CheckJetStream();
        if (!hasJetStream)
        {
            if (shouldError)
            {
                response.Error = JsApiErrors.NewJSNotEnabledForAccountError();
                SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            }

            return;
        }

        var request = new StreamConfigRequest();
        if (UnmarshalRequest(c, requestAcc, subject, msg, request) is Exception decodeErr)
        {
            response.Error = JsApiErrors.NewJSInvalidJSONError(decodeErr);
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var streamName = StreamNameFromSubject(subject);
        if (!string.Equals(streamName, request.Config.Name, StringComparison.Ordinal))
        {
            response.Error = JsApiErrors.NewJSStreamMismatchError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var (stream, lookupErr) = requestAcc.LookupStream(streamName);
        if (lookupErr is not null || stream is null)
        {
            response.Error = JsApiErrors.NewJSStreamNotFoundError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        stream.UpdateConfig(request.Config);
        response.Info = stream.GetInfo();
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsStreamNamesRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiStreamNamesResponse
        {
            Type = JsApiSubjects.JsApiStreamNamesResponseType,
            Streams = [],
            Limit = JsApiSubjects.JsApiNamesLimit,
        };

        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var (hasJetStream, shouldError) = requestAcc.CheckJetStream();
        if (!hasJetStream)
        {
            if (shouldError)
            {
                response.Error = JsApiErrors.NewJSNotEnabledForAccountError();
                SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            }

            return;
        }

        var offset = 0;
        var filter = string.Empty;
        if (JetStreamApi.IsJSONObjectOrArray(msg))
        {
            var request = new JsApiStreamNamesRequest();
            if (UnmarshalRequest(c, requestAcc, subject, msg, request) is Exception decodeErr)
            {
                response.Error = JsApiErrors.NewJSInvalidJSONError(decodeErr);
                SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
                return;
            }

            offset = request.Offset;
            filter = request.Subject ?? string.Empty;
        }

        var streams = requestAcc.FilteredStreams(filter)
            .OrderBy(s => s.Config.Name, StringComparer.Ordinal)
            .Select(s => s.Config.Name)
            .ToList();

        response.Total = streams.Count;
        if (offset > streams.Count)
            offset = streams.Count;
        response.Offset = offset;
        response.Streams = streams.Skip(offset).Take(JsApiSubjects.JsApiNamesLimit).ToList();
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsStreamListRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiStreamListResponse
        {
            Type = JsApiSubjects.JsApiStreamListResponseType,
            Streams = [],
            Limit = JsApiSubjects.JsApiListLimit,
        };

        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var (hasJetStream, shouldError) = requestAcc.CheckJetStream();
        if (!hasJetStream)
        {
            if (shouldError)
            {
                response.Error = JsApiErrors.NewJSNotEnabledForAccountError();
                SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            }

            return;
        }

        var offset = 0;
        var filter = string.Empty;
        if (JetStreamApi.IsJSONObjectOrArray(msg))
        {
            var request = new JsApiStreamListRequest();
            if (UnmarshalRequest(c, requestAcc, subject, msg, request) is Exception decodeErr)
            {
                response.Error = JsApiErrors.NewJSInvalidJSONError(decodeErr);
                SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
                return;
            }

            offset = request.Offset;
            filter = request.Subject ?? string.Empty;
        }

        var streams = requestAcc.FilteredStreams(filter)
            .OrderBy(s => s.Config.Name, StringComparer.Ordinal)
            .ToList();

        response.Total = streams.Count;
        if (offset > streams.Count)
            offset = streams.Count;
        response.Offset = offset;
        response.Streams = streams.Skip(offset).Take(JsApiSubjects.JsApiListLimit).Select(s => s.GetInfo()).ToList();
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsStreamInfoRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiStreamInfoResponse { Type = JsApiSubjects.JsApiStreamInfoResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var (hasJetStream, shouldError) = requestAcc.CheckJetStream();
        if (!hasJetStream)
        {
            if (shouldError)
            {
                response.Error = JsApiErrors.NewJSNotEnabledForAccountError();
                SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            }

            return;
        }

        var streamName = StreamNameFromSubject(subject);
        var (stream, lookupErr) = requestAcc.LookupStream(streamName);
        if (lookupErr is not null || stream is null)
        {
            response.Error = JsApiErrors.NewJSStreamNotFoundError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Info = stream.GetInfo();
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsStreamLeaderStepDownRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiStreamLeaderStepDownResponse { Type = JsApiSubjects.JsApiStreamLeaderStepDownResponseType };
        if (!JetStreamEnabledForDomain())
        {
            response.Error = JsApiErrors.NewJSClusterRequiredError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Success = true;
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsConsumerLeaderStepDownRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiConsumerLeaderStepDownResponse { Type = JsApiSubjects.JsApiConsumerLeaderStepDownResponseType };
        if (!JetStreamEnabledForDomain())
        {
            response.Error = JsApiErrors.NewJSClusterRequiredError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Success = true;
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsStreamRemovePeerRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiStreamRemovePeerResponse { Type = JsApiSubjects.JsApiStreamRemovePeerResponseType };
        if (!JetStreamEnabledForDomain())
        {
            response.Error = JsApiErrors.NewJSClusterRequiredError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        if (JetStreamApi.IsEmptyRequest(msg))
        {
            response.Error = JsApiErrors.NewJSBadRequestError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Success = true;
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsLeaderServerRemoveRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null || !ReferenceEquals(requestAcc, SystemAccount()))
            return;

        var response = new JsApiMetaServerRemoveResponse { Type = JsApiSubjects.JsApiMetaServerRemoveResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        if (JetStreamApi.IsEmptyRequest(msg))
        {
            response.Error = JsApiErrors.NewJSBadRequestError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var request = new JsApiMetaServerRemoveRequest();
        if (UnmarshalRequest(c, requestAcc, subject, msg, request) is Exception decodeErr)
        {
            response.Error = JsApiErrors.NewJSInvalidJSONError(decodeErr);
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Success = !string.IsNullOrWhiteSpace(request.PeerId) || !string.IsNullOrWhiteSpace(request.Server);
        if (!response.Success)
            response.Error = JsApiErrors.NewJSClusterServerNotMemberError();

        if (response.Error is null)
            SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
        else
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal string[] PeerSetToNames(string[] peers)
    {
        var names = new string[peers.Length];
        for (var i = 0; i < peers.Length; i++)
        {
            var info = GetNodeInfo(peers[i]);
            names[i] = info?.Name ?? peers[i];
        }

        return names;
    }

    internal string NameToPeer(JetStream js, string serverName, string clusterName, string domainName)
    {
        _ = js;
        foreach (var (nodeId, _) in _nodeToInfo)
        {
            var info = GetNodeInfo(nodeId);
            if (info is null || !string.Equals(info.Name, serverName, StringComparison.Ordinal))
                continue;
            if (!string.IsNullOrWhiteSpace(clusterName) && !string.Equals(info.Cluster, clusterName, StringComparison.Ordinal))
                continue;
            if (!string.IsNullOrWhiteSpace(domainName) && !string.Equals(info.Domain, domainName, StringComparison.Ordinal))
                continue;
            return nodeId;
        }

        return string.Empty;
    }

    internal void JsLeaderServerStreamMoveRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiStreamUpdateResponse { Type = JsApiSubjects.JsApiStreamUpdateResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var req = new JsApiMetaServerStreamMoveRequest();
        if (UnmarshalRequest(c, requestAcc, subject, msg, req) is Exception decodeErr)
        {
            response.Error = JsApiErrors.NewJSInvalidJSONError(decodeErr);
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var streamName = Internal.DataStructures.SubscriptionIndex.TokenAt(subject, 7);
        var (stream, lookupErr) = requestAcc.LookupStream(streamName);
        if (lookupErr is not null || stream is null)
        {
            response.Error = JsApiErrors.NewJSStreamNotFoundError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Info = stream.GetInfo();
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsLeaderServerStreamCancelMoveRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiStreamUpdateResponse { Type = JsApiSubjects.JsApiStreamUpdateResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var streamName = Internal.DataStructures.SubscriptionIndex.TokenAt(subject, 7);
        var (stream, lookupErr) = requestAcc.LookupStream(streamName);
        if (lookupErr is not null || stream is null)
        {
            response.Error = JsApiErrors.NewJSStreamNotFoundError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Info = stream.GetInfo();
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsLeaderAccountPurgeRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null || !ReferenceEquals(requestAcc, SystemAccount()))
            return;

        var response = new JsApiAccountPurgeResponse { Type = JsApiSubjects.JsApiAccountPurgeResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Initiated = true;
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsLeaderStepDownRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null || !ReferenceEquals(requestAcc, SystemAccount()))
            return;

        var response = new JsApiLeaderStepDownResponse { Type = JsApiSubjects.JsApiLeaderStepDownResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Success = true;
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsStreamDeleteRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiStreamDeleteResponse { Type = JsApiSubjects.JsApiStreamDeleteResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        if (!IsEmptyRequest(msg))
        {
            response.Error = JsApiErrors.NewJSNotEmptyRequestError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var streamName = StreamNameFromSubject(subject);
        var (stream, lookupErr) = requestAcc.LookupStream(streamName);
        if (lookupErr is not null || stream is null)
        {
            response.Error = JsApiErrors.NewJSStreamNotFoundError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        requestAcc.JetStream?.Lock.EnterWriteLock();
        try
        {
            stream.Delete();
            requestAcc.JetStream?.Streams.Remove(streamName);
            response.Success = true;
        }
        finally
        {
            requestAcc.JetStream?.Lock.ExitWriteLock();
        }

        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsMsgDeleteRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiMsgDeleteResponse { Type = JsApiSubjects.JsApiMsgDeleteResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var request = new JsApiMsgDeleteRequest();
        if (UnmarshalRequest(c, requestAcc, subject, msg, request) is Exception decodeErr)
        {
            response.Error = JsApiErrors.NewJSInvalidJSONError(decodeErr);
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var streamName = SubscriptionIndex.TokenAt(subject, 6);
        var (stream, lookupErr) = requestAcc.LookupStream(streamName);
        if (lookupErr is not null || stream?.Store is null)
        {
            response.Error = JsApiErrors.NewJSStreamNotFoundError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var (removed, removeErr) = request.NoErase
            ? stream.Store.RemoveMsg(request.Seq)
            : stream.Store.EraseMsg(request.Seq);
        if (removeErr is not null)
        {
            response.Error = JsApiErrors.NewJSStreamMsgDeleteFailedError(removeErr);
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Success = removed;
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsMsgGetRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiMsgGetResponse { Type = JsApiSubjects.JsApiMsgGetResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var request = new JsApiMsgGetRequest();
        if (JetStreamApi.IsJSONObjectOrArray(msg))
        {
            if (UnmarshalRequest(c, requestAcc, subject, msg, request) is Exception decodeErr)
            {
                response.Error = JsApiErrors.NewJSInvalidJSONError(decodeErr);
                SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
                return;
            }
        }

        var streamName = SubscriptionIndex.TokenAt(subject, 6);
        var (stream, lookupErr) = requestAcc.LookupStream(streamName);
        if (lookupErr is not null || stream?.Store is null)
        {
            response.Error = JsApiErrors.NewJSStreamNotFoundError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var storeMsg = stream.Store.LoadMsg(request.Seq, null);
        if (storeMsg is null)
        {
            response.Error = JsApiErrors.NewJSNoMessageFoundError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Message = new StoredMsg();
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsStreamPurgeRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiStreamPurgeResponse { Type = JsApiSubjects.JsApiStreamPurgeResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var request = new JsApiStreamPurgeRequest();
        if (JetStreamApi.IsJSONObjectOrArray(msg) &&
            UnmarshalRequest(c, requestAcc, subject, msg, request) is Exception decodeErr)
        {
            response.Error = JsApiErrors.NewJSInvalidJSONError(decodeErr);
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var streamName = StreamNameFromSubject(subject);
        var (stream, lookupErr) = requestAcc.LookupStream(streamName);
        if (lookupErr is not null || stream?.Store is null)
        {
            response.Error = JsApiErrors.NewJSStreamNotFoundError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        (ulong purged, Exception? purgeErr) result = string.IsNullOrWhiteSpace(request.Subject) && request.Sequence == 0 && request.Keep == 0
            ? stream.Store.Purge()
            : stream.Store.PurgeEx(request.Subject ?? string.Empty, request.Sequence, request.Keep);

        if (result.purgeErr is not null)
        {
            response.Error = JsApiErrors.NewJSStreamPurgeFailedError(result.purgeErr);
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Success = true;
        response.Purged = result.purged;
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsConsumerUnpinRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiConsumerUnpinResponse { Type = JsApiSubjects.JsApiConsumerUnpinResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var request = new JsApiConsumerUnpinRequest();
        if (JetStreamApi.IsJSONObjectOrArray(msg) &&
            UnmarshalRequest(c, requestAcc, subject, msg, request) is Exception decodeErr)
        {
            response.Error = JsApiErrors.NewJSInvalidJSONError(decodeErr);
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsStreamRestoreRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiStreamRestoreResponse { Type = JsApiSubjects.JsApiStreamRestoreResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var request = new JsApiStreamRestoreRequest();
        if (UnmarshalRequest(c, requestAcc, subject, msg, request) is Exception decodeErr)
        {
            response.Error = JsApiErrors.NewJSInvalidJSONError(decodeErr);
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var streamName = StreamNameFromSubject(subject);
        var restoreErr = ProcessStreamRestore(requestAcc, streamName, request);
        if (restoreErr is not null)
        {
            response.Error = JsApiErrors.NewJSStreamRestoreError(restoreErr);
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.DeliverSubject = $"$JSC.R.{streamName}";
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal Exception? ProcessStreamRestore(Account acc, string streamName, JsApiStreamRestoreRequest request)
    {
        _ = request;
        var (stream, lookupErr) = acc.LookupStream(streamName);
        if (lookupErr is not null || stream is null)
            return lookupErr ?? new InvalidOperationException("stream not found");

        stream.UpdateConfig(stream.GetConfig());
        return null;
    }

    internal void JsStreamSnapshotRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiStreamSnapshotResponse { Type = JsApiSubjects.JsApiStreamSnapshotResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var request = new JsApiStreamSnapshotRequest();
        if (JetStreamApi.IsJSONObjectOrArray(msg) &&
            UnmarshalRequest(c, requestAcc, subject, msg, request) is Exception decodeErr)
        {
            response.Error = JsApiErrors.NewJSInvalidJSONError(decodeErr);
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var streamName = StreamNameFromSubject(subject);
        var (stream, lookupErr) = requestAcc.LookupStream(streamName);
        if (lookupErr is not null || stream is null)
        {
            response.Error = JsApiErrors.NewJSStreamNotFoundError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var (info, snapErr) = StreamSnapshot(stream, request);
        if (snapErr is not null || info is null)
        {
            response.Error = JsApiErrors.NewJSStreamSnapshotError(snapErr ?? new InvalidOperationException("snapshot failed"));
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Config = info.Config;
        response.State = info.State;
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal (StreamInfo? Info, Exception? Error) StreamSnapshot(NatsStream stream, JsApiStreamSnapshotRequest request)
    {
        if (stream.Store is null)
            return (null, new InvalidOperationException("stream store not initialized"));

        var (_, err) = stream.Store.Snapshot(TimeSpan.FromSeconds(30), !request.NoConsumers, request.CheckMsgs);
        if (err is not null)
            return (null, err);
        return (stream.GetInfo(), null);
    }

    internal void JsConsumerCreateRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiConsumerCreateResponse { Type = JsApiSubjects.JsApiConsumerCreateResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Error = JsApiErrors.NewJSClusterUnSupportFeatureError();
        SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsConsumerNamesRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiConsumerNamesResponse
        {
            Type = JsApiSubjects.JsApiConsumerNamesResponseType,
            Consumers = [],
            Limit = JsApiSubjects.JsApiNamesLimit,
            Total = 0,
            Offset = 0,
        };

        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsConsumerListRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiConsumerListResponse
        {
            Type = JsApiSubjects.JsApiConsumerListResponseType,
            Consumers = [],
            Limit = JsApiSubjects.JsApiListLimit,
            Total = 0,
            Offset = 0,
        };

        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsConsumerInfoRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiConsumerInfoResponse { Type = JsApiSubjects.JsApiConsumerInfoResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Error = JsApiErrors.NewJSConsumerNotFoundError();
        SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsConsumerDeleteRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiConsumerDeleteResponse { Type = JsApiSubjects.JsApiConsumerDeleteResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Success = true;
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal void JsConsumerPauseRequest(ClientConnection c, Account acc, string subject, string reply, byte[] rawMessage)
    {
        var (ci, requestAcc, hdr, msg, err) = GetRequestInfo(c, rawMessage);
        if (err is not null || requestAcc is null)
            return;

        var response = new JsApiConsumerPauseResponse { Type = JsApiSubjects.JsApiConsumerPauseResponseType };
        if (JetStreamVersioning.ErrorOnRequiredApiLevel(GetRequiredApiHeader(hdr)))
        {
            response.Error = JsApiErrors.NewJSRequiredApiLevelError();
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        var request = new JsApiConsumerPauseRequest();
        if (UnmarshalRequest(c, requestAcc, subject, msg, request) is Exception decodeErr)
        {
            response.Error = JsApiErrors.NewJSInvalidJSONError(decodeErr);
            SendAPIErrResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
            return;
        }

        response.Paused = request.PauseUntil > DateTime.UtcNow;
        response.PauseUntil = request.PauseUntil;
        response.PauseRemaining = request.PauseUntil <= DateTime.UtcNow ? TimeSpan.Zero : request.PauseUntil - DateTime.UtcNow;
        SendAPIResponse(ci, requestAcc, subject, reply, Encoding.UTF8.GetString(msg), JsonResponse(response));
    }

    internal static bool IsEmptyRequest(byte[] request) => JetStreamApi.IsEmptyRequest(request);

    internal (string PreferredLeader, JsApiError? Error) GetStepDownPreferredPlacement(IRaftNode group, Placement? placement)
    {
        if (placement is null)
            return (string.Empty, null);

        if (!string.IsNullOrWhiteSpace(placement.Preferred))
        {
            foreach (var peer in group.Peers())
            {
                var info = GetNodeInfo(peer.Id);
                if (info is not null && string.Equals(info.Name, placement.Preferred, StringComparison.Ordinal))
                {
                    if (peer.Id == group.ID())
                    {
                        return (string.Empty, JsApiErrors.NewJSClusterNoPeersError(
                            new InvalidOperationException($"preferred server '{placement.Preferred}' is already leader")));
                    }

                    return (peer.Id, null);
                }
            }

            return (string.Empty, JsApiErrors.NewJSClusterNoPeersError(
                new InvalidOperationException($"preferred server '{placement.Preferred}' not known")));
        }

        foreach (var peer in group.Peers())
        {
            if (peer.Id == group.ID())
                continue;

            var info = GetNodeInfo(peer.Id);
            if (info is null || info.Offline)
                continue;

            if (!string.IsNullOrWhiteSpace(placement.Cluster) &&
                !string.Equals(info.Cluster, placement.Cluster, StringComparison.Ordinal))
            {
                continue;
            }

            if (placement.Tags is { Length: > 0 } &&
                placement.Tags.Except(info.Tags, StringComparer.Ordinal).Any())
            {
                continue;
            }

            return (peer.Id, null);
        }

        return (string.Empty, JsApiErrors.NewJSClusterNoPeersError(new InvalidOperationException("no replacement peer connected")));
    }

    private static string? GetRequiredApiHeader(byte[] header)
    {
        var value = NatsMessageHeaders.GetHeader(JsApiSubjects.JsRequiredApiLevel, header);
        return value is null ? null : Encoding.ASCII.GetString(value);
    }

    internal static string StreamNameFromSubject(string subject) => JetStreamApi.StreamNameFromSubject(subject);

    internal static string ConsumerNameFromSubject(string subject) => JetStreamApi.ConsumerNameFromSubject(subject);

    internal string JsonResponse(object response)
    {
        try
        {
            return JsonSerializer.Serialize(response);
        }
        catch (Exception ex)
        {
            Warnf("Problem marshaling JSON for JetStream API: {0}", ex.Message);
            return string.Empty;
        }
    }

    internal void SendJetStreamAPIAuditAdvisory(ClientInfo? ci, Account acc, string subject, string request, string response)
    {
        _ = ci;
        _ = acc;
        _ = subject;
        _ = request;
        _ = response;
    }

    private Exception? SendInternalAccountMsgWithReply(Account account, string subject, string reply, byte[]? header, string response, bool trackApi)
    {
        _ = trackApi;

        try
        {
            var sendQueue = account.GetSendQueue() ?? NewSendQueue(account);
            SendQueue.Send(sendQueue, subject, reply, header ?? [], Encoding.UTF8.GetBytes(response));
            return null;
        }
        catch (Exception ex)
        {
            return ex;
        }
    }
}
