using System.Collections.Concurrent;
using System.Security.Claims;

namespace Ikar.Server.Infrastructure;

public sealed class ApiRateLimitMiddleware(RequestDelegate next, ILogger<ApiRateLimitMiddleware> logger)
{
    private static readonly TimeSpan Window = TimeSpan.FromMinutes(1);
    private static readonly ConcurrentDictionary<string, RateLimitBucket> Buckets = new();
    private static long _lastCleanupTicks;

    public async Task InvokeAsync(HttpContext context)
    {
        if (!ShouldRateLimit(context.Request))
        {
            await next(context);
            return;
        }

        var now = DateTimeOffset.UtcNow;
        var key = BuildKey(context);
        var limit = context.User.Identity?.IsAuthenticated == true ? 1800 : 240;
        var bucket = Buckets.GetOrAdd(key, _ => new RateLimitBucket(now, 0));
        int count;
        DateTimeOffset resetAt;

        lock (bucket)
        {
            if (now - bucket.WindowStartedAt >= Window)
            {
                bucket.WindowStartedAt = now;
                bucket.Count = 0;
            }

            bucket.Count++;
            count = bucket.Count;
            resetAt = bucket.WindowStartedAt.Add(Window);
        }

        MaybeCleanup(now);

        if (count <= limit)
        {
            context.Response.Headers["X-RateLimit-Limit"] = limit.ToString();
            context.Response.Headers["X-RateLimit-Remaining"] = Math.Max(0, limit - count).ToString();
            context.Response.Headers["X-RateLimit-Reset"] = resetAt.ToUnixTimeSeconds().ToString();
            await next(context);
            return;
        }

        logger.LogWarning("Rate limit exceeded for {RateLimitKey} on {Path}.", key, context.Request.Path);
        context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        context.Response.Headers["Retry-After"] = Math.Max(1, (int)(resetAt - now).TotalSeconds).ToString();
        await context.Response.WriteAsJsonAsync(new { error = "Too many requests. Try again later." }, context.RequestAborted);
    }

    private static bool ShouldRateLimit(HttpRequest request) =>
        request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase) ||
        request.Path.StartsWithSegments("/hubs/messenger", StringComparison.OrdinalIgnoreCase);

    private static string BuildKey(HttpContext context)
    {
        var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (!string.IsNullOrWhiteSpace(userId))
        {
            return $"user:{userId}";
        }

        var forwardedFor = context.Request.Headers["X-Forwarded-For"].ToString();
        var ip = forwardedFor.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).FirstOrDefault()
            ?? context.Connection.RemoteIpAddress?.ToString()
            ?? "unknown";
        return $"ip:{ip}";
    }

    private static void MaybeCleanup(DateTimeOffset now)
    {
        var lastCleanup = new DateTimeOffset(Interlocked.Read(ref _lastCleanupTicks), TimeSpan.Zero);
        if (now - lastCleanup < Window)
        {
            return;
        }

        if (Interlocked.Exchange(ref _lastCleanupTicks, now.UtcTicks) == now.UtcTicks)
        {
            return;
        }

        foreach (var (key, bucket) in Buckets)
        {
            if (now - bucket.WindowStartedAt > Window.Add(Window))
            {
                Buckets.TryRemove(key, out _);
            }
        }
    }

    private sealed class RateLimitBucket(DateTimeOffset windowStartedAt, int count)
    {
        public DateTimeOffset WindowStartedAt { get; set; } = windowStartedAt;
        public int Count { get; set; } = count;
    }
}
