using System.Net;
using System.Text.Json;
using MaBibli.Shared.Dtos;

namespace MaBibli.Api.Services.Isbn;

public interface IOpenLibraryClient
{
    /// <summary>
    /// Cherche l'édition correspondant à un ISBN chez OpenLibrary, auteurs résolus.
    /// Renvoie une liste vide si l'ISBN est inconnu ou la source injoignable.
    /// </summary>
    Task<(IReadOnlyList<CandidatLivre> Candidats, string? Avertissement)> RechercherAsync(
        string isbn, string? urlCouverture, CancellationToken ct = default);
}

/// <summary>
/// Client OpenLibrary — second rideau de la cascade, pour les livres étrangers et tout ce que
/// la BnF ne connaît pas.
/// </summary>
/// <remarks>
/// <b>Bug à ne pas reproduire</b> (observé chez BookLogr) : <c>/isbn/{isbn}.json</c> ne donne pas
/// le nom de l'auteur mais une référence <c>/authors/OL…A</c>. Sans second appel, l'auteur reste
/// vide. Ce client fait ce second appel, et bascule sur l'œuvre rattachée quand l'édition ne
/// porte aucun auteur (cas mesuré : Introduction to Algorithms).
/// </remarks>
public sealed class OpenLibraryClient(HttpClient http, ILogger<OpenLibraryClient> logger) : IOpenLibraryClient
{
    public const string NomHttpClient = "openlibrary";

    public const string UrlBase = "https://openlibrary.org/";

    private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);

    public async Task<(IReadOnlyList<CandidatLivre> Candidats, string? Avertissement)> RechercherAsync(
        string isbn, string? urlCouverture, CancellationToken ct = default)
    {
        try
        {
            // /isbn/{isbn}.json répond en 302 vers /books/OL…M.json quand le livre existe, 404 sinon.
            // Le HttpClient suit la redirection par défaut.
            var edition = await LireAsync<OpenLibraryEdition>($"isbn/{isbn}.json", ct);
            if (edition is null)
            {
                return ([], null);
            }

            var noms = await ResoudreAuteursAsync(edition, ct);
            var candidat = OpenLibraryMapper.VersCandidat(edition, noms, isbn, urlCouverture);
            return (candidat is null ? [] : [candidat], null);
        }
        catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or JsonException)
        {
            logger.LogWarning(ex, "OpenLibrary injoignable ou illisible pour l'ISBN {Isbn}", isbn);
            return ([], $"OpenLibrary injoignable ({ex.GetType().Name}) pour {isbn}.");
        }
    }

    private async Task<IReadOnlyList<string>> ResoudreAuteursAsync(OpenLibraryEdition edition, CancellationToken ct)
    {
        var cles = OpenLibraryMapper.ClesAuteurs(edition);

        if (cles.Count == 0)
        {
            // Repli : certaines éditions n'ont pas de champ `authors`, seule l'œuvre en porte.
            var cleOeuvre = OpenLibraryMapper.CleOeuvre(edition);
            if (cleOeuvre is not null)
            {
                var oeuvre = await LireAsync<OpenLibraryWork>(cleOeuvre.TrimStart('/') + ".json", ct);
                if (oeuvre is not null)
                {
                    cles = OpenLibraryMapper.ClesAuteurs(oeuvre);
                }
            }
        }

        var noms = new List<string>();
        foreach (var cle in cles)
        {
            var auteur = await LireAsync<OpenLibraryAuthor>(cle.TrimStart('/') + ".json", ct);
            var nom = auteur is null ? null : OpenLibraryMapper.Nom(auteur);
            if (nom is not null)
            {
                noms.Add(nom);
            }
        }

        return noms;
    }

    private async Task<T?> LireAsync<T>(string chemin, CancellationToken ct) where T : class
    {
        using var reponse = await http.GetAsync(chemin, ct);
        if (reponse.StatusCode == HttpStatusCode.NotFound)
        {
            return null;
        }

        if (!reponse.IsSuccessStatusCode)
        {
            logger.LogWarning("OpenLibrary a répondu {Code} pour {Chemin}", (int)reponse.StatusCode, chemin);
            return null;
        }

        await using var flux = await reponse.Content.ReadAsStreamAsync(ct);
        return await JsonSerializer.DeserializeAsync<T>(flux, Json, ct);
    }
}
