using System.Text.Json;

namespace ClawdDotNet.Core.Deploymentcenter.Watchdog;

/// <summary>Was der Server zu einem Heartbeat zurückmeldet.</summary>
/// <param name="State">Der daraus abgeleitete Monitor-Zustand (<c>up</c>, <c>warning</c>, …).</param>
/// <param name="FailingChecks">Welche der mitgeschickten Prüfungen fehlgeschlagen sind.</param>
public sealed record WatchdogPingResult(string State, IReadOnlyList<string> FailingChecks);

/// <summary>
/// Sendet Heartbeats und Ereignisse an das Watchdog-Modul des Deploymentcenters.
/// </summary>
public interface IWatchdogClient
{
    Task<WatchdogPingResult> SendHeartbeatAsync(
        InstanceHealth health, int intervalSeconds, CancellationToken ct);

    Task SendEventAsync(
        string kind, string severity, string? message, object? meta, CancellationToken ct);
}

/// <summary>
/// Watchdog-Anbindung: <c>POST /api/watchdog/v1/ping</c> und
/// <c>POST /api/watchdog/v1/event</c>.
///
/// <para><b>Ein Monitor je Instanz.</b> Der Schlüssel des Monitors ist das Paar
/// <c>source</c> + <c>instance</c> (so das Datenbankschema:
/// <c>UNIQUE KEY uq_monitor (source, instance)</c>). Alle ClawdDotNet-Instanzen melden
/// unter derselben <c>source</c> und tragen ihre eigene <c>instance</c> — damit ist jede
/// laufende Instanz ein eigener Monitor mit eigenem Zustand, eigenem Intervall und
/// eigenem Metrik-Verlauf. Stürzt eine von dreien ab, fällt genau deren Monitor.</para>
///
/// <para>Der Monitor entsteht beim ersten Heartbeat von selbst (<c>INSERT … ON DUPLICATE
/// KEY UPDATE</c>) — eine Registrierung vorab gibt es nicht mehr und ist auch nicht
/// nötig.</para>
/// </summary>
public sealed class WatchdogClient : IWatchdogClient, IDisposable
{
    private readonly DeploymentcenterApi _api;
    private readonly bool _ownsApi;
    private readonly string _source;
    private readonly string _instance;
    private readonly string _group;
    private readonly string _os;
    private readonly string _version;

    public WatchdogClient(
        DeploymentcenterApi api, string source, string instance, string group, string os,
        string version, bool ownsApi = false)
    {
        _api = api;
        _ownsApi = ownsApi;
        _source = source;
        _instance = instance;
        _group = group;
        _os = os;
        _version = version;
    }

    public static WatchdogClient Create(
        string baseUrl, string token, string source, string instance, string group, string os,
        string version, HttpClient? httpClient = null)
        => new(new DeploymentcenterApi(baseUrl, token, httpClient),
               source, instance, group, os, version, ownsApi: true);

    public async Task<WatchdogPingResult> SendHeartbeatAsync(
        InstanceHealth health, int intervalSeconds, CancellationToken ct)
    {
        var payload = new Dictionary<string, object?>
        {
            ["source"] = _source,
            ["instance"] = _instance,
            ["type"] = "heartbeat",
            ["status"] = health.Status,
            ["interval"] = intervalSeconds,
            ["message"] = health.Message,
            ["group"] = _group,
            ["os"] = _os,

            // Landet in watchdog_monitors.app_version. Damit steht im Dashboard, welche
            // Fassung eine Instanz gerade fährt — bei mehreren Instanzen der
            // Unterschied zwischen „läuft" und „läuft noch auf der alten Version".
            ["version"] = _version
        };

        // Leere Objekte weglassen: Der Server übernimmt health_json nur, wenn etwas
        // mitkommt — ein leeres würde den letzten bekannten Zustand nicht ersetzen,
        // aber unnötig Platz im Protokoll kosten.
        if (health.Checks.Count > 0)
        {
            payload["checks"] = health.Checks.ToDictionary(
                c => c.Key,
                c => (object)new { ok = c.Value.Ok, message = c.Value.Message });
        }

        if (health.Metrics.Count > 0)
            payload["metrics"] = health.Metrics;

        var response = await _api.PostAsync("/api/watchdog/v1/ping", payload, ct)
            .ConfigureAwait(false);

        return ReadPingResult(response);
    }

    private static WatchdogPingResult ReadPingResult(JsonElement response)
    {
        if (!response.TryGetProperty("monitor", out var monitor)
            || monitor.ValueKind != JsonValueKind.Object)
        {
            return new WatchdogPingResult("unknown", []);
        }

        var state = monitor.TryGetProperty("state", out var s) && s.ValueKind == JsonValueKind.String
            ? s.GetString() ?? "unknown"
            : "unknown";

        var failing = new List<string>();
        if (monitor.TryGetProperty("failing_checks", out var checks)
            && checks.ValueKind == JsonValueKind.Array)
        {
            failing.AddRange(checks.EnumerateArray()
                .Where(e => e.ValueKind == JsonValueKind.String)
                .Select(e => e.GetString()!));
        }

        return new WatchdogPingResult(state, failing);
    }

    /// <summary>
    /// Ein einmaliges Vorkommnis statt einer zyklischen Meldung. Zulässige
    /// <paramref name="kind"/>-Werte siehe <see cref="WatchdogEventKind"/> — der Server
    /// weist andere ab.
    /// </summary>
    public async Task SendEventAsync(
        string kind, string severity, string? message, object? meta, CancellationToken ct)
    {
        var payload = new
        {
            source = _source,
            instance = _instance,
            kind,
            severity,
            message,
            meta
        };

        await _api.PostAsync("/api/watchdog/v1/event", payload, ct).ConfigureAwait(false);
    }

    public void Dispose()
    {
        if (_ownsApi)
            _api.Dispose();
    }
}

/// <summary>
/// Die vom Server akzeptierten Ereignisarten. Die frühere Anbindung schickte
/// <c>start</c> und <c>stop</c> — beide stehen nicht auf dieser Liste und wurden
/// stillschweigend als <c>started</c> abgelegt.
/// </summary>
public static class WatchdogEventKind
{
    public const string Started = "started";
    public const string StoppedGraceful = "stopped_graceful";
    public const string CrashSuspected = "crash_suspected";
    public const string HardError = "hard_error";
    public const string Recovered = "recovered";
    public const string WarningRaised = "warning_raised";
    public const string WarningCleared = "warning_cleared";
    public const string MaintenanceStart = "maintenance_start";
    public const string MaintenanceEnd = "maintenance_end";
    public const string WatchdogStarted = "watchdog_started";
}
