using System.Runtime.InteropServices;

namespace WslcApiDump;

/// <summary>
/// D13's open question, reduced to one run (docs/WINDOWS_PORT.md §13.2).
///
/// The plan routes container enumeration, session/container reattach and the Terminal panel's pty
/// to the service-internal COM interface `IWSLCSessionManager` (IID 82A7ABC8-…). Reading
/// `wslc.idl` turned up a problem the plan assumed away: **it declares interfaces and no
/// activatable class.** There is no CLSID in it, so `CoCreateInstance` has nothing to name, and
/// the only registered coclasses anywhere in WSL's IDLs belong to the *compat* surface and to the
/// WSL service proper.
///
/// The likely answer is that one of those coclasses also implements the internal interface — COM
/// objects routinely expose several — and `wslc.exe` simply QIs for it. That is a yes/no question,
/// and everything downstream depends on it:
///
/// * **yes** → write the internal arm (`ListContainers`, `OpenSessionByName`, `OpenContainer`,
///   `ResizeTty`), and a broker restart stops being fatal to a running sandbox.
/// * **no**  → D13 needs reopening. The realistic alternatives are a durable host-side roster
///   plus `wsl --shutdown` on restart, or revisiting the rejected `wslc.exe` arm for enumeration.
///
/// Two stages, because they carry very different risk:
///
/// * <c>--internal</c> activates each candidate class and QIs for the internal IIDs. It never
///   calls a method, so **it cannot crash on a vtable mismatch** — and QI alone answers the
///   entry-point question.
/// * <c>--internal-call</c> additionally calls through the interface (`GetVersion`, then
///   `ListSessions`). This one CAN take the process down if the real vtable differs from the
///   IDL — which is itself a finding, and the reason it is opt-in rather than default.
/// </summary>
internal static class InternalComProbe
{
    // The only registered coclasses in WSL's IDLs. wslc.idl contributes none — that is the
    // problem — so every candidate here comes from WSLCCompat.idl or wslservice.idl.
    private static readonly (string Name, Guid Clsid, string Source)[] Candidates =
    [
        ("WSLCCompatSessionManager", new Guid("a9b7a1b9-0671-405c-95f1-e0612cb4ce8f"),
            "WSLCCompat.idl — what the SDK itself activates, so the likeliest host"),
        ("WSLCCompatSessionManagerFactory", new Guid("9fcd2067-9fc6-4efa-9eb0-698169ebf7d3"),
            "WSLCCompat.idl"),
        ("LxssUserSession", new Guid("a9b7a1b9-0671-405c-95f1-e0612cb4ce7e"),
            "wslservice.idl — the WSL service proper; note it differs from the compat CLSID "
            + "only in the last byte (ce7e vs ce8f)"),
        ("LxssUserSessionInBox", new Guid("4f476546-b412-4579-b64c-123df331e3d6"),
            "wslservice.idl"),
    ];

    private static readonly (string Name, Guid Iid, string Why)[] Interfaces =
    [
        ("IWSLCSessionManager", new Guid("82A7ABC8-6B50-43FC-AB96-15FBBE7E8760"),
            "THE one that matters — OpenSessionByName/EnterSession/ListSessions, i.e. reattach"),
        ("IWSLCSession", new Guid("EF0661E4-6364-40EA-B433-E2FDF11F3519"),
            "ListContainers/OpenContainer. EXPECTED to fail here, and its failure is not a gap: a "
            + "session is RETURNED BY the manager (OpenSessionByName/EnterSession/CreateSession), "
            + "not QI'd off it. Probed only to confirm the manager is a manager and not a "
            + "do-everything object"),
        ("IWSLCVirtualMachine", new Guid("B5E2D8F1-9A3C-4E6B-8D1F-7C4A2E9B6D3A"),
            "GetId → the VM GUID an AF_HYPERV bind needs. Per the IDL only a factory the SYSTEM "
            + "service owns can produce one, so this is EXPECTED to fail — probed anyway, "
            + "because §13.2 retracted a decision on that reading and it deserves confirming"),
    ];

