using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace StellaOps.Auth.Client;
/// 
/// Caches JWKS documents for Authority.
/// 
public sealed class StellaOpsJwksCache
{
    private readonly HttpClient httpClient;
    private readonly StellaOpsDiscoveryCache discoveryCache;
    private readonly IOptionsMonitor optionsMonitor;
    private readonly TimeProvider timeProvider;
    private readonly ILogger? logger;
    private JsonWebKeySet? cachedSet;
    private DateTimeOffset cacheExpiresAt;
    private DateTimeOffset offlineExpiresAt;
    public StellaOpsJwksCache(
        HttpClient httpClient,
        StellaOpsDiscoveryCache discoveryCache,
        IOptionsMonitor optionsMonitor,
        TimeProvider? timeProvider = null,
        ILogger? logger = null)
    {
        this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        this.discoveryCache = discoveryCache ?? throw new ArgumentNullException(nameof(discoveryCache));
        this.optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
        this.timeProvider = timeProvider ?? TimeProvider.System;
        this.logger = logger;
    }
    public async Task GetAsync(CancellationToken cancellationToken)
    {
        var now = timeProvider.GetUtcNow();
        if (cachedSet is not null && now < cacheExpiresAt)
        {
            return cachedSet;
        }
        var options = optionsMonitor.CurrentValue;
        var configuration = await discoveryCache.GetAsync(cancellationToken).ConfigureAwait(false);
        logger?.LogDebug("Fetching JWKS from {JwksUri}.", configuration.JwksEndpoint);
        try
        {
            using var response = await httpClient.GetAsync(configuration.JwksEndpoint, cancellationToken).ConfigureAwait(false);
            response.EnsureSuccessStatusCode();
            var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
            cachedSet = new JsonWebKeySet(json);
            cacheExpiresAt = now + options.JwksCacheLifetime;
            offlineExpiresAt = cacheExpiresAt + options.OfflineCacheTolerance;
            return cachedSet;
        }
        catch (Exception exception) when (IsOfflineCandidate(exception, cancellationToken) && TryUseOfflineFallback(options, now, exception))
        {
            return cachedSet!;
        }
    }
    private static bool IsOfflineCandidate(Exception exception, CancellationToken cancellationToken)
    {
        if (exception is HttpRequestException)
        {
            return true;
        }
        if (exception is TaskCanceledException && !cancellationToken.IsCancellationRequested)
        {
            return true;
        }
        if (exception is TimeoutException)
        {
            return true;
        }
        return false;
    }
    private bool TryUseOfflineFallback(StellaOpsAuthClientOptions options, DateTimeOffset now, Exception exception)
    {
        if (!options.AllowOfflineCacheFallback || cachedSet is null)
        {
            return false;
        }
        if (options.OfflineCacheTolerance <= TimeSpan.Zero)
        {
            return false;
        }
        if (offlineExpiresAt == DateTimeOffset.MinValue)
        {
            return false;
        }
        if (now >= offlineExpiresAt)
        {
            return false;
        }
        logger?.LogWarning(exception, "JWKS fetch failed; reusing cached keys until {FallbackExpiresAt}.", offlineExpiresAt);
        return true;
    }
}