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

namespace MaBibli.Api.Services.Isbn;

/// <summary>
/// Lecture d'une réponse SRU de la BnF au schéma <c>dublincore</c>.
/// </summary>
/// <remarks>
/// Fonction pure : aucun accès réseau, ce qui permet de la tester sur des réponses réelles
/// enregistrées sur disque.
/// </remarks>
public static class BnfSruParser
{
    private static readonly XNamespace Srw = "http://www.loc.gov/zing/srw/";
    private static readonly XNamespace Dc = "http://purl.org/dc/elements/1.1/";

    /// <summary>Nombre de notices annoncé par <c>&lt;srw:numberOfRecords&gt;</c>, ou 0.</summary>
    public static int LireNombreDeNotices(string xml)
    {
        var doc = XDocument.Parse(xml);
        var valeur = doc.Descendants(Srw + "numberOfRecords").FirstOrDefault()?.Value;
        return int.TryParse(valeur, out var n) ? n : 0;
    }

    /// <summary>
    /// Convertit la réponse SRU en candidats, champs déjà nettoyés de la ponctuation ISBD.
    /// </summary>
    /// <param name="xml">Corps de la réponse SRU.</param>
    /// <param name="isbnInterroge">Forme d'ISBN qui a produit cette réponse (13 ou 10).</param>
    /// <param name="urlCouverture">URL de couverture OpenLibrary à attacher (la BnF n'en fournit pas).</param>
    public static IReadOnlyList<CandidatLivre> Parser(string xml, string isbnInterroge, string? urlCouverture)
    {
        var doc = XDocument.Parse(xml);
        var candidats = new List<CandidatLivre>();

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

            // Plusieurs dc:creator sont possibles : un livre peut avoir plusieurs auteurs.
            var auteurs = Tous(data, "creator")
                .Select(NettoyageIsbd.Auteur)
                .Where(a => a is not null)
                .Select(a => a!)
                .Distinct()
                .ToList();

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

            // Certaines notices recollent le nom de l'auteur en fin de titre sans le séparateur
            // ISBD — il faut donc les auteurs nettoyés avant de pouvoir finir le titre.
            titre = NettoyageIsbd.RetirerAuteurEnFin(titre, auteurs);

            if (titre is null)
            {
                // Une notice sans titre exploitable n'a aucun intérêt pour un pré-remplissage.
                continue;
            }

            var identifiant = record.Element(Srw + "recordIdentifier")?.Value?.Trim();

            candidats.Add(new CandidatLivre
            {
                Titre = titre,
                Auteurs = auteurs,
                Editeur = NettoyageIsbd.Editeur(Premier(data, "publisher")),
                Annee = Premier(data, "date"),
                Langue = Premier(data, "language"),
                CoverUrl = urlCouverture,
                Source = SourceMetadonnees.Bnf,
                IsbnInterroge = isbnInterroge,
                IdentifiantSource = identifiant,
                UrlNotice = UrlNotice(identifiant),
            });
        }

        return candidats;
    }

    private static string? UrlNotice(string? identifiant) =>
        string.IsNullOrWhiteSpace(identifiant)
            ? null
            : $"https://catalogue.bnf.fr/{identifiant}";

    /// <summary>
    /// Lit la <b>première</b> notice d'une réponse SRU interrogée par <c>bib.issn</c>.
    /// </summary>
    /// <remarks>
    /// On ne renvoie qu'une notice, et sans laisser le choix : le but n'est pas de cataloguer
    /// la revue — le modèle de données ne le permet pas — mais de <b>nommer</b> ce que
    /// l'utilisateur vient de scanner. Un ISSN désigne un et un seul titre de périodique, donc
    /// les notices suivantes ne diraient rien de plus.
    /// <para>
    /// Une notice de revue porte souvent <b>deux</b> <c>dc:title</c> : le titre clé qualifié
    /// (« Médor (Bruxelles) ») puis le titre développé avec sa mention de responsabilité.
    /// C'est le premier qu'on retient — le second est l'accroche commerciale, pas le nom.
    /// </para>
    /// </remarks>
    /// <returns><c>null</c> si la réponse ne contient aucune notice exploitable.</returns>
    public static PeriodiqueDetecte? ParserPeriodique(string xml, string issn)
    {
        var doc = XDocument.Parse(xml);

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

            var titre = NettoyageIsbd.TitrePeriodique(Premier(data, "title"));
            if (titre is null)
            {
                continue;
            }

            return new PeriodiqueDetecte
            {
                Issn = issn,
                Titre = titre,
                Editeur = NettoyageIsbd.Editeur(Premier(data, "publisher")),
                Annee = Premier(data, "date"),
            };
        }

        return null;
    }

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

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