    internal static void Run(bool callThrough, string? sessionName = null)
    {
        InternalComProbe.sessionName = sessionName;
        Console.WriteLine();
        Console.WriteLine("internal COM probe (docs/WINDOWS_PORT.md §13.2):");
        Console.WriteLine("  wslc.idl declares no coclass, so the question is whether an EXISTING");
        Console.WriteLine("  class answers a QI for the internal interfaces.");
        Console.WriteLine();

        var initialized = CoInitializeEx(IntPtr.Zero, CoinitMultithreaded);
        // S_FALSE means already initialized on this thread; RPC_E_CHANGED_MODE means the runtime
        // picked the other apartment. Neither is fatal for an out-of-proc server.
        if (initialized < 0 && (uint)initialized != RpcEChangedMode)
        {
            Console.WriteLine($"  CoInitializeEx failed: 0x{initialized:X8} — cannot probe");
            return;
        }

        var bound = false;
        foreach (var (name, clsid, source) in Candidates)
        {
            Console.WriteLine($"  {name}  {{{clsid}}}");
            Console.WriteLine($"    ({source})");

            var hr = CoCreateInstance(in clsid, IntPtr.Zero, ClsctxAll, in IidIUnknown, out var unknown);
            if (hr < 0)
            {
                Console.WriteLine($"    activation failed: {Hresult(hr)}");
                if ((uint)hr == ENoInterface)
                    // Every COM object implements IUnknown, so this combination is impossible
                    // unless the IID being passed is not IID_IUnknown. Say so loudly: the first
                    // version of this probe did exactly that and reported a confident false
                    // negative that read like a real finding about WSL.
                    Console.WriteLine("    !! E_NOINTERFACE on an IUnknown activation is IMPOSSIBLE — "
                        + "every COM object implements IUnknown. This probe is passing a wrong IID; "
                        + "treat the whole run as void and fix it, do NOT record a finding.");
                // A class factory is allowed to refuse IUnknown-first activation, so a failure
                // here is not yet an answer: ask for each internal interface directly before
                // concluding anything.
                foreach (var (interfaceName, iid, _) in Interfaces)
                {
                    var direct = iid;
                    var dhr = CoCreateInstance(in clsid, IntPtr.Zero, ClsctxAll, in direct, out var instance);
                    Console.WriteLine($"    direct activation as {interfaceName}: "
                        + (dhr >= 0 ? "**YES**" : Hresult(dhr)));
                    if (dhr < 0) continue;
                    if (interfaceName == "IWSLCSessionManager")
                    {
                        bound = true;
                        if (callThrough) CallThrough(instance);
                    }
                    Marshal.Release(instance);
                }
                Console.WriteLine();
                continue;
            }
            Console.WriteLine("    activated");

            try
            {
                foreach (var (interfaceName, iid, why) in Interfaces)
                {
                    var iidLocal = iid;
                    var qi = Marshal.QueryInterface(unknown, in iidLocal, out var candidate);
                    if (qi >= 0)
                    {
                        Console.WriteLine($"    QI {interfaceName}: **YES**");
                        if (interfaceName == "IWSLCSessionManager")
                        {
                            bound = true;
                            RaiseImpersonation(candidate);
                            if (callThrough) CallThrough(candidate);
                        }
                        Marshal.Release(candidate);
                    }
                    else
                    {
                        Console.WriteLine($"    QI {interfaceName}: no ({Hresult(qi)})");
                        foreach (var line in Wrap(why)) Console.WriteLine($"        {line}");
                    }
                }
            }
            finally
            {
                Marshal.Release(unknown);
            }
            Console.WriteLine();
        }

        Console.WriteLine(bound
            ? "  RESULT: IWSLCSessionManager IS reachable. D13's internal arm has an entry point —\n"
              + "          write it (ListContainers, OpenSessionByName, OpenContainer, ResizeTty)\n"
              + "          and record the CLSID that answered in §13.2."
            : "  RESULT: IWSLCSessionManager is NOT reachable from any known coclass. D13 needs\n"
              + "          reopening — see §13.2 for the two alternatives. Re-run with\n"
              + "          --internal-call only if a QI above succeeded; it adds nothing here.");
        if (!callThrough && bound)
            Console.WriteLine("  (pass --internal-call to also CALL through it — confirms the vtable "
                + "matches the IDL, and can crash if it does not)");
    }

