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

<PageTitle>MaBibli — ajouter par ISBN</PageTitle>

<h1 class="titre-page">Ajouter par ISBN</h1>

@if (_etape == Etape.Saisie)
{
    <p class="message-discret">
        Scannez le code-barres, ou saisissez l'ISBN imprimé sur le livre.
        Une douchette USB fonctionne telle quelle : le champ est déjà actif.
    </p>

    @*
        Le scan comme le lookup interrogent la BnF puis OpenLibrary : ils EXIGENT le réseau,
        et rien ne peut être ajouté au catalogue hors-ligne de toute façon. Le dire ici évite
        une caméra ouverte pour rien, puis un délai d'attente incompréhensible.
    *@
    @if (!Reseau.EnLigne)
    {
        <p class="message-avertissement" role="status">
            Hors ligne : la recherche par ISBN interroge la BnF et OpenLibrary, et l'ajout au
            catalogue passe par le serveur. Les deux redeviendront possibles au retour du réseau.
        </p>
    }

    @*
        Le champ est focalisé à l'ouverture et valide sur Entrée : c'est tout ce qu'exige une
        douchette USB, qui se présente au système comme un CLAVIER — elle « tape » les chiffres
        puis Entrée. Aucune permission, aucun HTTPS, aucun décodage : c'est le contournement le
        plus rentable du scan caméra, qui rate souvent sur la webcam d'un PC.
        La recherche n'est délibérément PAS déclenchée à chaque frappe : une douchette tape trop
        vite, et chaque caractère partirait en requête.
    *@
    <div class="barre-recherche">
        <input class="champ-saisie" type="text" inputmode="numeric" placeholder="978…"
               @ref="_champIsbn"
               @bind="_isbn" @bind:event="oninput" @onkeydown="SurTouche"
               aria-label="ISBN" autocomplete="off" />
    </div>

    <div class="actions-formulaire">
        <button type="button" class="bouton bouton-principal"
                disabled="@(_chargement || !Reseau.EnLigne || string.IsNullOrWhiteSpace(_isbn))"
                title="@MotifBlocage"
                @onclick="ChercherAsync">
            @(_chargement ? "Recherche…" : "Chercher")
        </button>
        @* La saisie manuelle reste le recours quand le code-barres est abîmé, absent,
           ou que la caméra est indisponible : elle ne disparaît jamais derrière le scan. *@
        <button type="button" class="bouton bouton-discret"
                disabled="@(!Reseau.EnLigne)" title="@MotifBlocage"
                @onclick="OuvrirScanner">
            Scanner
        </button>
        <a class="bouton bouton-discret" href="ajout/manuel">Saisir à la main</a>
    </div>
}

@*
    Le scan enchaîne la recherche tout seul (décision actée). Encore faut-il que ça SE VOIE :
    sans cet écran, on retombait sur la saisie avec un bouton « Chercher » intact, et l'on
    croyait que le scan n'avait rien déclenché.
*@
@if (_etape == Etape.Recherche)
{
    <p class="message-discret" role="status">
        Recherche de <span class="code-isbn">@FormatageIsbn.Afficher(_isbn)</span> à la BnF, puis chez OpenLibrary…
    </p>
}

@if (_etape == Etape.Scan)
{
    <ScannerCodeBarres OnCodeDetecte="SurCodeDetecteAsync" OnAnnuler="Recommencer" />
}

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

@if (_messagePeriodique is not null)
{
    <p class="message-avertissement" role="status">@_messagePeriodique</p>

    @* Une revue a désormais sa place : sa fiche, où l'on ajoute le numéro qu'on vient de
       scanner. Avant, ce code retombait sur la saisie manuelle d'un LIVRE — ce qu'un magazine
       n'est pas. *@
    @if (_periodique is not null)
    {
        <div class="actions-formulaire">
            <button type="button" class="bouton bouton-principal"
                    disabled="@(_enregistrement || !Reseau.EnLigne)" title="@MotifBlocage"
                    @onclick="OuvrirLaRevueAsync">
                @(_enregistrement ? "Ouverture…" : "Ouvrir la fiche de cette revue")
            </button>
        </div>
    }
}

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

