using System.Buffers;
using System.Buffers.Binary;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace AyCode.Core.Serializers.Binaries;

/// <summary>
/// Binary output that writes to a PipeWriter chunk-by-chunk via the PipeWriter's natural slabbing.
/// Two wire-format modes are supported, selected by the <c>multiMessage</c> ctor flag:
///
/// <list type="bullet">
///   <item><b><c>multiMessage = false</c></b> (single-message): pure AcBinary bytes are written into
///   the PipeWriter's slabs and committed via Advance — no per-chunk header bytes appear on the
///   wire. Bit-compatible with the single-shot <c>Serialize(value, opts) → byte[]</c> output.
///   The receiver can deserialize the byte stream as-is (e.g. via
///   <c>AcBinaryDeserializer.Deserialize(byte[])</c> after collecting, or any raw
///   <c>PipeReader</c>-based path). End-of-message = end-of-stream; caller signals it by closing
///   the writer (<c>pipeWriter.CompleteAsync()</c>).</item>
///
///   <item><b><c>multiMessage = true</c></b> (default): each chunk gets a 3-byte header
///   <c>[201][UINT16 size][data]</c>. The header is reserved at the start of each acquired slab;
///   the serializer writes data after it, and on commit the size is patched and the full chunk
///   is Advanced (zero-copy). At end-of-serialize, <see cref="Flush"/> writes a <c>[202]</c>
///   CHUNK_END marker (symmetrical with <c>[201]</c>) — this signals end-of-MESSAGE to the
///   receiver, which auto-resets its <see cref="AsyncPipeReaderInput"/> for the next message
///   on the same long-lived stream. Used by SignalR's <c>BinaryProtocolMode.AsyncSegment</c>
///   and any custom multi-message protocol over a long-lived transport.</item>
/// </list>
///
/// <para><b>Backpressure modes</b> (controlled by <see cref="FlushPolicy"/>) — independent of framing:</para>
/// <list type="bullet">
///   <item><see cref="FlushPolicy.PerChunk"/>: Grow() commits → flushes → awaits → acquires next.
///   <b>Peak memory:</b> ~chunk_size × 1. <b>Pro:</b> strictly bounded peak memory.
///   <b>Con:</b> no producer/flush parallelism. Auto-applied on Stream-backed PipeWriter
///   regardless of chosen policy.</item>
///   <item><see cref="FlushPolicy.DoubleBuffered"/> (default): fire-and-forget previous flush; next
///   Grow waits only if previous flush hasn't completed. <b>Peak memory:</b> ~chunk_size × 2.
///   <b>Pro:</b> max producer/flush parallelism with bounded memory.
///   <b>Con:</b> slow consumer blocks producer at next Grow (bounded by <c>flushTimeout</c>).</item>
///   <item><see cref="FlushPolicy.Coalesced"/>: chunks accumulate while a flush is in-flight; on
///   the per-window safety threshold (~64 KB), the producer waits for the prior flush, then fires
///   one batched flush covering the whole window and resets the window-counter. <b>Peak memory:</b>
///   ~64 KB per window. <b>Pro:</b> dramatically fewer FlushAsync syscalls (one per ~64 KB window
///   instead of one per chunk) → major throughput win on transports with non-trivial flush overhead.
///   <b>Con:</b> per-window peak ~64 KB; under heavy backpressure may fall back to an owned buffer,
///   losing zero-copy for that chunk.</item>
/// </list>
///
/// <para><b>Flush strategy</b> auto-selects on writer type — Stream-backed PipeWriters
/// (<c>PipeWriter.Create(Stream)</c>) run sequentially per chunk because of the
/// <c>StreamPipeWriter._tailMemory</c> reset race; Pipe-based and Kestrel transport writers
/// keep parallelism. Orthogonal to the framing flag: all four (framed/raw) × (sequential/parallel)
/// combinations work.</para>
///
/// <para><b>Timeout safety</b>: every synchronous flush-await is bounded by <c>flushTimeout</c>
/// (default <see cref="System.Threading.Timeout.InfiniteTimeSpan"/> when the type is used directly;
/// <see cref="AcBinaryHubProtocol"/> passes 10 s from its options). A <see cref="TimeoutException"/>
/// propagates to the caller, allowing the connection to abort instead of blocking forever.</para>
///
/// <para>Maximum chunk data size (in framed mode): 65535 bytes (UINT16 max).</para>
///
/// <para><b>When chunked-streaming is the right fit</b> (vs raw <c>byte[]</c> output via
/// <see cref="AcBinarySerializer.Serialize{T}(T, AcBinarySerializerOptions)"/>):</para>
/// <list type="bullet">
///   <item><b>Network transports</b> — TCP / UDP / WebSocket / SSE / HTTP/2 streams. Per-chunk
///         CPU overhead is invisible next to ms-scale RTT; pipeline parallelism lets sender,
///         transport, and receiver work in parallel on different parts of the message.</item>
///   <item><b>Multi-connection servers</b> — Kestrel-style (SignalR), gRPC / proprietary RPC.
///         Per-connection peak memory bounded by chunk-size; LOH allocation pressure (≥ 85 KB
///         messages) is avoided.</item>
///   <item><b>Message brokers / queues</b> — Kafka / Redis Streams / Service Bus clients exposing
///         <see cref="System.Buffers.IBufferWriter{T}"/> or <c>PipeWriter</c> sinks. Streaming
///         serialize writes directly into the transport buffer.</item>
///   <item><b>File streaming</b> — <c>FileStream</c>-backed <see cref="System.IO.Pipelines.PipeWriter"/>.
///         100 MB+ payloads to disk with constant peak memory.</item>
///   <item><b>In-memory cross-thread <see cref="System.IO.Pipelines.Pipe"/></b> — producer thread
///         serialises while consumer thread deserialises in parallel.</item>
///   <item><b>Custom transport adapters</b> — anything where the application owns a
///         <c>PipeWriter</c> or <c>IBufferWriter</c> sink and wants incremental output.</item>
/// </list>
///
/// <para><b>When raw <c>byte[]</c> output is the right fit</b>: same-process loopback IPC where
/// transport latency is near zero, single batch-style operations where peak memory is not a
/// constraint, sub-LOH messages (&lt; 85 KB) with no GC-pressure concerns. The chunked-streaming
/// per-chunk CPU overhead is fully visible in these scenarios — raw is faster end-to-end. Pick the
/// chunked path only when the transport stage is non-trivial (network, file, cross-thread queue).</para>
/// </summary>
public struct AsyncPipeWriterOutput : IBinaryOutputBase
{
    /// <summary>MsgAsyncChunkData type marker (201).</summary>
    private const byte ChunkDataMarker = 201;