    /// <summary>
    /// Prove the vtable is really the IDL's, not just that the IID is recognised. A QI can succeed
    /// against an interface whose layout has since moved — `wslc.idl` says outright that breaking
    /// changes to it are fine — and the first thing that would tell us is a crash here rather
    /// than in the broker.
    /// </summary>
    private static void CallThrough(IntPtr manager)
    {
        Console.WriteLine("      calling through (vtable check):");
        IWSLCSessionManager? proxy;
        try
        {
            proxy = (IWSLCSessionManager)Marshal.GetObjectForIUnknown(manager);
        }
        catch (Exception e)
        {
            Console.WriteLine($"        could not build the RCW: {e.GetType().Name}: {e.Message}");
            return;
        }

        try
        {
            var hr = proxy.GetVersion(out var version);
            Console.WriteLine(hr >= 0
                ? $"        GetVersion() = {version.Major}.{version.Minor}.{version.Revision} "
                  + "— slot 3 matches the IDL"
                : $"        GetVersion() failed: {Hresult(hr)}");
            // Cross-check against the compat SDK's own GetVersion: the same service answering
            // both is what says these are two faces of one object rather than a coincidence.
            if (hr >= 0)
                Console.WriteLine("        compare with WslcService.GetVersion() above — they "
                    + "should agree");
        }
        catch (Exception e)
        {
            Console.WriteLine($"        GetVersion() threw {e.GetType().Name}: {e.Message}");
            return;
        }

        try
        {
            var hr = proxy.ListSessions(out var sessions, out var count);
            if (hr < 0)
            {
                Console.WriteLine($"        ListSessions() failed: {Hresult(hr)}");
                return;
            }
            Console.WriteLine($"        ListSessions() = {count} session(s)");
            for (var i = 0; i < count; i++)
            {
                var entry = Marshal.PtrToStructure<WslcSessionListEntry>(
                    sessions + i * Marshal.SizeOf<WslcSessionListEntry>());
                Console.WriteLine($"          #{entry.SessionId} \"{entry.DisplayName}\" "
                    + $"(creator pid {entry.CreatorPid})");
            }
            // The callee allocated it; nobody else will free it.
            if (sessions != IntPtr.Zero) Marshal.FreeCoTaskMem(sessions);
            if (count == 0)
                // Worth saying, because "it returned S_OK" is weaker evidence than it looks: with
                // no entries, the WSLCSessionListEntry layout — two DWORDs then two INLINE
                // wchar_t buffers — was never actually unmarshalled. ListContainers uses the same
                // `size_is(, *Count)` double-pointer shape, so that layout is still unproven.
                Console.WriteLine("        (0 entries, so the ENTRY STRUCT layout is still "
                    + "untested — run with a session up: --session --keep --internal-call)");
        }
        catch (Exception e)
        {
            Console.WriteLine($"        ListSessions() threw {e.GetType().Name}: {e.Message}");
            return;
        }

        ProbeSessionHandoff(proxy);
    }