@if (_etape == Etape.Choix)
{
    @* Décision actée dans CLAUDE.md : plusieurs notices pour un même ISBN (rééditions
       successives partageant l'ISBN), on ne choisit JAMAIS à la place de l'utilisateur.
       Les candidats partagent titre et auteur : ce sont l'éditeur et l'année qui départagent,
       donc ils sont mis en avant. *@
    <p class="message-discret">
        @_candidats.Count notices correspondent à cet ISBN. Elles se distinguent par l'éditeur et l'année.
    </p>

    <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>

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

                    <button type="button" class="bouton bouton-principal"
                            @onclick="() => Choisir(candidat)">
                        Choisir cette édition
                    </button>
                </div>
            </li>
        }
    </ul>

    <div class="actions-formulaire">
        <button type="button" class="bouton bouton-discret" @onclick="Recommencer">Aucune : saisir à la main</button>
    </div>
}

@if (_etape == Etape.Formulaire)
{
    @* Le doublon prend toute la place tant qu'il n'est pas tranché : la fiche revient intacte
       si l'on renonce. C'est ici que le cas se présente le plus — scanner deux fois le même
       livre est précisément ce qui crée les fiches en double. *@
    @if (_doublons is { } doublons)
    {
        <AvertissementDoublon Doublons="doublons"
                              EnCours="_enregistrement"
                              OnConfirmer="ConfirmerAsync"
                              OnRenoncer="() => _doublons = null" />
    }
    else
    {
        <p class="message-discret">
            Vérifiez et complétez la fiche : tout reste modifiable.
        </p>

        <FormulaireLivre Saisie="_saisie"
                         LibelleValidation="Ajouter au catalogue"
                         Erreur="@_erreurFormulaire"
                         EnCours="_enregistrement"
                         MessageBlocage="@MotifBlocage"
                         OnValider="EnregistrerAsync"
                         OnAnnuler="Recommencer" />
    }
}

