using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using PolyTrader.Core;
using Microsoft.Extensions.Hosting;
using PolyTrader.Core.Security;
using PolyTraderSharp.Models;

namespace PolyTraderSharp.Services
{
    /// <summary>Ergebnis eines Sendeversuchs – für die UI-Rückmeldung des Test-Buttons.</summary>
    public sealed record WatchdogSendResult(bool Success, string Detail);

    /// <summary>
    /// Sendet im festen Takt einen Heartbeat an den Watchdog des Deploymentcenters
    /// (Dead-Man's-Switch). Bleiben die Heartbeats aus, alarmiert der Watchdog – er läuft bewusst
    /// auf einer anderen Maschine und bemerkt damit auch einen harten Server-Crash.
    ///
    /// **Umstellung 14.08.2026:** Ersetzt den eigenständigen Watchdog-Dienst (watchdog.mhdf.de) und
    /// LicenseLabrador durch das Deploymentcenter (siehe
    /// docs/umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md, Schnitt D-1). Endpunkte
    /// sind jetzt <c>/api/watchdog/v1/ping</c> und <c>/api/watchdog/v1/event</c>, Auth-Header
    /// <c>Authorization: Bearer</c> statt <c>X-Watchdog-Key</c>. <see cref="ServerSettings.WatchdogUrl"/>
    /// und <see cref="ServerSettings.WatchdogToken"/> zeigen ab jetzt auf das Deploymentcenter statt
    /// auf den alten Einzeldienst – UI, Maskierung und Live-Reload bleiben unverändert, es ist
    /// dieselbe erprobte Mechanik (21+ Tests), nur mit neuem Gegenüber.
    ///
    /// **Grundregel:** Ein Ausfall des Watchdogs darf PolyTrader NIEMALS beeinträchtigen. Deshalb
    /// ist jeder Sendeversuch vollständig gekapselt; aus <see cref="ExecuteAsync"/> darf keine
    /// Ausnahme entweichen (ein werfender BackgroundService würde den Host beenden).
    ///
    /// Bewusst eine eigene Implementierung statt des mitgelieferten Deploymentcenter-Testclients:
    /// dieser deaktiviert die TLS-Zertifikatsprüfung pauschal. Hier gilt die Standardprüfung, und
    /// <c>http://</c> ist nur für localhost erlaubt, damit der Agent-Token nicht im Klartext
    /// über das Netz geht.
    /// </summary>
    public class WatchdogHeartbeatService : BackgroundService
    {
        /// <summary>Untergrenze des Sende-Takts – schützt den Watchdog vor einem Client-Flood.</summary>
        private const int MinIntervalSeconds = 10;

        /// <summary>
        /// Takt, in dem die Konfiguration neu ausgewertet wird. Klein genug, dass Änderungen aus
        /// dem Settings-Fenster praktisch sofort greifen, und ohne messbare Last.
        /// </summary>
        private static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(1);

        /// <summary>
        /// Nachwirkzeit eines Fehlers: Nach einem <see cref="LogLevel.Error"/> meldet der Heartbeat
        /// so lange <c>status=error</c>, bis dieses Fenster ohne neuen Fehler verstrichen ist. Das
        /// entprellt einzelne Fehler (kein Flackern error→ok→error) und meldet die Erholung genau
        /// einmal, sobald wieder Ruhe ist.
        /// </summary>
        private static readonly TimeSpan ErrorStickyWindow = TimeSpan.FromMinutes(5);

        private const string DefaultSettingsPath = "server_settings.xml";

        private readonly TerminalLogger _logger;
        private readonly HttpClient _http;

        /// <summary>Quelle der Einstellungen – in Tests ersetzbar, im Betrieb die gitignorierte XML-Datei.</summary>
        private readonly Func<ServerSettings> _settingsLoader;

        private ServerSettings _settings;

        /// <summary>Zustand der letzten Übertragung – für entprosseltes Logging (kein Log-Spam im Minutentakt).</summary>
        private bool _lastSendFailed;
        private int _consecutiveFailures;

