using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Google.Protobuf;
using ZB.MOM.WW.MxGateway.Contracts.Proto;

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

/// <summary>
/// Writes worker frames to a stream with length-prefixed protobuf serialization. Callers enqueue a
/// frame at a <see cref="WorkerFrameWritePriority"/> and then contend for a single write lock; whoever
/// holds the lock drains every queued frame, control frames first, so a reply, fault, or heartbeat is
/// never delayed behind an event backlog — neither in the bytes it writes nor in the flush that
/// delivers them, because the drain flushes at every control-to-event boundary rather than only at the
/// end of the pass. A caller that loses the lock race does not wait for the winner's pass to end: it
/// awaits its own frame's completion racing its lock acquisition, so it returns at the boundary flush
/// that delivered its frame and the acquisition it walks away from is detached rather than dropped
/// (see <see cref="DetachLockWait"/>). The envelope <c>Sequence</c> is stamped by the draining
/// lock-holder at the moment of writing, so the on-wire order and the stamped sequence always agree
/// even under concurrent callers and priority reordering.
/// </summary>
public sealed class WorkerFrameWriter
{
    private sealed class PendingFrame
    {
        /// <summary>Initializes a new instance of the PendingFrame class.</summary>
        /// <param name="envelope">Worker envelope awaiting write.</param>
        /// <param name="priority">Priority class the frame was queued at.</param>
        public PendingFrame(WorkerEnvelope envelope, WorkerFrameWritePriority priority)
        {
            Envelope = envelope;
            IsControl = priority != WorkerFrameWritePriority.Event;
            Completion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
        }

        /// <summary>Gets the worker envelope awaiting write.</summary>
        public WorkerEnvelope Envelope { get; }

        /// <summary>
        /// Gets a value indicating whether this frame was queued as control-plane traffic. Recorded at
        /// construction from the same expression that picks the queue, so the class the drain sees can
        /// never disagree with the queue the frame sits in. The drain uses it to flush and complete
        /// written control frames at the moment it turns to events (see <see cref="DrainQueuedFramesAsync"/>).
        /// </summary>
        public bool IsControl { get; }

        /// <summary>Gets the completion source signaled once the frame has been written or has failed.</summary>
        public TaskCompletionSource<bool> Completion { get; }

        /// <summary>
        /// Set to <c>true</c> by <see cref="DequeueNext"/> — under <c>_gate</c> — at the instant the
        /// draining lock-holder takes ownership of this frame to write it. A cancelled caller
        /// tombstones its frame only while it is still unclaimed, so a claim and a cancel can never
        /// both win: the flag is the interlock between the two. A frame already claimed is mid-write
        /// and can no longer be recalled (see <see cref="WriteAsync(WorkerEnvelope, WorkerFrameWritePriority, CancellationToken)"/>).
        /// Mutated only under <c>_gate</c>.
        /// </summary>
        public bool Claimed;
    }

    private readonly WorkerFrameProtocolOptions _options;
    private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
    private readonly Stream _stream;
    private readonly object _gate = new object();
    private readonly Queue<PendingFrame> _controlFrames = new Queue<PendingFrame>();
    private readonly Queue<PendingFrame> _eventFrames = new Queue<PendingFrame>();

    // Only ever read/written by the current write-lock holder while draining, so no interlock is
    // needed. Starts at 0 and is committed only immediately before the stream write, so the first
    // written frame carries sequence 1 and a per-frame rejection leaves the counter untouched —
    // the next accepted frame reuses the number and the wire sequence stays contiguous.
    private ulong _nextSequence;

    /// <summary>Initializes a new instance of the WorkerFrameWriter class.</summary>
    /// <param name="stream">Stream to write frames to.</param>
    /// <param name="options">Protocol options for frame encoding.</param>
    public WorkerFrameWriter(
        Stream stream,
        WorkerFrameProtocolOptions options)
    {
        _stream = stream ?? throw new ArgumentNullException(nameof(stream));
        _options = options ?? throw new ArgumentNullException(nameof(options));
    }

