using System;
using System.Net;
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Polly;
using Polly.Extensions.Http;

namespace StellaOps.Auth.Client;

/// <summary>
/// DI helpers for the StellaOps auth client.
/// </summary>
public static class ServiceCollectionExtensions
{
    /// <summary>
    /// Registers the StellaOps auth client with the provided configuration.
    /// </summary>
    public static IServiceCollection AddStellaOpsAuthClient(this IServiceCollection services, Action<StellaOpsAuthClientOptions> configure)
    {
        ArgumentNullException.ThrowIfNull(services);
        ArgumentNullException.ThrowIfNull(configure);

        services.AddOptions<StellaOpsAuthClientOptions>()
            .Configure(configure)
            .PostConfigure(static options => options.Validate());

        services.TryAddSingleton<IStellaOpsTokenCache, InMemoryTokenCache>();

        services.AddHttpClient<StellaOpsDiscoveryCache>((provider, client) =>
        {
            var options = provider.GetRequiredService<IOptionsMonitor<StellaOpsAuthClientOptions>>().CurrentValue;
            client.Timeout = options.HttpTimeout;
        }).AddPolicyHandler(static (provider, _) => CreateRetryPolicy(provider));

        services.AddHttpClient<StellaOpsJwksCache>((provider, client) =>
        {
            var options = provider.GetRequiredService<IOptionsMonitor<StellaOpsAuthClientOptions>>().CurrentValue;
            client.Timeout = options.HttpTimeout;
        }).AddPolicyHandler(static (provider, _) => CreateRetryPolicy(provider));

        services.AddHttpClient<IStellaOpsTokenClient, StellaOpsTokenClient>((provider, client) =>
        {
            var options = provider.GetRequiredService<IOptionsMonitor<StellaOpsAuthClientOptions>>().CurrentValue;
            client.Timeout = options.HttpTimeout;
        }).AddPolicyHandler(static (provider, _) => CreateRetryPolicy(provider));

        return services;
    }

    /// <summary>
    /// Registers a file-backed token cache implementation.
    /// </summary>
    public static IServiceCollection AddStellaOpsFileTokenCache(this IServiceCollection services, string cacheDirectory)
    {
        ArgumentNullException.ThrowIfNull(services);
        ArgumentException.ThrowIfNullOrWhiteSpace(cacheDirectory);

        services.Replace(ServiceDescriptor.Singleton<IStellaOpsTokenCache>(provider =>
        {
            var logger = provider.GetService<Microsoft.Extensions.Logging.ILogger<FileTokenCache>>();
            var options = provider.GetRequiredService<IOptionsMonitor<StellaOpsAuthClientOptions>>().CurrentValue;
            return new FileTokenCache(cacheDirectory, TimeProvider.System, options.ExpirationSkew, logger);
        }));

        return services;
    }

    private static IAsyncPolicy<HttpResponseMessage> CreateRetryPolicy(IServiceProvider provider)
    {
        var options = provider.GetRequiredService<IOptionsMonitor<StellaOpsAuthClientOptions>>().CurrentValue;
        var delays = options.NormalizedRetryDelays;
        if (delays.Count == 0)
        {
            return Policy.NoOpAsync<HttpResponseMessage>();
        }

        var logger = provider.GetService<ILoggerFactory>()?.CreateLogger("StellaOps.Auth.Client.HttpRetry");

        return HttpPolicyExtensions
            .HandleTransientHttpError()
            .OrResult(static response => response.StatusCode == HttpStatusCode.TooManyRequests)
            .WaitAndRetryAsync(
                delays.Count,
                attempt => delays[attempt - 1],
                (outcome, delay, attempt, _) =>
                {
                    if (logger is null)
                    {
                        return;
                    }

                    if (outcome.Exception is not null)
                    {
                        logger.LogWarning(
                            outcome.Exception,
                            "Retrying Authority HTTP call ({Attempt}/{TotalAttempts}) after exception; waiting {Delay}.",
                            attempt,
                            delays.Count,
                            delay);
                    }
                    else
                    {
                        logger.LogWarning(
                            "Retrying Authority HTTP call ({Attempt}/{TotalAttempts}) due to status {StatusCode}; waiting {Delay}.",
                            attempt,
                            delays.Count,
                            outcome.Result!.StatusCode,
                            delay);
                    }
                });
    }
}
