@using MaBibli.Client.Services
@inject CacheHorsLigne Cache
@inject EtatReseau Reseau
@*
    Couverture d'un livre, avec substitut.

    Les URL de couverture OpenLibrary renvoient des 502 intermittents (mesuré en phase 2 :
    200, 200, puis 502 sur le même ISBN). On ne valide donc JAMAIS l'URL par un appel préalable :
    on l'affiche, et si l'image casse, l'attribut onerror la masque et laisse apparaître le
    substitut qui se trouve déjà dessous. Aucun aller-retour serveur, aucun état C# à gérer.

    @key sur l'img : si l'URL change, Blazor recrée l'élément au lieu de réutiliser celui que
    le onerror a marqué comme cassé.

    Agrandissement (IDEES.md) : seulement quand il y a une URL. Sans couverture, il n'y a rien
    à agrandir - le substitut à initiale reste un simple décor, jamais un bouton.

    ⚠️ Image cassée après coup (404/502 intermittent d'OpenLibrary) : le onerror désactive aussi
    le bouton déclencheur (this.closest('.couverture-declencheur').disabled = true), pas
    seulement l'image. Sans ça, cliquer une vignette cassée agrandissait quand même une image
    cassée (A4, IDEES.md).

    Cache hors-ligne (A5, IDEES.md) : EN LIGNE, l'affichage utilise l'URL réseau telle quelle —
    rien ne doit ralentir la voie rapide — et une mise en cache démarre en tâche de fond,
    sans attendre son résultat, pour que la même couverture reste visible hors-ligne. HORS
    LIGNE, on tente d'abord le cache : l'URL réseau échouerait de toute façon.
    ⚠️ L'allègement (redimensionner, convertir en WebP) n'est PAS fait ici : à mesurer avant
    de s'y engager, comme pour l'AOT — voir IDEES.md.
*@
<div class="couverture @Classe">
    <span class="couverture-substitut" aria-hidden="true">@Initiale</span>

    @if (ADesPixels)
    {
        @if (Agrandissable)
        {
            @* Un vrai <button> et pas une <div> cliquable : cible tactile, focus clavier,
               et activation à l'Entrée sans une ligne de code. *@
            <button type="button" class="couverture-declencheur"
                    aria-label="Agrandir la couverture de @Titre"
                    @onclick="Ouvrir">
                <img @key="UrlAffichee"
                     class="couverture-image"
                     src="@UrlAffichee"
                     alt="Couverture de @Titre"
                     loading="lazy"
                     onerror="this.classList.add('image-cassee'); const b=this.closest('.couverture-declencheur'); if(b){b.disabled=true;}" />
            </button>
        }
        else
        {
            <img @key="UrlAffichee"
                 class="couverture-image"
                 src="@UrlAffichee"
                 alt="Couverture de @Titre"
                 loading="lazy"
                 onerror="this.classList.add('image-cassee')" />
        }
    }
</div>

@if (_agrandie)
{
    @* Fermeture au clic extérieur (le clic sur l'image ne remonte pas) et à Échap.
       Le tabindex rend le calque focusable : sans focus, aucun keydown ne lui parviendrait. *@
    <div class="calque-couverture" role="dialog" aria-modal="true"
         aria-label="Couverture de @Titre"
         tabindex="-1" @ref="_calque"
         @onclick="Fermer" @onkeydown="SurToucheAsync">
        <img class="calque-image" src="@UrlAffichee" alt="Couverture de @Titre"
             @onclick:stopPropagation="true"
             onerror="this.classList.add('image-cassee')" />

        <button type="button" class="calque-fermer" aria-label="Fermer">✕</button>
    </div>
}

@code {
    [Parameter] public string? Url { get; set; }

    [Parameter] public string? Titre { get; set; }

    [Parameter] public string? Classe { get; set; }

    /// <summary>Le clic sur l'image l'affiche en grand. Sans effet s'il n'y a pas de couverture.</summary>
    [Parameter] public bool Agrandissable { get; set; }

    private bool _agrandie;
    private bool _aFocaliser;
    private ElementReference _calque;
    private string? _urlCachee;
    private (string? Url, bool EnLigne)? _urlResoluePour;

    private bool ADesPixels => !string.IsNullOrWhiteSpace(Url);

    /// <summary>
    /// URL réellement posée en <c>src</c> : l'URL réseau en ligne (voie rapide, inchangée), le
    /// blob mis en cache hors-ligne quand il existe, sinon l'URL réseau telle quelle (qui échoue
    /// simplement, comme avant cette fonctionnalité).
    /// </summary>
    private string? UrlAffichee => !Reseau.EnLigne && _urlCachee is not null ? _urlCachee : Url;

    private string Initiale =>
        string.IsNullOrWhiteSpace(Titre) ? "?" : Titre.Trim()[..1].ToUpperInvariant();

    private void Ouvrir()
    {
        _agrandie = true;
        _aFocaliser = true;
    }

    private void Fermer() => _agrandie = false;

    private void SurToucheAsync(KeyboardEventArgs e)
    {
        if (e.Key is "Escape" or "Esc")
        {
            Fermer();
        }
    }

    protected override async Task OnParametersSetAsync()
    {
        // ⚠️ Le jeton compare aussi l'état réseau, pas seulement l'URL : sans quoi, passer
        // en ligne → hors ligne sans que l'URL change (le cas le plus courant) ne relirait
        // jamais le cache, et la couverture resterait invisible hors-ligne.
        var jeton = (Url, Reseau.EnLigne);
        if (!ADesPixels || _urlResoluePour == jeton)
        {
            return;
        }

        _urlResoluePour = jeton;
        _urlCachee = null;

        if (Reseau.EnLigne)
        {
            // Tâche de fond, jamais attendue : la voie d'affichage en ligne ne doit rien à
            // ce cache, elle reste aussi rapide qu'avant.
            _ = Cache.MettreEnCacheCouvertureAsync(Url!);
        }
        else
        {
            _urlCachee = await Cache.LireCouvertureCacheeAsync(Url!);
            StateHasChanged();
        }
    }

    protected override async Task OnAfterRenderAsync(bool premierRendu)
    {
        if (_aFocaliser)
        {
            _aFocaliser = false;
            await _calque.FocusAsync();
        }
    }
}