    /// <summary>Writes a control-priority worker envelope frame to the stream with length prefix.</summary>
    /// <param name="envelope">Worker envelope to write.</param>
    /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
    /// <returns>A task that completes when the frame has been written and flushed.</returns>
    public Task WriteAsync(
        WorkerEnvelope envelope,
        CancellationToken cancellationToken = default)
    {
        return WriteAsync(envelope, WorkerFrameWritePriority.Control, cancellationToken);
    }

    /// <summary>Queues a worker envelope frame for writing at the given priority and drains the queue.</summary>
    /// <param name="envelope">Worker envelope to write.</param>
    /// <param name="priority">Scheduling priority; control frames are written ahead of event frames.</param>
    /// <param name="cancellationToken">Token to cancel waiting for the write lock.</param>
    /// <returns>A task that completes when the frame has been written and flushed.</returns>
    /// <remarks>
    /// Cancellation contract (WRK-22): if the token fires while this call is waiting for the write
    /// lock, the frame is tombstoned so it is never written — unless a draining lock-holder has
    /// already claimed it, in which case the frame may still reach the wire even though this call
    /// observes <see cref="OperationCanceledException"/>. That residual window is by design: blocking
    /// the canceller behind the very write it is abandoning would defeat the point of cancellation.
    /// The abandoned frame's completion gets a fault-observing continuation so a write failure after
    /// the caller unwinds never raises an unobserved-task exception (NEXT-04).
    /// <para>
    /// Latency contract: a control frame's bytes are written, flushed, and its completion
    /// resolved before the events a drain pass writes after it — the delivery point of a heartbeat,
    /// reply, fault, or shutdown ack is never charged for the event backlog behind it. The returned
    /// task resolves at that same instant even for a caller that lost the write-lock race, because
    /// this call awaits its own frame's completion racing the lock acquisition rather than the lock
    /// alone: the completion resolving first returns the caller immediately and hands the outstanding
    /// acquisition to <see cref="DetachLockWait"/>, which keeps its drain responsibility. Without that,
    /// a control frame delivered at a class boundary still reported back only after the winning
    /// drainer finished writing and flushing the entire event backlog behind it.
    /// </para>
    /// </remarks>
    public async Task WriteAsync(
        WorkerEnvelope envelope,
        WorkerFrameWritePriority priority,
        CancellationToken cancellationToken = default)
    {
        if (envelope is null)
        {
            throw new ArgumentNullException(nameof(envelope));
        }

        PendingFrame frame = new PendingFrame(envelope, priority);
        lock (_gate)
        {
            if (priority == WorkerFrameWritePriority.Event)
            {
                _eventFrames.Enqueue(frame);
            }
            else
            {
                _controlFrames.Enqueue(frame);
            }
        }

        // Contend for the single writer, but race that contention against this frame's own completion.
        // Whoever wins the lock drains every currently-queued frame in priority order, so this frame is
        // written by this call or by a concurrent caller that got the lock first — and in the latter
        // case the winner writes, flushes, and completes it at the control-to-event boundary, part-way
        // through its pass. Racing the two is what lets this call return at that instant instead of at
        // the winner's release; the acquisition it then walks away from is detached, never dropped, so
        // no drain responsibility leaves with it.
        Task lockWait = _writeLock.WaitAsync(cancellationToken);
        Task completion = frame.Completion.Task;
        await Task.WhenAny(completion, lockWait).ConfigureAwait(false);

        if (lockWait.Status != TaskStatus.RanToCompletion)
        {
            // This call does not hold the lock: either the completion won the race and the acquisition
            // is still outstanding, or the wait ended without the lock because the token fired. Hand the
            // wait over either way — an acquisition nobody is waiting on must still drain and release,
            // and a wait that ends without the lock must still have its outcome observed.
            DetachLockWait(lockWait);

            if (!completion.IsCompleted)
            {
                // The frame was not delivered, so the wait must have ended in cancellation. Tombstone
                // the queued frame so DequeueNext skips it — but only if a draining lock-holder has not
                // already claimed it. If it is claimed it is mid-write and cannot be recalled; the
                // caller still observes cancellation while the frame reaches the wire (documented
                // above). Rethrow from the wait rather than from the completion, so a claimed frame's
                // canceller is not held behind the very write it is abandoning.
                //
                // The wait ending without the lock IS cancellation in every reachable case — nothing
                // disposes _writeLock — so the tombstone's TrySetCanceled is honest. A hypothetical
                // faulted wait would take this same path and label the frame cancelled instead of
                // faulted; that is internal state only, since the await below rethrows the fault itself
                // to the caller.
                TombstoneIfUnclaimed(frame, cancellationToken);
                await lockWait.ConfigureAwait(false);
            }

            await completion.ConfigureAwait(false);
            return;
        }

        try
        {
            await DrainQueuedFramesAsync().ConfigureAwait(false);
        }
        finally
        {
            _writeLock.Release();
        }

        await completion.ConfigureAwait(false);
    }

