using MaBibli.Shared.Entites;
using MaBibli.Shared.Textes;
using Microsoft.EntityFrameworkCore;

namespace MaBibli.Api.Data;

/// <summary>
/// Remet en cohérence les colonnes normalisées, et réunit les auteurs que cette normalisation
/// révèle identiques.
/// </summary>
/// <remarks>
/// <b>Pourquoi ce passage existe.</b> La migration a recopié les anciennes données avec les
/// moyens du bord : <c>lower()</c> de SQLite ne retire pas les accents, donc « Émile Zola » et
/// « emile zola » y sont restés deux auteurs distincts et les titres normalisés sont approchés.
/// Seul C# sait appliquer les vraies règles.
/// <para>
/// Le passage est <b>idempotent</b> et ne touche que ce qui diffère : il ne coûte rien au
/// démarrage suivant. Il sert aussi de filet si les règles de normalisation changent un jour —
/// le prochain démarrage rattrape la base sans migration à écrire.
/// </para>
/// </remarks>
public sealed class ServiceRenormalisation(MaBibliDbContext db, ILogger<ServiceRenormalisation> logger)
{
    public async Task ExecuterAsync(CancellationToken ct = default)
    {
        var titres = await RenormaliserTitresAsync(ct);
        var (auteursCorriges, auteursFusionnes) = await RenormaliserAuteursAsync(ct);

        if (titres + auteursCorriges + auteursFusionnes > 0)
        {
            logger.LogInformation(
                "Renormalisation : {Titres} titre(s), {Auteurs} auteur(s) corrigés, {Fusions} fusionné(s).",
                titres, auteursCorriges, auteursFusionnes);
        }
    }

    private async Task<int> RenormaliserTitresAsync(CancellationToken ct)
    {
        var livres = await db.Livres.ToListAsync(ct);
        var corriges = 0;

        foreach (var livre in livres)
        {
            var attendu = NormalisationTexte.Normaliser(livre.Titre);
            if (livre.TitreNormalise == attendu)
            {
                continue;
            }

            livre.TitreNormalise = attendu;
            corriges++;
        }

        if (corriges > 0)
        {
            await db.SaveChangesAsync(ct);
        }

        return corriges;
    }

    private async Task<(int Corriges, int Fusionnes)> RenormaliserAuteursAsync(CancellationToken ct)
    {
        var auteurs = await db.Auteurs.Include(a => a.Livres).OrderBy(a => a.Id).ToListAsync(ct);
        var corriges = 0;
        var fusionnes = 0;

        // Premier tour : la clé de regroupement, qui couvre casse, accents et ordre des mots.
        // Le premier auteur rencontré pour une clé donnée est celui qui reste ; les suivants
        // sont absorbés. L'ordre par identifiant rend le résultat reproductible.
        var parCle = new Dictionary<string, Auteur>(StringComparer.Ordinal);
        var conserves = new List<Auteur>();

        foreach (var auteur in auteurs)
        {
            var cle = RapprochementAuteurs.Cle(auteur.Nom);
            var nomNormalise = NormalisationTexte.Normaliser(auteur.Nom);

            if (parCle.TryGetValue(cle, out var conserve))
            {
                // Deux fiches que la migration n'a pas pu rapprocher (accents) et que les vraies
                // règles réunissent. C'est une variante sûre : la fusion est automatique.
                Absorber(conserve, auteur);
                fusionnes++;
                continue;
            }

            parCle[cle] = auteur;
            conserves.Add(auteur);

            if (auteur.CleRegroupement == cle && auteur.NomNormalise == nomNormalise)
            {
                continue;
            }

            auteur.CleRegroupement = cle;
            auteur.NomNormalise = nomNormalise;
            corriges++;
        }

        // Second tour : les initiales abrégées, que la clé ne sait pas capturer
        // (« P.F. Hamilton » et « Peter F. Hamilton »). Sans ce tour, une base héritée
        // resterait éclatée là où une saisie neuve aurait été réunie d'emblée — deux
        // comportements pour une même règle.
        foreach (var auteur in conserves.ToList())
        {
            if (db.Entry(auteur).State == EntityState.Deleted)
            {
                continue;
            }

            var jumeau = conserves.FirstOrDefault(autre =>
                !ReferenceEquals(autre, auteur)
                && db.Entry(autre).State != EntityState.Deleted
                && RapprochementAuteurs.SontLeMemeAuteur(auteur.Nom, autre.Nom));

            if (jumeau is null)
            {
                continue;
            }

            // Le nom le plus complet l'emporte : « Peter F. Hamilton » plutôt que « P.F. ».
            var (conserve, absorbe) =
                NomsDeveloppes(auteur.Nom) >= NomsDeveloppes(jumeau.Nom)
                    ? (auteur, jumeau)
                    : (jumeau, auteur);

            Absorber(conserve, absorbe);
            conserves.Remove(absorbe);
            fusionnes++;
        }

        if (corriges > 0 || fusionnes > 0)
        {
            await db.SaveChangesAsync(ct);
        }

        return (corriges, fusionnes);
    }

