#if USE_WSLC
using System.Runtime.InteropServices;

namespace NucleicBroker.Wslc;

/// <summary>
/// D13's internal-COM arm, **Tier 1: recover** (docs/WINDOWS_PORT.md §13.2).
///
/// The compat SDK cannot re-adopt a running session — its `Session` constructor is lazy and
/// `Start()` refuses an existing name with `ERROR_ALREADY_EXISTS`. So a broker that restarts
/// while its session is up cannot reach the sandbox again, and today that costs the user a manual
/// `wsl --shutdown`. This class fixes exactly that: it opens the orphaned session through the
/// **service-internal** COM interface, reports what was running, terminates it, and lets the
/// facade create a fresh one through the ordinary compat path.
///
/// It deliberately does NOT try to keep those containers alive — that is Tier 2, and it is
/// blocked (§13.2): `Session.FromAbi()` throws on a service-side pointer, because the WinRT layer
/// the C# projection wraps lives client-side in `wslcsdk.dll`. Everything here is confirmed on
/// hardware; nothing here depends on that unresolved question.
///
/// **Four things that are not obvious and each cost a debugging round:**
///
/// 1. **There is no CLSID for `IWSLCSessionManager`.** `wslc.idl` declares interfaces and no
///    activatable class. The entry point is the *compat* coclass — `WSLCCompatSessionManager`
///    also implements the internal interface. One object, two faces.
/// 2. **The proxy must grant IMPERSONATE.** Per-user calls like `OpenSessionByName` fail
///    `0x80070542` (`ERROR_BAD_IMPERSONATION_LEVEL`) under .NET's default `IDENTIFY` — a security
///    error that reads exactly like "not found". `GetVersion`/`ListSessions` don't impersonate,
///    so they succeed and make it look like a per-method gap. <see cref="InitializeSecurity"/>
///    handles this process-wide; the per-proxy blanket here is belt and braces.
/// 3. **Vtable slots are fixed by declaration ORDER, not signature.** Only the methods actually
///    called need accurate signatures, which is what makes reaching `ListContainers` (method #19,
///    behind four methods taking a by-value `WSLCHandle` union) tractable at all.
/// 4. **The ABI is explicitly unstable.** `wslc.idl` says breaking changes are fine because
///    Microsoft ships both ends. We are not both ends, so every entry point here is probed and
///    every failure degrades to "no recovery" rather than propagating.
/// </summary>
internal sealed class WslcInternal : IDisposable
{
    private IWSLCSessionManager? manager;
    private IntPtr managerPtr;

    /// <summary>What a recovery found and did, for the host log and the `session.down` story.</summary>
    internal sealed record Recovery(IReadOnlyList<string> Containers, bool Terminated);

    /// <summary>
    /// Bind the internal interface, or return null. Called once at facade construction so the
    /// result can be reported in the `capabilities` hello (§2.3) rather than discovered when a
    /// user's broker restarts.
    /// </summary>
    internal static WslcInternal? TryBind()
    {
        var clsid = ClsidWslcCompatSessionManager;
        var iid = IidWslcSessionManager;
        // Ask for the internal interface directly. The compat coclass implements both, and going
        // straight for it means a machine where this arm is unavailable fails here rather than
        // half-way through a recovery.
        var hr = CoCreateInstance(in clsid, IntPtr.Zero, ClsctxAll, in iid, out var ptr);
        if (hr < 0)
        {
            // REGDB_E_CLASSNOTREG here means WSL simply isn't installed — the §8 onboarding
            // state, not a defect. Saying "recovery unavailable" without that distinction reads
            // as a broker fault on a machine that has not been set up yet.
            Console.Error.WriteLine((uint)hr == RegdbEClassNotReg
                ? "wslc: WSL is not installed — internal COM absent, as expected before onboarding"
                : $"wslc: internal COM did not bind (0x{hr:X8}) — a broker restart will not "
                  + "auto-recover a running session; D13 Tier 1 is unavailable on this machine");
            return null;
        }

        RaiseImpersonation(ptr);
        try
        {
            return new WslcInternal
            {
                managerPtr = ptr,
                manager = (IWSLCSessionManager)Marshal.GetObjectForIUnknown(ptr),
            };
        }
        catch (Exception e)
        {
            Marshal.Release(ptr);
            Console.Error.WriteLine($"wslc: internal COM bound but unusable: {e.Message}");
            return null;
        }
    }