    /// <summary>
    /// Queues a whole batch of envelopes at one priority under a single lock acquisition and drains
    /// it, so a burst of frames — the event drain loop's hot path — pays one flush for the batch
    /// rather than one per frame (WRK-25, realizing the WRK-12 coalescing on the path it was built
    /// for). Intra-batch order is preserved because the enqueue is atomic under <c>_gate</c> and each
    /// class queue is FIFO; the control-before-event guarantee still holds because any concurrently
    /// queued control frame is drained ahead of this batch by <see cref="DequeueNext"/>, and — since
    /// the control-frame completion decoupling — is also flushed and completed before this batch's
    /// remaining events are written, so a batch in flight does not delay a control frame's delivery.
    /// Every frame's "written and flushed before completion" contract is unchanged. An event batch that
    /// a control frame cuts into therefore pays one extra flush; an uninterrupted batch still pays
    /// exactly one. Mixed passes are not exotic — command replies are Control priority too, so
    /// sustained command traffic concurrent with event streaming can hit them routinely; the cost
    /// stays bounded at one extra flush per class transition present in the pass.
    /// </summary>
    /// <param name="envelopes">Envelopes to write, in order.</param>
    /// <param name="priority">Scheduling priority for the whole batch.</param>
    /// <param name="cancellationToken">Token to cancel waiting for the write lock.</param>
    /// <returns>A task that completes when every frame in the batch has been written and flushed.</returns>
    /// <remarks>
    /// A per-frame rejection inside the batch (for example one oversized event) surfaces from the
    /// awaited completions as its <see cref="WorkerFrameProtocolException"/>; the remaining frames are
    /// still observed so none faults unobserved. Cancellation while waiting for the lock tombstones
    /// every still-unclaimed frame in the batch, per the WRK-22 contract on
    /// <see cref="WriteAsync(WorkerEnvelope, WorkerFrameWritePriority, CancellationToken)"/>; frames
    /// the cancelled caller abandons (claimed mid-write, or already faulted) get a fault-observing
    /// continuation so a later write failure never raises an unobserved-task exception (NEXT-04).
    /// <para>
    /// This path deliberately keeps the plain wait-then-drain shape rather than the single-frame
    /// path's completion-versus-lock race. A batch caller's result is its whole set of completions,
    /// and the last of those resolves at the end-of-pass flush — the instant before the drainer
    /// releases the lock — so racing the acquisition would buy a batch caller nothing while adding a
    /// detached acquisition per call. Semantics here are unchanged by that race.
    /// </para>
    /// </remarks>
    public async Task WriteBatchAsync(
        IReadOnlyList<WorkerEnvelope> envelopes,
        WorkerFrameWritePriority priority,
        CancellationToken cancellationToken = default)
    {
        if (envelopes is null)
        {
            throw new ArgumentNullException(nameof(envelopes));
        }

        if (envelopes.Count == 0)
        {
            return;
        }

        PendingFrame[] frames = new PendingFrame[envelopes.Count];
        for (int index = 0; index < envelopes.Count; index++)
        {
            WorkerEnvelope envelope = envelopes[index]
                ?? throw new ArgumentException("Batch envelopes must not contain null.", nameof(envelopes));
            frames[index] = new PendingFrame(envelope, priority);
        }

        lock (_gate)
        {
            Queue<PendingFrame> queue = priority == WorkerFrameWritePriority.Event ? _eventFrames : _controlFrames;
            foreach (PendingFrame frame in frames)
            {
                queue.Enqueue(frame);
            }
        }

        try
        {
            await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            TombstoneUnclaimed(frames, cancellationToken);
            throw;
        }

        try
        {
            await DrainQueuedFramesAsync().ConfigureAwait(false);
        }
        finally
        {
            _writeLock.Release();
        }

        // Await every completion so no per-frame rejection faults unobserved, but surface the first
        // failure (in batch order) to the caller — the drain loop maps it back to the offending event.
        Exception? firstFailure = null;
        foreach (PendingFrame frame in frames)
        {
            try
            {
                await frame.Completion.Task.ConfigureAwait(false);
            }
            catch (Exception exception)
            {
                firstFailure ??= exception;
            }
        }

        if (firstFailure is not null)
        {
            ExceptionDispatchInfo.Capture(firstFailure).Throw();
        }
    }