        // ===== B.2: Fehlerstatus aus dem Log =====
        private readonly object _statusLock = new();
        private DateTime _lastAppErrorUtc = DateTime.MinValue;
        private string? _lastAppErrorMessage;
        private bool _startedEventSent;

        /// <summary>
        /// D-1: Selbst ermittelte Gesundheitsprüfungen (z.B. VPN verbunden, DB erreichbar), die der
        /// Deploymentcenter-Watchdog unter "checks" auswertet – ein als "ok" gemeldeter Heartbeat mit
        /// fehlschlagender Prüfung wird dort automatisch auf "warning" herabgestuft. Der Service
        /// interpretiert die Namen nicht selbst, er sammelt nur ein. Composition Root (Program.cs)
        /// registriert die konkreten Prüfungen.
        /// </summary>
        private readonly Dictionary<string, Func<(bool ok, string? message)>> _checks = new();
        private readonly object _checksLock = new();

        public WatchdogHeartbeatService(
            TerminalLogger logger,
            HttpClient? httpClient = null,
            Func<ServerSettings>? settingsLoader = null)
        {
            _logger = logger;
            _settingsLoader = settingsLoader ?? (() => ServerSettings.Load(DefaultSettingsPath));
            _settings = _settingsLoader();
            _http = httpClient ?? new HttpClient(new SocketsHttpHandler
            {
                PooledConnectionLifetime = TimeSpan.FromMinutes(2)
            })
            {
                Timeout = TimeSpan.FromSeconds(10)
            };

            // B.2: schwere Fehler aus dem App-Log mitschneiden, um sie im Heartbeat zu spiegeln.
            _logger.OnLogMessage += OnAppLog;
        }

        /// <summary>
        /// Schneidet <see cref="LogLevel.Error"/>-Meldungen mit, damit der nächste Heartbeat
        /// <c>status=error</c> melden kann. Läuft synchron auf dem loggenden Thread – daher bewusst
        /// minimal (nur Zeitstempel + gekürzte, entschärfte Meldung merken).
        /// </summary>
        private void OnAppLog(object? sender, LogMessageEventArgs e)
        {
            if (e.Level != LogLevel.Error) return;
            lock (_statusLock)
            {
                _lastAppErrorUtc = DateTime.UtcNow;
                _lastAppErrorMessage = Sanitize(e.Message);
            }
        }

        /// <summary>Lädt die Einstellungen neu (nach dem Speichern im Settings-Fenster).</summary>
        public void ReloadSettings() => _settings = _settingsLoader();

        /// <summary>
        /// D-1: Registriert eine benannte Gesundheitsprüfung, die bei jedem Heartbeat ausgewertet
        /// wird. Eine zweite Registrierung unter demselben Namen ersetzt die vorherige. Der Aufruf
        /// selbst darf nicht werfen – ein Fehler in einer Prüfung führt sonst nur dazu, dass diese
        /// eine Prüfung als "nicht ok" gemeldet wird, nie dazu, dass der Heartbeat ausfällt.
        /// </summary>
        public void RegisterCheck(string name, Func<(bool ok, string? message)> check)
        {
            lock (_checksLock) { _checks[name] = check; }
        }

        /// <summary>Entfernt eine zuvor registrierte Prüfung (z.B. wenn ein Dienst abgeschaltet wird).</summary>
        public void UnregisterCheck(string name)
        {
            lock (_checksLock) { _checks.Remove(name); }
        }

        /// <summary>Wertet alle registrierten Prüfungen aus. Liefert <c>null</c>, wenn keine registriert sind.</summary>
        private Dictionary<string, object>? EvaluateChecks()
        {
            Dictionary<string, Func<(bool ok, string? message)>> snapshot;
            lock (_checksLock)
            {
                if (_checks.Count == 0) return null;
                snapshot = new Dictionary<string, Func<(bool ok, string? message)>>(_checks);
            }

            var result = new Dictionary<string, object>();
            foreach (var (name, check) in snapshot)
            {
                try
                {
                    var (ok, message) = check();
                    result[name] = message == null
                        ? new { ok }
                        : new { ok, message = Sanitize(message) };
                }
                catch (Exception ex)
                {
                    // Eine werfende Prüfung darf den Heartbeat nicht zum Absturz bringen – sie
                    // meldet sich selbst als fehlgeschlagen.
                    result[name] = new { ok = false, message = $"Prüfung fehlgeschlagen: {ex.Message}" };
                }
            }
            return result;
        }

