using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;

namespace ZB.MOM.WW.MxGateway.Worker.Sta;

/// <summary>
///     Message-aware blocking wait shared by STA code that must sit idle without
///     starving the Windows message queue. <see cref="StaMessagePump"/> owns the
///     runtime's outer idle wait; this helper serves the inner waits performed
///     by commands that already hold the STA — the write-completion wait and the
///     ReadBulk first-value wait — which need the same "wake on a signal or on
///     an inbound message" behavior without owning a pump instance.
/// </summary>
/// <remarks>
///     Deliberately separate from <see cref="StaMessagePump"/> rather than a new
///     method on it: the two caches that use this are plain per-session objects
///     with no pump dependency (they take the pump step as an injected delegate
///     so tests can drive them off the STA). The wait itself is not duplicated —
///     both this and the pump call the single declaration in
///     <see cref="StaNativeMethods"/>.
/// </remarks>
internal static class StaWaitHelper
{
    /// <summary>
    ///     Ceiling on the fallback tick — the longest a wait may block when
    ///     nothing wakes it. A caller's poll interval is clamped to this, so a
    ///     caller's pump step keeps running at least this often no matter what
    ///     interval it asked for, even on a thread that never receives a message
    ///     and whose signal is never set.
    /// </summary>
    internal const int MaxFallbackTickMilliseconds = 50;

    private static int waitFailureReported;

    /// <summary>
    ///     Blocks the calling thread until <paramref name="signal"/> is set, a
    ///     Windows message is available to pump, or the timeout elapses —
    ///     whichever comes first. Unlike <see cref="Thread.Sleep(int)"/> this
    ///     never blinds the thread to inbound COM event messages: the wait
    ///     returns the instant one arrives, so the caller can dispatch it.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         <em>Caller contract: the pump step must drain the message queue
    ///         to empty on every call.</em> The wait uses
    ///         <see cref="StaNativeMethods.MwmoInputAvailable"/>, which reports
    ///         input that is merely <em>present</em> — including input an earlier
    ///         <c>PeekMessage</c> has already seen but not removed. A caller
    ///         whose pump step drains only part of the queue, or does nothing at
    ///         all, therefore leaves a message that satisfies the wake condition
    ///         forever: every wait returns immediately, and the caller's poll
    ///         loop degenerates into a busy loop that spins the thread at 100%
    ///         until its deadline. That is a CPU fault, not a correctness one —
    ///         the loop still honors its deadline and return shape — but it must
    ///         not ship. Drain with <c>PeekMessage(..., PM_REMOVE)</c> in a loop
    ///         until it returns false, exactly as
    ///         <see cref="StaMessagePump.PumpPendingMessages"/> does.
    ///     </para>
    ///     <para>
    ///         The wait does not <c>DangerousAddRef</c> the handle. That is
    ///         sound only because the callers never dispose their wait handles —
    ///         see the coupled invariant documented on the caches' signal fields.
    ///     </para>
    /// </remarks>
    /// <param name="signal">Wait handle set by whoever produces the awaited result.</param>
    /// <param name="timeoutMilliseconds">Maximum wait; values at or below zero return immediately.</param>
    internal static void WaitForSignalOrMessages(
        WaitHandle signal,
        int timeoutMilliseconds)
    {
        if (signal is null)
        {
            throw new ArgumentNullException(nameof(signal));
        }

        if (timeoutMilliseconds <= 0)
        {
            return;
        }

        SafeWaitHandle safeHandle = signal.SafeWaitHandle;
        IntPtr[] handles = [safeHandle.DangerousGetHandle()];
        uint result = StaNativeMethods.MsgWaitForMultipleObjectsEx(
            (uint)handles.Length,
            handles,
            (uint)timeoutMilliseconds,
            StaNativeMethods.QsAllInput,
            StaNativeMethods.MwmoInputAvailable);

        // Read before anything else can run another P/Invoke and overwrite the
        // thread's last-error slot.
        int lastError = result == StaNativeMethods.MsgWaitFailed
            ? Marshal.GetLastWin32Error()
            : 0;

        // The handle is a field of a live caller object, so it cannot be
        // collected mid-call; KeepAlive states that dependency explicitly
        // rather than relying on it.
        GC.KeepAlive(signal);

        if (result == StaNativeMethods.MsgWaitFailed)
        {
            ReportWaitFailureOnce(lastError);

            // Degrade to the pre-existing blind sleep instead of throwing or
            // hot-spinning: the caller is a bounded, deadline-driven poll loop,
            // and a failed wait must not turn a timed-out write into a fault.
            Thread.Sleep(timeoutMilliseconds);
        }
    }

    /// <summary>
    ///     Computes the wait slice for one iteration of a deadline-driven poll
    ///     loop: never past the deadline, never longer than the caller's poll
    ///     interval, and never longer than <see cref="MaxFallbackTickMilliseconds"/>.
    /// </summary>
    /// <param name="remaining">Time left before the caller's absolute deadline.</param>
    /// <param name="pollIntervalMilliseconds">The caller's requested pump cadence.</param>
    /// <returns>Milliseconds to wait; zero or less when the deadline has passed.</returns>
    internal static int ClampWaitMilliseconds(
        TimeSpan remaining,
        int pollIntervalMilliseconds)
    {
        int tick = pollIntervalMilliseconds < 1
            ? 1
            : Math.Min(pollIntervalMilliseconds, MaxFallbackTickMilliseconds);

        double remainingMilliseconds = remaining.TotalMilliseconds;
        if (remainingMilliseconds <= 0d)
        {
            return 0;
        }

        return remainingMilliseconds < tick
            ? (int)Math.Ceiling(remainingMilliseconds)
            : tick;
    }

    /// <summary>
    ///     Traces the first wait failure of the process and stays silent after
    ///     that.
    /// </summary>
    /// <remarks>
    ///     The degrade path is silent by design — a failed wait still honors the
    ///     caller's deadline — but a <em>permanently</em> failing wait means every
    ///     completion wait has quietly reverted to the sleep-polled behavior this
    ///     class exists to replace, and nothing else would ever say so. One trace
    ///     makes that discoverable; latching it keeps a repeating failure from
    ///     flooding the trace listener from inside a poll loop.
    /// </remarks>
    /// <param name="lastError">The Win32 error captured immediately after the failed wait.</param>
    private static void ReportWaitFailureOnce(int lastError)
    {
        if (Interlocked.Exchange(ref waitFailureReported, 1) != 0)
        {
            return;
        }

        Trace.TraceWarning(
            "MsgWaitForMultipleObjectsEx failed in the worker STA completion wait (Win32 error {0}). "
            + "The wait has degraded to blind sleeping: deadlines are still honored, but inbound "
            + "MXAccess COM events will only dispatch at poll-tick granularity. Reported once per process.",
            lastError);
    }
}
