@page "/auteurs"
@inject ServiceLivresApi Api
@inject EtatReseau Reseau
@implements IDisposable

<PageTitle>MaBibli — auteurs</PageTitle>

<h1 class="titre-page">Auteurs</h1>

@if (_erreur is not null)
{
    <p class="message-erreur" role="alert">@_erreur</p>
}

@if (_suggestions is { Count: > 0 })
{
    @*
        Ce qui arrive ici est ce que la machine REFUSE de trancher. Les variantes sûres — casse,
        accents, ordre « Nom, Prénom », initiales abrégées — ont déjà été réunies à l'écriture et
        n'apparaissent jamais dans cette liste. Restent les cas ambigus, typiquement un patronyme
        seul face à un nom complet : deux personnes d'une même famille écrivent aussi des livres.
    *@
    <section class="rapprochements">
        <h2 class="titre-section">Rapprochements possibles</h2>
        <p class="message-discret">
            Ces noms se ressemblent sans qu'on puisse l'affirmer. Un patronyme seul peut désigner
            une autre personne de la même famille — à vous de trancher. Une fusion se défait mal.
        </p>

        <ul class="liste-rapprochements">
            @foreach (var suggestion in _suggestions)
            {
                <li class="carte-rapprochement" @key="(suggestion.Source.Id, suggestion.Cible.Id)">
                    <p class="rapprochement-noms">
                        <strong>@suggestion.Source.Nom</strong>
                        (@Livres(suggestion.Source.NombreDeLivres))
                        est-il <strong>@suggestion.Cible.Nom</strong>
                        (@Livres(suggestion.Cible.NombreDeLivres)) ?
                    </p>

                    <div class="actions-formulaire">
                        @* Une fusion est une écriture : elle demande le réseau, comme le reste. *@
                        <button type="button" class="bouton bouton-principal"
                                disabled="@(_enCours || !Reseau.EnLigne)" title="@MotifBlocage"
                                @onclick="() => AccepterAsync(suggestion)">
                            Oui, réunir sous « @suggestion.Cible.Nom »
                        </button>
                        <button type="button" class="bouton bouton-discret"
                                disabled="@(_enCours || !Reseau.EnLigne)" title="@MotifBlocage"
                                @onclick="() => RefuserAsync(suggestion)">
                            Non, deux personnes
                        </button>
                    </div>
                </li>
            }
        </ul>
    </section>
}