    private void TombstoneIfUnclaimed(PendingFrame frame, CancellationToken cancellationToken)
    {
        lock (_gate)
        {
            if (!frame.Claimed)
            {
                frame.Completion.TrySetCanceled(cancellationToken);
            }
        }

        ObserveAbandonedFault(frame);
    }

    private void TombstoneUnclaimed(PendingFrame[] frames, CancellationToken cancellationToken)
    {
        lock (_gate)
        {
            foreach (PendingFrame frame in frames)
            {
                if (!frame.Claimed)
                {
                    frame.Completion.TrySetCanceled(cancellationToken);
                }
            }
        }

        foreach (PendingFrame frame in frames)
        {
            ObserveAbandonedFault(frame);
        }
    }

    /// <summary>
    ///     Observes any fault on a frame the cancelled caller stops awaiting (NEXT-04). A frame
    ///     claimed by a draining lock-holder — or already faulted by a concurrent
    ///     <c>FailAllQueued</c> — completes on a task nobody awaits after cancellation unwinds the
    ///     caller; a later write failure would then surface as an unobserved-task exception. A
    ///     cancelled task never triggers the faulted continuation, so attaching unconditionally is
    ///     safe. Attached outside <c>_gate</c> because an already-faulted task runs the
    ///     continuation inline.
    /// </summary>
    /// <param name="frame">Frame whose completion may fault without an awaiter.</param>
    private static void ObserveAbandonedFault(PendingFrame frame)
    {
        ObserveFault(frame.Completion.Task);
    }

    /// <summary>
    ///     Attaches the NEXT-04 fault-observing continuation to a task this writer starts and then
    ///     discards. Every such task must carry one: with no awaiter, a fault would otherwise reach
    ///     nobody and resurface as <see cref="TaskScheduler.UnobservedTaskException"/> at finalization
    ///     — a detached, unattributable failure long after the code that caused it. The continuation
    ///     runs only on the faulted path and only touches <see cref="Task.Exception"/>, so it can
    ///     never fault itself; it runs inline because an already-faulted task would otherwise pay a
    ///     scheduling hop to do nothing.
    /// </summary>
    /// <param name="task">Discarded task whose fault would otherwise go unobserved.</param>
    private static void ObserveFault(Task task)
    {
        _ = task.ContinueWith(
            faulted => _ = faulted.Exception,
            CancellationToken.None,
            TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);
    }

