using MaBibli.Shared.Dtos;

namespace MaBibli.Api.Services.Isbn;

public interface IBnfClient
{
    /// <summary>
    /// Interroge le SRU de la BnF pour une forme d'ISBN donnée.
    /// Renvoie une liste vide si la BnF ne connaît pas l'ISBN <b>ou</b> si elle est injoignable ;
    /// dans ce second cas <paramref name="avertissement"/> explique pourquoi.
    /// </summary>
    Task<(IReadOnlyList<CandidatLivre> Candidats, string? Avertissement)> RechercherAsync(
        string isbn, string? urlCouverture, CancellationToken ct = default);

    /// <summary>
    /// Interroge le SRU <b>par auteur</b> pour bâtir une bibliographie.
    /// </summary>
    /// <remarks>
    /// Renvoie un résultat vide et un <c>Avertissement</c> si la BnF est injoignable : comme
    /// pour le lookup ISBN, une source indisponible ne doit pas faire échouer l'écran.
    /// </remarks>
    Task<(ResultatBibliographie Resultat, string? Avertissement)> RechercherParAuteurAsync(
        string auteur, CancellationToken ct = default);
}

/// <summary>
/// Client de l'API SRU du catalogue général de la BnF (gratuite, sans clé).
/// </summary>
public sealed class BnfClient(HttpClient http, ILogger<BnfClient> logger) : IBnfClient
{
    /// <summary>Nom du client typé enregistré dans <c>IHttpClientFactory</c>.</summary>
    public const string NomHttpClient = "bnf";

    public const string UrlBase = "https://catalogue.bnf.fr/";

