using System.Net.NetworkInformation;
using Microsoft.Extensions.Logging;
using PSLauncher.Core.Health.OpenVRInterop;
using PSLauncher.Models;
// NB : on n'importe PAS System.Diagnostics car le namespace PSLauncher.Core.Process
// existe déjà et provoque une ambiguïté avec System.Diagnostics.Process.
// On qualifie complètement chaque usage pour rester sans ambiguïté.
using SysProcess = System.Diagnostics.Process;

namespace PSLauncher.Core.Health;

/// <summary>
/// Implémentation : Ping ICMP via <see cref="Ping"/>, et listing de processus
/// via <see cref="Process.GetProcessesByName(string)"/>. Pas de privilèges
/// administrateur requis (le ping classique ICMP fonctionne en user-mode sur
/// Windows 10+ ; pas besoin du raw socket).
/// </summary>
public sealed class SystemHealthService : ISystemHealthService
{
    private readonly ILogger<SystemHealthService> _logger;
    private readonly IOpenVrService _openVr;

    public SystemHealthService(ILogger<SystemHealthService> logger, IOpenVrService openVr)
    {
        _logger = logger;
        _openVr = openVr;
    }

    public async Task<HealthResult> RunCheckAsync(HealthCheckEntry entry, CancellationToken ct)
    {
        var kind = entry.Kind?.Trim().ToLowerInvariant();

        // VrDevice n'a PAS besoin d'un Target non vide (HMD = index 0 implicite),
        // donc on le sort du gate Target-vide.
        if (kind != "vrdevice" && string.IsNullOrWhiteSpace(entry.Target))
            return new HealthResult(HealthSeverity.Unknown, "Non configuré (Target vide)");

        try
        {
            return kind switch
            {
                "ping"     => await CheckPingAsync(entry, ct).ConfigureAwait(false),
                "process"  => CheckProcess(entry),
                "vrdevice" => CheckVrDevice(entry),
                _          => new HealthResult(HealthSeverity.Unknown, $"Kind inconnu : '{entry.Kind}'"),
            };
        }
        catch (OperationCanceledException) { throw; }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Health check {Name} ({Kind} {Target}) failed", entry.Name, entry.Kind, entry.Target);
            return new HealthResult(HealthSeverity.Error, $"Exception : {ex.GetType().Name} — {ex.Message}");
        }
    }

    private static async Task<HealthResult> CheckPingAsync(HealthCheckEntry entry, CancellationToken ct)
    {
        using var ping = new Ping();
        var timeout = entry.PingTimeoutMs ?? 1000;
        PingReply reply;
        try
        {
            reply = await ping.SendPingAsync(entry.Target, timeout).WaitAsync(ct).ConfigureAwait(false);
        }
        catch (PingException pex)
        {
            return new HealthResult(HealthSeverity.Error, $"Ping {entry.Target} : DNS introuvable ou réseau inaccessible ({pex.InnerException?.Message ?? pex.Message})");
        }

        if (reply.Status != IPStatus.Success)
            return new HealthResult(HealthSeverity.Error,
                $"Ping {entry.Target} : pas de réponse ({reply.Status})");

        var rtt = reply.RoundtripTime;
        var warn = entry.PingWarnMs ?? 50;
        var err  = entry.PingErrorMs ?? 200;

        if (rtt > err)
            return new HealthResult(HealthSeverity.Error,
                $"Ping {entry.Target} : {rtt} ms (seuil rouge {err} ms)");
        if (rtt > warn)
            return new HealthResult(HealthSeverity.Warning,
                $"Ping {entry.Target} : {rtt} ms (seuil orange {warn} ms)");
        return new HealthResult(HealthSeverity.Ok,
            $"Ping {entry.Target} : {rtt} ms");
    }

    private static HealthResult CheckProcess(HealthCheckEntry entry)
    {
        // Process.GetProcessesByName accepte le nom sans extension .exe
        var name = entry.Target.Trim();
        if (name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
            name = name[..^4];

        SysProcess[] procs;
        try
        {
            procs = SysProcess.GetProcessesByName(name);
        }
        catch (Exception ex)
        {
            return new HealthResult(HealthSeverity.Error, $"Lookup processus échoué : {ex.Message}");
        }
        try
        {
            if (procs.Length == 0)
                return new HealthResult(HealthSeverity.Error,
                    $"Processus « {name}.exe » non lancé");
            return new HealthResult(HealthSeverity.Ok,
                $"« {name}.exe » est en cours d'exécution ({procs.Length} instance{(procs.Length > 1 ? "s" : "")})");
        }
        finally
        {
            // Process objects portent un handle Win32 à libérer
            foreach (var p in procs) p.Dispose();
        }
    }

    /// <summary>
    /// Lit l'état d'un device SteamVR via OpenVR :
    /// - Runtime absent → Unknown + "SteamVR non installé"
    /// - HMD absent → Unknown + "SteamVR non lancé / aucun HMD branché"
    /// - Device cherché non trouvé → Error + "Appareil non détecté"
    /// - Battery &lt; 15% (et pas en charge) → Error
    /// - Battery 15-30% (et pas en charge) → Warning
    /// - Sinon → Ok avec le % batterie
    ///
    /// Pour les <c>TrackingReference</c> (base stations) qui n'exposent pas de
    /// batterie, on tombe sur l'état connecté/en veille uniquement.
    /// </summary>
    private HealthResult CheckVrDevice(HealthCheckEntry entry)
    {
        // Si Target vide pour VrDevice on assume "hmd" — c'est le cas le plus courant
        var alias = string.IsNullOrWhiteSpace(entry.Target) ? "hmd" : entry.Target.Trim();
        var result = _openVr.Query(alias);

        if (result.RuntimeState == VrRuntimeState.RuntimeMissing)
            return new HealthResult(HealthSeverity.Unknown, "SteamVR n'est pas installé sur cette machine");

        if (result.RuntimeState == VrRuntimeState.HmdAbsent)
            return new HealthResult(HealthSeverity.Error, "SteamVR n'est pas lancé ou aucun HMD n'est branché");

        if (result.RuntimeState == VrRuntimeState.InitFailed)
            return new HealthResult(HealthSeverity.Error,
                $"Échec init OpenVR{(result.ErrorDetail is null ? "" : " : " + result.ErrorDetail)}");

        if (!result.Found || result.IsConnected != true)
            return new HealthResult(HealthSeverity.Error,
                result.ErrorDetail ?? $"{result.DeviceLabel} non détecté");

        // Mode safe (Phase 1) : OpenVR ne nous donne plus de batterie/activity/serial
        // pour éviter le crash sur les vtable calls. Quand tous ces champs sont null
        // mais qu'on est en Ready+Connected, on affiche le message court fourni par
        // OpenVrService (ex. "HMD détecté — Phase 1.5 fournira la batterie").
        if (result.BatteryPercentage is null
            && result.IsCharging is null
            && result.ActivityLevel is null
            && string.IsNullOrEmpty(result.Serial))
        {
            return new HealthResult(HealthSeverity.Ok,
                result.ErrorDetail ?? $"{result.DeviceLabel} détecté");
        }

        // Device présent : on assemble un résumé batterie + activité
        var battery = result.BatteryPercentage;
        var pctText = battery is null ? "—" : $"{Math.Round(battery.Value * 100)}%";
        var chargeIcon = result.IsCharging == true ? " ⚡" : "";
        var serial = string.IsNullOrEmpty(result.Serial) ? "" : $" · {result.Serial}";
        var activityText = result.ActivityLevel switch
        {
            OpenVRNative.EDeviceActivityLevel.UserInteraction => "actif",
            OpenVRNative.EDeviceActivityLevel.UserInteraction_Timeout => "actif (en pause)",
            OpenVRNative.EDeviceActivityLevel.Standby                 => "en veille",
            OpenVRNative.EDeviceActivityLevel.Idle                    => "inactif",
            OpenVRNative.EDeviceActivityLevel.Idle_Timeout            => "inactif",
            _ => null,
        };
        var detail = $"{result.DeviceLabel} : {pctText}{chargeIcon}" +
                     (activityText is null ? "" : $" — {activityText}") +
                     serial;

        // Severity : prioriser charging et class spéciale (base stations sans batterie)
        if (result.DeviceClass == OpenVRNative.ETrackedDeviceClass.TrackingReference && battery is null)
        {
            // Base station : connectée = OK (même sans info batterie)
            return new HealthResult(HealthSeverity.Ok, detail);
        }

        if (result.IsCharging == true)
            return new HealthResult(HealthSeverity.Ok, detail);

        if (battery is null)
        {
            // Device sans batterie exposée (ex. base station filaire) : on rapporte juste "connecté"
            return new HealthResult(HealthSeverity.Ok, detail);
        }

        var pct = battery.Value;
        if (pct < 0.15f) return new HealthResult(HealthSeverity.Error,   detail + " (seuil rouge < 15%)");
        if (pct < 0.30f) return new HealthResult(HealthSeverity.Warning, detail + " (seuil orange < 30%)");
        return new HealthResult(HealthSeverity.Ok, detail);
    }
}
