using FrameProcessor.Configuration;
using Microsoft.Extensions.Options;

namespace FrameProcessor.UrlFetch;

/// <summary>
/// <see cref="IImageUrlFetcher"/> implementation backed by an <see cref="HttpClient"/>
/// from <see cref="IHttpClientFactory"/>. The client is configured with
/// <see cref="UrlFetchOptions.TimeoutSeconds"/>; redirect-hop enforcement lives on the
/// primary message handler (see <c>Program.cs</c>).
/// </summary>
public sealed class ImageUrlFetcher : IImageUrlFetcher
{
    private const int CopyBufferSize = 81_920;

    private readonly HttpClient _httpClient;
    private readonly IOptions<UrlFetchOptions> _options;

    public ImageUrlFetcher(HttpClient httpClient, IOptions<UrlFetchOptions> options)
    {
        _httpClient = httpClient;
        _options = options;
    }

    public async Task<Stream> FetchAsync(Uri uri, CancellationToken cancellationToken)
    {
        HttpResponseMessage response;
        try
        {
            response = await _httpClient
                .GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
                .ConfigureAwait(false);
        }
        catch (TaskCanceledException ex) when (!cancellationToken.IsCancellationRequested)
        {
            throw new ImageFetchException($"Timed out fetching {uri}", ex);
        }
        catch (HttpRequestException ex)
        {
            // HttpClient surfaces redirect-limit overflows here as well as transport errors.
            throw new ImageFetchException($"Request to {uri} failed: {ex.Message}", ex);
        }

        using (response)
        {
            if (!response.IsSuccessStatusCode)
            {
                throw new ImageFetchException(
                    $"Request to {uri} returned HTTP {(int)response.StatusCode}");
            }

            var maxBytes = _options.Value.MaxBytes;
            if (response.Content.Headers.ContentLength is long advertised && advertised > maxBytes)
            {
                throw new ImageFetchException(
                    $"Response Content-Length {advertised} exceeds maximum {maxBytes} bytes");
            }

            await using var source = await response.Content
                .ReadAsStreamAsync(cancellationToken)
                .ConfigureAwait(false);

            var buffer = new MemoryStream();
            var chunk = new byte[CopyBufferSize];
            long total = 0;
            int read;
            while ((read = await source.ReadAsync(chunk.AsMemory(0, chunk.Length), cancellationToken)
                .ConfigureAwait(false)) > 0)
            {
                total += read;
                if (total > maxBytes)
                {
                    buffer.Dispose();
                    throw new ImageFetchException(
                        $"Response exceeded maximum size of {maxBytes} bytes");
                }
                buffer.Write(chunk, 0, read);
            }

            buffer.Position = 0;
            return buffer;
        }
    }
}
