using System;
using System.Globalization;
using System.Net;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Authority.RateLimiting;
using StellaOps.Configuration;

namespace StellaOps.Authority;

internal static class AuthorityRateLimiter
{
    internal const string TokenLimiterName = "authority-token";
    internal const string AuthorizeLimiterName = "authority-authorize";
    internal const string InternalLimiterName = "authority-internal";
    private static readonly IAuthorityRateLimiterPartitionKeyResolver FallbackPartitionKeyResolver = new DefaultAuthorityRateLimiterPartitionKeyResolver();

    public static void Configure(RateLimiterOptions options, StellaOpsAuthorityOptions authorityOptions, ILogger? logger = null)
    {
        ArgumentNullException.ThrowIfNull(options);
        ArgumentNullException.ThrowIfNull(authorityOptions);

        options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
        options.GlobalLimiter = CreateGlobalLimiter(authorityOptions);
        options.OnRejected = async (context, cancellationToken) =>
        {
            var httpContext = context.HttpContext;
            var remoteIp = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
            logger ??= httpContext.RequestServices.GetService(typeof(ILoggerFactory)) is ILoggerFactory loggerFactory
                ? loggerFactory.CreateLogger("StellaOps.Authority.RateLimiting")
                : null;

            logger?.LogWarning(
                "Rate limit exceeded for path {Path} from {RemoteIp}.",
                httpContext.Request.Path,
                remoteIp);

            if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out TimeSpan retryAfter))
            {
                httpContext.Response.Headers["Retry-After"] = Math.Ceiling(retryAfter.TotalSeconds).ToString(CultureInfo.InvariantCulture);
            }

            await ValueTask.CompletedTask;
        };
    }

    public static PartitionedRateLimiter<HttpContext> CreateGlobalLimiter(StellaOpsAuthorityOptions authorityOptions)
    {
        ArgumentNullException.ThrowIfNull(authorityOptions);

        var rateLimiting = authorityOptions.Security.RateLimiting;
        var tokenOptions = rateLimiting.Token;
        var authorizeOptions = rateLimiting.Authorize;
        var internalOptions = rateLimiting.Internal;

        return PartitionedRateLimiter.Create<HttpContext, string>(context =>
        {
            var path = context.Request.Path;

            if (tokenOptions.Enabled && path.HasValue && path.Value!.Equals("/token", StringComparison.OrdinalIgnoreCase))
            {
                var partitionKey = ResolvePartitionKey(context, TokenLimiterName);
                return RateLimitPartition.GetFixedWindowLimiter(
                    partitionKey,
                    _ => CreateFixedWindowOptions(tokenOptions));
            }

            if (authorizeOptions.Enabled && path.HasValue && path.Value!.Equals("/authorize", StringComparison.OrdinalIgnoreCase))
            {
                var partitionKey = ResolvePartitionKey(context, AuthorizeLimiterName);
                return RateLimitPartition.GetFixedWindowLimiter(
                    partitionKey,
                    _ => CreateFixedWindowOptions(authorizeOptions));
            }

            if (internalOptions.Enabled && path.HasValue && path.Value!.StartsWith("/internal", StringComparison.OrdinalIgnoreCase))
            {
                var partitionKey = ResolvePartitionKey(context, InternalLimiterName);
                return RateLimitPartition.GetFixedWindowLimiter(
                    partitionKey,
                    _ => CreateFixedWindowOptions(internalOptions));
            }

            return RateLimitPartition.GetNoLimiter(ResolvePartitionKey(context));
        });
    }

    private static FixedWindowRateLimiterOptions CreateFixedWindowOptions(AuthorityEndpointRateLimitOptions options)
    {
        return new FixedWindowRateLimiterOptions
        {
            PermitLimit = options.PermitLimit,
            QueueLimit = options.QueueLimit,
            QueueProcessingOrder = options.QueueProcessingOrder,
            Window = options.Window,
            AutoReplenishment = true
        };
    }

    private static string ResolvePartitionKey(HttpContext context, string limiterName)
    {
        var metadata = context.Features.Get<IAuthorityRateLimiterFeature>()?.Metadata ?? new AuthorityRateLimiterMetadata();
        var serviceProvider = context.RequestServices;
        var resolver = serviceProvider is not null
            ? serviceProvider.GetService(typeof(IAuthorityRateLimiterPartitionKeyResolver)) as IAuthorityRateLimiterPartitionKeyResolver ?? FallbackPartitionKeyResolver
            : FallbackPartitionKeyResolver;
        return resolver.Resolve(limiterName, context, metadata);
    }

    private static string ResolvePartitionKey(HttpContext context)
    {
        return ResolvePartitionKey(context, "none");
    }
}