        /// <summary>
        /// Prüft die aktuelle Konfiguration. <c>null</c> = sendebereit, sonst ein kurzer,
        /// anzeigbarer Grund. Wird auch vom Test-Button der Settings-View genutzt.
        /// </summary>
        public string? GetConfigurationProblem()
        {
            if (string.IsNullOrWhiteSpace(_settings.WatchdogUrl))
                return "Keine Watchdog-URL konfiguriert.";
            if (string.IsNullOrWhiteSpace(_settings.WatchdogToken))
                return "Kein Agent-Token konfiguriert.";
            if (string.IsNullOrWhiteSpace(_settings.WatchdogSource))
                return "Keine Source konfiguriert.";
            if (!TryBuildEndpoint(_settings.WatchdogUrl, PingPath, out _, out string urlProblem))
                return urlProblem;
            return null;
        }

        /// <summary>
        /// Haupt-Schleife. Bewusst kein <see cref="PeriodicTimer"/> mit fest verdrahtetem Takt:
        /// die Konfiguration wird bei jedem Durchlauf neu ausgewertet, damit Ein-/Ausschalten,
        /// Takt-, URL- und Token-Änderungen aus dem Settings-Fenster **ohne Neustart** greifen.
        /// Der Sendezeitpunkt wird über eine Fälligkeit gesteuert, der Takt bleibt also exakt.
        /// </summary>
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            DateTime nextDueUtc = DateTime.MinValue;    // sofort fällig
            bool wasActive = false;
            string? loggedProblem = null;

            try
            {
                while (!stoppingToken.IsCancellationRequested)
                {
                    if (!_settings.WatchdogEnabled)
                    {
                        if (wasActive)
                        {
                            _logger.Info("Watchdog-Heartbeat deaktiviert – es werden keine Heartbeats mehr gesendet.");
                            wasActive = false;
                        }
                        loggedProblem = null;
                        await Task.Delay(PollInterval, stoppingToken).ConfigureAwait(false);
                        continue;
                    }

                    string? problem = GetConfigurationProblem();
                    if (problem != null)
                    {
                        // Fehlkonfiguration ist ein Betriebsfehler, kein Grund die App zu behindern.
                        // Jede Ursache wird genau einmal gemeldet, nicht bei jedem Durchlauf.
                        if (loggedProblem != problem)
                        {
                            _logger.Warning($"⚠️ Watchdog-Heartbeat aktiviert, aber nicht sendebereit: {problem} " +
                                            "Es werden KEINE Heartbeats gesendet – der Watchdog wird PolyTrader als 'down' melden.");
                            loggedProblem = problem;
                        }
                        wasActive = false;
                        await Task.Delay(PollInterval, stoppingToken).ConfigureAwait(false);
                        continue;
                    }

                    if (!wasActive)
                    {
                        _logger.Info($"Watchdog-Heartbeat aktiv: sendet alle {EffectiveIntervalSeconds()}s an " +
                                     $"{_settings.WatchdogUrl} (source={_settings.WatchdogSource}, instance={_settings.WatchdogInstance}).");
                        wasActive = true;
                        loggedProblem = null;
                        nextDueUtc = DateTime.MinValue;   // nach dem Aktivieren sofort senden

                        // B.3: Lifecycle – „started" genau einmal senden (der Watchdog protokolliert
                        // den Programmstart; danach übernimmt die Staleness-Erkennung).
                        if (!_startedEventSent)
                        {
                            await SendEventAsync("started", "info", $"PolyTrader gestartet (v{BuildInfo.Version})", stoppingToken)
                                .ConfigureAwait(false);
                            _startedEventSent = true;
                        }
                    }

                    if (DateTime.UtcNow >= nextDueUtc)
                    {
                        // B.2: aktuellen Gesundheitsstatus aus dem Log ableiten (ok/error).
                        (string status, string? message) = DeriveStatus();
                        await SendAndTrackAsync(status, message, stoppingToken).ConfigureAwait(false);
                        nextDueUtc = DateTime.UtcNow.AddSeconds(EffectiveIntervalSeconds());
                    }

                    await Task.Delay(PollInterval, stoppingToken).ConfigureAwait(false);
                }
            }
            catch (OperationCanceledException)
            {
                // Normales Herunterfahren.
            }
            catch (Exception ex)
            {
                // Letztes Sicherheitsnetz: der Heartbeat darf den Host nicht mitnehmen.
                _logger.Error($"❌ Watchdog-Heartbeat unerwartet beendet: {ex.Message}. " +
                              "PolyTrader läuft weiter, wird vom Watchdog aber als 'down' gemeldet.");
            }
        }

