using System.Globalization;
using System.Net;

namespace StellaOps.Feedser.Source.Common.Fetch;

/// <summary>
/// Provides retry/backoff behavior for source HTTP fetches.
/// </summary>
internal static class SourceRetryPolicy
{
    private static readonly StringComparer HeaderComparer = StringComparer.OrdinalIgnoreCase;

    public static async Task<HttpResponseMessage> SendWithRetryAsync(
        Func<HttpRequestMessage> requestFactory,
        Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sender,
        int maxAttempts,
        TimeSpan baseDelay,
        IJitterSource jitterSource,
        Action<SourceRetryAttemptContext>? onRetry,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(requestFactory);
        ArgumentNullException.ThrowIfNull(sender);
        ArgumentNullException.ThrowIfNull(jitterSource);

        var attempt = 0;

        while (true)
        {
            attempt++;
            using var request = requestFactory();
            HttpResponseMessage response;

            try
            {
                response = await sender(request, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex) when (attempt < maxAttempts)
            {
                var delay = ComputeDelay(baseDelay, attempt, jitterSource: jitterSource);
                onRetry?.Invoke(new SourceRetryAttemptContext(attempt, null, ex, delay));
                await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
                continue;
            }

            if (NeedsRetry(response) && attempt < maxAttempts)
            {
                var delay = ComputeDelay(
                    baseDelay,
                    attempt,
                    GetRetryAfter(response),
                    jitterSource);
                onRetry?.Invoke(new SourceRetryAttemptContext(attempt, response, null, delay));
                response.Dispose();
                await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
                continue;
            }

            return response;
        }
    }

    private static bool NeedsRetry(HttpResponseMessage response)
    {
        if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
        {
            return true;
        }

        if (IsRateLimitResponse(response))
        {
            return true;
        }

        var status = (int)response.StatusCode;
        return status >= 500 && status < 600;
    }

    private static TimeSpan ComputeDelay(TimeSpan baseDelay, int attempt, TimeSpan? retryAfter = null, IJitterSource? jitterSource = null)
    {
        if (retryAfter.HasValue && retryAfter.Value > TimeSpan.Zero)
        {
            return retryAfter.Value;
        }

        var exponential = TimeSpan.FromMilliseconds(baseDelay.TotalMilliseconds * Math.Pow(2, attempt - 1));
        var jitter = jitterSource?.Next(TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(250))
            ?? TimeSpan.FromMilliseconds(Random.Shared.Next(50, 250));
        return exponential + jitter;
    }

    private static bool IsRateLimitResponse(HttpResponseMessage response)
    {
        if (response.Headers.RetryAfter is not null)
        {
            return true;
        }

        if (response.StatusCode == System.Net.HttpStatusCode.Forbidden || response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
        {
            if (TryGetRateLimitRemaining(response, out var remaining) && remaining <= 0)
            {
                return true;
            }

            if (response.Headers.TryGetValues("X-RateLimit-Reset", out var _))
            {
                return true;
            }
        }

        return false;
    }

    private static bool TryGetRateLimitRemaining(HttpResponseMessage response, out long remaining)
    {
        remaining = 0;
        if (response.Headers.TryGetValues("X-RateLimit-Remaining", out var values))
        {
            foreach (var value in values)
            {
                if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed))
                {
                    remaining = parsed;
                    return true;
                }
            }
        }

        return false;
    }

    private static TimeSpan? GetRetryAfter(HttpResponseMessage response)
    {
        var retryAfter = response.Headers.RetryAfter;
        if (retryAfter is not null)
        {
            if (retryAfter.Delta.HasValue && retryAfter.Delta.Value > TimeSpan.Zero)
            {
                return retryAfter.Delta;
            }

            if (retryAfter.Date.HasValue)
            {
                var delta = retryAfter.Date.Value - DateTimeOffset.UtcNow;
                if (delta > TimeSpan.Zero)
                {
                    return delta;
                }
            }
        }

        if (response.Headers.TryGetValues("Retry-After", out var retryAfterValues))
        {
            foreach (var value in retryAfterValues)
            {
                if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var seconds) && seconds > 0)
                {
                    return TimeSpan.FromSeconds(seconds);
                }
            }
        }

        if (response.Headers.TryGetValues("X-RateLimit-Reset", out var resetValues))
        {
            foreach (var value in resetValues)
            {
                if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var epochSeconds))
                {
                    var resetTime = DateTimeOffset.FromUnixTimeSeconds(epochSeconds);
                    var delta = resetTime - DateTimeOffset.UtcNow;
                    if (delta > TimeSpan.Zero)
                    {
                        return delta;
                    }
                }
            }
        }

        return null;
    }
}

internal readonly record struct SourceRetryAttemptContext(int Attempt, HttpResponseMessage? Response, Exception? Exception, TimeSpan Delay);
