using System.Collections.Concurrent;

namespace NATS.Server.JetStream.Api;

/// <summary>
/// Limits concurrent JetStream API requests and deduplicates by request ID.
/// Go reference: jetstream_api.go rate limiting and deduplication logic.
/// The Go server uses a semaphore (default 256 slots) to prevent request storms and a
/// dedup cache keyed by Nats-Msg-Id to serve identical requests without reprocessing.
/// </summary>
public sealed class ApiRateLimiter : IDisposable
{
    private readonly SemaphoreSlim _semaphore;
    private readonly ConcurrentDictionary<string, CachedResponse> _dedupCache = new();
    private readonly TimeSpan _dedupTtl;
    private readonly int _maxConcurrent;

    public ApiRateLimiter(int maxConcurrent = 256, TimeSpan? dedupTtl = null)
    {
        _maxConcurrent = maxConcurrent;
        _semaphore = new SemaphoreSlim(maxConcurrent, maxConcurrent);
        _dedupTtl = dedupTtl ?? TimeSpan.FromSeconds(5);
    }

    /// <summary>Current number of in-flight requests.</summary>
    public int ActiveCount => _maxConcurrent - _semaphore.CurrentCount;

    /// <summary>Number of cached dedup responses.</summary>
    public int DedupCacheCount => _dedupCache.Count;

    /// <summary>
    /// Attempts to acquire a concurrency slot. Returns false if the limit is reached.
    /// Go reference: jetstream_api.go — non-blocking semaphore acquire; request is rejected
    /// immediately if no slots are available rather than queuing indefinitely.
    /// </summary>
    public async Task<bool> TryAcquireAsync(CancellationToken ct = default)
    {
        return await _semaphore.WaitAsync(0, ct);
    }

    /// <summary>
    /// Releases a concurrency slot.
    /// </summary>
    public void Release()
    {
        _semaphore.Release();
    }

    /// <summary>
    /// Checks if a request with the given ID has a cached response.
    /// Returns the cached response if found and not expired, null otherwise.
    /// Go reference: jetstream_api.go — dedup cache is keyed by Nats-Msg-Id header value.
    /// </summary>
    public JetStreamApiResponse? GetCachedResponse(string? requestId)
    {
        if (string.IsNullOrEmpty(requestId))
            return null;

        if (_dedupCache.TryGetValue(requestId, out var cached))
        {
            if (DateTime.UtcNow - cached.CachedAt < _dedupTtl)
                return cached.Response;

            // Expired — remove and return null.
            _dedupCache.TryRemove(requestId, out _);
        }

        return null;
    }

    /// <summary>
    /// Caches a response for deduplication.
    /// Go reference: jetstream_api.go — response is stored with a timestamp so that
    /// subsequent requests with the same Nats-Msg-Id within the TTL window get the same result.
    /// </summary>
    public void CacheResponse(string? requestId, JetStreamApiResponse response)
    {
        if (string.IsNullOrEmpty(requestId))
            return;

        _dedupCache[requestId] = new CachedResponse(response, DateTime.UtcNow);
    }

    /// <summary>
    /// Removes expired entries from the dedup cache.
    /// Call periodically to prevent unbounded growth.
    /// Go reference: jetstream_api.go — Go's dedup window uses a sliding expiry; entries older
    /// than dedupWindow are dropped on the next sweep.
    /// </summary>
    public int PurgeExpired()
    {
        var cutoff = DateTime.UtcNow - _dedupTtl;
        var removed = 0;
        foreach (var (key, value) in _dedupCache)
        {
            if (value.CachedAt < cutoff && _dedupCache.TryRemove(key, out _))
                removed++;
        }
        return removed;
    }

    public void Dispose()
    {
        _semaphore.Dispose();
    }

    private sealed record CachedResponse(JetStreamApiResponse Response, DateTime CachedAt);
}