    /// <summary>
    /// Grant the server permission to impersonate us on this proxy.
    ///
    /// Found the hard way: `OpenSessionByName` failed `0x80070542`
    /// (`HRESULT_FROM_WIN32(1346)` = `ERROR_BAD_IMPERSONATION_LEVEL`) on a session that
    /// `ListSessions` had just listed by name. It is not a "missing" error at all — the service
    /// impersonates the caller to resolve a **per-user** session, and a .NET COM client is handed
    /// `RPC_C_IMP_LEVEL_IDENTIFY` by default, which lets the server check who we are but not act
    /// as us. `GetVersion` and `ListSessions` never impersonate, which is exactly why those two
    /// succeeded and made the failure look like a per-method capability gap.
    ///
    /// `CoSetProxyBlanket` is the surgical fix — it applies to this proxy only and works after
    /// marshalling has already happened. `nucleic-brokerd` can instead call `CoInitializeSecurity`
    /// once at startup, before its first COM call, which covers every proxy it will ever hold;
    /// that is the production shape, and it must come first or it fails `RPC_E_TOO_LATE`.
    /// </summary>
    private static void RaiseImpersonation(IntPtr proxy)
    {
        var hr = CoSetProxyBlanket(
            proxy, RpcCAuthnDefault, RpcCAuthzDefault, ColeDefaultPrincipal,
            RpcCAuthnLevelDefault, RpcCImpLevelImpersonate, ColeDefaultAuthinfo, EoacNone);
        Console.WriteLine(hr >= 0
            ? "      proxy blanket raised to RPC_C_IMP_LEVEL_IMPERSONATE"
            : $"      CoSetProxyBlanket failed: {Hresult(hr)} — per-user calls will likely fail 0x80070542");
    }

    /// <summary>
    /// The question that decides the SHAPE of D13's internal arm.
    ///
    /// `IWSLCSession` (internal, `EF0661E4-…`) and `IWSLCCompatSession` (SDK-facing,
    /// `DD7B2EF9-…`) are different interfaces with different IIDs. `WSLCCompatSessionManager`
    /// already proved one object can wear both faces — so if the session `OpenSessionByName`
    /// returns ALSO answers a QI for the compat interface, then a re-adopted session can be
    /// handed to `Session.FromAbi(ptr)` and driven by the **existing** facade code.
    ///
    /// * **YES** → internal COM is used only to *find* things. Reattach costs one QI, and
    ///   nothing else in `WslcFacade` changes.
    /// * **NO**  → every re-adopted handle is internal-only, and start/stop/delete/exec each need
    ///   a second, hand-marshalled implementation. That is a much bigger arm, and it is the point
    ///   at which the C++/WinRT shim §13.1 mused about earns its keep.
    /// </summary>
    private static void ProbeSessionHandoff(IWSLCSessionManager proxy)
    {
        var name = sessionName;
        if (string.IsNullOrEmpty(name))
        {
            Console.WriteLine("        (no --session-name given; skipping the reattach handoff test)");
            return;
        }

        Console.WriteLine();
        Console.WriteLine($"      reattach handoff test — OpenSessionByName(\"{name}\"):");
        int hr;
        IntPtr session;
        try
        {
            hr = proxy.OpenSessionByName(name, out session);
        }
        catch (Exception e)
        {
            Console.WriteLine($"        threw {e.GetType().Name}: {e.Message}");
            return;
        }

        if (hr < 0)
        {
            Console.WriteLine($"        failed: {Hresult(hr)}");
            // Be specific about WHY, and never blame absence when the cause is security — an
            // earlier version printed "expected if no session of that name is running" directly
            // under a ListSessions that had just printed that session by name.
            Console.WriteLine((uint)hr switch
            {
                ErrorBadImpersonationLevel =>
                    "        → NOT a missing session: the server could not impersonate us. If the "
                    + "blanket was raised above and this still fails, the process needs "
                    + "CoInitializeSecurity(RPC_C_IMP_LEVEL_IMPERSONATE) BEFORE its first COM call.",
                0x8004060F =>
                    "        → WSLC_E_SESSION_NOT_FOUND: no session of that name. Create one: "
                    + "--session --keep --internal-call",
                _ =>
                    "        → unexpected; compare against the ListSessions output above, which "
                    + "says whether the session actually exists.",
            });
            return;
        }

        Console.WriteLine("        opened — the session half of reattach WORKS");
        try
        {
            var compat = IidCompatSession;
            var qi = Marshal.QueryInterface(session, in compat, out var projected);
            if (qi >= 0)
            {
                Console.WriteLine("        QI IWSLCCompatSession: **YES**");
                Console.WriteLine("        → a re-adopted session can be handed to "
                    + "Session.FromAbi() and driven by the EXISTING compat facade code.");
                Console.WriteLine("        → D13's internal arm shrinks to: find, then hand off.");
                ProbeObjectIdentity(session, projected);
                Marshal.Release(projected);
            }
            else
            {
                Console.WriteLine($"        QI IWSLCCompatSession: no ({Hresult(qi)})");
                Console.WriteLine("        → a re-adopted session is internal-only, so every "
                    + "operation on it needs a second hand-marshalled path.");
                Console.WriteLine("        → that is the case where a C++/WinRT shim is worth it.");
            }
        }
        finally
        {
            Marshal.Release(session);
        }
    }

