using Microsoft.JSInterop;

namespace MaBibli.Client.Services;

/// <summary>
/// État du réseau, tel que l'interface doit le montrer, et date du dernier rafraîchissement
/// des données mises en cache.
/// </summary>
/// <remarks>
/// <b>Deux sources, pas une.</b> <c>navigator.onLine</c> ne vaut que par sa négation : « faux »
/// est fiable, « vrai » ne prouve rien (portail captif, serveur arrêté, Wi-Fi sans Internet).
/// L'état affiché combine donc l'événement du navigateur et le sort réel des appels HTTP —
/// un appel qui échoue bascule l'application en « hors ligne » sans attendre l'accord du
/// navigateur, et le premier appel qui repasse la rebascule en ligne.
/// <para>
/// Ce service est le seul point d'observation de l'interface : la mise en page s'y abonne pour
/// afficher le bandeau et la date de synchronisation, les écrans pour désactiver les actions
/// d'écriture.
/// </para>
/// </remarks>
public sealed class EtatReseau(CacheHorsLigne cache) : IAsyncDisposable
{
    private DotNetObjectReference<EtatReseau>? _reference;

    private bool _navigateurEnLigne = true;
    private bool _dernierAppelEchoue;

    /// <summary>Faux dès qu'un appel a échoué ou que le navigateur signale la perte du réseau.</summary>
    public bool EnLigne => _navigateurEnLigne && !_dernierAppelEchoue;

    /// <summary>
    /// Faut-il tenter un appel réseau ? <b>Oui dès que le navigateur a une connexion</b>, même
    /// si le dernier appel a échoué.
    /// </summary>
    /// <remarks>
    /// C'est ce qui permet de sortir tout seul du mode hors-ligne quand la panne venait du
    /// <b>serveur</b> et non de l'appareil : <c>navigator.onLine</c> n'a alors jamais changé, donc
    /// aucun événement <c>online</c> ne viendra jamais. Sans cette distinction, l'application
    /// resterait bloquée sur le cache jusqu'au rechargement de la page — constaté en essai.
    /// Le prix est d'une requête qui échoue par lecture tant que le serveur est absent : elle
    /// échoue vite, et l'affichage retombe sur le cache comme avant.
    /// </remarks>
    public bool TenterLeReseau => _navigateurEnLigne;

    /// <summary>
    /// Instant du dernier rafraîchissement réussi des données en cache, <c>null</c> si l'on n'a
    /// jamais rien pu enregistrer sur cet appareil.
    /// </summary>
    public DateTimeOffset? DerniereSynchro { get; private set; }

    /// <summary>Vrai quand ce qui est affiché vient du cache et non du réseau.</summary>
    public bool DonneesDuCache => !EnLigne && DerniereSynchro is not null;

    /// <summary>Raison affichable du blocage des actions d'écriture.</summary>
    public const string MotifHorsLigne =
        "Hors ligne : la bibliothèque reste consultable, mais rien ne peut être modifié.";

    /// <summary>
    /// Bascule en ligne ↔ hors ligne, et <b>elle seule</b>.
    /// </summary>
    /// <remarks>
    /// La date de synchronisation a son propre événement (<see cref="SynchroChange"/>) : sans
    /// cette séparation, un écran qui rechargerait ses données sur <c>Change</c> déclencherait
    /// l'écriture d'un instantané, donc un nouvel événement, donc un rechargement — une boucle
    /// sans fin.
    /// </remarks>
    public event Action? Change;

    /// <summary>Nouvelle date de synchronisation : de quoi rafraîchir un affichage, rien de plus.</summary>
    public event Action? SynchroChange;

    /// <summary>À appeler une fois au démarrage : lit l'état courant et s'abonne aux bascules.</summary>
    public async Task DemarrerAsync()
    {
        _reference ??= DotNetObjectReference.Create(this);
        var enLigne = await cache.SurveillerAsync(_reference);
        Definir(enLigne, false);
    }

    [JSInvokable]
    public void SurChangementReseau(bool enLigne) => Definir(enLigne, _dernierAppelEchoue && !enLigne);

    /// <summary>Un appel HTTP a échoué : on est hors ligne, quoi qu'en dise le navigateur.</summary>
    public void SignalerEchecReseau() => Definir(_navigateurEnLigne, true);

    /// <summary>Un appel HTTP a abouti : la preuve la plus solide qu'on soit en ligne.</summary>
    public void SignalerSuccesReseau() => Definir(true, false);

    /// <summary>Enregistre qu'un instantané vient d'être rangé, avec sa date.</summary>
    public void SignalerSynchro(DateTimeOffset date)
    {
        if (DerniereSynchro is { } precedente && precedente >= date)
        {
            return;
        }

        DerniereSynchro = date;
        SynchroChange?.Invoke();
    }

    private void Definir(bool navigateurEnLigne, bool dernierAppelEchoue)
    {
        var avant = EnLigne;

        _navigateurEnLigne = navigateurEnLigne;
        _dernierAppelEchoue = dernierAppelEchoue;

        // L'événement ne parle que de l'état VISIBLE : les écrans s'y abonnent pour se recharger,
        // et le déclencher pour un changement interne les ferait boucler.
        if (avant != EnLigne)
        {
            Change?.Invoke();
        }
    }

    public ValueTask DisposeAsync()
    {
        _reference?.Dispose();
        _reference = null;
        return ValueTask.CompletedTask;
    }
}
