using Microsoft.Extensions.Logging;

namespace ClawdDotNet.Core.Deploymentcenter.Watchdog;

/// <summary>
/// Sendet im festen Takt Heartbeats an das Watchdog-Modul und meldet Start und Ende.
/// Ein nicht erreichbares Deploymentcenter darf ClawdDotNet nie beeinträchtigen —
/// alle Sendefehler werden geloggt und verschluckt.
///
/// <para><b>Sauberes Beenden.</b> Beim Herunterfahren geht ein Heartbeat mit
/// <c>status: "stopped"</c> raus. Der Evaluator lässt einen so gemeldeten Monitor in
/// Ruhe; ohne das erzeugte jedes geplante Beenden wenige Minuten später einen
/// Fehlalarm. Das reine Ereignis genügt dafür nicht — der Evaluator sieht nur den
/// Monitor-Zustand.</para>
/// </summary>
public sealed class WatchdogHeartbeatService : IAsyncDisposable
{
    private readonly IWatchdogClient _client;
    private readonly bool _ownsClient;
    private readonly IInstanceHealthProvider _health;
    private readonly int _intervalSeconds;
    private readonly ILogger _logger;

    private CancellationTokenSource? _cts;
    private Task? _loop;
    private string _lastState = "unknown";

    private string _shutdownStatus = WatchdogStatus.Stopped;
    private string _shutdownMessage = "Instanz planmäßig beendet.";
    private string _shutdownEventKind = WatchdogEventKind.StoppedGraceful;

    public WatchdogHeartbeatService(
        IWatchdogClient client,
        IInstanceHealthProvider health,
        int intervalSeconds,
        ILogger logger,
        bool ownsClient = false)
    {
        _client = client;
        _health = health;
        _intervalSeconds = Math.Clamp(intervalSeconds, 10, 86400);
        _logger = logger;
        _ownsClient = ownsClient;
    }

    /// <summary>
    /// Baut Client und Dienst in einem Zug. Wirft nur bei grob falscher Konfiguration
    /// (fehlende oder nicht-HTTPS-URL).
    /// </summary>
    public static WatchdogHeartbeatService Create(
        string baseUrl, string token, string source, string instance, string group, string os,
        string version, int intervalSeconds, IInstanceHealthProvider health, ILogger logger)
    {
        var client = WatchdogClient.Create(baseUrl, token, source, instance, group, os, version);
        return new WatchdogHeartbeatService(
            client, health, intervalSeconds, logger, ownsClient: true);
    }

    public bool IsRunning => _loop is { IsCompleted: false };

    /// <summary>Der zuletzt vom Server gemeldete Monitor-Zustand — für die Anzeige.</summary>
    public string LastState => _lastState;

    /// <summary>
    /// Meldet das nächste Herunterfahren als <b>Wartung</b> statt als planmäßiges Ende.
    ///
    /// <para>Gedacht für das Einspielen eines Updates: Die Instanz ist gleich weg,
    /// kommt aber wieder. <c>stopped</c> wäre die falsche Auskunft — es heißt „bewusst
    /// beendet" und lässt den Monitor liegen, bis jemand ihn wieder anfasst.
    /// <c>maintenance</c> sagt dasselbe über den Alarm aus, trägt aber die Absicht
    /// mit: Im Dashboard ist zu sehen, dass hier gerade aktualisiert wird, statt dass
    /// eine Instanz ohne Grund verschwindet.</para>
    /// </summary>
    public void AnnounceMaintenance(string message)
    {
        _shutdownStatus = WatchdogStatus.Maintenance;
        _shutdownMessage = message;
        _shutdownEventKind = WatchdogEventKind.MaintenanceStart;
    }

    public void Start()
    {
        if (IsRunning)
            return;

        _cts = new CancellationTokenSource();
        _loop = RunAsync(_cts.Token);
        _logger.LogInformation("Watchdog-Heartbeat gestartet (alle {Interval}s).", _intervalSeconds);
    }

