using ClawdDotNet.App.Settings;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Deploymentcenter;
using ClawdDotNet.Core.Deploymentcenter.Watchdog;
using Microsoft.Extensions.Logging;

namespace ClawdDotNet.App.Services;

/// <summary>
/// Die Anbindung ans Deploymentcenter, an einer Stelle gebündelt: Watchdog-Heartbeat,
/// Fehler-Stream, Bugtracker und Update-Prüfung.
///
/// <para>Zuvor lagen Watchdog (eigener Server, eigener Schlüssel) und Lizenz
/// (LicenseLabrador, eigener Server, eigener Public-Key) getrennt nebeneinander. Beides
/// sind jetzt Module derselben Anwendung mit einer Adresse und einem Token — und dazu
/// kommen Updates, Fehler-Stream und Bugtracker, die es vorher gar nicht gab.</para>
///
/// <para>Die Lizenz bleibt bewusst außen vor: Sie hat ein eigenes Antwortformat (kein
/// <c>status</c>/<c>error</c>-Umschlag), einen eigenen Zwischenspeicher und muss vor
/// allem anderen laufen. Dafür ist <see cref="LicenseGate"/> zuständig.</para>
/// </summary>
public sealed class DeploymentcenterService : IAsyncDisposable
{
    private readonly DeploymentcenterApi _api;
    private readonly string _appToken;
    private readonly string _build;
    private readonly ILogger _logger;

    private WatchdogHeartbeatService? _heartbeat;

    public IErrorReporter Errors { get; private set; } = NullErrorReporter.Instance;

    /// <summary>Null, wenn kein Token hinterlegt ist — dann lässt sich nichts melden.</summary>
    public BugtrackerClient? Bugtracker { get; private set; }

    /// <summary>Ergebnis der Update-Prüfung beim Start; null, solange sie nicht durch ist.</summary>
    public UpdateAvailability? Update { get; private set; }

    private DeploymentcenterService(
        DeploymentcenterApi api, string appToken, string build, ILogger logger)
    {
        _api = api;
        _appToken = appToken;
        _build = build;
        _logger = logger;
    }

    /// <summary>
    /// Baut die Anbindung auf, soweit sie konfiguriert ist. Gibt <c>null</c> zurück,
    /// wenn Adresse oder Token fehlen — der Aufrufer läuft dann ohne weiter, denn keines
    /// dieser Module darf ein Startgrund oder ein Hindernis sein.
    /// </summary>
    public static DeploymentcenterService? TryCreate(
        AppSettings settings, string build, ILoggerFactory loggerFactory)
    {
        var logger = loggerFactory.CreateLogger("ClawdDotNet.Deploymentcenter");

        if (string.IsNullOrWhiteSpace(settings.DeploymentcenterUrl))
        {
            logger.LogInformation("Deploymentcenter: keine Server-URL hinterlegt – Anbindung aus.");
            return null;
        }

        if (string.IsNullOrWhiteSpace(settings.DeploymentcenterToken))
        {
            logger.LogInformation(
                "Deploymentcenter: kein Token hinterlegt – Heartbeat, Fehler-Stream und "
                + "Bugtracker bleiben aus.");
            return null;
        }

        DeploymentcenterApi api;
        try
        {
            api = new DeploymentcenterApi(settings.DeploymentcenterUrl, settings.DeploymentcenterToken);
        }
        catch (ArgumentException ex)
        {
            logger.LogWarning(ex, "Deploymentcenter: Konfiguration unbrauchbar – Anbindung aus.");
            return null;
        }

        var service = new DeploymentcenterService(
            api, settings.DeploymentcenterToken, build, logger);

        if (settings.ErrorReportingEnabled)
        {
            service.Errors = new ErrorReporter(
                api, LicenseInfo.ProductSlug, settings.DeploymentcenterEnvironment, build, logger);
        }

        service.Bugtracker = new BugtrackerClient(
            api, LicenseInfo.ProductSlug, settings.DeploymentcenterEnvironment, build);

        return service;
    }

    // ─── Watchdog ───

    /// <summary>
    /// Startet den Instanz-Heartbeat, wenn der eingebaute Dienst eingeschaltet ist.
    ///
    /// <para>Jede laufende Instanz ist ein eigener Monitor: Der Server führt sie über
    /// das Paar <c>source</c> + <c>instance</c>. Fällt eine von mehreren aus, fällt
    /// genau deren Monitor — und nur der schlägt Alarm.</para>
    /// </summary>
    /// <param name="saveInstanceConfig">
    /// Wird aufgerufen, wenn ein neu bezogenes Sub-Token in die Instanzkonfiguration
    /// geschrieben werden soll (dort verschlüsselt).
    /// </param>
    public async Task StartWatchdogAsync(
        InstanceConfig instance,
        IInstanceHealthProvider health,
        Action saveInstanceConfig,
        CancellationToken ct = default)
    {
        var service = instance.Services.FirstOrDefault(s => s.Type == BuiltInServices.InstanceWatchdog);
        if (service is not { Enabled: true, AutoStart: true })
            return;

        var watchdog = instance.Watchdog;
        var token = await ResolveInstanceTokenAsync(instance, saveInstanceConfig, ct);

        try
        {
            _heartbeat = WatchdogHeartbeatService.Create(
                _api.BaseUrl,
                token,
                watchdog.Source,
                watchdog.ResolveInstance(instance.InstanceId),
                watchdog.Group,
                DescribePlatform(),
                _build,
                watchdog.IntervalSeconds,
                health,
                _logger);

            _heartbeat.Start();

            _logger.LogInformation("Instanz-Watchdog aktiv: {Source}/{Instance}, alle {Interval}s",
                watchdog.Source, watchdog.ResolveInstance(instance.InstanceId), watchdog.IntervalSeconds);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Instanz-Watchdog konnte nicht gestartet werden");
        }
    }