    /// <summary>
    ///     Keeps a write-lock acquisition whose caller has stopped waiting for it — its frame was
    ///     delivered inside the winning drainer's pass, or its token fired — from losing the drain
    ///     responsibility that comes with the lock. The wait is never simply dropped: if it goes on to
    ///     acquire the lock, the continuation drains whatever is queued and then releases, so the lock
    ///     is never acquired and silently held, and a frame enqueued between the previous drainer's
    ///     last <see cref="DequeueNext"/> and its release is still written by someone. A wait that ends
    ///     without the lock took no semaphore count and so has nothing to release.
    ///     <para>
    ///     The continuation is queued to the thread pool rather than run inline, because it starts a
    ///     drain pass: running that synchronously would charge whichever thread called <c>Release</c>
    ///     for the next caller's writes.
    ///     </para>
    /// </summary>
    /// <param name="lockWait">Outstanding write-lock acquisition the caller has walked away from.</param>
    private void DetachLockWait(Task lockWait)
    {
        // The continuation task is discarded, so it takes the NEXT-04 fault observer: nothing awaits
        // it, and a throw out of OnDetachedLockWaitSettled would otherwise be an unobserved-task
        // exception raised at finalization rather than an attributable failure here.
        ObserveFault(lockWait.ContinueWith(
            OnDetachedLockWaitSettled,
            CancellationToken.None,
            TaskContinuationOptions.None,
            TaskScheduler.Default));
    }

    /// <summary>
    ///     Settles a detached write-lock acquisition: drain and release if it took the lock, otherwise
    ///     observe its outcome and stop. Acquisition and cancellation are mutually exclusive —
    ///     <see cref="SemaphoreSlim"/> never hands a count to a wait it cancels — so neither branch can
    ///     leak a count, and neither can strand a queued frame: a cancelled wait never held the lock,
    ///     so whatever is queued is still owned by the next caller to acquire it.
    /// </summary>
    /// <param name="lockWait">Settled write-lock acquisition task.</param>
    private void OnDetachedLockWaitSettled(Task lockWait)
    {
        if (lockWait.Status != TaskStatus.RanToCompletion)
        {
            // Cancelled by the originating caller's token, or — only pathologically, a disposed
            // semaphore — faulted. No count was taken, so there is nothing to release and no drain to
            // inherit. Touch Exception so a fault on a task nobody awaits any more cannot surface as an
            // unobserved-task exception.
            _ = lockWait.Exception;
            return;
        }

        // Discarded, so it takes the NEXT-04 fault observer too. DrainDetachedAsync swallows the drain
        // itself, but its release sits in a finally outside that catch: a Release that ever throws (a
        // SemaphoreFullException from some future double-release regression, say) must fail somewhere
        // attributable rather than at finalization.
        ObserveFault(DrainDetachedAsync());
    }

    /// <summary>
    ///     Runs a drain pass under a write lock this writer acquired on behalf of a caller that has
    ///     already returned, then releases it. An empty queue makes <see cref="DrainQueuedFramesAsync"/>
    ///     a no-op, so the common case is acquire-drain-nothing-release; the pass earns its keep for a
    ///     frame enqueued after the previous drainer's last dequeue but before its release.
    /// </summary>
    /// <returns>A task that completes once the drain pass has ended and the lock has been released.</returns>
    private async Task DrainDetachedAsync()
    {
        try
        {
            await DrainQueuedFramesAsync().ConfigureAwait(false);
        }
        catch (Exception)
        {
            // DrainQueuedFramesAsync routes every write and flush failure onto the affected frames'
            // completions, so nothing is expected to escape it. If anything ever does, there is no
            // caller left on this pass to receive it and letting it out would only raise an
            // unobserved-task exception. The release below is the part that must not be skipped.
        }
        finally
        {
            _writeLock.Release();
        }
    }

