using ZB.MOM.NatsNet.Server.Internal.DataStructures;

namespace ZB.MOM.NatsNet.Server;

internal sealed partial class NatsConsumer
{
    internal static void InsertAtPosition(WaitingRequest request, WaitQueue waitQueue)
    {
        ArgumentNullException.ThrowIfNull(request);
        ArgumentNullException.ThrowIfNull(waitQueue);
        waitQueue.InsertSorted(request);
    }

    internal Dictionary<string, WaitingRequest> PendingRequests()
    {
        _mu.EnterReadLock();
        try
        {
            if (_waiting is null || _waiting.IsEmpty())
                return [];

            var requests = new Dictionary<string, WaitingRequest>(StringComparer.Ordinal);
            foreach (var waitingRequest in _waiting.Snapshot())
            {
                if (!string.IsNullOrWhiteSpace(waitingRequest.Reply))
                    requests[waitingRequest.Reply] = waitingRequest;
            }

            return requests;
        }
        finally
        {
            _mu.ExitReadLock();
        }
    }

    internal void SetPinnedTimer(string priorityGroup)
    {
        if (Config.PinnedTTL <= TimeSpan.Zero)
            return;

        _pinnedTtlTimer ??= new Timer(
            state =>
            {
                var consumer = (NatsConsumer)state!;
                consumer._mu.EnterWriteLock();
                try
                {
                    if (string.IsNullOrEmpty(consumer._currentPinId))
                        return;

                    consumer.UnassignPinId();
                    consumer.SendUnpinnedAdvisoryLocked(priorityGroup);
                }
                finally
                {
                    consumer._mu.ExitWriteLock();
                }

                consumer.SignalNewMessages();
            },
            this,
            Timeout.InfiniteTimeSpan,
            Timeout.InfiniteTimeSpan);

        _pinnedTtlTimer.Change(Config.PinnedTTL, Timeout.InfiniteTimeSpan);
    }

    internal void AssignNewPinId(WaitingRequest request)
    {
        ArgumentNullException.ThrowIfNull(request);
        if (request.PriorityGroup is not { Group.Length: > 0 } priorityGroup)
            return;

        _currentPinId = Guid.NewGuid().ToString("N");
        _pinnedTs = DateTime.UtcNow;
        priorityGroup.Id = _currentPinId;
        SetPinnedTimer(priorityGroup.Group);
        SendPinnedAdvisoryLocked(priorityGroup.Group);
    }

    internal void UnassignPinId()
    {
        _currentPinId = string.Empty;
        _pinnedTs = DateTime.UnixEpoch;
        _pinnedTtlTimer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
    }

    internal WaitingRequest? NextWaiting(int messageSize)
    {
        _mu.EnterWriteLock();
        try
        {
            if (_waiting is null || _waiting.IsEmpty())
                return null;

            var numCycled = 0;
            while (!_waiting.IsEmpty())
            {
                var waitingRequest = _waiting.Peek();
                if (waitingRequest is null)
                    return null;

                if (waitingRequest.Expires is DateTime expiresAt && expiresAt <= DateTime.UtcNow)
                {
                    _waiting.RemoveCurrent();
                    waitingRequest.Recycle();
                    continue;
                }

                if (waitingRequest.MaxBytes > 0)
                {
                    if (messageSize > waitingRequest.MaxBytes)
                    {
                        _waiting.RemoveCurrent();
                        waitingRequest.Recycle();
                        continue;
                    }

                    waitingRequest.MaxBytes -= messageSize;
                    if (waitingRequest.MaxBytes == 0)
                        waitingRequest.N = 1;
                }

                if (Config.PriorityPolicy == PriorityPolicy.PriorityPinnedClient)
                {
                    if (string.IsNullOrEmpty(_currentPinId))
                    {
                        if (string.IsNullOrEmpty(waitingRequest.PriorityGroup?.Id))
                            AssignNewPinId(waitingRequest);
                    }
                    else if (waitingRequest.PriorityGroup is { } priorityGroup)
                    {
                        if (string.Equals(priorityGroup.Id, _currentPinId, StringComparison.Ordinal))
                        {
                            // Matched the active pin, continue.
                        }
                        else if (string.IsNullOrEmpty(priorityGroup.Id))
                        {
                            _waiting.Cycle();
                            numCycled++;
                            if (numCycled >= _waiting.Len)
                                return null;
                            continue;
                        }
                        else
                        {
                            _waiting.RemoveCurrent();
                            waitingRequest.Recycle();
                            continue;
                        }
                    }
                }

                return _waiting.PopOrPopAndRequeue(Config.PriorityPolicy);
            }

            return null;
        }
        finally
        {
            _mu.ExitWriteLock();
        }
    }

