using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;

namespace StellaOps.Authority.RateLimiting;

/// <summary>
/// Captures request metadata required for rate limiting decisions before the rate limiter middleware executes.
/// </summary>
internal sealed class AuthorityRateLimiterMetadataMiddleware
{
    private static readonly string[] ForwardedHeaders =
    {
        "X-Forwarded-For",
        "X-Real-IP",
        "Forwarded"
    };

    private readonly RequestDelegate next;
    private readonly TimeProvider clock;
    private readonly ILogger<AuthorityRateLimiterMetadataMiddleware> logger;

    public AuthorityRateLimiterMetadataMiddleware(
        RequestDelegate next,
        TimeProvider clock,
        ILogger<AuthorityRateLimiterMetadataMiddleware> logger)
    {
        this.next = next ?? throw new ArgumentNullException(nameof(next));
        this.clock = clock ?? throw new ArgumentNullException(nameof(clock));
        this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task InvokeAsync(HttpContext context)
    {
        ArgumentNullException.ThrowIfNull(context);

        var remoteAddress = NormalizeAddress(context.Connection.RemoteIpAddress);
        var forwardedFor = ResolveForwardedIp(context.Request.Headers);

        var metadata = new AuthorityRateLimiterMetadata
        {
            Endpoint = NormalizeEndpoint(context.Request.Path),
            RemoteIp = remoteAddress ?? forwardedFor,
            ForwardedFor = forwardedFor,
        };

        context.Features.Set<IAuthorityRateLimiterFeature>(new AuthorityRateLimiterFeature(metadata));

        if (string.Equals(metadata.Endpoint, "/token", StringComparison.Ordinal))
        {
            metadata.ClientId = await ResolveTokenClientIdAsync(context).ConfigureAwait(false);
        }
        else if (string.Equals(metadata.Endpoint, "/authorize", StringComparison.Ordinal))
        {
            metadata.ClientId = ResolveAuthorizeClientId(context.Request.Query);
        }

        var userAgent = NormalizeUserAgent(context.Request.Headers.UserAgent.ToString());
        metadata.UserAgent = userAgent;

        if (!string.IsNullOrWhiteSpace(metadata.ClientId))
        {
            metadata.SetTag("authority.client_id", metadata.ClientId);
        }

        if (!string.IsNullOrWhiteSpace(metadata.ForwardedFor))
        {
            metadata.SetTag("authority.forwarded_for", metadata.ForwardedFor!);
        }

        metadata.SetTag("authority.endpoint", metadata.Endpoint ?? string.Empty);
        metadata.SetTag("authority.remote_ip", metadata.RemoteIp ?? "unknown");
        metadata.SetTag("authority.captured_at", clock.GetUtcNow().ToString("O", CultureInfo.InvariantCulture));
        if (!string.IsNullOrWhiteSpace(userAgent))
        {
            metadata.SetTag("authority.user_agent", userAgent);
        }

        await next(context).ConfigureAwait(false);
    }

    private static string? NormalizeEndpoint(PathString path)
    {
        if (!path.HasValue || string.IsNullOrWhiteSpace(path.Value))
        {
            return "/";
        }

        return path.Value!.TrimEnd('/').ToLowerInvariant();
    }

    private static string? NormalizeAddress(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();
    }

    private static string? ResolveForwardedIp(IHeaderDictionary headers)
    {
        foreach (var headerName in ForwardedHeaders)
        {
            if (!headers.TryGetValue(headerName, out var values) || StringValues.IsNullOrEmpty(values))
            {
                continue;
            }

            var raw = values.ToString();
            if (string.Equals(headerName, "Forwarded", StringComparison.OrdinalIgnoreCase))
            {
                var forwarded = ForwardedHeaderParser.Parse(raw);
                if (!string.IsNullOrWhiteSpace(forwarded))
                {
                    return forwarded;
                }
            }
            else
            {
                var candidate = raw.Split(',')[0].Trim();
                if (!string.IsNullOrWhiteSpace(candidate))
                {
                    return candidate;
                }
            }
        }

        return null;
    }

    private static string? ResolveAuthorizeClientId(IQueryCollection query)
    {
        if (query.TryGetValue("client_id", out var values) && !StringValues.IsNullOrEmpty(values))
        {
            return values.ToString();
        }

        return null;
    }

    private static string? NormalizeUserAgent(string? userAgent)
    {
        if (string.IsNullOrWhiteSpace(userAgent))
        {
            return null;
        }

        var trimmed = userAgent.Trim();
        return trimmed.Length == 0 ? null : trimmed;
    }

    private async Task<string?> ResolveTokenClientIdAsync(HttpContext context)
    {
        var request = context.Request;

        if (TryParseBasicClientId(request.Headers["Authorization"], out var clientId))
        {
            return clientId;
        }

        if (!HttpMethods.IsPost(request.Method) || !request.HasFormContentType)
        {
            return null;
        }

        try
        {
            request.EnableBuffering();
            var form = await request.ReadFormAsync(context.RequestAborted).ConfigureAwait(false);
            if (form.TryGetValue("client_id", out var values) && !StringValues.IsNullOrEmpty(values))
            {
                return values.ToString();
            }
        }
        catch (InvalidDataException ex)
        {
            logger.LogDebug(ex, "Failed to parse form data while resolving token endpoint client_id.");
        }
        finally
        {
            if (request.Body.CanSeek)
            {
                request.Body.Seek(0, SeekOrigin.Begin);
            }
        }

        return null;
    }

    private static bool TryParseBasicClientId(StringValues authorizationHeader, out string? clientId)
    {
        clientId = null;

        if (StringValues.IsNullOrEmpty(authorizationHeader))
        {
            return false;
        }

        for (var index = 0; index < authorizationHeader.Count; index++)
        {
            var value = authorizationHeader[index];
            if (string.IsNullOrWhiteSpace(value) || !value.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            var span = value.AsSpan(6).Trim();
            if (span.IsEmpty)
            {
                continue;
            }

            if (TryDecodeBase64(span, out var decoded))
            {
                var separator = decoded.IndexOf(':');
                clientId = separator >= 0 ? decoded[..separator] : decoded;
                clientId = string.IsNullOrWhiteSpace(clientId) ? null : clientId;
                if (clientId is not null)
                {
                    return true;
                }
            }
        }

        return false;
    }

    private static bool TryDecodeBase64(ReadOnlySpan<char> encoded, out string decoded)
    {
        decoded = string.Empty;

        if (encoded.IsEmpty)
        {
            return false;
        }

        try
        {
            var buffer = Convert.FromBase64String(encoded.ToString());
            decoded = Encoding.UTF8.GetString(buffer);
            return true;
        }
        catch (FormatException)
        {
            return false;
        }
    }

    private static class ForwardedHeaderParser
    {
        public static string? Parse(string headerValue)
        {
            if (string.IsNullOrWhiteSpace(headerValue))
            {
                return null;
            }

            var segments = headerValue.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
            foreach (var segment in segments)
            {
                var parts = segment.Split('=', 2, StringSplitOptions.TrimEntries);
                if (parts.Length != 2)
                {
                    continue;
                }

                if (!string.Equals(parts[0], "for", StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                var value = parts[1].Trim('"');
                if (!string.IsNullOrWhiteSpace(value))
                {
                    return value;
                }
            }

            return null;
        }
    }
}
