using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
using Serilog;

namespace Predictalytics.Hosting;

/// <summary>Single self-assessed health check sent along with a heartbeat.</summary>
public sealed class DcCheck
{
    public DcCheck(bool ok, string? message = null, double? value = null)
    {
        Ok = ok;
        Message = message;
        Value = value;
    }

    [JsonPropertyName("ok")] public bool Ok { get; }
    [JsonPropertyName("message")] public string? Message { get; }
    [JsonPropertyName("value")] public double? Value { get; }
}

/// <summary>What the application reports about itself in one heartbeat.</summary>
public sealed class DcHeartbeatSnapshot
{
    /// <summary>ok | warning | error — "stopped"/"maintenance" are sent by the service itself.</summary>
    public string Status { get; set; } = "ok";

    public string? Message { get; set; }

    /// <summary>A failing check downgrades an "ok" heartbeat to "warning" on the server.</summary>
    public Dictionary<string, DcCheck> Checks { get; } = new();

    /// <summary>Numeric values; the server keeps 14 days of history per metric.</summary>
    public Dictionary<string, double> Metrics { get; } = new();
}

/// <summary>
/// Sends periodic heartbeats to the Deployment Center Watchdog
/// (POST /api/watchdog/v1/ping) so an outage of this app — or of the whole machine —
/// raises an alarm. A Deployment Center outage must never impact the app: every call
/// is best effort.
///
/// The server evaluates by interval: no heartbeat for more than 2× the interval means
/// "warning", more than 4× means "down".
/// </summary>
public sealed class DcHeartbeatService : IDisposable
{
    private readonly DcApiClient _api;
    private readonly string _source;
    private readonly string _instance;
    private readonly int _intervalSeconds;
    private readonly Func<CancellationToken, Task<DcHeartbeatSnapshot>>? _snapshotProvider;
    private readonly DateTime _startedUtc = DateTime.UtcNow;

    // Guards against overlapping sends when a call takes longer than the interval.
    private readonly SemaphoreSlim _sendGate = new(1, 1);

    private System.Threading.Timer? _timer;
    private bool _lastSendFailed;
    private bool _tokenRejected;
    private bool _stoppingNotified;
    private volatile bool _disposed;

    public DcHeartbeatService(
        string token,
        string source,
        string instance,
        int intervalSeconds,
        Func<CancellationToken, Task<DcHeartbeatSnapshot>>? snapshotProvider = null)
    {
        _api = new DcApiClient(token);
        _source = string.IsNullOrWhiteSpace(source) ? "Predictalytics" : source;
        _instance = string.IsNullOrWhiteSpace(instance) ? "default" : instance;
        _intervalSeconds = Math.Max(15, intervalSeconds);
        _snapshotProvider = snapshotProvider;
    }

    public void Start()
    {
        _timer?.Dispose();
        _timer = new System.Threading.Timer(
            async _ => await SendHeartbeatAsync().ConfigureAwait(false),
            null, TimeSpan.Zero, TimeSpan.FromSeconds(_intervalSeconds));
        Log.Information("🐕 Deployment Center Heartbeat gestartet → {Url} (source={Source}, alle {Interval}s)",
            DcConfig.BaseUrl, _source, _intervalSeconds);
    }

    /// <summary>
    /// Announces a planned shutdown as status "stopped". The evaluator leaves such a monitor
    /// alone until a normal heartbeat arrives again — without it, every orderly shutdown
    /// produces a false alarm a few minutes later.
    /// </summary>
    public void NotifyStopping()
    {
        if (_stoppingNotified) return;
        _stoppingNotified = true;

        try
        {
            _timer?.Dispose();
            _timer = null;

            var payload = BuildPayload("stopped", "Predictalytics wird planmäßig beendet.", null, null);
            // Synchronous with a short cap: the form is closing and must not hang.
            _api.PostJsonAsync("/api/watchdog/v1/ping", payload).Wait(TimeSpan.FromSeconds(4));
        }
        catch
        {
            // Best effort only.
        }
    }