@if (_auteurs is null)
{
    <p class="message-discret">Chargement…</p>
}
else if (_auteurs.Count == 0)
{
    <p class="message-discret">Aucun auteur : la bibliothèque est vide.</p>
}
else
{
    @*
        Recherche et tri d'une seule pièce : la question qu'on se pose devant une liste
        d'auteurs est soit « où est untel ? » (recherche), soit « qui ai-je le plus ? » (tri).

        ⚠️ Tout se fait en mémoire, sans nouvel appel : la liste des auteurs est déjà
        entièrement chargée — c'est elle qui sert l'instantané hors-ligne — et la filtrer
        côté serveur aurait rendu la page inutilisable sans réseau. La normalisation passe
        par NormalisationTexte, comme partout : « emile » retrouve « Émile ».
    *@
    <div class="barre-recherche">
        <input class="champ-saisie" type="search" placeholder="Nom d'auteur…"
               value="@_recherche" @oninput="SurRecherche" aria-label="Rechercher un auteur" />
    </div>

    <div class="segments" role="group" aria-label="Tri">
        <button type="button" class="segment @(_tri == TriAuteurs.Nom ? "segment-actif" : null)"
                @onclick="() => _tri = TriAuteurs.Nom">Par nom</button>
        <button type="button" class="segment @(_tri == TriAuteurs.NombreDeLivres ? "segment-actif" : null)"
                @onclick="() => _tri = TriAuteurs.NombreDeLivres">Les plus présents</button>
    </div>

    @* Le compteur dit ce qu'on regarde : sur une liste filtrée, afficher le total ferait
       croire que la recherche n'a rien fait. *@
    <p class="compteur">
        @Affiches.Count auteur@(Affiches.Count > 1 ? "s" : "")
        @if (Affiches.Count != _auteurs.Count)
        {
            <text> sur @_auteurs.Count</text>
        }
    </p>

    @if (Affiches.Count == 0)
    {
        <p class="message-discret">Aucun auteur ne correspond à « @_recherche ».</p>
    }

    <ul class="liste-auteurs">
        @foreach (var auteur in Affiches)
        {
            @*
                Trois colonnes, dont une seule est élastique.

                Le nombre de livres passe à GAUCHE du nom, sur une largeur figée de trois
                chiffres : les compteurs s'alignent alors les uns sous les autres et se
                comparent d'un coup d'œil. Trois chiffres suffisent — personne ne possède
                mille livres d'un même auteur. Les actions se rangent tout à droite, elles
                aussi sur une largeur constante, pour que seul le nom absorbe la place
                restante au lieu de décaler les boutons ligne après ligne.
            *@
            <li class="carte-auteur-ligne" @key="auteur.Id">
                <span class="auteur-compte" title="@Livres(auteur.NombreDeLivres)">
                    @auteur.NombreDeLivres
                    <span class="lecture-ecran">@Livres(auteur.NombreDeLivres)</span>
                </span>

                <a class="lien-auteur auteur-nom" href="@($"?auteur={auteur.Id}")">@auteur.Nom</a>

                <span class="auteur-actions">
                @* Le declencheur decrit dans IDEES.md : depuis un auteur qu'on a deja, voir
                   tout ce qu'il a ecrit et marquer ce qui manque. *@
                <a class="bouton bouton-compact bouton-discret"
                   href="@($"auteurs/{auteur.Id}/bibliographie")">Bibliographie</a>
                @if (_auteurRenomme == auteur.Id)
                {
                    <input class="champ-saisie champ-auteur-renomme" @bind="_nomRenomme" />
                    <button type="button" class="bouton bouton-compact bouton-principal"
                            disabled="@_renommageEnCours" @onclick="RenommerAsync">
                        Enregistrer
                    </button>
                    <button type="button" class="bouton bouton-compact bouton-discret"
                            disabled="@_renommageEnCours" @onclick="AnnulerRenommage">
                        Annuler
                    </button>
                }
                else
                {
                    <button type="button" class="bouton bouton-compact bouton-discret"
                            disabled="@(_enCours || !Reseau.EnLigne)"
                            title="@MotifBlocage"
                            @onclick="() => CommencerRenommage(auteur)">
                        Renommer
                    </button>
                }
                </span>
            </li>
        }
    </ul>
}

@if (_collisionRenommage is not null)
{
    <section class="message-avertissement" role="alert">
        <p>
            Un auteur existe déjà sous le nom « @_collisionRenommage.Nom ».
            Les livres seront regroupés sous cette fiche.
            Confirmer la fusion ?
        </p>
        <div class="actions-formulaire">
            <button type="button" class="bouton bouton-principal"
                    disabled="@_renommageEnCours" @onclick="ConfirmerFusionAsync">
                Oui, fusionner
            </button>
            <button type="button" class="bouton bouton-discret"
                    disabled="@_renommageEnCours" @onclick="AnnulerRenommage">
                Annuler
            </button>
        </div>
    </section>
}

