namespace StellaOps.Feedser.Source.Common.Fetch;

/// <summary>
/// Provides retry/backoff behavior for source HTTP fetches.
/// </summary>
internal static class SourceRetryPolicy
{
    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, response.Headers.RetryAfter?.Delta, 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;
        }

        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;
    }
}

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