using System.Collections.Concurrent;

using LibApiKeyIdentity = ZB.MOM.WW.Auth.Abstractions.ApiKeys.ApiKeyIdentity;

namespace ZB.MOM.WW.MxGateway.Server.Security.Authentication;

/// <summary>
/// Maps the shared <see cref="ZB.MOM.WW.Auth.Abstractions.ApiKeys.ApiKeyIdentity"/> (which
/// carries the key's scopes plus the opaque constraints JSON blob) onto the gateway's
/// <see cref="ApiKeyIdentity"/> (which exposes the deserialized
/// <see cref="ApiKeyConstraints"/> the downstream authorization code enforces).
/// </summary>
/// <remarks>
/// The shared verifier does not interpret the constraints column; it returns the stored
/// JSON verbatim in <see cref="ZB.MOM.WW.Auth.Abstractions.ApiKeys.ApiKeyIdentity.Constraints"/>.
/// This mapper re-hydrates it via <see cref="ApiKeyConstraintSerializer"/> so the gateway's
/// constraint enforcement (<c>ConstraintEnforcer</c>) and request-identity accessor continue
/// to operate on the strongly-typed model unchanged.
/// </remarks>
public static class GatewayApiKeyIdentityMapper
{
    /// <summary>
    ///     Maximum number of parsed constraint blobs retained in <see cref="ConstraintCache"/>.
    ///     Blobs are admin-controlled (one per API key), so the cap is only a memory backstop for a
    ///     store with an unusually large number of distinct constrained keys.
    /// </summary>
    internal const int MaxCachedConstraintBlobs = 1024;

    /// <summary>
    ///     Bounded parsed-constraints cache keyed by the raw constraints JSON. The blob is parsed
    ///     once per authenticated RPC otherwise, so this keeps the JSON parse off the hot path.
    ///     Beyond <see cref="MaxCachedConstraintBlobs"/> entries the oldest insertion is evicted
    ///     rather than the cache refusing new entries — a hard stop at the cap would leave every key
    ///     admitted after it re-parsing its blob on every RPC for the process lifetime. Eviction is
    ///     approximate (FIFO over insertion order, not true LRU) because only the bound matters.
    /// </summary>
    private static readonly ConcurrentDictionary<string, ApiKeyConstraints> ConstraintCache =
        new(StringComparer.Ordinal);

    /// <summary>
    ///     Insertion-order queue used to evict the oldest cache entry once the cache exceeds
    ///     <see cref="MaxCachedConstraintBlobs"/>. Keeping it separate leaves
    ///     <see cref="ConstraintCache"/> reads lock-free; the lock guards only 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 => ConstraintCache.Count;

    private static ApiKeyConstraints DeserializeConstraints(string? constraintsJson)
    {
        if (string.IsNullOrWhiteSpace(constraintsJson))
        {
            return ApiKeyConstraints.Empty;
        }

        if (ConstraintCache.TryGetValue(constraintsJson, out ApiKeyConstraints? cached))
        {
            return cached;
        }

        ApiKeyConstraints parsed = ApiKeyConstraintSerializer.Deserialize(constraintsJson);

        // GetOrAdd returns whichever instance is in the cache after the call, so concurrent parsers
        // of the same blob converge on one instance; it also avoids the TryAdd-then-read race where
        // the key could be evicted between a failed TryAdd and the read back.
        ApiKeyConstraints result = ConstraintCache.GetOrAdd(constraintsJson, parsed);
        if (ReferenceEquals(result, parsed))
        {
            // We were the inserter — track for FIFO eviction and bound the cache.
            InsertionOrder.Enqueue(constraintsJson);
            EvictIfOverCapacity();
        }

        return result;
    }

    private static void EvictIfOverCapacity()
    {
        if (ConstraintCache.Count <= MaxCachedConstraintBlobs)
        {
            return;
        }

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

    /// <summary>
    /// Converts a shared API-key identity into the gateway identity, deserializing the opaque
    /// constraints JSON into <see cref="ApiKeyConstraints"/>.
    /// </summary>
    /// <param name="identity">The shared identity returned by the library verifier.</param>
    /// <returns>The gateway identity carrying the effective constraints.</returns>
    public static ApiKeyIdentity ToGatewayIdentity(LibApiKeyIdentity identity)
    {
        ArgumentNullException.ThrowIfNull(identity);

        // The library stores the opaque constraints blob in Constraints as the ConstraintsJson
        // string (or null when the key has no constraints).
        string? constraintsJson = identity.Constraints as string;

        return new ApiKeyIdentity(
            KeyId: identity.KeyId,
            // The gateway token prefix is fixed ("mxgw"); the key id is its own field. KeyPrefix
            // is retained only for surface compatibility with the gateway identity record.
            KeyPrefix: "mxgw",
            DisplayName: identity.DisplayName,
            Scopes: identity.Scopes,
            Constraints: DeserializeConstraints(constraintsJson));
    }
}
