@page "/souhaits"
@page "/souhaits/ordre"
@inject ServiceLivresApi Api
@inject EtatReseau Reseau
@inject NavigationManager Navigation
@implements IDisposable

<PageTitle>MaBibli — liste d'envies</PageTitle>

<h1 class="titre-page">Liste d'envies</h1>

@* Le rappel n'est pas décoratif : la bibliothèque est commune, cette liste ne l'est pas.
   Sans ce mot, on pourrait croire que le foyer entier voit ce qu'on note ici. *@
<p class="message-discret">
    Les livres que vous aimeriez acquérir. <strong>Cette liste est la vôtre</strong> :
    les autres membres du foyer ont la leur, et ne voient pas celle-ci.
    Ces livres ne sont pas dans le catalogue et ne comptent pas dans la bibliothèque.
</p>

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

@if (_souhaits is null)
{
    <p class="message-discret">Chargement…</p>
}
else
{
    @if (_souhaits.Count == 0)
    {
        @* Hors-ligne, « vide » et « jamais enregistrée » sont indiscernables : une liste jamais
           synchronisée rendrait exactement le même résultat qu'une liste réellement vide. On dit
           donc les deux plutôt que d'affirmer la première — même règle que pour une fiche
           absente de l'instantané. *@
        <p class="message-discret">
            @if (Reseau.EnLigne)
            {
                <text>
                    Votre liste est vide. Ajoutez une envie ci-dessous, ou ouvrez la
                    bibliographie d'un auteur depuis <a href="auteurs">la liste des auteurs</a>.
                </text>
            }
            else
            {
                <text>
                    Aucune envie dans les données enregistrées sur cet appareil. Votre liste est
                    peut-être vide, ou simplement pas encore synchronisée : le retour du réseau
                    tranchera.
                </text>
            }
        </p>
    }
    else
    {
        <p class="compteur">@_souhaits.Count livre@(_souhaits.Count > 1 ? "s" : "") souhaité@(_souhaits.Count > 1 ? "s" : "")</p>

        @* Un seul bloc d'export, en haut de liste : c'est l'action qu'on vient chercher quand on
           part en librairie, elle ne doit pas être au bout d'un défilement. *@
        @*
            ⚠️ L'export est produit PAR LE SERVEUR : hors-ligne il n'y a pas de fichier à
            télécharger, et un lien laissé actif finirait sur une page d'erreur du navigateur.
            Un lien ne se désactive pas — d'où la bascule vers un bouton inerte qui porte sa
            raison, comme au catalogue.
            La consolation est dite juste en dessous : l'usage que servait l'export « .txt »,
            emporter sa liste en librairie, est désormais couvert par la liste elle-même.
        *@
        <div class="actions-export">
            @if (Reseau.EnLigne)
            {
                <a class="bouton bouton-discret" href="@ServiceLivresApi.UrlExportSouhaits(FormatExportSouhaits.Texte)" download>
                    Exporter en texte
                </a>
                <a class="bouton bouton-discret" href="@ServiceLivresApi.UrlExportSouhaits(FormatExportSouhaits.Csv)" download>
                    Exporter en CSV
                </a>
            }
            else
            {
                <button type="button" class="bouton bouton-discret" disabled
                        title="@MotifExport">Exporter en texte</button>
                <button type="button" class="bouton bouton-discret" disabled
                        title="@MotifExport">Exporter en CSV</button>
            }
        </div>

        @if (!Reseau.EnLigne)
        {
            <p class="message-discret">
                Les exports sont fabriqués par le serveur : ils reviendront avec le réseau.
                La liste ci-dessous, elle, est celle enregistrée sur cet appareil — c'est
                exactement ce qu'on emportait dans le fichier texte.
            </p>
        }

        @if (ModeOrdre && _souhaits.Count > 1)
        {
            <p class="champ-aide">
                Du plus désiré au moins désiré. Utilisez les flèches pour déplacer une envie —
                sur ordinateur, vous pouvez aussi la faire glisser.
            </p>
        }

        @*
            ⚠️ Les flèches ne sont PAS un ornement à côté du glisser-déposer : le drag & drop
            HTML5 ne fonctionne pas au doigt sur mobile. Sans elles, réordonner n'existerait
            que sur PC — c'est-à-dire pas là où l'on consulte sa liste en librairie.
        *@
        <ul class="liste-souhaits">
            @foreach (var (souhait, index) in _souhaits.Select((s, i) => (s, i)))
            {
                <li class="carte-souhait @(_glisse == souhait.Id ? "carte-glissee" : null)"
                    @key="souhait.Id"
                    draggable="@((ModeOrdre && Reseau.EnLigne && _souhaits.Count > 1).ToString().ToLowerInvariant())"
                    @ondragstart="() => DebuterGlisse(souhait.Id)"
                    @ondragend="TerminerGlisse"
                    @ondragover:preventDefault="ModeOrdre"
                    @ondrop="() => DeposerAsync(index)">

                    @if (ModeOrdre && _souhaits.Count > 1)
                    {
                        <div class="rang-actions">
                            <button type="button" class="bouton bouton-compact"
                                    disabled="@(_enCours || !Reseau.EnLigne || index == 0)"
                                    title="@MotifBlocage"
                                    aria-label="@($"Monter « {souhait.Titre} »")"
                                    @onclick="() => DeplacerAsync(index, index - 1)">▲</button>

                            <span class="rang-numero" aria-hidden="true">@(index + 1)</span>

                            <button type="button" class="bouton bouton-compact"
                                    disabled="@(_enCours || !Reseau.EnLigne || index == _souhaits.Count - 1)"
                                    title="@MotifBlocage"
                                    aria-label="@($"Descendre « {souhait.Titre} »")"
                                    @onclick="() => DeplacerAsync(index, index + 1)">▼</button>
                        </div>
                    }

                    <Couverture Url="@souhait.CoverUrl" Titre="@souhait.Titre"
                                Classe="couverture-petite" Agrandissable="true" />

                    <div class="carte-corps">
                        <p class="carte-titre">@souhait.Titre</p>

                        @if (!string.IsNullOrWhiteSpace(souhait.Auteur))
                        {
                            <p class="carte-auteur">@souhait.Auteur</p>
                        }

                        <p class="carte-details">
                            @*
                                ⚠️ On SIGNALE, on ne supprime pas : la liste d'envies est
                                personnelle et le catalogue est commun, donc l'envie n'est
                                peut-être pas de celui qui a ajouté le livre — et la note
                                (« demandé à Noël ») disparaîtrait avec elle. Le rapprochement
                                est en outre faillible : l'étiquette mène à la fiche pour qu'on
                                puisse vérifier avant de retirer quoi que ce soit.
                            *@
                            @if (souhait is { Possede: true, LivreId: { } livreId })
                            {
                                <a class="etiquette etiquette-possede" href="@($"livres/{livreId}")">
                                    Déjà au catalogue
                                </a>
                            }

                            @if (!string.IsNullOrWhiteSpace(souhait.Editeur))
                            {
                                <span>@souhait.Editeur</span>
                            }
                            @if (!string.IsNullOrWhiteSpace(souhait.Annee))
                            {
                                <span>@souhait.Annee</span>
                            }
                            @if (!string.IsNullOrWhiteSpace(souhait.Isbn))
                            {
                                <span class="etiquette code-isbn">ISBN @FormatageIsbn.Afficher(souhait.Isbn)</span>
                            }
                        </p>

                        @if (!string.IsNullOrWhiteSpace(souhait.Note))
                        {
                            <p class="souhait-note">@souhait.Note</p>
                        }
                    </div>

                    @if (!ModeOrdre)
                    {
                    <button type="button" class="bouton bouton-compact bouton-danger"
                            disabled="@(_enCours || !Reseau.EnLigne)" title="@MotifBlocage"
                            @onclick="() => SupprimerAsync(souhait)"
                            aria-label="@($"Retirer « {souhait.Titre} » de la liste d'envies")">
                        Retirer
                    </button>
                    }
                </li>
            }
        </ul>
    }
}