    public async Task<(IReadOnlyList<CandidatLivre> Candidats, string? Avertissement)> RechercherAsync(
        string isbn, string? urlCouverture, CancellationToken ct = default)
    {
        // recordSchema=dublincore et non MARC : dc:title / dc:creator / dc:publisher se mappent
        // directement, là où l'UNIMARC demanderait un décodage complet.
        var url = "api/SRU"
                  + "?version=1.2"
                  + "&operation=searchRetrieve"
                  + $"&query={Uri.EscapeDataString($"bib.isbn all \"{isbn}\"")}"
                  + "&recordSchema=dublincore"
                  + "&maximumRecords=5";

        try
        {
            using var reponse = await http.GetAsync(url, ct);
            if (!reponse.IsSuccessStatusCode)
            {
                logger.LogWarning("BnF a répondu {Code} pour l'ISBN {Isbn}", (int)reponse.StatusCode, isbn);
                return ([], $"La BnF a répondu {(int)reponse.StatusCode} pour {isbn}.");
            }

            var xml = await reponse.Content.ReadAsStringAsync(ct);
            return (BnfSruParser.Parser(xml, isbn, urlCouverture), null);
        }
        catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
        {
            // Une source indisponible ne doit pas faire échouer la cascade : on bascule sur la suivante.
            logger.LogWarning(ex, "BnF injoignable pour l'ISBN {Isbn}", isbn);
            return ([], $"BnF injoignable ({ex.GetType().Name}) pour {isbn}.");
        }
        catch (System.Xml.XmlException ex)
        {
            logger.LogWarning(ex, "Réponse BnF illisible pour l'ISBN {Isbn}", isbn);
            return ([], $"Réponse BnF illisible pour {isbn}.");
        }
    }

    /// <summary>
    /// Notices lues par page, et nombre de pages : au plus <b>200 notices</b>.
    /// </summary>
    /// <remarks>
    /// Ce plafond est un arbitrage, pas une limite de l'API. Mesuré le 2026-08-18 : une requête
    /// coûte ~0,85 s, et les deux pages sont donc lancées <b>en parallèle</b>. Aller au-delà
    /// serait long pour un gain nul dans la plupart des cas — Bernard Werber tient en 203
    /// notices, Amélie Nothomb en 277 — et resterait de toute façon insuffisant pour un classique
    /// très réédité : Émile Zola en compte 2692. Comme le SRU classe par pertinence, les deux
    /// premières pages contiennent l'essentiel de l'œuvre ; le reste est de la réédition.
    /// <para>
    /// L'interface <b>doit</b> signaler que la liste est un extrait quand la BnF en annonce plus
    /// (<c>BibliographieDto.Tronquee</c>) : une bibliographie partielle présentée comme complète
    /// ferait croire à tort qu'un livre n'existe pas.
    /// </para>
    /// </remarks>
    public const int NoticesParPage = 100;

    public const int PagesMaximum = 2;

    public async Task<(ResultatBibliographie Resultat, string? Avertissement)> RechercherParAuteurAsync(
        string auteur, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(auteur))
        {
            return (new ResultatBibliographie(), null);
        }

        try
        {
            // Les pages sont lancées ensemble : à ~0,85 s l'unité, les enchaîner doublerait
            // l'attente de l'écran pour rien.
            var pages = await Task.WhenAll(
                Enumerable.Range(0, PagesMaximum)
                    .Select(i => LirePageAsync(auteur, (i * NoticesParPage) + 1, ct)));

            var lues = pages.Where(p => p is not null).Select(p => p!).ToList();
            if (lues.Count == 0)
            {
                return (new ResultatBibliographie(), $"La BnF n'a pas répondu pour « {auteur} ».");
            }

            return (Fusionner(lues), null);
        }
        catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
        {
            logger.LogWarning(ex, "BnF injoignable pour l'auteur {Auteur}", auteur);
            return (new ResultatBibliographie(), $"BnF injoignable ({ex.GetType().Name}) pour « {auteur} ».");
        }
        catch (System.Xml.XmlException ex)
        {
            logger.LogWarning(ex, "Réponse BnF illisible pour l'auteur {Auteur}", auteur);
            return (new ResultatBibliographie(), $"Réponse BnF illisible pour « {auteur} ».");
        }
    }

    private async Task<ResultatBibliographie?> LirePageAsync(
        string auteur, int premiereNotice, CancellationToken ct)
    {
        // bib.author : index auteur du catalogue général, vérifié le 2026-08-18.
        // « all » exige que tous les mots soient présents — mais sur l'ENSEMBLE des auteurs de la
        // notice, d'où le post-filtre de BnfBibliographieParser.
        var url = "api/SRU"
                  + "?version=1.2"
                  + "&operation=searchRetrieve"
                  + $"&query={Uri.EscapeDataString($"bib.author all \"{auteur}\"")}"
                  + "&recordSchema=dublincore"
                  + $"&maximumRecords={NoticesParPage}"
                  + $"&startRecord={premiereNotice}";

        using var reponse = await http.GetAsync(url, ct);
        if (!reponse.IsSuccessStatusCode)
        {
            logger.LogWarning(
                "BnF a répondu {Code} pour l'auteur {Auteur} (notice {Debut})",
                (int)reponse.StatusCode, auteur, premiereNotice);
            return null;
        }

        var xml = await reponse.Content.ReadAsStringAsync(ct);
        return BnfBibliographieParser.Parser(xml, auteur);
    }

    /// <summary>Recolle les pages en un seul résultat, en additionnant les compteurs.</summary>
    private static ResultatBibliographie Fusionner(IReadOnlyList<ResultatBibliographie> pages) =>
        new()
        {
            Notices = pages.SelectMany(p => p.Notices).ToList(),

            // Le total annoncé est le même sur chaque page : on le prend, on ne l'additionne pas.
            NombreAnnonce = pages.Max(p => p.NombreAnnonce),
            NombreLu = pages.Sum(p => p.NombreLu),
            EcarteesTypeNonLivre = pages.Sum(p => p.EcarteesTypeNonLivre),
            EcarteesAutreAuteur = pages.Sum(p => p.EcarteesAutreAuteur),
        };
}
