using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.DependencyInjection;

namespace Baya.WebFramework.ServiceConfiguration;

public static class RateLimitingServiceExtension
{
    /// <summary>Per-IP baseline applied to every endpoint that doesn't opt into a named policy.</summary>
    public const string GlobalPolicy = "global";

    /// <summary>Tighter limit for OTP request/verify endpoints (applied in backend-phase-2).</summary>
    public const string OtpPolicy = "otp";

    /// <summary>Limit for login/refresh and other auth endpoints.</summary>
    public const string AuthPolicy = "auth";

    /// <summary>Limit for money-sensitive actions (refund/payout) applied in later phases.</summary>
    public const string SensitivePolicy = "sensitive";

    /// <summary>
    /// The single deliberate policy for inbound PSP/BNPL webhooks. PSP callbacks are bursty (retries,
    /// batched settlements), so this is more permissive than <see cref="SensitivePolicy"/> and partitions
    /// per-provider (not just per-IP) so one provider's burst can't starve another.
    /// </summary>
    public const string WebhookPolicy = "webhook";

    /// <summary>
    /// Registers the built-in rate limiter with a per-IP global limit plus named policies that
    /// auth/OTP/sensitive endpoints opt into via <c>[EnableRateLimiting(name)]</c>. Over-limit
    /// requests get <c>429 Too Many Requests</c>. Pair with <c>app.UseRateLimiter()</c> placed
    /// before <c>app.UseAuthentication()</c>.
    /// </summary>
    public static IServiceCollection AddRateLimitingPolicies(this IServiceCollection services)
    {
        services.AddRateLimiter(options =>
        {
            options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

            options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
                RateLimitPartition.GetFixedWindowLimiter(
                    PartitionKey(context),
                    _ => new FixedWindowRateLimiterOptions
                    {
                        PermitLimit = 100,
                        Window = TimeSpan.FromMinutes(1),
                        QueueLimit = 0
                    }));

            AddFixedWindowPolicy(options, OtpPolicy, permitLimit: 5, windowSeconds: 60);
            AddFixedWindowPolicy(options, AuthPolicy, permitLimit: 10, windowSeconds: 60);
            AddFixedWindowPolicy(options, SensitivePolicy, permitLimit: 20, windowSeconds: 60);

            // Bursty PSP/BNPL callbacks, partitioned per-provider (see WebhookPartitionKey).
            AddFixedWindowPolicy(options, WebhookPolicy, permitLimit: 120, windowSeconds: 60, keyResolver: WebhookPartitionKey);

            // A deliberately tiny policy used by the phase-0 ping endpoint to demonstrate 429s.
            AddFixedWindowPolicy(options, GlobalPolicy, permitLimit: 5, windowSeconds: 10);
        });

        return services;
    }

    private static void AddFixedWindowPolicy(RateLimiterOptions options, string name, int permitLimit, int windowSeconds,
        Func<HttpContext, string>? keyResolver = null)
    {
        var resolvePartition = keyResolver ?? PartitionKey;
        options.AddPolicy(name, context =>
            RateLimitPartition.GetFixedWindowLimiter(
                resolvePartition(context),
                _ => new FixedWindowRateLimiterOptions
                {
                    PermitLimit = permitLimit,
                    Window = TimeSpan.FromSeconds(windowSeconds),
                    QueueLimit = 0
                }));
    }

    // The client IP as resolved by the forwarded-headers middleware (see ForwardedHeadersServiceExtension) —
    // behind a trusted proxy this is the real client, not the proxy, so each client gets its own bucket.
    private static string PartitionKey(HttpContext context) =>
        context.Connection.RemoteIpAddress?.ToString() ?? "unknown";

    // Per-provider partition so a single PSP's burst can't exhaust the shared webhook budget; still bounded
    // by the resolved client IP so a spoofed provider segment can't fan out unboundedly.
    private static string WebhookPartitionKey(HttpContext context)
    {
        var provider = context.Request.RouteValues.TryGetValue("provider", out var value) ? value?.ToString() : null;
        return $"webhook:{(string.IsNullOrWhiteSpace(provider) ? "unknown" : provider)}:{PartitionKey(context)}";
    }
}