    /// <summary>
    /// Open the orphaned session named <paramref name="name"/>, note what was running in it, and
    /// terminate it. Returns null when there is nothing to recover — which is the ordinary case
    /// and not an error.
    ///
    /// Terminating rather than adopting is the deliberate Tier 1 choice: the containers are lost,
    /// but they are lost today too, and `ContainerManager.reconcile` already copes with a sandbox
    /// that came back empty. What it buys is that the *next* `Start()` succeeds.
    /// </summary>
    internal Recovery? RecoverSession(string name)
    {
        if (manager is null) return null;

        int hr;
        IntPtr sessionPtr;
        try
        {
            hr = manager.OpenSessionByName(name, out sessionPtr);
        }
        catch (Exception e)
        {
            Console.Error.WriteLine($"wslc: OpenSessionByName('{name}') threw: {e.Message}");
            return null;
        }
        if (hr < 0)
        {
            Console.Error.WriteLine($"wslc: no recoverable session '{name}' (0x{hr:X8})"
                + ((uint)hr == ErrorBadImpersonationLevel
                    ? " — IMPERSONATE was not granted; CoInitializeSecurity must run before the "
                      + "first COM call in the process"
                    : ""));
            return null;
        }

        // The session proxy is a separate object from the manager, so it needs its own blanket.
        RaiseImpersonation(sessionPtr);
        try
        {
            var session = (IWSLCSession)Marshal.GetObjectForIUnknown(sessionPtr);
            var containers = ListContainers(session);
            var terminated = Terminate(session);
            Console.Error.WriteLine(
                $"wslc: recovered orphaned session '{name}' — {containers.Count} container(s) "
                + $"[{string.Join(", ", containers)}], terminated={terminated}");
            return new Recovery(containers, terminated);
        }
        catch (Exception e)
        {
            Console.Error.WriteLine($"wslc: recovery of '{name}' failed: {e.Message}");
            return null;
        }
        finally
        {
            Marshal.Release(sessionPtr);
        }
    }