        /// <summary>
        /// B.3/D-1: Meldet dem Watchdog beim Herunterfahren ein geordnetes Ende. Zwei Wege, weil sie
        /// unterschiedliche Dinge tun: der Heartbeat mit <c>status:"stopped"</c> ist das, was der
        /// Evaluator auswertet (er lässt einen "stopped"-Monitor in Ruhe, statt ihn nach dem
        /// Ausbleiben weiterer Heartbeats als "down" zu alarmieren); das Event "stopped_graceful"
        /// landet zusätzlich im Ereignisprotokoll. Best effort mit kurzem Timeout – blockiert das
        /// Beenden nicht nennenswert.
        /// </summary>
        public override async Task StopAsync(CancellationToken cancellationToken)
        {
            if (_startedEventSent && _settings.WatchdogEnabled)
            {
                try
                {
                    // 5 s statt 3 s: zwei Aufrufe teilen sich das Budget, keiner soll dadurch
                    // praktisch ohne Zeit dastehen. host.StopAsync erlaubt insgesamt bis zu 30 s.
                    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
                    await SendHeartbeatAsync("stopped", "PolyTrader wird geordnet beendet.", cts.Token)
                        .ConfigureAwait(false);
                    await SendEventAsync("stopped_graceful", "info", "PolyTrader wird geordnet beendet.", cts.Token)
                        .ConfigureAwait(false);
                }
                catch { /* Herunterfahren nie blockieren */ }
            }

            await base.StopAsync(cancellationToken).ConfigureAwait(false);
        }

        public override void Dispose()
        {
            _logger.OnLogMessage -= OnAppLog;
            base.Dispose();
        }