    /// <remarks>
    /// Invoked from a timer callback, i.e. as async void — an exception escaping here would
    /// take the whole process down. Nothing in this method may throw.
    /// </remarks>
    private async Task SendHeartbeatAsync()
    {
        if (_disposed || _tokenRejected) return;
        if (!await _sendGate.WaitAsync(0).ConfigureAwait(false)) return;

        try
        {
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(_intervalSeconds));

            DcHeartbeatSnapshot snapshot;
            try
            {
                snapshot = _snapshotProvider is null
                    ? new DcHeartbeatSnapshot()
                    : await _snapshotProvider(cts.Token).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                // Collecting the state must never keep the heartbeat from going out —
                // that would turn a diagnostic hiccup into a false "down".
                snapshot = new DcHeartbeatSnapshot { Status = "warning", Message = $"Statusermittlung fehlgeschlagen: {ex.Message}" };
            }

            snapshot.Metrics["uptime_sec"] = Math.Round((DateTime.UtcNow - _startedUtc).TotalSeconds);

            var payload = BuildPayload(snapshot.Status, snapshot.Message, snapshot.Checks, snapshot.Metrics);
            await _api.PostJsonAsync("/api/watchdog/v1/ping", payload, cts.Token).ConfigureAwait(false);

            if (_lastSendFailed)
            {
                _lastSendFailed = false;
                Log.Information("🐕 Deployment Center Heartbeat wieder erfolgreich zugestellt.");
            }
        }
        catch (DcApiException ex) when (ex.IsPermanent)
        {
            // Retrying cannot help — a rejected token would otherwise log forever.
            _tokenRejected = true;
            Log.Warning("🐕 Deployment Center weist das Token zurück ({Code}) — Heartbeats werden eingestellt. " +
                        "Bitte in den Settings ein Token mit dem Recht 'watchdog:ping' eintragen.", ex.Code ?? "unauthorized");
        }
        catch (Exception ex)
        {
            // Log the first failure as warning, subsequent ones quietly (no log flood).
            if (!_lastSendFailed)
            {
                _lastSendFailed = true;
                Log.Warning("🐕 Deployment Center Heartbeat fehlgeschlagen (weitere Fehler werden unterdrückt): {Error}", ex.Message);
            }
            else
            {
                Log.Debug(ex, "Deployment Center heartbeat failed");
            }
        }
        finally
        {
            _sendGate.Release();
        }
    }

    public void Dispose()
    {
        _disposed = true;
        _timer?.Dispose();
        _timer = null;

        // Give a send in flight a moment to finish before the HttpClient goes away. Called
        // from the UI thread (settings change, form closing), so the wait stays short — if it
        // expires, the pending call just fails and gets logged like any other network error.
        if (_sendGate.Wait(TimeSpan.FromMilliseconds(250)))
        {
            _sendGate.Release();
        }
        _api.Dispose();

        // _sendGate is deliberately not disposed: a late Release() on a disposed semaphore
        // would throw on the timer thread for no gain — SemaphoreSlim without a wait handle
        // holds no unmanaged resources.
    }

    private object BuildPayload(
        string status,
        string? message,
        Dictionary<string, DcCheck>? checks,
        Dictionary<string, double>? metrics) => new
        {
            source = _source,
            instance = _instance,
            type = "heartbeat",
            status,
            interval = _intervalSeconds,
            message,
            group = DcConfig.MonitorGroup,
            os = $"{RuntimeInformation.OSDescription} / .NET {System.Environment.Version}",
            // Since 2.1 the monitor can show which build is running — that is what ties
            // "this monitor went down" to "we rolled out 1.4.3 an hour ago".
            version = DcConfig.AppVersion,
            checks = checks is { Count: > 0 } ? checks : null,
            metrics = metrics is { Count: > 0 } ? metrics : null
        };
}