@code {
    /// <summary>Les deux façons de trier une liste d'auteurs, et il n'y en a pas de troisième.</summary>
    private enum TriAuteurs
    {
        /// <summary>Ordre alphabétique : « où est untel ? ».</summary>
        Nom = 0,

        /// <summary>Du plus présent au moins présent : « qui ai-je le plus ? ».</summary>
        NombreDeLivres = 1,
    }

    private IReadOnlyList<AuteurDto>? _auteurs;
    private string _recherche = string.Empty;
    private TriAuteurs _tri = TriAuteurs.Nom;
    private IReadOnlyList<SuggestionRapprochement>? _suggestions;
    private bool _enCours;
    private bool _renommageEnCours;
    private int? _auteurRenomme;
    private string _nomRenomme = string.Empty;
    private AuteurDto? _collisionRenommage;
    private string? _erreur;

    protected override void OnInitialized() => Reseau.Change += SurChangementReseau;

    private void SurChangementReseau() => _ = InvokeAsync(async () =>
    {
        await ChargerAsync();
        StateHasChanged();
    });

    public void Dispose() => Reseau.Change -= SurChangementReseau;

    /// <summary>Raison du blocage des fusions, ou <c>null</c> en ligne.</summary>
    private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne;

    protected override Task OnInitializedAsync() => ChargerAsync();

    private static string Livres(int nombre) => $"{nombre} livre{(nombre > 1 ? "s" : "")}";

    private void SurRecherche(ChangeEventArgs e) => _recherche = e.Value?.ToString() ?? string.Empty;

    /// <summary>
    /// La liste telle qu'elle s'affiche : filtrée par la recherche, puis triée.
    /// </summary>
    /// <remarks>
    /// La recherche est une <b>sous-chaîne du nom normalisé</b>, comme celle du catalogue :
    /// « zola » retrouve « Émile Zola », et « emile » aussi. Elle ne cherche pas mot à mot,
    /// contrairement au rapprochement d'auteurs, qui répond à une autre question — « ces deux
    /// noms désignent-ils la même personne ? » — et n'a rien à faire ici.
    /// <para>
    /// Le tri par nom est <b>ordinal sur la forme normalisée</b>, pour ne pas dépendre de la
    /// culture du navigateur : c'est la même règle que <c>FiltreLivresLocal</c>, et elle évite
    /// qu'un appareil classe « Éluard » avant « Duras » et un autre après.
    /// </para>
    /// </remarks>
    private IReadOnlyList<AuteurDto> Affiches
    {
        get
        {
            if (_auteurs is null)
            {
                return [];
            }

            var terme = NormalisationTexte.Normaliser(_recherche);
            var retenus = string.IsNullOrEmpty(terme)
                ? _auteurs
                : _auteurs.Where(a => NormalisationTexte.Normaliser(a.Nom).Contains(terme)).ToList();

            return _tri == TriAuteurs.NombreDeLivres
                ? retenus
                    .OrderByDescending(a => a.NombreDeLivres)
                    .ThenBy(a => NormalisationTexte.Normaliser(a.Nom), StringComparer.Ordinal)
                    .ToList()
                : retenus
                    .OrderBy(a => NormalisationTexte.Normaliser(a.Nom), StringComparer.Ordinal)
                    .ToList();
        }
    }

    private async Task ChargerAsync()
    {
        _erreur = null;

        try
        {
            _auteurs = await Api.ListerAuteursAsync();
            _suggestions = await Api.ListerRapprochementsAsync();
        }
        catch (Exception ex)
        {
            _erreur = $"Impossible de charger les auteurs : {ex.Message}";
        }
    }

    private Task AccepterAsync(SuggestionRapprochement suggestion) =>
        DeciderAsync(() => Api.AccepterRapprochementAsync(suggestion.Source.Id, suggestion.Cible.Id));

    private Task RefuserAsync(SuggestionRapprochement suggestion) =>
        DeciderAsync(() => Api.RefuserRapprochementAsync(suggestion.Source.Id, suggestion.Cible.Id));

    private void CommencerRenommage(AuteurDto auteur)
    {
        _auteurRenomme = auteur.Id;
        _nomRenomme = auteur.Nom;
        _collisionRenommage = null;
        _erreur = null;
    }

    private void AnnulerRenommage()
    {
        _auteurRenomme = null;
        _collisionRenommage = null;
        _nomRenomme = string.Empty;
    }

    private Task RenommerAsync() => EnvoyerRenommageAsync(false);

    private Task ConfirmerFusionAsync() => EnvoyerRenommageAsync(true);

    private async Task EnvoyerRenommageAsync(bool confirmerFusion)
    {
        if (_auteurRenomme is null || string.IsNullOrWhiteSpace(_nomRenomme))
        {
            _erreur = "Le nom de l'auteur est obligatoire.";
            return;
        }

        _renommageEnCours = true;
        _erreur = null;
        var resultat = await Api.RenommerAuteurAsync(
            _auteurRenomme.Value, _nomRenomme, confirmerFusion);
        _renommageEnCours = false;

        if (resultat is null)
        {
            _erreur = "Le renommage n'a pas pu être enregistré.";
            return;
        }

        if (resultat.Collision is not null)
        {
            _collisionRenommage = resultat.Collision;
            return;
        }

        AnnulerRenommage();
        await ChargerAsync();
    }

    private async Task DeciderAsync(Func<Task<string?>> action)
    {
        _enCours = true;
        var erreur = await action();
        _enCours = false;

        if (erreur is not null)
        {
            _erreur = erreur;
            return;
        }

        // Une fusion peut en rendre d'autres caduques (l'un des deux noms disparaît) : on relit
        // la liste complète plutôt que de retirer la ligne traitée à la main.
        await ChargerAsync();
    }
}
