using System.Xml.Linq;
using MaBibli.Shared.Textes;

namespace MaBibli.Api.Services.Isbn;

/// <summary>Une notice BnF réduite à ce qui sert à bâtir une bibliographie.</summary>
public sealed record NoticeAuteur
{
    public required string Titre { get; init; }

    /// <summary>Clé d'œuvre du titre : c'est elle qui réunit les rééditions.</summary>
    public required string CleTitre { get; init; }

    public string? Annee { get; init; }

    public string? Editeur { get; init; }

    public string? Isbn { get; init; }

    /// <summary>Auteurs nettoyés de la notice, tous rôles confondus.</summary>
    public IReadOnlyList<string> Auteurs { get; init; } = [];
}

/// <summary>Résultat de la lecture d'une réponse SRU « par auteur ».</summary>
public sealed record ResultatBibliographie
{
    public IReadOnlyList<NoticeAuteur> Notices { get; init; } = [];

    /// <summary>Nombre de notices annoncé par la BnF, tous types confondus.</summary>
    public int NombreAnnonce { get; init; }

    /// <summary>Notices lues dans cette réponse, avant tout filtrage.</summary>
    public int NombreLu { get; init; }

    /// <summary>Notices écartées parce qu'elles ne décrivent pas un livre (disque, jeu, film…).</summary>
    public int EcarteesTypeNonLivre { get; init; }

    /// <summary>Notices écartées parce qu'aucun de leurs auteurs n'est celui demandé.</summary>
    public int EcarteesAutreAuteur { get; init; }
}

/// <summary>
/// Lecture d'une réponse SRU de la BnF interrogée <b>par auteur</b>
/// (<c>bib.author all "…"</c>, <c>recordSchema=dublincore</c>).
/// </summary>
/// <remarks>
/// Fonction pure, testée sur des réponses réelles enregistrées dans <c>Fixtures/</c>.
/// <para>
/// <b>Deux filtres, mesurés sur des réponses réelles le 2026-08-18, sont indispensables.</b>
/// Sans eux l'écran de bibliographie ment à l'utilisateur.
/// </para>
/// <list type="number">
/// <item>
/// <b>Le type de document.</b> L'index auteur ne distingue pas un livre d'un livre audio, d'un
/// jeu vidéo ou d'un spectacle. Sur 200 notices de Bernard Werber : 180 imprimés, mais aussi
/// 13 enregistrements sonores, 3 images animées, 2 jeux multimédias et 1 spectacle. Une liste
/// d'envies de livres n'a pas à proposer d'acheter un jeu.
/// </item>
/// <item>
/// <b>L'auteur réel de la notice.</b> C'est le filtre <b>critique</b>, et il n'est pas
/// théorique. <c>bib.author all "Émile Zola"</c> rapproche les mots « émile » et « zola » sur
/// l'<i>ensemble</i> des auteurs d'une notice : un livre signé d'un « Émile » quelconque et d'un
/// « Zola » quelconque remonte donc aussi. Mesuré sur 200 notices d'« Émile Zola » : <b>67 sont
/// écartées</b>, dont toute l'œuvre de sa fille Denise Le Blond-Zola (« Émile Zola raconté par
/// sa fille », « Zola et Cézanne »), qui parle de lui sans qu'il l'ait écrite. Les proposer à
/// l'achat comme du Zola serait faux.
/// </item>
/// </list>
/// <para>
/// Ce second filtre a un <b>coût assumé</b> : il écarte aussi les adaptations dont l'auteur n'est
/// pas le scénariste — les bandes dessinées tirées de Werber sont signées Corbeyran, et
/// disparaissent (17 notices sur 200). C'est le bon compromis : afficher l'œuvre d'un tiers sous
/// le nom de l'auteur est une erreur visible et gênante, en omettre une adaptation ne l'est pas.
/// </para>
/// </remarks>
public static class BnfBibliographieParser
{
    private static readonly XNamespace Srw = "http://www.loc.gov/zing/srw/";
    private static readonly XNamespace Dc = "http://purl.org/dc/elements/1.1/";
    private static readonly XNamespace Xml = "http://www.w3.org/XML/1998/namespace";

    /// <summary>
    /// Types <c>dc:type</c> (en français) considérés comme des livres.
    /// </summary>
    /// <remarks>
    /// Liste <b>positive</b> et non liste d'exclusion : les types inconnus sont écartés. Une
    /// bibliographie tronquée vaut mieux qu'une bibliographie qui propose un DVD.
    /// </remarks>
    private static readonly HashSet<string> TypesLivre =
    [
        NormalisationTexte.Normaliser("texte imprimé"),
        NormalisationTexte.Normaliser("ressource électronique"),
    ];