    internal static (Account Account, string Interest) TrackDownAccountAndInterest(Account account, string interest)
    {
        ArgumentNullException.ThrowIfNull(account);
        return (account, interest);
    }

    internal ulong DeliveryCount(ulong streamSequence)
    {
        _mu.EnterReadLock();
        try
        {
            if (_rdc is null)
                return 1;

            return _rdc.TryGetValue(streamSequence, out var count) && count >= 1 ? count : 1;
        }
        finally
        {
            _mu.ExitReadLock();
        }
    }

    internal ulong IncDeliveryCount(ulong streamSequence)
    {
        _mu.EnterWriteLock();
        try
        {
            _rdc ??= [];
            _rdc[streamSequence] = _rdc.GetValueOrDefault(streamSequence) + 1;
            return _rdc[streamSequence] + 1;
        }
        finally
        {
            _mu.ExitWriteLock();
        }
    }

    internal void DecDeliveryCount(ulong streamSequence)
    {
        _mu.EnterWriteLock();
        try
        {
            _rdc ??= [];
            _rdc[streamSequence] = _rdc.GetValueOrDefault(streamSequence) - 1;
        }
        finally
        {
            _mu.ExitWriteLock();
        }
    }

    internal void NotifyDeliveryExceeded(ulong streamSequence, ulong deliveryCount)
    {
        var advisory = new
        {
            Stream,
            Consumer = Name,
            StreamSeq = streamSequence,
            Deliveries = deliveryCount,
            Timestamp = DateTime.UtcNow,
        };

        _ = SendAdvisory($"{JsApiSubjects.JsAdvisoryConsumerMaxDelivery}.{Stream}.{Name}", advisory);
    }

    internal bool IsFilteredMatch(string subject)
    {
        if (Config.FilterSubjects is not { Length: > 0 } && string.IsNullOrWhiteSpace(Config.FilterSubject))
            return true;

        if (!string.IsNullOrWhiteSpace(Config.FilterSubject) && SubjectIsExactOrSubsetMatch(subject, Config.FilterSubject!))
            return true;

        if (Config.FilterSubjects is not { Length: > 0 })
            return false;

        return Config.FilterSubjects.Any(filter => SubjectIsExactOrSubsetMatch(subject, filter));
    }

    internal bool IsEqualOrSubsetMatch(string subject)
    {
        if (Config.FilterSubjects is not { Length: > 0 } && string.IsNullOrWhiteSpace(Config.FilterSubject))
            return false;

        if (!string.IsNullOrWhiteSpace(Config.FilterSubject) && SubjectIsExactOrSubsetMatch(Config.FilterSubject!, subject))
            return true;

        if (Config.FilterSubjects is not { Length: > 0 })
            return false;

        return Config.FilterSubjects.Any(filter => SubjectIsExactOrSubsetMatch(filter, subject));
    }

    private static bool SubjectIsExactOrSubsetMatch(string subject, string filter) =>
        string.Equals(subject, filter, StringComparison.Ordinal) || SubscriptionIndex.SubjectIsSubsetMatch(subject, filter);
}
