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

@*
    Écran séparé, comme la fiche livre l'est de son édition : la page « Envies » ne montre plus
    que la liste, et l'ajout a la place de proposer une vraie recherche.

    Trois façons d'alimenter la liste, dans l'ordre où le besoin se présente :
      — par titre/auteur, le cas NORMAL : on souhaite un livre qu'on n'a pas en main ;
      — par ISBN, quand on l'a en main (en librairie, chez quelqu'un) ;
      — à la main, quand aucune source ne connaît le livre.
*@

<PageTitle>MaBibli — ajouter une envie</PageTitle>

<h1 class="titre-page">Ajouter une envie</h1>

@if (!Reseau.EnLigne)
{
    <p class="message-avertissement" role="status">
        Hors ligne : la recherche interroge la BnF et OpenLibrary, et l'enregistrement passe par
        le serveur. Votre liste reste consultable.
    </p>
}

<div class="onglets-secondaires" role="group" aria-label="Manière de chercher">
    <button type="button" class="segment @(_mode == Mode.Titre ? "segment-actif" : null)"
            @onclick="() => Basculer(Mode.Titre)">Par titre</button>
    <button type="button" class="segment @(_mode == Mode.Isbn ? "segment-actif" : null)"
            @onclick="() => Basculer(Mode.Isbn)">Par ISBN</button>
    <button type="button" class="segment @(_mode == Mode.Manuel ? "segment-actif" : null)"
            @onclick="() => Basculer(Mode.Manuel)">À la main</button>
</div>

@if (_mode == Mode.Titre)
{
    <div class="champ">
        <label class="champ-libelle" for="titre-cherche">Titre <span class="obligatoire">*</span></label>
        <input id="titre-cherche" class="champ-saisie" type="text" @ref="_premierChamp"
               @bind="_titreCherche" @bind:event="oninput" @onkeydown="SurTouche"
               placeholder="La bête humaine" />
    </div>

    <div class="champ">
        <label class="champ-libelle" for="auteur-cherche">Auteur</label>
        <input id="auteur-cherche" class="champ-saisie" type="text"
               @bind="_auteurCherche" @bind:event="oninput" @onkeydown="SurTouche"
               placeholder="Facultatif : resserre la recherche" />
    </div>

    <div class="actions-formulaire">
        <button type="button" class="bouton bouton-principal"
                disabled="@(_recherche || !Reseau.EnLigne || string.IsNullOrWhiteSpace(_titreCherche))"
                title="@MotifBlocage" @onclick="ChercherParTitreAsync">
            @(_recherche ? "Recherche…" : "Chercher")
        </button>
    </div>
}

@if (_mode == Mode.Isbn)
{
    <div class="champ">
        <label class="champ-libelle" for="isbn-cherche">ISBN</label>
        <input id="isbn-cherche" class="champ-saisie" type="text" inputmode="numeric"
               placeholder="978…" @ref="_premierChamp"
               @bind="_isbn" @bind:event="oninput" @onkeydown="SurTouche" />
        <p class="champ-aide">Le livre est en main : le code-barres suffit.</p>
    </div>

    <div class="actions-formulaire">
        <button type="button" class="bouton bouton-principal"
                disabled="@(_recherche || !Reseau.EnLigne || string.IsNullOrWhiteSpace(_isbn))"
                title="@MotifBlocage" @onclick="ChercherParIsbnAsync">
            @(_recherche ? "Recherche…" : "Chercher")
        </button>

        @*
            J2 : le cas d'usage est direct — le livre est en main, en librairie. C'est le même
            composant de scan que l'écran d'ajout au catalogue, sans création de Livre au bout :
            un souhait n'est pas une possession.
        *@
        <button type="button" class="bouton bouton-discret"
                disabled="@(_scan || !Reseau.EnLigne)" title="@MotifBlocage"
                @onclick="OuvrirScanner">
            Scanner
        </button>
    </div>
}

@if (_scan)
{
    <ScannerCodeBarres OnCodeDetecte="SurCodeDetecteAsync" OnAnnuler="() => _scan = false" />
}