    /// <summary>Reporte les livres de <paramref name="absorbe"/> sur <paramref name="conserve"/>.</summary>
    private void Absorber(Auteur conserve, Auteur absorbe)
    {
        // La fiche conservée est celle au plus petit identifiant, or cet ordre est un hasard de
        // la migration. On rattrape sur le nom d'affichage : sans cet arbitrage, la bibliothèque
        // perdrait ses accents au profit des saisies bâclées.
        if (QualiteAffichage(absorbe.Nom) > QualiteAffichage(conserve.Nom))
        {
            conserve.Nom = absorbe.Nom;
            conserve.NomNormalise = NormalisationTexte.Normaliser(absorbe.Nom);
            conserve.CleRegroupement = RapprochementAuteurs.Cle(absorbe.Nom);
        }

        var dejaLies = conserve.Livres.Select(l => l.LivreId).ToHashSet();

        foreach (var lien in absorbe.Livres.ToList())
        {
            db.LivreAuteurs.Remove(lien);

            // Un livre déjà signé par la fiche conservée ne doit pas recevoir un second lien :
            // la clé primaire est (LivreId, AuteurId).
            if (dejaLies.Add(lien.LivreId))
            {
                db.LivreAuteurs.Add(new LivreAuteur
                {
                    LivreId = lien.LivreId,
                    AuteurId = conserve.Id,
                    Position = lien.Position,
                });
            }
        }

        db.Auteurs.Remove(absorbe);
    }

    /// <summary>
    /// Note grossière d'un nom en tant que <b>nom d'affichage</b>, pour départager des variantes
    /// équivalentes.
    /// </summary>
    /// <remarks>
    /// Trois signaux, dans cet ordre d'importance : la forme inversée « ZOLA, Émile » est une
    /// convention de catalogue, pas quelque chose qu'on veut lire dans une liste ; les capitales
    /// intégrales sont une saisie négligée ; les accents, eux, sont de l'information qu'on ne
    /// veut pas perdre. Le résultat vise « Émile Zola ».
    /// </remarks>
    private static int QualiteAffichage(string nom)
    {
        var note = nom.Count(c => char.IsLetter(c) && c > 127) * 2;

        if (nom.Contains(','))
        {
            note -= 3;
        }

        note -= nom
            .Split(' ', StringSplitOptions.RemoveEmptyEntries)
            .Count(mot => mot.Length > 1 && mot.Where(char.IsLetter).All(char.IsUpper));

        return note;
    }

    /// <summary>Nombre de mots qui ne sont pas de simples initiales.</summary>
    private static int NomsDeveloppes(string nom) =>
        NormalisationTexte.Mots(nom).Count(mot => mot.Length > 1);
}