    // Runs only under _writeLock. Drains control frames before event frames, stamping and writing each.
    // The stream write itself is not cancellable: a frame is written atomically or fails, never left
    // half-written on the pipe because a caller gave up waiting.
    //
    // Flushes are coalesced within a priority class rather than blindly across the whole pass (WRK-12 /
    // IPC-15, narrowed by the control-frame completion decoupling): each frame is written to the stream
    // but not flushed individually, and one FlushAsync runs at the end of the pass — plus one at each
    // control-to-event boundary, which flushes and completes the control frames written so far before
    // the event backlog behind them is written, instead of after it. Without that boundary flush the
    // priority scheduler only got control *bytes* out early: their delivery point, and every waiting
    // caller's completion, still sat behind up to a full event batch.
    //
    // A caller's Completion therefore still signals only after its bytes have been written AND flushed —
    // the contract is unchanged, the moment it is reached simply stops being pinned to the end of the
    // pass. Cost is bounded: a pure-event pass (the event hot path) still pays exactly one flush, a burst
    // of control frames still pays one for the whole burst, and only a pass that actually mixes both
    // classes pays a second — never one flush per control frame, which is the syscall-per-heartbeat cost
    // WRK-12 removed.
    private async Task DrainQueuedFramesAsync()
    {
        List<PendingFrame> written = new List<PendingFrame>();
        bool writtenHoldsControl = false;
        while (true)
        {
            PendingFrame? frame = DequeueNext();
            if (frame is null)
            {
                break;
            }

            if (writtenHoldsControl && !frame.IsControl)
            {
                // Class transition: the frames written so far include at least one control frame whose
                // caller is waiting on delivery. Flush and complete them here rather than parking them
                // behind the events this pass is about to write. Charged once per transition, not once
                // per control frame. Event frames already in the list ride along — they too are written
                // and now flushed, so completing them early is the same contract, earlier.
                try
                {
                    await _stream.FlushAsync(CancellationToken.None).ConfigureAwait(false);
                }
                catch (Exception exception)
                {
                    // Same shape as the end-of-pass flush failure: the bytes reached the stream but the
                    // flush that guarantees delivery failed, so the pipe is broken. Fail the frame just
                    // claimed (it is out of its queue and nothing else will ever complete it), every
                    // written-but-unflushed frame, and everything still queued, then stop draining.
                    frame.Completion.TrySetException(exception);
                    FailFrames(written, exception);
                    FailAllQueued(exception);
                    return;
                }

                // Completed frames leave the list, so a later failure in this pass cannot fail them.
                CompleteFrames(written);
                written.Clear();
                writtenHoldsControl = false;
            }

            try
            {
                await WriteFrameAsync(frame.Envelope).ConfigureAwait(false);
                written.Add(frame);
                writtenHoldsControl |= frame.IsControl;
            }
            catch (WorkerFrameProtocolException exception) when (IsPerFrameRejection(exception))
            {
                // Validation, empty-payload, and oversized-frame errors are specific to this frame and
                // do not damage the stream; fail only this frame and keep draining the rest. Nothing was
                // written for it, so it needs no flush.
                frame.Completion.TrySetException(exception);
            }
            catch (Exception exception)
            {
                // A stream write failure means the pipe is broken; fail this frame, every frame already
                // written this batch but not yet flushed, and every frame still queued so no caller
                // awaits forever, then stop draining.
                frame.Completion.TrySetException(exception);
                FailFrames(written, exception);
                FailAllQueued(exception);
                return;
            }
        }

        if (written.Count == 0)
        {
            return;
        }

        try
        {
            await _stream.FlushAsync(CancellationToken.None).ConfigureAwait(false);
        }
        catch (Exception exception)
        {
            // The batch reached the stream but the flush that guarantees delivery failed: the pipe is
            // broken. Fail every frame still in the batch (the queue was already drained) so no caller
            // treats an unflushed write as delivered. Frames a boundary flush already completed are not
            // in the list — their bytes were flushed, so this later failure does not reach back to them.
            FailFrames(written, exception);
            return;
        }

        CompleteFrames(written);
    }

    private static void CompleteFrames(List<PendingFrame> frames)
    {
        foreach (PendingFrame frame in frames)
        {
            frame.Completion.TrySetResult(true);
        }
    }

    private static void FailFrames(List<PendingFrame> frames, Exception exception)
    {
        foreach (PendingFrame frame in frames)
        {
            frame.Completion.TrySetException(exception);
        }
    }

    private static bool IsPerFrameRejection(WorkerFrameProtocolException exception)
    {
        return exception.ErrorCode is WorkerFrameProtocolErrorCode.InvalidEnvelope
            or WorkerFrameProtocolErrorCode.MessageTooLarge
            or WorkerFrameProtocolErrorCode.ProtocolVersionMismatch
            or WorkerFrameProtocolErrorCode.SessionMismatch;
    }