@* L1 : la BnF met environ une seconde à répondre. Sans indicateur, l'écran ne dit rien et
   l'on reclique — donc on relance l'appel. *@
@if (_recherche)
{
    <Patience Message="Interrogation de la BnF, puis d'OpenLibrary…" />
}

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

@foreach (var avertissement in _avertissements)
{
    <p class="message-avertissement">@avertissement</p>
}

@if (_candidats.Count > 0)
{
    @* On ne choisit jamais à la place de l'utilisateur : ce sont l'éditeur, l'année et
       l'auteur qui départagent des notices au titre identique. *@
    <p class="message-discret">
        @_candidats.Count notice@(_candidats.Count > 1 ? "s" : "") trouvée@(_candidats.Count > 1 ? "s" : "").
        Choisissez celle qui correspond, ou complétez à la main.
    </p>

    <div class="champ champ-tri">
        <label class="champ-libelle" for="tri-resultats">Trier par</label>
        <select id="tri-resultats" class="champ-saisie" value="@_tri"
                @onchange="e => ChangerTri(e.Value?.ToString())">
            <option value="@TriResultats.Date">Date (récent en premier)</option>
            <option value="@TriResultats.Titre">Titre</option>
            <option value="@TriResultats.Editeur">Éditeur</option>
        </select>
    </div>

    <ul class="liste-candidats">
        @foreach (var (candidat, index) in _candidats.Select((c, i) => (c, i)))
        {
            <li class="carte-candidat" @key="index">
                <Couverture Url="@candidat.CoverUrl" Titre="@candidat.Titre" Classe="couverture-petite" />

                <div class="carte-corps">
                    <p class="carte-titre">@candidat.Titre</p>
                    @if (!string.IsNullOrWhiteSpace(candidat.Auteur))
                    {
                        <p class="carte-auteur">@candidat.Auteur</p>
                    }

                    <p class="candidat-distinction">
                        <span class="candidat-editeur">@(candidat.Editeur ?? "éditeur inconnu")</span>
                        <span class="candidat-annee">@(candidat.Annee ?? "année inconnue")</span>
                    </p>

                    @if (!string.IsNullOrWhiteSpace(candidat.IsbnInterroge))
                    {
                        <p class="carte-details">
                            <span class="etiquette code-isbn">ISBN @FormatageIsbn.Afficher(candidat.IsbnInterroge)</span>
                        </p>
                    }

                    <button type="button" class="bouton bouton-principal"
                            disabled="@(_ajout || !Reseau.EnLigne)" title="@MotifBlocage"
                            @onclick="() => AjouterCandidatAsync(candidat)">
                        Ajouter à ma liste
                    </button>
                </div>
            </li>
        }
    </ul>
}

@if (_mode == Mode.Manuel)
{
    @* Le recours quand aucune source ne connaît le livre — un ouvrage étranger, un titre à
       paraître, un livre d'occasion sans ISBN. Il ne disparaît jamais derrière la recherche. *@
    <div class="champ">
        <label class="champ-libelle" for="titre-souhait">Titre <span class="obligatoire">*</span></label>
        <input id="titre-souhait" class="champ-saisie" type="text" @ref="_premierChamp"
               @bind="_saisie.Titre" @bind:event="oninput" />
    </div>

    <div class="champ">
        <label class="champ-libelle" for="auteur-souhait">Auteur</label>
        <input id="auteur-souhait" class="champ-saisie" type="text"
               @bind="_saisie.Auteur" @bind:event="oninput" />
    </div>

    <div class="champ">
        <label class="champ-libelle" for="note-souhait">Note</label>
        <input id="note-souhait" class="champ-saisie" type="text" @bind="_saisie.Note" @bind:event="oninput"
               placeholder="« demandé à Noël », « en poche seulement »…" />
    </div>

    <div class="actions-formulaire">
        <button type="button" class="bouton bouton-principal"
                disabled="@(_ajout || !Reseau.EnLigne || string.IsNullOrWhiteSpace(_saisie.Titre))"
                title="@MotifBlocage" @onclick="() => AjouterAsync(_saisie)">
            Ajouter à ma liste
        </button>
    </div>
}