@* L'ajout est passé dans son propre écran : la page ne montre plus que la liste, comme la
   fiche livre ne montre plus son formulaire. *@
<nav class="actions-flottantes">
    @if (ModeOrdre)
    {
        <a class="bouton bouton-discret" href="souhaits">Terminer</a>
    }
    else if (Reseau.EnLigne)
    {
        <a class="bouton bouton-discret" href="souhaits/ordre">Modifier l'ordre</a>
    }
    @if (Reseau.EnLigne)
    {
        <a class="bouton bouton-principal" href="souhaits/ajout">Ajouter une envie</a>
    }
    else
    {
        <button type="button" class="bouton bouton-principal" disabled
                title="@EtatReseau.MotifHorsLigne">Ajouter une envie</button>
    }
</nav>

@code {
    private IReadOnlyList<SouhaitDto>? _souhaits;
    private bool _enCours;
    private int? _glisse;
    private string? _erreur;
    private bool ModeOrdre =>
        Navigation.ToAbsoluteUri(Navigation.Uri).AbsolutePath.TrimEnd('/')
            .EndsWith("/souhaits/ordre", StringComparison.OrdinalIgnoreCase);

    protected override async Task OnInitializedAsync()
    {
        Reseau.Change += SurChangementReseau;
        Navigation.LocationChanged += SurNavigation;
        await ChargerAsync();
    }

    private void SurNavigation(object? _, LocationChangedEventArgs __) => InvokeAsync(StateHasChanged);

    /// <summary>
    /// Recharge la liste au retour du réseau, et redessine pour rendre les actions à l'écran.
    /// </summary>
    /// <remarks>
    /// On s'abonne à <c>Change</c> et jamais à <c>SynchroChange</c> : recharger écrit un
    /// instantané, qui émettrait une nouvelle synchro, qui rechargerait… sans fin.
    /// </remarks>
    private void SurChangementReseau() => _ = InvokeAsync(async () =>
    {
        await ChargerAsync();
        StateHasChanged();
    });

    public void Dispose()
    {
        Reseau.Change -= SurChangementReseau;
        Navigation.LocationChanged -= SurNavigation;
    }

    /// <summary>Raison du blocage des écritures, ou <c>null</c> quand tout est possible.</summary>
    private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne;

    private const string MotifExport =
        "L'export est produit par le serveur : indisponible hors ligne.";

    /// <remarks>
    /// ⚠️ Aucun message HTTP brut ne doit remonter jusqu'ici. C'était le défaut d'origine :
    /// hors-ligne, l'écran affichait « net_http_message_not_success_statuscode_reason, 404,
    /// Not Found ». La liste passe désormais par le même repli sur instantané que le reste de
    /// l'application, et une panne se dit en français.
    /// </remarks>
    private async Task ChargerAsync()
    {
        try
        {
            _erreur = null;
            _souhaits = await Api.ListerSouhaitsAsync();
        }
        catch (Exception)
        {
            _souhaits ??= [];
            _erreur = Reseau.EnLigne
                ? "La liste d'envies n'a pas pu être chargée. Réessayez dans un instant."
                : "Liste d'envies indisponible hors ligne : aucune donnée n'a encore pu être "
                  + "enregistrée sur cet appareil.";
        }
    }

    /// <summary>
    /// Déplace une envie d'une position à une autre, puis enregistre l'ordre complet.
    /// </summary>
    /// <remarks>
    /// La nouvelle suite est calculée <b>localement</b> et envoyée en entier : le serveur ne
    /// reçoit pas « monte de un » mais « voici l'ordre ». C'est la même opération pour les
    /// flèches et pour le glisser-déposer, donc un seul chemin à vérifier.
    /// <para>
    /// La liste affichée est réordonnée <b>avant</b> l'aller-retour réseau : un déplacement qui
    /// attendrait la réponse donnerait l'impression que la flèche n'a pas répondu. En cas
    /// d'échec, le rechargement remet l'ordre du serveur.
    /// </para>
    /// </remarks>
    private async Task DeplacerAsync(int depuis, int vers)
    {
        if (_souhaits is null || depuis == vers || vers < 0 || vers >= _souhaits.Count)
        {
            return;
        }

        var ordonnee = _souhaits.ToList();
        var deplacee = ordonnee[depuis];
        ordonnee.RemoveAt(depuis);
        ordonnee.Insert(vers, deplacee);

        _souhaits = ordonnee;
        _enCours = true;

        _erreur = await Api.ReordonnerSouhaitsAsync(ordonnee.Select(s => s.Id).ToList());

        _enCours = false;

        if (_erreur is not null)
        {
            // L'ordre affiché n'est pas celui du serveur : on reprend le sien plutôt que de
            // laisser croire que le déplacement a été enregistré.
            await ChargerAsync();
        }
    }

    /// <summary>Fin d'un glisser-déposer : la carte saisie prend la place de celle survolée.</summary>
    private async Task DeposerAsync(int cible)
    {
        if (!ModeOrdre || _souhaits is null || _glisse is not { } id)
        {
            return;
        }

        var depuis = _souhaits.ToList().FindIndex(s => s.Id == id);
        _glisse = null;

        if (depuis >= 0)
        {
            await DeplacerAsync(depuis, cible);
        }
    }

    private void DebuterGlisse(int id)
    {
        if (ModeOrdre && Reseau.EnLigne)
        {
            _glisse = id;
        }
    }

    private void TerminerGlisse() => _glisse = null;

    private async Task SupprimerAsync(SouhaitDto souhait)
    {
        _enCours = true;
        _erreur = await Api.SupprimerSouhaitAsync(souhait.Id);
        _enCours = false;

        if (_erreur is null)
        {
            await ChargerAsync();
        }
    }
}