    /// <summary>MsgAsyncChunkEnd marker (202) — written at end-of-serialize in framed mode.</summary>
    private const byte ChunkEndMarker = 202;

    /// <summary>Header size: 1 byte type + 2 bytes UINT16 size.</summary>
    private const int HeaderSize = 3;

    /// <summary>Maximum chunk data size (UINT16 max).</summary>
    public const int MaxChunkSize = ushort.MaxValue;

    /// <summary>
    /// Cached <see cref="StreamPipeWriterType"/> runtime type, discovered via the public
    /// <see cref="PipeWriter.Create(Stream, StreamPipeWriterOptions)"/> factory at class-load
    /// time (no magic strings, no reflection lookup, refactor-safe — if MS ever renames the
    /// internal type, this auto-tracks). The dummy instance is unreachable after class init
    /// and GC-collected; the static field retains only the <see cref="Type"/> reference.
    ///
    /// <para><b>internal</b> visibility — exposed for sanity-check tests in
    /// <c>AyCode.Core.Tests</c> (verifies <c>new Pipe().Writer.GetType() != StreamPipeWriterType</c>
    /// and that the runtime detect can never accidentally fire on Pipe-based writers).</para>
    /// </summary>
    internal static readonly Type StreamPipeWriterType = PipeWriter.Create(Stream.Null).GetType();