    private static string? sessionName;

    /// <summary>
    /// Why did `Session.FromAbi` throw `InvalidCastException` on a pointer that had just QI'd
    /// successfully to `IWSLCCompatSession`?
    ///
    /// The hypothesis is that there are **three** layers, not two: `wslc.idl` (service-internal
    /// COM), `WSLCCompat.idl` (compat COM), and a **WinRT** layer implemented *client-side* in
    /// `wslcsdk.dll` — which exports `DllGetActivationFactory`, so its `Session` is an in-process
    /// wrapper object that holds compat COM proxies rather than being one. If so, the service's
    /// object can never satisfy `FromAbi`, because the WinRT default interface only ever exists
    /// on the client-side wrapper, and no API turns a compat pointer into one.
    ///
    /// Decisive test, and it needs no knowledge of the WinRT IID: take the `Session` the SDK
    /// itself created, reach through to its underlying native pointer, and compare **COM
    /// identity** (QI for IUnknown — the canonical pointer) against the one `OpenSessionByName`
    /// returned. Same object → the layers coincide and something else broke FromAbi. Different
    /// objects → the wrapper is client-side and the handoff is impossible as designed.
    /// </summary>
    private static void ProbeObjectIdentity(IntPtr internalSession, IntPtr compatFace)
    {
        Console.WriteLine();
        Console.WriteLine("      object identity — is the WinRT Session the SAME object?");
        if (sdkSession is null)
        {
            Console.WriteLine("        (no SDK-created session in this run; pass --session --keep)");
            return;
        }

        IntPtr sdkPtr;
        try
        {
            // CsWinRT keeps the native pointer on IWinRTObject.NativeObject.ThisPtr. Reached by
            // reflection so this spike keeps building even if the projection's shape changes.
            var winrt = sdkSession.GetType().GetInterface("WinRT.IWinRTObject");
            var native = winrt?.GetProperty("NativeObject")?.GetValue(sdkSession);
            var ptr = native?.GetType().GetProperty("ThisPtr")?.GetValue(native);
            if (ptr is not IntPtr value || value == IntPtr.Zero)
            {
                Console.WriteLine("        could not reach IWinRTObject.NativeObject.ThisPtr");
                return;
            }
            sdkPtr = value;
        }
        catch (Exception e)
        {
            Console.WriteLine($"        reflection failed: {e.GetType().Name}: {e.Message}");
            return;
        }

        // COM identity is defined as the IUnknown pointer, so canonicalise both before comparing.
        var iid = IidIUnknown;
        if (Marshal.QueryInterface(sdkPtr, in iid, out var sdkIdentity) < 0) return;
        if (Marshal.QueryInterface(internalSession, in iid, out var internalIdentity) < 0)
        {
            Marshal.Release(sdkIdentity);
            return;
        }

        try
        {
            var same = sdkIdentity == internalIdentity;
            Console.WriteLine($"        SDK Session IUnknown      = 0x{sdkIdentity:X}");
            Console.WriteLine($"        OpenSessionByName IUnknown = 0x{internalIdentity:X}");
            Console.WriteLine($"        → {(same ? "SAME object" : "DIFFERENT objects")}");

            // The other half: does the SDK's own object even implement the compat COM interface?
            var compat = IidCompatSession;
            var qi = Marshal.QueryInterface(sdkPtr, in compat, out var sdkCompat);
            if (qi >= 0) Marshal.Release(sdkCompat);
            Console.WriteLine($"        SDK Session QI IWSLCCompatSession: {(qi >= 0 ? "yes" : "no")}");

            Console.WriteLine(same
                ? "        → the layers coincide; FromAbi failed for some OTHER reason — investigate."
                : "        → the WinRT Session is a CLIENT-SIDE WRAPPER (wslcsdk.dll exports\n"
                  + "          DllGetActivationFactory), so a service-side pointer can never satisfy\n"
                  + "          FromAbi. The 'find, then hand off' shape does NOT work, and a\n"
                  + "          re-adopted session must be driven through internal COM directly.");
        }
        finally
        {
            Marshal.Release(sdkIdentity);
            Marshal.Release(internalIdentity);
        }
    }