<nav class="actions-flottantes">
    <a class="bouton bouton-discret" href="souhaits">Retour à ma liste</a>
</nav>

@code {
    private enum Mode { Titre, Isbn, Manuel }

    private Mode _mode = Mode.Titre;
    private ElementReference _premierChamp;
    private bool _focusAFaire = true;

    private string _titreCherche = string.Empty;
    private string _auteurCherche = string.Empty;
    private string _isbn = string.Empty;

    private EnregistrementSouhait _saisie = new();
    private IReadOnlyList<CandidatLivre> _candidats = [];
    private IReadOnlyList<string> _avertissements = [];
    private TriResultats _tri = TriResultats.Date;
    private bool _recherche;
    private bool _ajout;

    /// <summary>Le scanner est ouvert. Il ne l'est que depuis la voie « par ISBN ».</summary>
    private bool _scan;
    private string? _erreur;

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

    private void SurChangementReseau() => _ = InvokeAsync(StateHasChanged);

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

    private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne;

    private enum TriResultats
    {
        Date,
        Titre,
        Editeur,
    }

    /// <summary>
    /// Focalise le premier champ à l'ouverture et à chaque changement de mode.
    /// </summary>
    /// <remarks>
    /// Même raison qu'à l'écran d'ajout par ISBN : on arrive ici pour taper, et le mode « Par
    /// ISBN » doit rester utilisable à la douchette. Le drapeau évite de reprendre le focus à
    /// chaque rendu, ce qui arracherait le curseur en pleine frappe.
    /// </remarks>
    protected override async Task OnAfterRenderAsync(bool premierRendu)
    {
        if (!_focusAFaire)
        {
            return;
        }

        _focusAFaire = false;

        try
        {
            await _premierChamp.FocusAsync();
        }
        catch (InvalidOperationException)
        {
        }
    }

    private void OuvrirScanner()
    {
        _erreur = null;
        _candidats = [];
        _avertissements = [];
        _scan = true;
    }

    /// <summary>
    /// Enchaîne la recherche sur le code lu, sans bouton à confirmer.
    /// </summary>
    /// <remarks>
    /// Le code reste dans le champ : un décodage erroné se corrige et se relance. C'est la
    /// décision déjà actée pour l'écran d'ajout au catalogue, et la seule cohérente avec une
    /// douchette, qui envoie <c>Entrée</c> d'elle-même.
    /// <para>
    /// ⚠️ L'add-on EAN-2 (numéro de parution) est ignoré ici, et ce n'est pas un oubli : il ne
    /// concerne que les revues, qui ne se souhaitent pas — cet écran alimente une liste
    /// d'<i>œuvres</i>. Un code de périodique est d'ailleurs intercepté plus bas et nommé.
    /// </para>
    /// </remarks>
    private async Task SurCodeDetecteAsync(CodeScanne lecture)
    {
        _scan = false;
        _isbn = lecture.Code;
        await ChercherParIsbnAsync();
    }

    private void Basculer(Mode mode)
    {
        _scan = false;
        _mode = mode;
        _focusAFaire = true;
        _erreur = null;
        _candidats = [];
        _avertissements = [];
        _tri = TriResultats.Date;
    }

    private async Task SurTouche(KeyboardEventArgs e)
    {
        if (e.Key != "Enter")
        {
            return;
        }

        if (_mode == Mode.Titre && !string.IsNullOrWhiteSpace(_titreCherche))
        {
            await ChercherParTitreAsync();
        }
        else if (_mode == Mode.Isbn && !string.IsNullOrWhiteSpace(_isbn))
        {
            await ChercherParIsbnAsync();
        }
    }

    private async Task ChercherParTitreAsync() =>
        await ChercherAsync(() => Api.ChercherNoticesAsync(_titreCherche, _auteurCherche));

    private async Task ChercherParIsbnAsync() =>
        await ChercherAsync(() => Api.ChercherIsbnAsync(_isbn.Trim()));

    private async Task ChercherAsync(Func<Task<ResultatLookupIsbn?>> appel)
    {
        _recherche = true;
        _erreur = null;
        _candidats = [];
        _avertissements = [];

        try
        {
            var resultat = await appel();

            if (resultat is null)
            {
                _erreur = Reseau.EnLigne
                    ? "La recherche n'a rien pu ramener. Réessayez, ou complétez à la main."
                    : "La recherche interroge la BnF : indisponible hors ligne.";
                return;
            }

            _avertissements = resultat.Avertissements;

            // Un code de périodique n'est pas un livre : on le dit ici aussi, plutôt que de
            // laisser un « aucun résultat » qui enverrait chercher un défaut inexistant.
            if (resultat.Periodique is { } revue)
            {
                var nom = revue.Titre is null ? "un magazine" : $"le magazine « {revue.Titre} »";
                _erreur = $"Ce code désigne {nom} (ISSN {revue.Issn}), pas un livre.";
                return;
            }

            _candidats = resultat.Candidats;
            _candidats = Trier(_candidats);

            if (_candidats.Count == 0)
            {
                _erreur = "Aucune notice ne correspond. Vous pouvez l'ajouter à la main.";
            }
        }
        finally
        {
            _recherche = false;
        }
    }

    private void ChangerTri(string? valeur)
    {
        if (!Enum.TryParse<TriResultats>(valeur, out var tri))
        {
            return;
        }

        _tri = tri;
        _candidats = Trier(_candidats);
    }

    private IReadOnlyList<CandidatLivre> Trier(IEnumerable<CandidatLivre> candidats) =>
        _tri switch
        {
            TriResultats.Titre => candidats
                .OrderBy(c => c.Titre, StringComparer.CurrentCultureIgnoreCase)
                .ThenBy(c => c.Editeur, StringComparer.CurrentCultureIgnoreCase)
                .ToList(),
            TriResultats.Editeur => candidats
                .OrderBy(c => c.Editeur ?? string.Empty, StringComparer.CurrentCultureIgnoreCase)
                .ThenBy(c => c.Titre, StringComparer.CurrentCultureIgnoreCase)
                .ToList(),
            _ => candidats
                .OrderByDescending(c => AnneeTriable(c.Annee))
                .ThenBy(c => c.Titre, StringComparer.CurrentCultureIgnoreCase)
                .ToList(),
        };

    private static int AnneeTriable(string? date)
    {
        if (string.IsNullOrWhiteSpace(date))
        {
            return 0;
        }

        for (var i = 0; i + 4 <= date.Length; i++)
        {
            if (int.TryParse(date.AsSpan(i, 4), out var annee) && annee is >= 1000 and <= 2999)
            {
                return annee;
            }
        }

        return 0;
    }

    /// <summary>
    /// Enregistre le candidat choisi, <b>couverture comprise</b>.
    /// </summary>
    /// <remarks>
    /// C'est ce qui remplit enfin <c>LivreSouhaite.CoverUrl</c>, jusqu'ici toujours vide : la
    /// couverture vient d'OpenLibrary dès qu'un ISBN est connu. Sans ISBN il n'y en a pas, et
    /// l'on n'en invente pas — la vignette retombe sur son substitut à initiale.
    /// </remarks>
    private Task AjouterCandidatAsync(CandidatLivre candidat) =>
        AjouterAsync(new EnregistrementSouhait
        {
            Titre = candidat.Titre,
            Auteur = candidat.Auteur,
            Editeur = candidat.Editeur,
            Annee = candidat.Annee,
            Isbn = candidat.IsbnInterroge,
            CoverUrl = candidat.CoverUrl,
        });

    private async Task AjouterAsync(EnregistrementSouhait saisie)
    {
        _ajout = true;
        _erreur = null;

        var resultat = await Api.AjouterSouhaitAsync(saisie);

        _ajout = false;

        if (!resultat.EstOk)
        {
            _erreur = resultat.Erreur;
            return;
        }

        // Retour à la liste : l'envie vient de s'y poser en dernier, et c'est là qu'on la classe.
        Navigation.NavigateTo("souhaits");
    }
}
