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 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>
    /// <param name="trie">The permission trie to install.</param>
    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>
    /// <param name="clusterId">The cluster identifier.</param>
    /// <returns>The current trie for the cluster, or null when nothing is installed.</returns>
    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>
    /// <param name="clusterId">The cluster identifier.</param>
    /// <param name="generationId">The generation identifier.</param>
    /// <returns>The trie for that cluster and generation, or null if that pair isn't cached.</returns>
    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>
    /// <param name="clusterId">The cluster identifier.</param>
    /// <returns>The current generation id for the cluster, or null if nothing is cached.</returns>
    public long? CurrentGenerationId(string clusterId)
        => _byCluster.TryGetValue(clusterId, out var entry) ? entry.Current.GenerationId : null;

    /// <summary>Drop every cached trie for one cluster.</summary>
    /// <param name="clusterId">The cluster identifier.</param>
    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. Thread-safe: uses a CAS loop with
    ///     <see cref="ConcurrentDictionary{TKey,TValue}.TryUpdate"/> (reference equality on the
    ///     class-typed entry) so a concurrent <see cref="Install"/> on the same cluster is never
    ///     silently overwritten.
    /// </summary>
    /// <param name="clusterId">The cluster identifier.</param>
    /// <param name="keepLatest">The number of most recent generations to retain.</param>
    public void Prune(string clusterId, int keepLatest = 3)
    {
        if (keepLatest < 1) throw new ArgumentOutOfRangeException(nameof(keepLatest), keepLatest, "keepLatest must be >= 1");

        // CAS retry loop: read a snapshot, compute the pruned entry, atomically swap.
        // Retry if another writer (Install or a concurrent Prune) updated the entry first.
        while (true)
        {
            if (!_byCluster.TryGetValue(clusterId, out var observed)) return;
            if (observed.Tries.Count <= keepLatest) return;

            var keep = observed.Tries
                .OrderByDescending(kvp => kvp.Key)
                .Take(keepLatest)
                .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

            // Preserve the current pointer; if it was pruned (shouldn't happen since Current
            // is always the newest generation), fall back to the newest retained entry.
            var current = keep.TryGetValue(observed.Current.GenerationId, out var kept)
                ? kept
                : keep.OrderByDescending(kvp => kvp.Key).First().Value;

            var pruned = new ClusterEntry(current, keep);
            // TryUpdate uses reference equality for ClusterEntry (class, not record) so it
            // succeeds only when the stored reference is still the one we observed.
            if (_byCluster.TryUpdate(clusterId, pruned, observed))
                return;
            // Another thread updated the entry between our read and our write — re-read and retry.
        }
    }

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

    // Class (not record) so TryUpdate in Prune uses reference equality for the CAS comparison.
    private sealed class ClusterEntry(PermissionTrie current, IReadOnlyDictionary<long, PermissionTrie> tries)
    {
        /// <summary>Gets the current (latest) permission trie for the cluster.</summary>
        public PermissionTrie Current { get; } = current;
        /// <summary>Gets all cached tries for the cluster keyed by generation ID.</summary>
        public IReadOnlyDictionary<long, PermissionTrie> Tries { get; } = tries;

        /// <summary>Creates a cluster entry from a single trie.</summary>
        /// <param name="trie">The permission trie to create the entry from.</param>
        /// <returns>A new cluster entry whose current trie and generation map both hold only <paramref name="trie"/>.</returns>
        public static ClusterEntry FromSingle(PermissionTrie trie) =>
            new(trie, new Dictionary<long, PermissionTrie> { [trie.GenerationId] = trie });

        /// <summary>Creates a new entry with an additional trie, updating current if it's newer.</summary>
        /// <param name="trie">The new permission trie to add.</param>
        /// <returns>A new cluster entry with the trie merged in and the current pointer updated if needed.</returns>
        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);
        }
    }
}