    internal static object? sdkSession;

    /// <summary>
    /// Does <c>Session.FromAbi(ptr)</c> take a reference, or borrow the caller's?
    ///
    /// This is not a detail — it is the difference between a leak and a use-after-free, and the
    /// facade will call it on every reattach. `QueryInterface` already handed us one reference;
    /// if `FromAbi` AddRefs as well we must `Release` ours, and if it merely wraps the pointer we
    /// must NOT. Neither CsWinRT's docs nor the IDL say which.
    ///
    /// Measured rather than assumed, by the only reliable means COM offers: `AddRef`/`Release`
    /// return the new count, so an AddRef/Release pair straddling the call reads the delta.
    /// The absolute numbers are meaningless (proxies keep their own counts); the DIFFERENCE is
    /// the answer.
    /// </summary>
    private static void ProbeFromAbiOwnership(IntPtr sessionPtr)
    {
        Console.WriteLine();
        Console.WriteLine("      FromAbi ownership (leak vs. use-after-free):");
        try
        {
            // Baseline: AddRef then Release, reading the count at the peak.
            var before = Marshal.AddRef(sessionPtr);
            Marshal.Release(sessionPtr);

            var projection = global::Microsoft.WSL.Containers.Session.FromAbi(sessionPtr);
            if (projection is null)
            {
                Console.WriteLine("        FromAbi returned null — cannot judge");
                return;
            }

            var after = Marshal.AddRef(sessionPtr);
            Marshal.Release(sessionPtr);

            Console.WriteLine($"        refcount {before} → {after} across FromAbi()");
            Console.WriteLine(after > before
                ? "        → FromAbi ADDS a reference. The facade must Release its QI reference "
                  + "after handing the pointer over, or every reattach leaks the session."
                : "        → FromAbi BORROWS the pointer. The facade must NOT Release its QI "
                  + "reference — the projection depends on it staying alive.");

            // Prove the projection is actually usable, not just constructed: GetImages() is the
            // cheapest read on Session and mutates nothing. If this works, a re-adopted session
            // really is a first-class compat Session.
            try
            {
                var images = projection.GetImages();
                Console.WriteLine($"        projection.GetImages() = {images.Count} image(s) "
                    + "— the re-adopted session is FULLY USABLE through the compat projection");
            }
            catch (Exception e)
            {
                Console.WriteLine($"        projection.GetImages() threw {e.GetType().Name}: "
                    + $"{e.Message} — it projects but does not work; investigate before relying on it");
            }
        }
        catch (Exception e)
        {
            Console.WriteLine($"        FromAbi threw {e.GetType().Name}: {e.Message}");
        }
    }

