using System;
using System.Net;
using Microsoft.AspNetCore.Http;

namespace StellaOps.Authority.RateLimiting;

internal interface IAuthorityRateLimiterPartitionKeyResolver
{
    string Resolve(string limiterName, HttpContext context, AuthorityRateLimiterMetadata metadata);
}

internal sealed class DefaultAuthorityRateLimiterPartitionKeyResolver : IAuthorityRateLimiterPartitionKeyResolver
{
    public string Resolve(string limiterName, HttpContext context, AuthorityRateLimiterMetadata metadata)
    {
        ArgumentNullException.ThrowIfNull(limiterName);
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(metadata);

        var remoteIp = ResolveRemoteIp(context, metadata);
        metadata.RemoteIp = remoteIp;

        if ((string.Equals(limiterName, AuthorityRateLimiter.TokenLimiterName, StringComparison.Ordinal) ||
             string.Equals(limiterName, AuthorityRateLimiter.AuthorizeLimiterName, StringComparison.Ordinal)) &&
            !string.IsNullOrWhiteSpace(metadata.ClientId))
        {
            return $"{limiterName}:{remoteIp}:{metadata.ClientId}";
        }

        return $"{limiterName}:{remoteIp}";
    }

    private static string ResolveRemoteIp(HttpContext context, AuthorityRateLimiterMetadata metadata)
    {
        if (!string.IsNullOrWhiteSpace(metadata.RemoteIp))
        {
            return metadata.RemoteIp!;
        }

        if (!string.IsNullOrWhiteSpace(metadata.ForwardedFor))
        {
            return metadata.ForwardedFor!;
        }

        var normalized = NormalizeRemoteIp(context.Connection.RemoteIpAddress);
        return string.IsNullOrWhiteSpace(normalized) ? "unknown" : normalized!;
    }

    private static string? NormalizeRemoteIp(IPAddress? address)
    {
        if (address is null)
        {
            return null;
        }

        if (IPAddress.Any.Equals(address) || IPAddress.IPv6Any.Equals(address) ||
            IPAddress.IPv6None.Equals(address) || IPAddress.None.Equals(address))
        {
            return null;
        }

        return address.ToString();
    }
}