    /// <summary>
    /// Liefert das Token, mit dem diese Instanz meldet: das zwischengespeicherte
    /// Sub-Token, sonst ein frisch gezogenes, sonst das anwendungsweite.
    ///
    /// <para>Der Umweg lohnt sich, weil danach auf der Instanz nicht mehr das
    /// Master-Token liegt, sondern ein auf <c>watchdog:ping</c> und
    /// <c>bugtracker:report</c> beschränktes, das sich einzeln widerrufen lässt.
    /// Scheitert das, wird trotzdem gemeldet — Monitoring, das nur bei perfekter
    /// Rechtelage läuft, ist genau dann still, wenn man es braucht.</para>
    /// </summary>
    private async Task<string> ResolveInstanceTokenAsync(
        InstanceConfig instance, Action saveInstanceConfig, CancellationToken ct)
    {
        if (instance.Watchdog.HasToken)
            return instance.Watchdog.AgentToken;

        try
        {
            var provisioned = await new TokenProvisioner(_api).ProvisionAsync(
                clientName: $"ClawdDotNet {instance.InstanceName}",
                instanceId: instance.InstanceId,
                scopes: TokenProvisioner.InstanceScopes,
                ct: ct);

            instance.Watchdog.AgentToken = provisioned.Token;
            saveInstanceConfig();

            _logger.LogInformation(
                "Deploymentcenter: eigenes Token für diese Instanz bezogen ({TokenId}, Rechte: {Scopes})",
                provisioned.TokenId, string.Join(", ", provisioned.Scopes));

            return provisioned.Token;
        }
        catch (Exception ex)
        {
            // Häufigster Fall: Das hinterlegte Token ist selbst ein Sub-Token und darf
            // keine weiteren ausstellen. Kein Grund, das Monitoring aufzugeben.
            _logger.LogInformation(
                "Deploymentcenter: kein eigenes Instanz-Token beziehbar ({Reason}) – "
                + "es wird mit dem hinterlegten Token gemeldet.", ex.Message);

            return _appToken;
        }
    }

    private static string DescribePlatform() =>
        $"{System.Runtime.InteropServices.RuntimeInformation.OSDescription} / "
        + $".NET {System.Environment.Version}";

    // ─── Updates ───

    /// <summary>
    /// Fragt einmalig, ob ein neueres Release vorliegt. Bewusst ohne Folgen: Das
    /// Ergebnis wird protokolliert und über <see cref="Update"/> bereitgestellt; ob und
    /// wann aktualisiert wird, entscheidet der Benutzer.
    /// </summary>
    public async Task CheckForUpdateAsync(AppSettings settings, string currentVersion, CancellationToken ct = default)
    {
        if (!settings.UpdateCheckEnabled)
            return;

        try
        {
            var client = new global::Deploymentcenter.Client.UpdateClient();

            // Seit 2.4 liegt die Release-Ablage hinter HTTP-Basic-Auth; der Lizenzschlüssel
            // ist der Zugang. Die Anwendung kennt ihn ohnehin, es muss nichts zusätzlich
            // verteilt werden. Ohne ihn kommt ab dem Scharfschalten nur noch 401.
            //
            // platform bleibt offen: Das SDK setzt die Kennung des laufenden Systems ein.
            // Sie wegzulassen wäre keine Option — der Server liefert dann ausschließlich
            // plattformunabhängige Pakete.
            var result = await client.CheckForUpdateAsync(
                settings.DeploymentcenterUrl, LicenseInfo.ProductSlug, currentVersion,
                settings.UpdateChannel,
                credentials: global::Deploymentcenter.Client.ReleaseCredentials
                    .FromLicenseKey(settings.LicenseKey),
                cancellationToken: ct);

            // Ein 401 ist kein Netzwerkfehler, sondern eine Aussage über die Lizenz.
            // Ohne die Unterscheidung sucht man bei einem abgelaufenen Vertrag am Netz.
            if (result.Unauthorized)
            {
                _logger.LogWarning(
                    "Update-Prüfung abgewiesen: {Message} – der Lizenzschlüssel öffnet die "
                    + "Release-Ablage nicht (mehr).", result.Message);
                return;
            }

            if (result.Error is not null)
            {
                _logger.LogDebug(result.Error, "Update-Prüfung fehlgeschlagen (ignoriert).");
                return;
            }

            // Seit SDK 2.1 liefern beide Wege vollständige Daten: die statische
            // latest.json in camelCase, die API in snake_case, jeweils über ein eigenes
            // Modell. Vorher kam über den API-Zweig außer der Versionsnummer nichts an —
            // und der ist genau der Rückfall, wenn die latest.json fehlt.
            Update = new UpdateAvailability(
                result.UpdateAvailable,
                result.LatestRelease?.Version ?? currentVersion,
                result.IsCritical,
                result.LatestRelease?.Changelog,
                result.LatestRelease?.PackageUrl);

            if (result.UpdateAvailable)
            {
                _logger.LogInformation("Update verfügbar: {Version}{Critical}",
                    Update.LatestVersion, Update.IsCritical ? " (kritisch)" : "");
            }
            else
            {
                _logger.LogInformation("Kein Update verfügbar (installiert: {Version}).", currentVersion);
            }
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Update-Prüfung fehlgeschlagen (ignoriert).");
        }
    }

