using ClawdDotNet.App.Settings;
using Deploymentcenter.Client;
using Microsoft.Extensions.Logging;

namespace ClawdDotNet.App.Services;

/// <summary>
/// Durchsetzung der Lizenz für ClawdDotNet gegen das Lizenzmodul des Deploymentcenters.
///
/// <para><b>Nur ein Urteil sperrt.</b> Das SDK trennt seit 2.1 zwei Dinge, die vorher
/// beide als „Lizenz ungültig" ankamen: eine Aussage des Servers über die Lizenz
/// (<c>revoked</c>, <c>expired</c>, <c>not_found</c>, <c>activation_limit</c>,
/// <c>suspended</c>, <c>clock_rollback</c>) und ein gescheiterter Versuch, überhaupt
/// eine zu bekommen (<see cref="LicenseValidationResult.IsTransient"/>). Nur das Urteil
/// beendet die Anwendung. Ein Serverausfall darf nicht jede Installation gleichzeitig
/// aussperren.</para>
///
/// <para>Die Offline-Gnadenfrist steckt im SDK: Es legt nach jeder erfolgreichen Prüfung
/// einen mit AES-GCM verschlüsselten, an die Hardware gebundenen Zwischenspeicher an und
/// trägt damit über Ausfälle hinweg — begrenzt durch <c>cache_ttl_hours</c> des Projekts
/// (Vorgabe 168 h), nicht mehr durch das Ablaufdatum der Lizenz.</para>
///
/// <para><b>Was diese Fassung nicht kann.</b> Deaktivieren (Aktivierungsplatz freigeben)
/// verlangt den <c>shared_key</c> des Servers. Der gehört nicht in eine ausgelieferte
/// Anwendung, deshalb läuft der Weg über die Hardware-Liste im WebUI („Freigeben").
/// Eine Signaturprüfung findet nicht statt — siehe <see cref="LicenseInfo"/>.</para>
/// </summary>
public sealed class LicenseGate
{
    private readonly SettingsManager _settings;
    private readonly ILogger _logger;
    private readonly ILicensePrompt _prompt;
    private readonly LicenseClient _client;
    private readonly string _serverUrl;

    private string? _hardwareId;

    public LicenseGate(SettingsManager settings, ILogger logger, ILicensePrompt prompt)
    {
        _settings = settings;
        _logger = logger;
        _prompt = prompt;

        _serverUrl = string.IsNullOrWhiteSpace(settings.AppSettings.DeploymentcenterUrl)
            ? LicenseInfo.DefaultServerUrl
            : settings.AppSettings.DeploymentcenterUrl.Trim();

        // Die Version landet in der Aktivierungsliste des Deploymentcenters. Ohne das
        // trug dort jede Installation dieselbe "1.0.0", obwohl die Spalte dafür da ist.
        LicenseClient.DefaultAppVersion = ReleaseInfo.Version;

        // Kein eigener HttpClient mehr: Der interne des SDK hat seit 2.1 eine Zeitgrenze
        // von 15 s. Vorher waren es 100 s — und damit ein Standbild beim Start, wenn der
        // Server nicht antwortete.
        _client = new LicenseClient();
    }

    /// <summary>
    /// True, wenn eine Serveradresse hinterlegt ist. Ohne sie wird die Prüfung
    /// übersprungen — sonst gäbe es eine Henne-Ei-Sperre, bevor überhaupt jemand etwas
    /// eintragen kann.
    /// </summary>
    public bool IsEnforcementConfigured => !string.IsNullOrWhiteSpace(_serverUrl);

    /// <summary>
    /// Die Hardware-ID v2 dieses Rechners. Einmal berechnet und behalten: Die Ermittlung
    /// liest unter Linux Dateien und zählt Netzwerkschnittstellen auf.
    /// </summary>
    public string HardwareId =>
        // Voll qualifiziert: Sonst zeigte der Name auf diese Eigenschaft selbst.
        _hardwareId ??= global::Deploymentcenter.Client.HardwareId
            .GetHardwareId(LicenseInfo.ProductSlug).HardwareId;

    /// <summary>
    /// Prüft den hinterlegten Schlüssel erneut — für die laufende Nachprüfung. Ein
    /// widerrufener Schlüssel schlägt damit auch durch, ohne dass jemand neu startet.
    /// </summary>
    public Task<LicenseValidationResult> RevalidateAsync(CancellationToken ct = default)
        => ValidateAsync(_settings.AppSettings.LicenseKey, ct);