    private static IEnumerable<string> Wrap(string text)
    {
        var words = text.Split(' ', StringSplitOptions.RemoveEmptyEntries);
        var line = new System.Text.StringBuilder();
        foreach (var word in words)
        {
            if (line.Length + word.Length + 1 > 88 && line.Length > 0)
            {
                yield return line.ToString();
                line.Clear();
            }
            if (line.Length > 0) line.Append(' ');
            line.Append(word);
        }
        if (line.Length > 0) yield return line.ToString();
    }

    private static string Hresult(int code) => (uint)code switch
    {
        0x80040154 => "REGDB_E_CLASSNOTREG — that class is not registered on this machine",
        0x80004002 => "E_NOINTERFACE — the class does not implement it",
        0x80070005 => "E_ACCESSDENIED — registered, but this token may not activate it",
        0x800401F0 => "CO_E_NOTINITIALIZED",
        0x80070542 => "ERROR_BAD_IMPERSONATION_LEVEL — the server needs to impersonate the caller "
            + "and this proxy only grants IDENTIFY. A SECURITY error, not a missing object",
        0x80010119 => "RPC_E_TOO_LATE — CoInitializeSecurity after the first COM call",
        0x8004060F => "WSLC_E_SESSION_NOT_FOUND",
        _ => $"0x{code:X8}",
    };

    // MARK: - Interop

    /// <summary>
    /// `IID_IUnknown`, spelled out. **Do not** reach for `typeof(object).GUID` here — that is the
    /// CLR's type GUID for `System.Object`, not `IID_IUnknown`, and activating with it asks every
    /// class factory for an interface nothing implements. The failure is maximally misleading:
    /// each registered class answers `E_NOINTERFACE` *at activation*, which reads exactly like
    /// "this class does not expose the interface you wanted" and is really "you asked for
    /// gibberish". That mistake produced a false negative on this very probe.
    /// </summary>
    private static readonly Guid IidIUnknown = new("00000000-0000-0000-C000-000000000046");

    /// `IWSLCCompatSession` from WSLCCompat.idl — the SDK-facing session interface, and the one
    /// the C#/WinRT `Session` projection wraps. Distinct from the internal `IWSLCSession`
    /// (`EF0661E4-…`); whether one object answers both is the reattach-handoff question.
    private static readonly Guid IidCompatSession = new("DD7B2EF9-AA01-4F21-8A3A-29D394CBB579");

    private const uint ClsctxAll = 0x17;          // INPROC_SERVER|HANDLER|LOCAL_SERVER|REMOTE_SERVER
    private const uint CoinitMultithreaded = 0;
    private const uint RpcEChangedMode = 0x80010106;
    private const uint ENoInterface = 0x80004002;
    private const uint ErrorBadImpersonationLevel = 0x80070542;

    // CoSetProxyBlanket's "keep the default" sentinels are -1, not 0 — passing 0 for the
    // principal name means "no principal" rather than "default" and fails differently.
    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 static readonly IntPtr ColeDefaultPrincipal = new(-1);
    private static readonly IntPtr ColeDefaultAuthinfo = new(-1);

    [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 CoInitializeEx(IntPtr reserved, uint coInit);

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

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

    /// Matches `WSLCSessionListEntry` in wslc.idl: two 32-bit fields then two INLINE wide-char
    /// buffers (not pointers), which is why these are ByValTStr and why the sizes must be exact.
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    private struct WslcSessionListEntry
    {
        public uint SessionId;
        public uint CreatorPid;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string DisplayName;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 257)] public string Sid;
    }

    /// <summary>
    /// Declared only as far as <c>ListSessions</c> — but every preceding method must still be
    /// declared with the right parameter count, because a vtable is addressed by slot. The
    /// unused ones take <c>IntPtr</c> placeholders for their struct and interface pointers.
    /// <c>PreserveSig</c> throughout, so a failing HRESULT is a value to report rather than an
    /// exception to decode.
    /// </summary>
    [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);
    }
}