    /// <summary>
    /// Meldet dem Watchdog, dass das Herunterfahren gleich ein Update ist und kein
    /// Ausfall. Muss <b>vor</b> dem Herunterfahren gerufen werden — die Meldung geht
    /// beim Beenden des Heartbeats raus.
    /// </summary>
    public void AnnounceUpdate(string version)
        => _heartbeat?.AnnounceMaintenance($"Update auf {version} wird eingespielt.");

    /// <summary>
    /// Startet den <c>update-agent</c> und übergibt ihm das Feld.
    ///
    /// <para><b>Beendet die Anwendung bewusst nicht selbst.</b> <c>LaunchUpdateAgent</c>
    /// kann das (<c>exitCurrentApp</c>), täte es aber über
    /// <see cref="Environment.Exit"/> — und das übergeht jedes Aufräumen: Der Watchdog
    /// meldete nichts, Instanzdatenbank und Chatverläufe blieben auf halbem Weg stehen.
    /// Der Aufrufer fährt deshalb erst geordnet herunter und beendet danach selbst.
    /// Der Agent bekommt unsere Prozesskennung und wartet, bis wir wirklich weg
    /// sind.</para>
    /// </summary>
    /// <returns>Der Grund, wenn es nicht losging; sonst <c>null</c>.</returns>
    public string? StartUpdate(AppSettings settings, string currentVersion)
    {
        var agentPath = global::Deploymentcenter.Client.UpdateClient.ResolveAgentPath();

        if (agentPath is null)
        {
            // Kein fest verdrahteter Name: Unter Linux trägt der Agent keine Endung.
            _logger.LogWarning(
                "Update angefordert, aber kein update-agent neben der Anwendung gefunden.");

            return "Der update-agent wurde nicht gefunden. Er gehört neben die "
                   + "Programmdatei; das Installationspaket bringt ihn normalerweise mit.";
        }

        try
        {
            var started = global::Deploymentcenter.Client.UpdateClient.LaunchUpdateAgent(
                agentPath: agentPath,
                projectId: LicenseInfo.ProductSlug,
                channel: settings.UpdateChannel,
                action: "update",
                version: "latest",
                exitCurrentApp: false,
                currentVersion: currentVersion,
                waitForCurrentProcess: true,
                licenseKey: settings.LicenseKey,

                // Ohne Signatur nicht installieren. Der SHA256 stammt aus derselben
                // Quelle wie das Paket — wer den Webroot oder die FTP-Zugangsdaten
                // kontrolliert, tauscht beide gemeinsam aus. Ausgerechnet auf dem Pfad,
                // der anschließend fremden Code ausführt.
                //
                // Tragfähig, weil ab 0.1.2 jedes Release signiert ist. Ein Rücksprung
                // auf 0.1.0 oder 0.1.1 bricht damit ab — das ist gewollt: Die beiden
                // stammen aus der Zeit vor dem Signierschlüssel.
                requireSignature: true);

            if (!started)
                return "Der update-agent ließ sich nicht starten.";

            _logger.LogInformation("update-agent gestartet ({Path}), Anwendung wird beendet.",
                agentPath);

            return null;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "update-agent konnte nicht gestartet werden.");
            return $"Der update-agent konnte nicht gestartet werden: {ex.Message}";
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (_heartbeat is not null)
            await _heartbeat.DisposeAsync();

        if (Errors is IDisposable disposableReporter)
            disposableReporter.Dispose();

        _api.Dispose();
    }
}

/// <summary>Ergebnis der Update-Prüfung.</summary>
/// <param name="DownloadUrl">
/// Adresse des Pakets — für einen späteren Anschluss des <c>update-agent</c>. Solange
/// der nicht eingebunden ist, dient sie nur der Anzeige.
/// </param>
public sealed record UpdateAvailability(
    bool IsAvailable, string LatestVersion, bool IsCritical, string? ReleaseNotes,
    string? DownloadUrl = null);