    /// <summary>Préfixe des <c>dc:identifier</c> qui portent un ISBN : « ISBN 2226086366 ».</summary>
    private const string PrefixeIsbn = "ISBN ";

    /// <summary>
    /// Convertit une réponse SRU en notices exploitables.
    /// </summary>
    /// <param name="xml">Corps de la réponse SRU.</param>
    /// <param name="auteurDemande">
    /// Nom de l'auteur dont on veut la bibliographie. Chaque notice doit compter cet auteur
    /// parmi les siens, au sens de <see cref="RapprochementAuteurs"/> — la même règle que celle
    /// qui regroupe les auteurs du catalogue, donc tolérante à la casse, aux accents, à l'ordre
    /// « Nom, Prénom » et aux initiales.
    /// </param>
    public static ResultatBibliographie Parser(string xml, string auteurDemande)
    {
        var doc = XDocument.Parse(xml);

        var annonce = doc.Descendants(Srw + "numberOfRecords").FirstOrDefault()?.Value;
        var notices = new List<NoticeAuteur>();
        int lues = 0, horsType = 0, autreAuteur = 0;

        foreach (var record in doc.Descendants(Srw + "record"))
        {
            var data = record.Element(Srw + "recordData");
            if (data is null)
            {
                continue;
            }

            lues++;

            if (!EstUnLivre(data))
            {
                horsType++;
                continue;
            }

            var auteurs = Tous(data, "creator")
                .Select(NettoyageIsbd.Auteur)
                .Where(a => !string.IsNullOrWhiteSpace(a))
                .Select(a => a!)
                .Distinct()
                .ToList();

            if (!auteurs.Any(a => RapprochementAuteurs.SontLeMemeAuteur(a, auteurDemande)))
            {
                autreAuteur++;
                continue;
            }

            var titre = NettoyageIsbd.Titre(Premier(data, "title"));
            titre = NettoyageIsbd.RetirerAuteurEnFin(titre, auteurs);

            if (titre is null)
            {
                continue;
            }

            notices.Add(new NoticeAuteur
            {
                Titre = titre,
                CleTitre = CleOeuvre.Cle(titre),
                Annee = Premier(data, "date"),
                Editeur = NettoyageIsbd.Editeur(Premier(data, "publisher")),
                Isbn = Isbn(data),
                Auteurs = auteurs,
            });
        }

        return new ResultatBibliographie
        {
            Notices = notices,
            NombreAnnonce = int.TryParse(annonce, out var n) ? n : lues,
            NombreLu = lues,
            EcarteesTypeNonLivre = horsType,
            EcarteesAutreAuteur = autreAuteur,
        };
    }

    /// <summary>
    /// La notice décrit-elle un livre ?
    /// </summary>
    /// <remarks>
    /// Seuls les <c>dc:type</c> en français sont consultés : les variantes anglaises sont plus
    /// grossières (« text » couvre aussi bien un imprimé qu'un enregistrement transcrit).
    /// </remarks>
    private static bool EstUnLivre(XElement data) =>
        data.Descendants(Dc + "type")
            .Where(t => (string?)t.Attribute(Xml + "lang") == "fre")
            .Select(t => NormalisationTexte.Normaliser(t.Value))
            .Any(TypesLivre.Contains);

    /// <summary>
    /// ISBN de la notice, extrait des <c>dc:identifier</c>.
    /// </summary>
    /// <remarks>
    /// Le Dublin Core BnF mélange dans ce champ l'ark du catalogue et, quand il existe, l'ISBN
    /// préfixé du mot « ISBN ». Mesuré : 84 notices sur 100 en portent un, presque toujours sous
    /// sa forme à 10 chiffres — la BnF indexant l'ISBN tel qu'imprimé (voir CLAUDE.md).
    /// </remarks>
    private static string? Isbn(XElement data) =>
        Tous(data, "identifier")
            .Where(v => v.StartsWith(PrefixeIsbn, StringComparison.OrdinalIgnoreCase))
            .Select(v => v[PrefixeIsbn.Length..].Trim())
            .FirstOrDefault(v => v.Length > 0);

    private static string? Premier(XElement data, string nomLocal) =>
        Tous(data, nomLocal).FirstOrDefault();

    private static IEnumerable<string> Tous(XElement data, string nomLocal) =>
        data.Descendants(Dc + nomLocal)
            .Select(e => e.Value?.Trim())
            .Where(v => !string.IsNullOrEmpty(v))
            .Select(v => v!);
}