    // Returns the next frame to write, control frames first, skipping any frame a cancelled caller
    // tombstoned while it waited for the lock (WRK-22). The frame actually returned is marked Claimed
    // under _gate in the same critical section that checks the tombstone, so a claim and a concurrent
    // cancel are mutually exclusive: whichever acquires _gate first wins.
    private PendingFrame? DequeueNext()
    {
        lock (_gate)
        {
            while (_controlFrames.Count > 0)
            {
                PendingFrame frame = _controlFrames.Dequeue();
                if (frame.Completion.Task.IsCanceled)
                {
                    continue;
                }

                frame.Claimed = true;
                return frame;
            }

            while (_eventFrames.Count > 0)
            {
                PendingFrame frame = _eventFrames.Dequeue();
                if (frame.Completion.Task.IsCanceled)
                {
                    continue;
                }

                frame.Claimed = true;
                return frame;
            }

            return null;
        }
    }

    private void FailAllQueued(Exception exception)
    {
        lock (_gate)
        {
            while (_controlFrames.Count > 0)
            {
                _controlFrames.Dequeue().Completion.TrySetException(exception);
            }

            while (_eventFrames.Count > 0)
            {
                _eventFrames.Dequeue().Completion.TrySetException(exception);
            }
        }
    }

    private async Task WriteFrameAsync(WorkerEnvelope envelope)
    {
        WorkerEnvelopeValidator.Validate(envelope, _options);

        // Stamp the sequence at the actual point of writing, under the write lock, so the wire order
        // and the stamped sequence agree regardless of caller concurrency or priority.
        //
        // Peek-stamp-commit (WRK-23): the sequence participates in CalculateSize() (varint width),
        // so it must be stamped before the size checks — but a per-frame rejection must not burn a
        // number, or the wire shows phantom gaps that an operator reads as lost frames. Stamp a
        // candidate, validate the stamped envelope, and commit the counter only once the frame is
        // certain to be written.
        ulong candidateSequence = unchecked(_nextSequence + 1);
        envelope.Sequence = candidateSequence;

        int payloadLength = envelope.CalculateSize();
        if (payloadLength == 0)
        {
            throw new WorkerFrameProtocolException(
                WorkerFrameProtocolErrorCode.InvalidEnvelope,
                "Worker envelope cannot serialize to an empty payload.");
        }

        if (payloadLength > _options.MaxMessageBytes)
        {
            throw new WorkerFrameProtocolException(
                WorkerFrameProtocolErrorCode.MessageTooLarge,
                $"Worker envelope payload length {payloadLength} exceeds the configured maximum of {_options.MaxMessageBytes} bytes.");
        }

        _nextSequence = candidateSequence;

        // Serialize once into a single pooled buffer that carries the 4-byte length prefix followed
        // by the payload, then issue one stream write. This avoids a second serialization pass, a
        // separate prefix array, a separate prefix write, and any per-frame heap allocation — the
        // gateway-side writer's GWC-30 shape, now matched on the net48 side. The rented buffer may
        // be larger than requested, so only the first frameLength bytes are ever written. The buffer
        // is returned only after this frame's write has completed; the batch flush deferred to the
        // end of the drain (see DrainQueuedFramesAsync) does not read from it.
        int frameLength = sizeof(uint) + payloadLength;
        byte[] frame = ArrayPool<byte>.Shared.Rent(frameLength);
        try
        {
            WriteUInt32LittleEndian(frame, (uint)payloadLength);
            envelope.WriteTo(new Span<byte>(frame, sizeof(uint), payloadLength));

            await _stream.WriteAsync(frame, 0, frameLength, CancellationToken.None).ConfigureAwait(false);
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(frame);
        }
    }

    private static void WriteUInt32LittleEndian(
        byte[] buffer,
        uint value)
    {
        buffer[0] = (byte)value;
        buffer[1] = (byte)(value >> 8);
        buffer[2] = (byte)(value >> 16);
        buffer[3] = (byte)(value >> 24);
    }
}