    private readonly PipeWriter _pipeWriter;
    private readonly int _chunkSize;
    private readonly bool _multiMessage;
    private readonly FlushPolicy _flushPolicy;
    private readonly bool _serializeFlushAndAcquire;
    private readonly TimeSpan _flushTimeout;
    // Cumulative total of data bytes committed since Initialize. Used by GetTotalPosition for the
    // Position property and the Serialize* methods' return value. Never resets mid-Serialize.
    private int _committedBytes;
    // Window-local counter of data bytes committed since the last FlushAsync was fired (Coalesced
    // mode safety-net). Resets when a new FlushAsync starts, so each flush window starts from 0
    // and the safety-net trip-point (~64 KB) bounds peak in-flight buffer per window — not the
    // total payload size. Without this, Coalesced would degrade to per-chunk-sync behavior after
    // the first ~15 chunks of any multi-MB payload.
    private int _unflushedBytes;
    private int _currentChunkStart;
    // Whether the current chunk's buffer is the owned ArrayPool fallback (true) or a zero-copy
    // PipeWriter slab (false). Used by CommitCurrentChunk to pick the commit strategy.
    private bool _hasOwnedBuffer;
    // Lazy-allocated, long-lived ArrayPool buffer used as the fallback when PipeWriter.GetMemory
    // returns non-array-backed memory (custom non-byte[] PipeWriter — practically never on
    // production writers). Reused across chunks within a single serialize lifecycle; returned
    // to the pool exactly once in Flush(). Stays null entirely on the zero-copy path.
    private byte[]? _ownedBuffer;
    private ValueTask<FlushResult> _lastFlush;

    /// <summary>
    /// Static diagnostic sink for sender-side state inspection. <c>null</c> by default — set
    /// from tests to capture <c>AcquireChunk</c> / <c>CommitCurrentChunk</c> events with full
    /// segment + bookkeeping values. <see cref="EmitDiagnostic"/> is <see cref="ConditionalAttribute"/>-
    /// decorated, so call sites are removed in RELEASE (zero runtime cost).
    /// </summary>
    public static Action<string>? DiagnosticLog;

    [Conditional("DEBUG")]
    private static void EmitDiagnostic(string message) => DiagnosticLog?.Invoke(message);

    /// <summary>Creates an output bound to the given PipeWriter — multi-message or single-message mode per <paramref name="multiMessage"/>.</summary>
    /// <param name="pipeWriter">Target pipe (typically Kestrel's transport output for SignalR, NamedPipe, FileStream, or any custom <see cref="PipeWriter"/>).</param>
    /// <param name="chunkSize">Per-chunk data size (max <see cref="MaxChunkSize"/>). Default 4 KB matches Kestrel's slab size.</param>
    /// <param name="multiMessage"><c>true</c> (default) → multi-message wire format: <c>[201][UINT16][data]</c> per-chunk header
    /// + <c>[202]</c> end-of-message marker on Flush. Receiver auto-resets between messages.
    /// <c>false</c> → single-message: raw AcBinary bytes only, byte-compatible with the single-shot <c>byte[]</c> output;
    /// caller signals end-of-message by closing the writer. See class summary.</param>
    /// <param name="flushPolicy">See class summary — strictly bounded (<see cref="FlushPolicy.PerChunk"/>),
    /// double-buffered (<see cref="FlushPolicy.DoubleBuffered"/>, default), or coalesced
    /// (<see cref="FlushPolicy.Coalesced"/>).</param>
    /// <param name="flushTimeout">Per-flush timeout. <c>null</c> → <see cref="System.Threading.Timeout.InfiniteTimeSpan"/>
    /// (wait forever — legacy behavior). Pass a positive value to fail fast on stuck consumers.</param>
    public AsyncPipeWriterOutput(PipeWriter pipeWriter, int chunkSize = 4096, bool multiMessage = true, FlushPolicy flushPolicy = FlushPolicy.DoubleBuffered, TimeSpan? flushTimeout = null)
    {
        if (chunkSize > MaxChunkSize)
            throw new ArgumentOutOfRangeException(nameof(chunkSize), chunkSize, $"Chunk size cannot exceed {MaxChunkSize} (UINT16 max).");

        _pipeWriter = pipeWriter;
        _chunkSize = chunkSize;
        _multiMessage = multiMessage;
        _flushPolicy = flushPolicy;

        // null → Timeout.InfiniteTimeSpan ("wait forever" — natively supported by Task.Wait as -1ms).
        // A positive value enables bounded waiting; on timeout a TimeoutException propagates to the caller.
        _flushTimeout = flushTimeout ?? Timeout.InfiniteTimeSpan;

        // StreamPipeWriter (PipeWriter.Create(Stream)) resets internal _tailMemory to default
        // at FlushAsync completion — racing with the AcquireChunk-during-flush parallelism this
        // class deliberately uses. For Stream-backed writers, fully await the just-started flush
        // before acquiring the next chunk's memory (the writer-correct usage pattern; flush is
        // a real I/O operation here). Pipe-based writers (Kestrel transport, SignalR) do NOT
        // reset state on flush completion → the parallelism feature stays intact for them.
        //
        // IsAssignableFrom (not ==) so any future BCL subclass of StreamPipeWriter automatically
        // picks the safe sequential path — forward-compat, no silent breakage on .NET upgrades.
        _serializeFlushAndAcquire = StreamPipeWriterType.IsInstanceOfType(pipeWriter);

        _committedBytes = 0;
        _unflushedBytes = 0;
        _hasOwnedBuffer = false;
        _ownedBuffer = null;
        _lastFlush = default;
    }

