using System.Text.Json;
using Microsoft.JSInterop;

namespace MaBibli.Client.Services;

/// <summary>Clés des instantanés rangés dans IndexedDB.</summary>
/// <remarks>
/// Un instantané par vue de l'API, et non par requête : c'est ce qui permet de chercher et de
/// trier hors-ligne sur <b>tout</b> le fonds, y compris des recherches jamais tapées en ligne.
/// </remarks>
public static class ClesCache
{
    /// <summary>Le catalogue <b>entier</b>, sans aucun filtre. La recherche hors-ligne s'y applique.</summary>
    public const string Catalogue = "catalogue";

    public const string Auteurs = "auteurs";

    public const string PretsEnCours = "prets-en-cours";

    public const string Utilisateur = "utilisateur";
}

/// <summary>Un instantané relu du cache, avec la date de la synchronisation qui l'a produit.</summary>
public sealed record Instantane<T>(T Donnees, DateTimeOffset Date);

/// <summary>
/// Cache de consultation hors-ligne, en <b>lecture seule</b> (CLAUDE.md).
/// </summary>
/// <remarks>
/// Rien n'est mis en file d'attente et rien n'est synchronisé en retour : ce cache ne sert qu'à
/// afficher la bibliothèque déjà enregistrée quand le réseau manque. Les écritures sont refusées
/// en amont, par <see cref="ServiceLivresApi"/>.
/// <para>
/// Toute panne du stockage (IndexedDB indisponible, quota, navigation privée) est avalée : ne pas
/// pouvoir cacher n'est pas une raison d'empêcher l'application de fonctionner en ligne.
/// </para>
/// </remarks>
public sealed class CacheHorsLigne(IJSRuntime js) : IAsyncDisposable
{
    private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);

    private IJSObjectReference? _module;

    /// <summary>Vrai tant que le stockage n'a pas montré qu'il était inutilisable.</summary>
    public bool Disponible { get; private set; } = true;

    private async ValueTask<IJSObjectReference> ModuleAsync() =>
        _module ??= await js.InvokeAsync<IJSObjectReference>("import", "./js/cache-hors-ligne.js");

    public async Task<Instantane<T>?> LireAsync<T>(string cle)
    {
        try
        {
            var module = await ModuleAsync();
            var brut = await module.InvokeAsync<EntreeCache?>("lire", cle);

            if (brut?.Json is null)
            {
                return null;
            }

            var donnees = JsonSerializer.Deserialize<T>(brut.Json, Json);
            return donnees is null ? null : new Instantane<T>(donnees, brut.Date);
        }
        catch (Exception)
        {
            // Cache illisible : on se comporte comme s'il était vide.
            Disponible = false;
            return null;
        }
    }

    public async Task EcrireAsync<T>(string cle, T valeur, DateTimeOffset date)
    {
        try
        {
            var module = await ModuleAsync();
            await module.InvokeVoidAsync(
                "ecrire", cle, JsonSerializer.Serialize(valeur, Json), date.ToString("O"));
        }
        catch (Exception)
        {
            Disponible = false;
        }
    }

    /// <summary>État déclaré par le navigateur. Voir la nuance dans <c>cache-hors-ligne.js</c>.</summary>
    public async Task<bool> EnLigneAsync()
    {
        try
        {
            var module = await ModuleAsync();
            return await module.InvokeAsync<bool>("enLigne");
        }
        catch (Exception)
        {
            return true; // Sans information, on suppose le réseau : un échec d'appel corrigera.
        }
    }

    /// <summary>Abonne <paramref name="destinataire"/> aux événements <c>online</c>/<c>offline</c>.</summary>
    public async Task<bool> SurveillerAsync<T>(DotNetObjectReference<T> destinataire) where T : class
    {
        try
        {
            var module = await ModuleAsync();
            return await module.InvokeAsync<bool>("surveiller", destinataire);
        }
        catch (Exception)
        {
            return true;
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (_module is null)
        {
            return;
        }

        try
        {
            await _module.DisposeAsync();
        }
        catch (JSDisconnectedException)
        {
            // Page en cours de fermeture : il n'y a plus personne à qui parler.
        }
    }

    private sealed record EntreeCache(string? Json, DateTimeOffset Date);
}