        /// <summary>
        /// Sendet einen Heartbeat. Kapselt jeden Fehler und liefert ihn als Ergebnis zurück –
        /// wirft nie. Öffentlich, damit der Test-Button und (später) die Statusmeldung darauf aufsetzen.
        /// </summary>
        public async Task<WatchdogSendResult> SendHeartbeatAsync(
            string status = "ok", string? message = null, CancellationToken ct = default)
        {
            string? problem = GetConfigurationProblem();
            if (problem != null) return new WatchdogSendResult(false, problem);

            if (!TryBuildEndpoint(_settings.WatchdogUrl, PingPath, out Uri? uri, out string urlProblem))
                return new WatchdogSendResult(false, urlProblem);

            string token;
            try
            {
                // Erlaubt einen mit dem Master-Key verschlüsselten Token (enc:v1:…);
                // Klartext wird unverändert durchgereicht.
                token = SecretProtection.Unprotect(_settings.WatchdogToken);
            }
            catch (Exception ex)
            {
                return new WatchdogSendResult(false, $"Agent-Token nicht lesbar: {ex.Message}");
            }

            var payload = new
            {
                source = _settings.WatchdogSource,
                instance = string.IsNullOrWhiteSpace(_settings.WatchdogInstance) ? "default" : _settings.WatchdogInstance,
                type = "heartbeat",
                status,
                message,
                // Feldname bewusst "interval": die Watchdog-API liest ausschließlich
                // "interval"/"expected_interval_sec" und ignoriert "intervalSeconds" still.
                interval = EffectiveIntervalSeconds(),
                group = "Applications",
                os = RuntimeInformation.OSDescription,
                // D-1: aus BuildInfo statt aus der Assembly-Version - stimmt jetzt mit
                // Directory.Build.props <Version> überein statt stets 1.0.0.0 zu tragen.
                version = BuildInfo.Version,
                checks = EvaluateChecks()
            };

            try
            {
                using var request = new HttpRequestMessage(HttpMethod.Post, uri)
                {
                    Content = JsonContent.Create(payload)
                };
                // D-1: Authorization: Bearer statt X-Watchdog-Key - das Deploymentcenter kennt den
                // alten Header nicht.
                request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);

                using var response = await _http.SendAsync(request, ct).ConfigureAwait(false);
                if (response.IsSuccessStatusCode)
                    return new WatchdogSendResult(true, $"HTTP {(int)response.StatusCode}");

                return new WatchdogSendResult(false, $"HTTP {(int)response.StatusCode} {response.ReasonPhrase}");
            }
            catch (OperationCanceledException) when (ct.IsCancellationRequested)
            {
                throw;   // geordnetes Herunterfahren nicht als Fehler behandeln
            }
            catch (Exception ex)
            {
                return new WatchdogSendResult(false, ex.Message);
            }
        }

        /// <summary>Sendet und protokolliert nur Zustandswechsel – kein Log-Eintrag pro Tick.</summary>
        private async Task SendAndTrackAsync(string status, string? message, CancellationToken ct)
        {
            WatchdogSendResult result;
            try
            {
                result = await SendHeartbeatAsync(status, message, ct).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                return;
            }

            if (result.Success)
            {
                if (_lastSendFailed)
                {
                    _logger.Info($"✅ Watchdog wieder erreichbar (nach {_consecutiveFailures} fehlgeschlagenen Versuchen).");
                }
                _lastSendFailed = false;
                _consecutiveFailures = 0;
                return;
            }

            _consecutiveFailures++;
            if (!_lastSendFailed)
            {
                // Nur der erste Fehler wird gemeldet; danach Ruhe bis zur Erholung.
                _logger.Warning($"⚠️ Watchdog-Heartbeat fehlgeschlagen: {result.Detail}. " +
                                "PolyTrader arbeitet normal weiter; weitere Fehlversuche werden nicht einzeln geloggt.");
            }
            _lastSendFailed = true;
        }

        /// <summary>
        /// B.3: Sendet ein Lifecycle-Event an <c>/api/watchdog/v1/event</c> (z. B. „started",
        /// „stopped_graceful"). Best effort – wirft nie und blockiert das Herunterfahren nicht.
        /// </summary>
        public async Task<WatchdogSendResult> SendEventAsync(string kind, string severity, string? message, CancellationToken ct = default)
        {
            string? problem = GetConfigurationProblem();
            if (problem != null) return new WatchdogSendResult(false, problem);

            if (!TryBuildEndpoint(_settings.WatchdogUrl, EventPath, out Uri? uri, out string urlProblem))
                return new WatchdogSendResult(false, urlProblem);

            string token;
            try { token = SecretProtection.Unprotect(_settings.WatchdogToken); }
            catch (Exception ex) { return new WatchdogSendResult(false, $"Agent-Token nicht lesbar: {ex.Message}"); }

            var payload = new
            {
                source = _settings.WatchdogSource,
                instance = string.IsNullOrWhiteSpace(_settings.WatchdogInstance) ? "default" : _settings.WatchdogInstance,
                kind,
                severity,
                message
            };

            try
            {
                using var request = new HttpRequestMessage(HttpMethod.Post, uri) { Content = JsonContent.Create(payload) };
                request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
                using var response = await _http.SendAsync(request, ct).ConfigureAwait(false);
                return response.IsSuccessStatusCode
                    ? new WatchdogSendResult(true, $"HTTP {(int)response.StatusCode}")
                    : new WatchdogSendResult(false, $"HTTP {(int)response.StatusCode} {response.ReasonPhrase}");
            }
            catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
            catch (Exception ex) { return new WatchdogSendResult(false, ex.Message); }
        }

        /// <summary>
        /// B.2: Leitet den zu meldenden Gesundheitsstatus aus dem App-Log ab. Ein <see cref="LogLevel.Error"/>
        /// hält den Status für <see cref="ErrorStickyWindow"/> auf <c>error</c> (mit der letzten, entschärften
        /// Fehlermeldung); danach wieder <c>ok</c>. Bewusst keine Warnungs-Eskalation: App-Warnungen sind zu
        /// häufig, um daraus einen Monitor-Alarm zu rechtfertigen.
        /// </summary>
        internal (string status, string? message) DeriveStatus()
        {
            lock (_statusLock)
            {
                if (_lastAppErrorMessage != null && (DateTime.UtcNow - _lastAppErrorUtc) <= ErrorStickyWindow)
                    return ("error", _lastAppErrorMessage);
            }
            return ("ok", null);
        }

        /// <summary>
        /// Entschärft eine Log-Meldung vor dem Versand an den (extern gehosteten) Watchdog: entfernt
        /// Wallet-Adressen/Keys (0x-Hex) und verschlüsselte Secrets (enc:v1:…) und kürzt auf 200 Zeichen.
        /// Der Watchdog soll nur „etwas ist kaputt" erfahren, keine sensiblen Nutzdaten.
        /// </summary>
        internal static string Sanitize(string? message)
        {
            // D-3: Musterprüfung selbst liegt jetzt in SecretRedactor - gemeinsam mit dem
            // Fehler-Reporter genutzt, der dieselben Muster aber ohne Zeilen-/Längenlimit braucht
            // (ein Stacktrace ist von Natur aus mehrzeilig und soll lesbar bleiben).
            string s = SecretRedactor.RemoveSecrets(message);
            s = s.Replace("\r", " ").Replace("\n", " ").Trim();
            return s.Length > 200 ? s.Substring(0, 197) + "…" : s;
        }

        /// <summary>Sende-Takt, nach unten begrenzt.</summary>
        private int EffectiveIntervalSeconds() =>
            Math.Max(MinIntervalSeconds, _settings.WatchdogIntervalSeconds);

        /// <summary>D-1: Pfad des Heartbeat-Endpunkts im Deploymentcenter (vorher "/api/heartbeat").</summary>
        internal const string PingPath = "watchdog/v1/ping";

        /// <summary>D-1: Pfad des Event-Endpunkts im Deploymentcenter (vorher "/api/event").</summary>
        internal const string EventPath = "watchdog/v1/event";

        /// <summary>
        /// Baut die Ziel-URL und erzwingt TLS. <c>http://</c> ist ausschließlich für localhost
        /// erlaubt (lokales Testen); sonst würde der Agent-Token im Klartext übertragen.
        /// </summary>
        internal static bool TryBuildEndpoint(string baseUrl, string relativePath, out Uri? uri, out string problem)
        {
            uri = null;
            problem = string.Empty;

            if (!Uri.TryCreate(baseUrl?.Trim().TrimEnd('/'), UriKind.Absolute, out Uri? parsed))
            {
                problem = $"Watchdog-URL ist keine gültige absolute URL: '{baseUrl}'.";
                return false;
            }

            bool isHttps = parsed.Scheme == Uri.UriSchemeHttps;
            bool isLocalHttp = parsed.Scheme == Uri.UriSchemeHttp && parsed.IsLoopback;
            if (!isHttps && !isLocalHttp)
            {
                problem = $"Watchdog-URL muss HTTPS verwenden (http:// nur für localhost): '{baseUrl}'. " +
                          "Über http:// ginge der Agent-Token im Klartext über das Netz.";
                return false;
            }

            uri = new Uri(parsed, $"{parsed.AbsolutePath.TrimEnd('/')}/api/{relativePath}");
            return true;
        }
    }
}
