namespace MaBibli.Shared.Textes;

/// <summary>Verdict du rapprochement de deux noms d'auteur.</summary>
public enum NiveauRapprochement
{
    /// <summary>Rien de commun : ce sont deux auteurs distincts.</summary>
    Aucun = 0,

    /// <summary>
    /// Variante <b>sûre</b> : même nom à la casse, aux accents, à l'ordre ou à l'abréviation près.
    /// Peut être fusionné automatiquement.
    /// </summary>
    Certain = 1,

    /// <summary>
    /// Rapprochement <b>plausible mais ambigu</b> : un nom est contenu dans l'autre
    /// (« Hamilton » vers « Peter F. Hamilton »). À <b>proposer</b>, jamais à appliquer seul.
    /// </summary>
    Suggere = 2,
}

/// <summary>
/// Règles de rapprochement des noms d'auteurs.
/// </summary>
/// <remarks>
/// Décision actée dans CLAUDE.md : le regroupement <b>automatique</b> se limite aux variantes
/// sûres — casse, accents, ordre <c>Nom, Prénom</c> ↔ <c>Prénom Nom</c>, initiales abrégées
/// quand le reste concorde. Tout le reste est <b>proposé</b> à l'utilisateur.
/// <para>
/// La raison est asymétrique : ne pas fusionner laisse deux fiches à réunir d'un clic, alors
/// qu'une fusion erronée mélange les livres de deux personnes et se défait mal. Dans le doute,
/// on ne fusionne pas.
/// </para>
/// </remarks>
public static class RapprochementAuteurs
{
    /// <summary>
    /// Clé de regroupement : mots normalisés <b>triés</b>, ce qui rend l'ordre indifférent.
    /// </summary>
    /// <remarks>
    /// « Zola, Émile » et « Émile Zola » donnent tous deux <c>emile zola</c>. C'est cette clé qui
    /// porte l'unicité en base : deux auteurs ne peuvent pas coexister avec la même.
    /// <para>
    /// Elle ne couvre <b>pas</b> les initiales (<c>p f hamilton</c> ≠ <c>f hamilton peter</c>) :
    /// ce cas relève de <see cref="Comparer"/>, qui aligne les mots au lieu de les hacher.
    /// </para>
    /// </remarks>
    public static string Cle(string? nom)
    {
        var mots = NormalisationTexte.Mots(nom);
        Array.Sort(mots, StringComparer.Ordinal);
        return string.Join(' ', mots);
    }

    /// <summary>
    /// Compare deux noms d'auteur et dit s'ils désignent sûrement, peut-être, ou sûrement pas
    /// la même personne.
    /// </summary>
    public static NiveauRapprochement Comparer(string? a, string? b)
    {
        var motsA = NormalisationTexte.Mots(a);
        var motsB = NormalisationTexte.Mots(b);

        if (motsA.Length == 0 || motsB.Length == 0)
        {
            return NiveauRapprochement.Aucun;
        }

        if (motsA.Length == motsB.Length)
        {
            // Même nombre de mots : soit tout s'aligne (variante sûre), soit ce sont deux personnes.
            // On exige au moins un mot entier commun : « P. H. » et « Peter Hamilton » s'alignent
            // par les seules initiales, ce qui ne concorde sur rien de vérifiable.
            return Aligne(motsA, motsB, out var pleinPartage) && pleinPartage
                ? NiveauRapprochement.Certain
                : NiveauRapprochement.Aucun;
        }

        // Nombres de mots différents : au mieux l'un est un fragment de l'autre
        // (« Hamilton » ⊂ « Peter F. Hamilton »). Jamais sûr — deux personnes partagent
        // couramment un patronyme.
        var (court, longue) = motsA.Length < motsB.Length ? (motsA, motsB) : (motsB, motsA);

        return Aligne(court, longue, out var motPleinPartage) && motPleinPartage
            ? NiveauRapprochement.Suggere
            : NiveauRapprochement.Aucun;
    }

    /// <summary>Raccourci : les deux noms sont-ils une variante sûre l'un de l'autre ?</summary>
    public static bool SontLeMemeAuteur(string? a, string? b) =>
        Comparer(a, b) == NiveauRapprochement.Certain;

    /// <summary>
    /// Cherche à placer chaque mot de <paramref name="aPlacer"/> sur un mot distinct de
    /// <paramref name="cible"/>, par égalité ou par initiale.
    /// </summary>
    /// <param name="motPleinPartage">
    /// Vrai si au moins un couple aligné est une égalité de <b>mots entiers</b>. Sans cette
    /// garantie, « P. » se rapprocherait de n'importe quel prénom commençant par P — une
    /// suggestion sans valeur qui n'apprendrait rien à l'utilisateur.
    /// </param>
    /// <remarks>
    /// Recherche exhaustive avec retour arrière. Les noms de personnes tiennent en quelques mots,
    /// le coût est négligeable ; une correspondance gloutonne, elle, échouerait sur
    /// « P. Peter » vs « Peter Paul » en consommant le mauvais mot en premier.
    /// </remarks>
    private static bool Aligne(string[] aPlacer, string[] cible, out bool motPleinPartage)
    {
        var pris = new bool[cible.Length];
        motPleinPartage = false;
        return Placer(aPlacer, cible, pris, 0, ref motPleinPartage);
    }

    private static bool Placer(
        string[] aPlacer, string[] cible, bool[] pris, int index, ref bool motPleinPartage)
    {
        if (index == aPlacer.Length)
        {
            return true;
        }

        var mot = aPlacer[index];

        for (var i = 0; i < cible.Length; i++)
        {
            if (pris[i])
            {
                continue;
            }

            var autre = cible[i];
            var egalite = mot == autre;
            var initiale = !egalite && EstInitialeDe(mot, autre);

            if (!egalite && !initiale)
            {
                continue;
            }

            pris[i] = true;
            var pleinAvant = motPleinPartage;
            motPleinPartage = pleinAvant || (egalite && mot.Length > 1);

            if (Placer(aPlacer, cible, pris, index + 1, ref motPleinPartage))
            {
                return true;
            }

            pris[i] = false;
            motPleinPartage = pleinAvant;
        }

        return false;
    }

    /// <summary>« p » est l'initiale de « peter ». Une lettre seule d'un côté, le mot de l'autre.</summary>
    private static bool EstInitialeDe(string a, string b) =>
        (a.Length == 1 && b.Length > 1 && b[0] == a[0])
        || (b.Length == 1 && a.Length > 1 && a[0] == b[0]);
}