    /// <summary>
    /// The container roster of a session, by name. Enumeration the compat SDK has no call for at
    /// all — <c>Session</c> exposes no listing and <c>Container</c> carries no <c>Name</c>.
    /// </summary>
    private static IReadOnlyList<string> ListContainers(IWSLCSession session)
    {
        // Flags=All, or the listing is running-containers-only and a stopped container silently
        // vanishes from the recovery report.
        var options = new WslcListContainersOptions
        {
            Flags = WslcListContainersFlagsAll,
            Limit = 0,
            Filters = IntPtr.Zero,
            FiltersCount = 0,
        };
        var optionsPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf<WslcListContainersOptions>());
        var containers = IntPtr.Zero;
        var ports = IntPtr.Zero;
        try
        {
            Marshal.StructureToPtr(options, optionsPtr, fDeleteOld: false);
            var hr = session.ListContainers(optionsPtr, out containers, out var count,
                out ports, out _);
            if (hr < 0)
            {
                Console.Error.WriteLine($"wslc: ListContainers failed (0x{hr:X8})");
                return [];
            }

            var size = Marshal.SizeOf<WslcContainerEntry>();
            var names = new List<string>((int)count);
            for (var i = 0; i < count; i++)
            {
                var entry = Marshal.PtrToStructure<WslcContainerEntry>(containers + i * size);
                names.Add(string.IsNullOrEmpty(entry.Name) ? entry.Id : entry.Name);
            }
            return names;
        }
        finally
        {
            Marshal.FreeCoTaskMem(optionsPtr);
            // Both out-arrays are callee-allocated; nobody else frees them.
            if (containers != IntPtr.Zero) Marshal.FreeCoTaskMem(containers);
            if (ports != IntPtr.Zero) Marshal.FreeCoTaskMem(ports);
        }
    }

    private static bool Terminate(IWSLCSession session)
    {
        try
        {
            var hr = session.Terminate();
            if (hr >= 0) return true;
            Console.Error.WriteLine($"wslc: session Terminate failed (0x{hr:X8})");
            return false;
        }
        catch (Exception e)
        {
            Console.Error.WriteLine($"wslc: session Terminate threw: {e.Message}");
            return false;
        }
    }

    public void Dispose()
    {
        manager = null;
        if (managerPtr != IntPtr.Zero)
        {
            Marshal.Release(managerPtr);
            managerPtr = IntPtr.Zero;
        }
    }

    // MARK: - COM security

    /// <summary>
    /// Grant servers the right to impersonate this process, for **every** proxy it will hold.
    ///
    /// Must run before the first COM call in the process or it fails `RPC_E_TOO_LATE` — and the
    /// compat SDK makes COM calls of its own, so this has to precede any `WslcService`/`Session`
    /// use, not merely precede the internal arm. Failing is not fatal: only the per-user internal
    /// calls need it, so the sandbox still runs and recovery is what degrades.
    /// </summary>
    internal static void InitializeSecurity()
    {
        var hr = CoInitializeSecurity(
            IntPtr.Zero, -1, IntPtr.Zero, IntPtr.Zero,
            RpcCAuthnLevelDefault, RpcCImpLevelImpersonate, IntPtr.Zero, EoacNone, IntPtr.Zero);
        // RPC_E_TOO_LATE means something already initialised security — worth saying, because it
        // silently removes session recovery and nothing else will mention it.
        if (hr < 0)
            Console.Error.WriteLine(
                $"wslc: CoInitializeSecurity failed (0x{hr:X8})"
                + ((uint)hr == RpcETooLate
                    ? " — RPC_E_TOO_LATE: a COM call ran first. Session recovery will fail "
                      + "0x80070542."
                    : ""));
    }

    private static void RaiseImpersonation(IntPtr proxy) =>
        // Per-proxy, and harmless if CoInitializeSecurity already covered it. Kept because the
        // process-wide call is order-dependent and this one is not.
        CoSetProxyBlanket(
            proxy, RpcCAuthnDefault, RpcCAuthzDefault, ColeDefaultPrincipal,
            RpcCAuthnLevelDefault, RpcCImpLevelImpersonate, ColeDefaultAuthinfo, EoacNone);

    // MARK: - Interop

    /// `WSLCCompatSessionManager` from WSLCCompat.idl. Not a typo that this is the *compat*
    /// class: `wslc.idl` declares no coclass, and this one answers a QI for the internal
    /// interface (confirmed on hardware, §13.2).
    private static readonly Guid ClsidWslcCompatSessionManager =
        new("a9b7a1b9-0671-405c-95f1-e0612cb4ce8f");

    private static readonly Guid IidWslcSessionManager = new("82A7ABC8-6B50-43FC-AB96-15FBBE7E8760");

    private const uint ClsctxAll = 0x17;
    private const uint RpcCAuthnDefault = 0xFFFFFFFF;
    private const uint RpcCAuthzDefault = 0xFFFFFFFF;
    private const uint RpcCAuthnLevelDefault = 0;
    private const uint RpcCImpLevelImpersonate = 3;
    private const uint EoacNone = 0;
    private const uint RpcETooLate = 0x80010119;
    private const uint RegdbEClassNotReg = 0x80040154;
    private const uint ErrorBadImpersonationLevel = 0x80070542;
    private const uint WslcListContainersFlagsAll = 1;
    private static readonly IntPtr ColeDefaultPrincipal = new(-1);
    private static readonly IntPtr ColeDefaultAuthinfo = new(-1);

    [DllImport("ole32.dll")]
    private static extern int CoCreateInstance(
        in Guid clsid, IntPtr outer, uint clsContext, in Guid iid, out IntPtr instance);

    [DllImport("ole32.dll")]
    private static extern int CoSetProxyBlanket(
        IntPtr proxy, uint authnService, uint authzService, IntPtr serverPrincipalName,
        uint authnLevel, uint impersonationLevel, IntPtr authInfo, uint capabilities);

    [DllImport("ole32.dll")]
    private static extern int CoInitializeSecurity(
        IntPtr securityDescriptor, int authSvcCount, IntPtr authSvc, IntPtr reserved1,
        uint authnLevel, uint impersonationLevel, IntPtr authList, uint capabilities,
        IntPtr reserved3);

    [StructLayout(LayoutKind.Sequential)]
    private struct WslcListContainersOptions
    {
        public uint Flags;
        public int Limit;
        public IntPtr Filters;
        public uint FiltersCount;
    }

    /// <summary>
    /// `WSLCContainerEntry` from wslc.idl. The three char arrays are **inline fixed buffers**,
    /// not pointers — `ByValTStr`/`Ansi`, with the sizes straight from the IDL's `+ 1` constants
    /// (255+1, 255+1, 64+1). Getting a size wrong here does not fail loudly; it silently shifts
    /// every later field. The equivalent layout was validated on hardware via
    /// `ListSessions`, whose entry struct has the same shape (§13.2).
    /// </summary>
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    private struct WslcContainerEntry
    {
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string Name;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string Image;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)] public string Id;
        public ulong StateChangedAt;
        public ulong CreatedAt;
        public uint State;
    }

    [ComImport, Guid("82A7ABC8-6B50-43FC-AB96-15FBBE7E8760"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IWSLCSessionManager
    {
        [PreserveSig] int GetVersion(out WslcVersion version);
        [PreserveSig] int CreateSession(IntPtr settings, uint flags, IntPtr warningCallback, out IntPtr session);
        [PreserveSig] int EnterSession(
            [MarshalAs(UnmanagedType.LPWStr)] string displayName,
            [MarshalAs(UnmanagedType.LPWStr)] string storagePath,
            IntPtr warningCallback, out IntPtr session);
        [PreserveSig] int ListSessions(out IntPtr sessions, out uint count);
        [PreserveSig] int OpenSession(uint id, out IntPtr session);
        [PreserveSig] int OpenSessionByName(
            [MarshalAs(UnmanagedType.LPWStr)] string displayName, out IntPtr session);
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct WslcVersion
    {
        public uint Major;
        public uint Minor;
        public uint Revision;
    }

    /// <summary>
    /// `IWSLCSession`, declared only as far as <c>Terminate</c> (method #23).
    ///
    /// **Every method ahead of the ones we call must still be declared**, because a COM vtable is
    /// addressed by slot — but only the called ones need accurate signatures, since a method that
    /// is never invoked is never marshalled. That is what makes this tractable: four of the
    /// placeholders (`LoadImage`, `ImportImage`, `SaveImage`, `SaveImages`) take `WSLCHandle` — a
    /// tagged union — **by value**, which would be genuinely awkward to marshal and does not have
    /// to be. Parameter *counts* are kept faithful to the IDL purely as documentation.
    ///
    /// Do not reorder. Do not delete an unused entry. Either silently shifts every slot below it.
    /// </summary>
    [ComImport, Guid("EF0661E4-6364-40EA-B433-E2FDF11F3519"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IWSLCSession
    {
        [PreserveSig] int GetId(out uint id);                                           // 1
        [PreserveSig] int GetDisplayName(out IntPtr displayName);                       // 2
        [PreserveSig] int GetState(out uint state);                                     // 3
        [PreserveSig] int GetTerminationEvent(out IntPtr eventHandle);                  // 4
        [PreserveSig] int GetTerminationReason(out uint reason, out IntPtr details);    // 5
        [PreserveSig] int PullImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d);            // 6
        [PreserveSig] int BuildImage(IntPtr a, IntPtr b, IntPtr c);                     // 7
        [PreserveSig] int LoadImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d);            // 8  (WSLCHandle by value)
        [PreserveSig] int ImportImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d, IntPtr e); // 9  (WSLCHandle by value)
        [PreserveSig] int SaveImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d);            // 10 (WSLCHandle by value)
        [PreserveSig] int SaveImages(IntPtr a, IntPtr b, IntPtr c, IntPtr d);           // 11 (WSLCHandle by value)
        [PreserveSig] int ListImages(IntPtr a, IntPtr b, IntPtr c);                     // 12
        [PreserveSig] int DeleteImage(IntPtr a, IntPtr b, IntPtr c);                    // 13
        [PreserveSig] int TagImage(IntPtr a);                                           // 14
        [PreserveSig] int InspectImage(IntPtr a, IntPtr b);                             // 15
        [PreserveSig] int PruneImages(IntPtr a, IntPtr b, IntPtr c, IntPtr d, IntPtr e); // 16
        [PreserveSig] int CreateContainer(IntPtr a, IntPtr b, IntPtr c);                // 17
        [PreserveSig] int OpenContainer(IntPtr a, IntPtr b);                            // 18
        [PreserveSig] int ListContainers(                                               // 19
            IntPtr options, out IntPtr containers, out uint count,
            out IntPtr ports, out uint portsCount);
        [PreserveSig] int PruneContainers(IntPtr a, IntPtr b, IntPtr c);                // 20
        [PreserveSig] int CreateRootNamespaceProcess(
            IntPtr a, IntPtr b, IntPtr c, IntPtr d, IntPtr e, IntPtr f);                // 21
        [PreserveSig] int FormatVirtualDisk(IntPtr a);                                  // 22
        [PreserveSig] int Terminate();                                                  // 23
    }
}
#endif
