using Deploymentcenter.Client;
using Serilog;

namespace Predictalytics.Hosting;

/// <summary>An activated license: the key belonging to this machine plus the last result.</summary>
public sealed class LicenseSession
{
    public LicenseSession(LicenseClient client, string licenseKey, LicenseValidationResult result)
    {
        Client = client;
        LicenseKey = licenseKey;
        LastResult = result;
    }

    public LicenseClient Client { get; }
    public string LicenseKey { get; }
    public LicenseValidationResult LastResult { get; internal set; }
}

/// <summary>Ergebnis der Pruefung des zwischengespeicherten Schluessels.</summary>
/// <param name="Session">Gesetzt, wenn eine nutzbare Lizenz vorliegt.</param>
/// <param name="LastResult">Letzte Serverantwort — null, wenn gar kein Schluessel hinterlegt war.</param>
public readonly record struct CachedLicenseCheck(LicenseSession? Session, LicenseValidationResult? LastResult)
{
    public bool IsUsable => Session is not null;
}

/// <summary>
/// Startup license gate backed by the Deployment Center (POST /api/license/v1/validate).
/// <para>
/// Endpoint and product slug are deliberately compiled in (see <see cref="DcConfig"/>):
/// a configurable endpoint would let anyone point the app at a fake license server.
/// </para>
/// <para>
/// Enthaelt bewusst keinen Dialog: die interaktive Aktivierung liegt beim Host
/// (Avalonia-Fenster), der Headless-Modus bezieht den Schluessel aus der
/// Umgebungsvariable oder dem verschluesselten Cache.
/// </para>
/// </summary>
public static class LicenseGuard
{
    /// <summary>Re-check interval while the app is running (12 h).</summary>
    public static readonly TimeSpan RevalidationInterval = TimeSpan.FromHours(12);

    public static LicenseClient CreateClient() => new();

    /// <summary>Hardware ID v2 of this machine — shown in the dialog and needed for support.</summary>
    public static HardwareIdResult GetHardwareInfo() => HardwareId.GetHardwareId(DcConfig.ProductSlug);

    /// <summary>Ablageort des verschluesselten Lizenz-Caches.</summary>
    public static string StorageDirectory => LicenseConfig.GetStorageDirectory(DcConfig.ProductSlug);

    /// <summary>
    /// Prueft den Schluessel der letzten erfolgreichen Aktivierung aus dem verschluesselten
    /// Cache. Der Host entscheidet, was bei einem negativen Ergebnis passiert — Dialog
    /// anzeigen (GUI) oder mit Fehlercode beenden (headless).
    /// </summary>
    public static async Task<CachedLicenseCheck> TryUseCachedAsync(LicenseClient? client = null)
    {
        client ??= CreateClient();
        var hardware = GetHardwareInfo();

        // The client takes the key as a parameter on every call; the key of the last
        // successful activation lives in the encrypted local cache.
        var cachedKey = LicenseClient.TryGetCachedKey(DcConfig.ProductSlug);
        if (string.IsNullOrWhiteSpace(cachedKey))
        {
            return new CachedLicenseCheck(null, null);
        }

        var result = await client.ValidateAsync(DcConfig.ProductSlug, cachedKey!, DcConfig.BaseUrl);
        if (result.IsValid)
        {
            Log.Information("🔑 Lizenz geprüft: {Status}{Cached} (HWID {Hwid}, Quelle {Source})",
                result.Status, result.IsCached ? " — aus Offline-Cache" : "",
                hardware.HardwareId, hardware.HwidSource);
            WarnIfGraceRunningOut(result);
            return new CachedLicenseCheck(new LicenseSession(client, cachedKey!, result), result);
        }

        Log.Warning("🔑 Gespeicherter Lizenzschlüssel nicht nutzbar ({Status}): {Message}",
            result.Status, result.Message);
        return new CachedLicenseCheck(null, result);
    }