    /// <summary>
    /// Startprüfung. Fragt über <see cref="ILicensePrompt"/> nach, bis eine nutzbare
    /// Lizenz vorliegt, oder gibt <c>false</c> zurück — dann beendet der Aufrufer die
    /// Anwendung.
    /// </summary>
    public async Task<bool> RunStartupCheckAsync(CancellationToken ct = default)
    {
        if (!IsEnforcementConfigured)
        {
            _logger.LogWarning(
                "Lizenzprüfung nicht konfiguriert (keine Deploymentcenter-URL) – übersprungen.");
            return true;
        }

        var key = _settings.AppSettings.LicenseKey;

        if (string.IsNullOrWhiteSpace(key))
        {
            key = await _prompt.RequestKeyAsync(HardwareId, null, null);
            if (key is null) return false;
        }

        var result = await ValidateAsync(key, ct);

        while (!result.IsValid)
        {
            // Kein Urteil, sondern ein gescheiterter Versuch: weiterlaufen. Ein anderer
            // Schlüssel würde daran nichts ändern, und danach zu fragen ließe den
            // Benutzer raten. Der Notausschalter greift, sobald der Server wieder
            // antwortet (LicenseWatch).
            if (result.IsTransient)
            {
                _logger.LogWarning(
                    "Lizenz nicht prüfbar ({Status}): {Message} – Start wird fortgesetzt.",
                    result.Status, result.Message);

                await _prompt.ShowInfoAsync("ClawdDotNet – Lizenz",
                    "Die Lizenz konnte nicht geprüft werden (Server nicht erreichbar oder "
                    + "Offline-Frist abgelaufen). ClawdDotNet läuft weiter und prüft später "
                    + "erneut.");

                return true;
            }

            if (result.Status == "clock_rollback")
            {
                // Bewusst ohne Details: Ein genauer Text wäre eine Bauanleitung.
                await _prompt.ShowErrorAsync("ClawdDotNet – Lizenz",
                    "Die Lizenzprüfung konnte nicht abgeschlossen werden. "
                    + "Bitte den Support kontaktieren.");
                return false;
            }

            _logger.LogWarning(
                "Lizenz abgelehnt: {Status} – {Message} (Produkt {Slug}, Server {Server})",
                result.Status, result.Message, LicenseInfo.ProductSlug, _serverUrl);

            var retry = await _prompt.RequestKeyAsync(HardwareId, DescribeProblem(result), key);
            if (retry is null) return false;

            key = retry;
            result = await ValidateAsync(key, ct);
        }

        _settings.AppSettings.LicenseKey = key;

        try
        {
            _settings.Save();
        }
        catch (SettingsPersistenceException ex)
        {
            // Der Schlüssel ist gültig, ließ sich aber nicht sichern. Weiterlaufen ja —
            // beim nächsten Start wird eben erneut gefragt.
            _logger.LogWarning(ex, "Lizenzschlüssel konnte nicht gespeichert werden");
        }

        if (result.IsCached)
        {
            var until = Describe(result.CacheExpiresAt);

            _logger.LogInformation("Lizenz offline gültig, Gnadenfrist bis {Until}.", until);

            await _prompt.ShowInfoAsync("ClawdDotNet – Lizenz",
                $"Lizenzserver nicht erreichbar. Offline gültig bis {until}.");
        }

        return true;
    }

    private async Task<LicenseValidationResult> ValidateAsync(string key, CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(key))
        {
            return new LicenseValidationResult
            {
                IsValid = false,
                Status = "not_found",
                Message = "Kein Lizenzschlüssel hinterlegt."
            };
        }

        try
        {
            return await _client.ValidateAsync(
                LicenseInfo.ProductSlug, key, _serverUrl, ReleaseInfo.Version, ct);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception ex)
        {
            // Das SDK fängt Netz- und HTTP-Fehler selbst ab und liefert sie als
            // IsTransient. Was hier noch ankommt, ist unerwartet — und darf trotzdem
            // nicht als Urteil über die Lizenz gelten.
            _logger.LogError(ex, "Lizenzprüfung fehlgeschlagen.");

            return new LicenseValidationResult
            {
                IsValid = false,
                Status = "server_unavailable",
                Message = "Lizenz konnte nicht geprüft werden.",
                IsTransient = true
            };
        }
    }

    private static string Describe(long? unixSeconds) =>
        unixSeconds is { } seconds and > 0
            ? DateTimeOffset.FromUnixTimeSeconds(seconds).LocalDateTime.ToString("g")
            : "auf Weiteres";

    private static string DescribeProblem(LicenseValidationResult result) => result.Status switch
    {
        "revoked" => "Diese Lizenz wurde widerrufen oder diese Hardware ist gesperrt.",
        "suspended" => "Diese Lizenz ist vorübergehend ausgesetzt.",
        "expired" => "Diese Lizenz ist abgelaufen.",
        "activation_limit" => "Das Aktivierungslimit dieser Lizenz ist erreicht. "
                              + "Ein Platz lässt sich im Deploymentcenter in der Hardware-Liste "
                              + "über \"Freigeben\" räumen.",
        // Der Server verwendet not_found für zwei verschiedene Dinge: unbekanntes
        // Projekt und unbekannter Schlüssel. Welches davon, steht nur in message —
        // deshalb wird die Serverantwort hier mitgegeben. Ohne sie sieht ein falsch
        // eingetragener Produkt-Slug wie ein vertippter Lizenzschlüssel aus, und man
        // sucht am falschen Ende.
        "not_found" => "Lizenzschlüssel oder Produkt unbekannt. Im Deploymentcenter muss "
                       + $"ein Projekt mit dem Slug \"{LicenseInfo.ProductSlug}\" angelegt "
                       + "sein und der Schlüssel dort hinterlegt."
                       + (string.IsNullOrWhiteSpace(result.Message)
                           ? ""
                           : $" (Server: {result.Message})"),
        _ => string.IsNullOrWhiteSpace(result.Message)
            ? "Die Lizenz ist nicht gültig."
            : result.Message
    };
}
