using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Extensions.Logging;
// Alias pour éviter la collision avec le namespace PSLauncher.Core.Process
using SysProcess = System.Diagnostics.Process;

namespace PSLauncher.Core.Health.OpenVRInterop;

/// <summary>
/// Wrapper haut niveau autour de <see cref="OpenVRNative"/>. Singleton
/// thread-safe : init paresseuse au premier appel <see cref="Query"/>, état
/// mémorisé pour les appels suivants. La vtable IVRSystem reste vivante toute
/// la durée de vie du process — coût CPU négligeable au repos, init-shutdown
/// répété coûte cher (centaines de ms).
///
/// Robustesse : tout va dans des try/catch. SteamVR crashé / fermé pendant
/// la session → le service détecte au prochain Query (vtable call lève) et
/// repasse en <see cref="VrRuntimeState.HmdAbsent"/> proprement, prêt à
/// re-tenter au tick suivant.
/// </summary>
public sealed class OpenVrService : IOpenVrService, IDisposable
{
    private const string IVRSystemInterfaceVersion = "FnTable:IVRSystem_022";

    private readonly ILogger<OpenVrService> _logger;
    private readonly object _lock = new();

    private bool _resolverEnsured;
    private bool _initAttempted;
    private VrRuntimeState _lastLoggedState = (VrRuntimeState)(-99); // sentinelle « jamais loggé »

    /// <summary>
    /// Timestamp UTC de la PREMIÈRE détection de vrserver.exe lancé après une
    /// période où il était absent. Remis à null si vrserver disparaît. Sert à
    /// imposer un cooldown avant d'oser init OpenVR : entre l'apparition de
    /// vrserver dans la liste process et le moment où ses drivers + ses shared
    /// memories sont prêts à servir des requêtes vtable, il y a typiquement 5-10 s
    /// de race window pendant laquelle <see cref="OpenVRNative.GetVTableSlot"/>
    /// + premier appel = AccessViolation (lecture mémoire pas encore mappée).
    /// </summary>
    private DateTime? _vrServerFirstSeenUtc;
    private const double VrServerSettleSeconds = 8.0;
    private uint _initToken;
    private IntPtr _vrSystemVTable = IntPtr.Zero;

    // Delegates marshalés une seule fois après init pour éviter le coût
    // GetDelegateForFunctionPointer à chaque tick.
    private OpenVRNative.GetTrackedDeviceActivityLevelFn?         _fnActivity;
    private OpenVRNative.GetTrackedDeviceIndexForControllerRoleFn? _fnRoleToIndex;
    private OpenVRNative.GetTrackedDeviceClassFn?                 _fnDeviceClass;
    private OpenVRNative.IsTrackedDeviceConnectedFn?              _fnIsConnected;
    private OpenVRNative.GetBoolTrackedDevicePropertyFn?          _fnGetBool;
    private OpenVRNative.GetFloatTrackedDevicePropertyFn?         _fnGetFloat;
    private OpenVRNative.GetStringTrackedDevicePropertyFn?        _fnGetString;

    public OpenVrService(ILogger<OpenVrService> logger)
    {
        _logger = logger;
    }