    /// <summary>Aktiviert einen neu eingegebenen Schluessel.</summary>
    public static async Task<(LicenseSession? Session, LicenseValidationResult Result)> ActivateAsync(
        LicenseClient client, string licenseKey)
    {
        var key = licenseKey.Trim().ToUpperInvariant();
        var result = await client.ValidateAsync(DcConfig.ProductSlug, key, DcConfig.BaseUrl);
        return result.IsValid
            ? (new LicenseSession(client, key, result), result)
            : (null, result);
    }

    /// <summary>
    /// Starts the periodic in-app revalidation. Detects revocation/expiry while the app keeps
    /// running; only a definitive negative triggers <paramref name="onUnusable"/> — a server
    /// outage must not.
    /// </summary>
    public static IDisposable StartPeriodicRevalidation(
        LicenseSession session, Action<LicenseValidationResult> onUnusable)
    {
        var cts = new CancellationTokenSource();

        _ = Task.Run(async () =>
        {
            using var timer = new PeriodicTimer(RevalidationInterval);
            try
            {
                while (await timer.WaitForNextTickAsync(cts.Token))
                {
                    try
                    {
                        var result = await session.Client.ValidateAsync(
                            DcConfig.ProductSlug, session.LicenseKey, DcConfig.BaseUrl);
                        session.LastResult = result;

                        if (result.IsValid)
                        {
                            if (result.IsCached)
                            {
                                Log.Warning("Lizenzserver nicht erreichbar — Prüfung erfolgte aus dem Offline-Cache.");
                            }
                            WarnIfGraceRunningOut(result);
                            continue;
                        }

                        if (result.IsTransient)
                        {
                            // server_unavailable / cache_expired: no verdict, only a failed connection.
                            // A running installation must not be shut down for that — but an exhausted
                            // grace period is worth more than a warning: the next start will stop at
                            // the license dialog.
                            if (result.Status.Equals("cache_expired", StringComparison.OrdinalIgnoreCase))
                            {
                                Log.Error("Offline-Gnadenfrist abgelaufen und das Deployment Center ist nicht erreichbar. " +
                                          "Diese Sitzung läuft weiter, der nächste Start verlangt aber eine Online-Prüfung: {Message}",
                                    result.Message);
                            }
                            else
                            {
                                Log.Warning("Lizenz-Revalidierung vorläufig fehlgeschlagen ({Status}): {Message} — wird erneut versucht.",
                                    result.Status, result.Message);
                            }
                            continue;
                        }

                        Log.Fatal("Lizenzprüfung fehlgeschlagen ({Status}): {Message} — Anwendung wird beendet.",
                            result.Status, result.Message);
                        onUnusable(result);
                        return;
                    }
                    catch (Exception ex)
                    {
                        // Never kill the app from an unexpected exception here.
                        Log.Warning(ex, "Periodische Lizenz-Revalidierung fehlgeschlagen (wird erneut versucht).");
                    }
                }
            }
            catch (OperationCanceledException)
            {
                // Regulaeres Beenden.
            }
        }, cts.Token);

        return new Stopper(cts);
    }

    /// <summary>
    /// Since 2.1 the offline grace period is really bounded (cache_ttl_hours, default 168 h).
    /// A machine that is offline on purpose should learn that before it runs out, not after.
    /// </summary>
    private static void WarnIfGraceRunningOut(LicenseValidationResult result)
    {
        if (result.CacheExpiresAt is not { } expiresAt || expiresAt <= 0) return;

        var remaining = DateTimeOffset.FromUnixTimeSeconds(expiresAt) - DateTimeOffset.UtcNow;
        if (remaining > TimeSpan.FromHours(48)) return;

        Log.Warning("🔑 Offline-Gnadenfrist endet in {Hours:F0} h ({Until:yyyy-MM-dd HH:mm} UTC) — " +
                    "bis dahin muss das Deployment Center einmal erreichbar sein.",
            Math.Max(0, remaining.TotalHours), DateTimeOffset.FromUnixTimeSeconds(expiresAt).UtcDateTime);
    }

    private sealed class Stopper(CancellationTokenSource cts) : IDisposable
    {
        public void Dispose()
        {
            try { cts.Cancel(); } catch { /* bereits beendet */ }
            cts.Dispose();
        }
    }
}
