using System.Collections.Concurrent;
using System.Text;
using System.Text.RegularExpressions;

namespace ZB.MOM.WW.GalaxyRepository;

/// <summary>
///     Anchored, case-insensitive glob matcher (<c>*</c> and <c>?</c> wildcards) used by the
///     hierarchy and browse projectors to filter object tag names and browse subtrees.
///     Compiled regexes are cached and the cache is bounded so an unbounded stream of distinct
///     client-supplied globs cannot grow memory without limit.
/// </summary>
public static class GalaxyGlobMatcher
{
    /// <summary>
    ///     Maximum number of compiled-regex entries retained in <see cref="RegexCache"/>.
    ///     The cache is keyed by glob pattern and patterns flow in from two sources:
    ///     admin-controlled API-key constraints (naturally bounded) and the
    ///     client-supplied <c>DiscoverHierarchyRequest.TagNameGlob</c> (unbounded — a
    ///     client can iterate through generated names and create millions of distinct
    ///     globs over the process lifetime). Capping the cache bounds memory while
    ///     keeping the hot working set hit-cached.
    /// </summary>
    internal const int RegexCacheCapacity = 256;

    /// <summary>
    ///     Bounded compiled-regex cache keyed by glob pattern. <c>IsMatch</c> is called
    ///     once per object per <c>DiscoverHierarchy</c>/<c>WatchDeployEvents</c>
    ///     evaluation, so the same handful of glob patterns are translated
    ///     repeatedly; caching avoids rebuilding and recompiling the regex on every
    ///     call. Beyond <see cref="RegexCacheCapacity"/> entries the oldest insertion
    ///     is evicted so a client cannot grow the cache without bound by submitting
    ///     unique patterns. Eviction is approximate (FIFO over insertion order, not
    ///     true LRU) because we only need the bound, not exact recency tracking.
    /// </summary>
    private static readonly ConcurrentDictionary<string, Regex> RegexCache = new(StringComparer.Ordinal);

    /// <summary>
    ///     Insertion-order queue used to evict the oldest cache entry when the cache
    ///     exceeds <see cref="RegexCacheCapacity"/>. A separate queue keeps the
    ///     <see cref="RegexCache"/> reads lock-free; the lock below only guards the
    ///     eviction path.
    /// </summary>
    private static readonly ConcurrentQueue<string> InsertionOrder = new();
    private static readonly object EvictionLock = new();

    /// <summary>
    ///     Current cache size, exposed for tests asserting the cap is honoured.
    /// </summary>
    internal static int CurrentCacheSize => RegexCache.Count;

    /// <summary>Determines whether a value matches a glob pattern (with * and ? wildcards).</summary>
    /// <param name="value">The value to test against the glob pattern.</param>
    /// <param name="glob">The glob pattern with * and ? wildcards.</param>
    public static bool IsMatch(string value, string glob)
    {
        if (string.IsNullOrWhiteSpace(glob))
        {
            return true;
        }

        return GetOrCreateRegex(glob).IsMatch(value ?? string.Empty);
    }

    private static Regex GetOrCreateRegex(string glob)
    {
        if (RegexCache.TryGetValue(glob, out Regex? existing))
        {
            return existing;
        }

        Regex compiled = new(
            BuildRegex(glob),
            RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled,
            TimeSpan.FromMilliseconds(100));

        // GetOrAdd atomically returns whichever instance is in the cache after the
        // call — either the locally-compiled regex (we won the race) or the regex
        // another thread inserted (we lost). It also avoids the TryAdd-then-indexer
        // pattern where the key could be evicted between the failed TryAdd and the
        // indexer read, producing a KeyNotFoundException under contention near the cap.
        Regex result = RegexCache.GetOrAdd(glob, compiled);
        if (ReferenceEquals(result, compiled))
        {
            // We were the inserter — track for FIFO eviction and bound the cache.
            InsertionOrder.Enqueue(glob);
            EvictIfOverCapacity();
        }
        return result;
    }

    private static void EvictIfOverCapacity()
    {
        if (RegexCache.Count <= RegexCacheCapacity)
        {
            return;
        }

        // Serialize eviction so two threads do not race past the cap together.
        lock (EvictionLock)
        {
            while (RegexCache.Count > RegexCacheCapacity && InsertionOrder.TryDequeue(out string? oldest))
            {
                RegexCache.TryRemove(oldest, out _);
            }
        }
    }

    private static string BuildRegex(string glob)
    {
        StringBuilder builder = new("^", glob.Length + 2);
        foreach (char character in glob)
        {
            switch (character)
            {
                case '*':
                    builder.Append(".*");
                    break;
                case '?':
                    builder.Append('.');
                    break;
                default:
                    builder.Append(Regex.Escape(character.ToString()));
                    break;
            }
        }

        builder.Append('$');
        return builder.ToString();
    }
}