    public VrDeviceQueryResult Query(string deviceAlias)
    {
        lock (_lock)
        {
            // ============================================================
            // MODE SAFE : on N'UTILISE PAS la vtable IVRSystem.
            //
            // Pourquoi : les fonctions de la vtable (IsTrackedDeviceConnected,
            // GetBoolTrackedDeviceProperty, etc.) font du SHM/IPC avec vrserver
            // qui peut être dans un état partiellement initialisé pendant et
            // après le démarrage de SteamVR. Ces fonctions accèdent à de la
            // mémoire pas encore mappée → AccessViolation natif → SEH passe
            // au travers du CLR via Marshal.GetDelegateForFunctionPointer
            // → process killed sans qu'aucun catch ne se déclenche.
            //
            // Repousser le délai de settle ne suffit pas (race window peut
            // se rouvrir à n'importe quel moment de la session : (re)connexion
            // d'un device, rechargement d'un driver…). On utilise donc UNIQUEMENT
            // l'API flat C : VR_IsRuntimeInstalled + VR_IsHmdPresent qui ne
            // touchent pas au SHM. Limitation : pas de batterie, pas de per-device
            // info — juste "HMD détecté oui/non". La batterie reviendra en
            // Phase 1.5 via un subprocess séparé (PSLauncher.VrProbe.exe) qui
            // peut crasher sans tuer le launcher.
            // ============================================================

            var label = ResolveLabel(deviceAlias);
            var state = EnsureReadySafe();
            LogStateTransition(state);

            string detail;
            switch (state)
            {
                case VrRuntimeState.RuntimeMissing:
                    return new VrDeviceQueryResult(state, false, label, null,
                        OpenVRNative.ETrackedDeviceClass.Invalid,
                        null, null, null, null, null,
                        "SteamVR n'est pas installé sur cette machine");

                case VrRuntimeState.HmdAbsent:
                    return new VrDeviceQueryResult(state, false, label, null,
                        OpenVRNative.ETrackedDeviceClass.Invalid,
                        false, null, null, null, null,
                        "SteamVR n'est pas lancé ou aucun HMD n'est branché");

                case VrRuntimeState.Ready:
                    detail = $"{label} : SteamVR actif, HMD détecté (info détaillée par appareil arrive en Phase 1.5)";
                    return new VrDeviceQueryResult(state, true, label, null,
                        OpenVRNative.ETrackedDeviceClass.HMD,
                        true, null, null, null, null,
                        detail);

                default:
                    return new VrDeviceQueryResult(state, false, label, null,
                        OpenVRNative.ETrackedDeviceClass.Invalid,
                        null, null, null, null, null,
                        "Échec d'initialisation OpenVR");
            }
        }
    }

