using System.Text;

namespace NATS.Server.Subscriptions;

/// <summary>
/// SubList is a routing mechanism to handle subject distribution and
/// provides a facility to match subjects from published messages to
/// interested subscribers. Subscribers can have wildcard subjects to
/// match multiple published subjects.
/// </summary>
public sealed class SubList : IDisposable
{
    private const int CacheMax = 1024;
    private const int CacheSweep = 256;
    [ThreadStatic]
    private static MatchBuilder? s_matchBuilder;
    [ThreadStatic]
    private static List<RoutedSubKey>? s_remoteSubRemovalKeys;

    private readonly ReaderWriterLockSlim _lock = new();
    private readonly TrieLevel _root = new();
    private readonly SubListCacheSweeper _sweeper = new();
    private readonly Dictionary<RoutedSubKey, RemoteSubscription> _remoteSubs = [];
    private Dictionary<string, CachedResult>? _cache = new(StringComparer.Ordinal);
    private uint _count;
    private volatile bool _disposed;
    private long _generation;
    private ulong _matches;
    private ulong _cacheHits;
    private ulong _inserts;
    private ulong _removes;
    private int _highFanoutNodes;
    private Action<bool>? _interestStateNotification;
    private readonly Dictionary<string, List<Action<bool>>> _queueInsertNotifications = new(StringComparer.Ordinal);
    private readonly Dictionary<string, List<Action<bool>>> _queueRemoveNotifications = new(StringComparer.Ordinal);

    private readonly record struct CachedResult(SubListResult Result, long Generation);
    public event Action<InterestChange>? InterestChanged;

    public SubList()
        : this(enableCache: true)
    {
    }

    public SubList(bool enableCache)
    {
        if (!enableCache)
            _cache = null;
    }

    public static SubList NewSublistNoCache() => new(enableCache: false);

    public bool CacheEnabled() => _cache != null;

    public void RegisterNotification(Action<bool> callback) => _interestStateNotification = callback;

    public void ClearNotification() => _interestStateNotification = null;

    public bool RegisterQueueNotification(string subject, string queue, Action<bool> callback)
    {
        if (callback == null || string.IsNullOrWhiteSpace(subject) || string.IsNullOrWhiteSpace(queue))
            return false;
        if (SubjectMatch.SubjectHasWildcard(subject) || SubjectMatch.SubjectHasWildcard(queue))
            return false;

        bool hasInterest;
        var key = QueueNotifyKey(subject, queue);
        _lock.EnterWriteLock();
        try
        {
            hasInterest = HasExactQueueInterestNoLock(subject, queue);
            var map = hasInterest ? _queueRemoveNotifications : _queueInsertNotifications;
            if (!AddQueueNotifyNoLock(map, key, callback))
                return false;
        }
        finally
        {
            _lock.ExitWriteLock();
        }

        callback(hasInterest);
        return true;
    }

