using Microsoft.Extensions.Logging;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

namespace JSMR.Infrastructure.Http;

public abstract class ApiClient(HttpClient http, ILogger logger, JsonSerializerOptions? json = null)
{
    protected async Task<TResponse> GetJsonAsync<TResponse>(
        string url,
        Action<HttpRequestHeaders>? configureHeaders = null,
        CancellationToken cancellationToken = default)
    {
        using HttpRequestMessage request = new(HttpMethod.Get, url);
        configureHeaders?.Invoke(request.Headers);

        LogRequest(request);

        using HttpResponseMessage response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
        await EnsureSuccess(response).ConfigureAwait(false);

        Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);

        try
        {
            return await JsonSerializer.DeserializeAsync<TResponse>(stream, json, cancellationToken).ConfigureAwait(false)
                ?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(TResponse).Name} from {url}.");
        }
        catch (JsonException ex)
        {
            logger.LogError(ex,
                "Failed to deserialize JSON from {Url}. ContentLengthHeader={ContentLengthHeader}",
                url,
                response.Content.Headers.ContentLength);

            throw;
        }
        
    }

    protected async Task<TResponse> GetJsonpAsync<TResponse>(
        string url,
        Action<HttpRequestHeaders>? configureHeaders = null,
        CancellationToken cancellationToken = default)
    {
        using HttpRequestMessage request = new(HttpMethod.Get, url);
        configureHeaders?.Invoke(request.Headers);

        LogRequest(request);

        using HttpResponseMessage response = await http.SendAsync(
            request,
            HttpCompletionOption.ResponseHeadersRead,
            cancellationToken).ConfigureAwait(false);

        await EnsureSuccess(response).ConfigureAwait(false);

        string body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);

        string jsonBody = ExtractJsonFromJsonp(body);

        return JsonSerializer.Deserialize<TResponse>(jsonBody, json)
            ?? throw new InvalidOperationException($"Failed to deserialize JSONP payload to {typeof(TResponse).Name} from {url}.");
    }

    private static string ExtractJsonFromJsonp(string body)
    {
        if (string.IsNullOrWhiteSpace(body))
            throw new InvalidOperationException("Response body was empty.");

        body = body.Trim();

        int firstParen = body.IndexOf('(');
        int lastParen = body.LastIndexOf(')');

        if (firstParen < 0 || lastParen <= firstParen)
            throw new InvalidOperationException("Response was not valid JSONP.");

        return body[(firstParen + 1)..lastParen].Trim();
    }

    protected async Task<TResponse> PostJsonAsync<TRequest, TResponse>(
        string url,
        TRequest payload,
        Action<HttpRequestHeaders>? configureHeaders = null,
        CancellationToken cancellationToken = default)
    {
        StringContent content = new(JsonSerializer.Serialize(payload, json), Encoding.UTF8, "application/json");

        using HttpRequestMessage request = new(HttpMethod.Post, url)
        {
            Content = content
        };

        configureHeaders?.Invoke(request.Headers);

        LogRequest(request);

        using HttpResponseMessage response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
        await EnsureSuccess(response).ConfigureAwait(false);

        Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);

        return await JsonSerializer.DeserializeAsync<TResponse>(stream, json, cancellationToken).ConfigureAwait(false)
            ?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(TResponse).Name} from {url}.");
    }

    protected virtual void LogRequest(HttpRequestMessage request)
        => logger.LogDebug("HTTP {Method} {Uri}", request.Method, request.RequestUri);

    protected virtual void LogFailure(HttpResponseMessage response, string body)
        => logger.LogWarning("HTTP {Status} for {Uri}. Body: {Body}", (int)response.StatusCode, response.RequestMessage?.RequestUri, Truncate(body, 500));

    protected static string Truncate(string s, int max) => s.Length <= max ? s : s[..max] + "…";

    protected async Task EnsureSuccess(HttpResponseMessage res)
    {
        if (res.IsSuccessStatusCode)
            return;

        string body;
        try { body = await res.Content.ReadAsStringAsync().ConfigureAwait(false); }
        catch { body = "<unable to read body>"; }

        LogFailure(res, body);

        //Throw a richer exception(you can customize per API)
        throw new HttpRequestException(
            $"Request to {res.RequestMessage?.RequestUri} failed: {(int)res.StatusCode} {res.ReasonPhrase}. Body: {Truncate(body, 1000)}",
            null,
            res.StatusCode);
    }
}