using System.Collections.Concurrent;

namespace ZB.MOM.WW.OtOpcUa.Core.Authorization;

/// <summary>
///     Process-singleton cache of <see cref="PermissionTrie"/> instances keyed on
///     <c>(ClusterId, GenerationId)</c>. Hot-path evaluation reads
///     <see cref="GetTrie(string)"/> without awaiting DB access; the cache is populated
///     out-of-band on publish + on first reference via
///     <see cref="Install(PermissionTrie)"/>.
/// </summary>
/// <remarks>
///     Per decision #148 and Phase 6.2 Stream B.4 the cache is generation-sealed: once a
///     trie is installed for <c>(ClusterId, GenerationId)</c> the entry is immutable. When a
///     new generation publishes, the caller calls <see cref="Install"/> with the new trie
///     + the cache atomically updates its "current generation" pointer for that cluster.
///     Older generations are retained so an in-flight request evaluating the prior generation
///     still succeeds — GC via <see cref="Prune(string, int)"/>.
/// </remarks>
public sealed class PermissionTrieCache
{
    private readonly ConcurrentDictionary<string, ClusterEntry> _byCluster =
        new(StringComparer.OrdinalIgnoreCase);

    /// <summary>Install a trie for a cluster + make it the current generation.</summary>
    public void Install(PermissionTrie trie)
    {
        ArgumentNullException.ThrowIfNull(trie);
        _byCluster.AddOrUpdate(trie.ClusterId,
            _ => ClusterEntry.FromSingle(trie),
            (_, existing) => existing.WithAdditional(trie));
    }

    /// <summary>Get the current-generation trie for a cluster; null when nothing installed.</summary>
    public PermissionTrie? GetTrie(string clusterId)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(clusterId);
        return _byCluster.TryGetValue(clusterId, out var entry) ? entry.Current : null;
    }

    /// <summary>Get a specific (cluster, generation) trie; null if that pair isn't cached.</summary>
    public PermissionTrie? GetTrie(string clusterId, long generationId)
    {
        if (!_byCluster.TryGetValue(clusterId, out var entry)) return null;
        return entry.Tries.TryGetValue(generationId, out var trie) ? trie : null;
    }

    /// <summary>The generation id the <see cref="GetTrie(string)"/> shortcut currently serves for a cluster.</summary>
    public long? CurrentGenerationId(string clusterId)
        => _byCluster.TryGetValue(clusterId, out var entry) ? entry.Current.GenerationId : null;

    /// <summary>Drop every cached trie for one cluster.</summary>
    public void Invalidate(string clusterId) => _byCluster.TryRemove(clusterId, out _);

    /// <summary>
    ///     Retain only the most-recent <paramref name="keepLatest"/> generations for a cluster.
    ///     No-op when there's nothing to drop.
    /// </summary>
    public void Prune(string clusterId, int keepLatest = 3)
    {
        if (keepLatest < 1) throw new ArgumentOutOfRangeException(nameof(keepLatest), keepLatest, "keepLatest must be >= 1");
        if (!_byCluster.TryGetValue(clusterId, out var entry)) return;

        if (entry.Tries.Count <= keepLatest) return;
        var keep = entry.Tries
            .OrderByDescending(kvp => kvp.Key)
            .Take(keepLatest)
            .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
        _byCluster[clusterId] = new ClusterEntry(entry.Current, keep);
    }

    /// <summary>Diagnostics counter: number of cached (cluster, generation) tries.</summary>
    public int CachedTrieCount => _byCluster.Values.Sum(e => e.Tries.Count);

    private sealed record ClusterEntry(PermissionTrie Current, IReadOnlyDictionary<long, PermissionTrie> Tries)
    {
        public static ClusterEntry FromSingle(PermissionTrie trie) =>
            new(trie, new Dictionary<long, PermissionTrie> { [trie.GenerationId] = trie });

        public ClusterEntry WithAdditional(PermissionTrie trie)
        {
            var next = new Dictionary<long, PermissionTrie>(Tries) { [trie.GenerationId] = trie };
            // The highest generation wins as "current" — handles out-of-order installs.
            var current = trie.GenerationId >= Current.GenerationId ? trie : Current;
            return new ClusterEntry(current, next);
        }
    }
}