@code {
    private enum Etape { Saisie, Scan, Recherche, Choix, Formulaire }

    private Etape _etape = Etape.Saisie;
    private string _isbn = string.Empty;
    private ElementReference _champIsbn;

    // Le focus ne se redonne qu'une fois par entrée dans l'étape de saisie : le reprendre à
    // chaque rendu arracherait le curseur à l'utilisateur en pleine frappe.
    private bool _focusAFaire = true;

    private string? _messagePeriodique;
    private PeriodiqueDetecte? _periodique;

    /// <summary>Add-on EAN-2 du dernier scan, quand il y en avait un. Voir <see cref="CodeScanne"/>.</summary>
    private string? _parutionLue;
    private bool _chargement;
    private bool _enregistrement;
    private string? _erreur;
    private string? _erreurFormulaire;
    private IReadOnlyList<CandidatLivre> _candidats = [];
    private IReadOnlyList<string> _avertissements = [];
    private EnregistrementLivre _saisie = new();
    private DoublonsLivre? _doublons;

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

    /// <summary>
    /// Donne le focus au champ ISBN dès que l'étape de saisie s'affiche.
    /// </summary>
    /// <remarks>
    /// C'est ce qui rend une <b>douchette USB</b> utilisable sans rien d'autre : elle tape dans
    /// le champ actif, quel qu'il soit. Sans focus automatique, l'utilisateur devrait cliquer
    /// dans le champ avant chaque livre — et le gain sur le scan caméra disparaîtrait.
    /// </remarks>
    protected override async Task OnAfterRenderAsync(bool premierRendu)
    {
        if (_etape != Etape.Saisie || !_focusAFaire)
        {
            return;
        }

        _focusAFaire = false;

        try
        {
            await _champIsbn.FocusAsync();
        }
        catch (InvalidOperationException)
        {
            // L'élément a disparu entre le rendu et l'appel (navigation rapide) : sans intérêt.
        }
    }

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

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

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

    private void OuvrirScanner()
    {
        _erreur = null;
        _messagePeriodique = null;
        _periodique = null;
        _parutionLue = null;
        _avertissements = [];
        _etape = Etape.Scan;
    }

    // Le scan enchaîne directement sur le flux de lookup existant : l'utilisateur
    // ne retape jamais ce qui vient d'être scanné, et n'a aucun bouton à confirmer.
    // Le code lu reste dans le champ : si le décodage était mauvais, il se corrige et se relance.
    private async Task SurCodeDetecteAsync(CodeScanne lecture)
    {
        _isbn = lecture.Code;

        // Gardé de côté pour la seule chose qui sache quoi en faire : la fiche d'une revue.
        // Sur un livre, l'add-on EAN-5 (le prix) n'est même pas remonté, et l'EAN-2 n'a pas
        // de sens.
        _parutionLue = lecture.Parution;

        await ChercherAsync();
    }

    private async Task SurTouche(KeyboardEventArgs e)
    {
        if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(_isbn))
        {
            // Saisi à la main ou à la douchette : aucun add-on, et surtout pas celui d'un
            // scan précédent, qui se retrouverait proposé pour un autre magazine.
            _parutionLue = null;
            await ChercherAsync();
        }
    }

    private async Task ChercherAsync()
    {
        _chargement = true;
        _erreur = null;
        _messagePeriodique = null;
        _periodique = null;
        _avertissements = [];
        _etape = Etape.Recherche;

        try
        {
            var resultat = await Api.ChercherIsbnAsync(_isbn.Trim());

            if (resultat is null)
            {
                _erreur = $"« {_isbn} » n'est pas un ISBN valide.";
                RevenirALaSaisie();
                return;
            }

            _avertissements = resultat.Avertissements;
            _candidats = resultat.Candidats;

            // Un code de périodique (977) n'a JAMAIS de candidat : ce n'est pas un échec de
            // recherche, c'est un code qui ne décrit pas un livre. Le dire avant tout le reste.
            if (resultat.Periodique is { } revue)
            {
                DecrireLeMagazine(revue);
                return;
            }

            if (_candidats.Count == 0)
            {
                // Aucune notice : on ne bloque pas, le formulaire manuel reste la porte de sortie.
                _erreur = "Aucune notice trouvée pour cet ISBN. Complétez la fiche à la main.";
                _saisie = new EnregistrementLivre { Isbn = resultat.IsbnDemande };
                _etape = Etape.Formulaire;
                return;
            }

            if (_candidats.Count == 1)
            {
                Choisir(_candidats[0]);
                return;
            }

            _etape = Etape.Choix;
        }
        catch (Exception)
        {
            _erreur = Reseau.EnLigne
                ? "La recherche a échoué. Réessayez, ou saisissez la fiche à la main."
                : "La recherche par ISBN interroge la BnF et OpenLibrary : indisponible hors ligne.";
            RevenirALaSaisie();
        }
        finally
        {
            _chargement = false;
        }
    }

    /// <summary>
    /// Dit ce qu'est le code scanné, et propose la fiche de revue correspondante.
    /// </summary>
    /// <remarks>
    /// ⚠️ <b>Ne remplit surtout pas le formulaire d'un livre</b>, ce qu'il faisait tant que les
    /// revues n'avaient pas de modèle : un magazine n'est pas un livre, et ranger un code
    /// <c>977</c> dans <c>Livre.Isbn</c> ferait échouer tout lookup ultérieur sur la fiche.
    /// <para>
    /// Le <b>numéro</b> se saisit ensuite à la main : les deux chiffres de parution du
    /// code-barres ne sont pas un numéro fiable, et l'add-on EAN-2 qui le porterait vraiment
    /// n'a pas pu être vérifié sur un magazine réel.
    /// </para>
    /// </remarks>
    private void DecrireLeMagazine(PeriodiqueDetecte revue)
    {
        var nom = revue.Titre is null ? "un magazine" : $"le magazine « {revue.Titre} »";

        _periodique = revue;
        _messagePeriodique =
            $"Ce code-barres désigne {nom} (ISSN {revue.Issn}), pas un livre : "
            + "il commence par 977, réservé aux publications en série. "
            + "Ouvrez sa fiche pour y ajouter le numéro que vous venez de scanner.";

        _erreur = null;
        RevenirALaSaisie();
    }

    /// <summary>
    /// Ouvre la fiche de la revue scannée, en la créant si elle n'existe pas encore.
    /// </summary>
    /// <remarks>
    /// Le serveur fait « créer ou retrouver » : scanner le numéro suivant du même magazine
    /// retombe sur la même fiche, ce qui est exactement le geste attendu.
    /// <para>
    /// Sans titre connu de la BnF, on n'invente rien : l'ISSN sert de nom provisoire, que
    /// l'utilisateur corrigera sur la fiche.
    /// </para>
    /// </remarks>
    private async Task OuvrirLaRevueAsync()
    {
        if (_periodique is not { } revue)
        {
            return;
        }

        _enregistrement = true;
        _erreur = null;

        var resultat = await Api.CreerRevueAsync(new EnregistrementRevue
        {
            Titre = revue.Titre ?? $"Revue ISSN {revue.Issn}",
            Issn = revue.Issn,
            Editeur = revue.Editeur,
        });

        _enregistrement = false;

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

        // Le numéro lu sur l'add-on EAN-2 suit la navigation : la fiche le proposera dans un
        // champ modifiable. On ne l'enregistre pas soi-même — voir CodeScanne.
        var numero = string.IsNullOrWhiteSpace(_parutionLue)
            ? string.Empty
            : $"?numero={Uri.EscapeDataString(_parutionLue)}";

        Navigation.NavigateTo($"revues/{resultat.Valeur!.Id}{numero}");
    }

    /// <summary>Ramène l'écran à la saisie, champ ISBN de nouveau actif pour la douchette.</summary>
    private void RevenirALaSaisie()
    {
        _etape = Etape.Saisie;
        _focusAFaire = true;
    }

    private void Choisir(CandidatLivre candidat)
    {
        _saisie = new EnregistrementLivre
        {
            Isbn = _isbn.Trim(),
            Titre = candidat.Titre,
            Auteur = candidat.Auteur,
            Editeur = candidat.Editeur,
            CoverUrl = candidat.CoverUrl,
            Format = Format.Physique,
            Statut = Statut.ALire,
        };

        _erreur = null;
        _etape = Etape.Formulaire;
    }

    private void Recommencer()
    {
        _erreur = null;
        _messagePeriodique = null;
        _periodique = null;
        _erreurFormulaire = null;
        _doublons = null;
        _candidats = [];
        RevenirALaSaisie();
    }

    private Task EnregistrerAsync() => AjouterAsync(false);

    /// <summary>Ajout maintenu après avoir vu ce que le catalogue contenait déjà.</summary>
    private Task ConfirmerAsync() => AjouterAsync(true);

    private async Task AjouterAsync(bool confirmerDoublon)
    {
        _enregistrement = true;
        _erreurFormulaire = null;

        var resultat = await Api.CreerAsync(_saisie, confirmerDoublon);

        _enregistrement = false;

        // Rien n'a été écrit : le catalogue contient déjà quelque chose de semblable, et c'est
        // à l'utilisateur de dire s'il s'agit du même livre. Voir AvertissementDoublon.
        if (resultat.Doublons is { } doublons)
        {
            _doublons = doublons;
            return;
        }

        _doublons = null;

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

        Navigation.NavigateTo("/");
    }
}