    /// <summary>
    /// Variante safe d'EnsureReady : ne fait JAMAIS de VR_InitInternal2 ni
    /// de GetGenericInterface. Décide l'état uniquement depuis :
    ///   - <see cref="OpenVRNative.VR_IsRuntimeInstalled"/> (lookup statique
    ///     dans openvr_api.dll, aucun IPC, immédiat),
    ///   - vrserver.exe alive (process check),
    ///   - <see cref="OpenVRNative.VR_IsHmdPresent"/> (consulte un mutex/event
    ///     nommé partagé avec vrserver, mais pas de SHM).
    /// </summary>
    private VrRuntimeState EnsureReadySafe()
    {
        if (!_resolverEnsured)
        {
            OpenVRNative.EnsureResolverInstalled();
            _resolverEnsured = true;
        }

        try
        {
            if (!OpenVRNative.VR_IsRuntimeInstalled())
                return VrRuntimeState.RuntimeMissing;
        }
        catch (DllNotFoundException) { return VrRuntimeState.RuntimeMissing; }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "VR_IsRuntimeInstalled threw");
            return VrRuntimeState.RuntimeMissing;
        }

        // vrserver.exe en l'air ?
        if (!IsVrServerRunning()) return VrRuntimeState.HmdAbsent;

        // Cooldown de settle : on garde le délai même en mode safe parce que
        // VR_IsHmdPresent consulte un objet de synchro qui peut être pas encore
        // créé pendant les premières secondes du boot SteamVR.
        if (!HasVrServerSettled()) return VrRuntimeState.HmdAbsent;

        try
        {
            if (!OpenVRNative.VR_IsHmdPresent()) return VrRuntimeState.HmdAbsent;
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "VR_IsHmdPresent threw");
            return VrRuntimeState.HmdAbsent;
        }

        return VrRuntimeState.Ready;
    }

    private VrRuntimeState EnsureReady()
    {
        if (!_resolverEnsured)
        {
            OpenVRNative.EnsureResolverInstalled();
            _resolverEnsured = true;
        }

        if (_vrSystemVTable != IntPtr.Zero) return VrRuntimeState.Ready;

        // Probe runtime + HMD ; les deux sont des appels rapides qui ne déclenchent
        // pas l'init du compositor même si SteamVR est éteint.
        try
        {
            if (!OpenVRNative.VR_IsRuntimeInstalled())
                return VrRuntimeState.RuntimeMissing;
        }
        catch (DllNotFoundException) { return VrRuntimeState.RuntimeMissing; }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "VR_IsRuntimeInstalled threw");
            return VrRuntimeState.RuntimeMissing;
        }

        try
        {
            if (!OpenVRNative.VR_IsHmdPresent()) return VrRuntimeState.HmdAbsent;
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "VR_IsHmdPresent threw");
            return VrRuntimeState.HmdAbsent;
        }

        // Garde 1 : vrserver.exe DOIT être actif. Sans lui, VR_InitInternal2 en
        // mode Background peut soit hang, soit charger des drivers (lighthouse,
        // oculus_link…) qui plantent en SEH.
        if (!IsVrServerRunning())
        {
            return VrRuntimeState.HmdAbsent;
        }

        // Garde 2 : on attend que vrserver soit en l'air depuis assez longtemps
        // pour que ses drivers et ses shared memories soient initialisés.
        // Sinon : init succeed → premier vtable call → AccessViolation natif
        // (la shared memory n'est pas encore mappée). Le SEH passe au travers du
        // CLR via Marshal.GetDelegateForFunctionPointer et tue le process.
        // 8s = compromis entre réactivité (l'utilisateur a lancé SteamVR, on veut
        // pas attendre 30 s) et fiabilité (sur SSD c'est ~3-5 s, sur HDD ça peut
        // monter à 7 s). On garde HmdAbsent affiché pendant le warmup ; quand le
        // ticker se redéclenche après le délai, on enchaînera proprement sur Ready.
        if (!HasVrServerSettled())
        {
            return VrRuntimeState.HmdAbsent;
        }

        // Init en mode Background : on lit les states sans devenir une scene app.
        // L'init peut prendre 100-500 ms la première fois ; bloquer le caller est OK
        // car le health loop tourne sur thread dédié.
        if (_initAttempted && _initToken == 0) return VrRuntimeState.InitFailed;
        _initAttempted = true;

        try
        {
            _initToken = OpenVRNative.VR_InitInternal2(out var initError, OpenVRNative.VRApplicationBackground, null);
            if (initError != 0 || _initToken == 0)
            {
                _logger.LogWarning("VR_InitInternal2 failed: {Err}", OpenVRNative.GetInitErrorDescription(initError));
                _initToken = 0;
                return VrRuntimeState.InitFailed;
            }

            _vrSystemVTable = OpenVRNative.VR_GetGenericInterface(IVRSystemInterfaceVersion, out var ifaceError);
            if (_vrSystemVTable == IntPtr.Zero || ifaceError != 0)
            {
                _logger.LogWarning("VR_GetGenericInterface({Iface}) failed: {Err}",
                    IVRSystemInterfaceVersion, OpenVRNative.GetInitErrorDescription(ifaceError));
                ShutdownInternal();
                return VrRuntimeState.InitFailed;
            }

            BindVTableDelegates();
            return VrRuntimeState.Ready;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "OpenVR init threw");
            ShutdownInternal();
            return VrRuntimeState.InitFailed;
        }
    }

    private void BindVTableDelegates()
    {
        IntPtr P(int slot) => OpenVRNative.GetVTableSlot(_vrSystemVTable, slot);

        _fnActivity      = Marshal.GetDelegateForFunctionPointer<OpenVRNative.GetTrackedDeviceActivityLevelFn>(P(OpenVRNative.IVRSystemSlot.GetTrackedDeviceActivityLevel));
        _fnRoleToIndex   = Marshal.GetDelegateForFunctionPointer<OpenVRNative.GetTrackedDeviceIndexForControllerRoleFn>(P(OpenVRNative.IVRSystemSlot.GetTrackedDeviceIndexForControllerRole));
        _fnDeviceClass   = Marshal.GetDelegateForFunctionPointer<OpenVRNative.GetTrackedDeviceClassFn>(P(OpenVRNative.IVRSystemSlot.GetTrackedDeviceClass));
        _fnIsConnected   = Marshal.GetDelegateForFunctionPointer<OpenVRNative.IsTrackedDeviceConnectedFn>(P(OpenVRNative.IVRSystemSlot.IsTrackedDeviceConnected));
        _fnGetBool       = Marshal.GetDelegateForFunctionPointer<OpenVRNative.GetBoolTrackedDevicePropertyFn>(P(OpenVRNative.IVRSystemSlot.GetBoolTrackedDeviceProperty));
        _fnGetFloat      = Marshal.GetDelegateForFunctionPointer<OpenVRNative.GetFloatTrackedDevicePropertyFn>(P(OpenVRNative.IVRSystemSlot.GetFloatTrackedDeviceProperty));
        _fnGetString     = Marshal.GetDelegateForFunctionPointer<OpenVRNative.GetStringTrackedDevicePropertyFn>(P(OpenVRNative.IVRSystemSlot.GetStringTrackedDeviceProperty));
    }

    private uint? ResolveDeviceIndex(string alias)
    {
        if (_fnRoleToIndex is null || _fnDeviceClass is null) return null;
        var a = alias.Trim().ToLowerInvariant();
        const uint K_unMaxTrackedDeviceCount = 64;
        const uint K_InvalidIndex = 0xFFFFFFFF;

        switch (a)
        {
            case "hmd":
                return 0; // index 0 = HMD principal par convention OpenVR

            case "left_controller":
            {
                var idx = _fnRoleToIndex(OpenVRNative.ETrackedControllerRole.LeftHand);
                return idx == K_InvalidIndex ? null : idx;
            }

            case "right_controller":
            {
                var idx = _fnRoleToIndex(OpenVRNative.ETrackedControllerRole.RightHand);
                return idx == K_InvalidIndex ? null : idx;
            }
        }

        // tracker_N / base_station_N : on énumère tous les devices et on prend le N-ième de la classe demandée
        if (TryParseIndexed(a, "tracker_", out var trackerN))
            return FindNthOfClass(OpenVRNative.ETrackedDeviceClass.GenericTracker, trackerN);

        if (TryParseIndexed(a, "base_station_", out var stationN))
            return FindNthOfClass(OpenVRNative.ETrackedDeviceClass.TrackingReference, stationN);

        // Fallback : si l'utilisateur a tapé un index brut "0", "1", … on le respecte
        if (uint.TryParse(a, out var raw) && raw < K_unMaxTrackedDeviceCount)
            return raw;

        return null;
    }

    private static bool TryParseIndexed(string alias, string prefix, out int n)
    {
        n = 0;
        if (!alias.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) return false;
        var tail = alias[prefix.Length..];
        return int.TryParse(tail, out n) && n >= 1;
    }

    private uint? FindNthOfClass(OpenVRNative.ETrackedDeviceClass targetClass, int n)
    {
        if (_fnDeviceClass is null) return null;
        const uint K_unMaxTrackedDeviceCount = 64;
        var found = 0;
        for (uint i = 0; i < K_unMaxTrackedDeviceCount; i++)
        {
            if (_fnDeviceClass(i) == targetClass)
            {
                found++;
                if (found == n) return i;
            }
        }
        return null;
    }

    private static string ResolveLabel(string alias) => alias.Trim().ToLowerInvariant() switch
    {
        "hmd"              => "HMD",
        "left_controller"  => "Manette gauche",
        "right_controller" => "Manette droite",
        var a when a.StartsWith("tracker_") => $"Tracker {a[8..]}",
        var a when a.StartsWith("base_station_") => $"Base station {a[13..]}",
        _ => alias,
    };

    private bool? ReadBoolProperty(uint deviceIndex, int property)
    {
        try
        {
            var v = _fnGetBool!(deviceIndex, property, out var err);
            return err == 0 ? v : null;
        }
        catch { return null; }
    }

    private float? ReadFloatProperty(uint deviceIndex, int property)
    {
        try
        {
            var v = _fnGetFloat!(deviceIndex, property, out var err);
            return err == 0 ? v : null;
        }
        catch { return null; }
    }

    private string? ReadStringProperty(uint deviceIndex, int property)
    {
        try
        {
            var buf = Marshal.AllocHGlobal(256);
            try
            {
                var written = _fnGetString!(deviceIndex, property, buf, 256, out var err);
                if (err != 0 || written == 0) return null;
                // OpenVR renvoie un C-string ANSI null-terminé
                return Marshal.PtrToStringAnsi(buf, (int)Math.Min(written, 256u)).TrimEnd('\0');
            }
            finally
            {
                Marshal.FreeHGlobal(buf);
            }
        }
        catch { return null; }
    }

    /// <summary>
    /// Log au niveau INFO la première occurrence d'un état + chaque transition.
    /// Évite de spammer (un check s'exécute toutes les 5 s, on aurait 700 messages
    /// par heure si on loggait à chaque tick) tout en laissant une trace claire
    /// dans le journal au cas où ça re-crasherait — la dernière ligne avant le
    /// silence de Serilog dirait par exemple « OpenVR state: HmdAbsent ».
    /// </summary>
    private void LogStateTransition(VrRuntimeState newState)
    {
        if (newState == _lastLoggedState) return;
        _lastLoggedState = newState;
        var detail = newState switch
        {
            VrRuntimeState.RuntimeMissing => "openvr_api.dll introuvable (SteamVR pas installé)",
            VrRuntimeState.HmdAbsent      => "vrserver.exe pas lancé ou aucun HMD branché",
            VrRuntimeState.InitFailed     => "VR_InitInternal2 a échoué",
            VrRuntimeState.Ready          => "OK, vtable IVRSystem_022 bindée",
            _                             => "?",
        };
        _logger.LogInformation("OpenVR state: {State} ({Detail})", newState, detail);
    }

    /// <summary>
    /// vrserver.exe est le process central de SteamVR ; sa présence garantit qu'on
    /// peut init OpenVR sans déclencher de chargement de drivers risqué. Cache de
    /// 2 s pour ne pas faire un GetProcessesByName à chaque tick.
    /// </summary>
    private DateTime _vrServerCacheUntil = DateTime.MinValue;
    private bool _vrServerCacheValue;

    private bool IsVrServerRunning()
    {
        var now = DateTime.UtcNow;
        if (now < _vrServerCacheUntil) return _vrServerCacheValue;
        try
        {
            var procs = SysProcess.GetProcessesByName("vrserver");
            try
            {
                _vrServerCacheValue = procs.Length > 0;
            }
            finally
            {
                foreach (var p in procs) p.Dispose();
            }
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "vrserver process probe failed, assuming absent");
            _vrServerCacheValue = false;
        }
        _vrServerCacheUntil = now.AddSeconds(2);

        // Update first-seen tracking : on note l'instant où vrserver apparaît,
        // et on l'oublie quand il disparaît. C'est ce timestamp qu'on consulte
        // dans EnsureReady pour décider si la fenêtre de race est passée.
        if (_vrServerCacheValue && _vrServerFirstSeenUtc is null)
        {
            _vrServerFirstSeenUtc = now;
            _logger.LogInformation("vrserver.exe just appeared — waiting {Secs}s for SteamVR drivers/IPC to settle before init", VrServerSettleSeconds);
        }
        else if (!_vrServerCacheValue && _vrServerFirstSeenUtc is not null)
        {
            _logger.LogInformation("vrserver.exe disappeared — resetting OpenVR session");
            _vrServerFirstSeenUtc = null;
            // Si on était Ready, l'invalider : la prochaine session SteamVR exigera un nouvel init
            if (_initToken != 0) ShutdownInternal();
            _initAttempted = false;
        }
        return _vrServerCacheValue;
    }

    private bool HasVrServerSettled()
    {
        if (_vrServerFirstSeenUtc is null) return false;
        return (DateTime.UtcNow - _vrServerFirstSeenUtc.Value).TotalSeconds >= VrServerSettleSeconds;
    }

    private void MarkSessionBroken()
    {
        ShutdownInternal();
        _initAttempted = false; // permettre une retente au prochain tick
    }

    private void ShutdownInternal()
    {
        _vrSystemVTable = IntPtr.Zero;
        _fnActivity = null;
        _fnRoleToIndex = null;
        _fnDeviceClass = null;
        _fnIsConnected = null;
        _fnGetBool = null;
        _fnGetFloat = null;
        _fnGetString = null;

        if (_initToken != 0)
        {
            try { OpenVRNative.VR_ShutdownInternal(); } catch { /* ignore */ }
            _initToken = 0;
        }
    }

    public void Dispose()
    {
        lock (_lock) ShutdownInternal();
    }
}