    /// <summary>
    /// Synchronously awaits a FlushAsync ValueTask.
    /// Fast-path: if already completed, returns without Task allocation.
    /// Slow-path: blocks with <see cref="_flushTimeout"/> — throws <see cref="TimeoutException"/>
    /// if the flush does not complete within it (guards against slow/stuck/disconnected consumers).
    /// <see cref="System.Threading.Timeout.InfiniteTimeSpan"/> means "wait forever" (natively supported by <c>Task.Wait</c>).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void SyncAwaitFlush(ValueTask<FlushResult> vt)
    {
        if (vt.IsCompletedSuccessfully) return;

        var task = vt.AsTask();

        if (!task.Wait(_flushTimeout))
            throw new TimeoutException($"PipeWriter.FlushAsync exceeded {_flushTimeout.TotalSeconds:F1}s — " + "consumer may be too slow, stuck, or disconnected.");

        // Completed within timeout — propagate any faulted exception
        task.GetAwaiter().GetResult();
    }

    /// <summary>
    /// Provides the initial buffer from the PipeWriter — in framed mode, the buffer's first 3
    /// bytes are reserved for the chunk header (filled at <see cref="CommitCurrentChunk"/>);
    /// in raw mode, no reservation, the data starts at <c>position = 0</c>.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Initialize(out byte[] buffer, out int position, out int bufferEnd)
    {
        _committedBytes = 0;
        _unflushedBytes = 0;
        _lastFlush = default;

        AcquireChunk(_chunkSize, out buffer, out position, out bufferEnd);
        _currentChunkStart = position;
    }