    public bool ClearQueueNotification(string subject, string queue, Action<bool> callback)
    {
        var key = QueueNotifyKey(subject, queue);

        _lock.EnterWriteLock();
        try
        {
            var removed = RemoveQueueNotifyNoLock(_queueRemoveNotifications, key, callback);
            removed |= RemoveQueueNotifyNoLock(_queueInsertNotifications, key, callback);
            return removed;
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void Dispose()
    {
        _disposed = true;
        _lock.Dispose();
    }

    public uint Count
    {
        get
        {
            _lock.EnterReadLock();
            try { return _count; }
            finally { _lock.ExitReadLock(); }
        }
    }

    /// <summary>
    /// Returns all subscriptions in the trie. For monitoring only.
    /// </summary>
    public List<Subscription> GetAllSubscriptions()
    {
        _lock.EnterReadLock();
        try
        {
            var result = new List<Subscription>();
            CollectAll(_root, result);
            return result;
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    private static void CollectAll(TrieLevel level, List<Subscription> result)
    {
        foreach (var (_, node) in level.Nodes)
        {
            foreach (var sub in node.PlainSubs) result.Add(sub);
            foreach (var (_, qset) in node.QueueSubs)
                foreach (var sub in qset) result.Add(sub);
            if (node.Next != null) CollectAll(node.Next, result);
        }
        if (level.Pwc != null)
        {
            foreach (var sub in level.Pwc.PlainSubs) result.Add(sub);
            foreach (var (_, qset) in level.Pwc.QueueSubs)
                foreach (var sub in qset) result.Add(sub);
            if (level.Pwc.Next != null) CollectAll(level.Pwc.Next, result);
        }
        if (level.Fwc != null)
        {
            foreach (var sub in level.Fwc.PlainSubs) result.Add(sub);
            foreach (var (_, qset) in level.Fwc.QueueSubs)
                foreach (var sub in qset) result.Add(sub);
            if (level.Fwc.Next != null) CollectAll(level.Fwc.Next, result);
        }
    }

    /// <summary>
    /// Returns the current number of entries in the cache.
    /// </summary>
    public int CacheCount
    {
        get
        {
            _lock.EnterReadLock();
            try { return _cache?.Count ?? 0; }
            finally { _lock.ExitReadLock(); }
        }
    }

    internal int HighFanoutNodeCountForTest => Volatile.Read(ref _highFanoutNodes);

    internal Task TriggerCacheSweepAsyncForTest() => _sweeper.TriggerSweepAsync(SweepCache);

    public void ApplyRemoteSub(RemoteSubscription sub)
    {
        _lock.EnterWriteLock();
        try
        {
            var key = RoutedSubKey.FromRemoteSubscription(sub);
            var changed = false;
            if (sub.IsRemoval)
            {
                changed = _remoteSubs.Remove(key);
                if (changed)
                {
                    InterestChanged?.Invoke(new InterestChange(
                        InterestChangeKind.RemoteRemoved,
                        sub.Subject,
                        sub.Queue,
                        sub.Account));
                }
            }
            else
            {
                if (!_remoteSubs.TryGetValue(key, out var existing) || existing != sub)
                {
                    _remoteSubs[key] = sub;
                    changed = true;
                    InterestChanged?.Invoke(new InterestChange(
                        InterestChangeKind.RemoteAdded,
                        sub.Subject,
                        sub.Queue,
                        sub.Account));
                }
            }

            if (changed)
                Interlocked.Increment(ref _generation);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void UpdateRemoteQSub(RemoteSubscription sub)
    {
        if (sub.Queue == null)
            return;

        _lock.EnterWriteLock();
        try
        {
            var key = RoutedSubKey.FromRemoteSubscription(sub);
            if (!_remoteSubs.TryGetValue(key, out var existing))
                return;

            var nextWeight = Math.Max(1, sub.QueueWeight);
            if (existing.QueueWeight == nextWeight)
                return;

            _remoteSubs[key] = existing with { QueueWeight = nextWeight };
            Interlocked.Increment(ref _generation);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public int RemoveRemoteSubs(string routeId)
    {
        _lock.EnterWriteLock();
        try
        {
            var removalKeys = RentRemoteSubRemovalKeys();
            var removed = 0;
            foreach (var (key, _) in _remoteSubs)
            {
                if (!string.Equals(key.RouteId, routeId, StringComparison.Ordinal))
                    continue;

                removalKeys.Add(key);
            }

            foreach (var key in removalKeys)
            {
                if (_remoteSubs.Remove(key, out var removedSub))
                {
                    removed++;
                    InterestChanged?.Invoke(new InterestChange(
                        InterestChangeKind.RemoteRemoved,
                        removedSub.Subject,
                        removedSub.Queue,
                        removedSub.Account));
                }
            }

            removalKeys.Clear();

            if (removed > 0)
                Interlocked.Increment(ref _generation);

            return removed;
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public int RemoveRemoteSubsForAccount(string routeId, string account)
    {
        _lock.EnterWriteLock();
        try
        {
            var removalKeys = RentRemoteSubRemovalKeys();
            var removed = 0;
            foreach (var (key, _) in _remoteSubs)
            {
                if (!string.Equals(key.RouteId, routeId, StringComparison.Ordinal)
                    || !string.Equals(key.Account, account, StringComparison.Ordinal))
                {
                    continue;
                }

                removalKeys.Add(key);
            }

            foreach (var key in removalKeys)
            {
                if (_remoteSubs.Remove(key, out var removedSub))
                {
                    removed++;
                    InterestChanged?.Invoke(new InterestChange(
                        InterestChangeKind.RemoteRemoved,
                        removedSub.Subject,
                        removedSub.Queue,
                        removedSub.Account));
                }
            }

            removalKeys.Clear();

            if (removed > 0)
                Interlocked.Increment(ref _generation);

            return removed;
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public bool HasRemoteInterest(string subject)
        => HasRemoteInterest("$G", subject);

    public bool HasRemoteInterest(string account, string subject)
    {
        _lock.EnterReadLock();
        try
        {
            foreach (var remoteSub in _remoteSubs.Values)
            {
                if (remoteSub.IsRemoval)
                    continue;
                if (!string.Equals(remoteSub.Account, account, StringComparison.Ordinal))
                    continue;

                if (SubjectMatch.MatchLiteral(subject, remoteSub.Subject))
                    return true;
            }

            return false;
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    public void Insert(Subscription sub)
    {
        var subject = sub.Subject;

        _lock.EnterWriteLock();
        try
        {
            var hadInterest = _count > 0;
            var level = _root;
            TrieNode? node = null;
            bool sawFwc = false;

            foreach (var token in new TokenEnumerator(subject))
            {
                if (token.Length == 0 || sawFwc)
                    throw new ArgumentException("Invalid subject", nameof(sub));

                if (token.Length == 1 && token[0] == SubjectMatch.Pwc)
                {
                    node = level.Pwc ??= new TrieNode();
                }
                else if (token.Length == 1 && token[0] == SubjectMatch.Fwc)
                {
                    node = level.Fwc ??= new TrieNode();
                    sawFwc = true;
                }
                else
                {
                    if (!TryGetLiteralNode(level, token, out _, out node))
                    {
                        var key = token.ToString();
                        node = new TrieNode();
                        level.Nodes[key] = node;
                    }
                }

                node.Next ??= new TrieLevel();
                level = node.Next;
            }

            if (node == null)
                throw new ArgumentException("Invalid subject", nameof(sub));

            if (sub.Queue == null)
            {
                node.PlainSubs.Add(sub);
                if (!node.PackedListEnabled && node.PlainSubs.Count > 256)
                {
                    node.PackedListEnabled = true;
                    Interlocked.Increment(ref _highFanoutNodes);
                }
            }
            else
            {
                if (!node.QueueSubs.TryGetValue(sub.Queue, out var qset))
                {
                    qset = [];
                    node.QueueSubs[sub.Queue] = qset;
                }
                qset.Add(sub);
            }

            _count++;
            _inserts++;
            Interlocked.Increment(ref _generation);
            if (sub.Queue != null && _queueInsertNotifications.Count > 0)
                CheckForQueueInsertNotificationNoLock(sub.Subject, sub.Queue);
            if (!hadInterest && _count > 0)
                _interestStateNotification?.Invoke(true);
            InterestChanged?.Invoke(new InterestChange(
                InterestChangeKind.LocalAdded,
                sub.Subject,
                sub.Queue,
                sub.Client?.Account?.Name ?? "$G"));
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void Remove(Subscription sub)
    {
        if (_disposed) return;
        _lock.EnterWriteLock();
        try
        {
            var hadInterest = _count > 0;
            if (RemoveInternal(sub))
            {
                _removes++;
                Interlocked.Increment(ref _generation);
                if (sub.Queue != null && _queueRemoveNotifications.Count > 0)
                    CheckForQueueRemoveNotificationNoLock(sub.Subject, sub.Queue);
                if (hadInterest && _count == 0)
                    _interestStateNotification?.Invoke(false);
                InterestChanged?.Invoke(new InterestChange(
                    InterestChangeKind.LocalRemoved,
                    sub.Subject,
                    sub.Queue,
                    sub.Client?.Account?.Name ?? "$G"));
            }
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    /// <summary>
    /// Core remove logic without lock acquisition or generation bumping.
    /// Assumes write lock is held. Returns true if a subscription was actually removed.
    /// </summary>
    private bool RemoveInternal(Subscription sub)
    {
        var level = _root;
        TrieNode? node = null;
        bool sawFwc = false;

        var pathList = new List<(TrieLevel level, TrieNode node, string token, bool isPwc, bool isFwc)>();

        foreach (var token in new TokenEnumerator(sub.Subject))
        {
            if (token.Length == 0 || sawFwc)
                return false;

            bool isPwc = token.Length == 1 && token[0] == SubjectMatch.Pwc;
            bool isFwc = token.Length == 1 && token[0] == SubjectMatch.Fwc;

            if (isPwc)
            {
                node = level.Pwc;
            }
            else if (isFwc)
            {
                node = level.Fwc;
                sawFwc = true;
            }
            else
            {
                if (!TryGetLiteralNode(level, token, out var existingToken, out node))
                    return false;

                pathList.Add((level, node, existingToken, isPwc: false, isFwc: false));
                if (node.Next == null)
                    return false; // corrupted trie state
                level = node.Next;
                continue;
            }

            if (node == null)
                return false; // not found

            var tokenStr = isPwc ? "*" : ">";
            pathList.Add((level, node, tokenStr, isPwc, isFwc));
            if (node.Next == null)
                return false; // corrupted trie state
            level = node.Next;
        }

        if (node == null) return false;

        // Remove from node
        bool removed;
        if (sub.Queue == null)
        {
            removed = node.PlainSubs.Remove(sub);
        }
        else
        {
            removed = false;
            if (node.QueueSubs.TryGetValue(sub.Queue, out var qset))
            {
                removed = qset.Remove(sub);
                if (qset.Count == 0)
                    node.QueueSubs.Remove(sub.Queue);
            }
        }

        if (!removed) return false;

        _count--;

        // Prune empty nodes (walk backwards)
        for (int i = pathList.Count - 1; i >= 0; i--)
        {
            var (l, n, t, isPwc, isFwc) = pathList[i];
            if (n.IsEmpty)
            {
                if (isPwc) l.Pwc = null;
                else if (isFwc) l.Fwc = null;
                else l.Nodes.Remove(t);
            }
        }

        return true;
    }

    public SubListResult Match(string subject)
    {
        Interlocked.Increment(ref _matches);
        var currentGen = Interlocked.Read(ref _generation);

        _lock.EnterReadLock();
        try
        {
            if (_cache != null && _cache.TryGetValue(subject, out var cached) && cached.Generation == currentGen)
            {
                Interlocked.Increment(ref _cacheHits);
                return cached.Result;
            }
        }
        finally
        {
            _lock.ExitReadLock();
        }

        var tokens = Tokenize(subject);
        if (tokens == null)
            return SubListResult.Empty;

        _lock.EnterWriteLock();
        try
        {
            currentGen = Interlocked.Read(ref _generation);
            if (_cache != null && _cache.TryGetValue(subject, out var cached) && cached.Generation == currentGen)
            {
                Interlocked.Increment(ref _cacheHits);
                return cached.Result;
            }

            var builder = RentMatchBuilder();
            MatchLevel(_root, tokens, 0, builder);
            var result = builder.ToResult();

            if (_cache != null)
            {
                _cache[subject] = new CachedResult(result, currentGen);
                if (_cache.Count > CacheMax)
                    _sweeper.ScheduleSweep(SweepCache);
            }

            return result;
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public SubListResult MatchBytes(ReadOnlySpan<byte> subjectUtf8)
    {
        return Match(Encoding.ASCII.GetString(subjectUtf8));
    }

    public IReadOnlyList<RemoteSubscription> MatchRemote(string account, string subject)
    {
        _lock.EnterReadLock();
        try
        {
            var expanded = new List<RemoteSubscription>();
            foreach (var remoteSub in _remoteSubs.Values)
            {
                if (remoteSub.IsRemoval)
                    continue;
                if (!string.Equals(remoteSub.Account, account, StringComparison.Ordinal))
                    continue;
                if (!SubjectMatch.MatchLiteral(subject, remoteSub.Subject))
                    continue;

                var weight = Math.Max(1, remoteSub.QueueWeight);
                for (var i = 0; i < weight; i++)
                    expanded.Add(remoteSub);
            }

            return expanded;
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    private static string QueueNotifyKey(string subject, string queue) => $"{subject} {queue}";

    private static bool AddQueueNotifyNoLock(Dictionary<string, List<Action<bool>>> map, string key, Action<bool> callback)
    {
        if (!map.TryGetValue(key, out var callbacks))
        {
            callbacks = [];
            map[key] = callbacks;
        }
        else if (callbacks.Contains(callback))
        {
            return false;
        }

        callbacks.Add(callback);
        return true;
    }

    private static bool RemoveQueueNotifyNoLock(Dictionary<string, List<Action<bool>>> map, string key, Action<bool> callback)
    {
        if (!map.TryGetValue(key, out var callbacks))
            return false;

        var removed = callbacks.Remove(callback);
        if (callbacks.Count == 0)
            map.Remove(key);
        return removed;
    }

    private static bool TryGetLiteralNode(TrieLevel level, ReadOnlySpan<char> token, out string existingToken, out TrieNode node)
    {
        foreach (var (candidate, existingNode) in level.Nodes)
        {
            if (!SubjectMatch.TokenEquals(token, candidate))
                continue;

            existingToken = candidate;
            node = existingNode;
            return true;
        }

        existingToken = string.Empty;
        node = null!;
        return false;
    }

    private static MatchBuilder RentMatchBuilder()
    {
        var builder = s_matchBuilder ??= new MatchBuilder();
        builder.Reset();
        return builder;
    }

    private static List<RoutedSubKey> RentRemoteSubRemovalKeys()
    {
        var keys = s_remoteSubRemovalKeys ??= [];
        keys.Clear();
        return keys;
    }

    private bool HasExactQueueInterestNoLock(string subject, string queue)
    {
        var subs = new List<Subscription>();
        CollectAll(_root, subs);
        foreach (var sub in subs)
        {
            if (sub.Queue != null
                && string.Equals(sub.Subject, subject, StringComparison.Ordinal)
                && string.Equals(sub.Queue, queue, StringComparison.Ordinal))
            {
                return true;
            }
        }

        return false;
    }

    private void CheckForQueueInsertNotificationNoLock(string subject, string queue)
    {
        var key = QueueNotifyKey(subject, queue);
        if (!_queueInsertNotifications.TryGetValue(key, out var callbacks) || callbacks.Count == 0)
            return;

        foreach (var callback in callbacks)
            callback(true);

        if (!_queueRemoveNotifications.TryGetValue(key, out var removeCallbacks))
        {
            removeCallbacks = [];
            _queueRemoveNotifications[key] = removeCallbacks;
        }
        removeCallbacks.AddRange(callbacks);
        _queueInsertNotifications.Remove(key);
    }

    private void CheckForQueueRemoveNotificationNoLock(string subject, string queue)
    {
        var key = QueueNotifyKey(subject, queue);
        if (!_queueRemoveNotifications.TryGetValue(key, out var callbacks) || callbacks.Count == 0)
            return;
        if (HasExactQueueInterestNoLock(subject, queue))
            return;

        foreach (var callback in callbacks)
            callback(false);

        if (!_queueInsertNotifications.TryGetValue(key, out var insertCallbacks))
        {
            insertCallbacks = [];
            _queueInsertNotifications[key] = insertCallbacks;
        }
        insertCallbacks.AddRange(callbacks);
        _queueRemoveNotifications.Remove(key);
    }

    private void SweepCache()
    {
        _lock.EnterWriteLock();
        try
        {
            if (_cache == null || _cache.Count <= CacheMax)
                return;

            var removeCount = Math.Min(CacheSweep, _cache.Count - CacheMax);
            var keys = _cache.Keys.Take(removeCount).ToArray();
            foreach (var key in keys)
                _cache.Remove(key);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    /// <summary>
    /// Tokenize the subject into an array of token strings.
    /// Returns null if the subject is invalid (empty tokens).
    /// </summary>
    private static string[]? Tokenize(string subject)
    {
        if (string.IsNullOrEmpty(subject))
            return null;

        var tokens = new List<string>();
        int start = 0;
        for (int i = 0; i <= subject.Length; i++)
        {
            if (i == subject.Length || subject[i] == SubjectMatch.Sep)
            {
                if (i - start == 0)
                    return null; // empty token
                tokens.Add(subject[start..i]);
                start = i + 1;
            }
        }

        return tokens.Count > 0 ? tokens.ToArray() : null;
    }

    /// <summary>
    /// Recursively descend into the trie matching tokens.
    /// This follows the Go matchLevel() algorithm closely.
    /// </summary>
    private static void MatchLevel(TrieLevel? level, string[] tokens, int tokenIndex,
        List<Subscription> plainSubs, List<List<Subscription>> queueSubs)
    {
        TrieNode? pwc = null;
        TrieNode? node = null;

        for (int i = tokenIndex; i < tokens.Length; i++)
        {
            if (level == null)
                return;

            // Full wildcard (>) at this level matches all remaining tokens.
            if (level.Fwc != null)
                AddNodeToResults(level.Fwc, plainSubs, queueSubs);

            // Partial wildcard (*) -- recurse with remaining tokens.
            pwc = level.Pwc;
            if (pwc != null)
                MatchLevel(pwc.Next, tokens, i + 1, plainSubs, queueSubs);

            // Literal match
            node = null;
            if (level.Nodes.TryGetValue(tokens[i], out var found))
            {
                node = found;
                level = node.Next;
            }
            else
            {
                level = null;
            }
        }

        // After processing all tokens, add results from the final literal node.
        if (node != null)
            AddNodeToResults(node, plainSubs, queueSubs);

        // Also add results from the partial wildcard at the last level,
        // which handles the case where * matches the final token.
        if (pwc != null)
            AddNodeToResults(pwc, plainSubs, queueSubs);
    }

    private static void MatchLevel(TrieLevel? level, string[] tokens, int tokenIndex, MatchBuilder builder)
    {
        TrieNode? pwc = null;
        TrieNode? node = null;

        for (int i = tokenIndex; i < tokens.Length; i++)
        {
            if (level == null)
                return;

            if (level.Fwc != null)
                AddNodeToResults(level.Fwc, builder);

            pwc = level.Pwc;
            if (pwc != null)
                MatchLevel(pwc.Next, tokens, i + 1, builder);

            node = null;
            if (level.Nodes.TryGetValue(tokens[i], out var found))
            {
                node = found;
                level = node.Next;
            }
            else
            {
                level = null;
            }
        }

        if (node != null)
            AddNodeToResults(node, builder);

        if (pwc != null)
            AddNodeToResults(pwc, builder);
    }

    private static void AddNodeToResults(TrieNode node,
        List<Subscription> plainSubs, List<List<Subscription>> queueSubs)
    {
        // Add plain subscriptions
        foreach (var sub in node.PlainSubs)
            plainSubs.Add(sub);

        // Add queue subscriptions grouped by queue name
        foreach (var (queueName, subs) in node.QueueSubs)
        {
            if (subs.Count == 0) continue;

            // Find existing queue group or create new one
            List<Subscription>? existing = null;
            foreach (var qs in queueSubs)
            {
                if (qs.Count > 0 && qs[0].Queue == queueName)
                {
                    existing = qs;
                    break;
                }
            }
            if (existing == null)
            {
                existing = new List<Subscription>();
                queueSubs.Add(existing);
            }
            existing.AddRange(subs);
        }
    }

    private static void AddNodeToResults(TrieNode node, MatchBuilder builder)
    {
        builder.PlainSubs.AddRange(node.PlainSubs);

        foreach (var (queueName, subs) in node.QueueSubs)
        {
            if (subs.Count == 0)
                continue;

            builder.AddQueueGroup(queueName, subs);
        }
    }

    public SubListStats Stats()
    {
        _lock.EnterReadLock();
        uint numSubs, numCache;
        ulong inserts, removes;
        try
        {
            numSubs = _count;
            numCache = (uint)(_cache?.Count ?? 0);
            inserts = _inserts;
            removes = _removes;
        }
        finally
        {
            _lock.ExitReadLock();
        }

        var matches = Interlocked.Read(ref _matches);
        var cacheHits = Interlocked.Read(ref _cacheHits);
        var hitRate = matches > 0 ? (double)cacheHits / matches : 0.0;

        uint maxFanout = 0;
        long totalFanout = 0;
        int cacheEntries = 0;

        _lock.EnterReadLock();
        try
        {
            if (_cache != null)
            {
                foreach (var (_, entry) in _cache)
                {
                    var r = entry.Result;
                    var f = r.PlainSubs.Length + r.QueueSubs.Length;
                    totalFanout += f;
                    if (f > maxFanout) maxFanout = (uint)f;
                    cacheEntries++;
                }
            }
        }
        finally
        {
            _lock.ExitReadLock();
        }

        return new SubListStats
        {
            NumSubs = numSubs,
            NumCache = numCache,
            NumInserts = inserts,
            NumRemoves = removes,
            NumMatches = matches,
            CacheHitRate = hitRate,
            MaxFanout = maxFanout,
            AvgFanout = cacheEntries > 0 ? (double)totalFanout / cacheEntries : 0.0,
            TotalFanout = (int)totalFanout,
            CacheEntries = cacheEntries,
            CacheHits = cacheHits,
        };
    }

    public bool HasInterest(string subject)
    {
        var currentGen = Interlocked.Read(ref _generation);
        _lock.EnterReadLock();
        try
        {
            if (_cache != null && _cache.TryGetValue(subject, out var cached) && cached.Generation == currentGen)
            {
                var r = cached.Result;
                return r.PlainSubs.Length > 0 || r.QueueSubs.Length > 0;
            }
        }
        finally
        {
            _lock.ExitReadLock();
        }

        var tokens = Tokenize(subject);
        if (tokens == null) return false;

        _lock.EnterReadLock();
        try
        {
            return HasInterestLevel(_root, tokens, 0);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    public (int plainCount, int queueCount) NumInterest(string subject)
    {
        var tokens = Tokenize(subject);
        if (tokens == null) return (0, 0);

        _lock.EnterReadLock();
        try
        {
            int np = 0, nq = 0;
            CountInterestLevel(_root, tokens, 0, ref np, ref nq);
            return (np, nq);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    public void RemoveBatch(IEnumerable<Subscription> subs)
    {
        _lock.EnterWriteLock();
        try
        {
            var wasEnabled = _cache != null;
            _cache = null;

            foreach (var sub in subs)
            {
                if (RemoveInternal(sub))
                {
                    _removes++;
                    if (sub.Queue != null && _queueRemoveNotifications.Count > 0)
                        CheckForQueueRemoveNotificationNoLock(sub.Subject, sub.Queue);
                }
            }

            Interlocked.Increment(ref _generation);

            if (wasEnabled)
                _cache = new Dictionary<string, CachedResult>(StringComparer.Ordinal);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public IReadOnlyList<Subscription> All()
    {
        var subs = new List<Subscription>();
        _lock.EnterReadLock();
        try
        {
            CollectAllSubs(_root, subs);
        }
        finally
        {
            _lock.ExitReadLock();
        }
        return subs;
    }

    public IReadOnlyList<Subscription> LocalSubs(bool includeLeafHubs = false)
    {
        var subs = new List<Subscription>();
        _lock.EnterReadLock();
        try
        {
            CollectLocalSubs(_root, subs, includeLeafHubs);
        }
        finally
        {
            _lock.ExitReadLock();
        }
        return subs;
    }

    internal int NumLevels()
    {
        _lock.EnterReadLock();
        try
        {
            return VisitLevel(_root, 0);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    public SubListResult ReverseMatch(string subject)
    {
        var tokens = Tokenize(subject);
        if (tokens == null)
            return SubListResult.Empty;

        _lock.EnterReadLock();
        try
        {
            var plainSubs = new List<Subscription>();
            var queueSubs = new List<List<Subscription>>();
            ReverseMatchLevel(_root, tokens, 0, plainSubs, queueSubs);

            if (plainSubs.Count == 0 && queueSubs.Count == 0)
                return SubListResult.Empty;

            var queueSubsArr = new Subscription[queueSubs.Count][];
            for (int i = 0; i < queueSubs.Count; i++)
                queueSubsArr[i] = queueSubs[i].ToArray();
            return new SubListResult(plainSubs.ToArray(), queueSubsArr);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    private static bool HasInterestLevel(TrieLevel? level, string[] tokens, int tokenIndex)
    {
        TrieNode? pwc = null;
        TrieNode? node = null;

        for (int i = tokenIndex; i < tokens.Length; i++)
        {
            if (level == null) return false;
            if (level.Fwc != null && NodeHasInterest(level.Fwc)) return true;

            pwc = level.Pwc;
            if (pwc != null && HasInterestLevel(pwc.Next, tokens, i + 1)) return true;

            node = null;
            if (level.Nodes.TryGetValue(tokens[i], out var found))
            {
                node = found;
                level = node.Next;
            }
            else
            {
                level = null;
            }
        }

        if (node != null && NodeHasInterest(node)) return true;
        if (pwc != null && NodeHasInterest(pwc)) return true;
        return false;
    }

    private static bool NodeHasInterest(TrieNode node)
    {
        return node.PlainSubs.Count > 0 || node.QueueSubs.Count > 0;
    }

    private static void CountInterestLevel(TrieLevel? level, string[] tokens, int tokenIndex,
        ref int np, ref int nq)
    {
        TrieNode? pwc = null;
        TrieNode? node = null;

        for (int i = tokenIndex; i < tokens.Length; i++)
        {
            if (level == null) return;
            if (level.Fwc != null) AddNodeCounts(level.Fwc, ref np, ref nq);

            pwc = level.Pwc;
            if (pwc != null) CountInterestLevel(pwc.Next, tokens, i + 1, ref np, ref nq);

            node = null;
            if (level.Nodes.TryGetValue(tokens[i], out var found))
            {
                node = found;
                level = node.Next;
            }
            else
            {
                level = null;
            }
        }

        if (node != null) AddNodeCounts(node, ref np, ref nq);
        if (pwc != null) AddNodeCounts(pwc, ref np, ref nq);
    }

    private static void AddNodeCounts(TrieNode node, ref int np, ref int nq)
    {
        np += node.PlainSubs.Count;
        foreach (var (_, qset) in node.QueueSubs)
            nq += qset.Count;
    }

    private static void CollectAllSubs(TrieLevel level, List<Subscription> subs)
    {
        foreach (var (_, node) in level.Nodes)
        {
            foreach (var sub in node.PlainSubs)
                subs.Add(sub);
            foreach (var (_, qset) in node.QueueSubs)
                foreach (var sub in qset)
                    subs.Add(sub);
            if (node.Next != null)
                CollectAllSubs(node.Next, subs);
        }
        if (level.Pwc != null)
        {
            foreach (var sub in level.Pwc.PlainSubs)
                subs.Add(sub);
            foreach (var (_, qset) in level.Pwc.QueueSubs)
                foreach (var sub in qset)
                    subs.Add(sub);
            if (level.Pwc.Next != null)
                CollectAllSubs(level.Pwc.Next, subs);
        }
        if (level.Fwc != null)
        {
            foreach (var sub in level.Fwc.PlainSubs)
                subs.Add(sub);
            foreach (var (_, qset) in level.Fwc.QueueSubs)
                foreach (var sub in qset)
                    subs.Add(sub);
            if (level.Fwc.Next != null)
                CollectAllSubs(level.Fwc.Next, subs);
        }
    }

    private static void CollectLocalSubs(TrieLevel level, List<Subscription> subs, bool includeLeafHubs)
    {
        foreach (var (_, node) in level.Nodes)
        {
            AddNodeLocalSubs(node, subs, includeLeafHubs);
            if (node.Next != null)
                CollectLocalSubs(node.Next, subs, includeLeafHubs);
        }
        if (level.Pwc != null)
        {
            AddNodeLocalSubs(level.Pwc, subs, includeLeafHubs);
            if (level.Pwc.Next != null)
                CollectLocalSubs(level.Pwc.Next, subs, includeLeafHubs);
        }
        if (level.Fwc != null)
        {
            AddNodeLocalSubs(level.Fwc, subs, includeLeafHubs);
            if (level.Fwc.Next != null)
                CollectLocalSubs(level.Fwc.Next, subs, includeLeafHubs);
        }
    }

    private static void AddNodeLocalSubs(TrieNode node, List<Subscription> subs, bool includeLeafHubs)
    {
        foreach (var sub in node.PlainSubs)
            AddLocalSub(sub, subs, includeLeafHubs);
        foreach (var (_, qset) in node.QueueSubs)
            foreach (var sub in qset)
                AddLocalSub(sub, subs, includeLeafHubs);
    }

    private static void AddLocalSub(Subscription sub, List<Subscription> subs, bool includeLeafHubs)
    {
        if (sub.Client == null)
            return;

        var kind = sub.Client.Kind;
        if (kind is global::NATS.Server.ClientKind.Client
            or global::NATS.Server.ClientKind.System
            or global::NATS.Server.ClientKind.JetStream
            or global::NATS.Server.ClientKind.Account
            || (includeLeafHubs && kind == global::NATS.Server.ClientKind.Leaf))
        {
            subs.Add(sub);
        }
    }

    private static int VisitLevel(TrieLevel? level, int depth)
    {
        if (level == null || (level.Nodes.Count == 0 && level.Pwc == null && level.Fwc == null))
            return depth;

        depth++;
        var maxDepth = depth;
        foreach (var (_, node) in level.Nodes)
        {
            var childDepth = VisitLevel(node.Next, depth);
            if (childDepth > maxDepth)
                maxDepth = childDepth;
        }
        if (level.Pwc != null)
        {
            var pwcDepth = VisitLevel(level.Pwc.Next, depth);
            if (pwcDepth > maxDepth)
                maxDepth = pwcDepth;
        }
        if (level.Fwc != null)
        {
            var fwcDepth = VisitLevel(level.Fwc.Next, depth);
            if (fwcDepth > maxDepth)
                maxDepth = fwcDepth;
        }

        return maxDepth;
    }

    private static void ReverseMatchLevel(TrieLevel? level, string[] tokens, int tokenIndex,
        List<Subscription> plainSubs, List<List<Subscription>> queueSubs)
    {
        if (level == null || tokenIndex >= tokens.Length)
            return;

        var token = tokens[tokenIndex];
        bool isLast = tokenIndex == tokens.Length - 1;

        if (token == ">")
        {
            CollectAllNodes(level, plainSubs, queueSubs);
            return;
        }

        if (token == "*")
        {
            foreach (var (_, node) in level.Nodes)
            {
                if (isLast)
                    AddNodeToResults(node, plainSubs, queueSubs);
                else
                    ReverseMatchLevel(node.Next, tokens, tokenIndex + 1, plainSubs, queueSubs);
            }
        }
        else
        {
            if (level.Nodes.TryGetValue(token, out var node))
            {
                if (isLast)
                    AddNodeToResults(node, plainSubs, queueSubs);
                else
                    ReverseMatchLevel(node.Next, tokens, tokenIndex + 1, plainSubs, queueSubs);
            }
        }

        if (level.Pwc != null)
        {
            if (isLast)
                AddNodeToResults(level.Pwc, plainSubs, queueSubs);
            else
                ReverseMatchLevel(level.Pwc.Next, tokens, tokenIndex + 1, plainSubs, queueSubs);
        }
        if (level.Fwc != null)
        {
            AddNodeToResults(level.Fwc, plainSubs, queueSubs);
        }
    }

    private static void CollectAllNodes(TrieLevel level, List<Subscription> plainSubs,
        List<List<Subscription>> queueSubs)
    {
        foreach (var (_, node) in level.Nodes)
        {
            AddNodeToResults(node, plainSubs, queueSubs);
            if (node.Next != null)
                CollectAllNodes(node.Next, plainSubs, queueSubs);
        }
        if (level.Pwc != null)
        {
            AddNodeToResults(level.Pwc, plainSubs, queueSubs);
            if (level.Pwc.Next != null)
                CollectAllNodes(level.Pwc.Next, plainSubs, queueSubs);
        }
        if (level.Fwc != null)
        {
            AddNodeToResults(level.Fwc, plainSubs, queueSubs);
            if (level.Fwc.Next != null)
                CollectAllNodes(level.Fwc.Next, plainSubs, queueSubs);
        }
    }

    /// <summary>Enumerates '.' separated tokens in a subject without allocating.</summary>
    private ref struct TokenEnumerator
    {
        private ReadOnlySpan<char> _remaining;

        public TokenEnumerator(string subject)
        {
            _remaining = subject.AsSpan();
            Current = default;
        }

        public ReadOnlySpan<char> Current { get; private set; }

        public TokenEnumerator GetEnumerator() => this;

        public bool MoveNext()
        {
            if (_remaining.IsEmpty)
                return false;

            int sep = _remaining.IndexOf(SubjectMatch.Sep);
            if (sep < 0)
            {
                Current = _remaining;
                _remaining = default;
            }
            else
            {
                Current = _remaining[..sep];
                _remaining = _remaining[(sep + 1)..];
            }
            return true;
        }
    }

    private sealed class TrieLevel
    {
        public readonly Dictionary<string, TrieNode> Nodes = new(StringComparer.Ordinal);
        public TrieNode? Pwc; // partial wildcard (*)
        public TrieNode? Fwc; // full wildcard (>)
    }

    private sealed class TrieNode
    {
        public TrieLevel? Next;
        public readonly HashSet<Subscription> PlainSubs = [];
        public readonly Dictionary<string, HashSet<Subscription>> QueueSubs = new(StringComparer.Ordinal);
        public bool PackedListEnabled;

        public bool IsEmpty => PlainSubs.Count == 0 && QueueSubs.Count == 0 &&
                               (Next == null || (Next.Nodes.Count == 0 && Next.Pwc == null && Next.Fwc == null));
    }

    private sealed class MatchBuilder
    {
        private readonly Dictionary<string, int> _queueIndexes = new(StringComparer.Ordinal);
        private readonly List<List<Subscription>> _queueGroups = [];
        private int _queueGroupCount;

        public List<Subscription> PlainSubs { get; } = [];

        public void Reset()
        {
            PlainSubs.Clear();
            _queueIndexes.Clear();
            for (var i = 0; i < _queueGroupCount; i++)
                _queueGroups[i].Clear();
            _queueGroupCount = 0;
        }

        public void AddQueueGroup(string queueName, HashSet<Subscription> subs)
        {
            if (!_queueIndexes.TryGetValue(queueName, out var index))
            {
                index = _queueGroupCount++;
                _queueIndexes[queueName] = index;
                if (index == _queueGroups.Count)
                    _queueGroups.Add([]);
            }

            _queueGroups[index].AddRange(subs);
        }

        public SubListResult ToResult()
        {
            if (PlainSubs.Count == 0 && _queueGroupCount == 0)
                return SubListResult.Empty;

            var queueSubsArr = new Subscription[_queueGroupCount][];
            for (var i = 0; i < _queueGroupCount; i++)
                queueSubsArr[i] = _queueGroups[i].ToArray();

            return new SubListResult(PlainSubs.ToArray(), queueSubsArr);
        }
    }
}