    private async Task RunAsync(CancellationToken ct)
    {
        await TrySendAsync(
            () => _client.SendEventAsync(
                WatchdogEventKind.Started, "info", "Instanz gestartet.", null, ct),
            "Start-Ereignis").ConfigureAwait(false);

        // Erster Beat sofort, damit ein neuer Monitor nicht erst nach einem vollen
        // Intervall im Dashboard auftaucht.
        await BeatAsync(ct).ConfigureAwait(false);

        try
        {
            using var timer = new PeriodicTimer(TimeSpan.FromSeconds(_intervalSeconds));
            while (await timer.WaitForNextTickAsync(ct).ConfigureAwait(false))
                await BeatAsync(ct).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            // Regulärer Stopp.
        }
    }

    private async Task BeatAsync(CancellationToken ct)
    {
        InstanceHealth health;
        try
        {
            health = await _health.GetAsync(ct).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception ex)
        {
            // Selbst wenn die Zustandsermittlung scheitert, soll ein Lebenszeichen
            // rausgehen — sonst sieht ein Fehler in unserem Code aus wie ein Ausfall.
            _logger.LogWarning(ex, "Watchdog: Zustandsermittlung fehlgeschlagen – melde warning.");
            health = new InstanceHealth(
                WatchdogStatus.Warning, "Zustand konnte nicht ermittelt werden.",
                new Dictionary<string, double>(), new Dictionary<string, HealthCheck>());
        }

        await TrySendAsync(async () =>
        {
            var result = await _client.SendHeartbeatAsync(health, _intervalSeconds, ct)
                .ConfigureAwait(false);

            if (result.State != _lastState)
            {
                _logger.LogInformation("Watchdog: Monitor-Zustand {Previous} → {State}{Failing}",
                    _lastState, result.State,
                    result.FailingChecks.Count > 0
                        ? $" (fehlgeschlagen: {string.Join(", ", result.FailingChecks)})"
                        : "");

                _lastState = result.State;
            }
        }, "Heartbeat").ConfigureAwait(false);
    }

    private async Task TrySendAsync(Func<Task> send, string what)
    {
        try
        {
            await send().ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (DeploymentcenterException ex) when (ex.IsAuthorizationProblem)
        {
            // Ein abgelehntes Token ist kein Rauschen: Ohne Eingriff bleibt der Monitor
            // für immer stumm, und niemand merkt es, weil ja nichts abstürzt.
            _logger.LogWarning(
                "Watchdog: {What} abgelehnt ({Code}) – Token prüfen (Recht watchdog:ping).",
                what, ex.Code);
        }
        catch (Exception ex)
        {
            // Ausfall des Monitorings darf den Betrieb nie stören.
            _logger.LogDebug(ex, "Watchdog: {What} konnte nicht gesendet werden (ignoriert).", what);
        }
    }

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

        await _cts.CancelAsync().ConfigureAwait(false);

        if (_loop is not null)
        {
            try { await _loop.ConfigureAwait(false); }
            catch (OperationCanceledException) { /* erwartet */ }
            catch (Exception ex) { _logger.LogDebug(ex, "Watchdog: Heartbeat-Schleife endete mit Fehler."); }
        }

        // Angekündigtes Ende, mit kurzer Frist. Hier wird alles geschluckt (auch ein
        // Zeitüberlauf), damit das Herunterfahren nie hängt oder wirft.
        try
        {
            using var stopCts = new CancellationTokenSource(TimeSpan.FromSeconds(3));

            await _client.SendHeartbeatAsync(
                new InstanceHealth(
                    _shutdownStatus, _shutdownMessage,
                    new Dictionary<string, double>(), new Dictionary<string, HealthCheck>()),
                _intervalSeconds, stopCts.Token).ConfigureAwait(false);

            await _client.SendEventAsync(
                _shutdownEventKind, "info", _shutdownMessage, null, stopCts.Token)
                .ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Watchdog: Ende konnte nicht gemeldet werden (ignoriert).");
        }

        _cts.Dispose();

        if (_ownsClient && _client is IDisposable disposable)
            disposable.Dispose();
    }
}