    /// <summary>
    /// Called when the context's buffer is full. Commits the chunk to the PipeWriter (in framed
    /// mode, patches the <c>[201][UINT16 size]</c> header before Advance; in raw mode, simply
    /// Advances the data bytes), then fires a background flush and acquires the next chunk.
    /// </summary>
    [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
    public void Grow(ref byte[] buffer, ref int position, ref int bufferEnd, int needed)
    {
        if (_serializeFlushAndAcquire || _flushPolicy == FlushPolicy.PerChunk)
        {
            // SEQUENTIAL path — fully synchronous per chunk: commit → flush → await → acquire.
            // Triggered by either:
            //   - Stream-backed PipeWriter (NamedPipe / FileStream / NetworkStream / etc.) — auto,
            //     forced by the BCL StreamPipeWriter._tailMemory reset race.
            //   - FlushPolicy.PerChunk on a Pipe-based writer — explicit caller choice for
            //     strictly bounded peak memory (~chunk_size × 1).
            // _committedBytes throttling doesn't apply — sequentiality is enforced per chunk.
            CommitCurrentChunk(buffer, position);
            SyncAwaitFlush(_pipeWriter.FlushAsync());
        }
        else
        {
            // PARALLEL paths (Pipe-based writer + DoubleBuffered or Coalesced) — serializer writes
            // the next chunk into the PipeWriter's buffer concurrently with the background FlushAsync.
            //   FlushPolicy.DoubleBuffered: wait at next Grow if the previous parallel flush hasn't
            //     completed yet → max two chunks in flight (~chunk_size × 2 peak memory).
            //   FlushPolicy.Coalesced: skip the per-chunk wait — bytes accumulate in the PipeWriter's
            //     buffer until _unflushedBytes approaches the Pipe's PauseWriterThreshold (~64 KB).
            //     At that point we wait for the prior flush, then fire ONE flush that batches the
            //     entire window. After firing, _unflushedBytes resets so the next window can begin.
            //     This produces ~64 KB-sized flush windows instead of per-chunk flushes — fewer
            //     syscalls, better network throughput.
            // The conditional FlushAsync at the end avoids double-flush if the previous flush
            // is still in progress (Coalesced skip path — keep accumulating).
            if ((_flushPolicy == FlushPolicy.DoubleBuffered && !_lastFlush.IsCompleted) || _unflushedBytes > MaxChunkSize - _chunkSize) SyncAwaitFlush(_lastFlush);

            CommitCurrentChunk(buffer, position);

            if (_lastFlush.IsCompleted)
            {
                // The accumulated unflushed bytes are about to be moved into this flush — reset the
                // window-counter so the next flush window starts fresh. Without this reset, the
                // safety-net would trip permanently after the first ~15 chunks of a multi-MB payload,
                // degrading Coalesced to per-chunk-sync behavior. _committedBytes (cumulative total
                // for GetTotalPosition) is intentionally unchanged — only the window-counter resets.
                _unflushedBytes = 0;
                _lastFlush = _pipeWriter.FlushAsync();
            }
        }

        // Acquire new chunk with header reservation (common to both paths).
        //
        // Unit alignment: `needed` is the data-byte count requested by the caller (via EnsureCapacity);
        // `_chunkSize` is the chunk-on-wire total size (header + data) — same units AcquireChunk's
        // `requestSize` parameter expects. Convert `needed` to the chunk-on-wire scale (`+ headerOffset`)
        // before the Max() so apples-to-apples. AcquireChunk then subtracts `headerOffset` internally to
        // size the data region.
        //
        // Without this alignment, a caller request of exactly `_chunkSize - headerOffset` data bytes
        // would yield a chunk whose data region is `headerOffset` bytes SHORT of the request — every
        // EnsureCapacity for a value larger than the chunk-data capacity would silently under-provision,
        // surfacing later as `ArgumentOutOfRangeException` in `AsSpan`/`CopyTo` at corrupt offsets.
        // The sanity guard in `BinarySerializationContext.EnsureCapacity` (post-Grow revalidation +
        // `ThrowGrowFailedToSatisfy`) catches the remaining > MaxChunkSize case explicitly — see
        // `BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-b7k9`.
        var headerOffset = _multiMessage ? HeaderSize : 0;
        AcquireChunk(Math.Max(needed + headerOffset, _chunkSize), out buffer, out position, out bufferEnd);
        _currentChunkStart = position;
    }

    /// <summary>
    /// Returns total serialized data bytes (excluding framing overhead).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public int GetTotalPosition(int currentPosition) => _committedBytes + (currentPosition - _currentChunkStart);

    /// <summary>
    /// Commits the last (partial) chunk to the PipeWriter, writes the <c>[202]</c> CHUNK_END marker
    /// in framed mode, and flushes everything to the underlying writer. End-of-serialize is fully
    /// owned by this output: when <see cref="Flush"/> returns, every byte is downstream — the
    /// caller does NOT need a follow-up <c>pipeWriter.FlushAsync()</c>.
    ///
    /// <para><b>Behaviour by mode</b>:</para>
    /// <list type="bullet">
    ///   <item><b>Raw mode</b> (<c>emitChunkFraming = false</c>): final chunk <c>Advance</c> →
    ///         <c>FlushAsync</c>. No end marker (raw mode has no framing concept).</item>
    ///   <item><b>Framed mode</b> (<c>emitChunkFraming = true</c>): final chunk <c>Advance</c> →
    ///         <c>[202]</c> CHUNK_END marker (symmetric with the <c>[201]</c> header that
    ///         <see cref="CommitCurrentChunk"/> writes per chunk) → <c>FlushAsync</c>. The end
    ///         marker is written here so the wire-format contract is fully owned by this output;
    ///         protocol layers above (e.g. <c>AcBinaryHubProtocol</c>) no longer need to inject
    ///         their own <c>[202]</c> + flush.</item>
    /// </list>
    ///
    /// <para>Zero-copy: no data copying in either mode. The pre-flush wait covers any in-flight
    /// fire-and-forget flush from <see cref="Grow"/> on the Pipe-based parallel path.</para>
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveOptimization)]
    public void Flush(byte[] buffer, int position)
    {
        // Wait for any in-flight flush from previous Grow
        SyncAwaitFlush(_lastFlush);

        CommitCurrentChunk(buffer, position);

        // Framed mode: write the [202] CHUNK_END marker — symmetric with [201] header that
        // CommitCurrentChunk writes per chunk. Both ends of the framing contract are owned here.
        if (_multiMessage)
        {
            var span = _pipeWriter.GetSpan(1);
            span[0] = ChunkEndMarker;
            _pipeWriter.Advance(1);

            EmitDiagnostic("Flush[framed]: wrote [202] CHUNK_END");
        }

        // Always flush — final chunk (and end marker, if framed) → downstream. Caller does not
        // need a follow-up FlushAsync.
        SyncAwaitFlush(_pipeWriter.FlushAsync());

        // End of serialize lifecycle — return the owned fallback buffer to ArrayPool exactly
        // once (NOT per chunk). The buffer was reused across all chunks in this lifecycle;
        // releasing it now avoids per-chunk rent/return churn even when the fallback path
        // actually fires.
        ReleaseOwnedBuffer();
    }

    /// <summary>
    /// Releases the lazy-allocated owned fallback buffer (if any) back to <see cref="ArrayPool{T}"/>.
    /// Idempotent — safe to call multiple times. Called from <see cref="Flush"/> at the end of a
    /// successful serialize cycle, and also from <see cref="Reset"/> for defensive cleanup if a
    /// caller reuses the struct.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void ReleaseOwnedBuffer()
    {
        if (_ownedBuffer != null)
        {
            ArrayPool<byte>.Shared.Return(_ownedBuffer);
            _ownedBuffer = null;
        }

        _hasOwnedBuffer = false;
    }

    /// <summary>
    /// Defensive cleanup — releases the owned fallback buffer if one was retained. Normally a
    /// no-op for PipeWriter-based output (chunks are owned by PipeWriter), but covers the rare
    /// case where the same struct instance is reused after a serialize without going through Flush.
    /// </summary>
    public void Reset() => ReleaseOwnedBuffer();

    /// <summary>
    /// Commits the current chunk to the PipeWriter. In framed mode, patches the reserved
    /// <c>[201][UINT16 dataBytes]</c> header before Advance; in raw mode, simply Advances the data.
    /// For owned buffers, copies to PipeWriter first (without releasing the buffer — Flush() does
    /// that once at the end).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void CommitCurrentChunk(byte[] buffer, int position)
    {
        var dataBytes = position - _currentChunkStart;
        if (dataBytes <= 0) return;

        if (_multiMessage)
        {
            var headerStart = _currentChunkStart - HeaderSize;
            buffer[headerStart] = ChunkDataMarker;

            BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(headerStart + 1, 2), (ushort)dataBytes);

            EmitDiagnostic($"CommitCurrentChunk[framed]: dataBytes={dataBytes} headerStart={headerStart} _currentChunkStart={_currentChunkStart} position={position} _hasOwnedBuffer={_hasOwnedBuffer} → Advance({HeaderSize + dataBytes})");

            if (_hasOwnedBuffer) CopyOwnedToPipeWriter(buffer, headerStart, HeaderSize + dataBytes);
            else _pipeWriter.Advance(HeaderSize + dataBytes);
        }
        else
        {
            EmitDiagnostic($"CommitCurrentChunk[raw]: dataBytes={dataBytes} _currentChunkStart={_currentChunkStart} position={position} _hasOwnedBuffer={_hasOwnedBuffer} → Advance({dataBytes})");

            if (_hasOwnedBuffer) CopyOwnedToPipeWriter(buffer, _currentChunkStart, dataBytes);
            else _pipeWriter.Advance(dataBytes);
        }

        // Both counters track only data bytes (not framing). _committedBytes is the cumulative
        // total used by GetTotalPosition; _unflushedBytes is the per-window counter used by the
        // Coalesced safety-net (resets each time a new FlushAsync starts).
        _committedBytes += dataBytes;
        _unflushedBytes += dataBytes;
    }

    /// <summary>
    /// Copies the owned-buffer chunk into the PipeWriter (since GetMemory returned non-array-backed
    /// memory and the serializer needs <c>byte[]</c>). Does NOT return the buffer to the pool —
    /// the <see cref="_ownedBuffer"/> reference stays alive across chunks within this serialize
    /// lifecycle; <see cref="Flush"/> releases it exactly once at the end.
    /// </summary>
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void CopyOwnedToPipeWriter(byte[] buffer, int start, int length)
    {
        var span = _pipeWriter.GetSpan(length);

        buffer.AsSpan(start, length).CopyTo(span);
        _pipeWriter.Advance(length);
    }

    [MethodImpl(MethodImplOptions.AggressiveOptimization)]
    private void AcquireChunk(int requestSize, out byte[] buffer, out int position, out int bufferEnd)
    {
        // Header reservation only in framed mode — raw mode skips it for byte-compat with the
        // single-shot byte[] output (each chunk holds pure AcBinary bytes, no markers).
        var headerOffset = _multiMessage ? HeaderSize : 0;

        // chunkSize is the chunk-on-wire total size (header + data) — subtract the header overhead
        // here so the actual data payload + header fits exactly in chunkSize bytes on the wire.
        // Without this, every WriteFile would emit chunkSize+3 bytes, which spans 2 kernel-page slots
        // for any page-aligned chunkSize (Kestrel slab 4 KB, max 64 KB) → fragmented kernel transfers
        // + the consumer's framing-state-machine sees chunk headers split across Feed boundaries.
        var dataSize = Math.Min(Math.Max(requestSize, _chunkSize), MaxChunkSize) - headerOffset;
        var totalRequest = dataSize + headerOffset;
        var memory = _pipeWriter.GetMemory(totalRequest);

        EmitDiagnostic($"AcquireChunk: framed={_multiMessage} requestSize={requestSize} dataSize={dataSize} totalRequest={totalRequest} memory.Length={memory.Length} _committedBytes={_committedBytes}");

        if (MemoryMarshal.TryGetArray(memory, out ArraySegment<byte> segment) && segment.Array != null)
        {
            buffer = segment.Array;
            position = segment.Offset + headerOffset;
            bufferEnd = segment.Offset + headerOffset + dataSize;

            _hasOwnedBuffer = false;

            EmitDiagnostic($"AcquireChunk[zc]: segment.Array.Length={segment.Array.Length} segment.Offset={segment.Offset} segment.Count={segment.Count} → buffer[{position}..{bufferEnd}]");
        }
        else
        {
            // Lazy reuse: keep the same ArrayPool buffer across chunks within this serialize
            // lifecycle. Only re-rent if the existing buffer is too small for the next chunk.
            // Released exactly once in Flush() — no per-chunk rent/return churn even when this
            // (rare) path actually fires.
            if (_ownedBuffer == null || _ownedBuffer.Length < totalRequest)
            {
                if (_ownedBuffer != null) ArrayPool<byte>.Shared.Return(_ownedBuffer);
                _ownedBuffer = ArrayPool<byte>.Shared.Rent(totalRequest);

                EmitDiagnostic($"AcquireChunk[ob-rent]: rented={_ownedBuffer.Length} (totalRequest={totalRequest})");
            }
            else
            {
                EmitDiagnostic($"AcquireChunk[ob-reuse]: reused={_ownedBuffer.Length} (totalRequest={totalRequest})");
            }

            buffer = _ownedBuffer;
            position = headerOffset;
            bufferEnd = headerOffset + dataSize;

            _hasOwnedBuffer = true;
        }
    }
}
