// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/parser.go in the NATS server Go source.

using System.Text;
using ZB.MOM.NatsNet.Server.Internal;

namespace ZB.MOM.NatsNet.Server.Protocol;

/// <summary>
/// NATS wire protocol parser — byte-by-byte state machine.
/// Mirrors Go <c>client.parse</c>, <c>protoSnippet</c>, <c>overMaxControlLineLimit</c>,
/// <c>clonePubArg</c>, and <c>parseState.getHeader</c> from parser.go.
/// </summary>
public static class ProtocolParser
{
    // =====================================================================
    // Parse — main state machine
    // =====================================================================

    /// <summary>
    /// Parses raw bytes through the NATS protocol state machine.
    /// Mirrors Go <c>client.parse(buf []byte) error</c>.
    /// </summary>
    public static Exception? Parse(ParseContext c, IProtocolHandler handler, byte[] buf)
    {
        if (handler.IsMqtt)
            return null; // MQTT clients handled separately

        // Snapshot connection state
        var authSet = handler.IsAwaitingAuth;
        var mcl = c.MaxControlLine;
        var trace = handler.Trace;
        var kind = c.Kind;
        var lmsg = false;

        for (var i = 0; i < buf.Length; i++)
        {
            var b = buf[i];

            switch (c.State)
            {
                case ParserState.OpStart:
                    c.Op = b;
                    if (b != 'C' && b != 'c')
                    {
                        if (authSet)
                        {
                            if (!handler.TryRegisterNoAuthUser())
                                goto authErr;
                            authSet = false;
                        }
                        if (kind == ClientKind.Gateway && handler.IsGatewayInboundNotConnected)
                            goto authErr;
                    }

                    switch (b)
                    {
                        case (byte)'P': case (byte)'p': c.State = ParserState.OpP; break;
                        case (byte)'H': case (byte)'h': c.State = ParserState.OpH; break;
                        case (byte)'S': case (byte)'s': c.State = ParserState.OpS; break;
                        case (byte)'U': case (byte)'u': c.State = ParserState.OpU; break;
                        case (byte)'R': case (byte)'r':
                            if (kind == ClientKind.Client) goto parseErr;
                            c.State = ParserState.OpR; break;
                        case (byte)'L': case (byte)'l':
                            if (kind != ClientKind.Leaf && kind != ClientKind.Router) goto parseErr;
                            c.State = ParserState.OpL; break;
                        case (byte)'A': case (byte)'a':
                            if (kind == ClientKind.Client) goto parseErr;
                            c.State = ParserState.OpA; break;
                        case (byte)'C': case (byte)'c': c.State = ParserState.OpC; break;
                        case (byte)'I': case (byte)'i': c.State = ParserState.OpI; break;
                        case (byte)'+': c.State = ParserState.OpPlus; break;
                        case (byte)'-': c.State = ParserState.OpMinus; break;
                        default: goto parseErr;
                    }
                    break;

                // ---- HPUB / HMSG ----
                case ParserState.OpH:
                    switch (b) {
                        case (byte)'P': case (byte)'p': c.State = ParserState.OpHp; break;
                        case (byte)'M': case (byte)'m': c.State = ParserState.OpHm; break;
                        default: goto parseErr;
                    }
                    break;
                case ParserState.OpHp:
                    if (b == 'U' || b == 'u') c.State = ParserState.OpHpu; else goto parseErr; break;
                case ParserState.OpHpu:
                    if (b == 'B' || b == 'b') c.State = ParserState.OpHpub; else goto parseErr; break;
                case ParserState.OpHpub:
                    if (b == ' ' || b == '\t') c.State = ParserState.OpHpubSpc; else goto parseErr; break;
                case ParserState.OpHpubSpc:
                    if (b == ' ' || b == '\t') break; // continue
                    c.Pa.HeaderSize = 0;
                    c.State = ParserState.HpubArg;
                    c.ArgStart = i;
                    break;
                case ParserState.HpubArg:
                    switch (b)
                    {
                        case (byte)'\r': c.Drop = 1; break;
                        case (byte)'\n':
                        {
                            var arg = CollectArg(c, buf, i);
                            var err = OverMaxControlLineLimit(c, handler, arg, mcl);
                            if (err != null) return err;
                            if (trace) handler.TraceInOp("HPUB", arg);
                            byte[]? remaining = i < buf.Length - 1 ? buf[(i + 1)..] : null;
                            err = ProcessHeaderPub(c, arg, remaining);
                            if (err != null) return err;
                            c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.MsgPayload;
                            if (c.MsgBuf == null) i = c.ArgStart + c.Pa.Size - ServerConstants.LenCrLf;
                            break;
                        }
                        default:
                            if (c.ArgBuf != null) AppendToArgBuf(c, b);
                            break;
                    }
                    break;

                case ParserState.OpHm:
                    if (b == 'S' || b == 's') c.State = ParserState.OpHms; else goto parseErr; break;
                case ParserState.OpHms:
                    if (b == 'G' || b == 'g') c.State = ParserState.OpHmsg; else goto parseErr; break;
                case ParserState.OpHmsg:
                    if (b == ' ' || b == '\t') c.State = ParserState.OpHmsgSpc; else goto parseErr; break;
                case ParserState.OpHmsgSpc:
                    if (b == ' ' || b == '\t') break;
                    c.Pa.HeaderSize = 0;
                    c.State = ParserState.HmsgArg;
                    c.ArgStart = i;
                    break;
                case ParserState.HmsgArg:
                    switch (b)
                    {
                        case (byte)'\r': c.Drop = 1; break;
                        case (byte)'\n':
                        {
                            var arg = CollectArg(c, buf, i);
                            var err = OverMaxControlLineLimit(c, handler, arg, mcl);
                            if (err != null) return err;
                            if (kind == ClientKind.Router || kind == ClientKind.Gateway)
                            {
                                if (trace) handler.TraceInOp("HMSG", arg);
                                err = ProcessRoutedHeaderMsgArgs(c, arg);
                            }
                            else if (kind == ClientKind.Leaf)
                            {
                                if (trace) handler.TraceInOp("HMSG", arg);
                                err = ProcessLeafHeaderMsgArgs(c, arg);
                            }
                            if (err != null) return err;
                            c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.MsgPayload;
                            i = c.ArgStart + c.Pa.Size - ServerConstants.LenCrLf;
                            break;
                        }
                        default:
                            if (c.ArgBuf != null) AppendToArgBuf(c, b);
                            break;
                    }
                    break;

                // ---- PUB ----
                case ParserState.OpP:
                    switch (b) {
                        case (byte)'U': case (byte)'u': c.State = ParserState.OpPu; break;
                        case (byte)'I': case (byte)'i': c.State = ParserState.OpPi; break;
                        case (byte)'O': case (byte)'o': c.State = ParserState.OpPo; break;
                        default: goto parseErr;
                    }
                    break;
                case ParserState.OpPu:
                    if (b == 'B' || b == 'b') c.State = ParserState.OpPub; else goto parseErr; break;
                case ParserState.OpPub:
                    if (b == ' ' || b == '\t') c.State = ParserState.OpPubSpc; else goto parseErr; break;
                case ParserState.OpPubSpc:
                    if (b == ' ' || b == '\t') break;
                    c.Pa.HeaderSize = -1;
                    c.State = ParserState.PubArg;
                    c.ArgStart = i;
                    break;
                case ParserState.PubArg:
                    switch (b)
                    {
                        case (byte)'\r': c.Drop = 1; break;
                        case (byte)'\n':
                        {
                            var arg = CollectArg(c, buf, i);
                            var err = OverMaxControlLineLimit(c, handler, arg, mcl);
                            if (err != null) return err;
                            if (trace) handler.TraceInOp("PUB", arg);
                            err = ProcessPub(c, arg);
                            if (err != null) return err;
                            c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.MsgPayload;
                            if (c.MsgBuf == null) i = c.ArgStart + c.Pa.Size - ServerConstants.LenCrLf;
                            break;
                        }
                        default:
                            if (c.ArgBuf != null) AppendToArgBuf(c, b);
                            break;
                    }
                    break;

                // ---- MSG_PAYLOAD / MSG_END ----
                case ParserState.MsgPayload:
                    if (c.MsgBuf != null)
                    {
                        var toCopy = c.Pa.Size - c.MsgBuf.Length;
                        var avail = buf.Length - i;
                        if (avail < toCopy) toCopy = avail;
                        if (toCopy > 0)
                        {
                            var start = c.MsgBuf.Length;
                            var tmp = new byte[start + toCopy];
                            Array.Copy(c.MsgBuf, tmp, start);
                            Array.Copy(buf, i, tmp, start, toCopy);
                            c.MsgBuf = tmp;
                            i = i + toCopy - 1;
                        }
                        else
                        {
                            var tmp = c.MsgBuf;
                            Array.Resize(ref tmp, tmp.Length + 1);
                            tmp[^1] = b;
                            c.MsgBuf = tmp;
                        }
                        if (c.MsgBuf.Length >= c.Pa.Size) c.State = ParserState.MsgEndR;
                    }
                    else if (i - c.ArgStart + 1 >= c.Pa.Size)
                    {
                        c.State = ParserState.MsgEndR;
                    }
                    break;

                case ParserState.MsgEndR:
                    if (b != '\r') goto parseErr;
                    if (c.MsgBuf != null) AppendToMsgBuf(c, b);
                    c.State = ParserState.MsgEndN;
                    break;

                case ParserState.MsgEndN:
                    if (b != '\n') goto parseErr;
                    if (c.MsgBuf != null)
                        AppendToMsgBuf(c, b);
                    else
                        c.MsgBuf = buf[c.ArgStart..(i + 1)];

                    // Check for mappings
                    if ((kind == ClientKind.Client || kind == ClientKind.Leaf) && handler.HasMappings)
                    {
                        if (handler.SelectMappedSubject())
                        {
                            if (trace)
                                handler.TraceInOp("MAPPING",
                                    Encoding.ASCII.GetBytes($"{AsString(c.Pa.Mapped)} -> {AsString(c.Pa.Subject)}"));
                        }
                    }
                    if (trace) handler.TraceMsg(c.MsgBuf);
                    handler.ProcessInboundMsg(c.MsgBuf);

                    c.ArgBuf = null; c.MsgBuf = null;
                    c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
                    // Drop all pub args
                    c.Pa.Reset();
                    lmsg = false;
                    break;

                // ---- A+/A- (account sub/unsub) ----
                case ParserState.OpA:
                    switch (b) {
                        case (byte)'+': c.State = ParserState.OpAsub; break;
                        case (byte)'-': case (byte)'u': c.State = ParserState.OpAusub; break;
                        default: goto parseErr;
                    }
                    break;
                case ParserState.OpAsub:
                    if (b == ' ' || b == '\t') c.State = ParserState.OpAsubSpc; else goto parseErr; break;
                case ParserState.OpAsubSpc:
                    if (b == ' ' || b == '\t') break;
                    c.State = ParserState.AsubArg;
                    c.ArgStart = i;
                    break;
                case ParserState.AsubArg:
                    switch (b)
                    {
                        case (byte)'\r': c.Drop = 1; break;
                        case (byte)'\n':
                        {
                            var arg = CollectArg(c, buf, i);
                            var err = OverMaxControlLineLimit(c, handler, arg, mcl);
                            if (err != null) return err;
                            if (trace) handler.TraceInOp("A+", arg);
                            err = handler.ProcessAccountSub(arg);
                            if (err != null) return err;
                            c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
                            break;
                        }
                        default:
                            if (c.ArgBuf != null) AppendToArgBuf(c, b);
                            break;
                    }
                    break;
                case ParserState.OpAusub:
                    if (b == ' ' || b == '\t') c.State = ParserState.OpAusubSpc; else goto parseErr; break;
                case ParserState.OpAusubSpc:
                    if (b == ' ' || b == '\t') break;
                    c.State = ParserState.AusubArg;
                    c.ArgStart = i;
                    break;
                case ParserState.AusubArg:
                    switch (b)
                    {
                        case (byte)'\r': c.Drop = 1; break;
                        case (byte)'\n':
                        {
                            var arg = CollectArg(c, buf, i);
                            var err = OverMaxControlLineLimit(c, handler, arg, mcl);
                            if (err != null) return err;
                            if (trace) handler.TraceInOp("A-", arg);
                            handler.ProcessAccountUnsub(arg);
                            c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
                            break;
                        }
                        default:
                            if (c.ArgBuf != null) AppendToArgBuf(c, b);
                            break;
                    }
                    break;

                // ---- SUB ----
                case ParserState.OpS:
                    if (b == 'U' || b == 'u') c.State = ParserState.OpSu; else goto parseErr; break;
                case ParserState.OpSu:
                    if (b == 'B' || b == 'b') c.State = ParserState.OpSub; else goto parseErr; break;
                case ParserState.OpSub:
                    if (b == ' ' || b == '\t') c.State = ParserState.OpSubSpc; else goto parseErr; break;
                case ParserState.OpSubSpc:
                    if (b == ' ' || b == '\t') break;
                    c.State = ParserState.SubArg;
                    c.ArgStart = i;
                    break;
                case ParserState.SubArg:
                    switch (b)
                    {
                        case (byte)'\r': c.Drop = 1; break;
                        case (byte)'\n':
                        {
                            var arg = CollectArg(c, buf, i);
                            var err = OverMaxControlLineLimit(c, handler, arg, mcl);
                            if (err != null) return err;
                            var subTokens = SplitArgs(arg);
                            if (subTokens.Count is < 2 or > 3)
                                return new InvalidOperationException(
                                    $"processClientSub Parse Error '{Encoding.ASCII.GetString(arg)}'");
                            switch (kind)
                            {
                                case ClientKind.Client:
                                    if (trace) handler.TraceInOp("SUB", arg);
                                    err = handler.ProcessClientSub(arg);
                                    break;
                                case ClientKind.Router:
                                    if (c.Op is (byte)'R' or (byte)'r')
                                    {
                                        if (trace) handler.TraceInOp("RS+", arg);
                                        err = handler.ProcessRemoteSub(arg, false);
                                    }
                                    else
                                    {
                                        if (trace) handler.TraceInOp("LS+", arg);
                                        err = handler.ProcessRemoteSub(arg, true);
                                    }
                                    break;
                                case ClientKind.Gateway:
                                    if (trace) handler.TraceInOp("RS+", arg);
                                    err = handler.ProcessGatewayRSub(arg);
                                    break;
                                case ClientKind.Leaf:
                                    if (trace) handler.TraceInOp("LS+", arg);
                                    err = handler.ProcessLeafSub(arg);
                                    break;
                            }
                            if (err != null) return err;
                            c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
                            break;
                        }
                        default:
                            if (c.ArgBuf != null) AppendToArgBuf(c, b);
                            break;
                    }
                    break;

                // ---- L (LS+/LS- or LMSG) ----
                case ParserState.OpL:
                    switch (b) {
                        case (byte)'S': case (byte)'s': c.State = ParserState.OpLs; break;
                        case (byte)'M': case (byte)'m': c.State = ParserState.OpM; break;
                        default: goto parseErr;
                    }
                    break;
                case ParserState.OpLs:
                    switch (b) {
                        case (byte)'+': c.State = ParserState.OpSub; break;
                        case (byte)'-': c.State = ParserState.OpUnsub; break;
                        default: goto parseErr;
                    }
                    break;

                // ---- R (RS+/RS- or RMSG) ----
                case ParserState.OpR:
                    switch (b) {
                        case (byte)'S': case (byte)'s': c.State = ParserState.OpRs; break;
                        case (byte)'M': case (byte)'m': c.State = ParserState.OpM; break;
                        default: goto parseErr;
                    }
                    break;
                case ParserState.OpRs:
                    switch (b) {
                        case (byte)'+': c.State = ParserState.OpSub; break;
                        case (byte)'-': c.State = ParserState.OpUnsub; break;
                        default: goto parseErr;
                    }
                    break;

                // ---- UNSUB ----
                case ParserState.OpU:
                    if (b == 'N' || b == 'n') c.State = ParserState.OpUn; else goto parseErr; break;
                case ParserState.OpUn:
                    if (b == 'S' || b == 's') c.State = ParserState.OpUns; else goto parseErr; break;
                case ParserState.OpUns:
                    if (b == 'U' || b == 'u') c.State = ParserState.OpUnsu; else goto parseErr; break;
                case ParserState.OpUnsu:
                    if (b == 'B' || b == 'b') c.State = ParserState.OpUnsub; else goto parseErr; break;
                case ParserState.OpUnsub:
                    if (b == ' ' || b == '\t') c.State = ParserState.OpUnsubSpc; else goto parseErr; break;
                case ParserState.OpUnsubSpc:
                    if (b == ' ' || b == '\t') break;
                    c.State = ParserState.UnsubArg;
                    c.ArgStart = i;
                    break;
                case ParserState.UnsubArg:
                    switch (b)
                    {
                        case (byte)'\r': c.Drop = 1; break;
                        case (byte)'\n':
                        {
                            var arg = CollectArg(c, buf, i);
                            var err = OverMaxControlLineLimit(c, handler, arg, mcl);
                            if (err != null) return err;
                            var unsubTokens = SplitArgs(arg);
                            if (unsubTokens.Count is 0 or > 2)
                                return new InvalidOperationException(
                                    $"processClientUnsub Parse Error '{Encoding.ASCII.GetString(arg)}'");
                            switch (kind)
                            {
                                case ClientKind.Client:
                                    if (trace) handler.TraceInOp("UNSUB", arg);
                                    err = handler.ProcessClientUnsub(arg);
                                    break;
                                case ClientKind.Router:
                                {
                                    if (trace)
                                    {
                                        var op = c.Op is (byte)'R' or (byte)'r' ? "RS-" : "LS-";
                                        handler.TraceInOp(op, arg);
                                    }
                                    var leafUnsub = c.Op is (byte)'L' or (byte)'l';
                                    err = handler.ProcessRemoteUnsub(arg, leafUnsub);
                                    break;
                                }
                                case ClientKind.Gateway:
                                    if (trace) handler.TraceInOp("RS-", arg);
                                    err = handler.ProcessGatewayRUnsub(arg);
                                    break;
                                case ClientKind.Leaf:
                                    if (trace) handler.TraceInOp("LS-", arg);
                                    err = handler.ProcessLeafUnsub(arg);
                                    break;
                            }
                            if (err != null) return err;
                            c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
                            break;
                        }
                        default:
                            if (c.ArgBuf != null) AppendToArgBuf(c, b);
                            break;
                    }
                    break;

                // ---- PING ----
                case ParserState.OpPi:
                    if (b == 'N' || b == 'n') c.State = ParserState.OpPin; else goto parseErr; break;
                case ParserState.OpPin:
                    if (b == 'G' || b == 'g') c.State = ParserState.OpPing; else goto parseErr; break;
                case ParserState.OpPing:
                    if (b == '\n')
                    {
                        if (trace) handler.TraceInOp("PING", null);
                        handler.ProcessPing();
                        c.Drop = 0; c.State = ParserState.OpStart;
                    }
                    break;

                // ---- PONG ----
                case ParserState.OpPo:
                    if (b == 'N' || b == 'n') c.State = ParserState.OpPon; else goto parseErr; break;
                case ParserState.OpPon:
                    if (b == 'G' || b == 'g') c.State = ParserState.OpPong; else goto parseErr; break;
                case ParserState.OpPong:
                    if (b == '\n')
                    {
                        if (trace) handler.TraceInOp("PONG", null);
                        handler.ProcessPong();
                        c.Drop = 0; c.State = ParserState.OpStart;
                    }
                    break;

                // ---- CONNECT ----
                case ParserState.OpC:
                    if (b == 'O' || b == 'o') c.State = ParserState.OpCo; else goto parseErr; break;
                case ParserState.OpCo:
                    if (b == 'N' || b == 'n') c.State = ParserState.OpCon; else goto parseErr; break;
                case ParserState.OpCon:
                    if (b == 'N' || b == 'n') c.State = ParserState.OpConn; else goto parseErr; break;
                case ParserState.OpConn:
                    if (b == 'E' || b == 'e') c.State = ParserState.OpConne; else goto parseErr; break;
                case ParserState.OpConne:
                    if (b == 'C' || b == 'c') c.State = ParserState.OpConnec; else goto parseErr; break;
                case ParserState.OpConnec:
                    if (b == 'T' || b == 't') c.State = ParserState.OpConnect; else goto parseErr; break;
                case ParserState.OpConnect:
                    if (b == ' ' || b == '\t') break;
                    c.State = ParserState.ConnectArg;
                    c.ArgStart = i;
                    break;
                case ParserState.ConnectArg:
                    switch (b)
                    {
                        case (byte)'\r': c.Drop = 1; break;
                        case (byte)'\n':
                        {
                            var arg = CollectArg(c, buf, i);
                            var err = OverMaxControlLineLimit(c, handler, arg, mcl);
                            if (err != null) return err;
                            if (SplitArgs(arg).Count == 0) goto parseErr;
                            if (trace) handler.TraceInOp("CONNECT", arg);
                            err = handler.ProcessConnect(arg);
                            if (err != null) return err;
                            c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
                            authSet = handler.IsAwaitingAuth;
                            break;
                        }
                        default:
                            if (c.ArgBuf != null) AppendToArgBuf(c, b);
                            break;
                    }
                    break;

                // ---- MSG (route/leaf/gateway) ----
                case ParserState.OpM:
                    if (b == 'S' || b == 's') c.State = ParserState.OpMs; else goto parseErr; break;
                case ParserState.OpMs:
                    if (b == 'G' || b == 'g') c.State = ParserState.OpMsg; else goto parseErr; break;
                case ParserState.OpMsg:
                    if (b == ' ' || b == '\t') c.State = ParserState.OpMsgSpc; else goto parseErr; break;
                case ParserState.OpMsgSpc:
                    if (b == ' ' || b == '\t') break;
                    c.Pa.HeaderSize = -1;
                    c.State = ParserState.MsgArg;
                    c.ArgStart = i;
                    break;
                case ParserState.MsgArg:
                    switch (b)
                    {
                        case (byte)'\r': c.Drop = 1; break;
                        case (byte)'\n':
                        {
                            var arg = CollectArg(c, buf, i);
                            var err = OverMaxControlLineLimit(c, handler, arg, mcl);
                            if (err != null) return err;
                            if (kind == ClientKind.Router || kind == ClientKind.Gateway)
                            {
                                if (c.Op is (byte)'R' or (byte)'r')
                                {
                                    if (trace) handler.TraceInOp("RMSG", arg);
                                    err = ProcessRoutedMsgArgs(c, arg);
                                }
                                else
                                {
                                    if (trace) handler.TraceInOp("LMSG", arg);
                                    lmsg = true;
                                    err = ProcessRoutedOriginClusterMsgArgs(c, arg);
                                }
                            }
                            else if (kind == ClientKind.Leaf)
                            {
                                if (trace) handler.TraceInOp("LMSG", arg);
                                err = ProcessLeafMsgArgs(c, arg);
                            }
                            if (err != null) return err;
                            c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.MsgPayload;
                            i = c.ArgStart + c.Pa.Size - ServerConstants.LenCrLf;
                            break;
                        }
                        default:
                            if (c.ArgBuf != null) AppendToArgBuf(c, b);
                            break;
                    }
                    break;

                // ---- INFO ----
                case ParserState.OpI:
                    if (b == 'N' || b == 'n') c.State = ParserState.OpIn; else goto parseErr; break;
                case ParserState.OpIn:
                    if (b == 'F' || b == 'f') c.State = ParserState.OpInf; else goto parseErr; break;
                case ParserState.OpInf:
                    if (b == 'O' || b == 'o') c.State = ParserState.OpInfo; else goto parseErr; break;
                case ParserState.OpInfo:
                    if (b == ' ' || b == '\t') break;
                    c.State = ParserState.InfoArg;
                    c.ArgStart = i;
                    break;
                case ParserState.InfoArg:
                    switch (b)
                    {
                        case (byte)'\r': c.Drop = 1; break;
                        case (byte)'\n':
                        {
                            var arg = CollectArg(c, buf, i);
                            var err = OverMaxControlLineLimit(c, handler, arg, mcl);
                            if (err != null) return err;
                            if (SplitArgs(arg).Count == 0) goto parseErr;
                            err = handler.ProcessInfo(arg);
                            if (err != null) return err;
                            c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
                            break;
                        }
                        default:
                            if (c.ArgBuf != null) AppendToArgBuf(c, b);
                            break;
                    }
                    break;

                // ---- +OK ----
                case ParserState.OpPlus:
                    if (b == 'O' || b == 'o') c.State = ParserState.OpPlusO; else goto parseErr; break;
                case ParserState.OpPlusO:
                    if (b == 'K' || b == 'k') c.State = ParserState.OpPlusOk; else goto parseErr; break;
                case ParserState.OpPlusOk:
                    if (b == '\n') { c.Drop = 0; c.State = ParserState.OpStart; }
                    break;

                // ---- -ERR ----
                case ParserState.OpMinus:
                    if (b == 'E' || b == 'e') c.State = ParserState.OpMinusE; else goto parseErr; break;
                case ParserState.OpMinusE:
                    if (b == 'R' || b == 'r') c.State = ParserState.OpMinusEr; else goto parseErr; break;
                case ParserState.OpMinusEr:
                    if (b == 'R' || b == 'r') c.State = ParserState.OpMinusErr; else goto parseErr; break;
                case ParserState.OpMinusErr:
                    if (b == ' ' || b == '\t') c.State = ParserState.OpMinusErrSpc; else goto parseErr; break;
                case ParserState.OpMinusErrSpc:
                    if (b == ' ' || b == '\t') break;
                    c.State = ParserState.MinusErrArg;
                    c.ArgStart = i;
                    break;
                case ParserState.MinusErrArg:
                    switch (b)
                    {
                        case (byte)'\r': c.Drop = 1; break;
                        case (byte)'\n':
                        {
                            var arg = CollectArg(c, buf, i);
                            var err = OverMaxControlLineLimit(c, handler, arg, mcl);
                            if (err != null) return err;
                            handler.ProcessErr(Encoding.ASCII.GetString(arg));
                            c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
                            break;
                        }
                        default:
                            if (c.ArgBuf != null) AppendToArgBuf(c, b);
                            break;
                    }
                    break;

                default:
                    goto parseErr;
            }
        }

        // --- Split buffer handling ---

        // Check for split buffer scenarios for any ARG state.
        if (c.State is ParserState.SubArg or ParserState.UnsubArg
            or ParserState.PubArg or ParserState.HpubArg
            or ParserState.AsubArg or ParserState.AusubArg
            or ParserState.MsgArg or ParserState.HmsgArg
            or ParserState.MinusErrArg or ParserState.ConnectArg or ParserState.InfoArg)
        {
            if (c.ArgBuf == null)
            {
                var end = buf.Length - c.Drop;
                c.ArgBuf = new byte[end - c.ArgStart];
                Array.Copy(buf, c.ArgStart, c.ArgBuf, 0, c.ArgBuf.Length);
            }
            var err = OverMaxControlLineLimit(c, handler, c.ArgBuf, mcl);
            if (err != null) return err;
        }

        // Check for split msg
        if (c.State is ParserState.MsgPayload or ParserState.MsgEndR or ParserState.MsgEndN
            && c.MsgBuf == null)
        {
            if (c.ArgBuf == null)
            {
                var cloneErr = ClonePubArg(c, handler, lmsg);
                if (cloneErr != null) goto parseErr;
            }

            var lrem = buf.Length - c.ArgStart;
            if (lrem > c.Pa.Size + ServerConstants.LenCrLf)
                goto parseErr;
            c.MsgBuf = new byte[lrem];
            Array.Copy(buf, c.ArgStart, c.MsgBuf, 0, lrem);
        }

        return null;

    authErr:
        handler.AuthViolation();
        return ServerErrors.ErrAuthentication;

    parseErr:
        handler.SendErr("Unknown Protocol Operation");
        var snip = ProtoSnippet(buf.Length > 0 ? Math.Min(buf.Length - 1, Math.Max(0, 0)) : 0,
            ServerConstants.ProtoSnippetSize, buf);
        return new InvalidOperationException(
            $"{handler.KindString()} parser ERROR, state={(int)c.State}: proto='{snip}...'");
    }

    // =====================================================================
    // ProtoSnippet
    // =====================================================================

    /// <summary>
    /// Returns a quoted snippet of the protocol buffer for error messages.
    /// Mirrors Go <c>protoSnippet(start, max int, buf []byte) string</c>.
    /// </summary>
    public static string ProtoSnippet(int start, int max, byte[] buf)
    {
        var stop = start + max;
        var bufSize = buf.Length;
        if (start >= bufSize)
            return "\"\"";
        if (stop > bufSize)
            stop = bufSize - 1;
        return $"\"{Encoding.ASCII.GetString(buf, start, stop - start)}\"";
    }

    // =====================================================================
    // OverMaxControlLineLimit
    // =====================================================================

    /// <summary>
    /// Checks if the argument exceeds the max control line limit.
    /// Only enforced for CLIENT connections.
    /// Mirrors Go <c>client.overMaxControlLineLimit</c>.
    /// </summary>
    public static Exception? OverMaxControlLineLimit(ParseContext c, IProtocolHandler handler, byte[] arg, int mcl)
    {
        if (c.Kind != ClientKind.Client)
            return null;
        if (arg.Length > mcl)
        {
            var snip = ProtoSnippet(0, ServerConstants.MaxControlLineSnippetSize, arg);
            var err = ErrorContextHelper.NewErrorCtx(ServerErrors.ErrMaxControlLine,
                "State {0}, max_control_line {1}, Buffer len {2} (snip: {3}...)",
                (int)c.State, mcl, c.ArgBuf?.Length ?? arg.Length, snip);
            handler.SendErr(err.Message);
            handler.CloseConnection(0); // MaxControlLineExceeded
            return err;
        }
        return null;
    }

    // =====================================================================
    // ProcessPub — parse PUB arguments
    // =====================================================================

    /// <summary>
    /// Parses PUB protocol arguments: "subject [reply] size".
    /// Mirrors Go <c>client.processPub</c> from client.go.
    /// </summary>
    public static Exception? ProcessPub(ParseContext c, byte[] arg)
    {
        var tokens = SplitArgs(arg);
        byte[]? subject, reply, szb;

        switch (tokens.Count)
        {
            case 2:
                subject = tokens[0];
                reply = null;
                szb = tokens[1];
                break;
            case 3:
                subject = tokens[0];
                reply = tokens[1];
                szb = tokens[2];
                break;
            default:
                return new InvalidOperationException(
                    $"processPub error: {ProtoSnippet(0, ServerConstants.ProtoSnippetSize, arg)}");
        }

        if (subject.Length == 0)
            return new InvalidOperationException(
                $"processPub error: empty subject {ProtoSnippet(0, ServerConstants.ProtoSnippetSize, arg)}");

        if (!TryParseSize(szb, out var size))
            return new InvalidOperationException(
                $"processPub error: bad size {ProtoSnippet(0, ServerConstants.ProtoSnippetSize, arg)}");

        if (c.MaxPayload >= 0 && size > c.MaxPayload)
            return ServerErrors.ErrMaxPayload;

        c.Pa.Subject = subject;
        c.Pa.Reply = reply;
        c.Pa.SizeBytes = szb;
        c.Pa.Size = size;
        c.Pa.Arg = arg;
        return null;
    }

    // =====================================================================
    // ProcessHeaderPub — parse HPUB arguments
    // =====================================================================

    /// <summary>
    /// Parses HPUB protocol arguments: "subject [reply] hdr_size total_size".
    /// Mirrors Go <c>client.processHeaderPub</c> from client.go.
    /// </summary>
    public static Exception? ProcessHeaderPub(ParseContext c, byte[] arg, byte[]? remaining)
    {
        if (!c.HasHeaders)
            return ServerErrors.ErrMsgHeadersNotSupported;

        var tokens = SplitArgs(arg);
        byte[]? subject, reply, hdb, szb;

        switch (tokens.Count)
        {
            case 3:
                subject = tokens[0];
                reply = null;
                hdb = tokens[1];
                szb = tokens[2];
                break;
            case 4:
                subject = tokens[0];
                reply = tokens[1];
                hdb = tokens[2];
                szb = tokens[3];
                break;
            default:
                return new InvalidOperationException(
                    $"processHeaderPub error: {ProtoSnippet(0, ServerConstants.ProtoSnippetSize, arg)}");
        }

        if (subject.Length == 0)
            return new InvalidOperationException("processHeaderPub error: empty subject");

        if (!TryParseSize(hdb, out var hdr))
            return new InvalidOperationException("processHeaderPub error: bad header size");
        if (!TryParseSize(szb, out var size))
            return new InvalidOperationException("processHeaderPub error: bad size");

        if (hdr > size)
            return ServerErrors.ErrBadMsgHeader;

        if (c.MaxPayload >= 0 && size > c.MaxPayload)
            return ServerErrors.ErrMaxPayload;

        c.Pa.Subject = subject;
        c.Pa.Reply = reply;
        c.Pa.HeaderBytes = hdb;
        c.Pa.SizeBytes = szb;
        c.Pa.HeaderSize = hdr;
        c.Pa.Size = size;
        c.Pa.Arg = arg;
        return null;
    }

    // =====================================================================
    // ProcessRoutedMsgArgs — parse RMSG arguments
    // =====================================================================

    /// <summary>
    /// Parses RMSG protocol arguments: "account subject [+ reply] [| queue...] [reply] size".
    /// Mirrors Go <c>client.processRoutedMsgArgs</c> from client.go.
    /// </summary>
    public static Exception? ProcessRoutedMsgArgs(ParseContext c, byte[] arg)
    {
        var tokens = SplitArgs(arg);
        if (tokens.Count < 3)
            return new InvalidOperationException("processRoutedMsgArgs error: not enough args");

        c.Pa.Account = tokens[0];
        c.Pa.Subject = tokens[1];

        if (tokens.Count >= 5 && tokens[2].Length == 1 && tokens[2][0] == '+')
        {
            // + reply queues... size
            c.Pa.Reply = tokens[3];
            c.Pa.Queues = [];
            for (var j = 4; j < tokens.Count - 1; j++)
                c.Pa.Queues.Add(tokens[j]);
            var szb = tokens[^1];
            if (!TryParseSize(szb, out var size))
                return new InvalidOperationException("processRoutedMsgArgs error: bad size");
            c.Pa.SizeBytes = szb;
            c.Pa.Size = size;
        }
        else if (tokens.Count >= 4 && tokens[2].Length == 1 && tokens[2][0] == '|')
        {
            // | queues... size (no reply)
            c.Pa.Reply = [];;
            c.Pa.Queues = [];
            for (var j = 3; j < tokens.Count - 1; j++)
                c.Pa.Queues.Add(tokens[j]);
            var szb = tokens[^1];
            if (!TryParseSize(szb, out var size))
                return new InvalidOperationException("processRoutedMsgArgs error: bad size");
            c.Pa.SizeBytes = szb;
            c.Pa.Size = size;
        }
        else if (tokens.Count == 4)
        {
            // account subject reply size
            c.Pa.Reply = tokens[2];
            var szb = tokens[3];
            if (!TryParseSize(szb, out var size))
                return new InvalidOperationException("processRoutedMsgArgs error: bad size");
            c.Pa.SizeBytes = szb;
            c.Pa.Size = size;
        }
        else if (tokens.Count == 3)
        {
            // account subject size
            c.Pa.Reply = null;
            var szb = tokens[2];
            if (!TryParseSize(szb, out var size))
                return new InvalidOperationException("processRoutedMsgArgs error: bad size");
            c.Pa.SizeBytes = szb;
            c.Pa.Size = size;
        }
        else
        {
            return new InvalidOperationException("processRoutedMsgArgs error: bad args");
        }

        c.Pa.Arg = arg;
        return null;
    }

    // =====================================================================
    // ProcessRoutedHeaderMsgArgs — parse route HMSG arguments
    // =====================================================================

    /// <summary>
    /// Parses route HMSG protocol arguments: "account subject [+ reply queues...] [| queues...] [reply] hdr_size total_size".
    /// Mirrors Go <c>client.processRoutedHeaderMsgArgs</c> from client.go.
    /// </summary>
    public static Exception? ProcessRoutedHeaderMsgArgs(ParseContext c, byte[] arg)
    {
        var tokens = SplitArgs(arg);
        if (tokens.Count < 4)
            return new InvalidOperationException("processRoutedHeaderMsgArgs error: not enough args");

        c.Pa.Account = tokens[0];
        c.Pa.Subject = tokens[1];

        if (tokens.Count >= 6 && tokens[2].Length == 1 && tokens[2][0] == '+')
        {
            // + reply queues... hdr_size total_size
            c.Pa.Reply = tokens[3];
            c.Pa.Queues = [];
            for (var j = 4; j < tokens.Count - 2; j++)
                c.Pa.Queues.Add(tokens[j]);
            if (!TryParseSize(tokens[^2], out var hdr))
                return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad hdr size");
            if (!TryParseSize(tokens[^1], out var size))
                return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad size");
            if (hdr > size) return ServerErrors.ErrBadMsgHeader;
            c.Pa.HeaderSize = hdr;
            c.Pa.SizeBytes = tokens[^1];
            c.Pa.Size = size;
        }
        else if (tokens.Count >= 5 && tokens[2].Length == 1 && tokens[2][0] == '|')
        {
            // | queues... hdr_size total_size (no reply)
            c.Pa.Reply = [];
            c.Pa.Queues = [];
            for (var j = 3; j < tokens.Count - 2; j++)
                c.Pa.Queues.Add(tokens[j]);
            if (!TryParseSize(tokens[^2], out var hdr))
                return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad hdr size");
            if (!TryParseSize(tokens[^1], out var size))
                return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad size");
            if (hdr > size) return ServerErrors.ErrBadMsgHeader;
            c.Pa.HeaderSize = hdr;
            c.Pa.SizeBytes = tokens[^1];
            c.Pa.Size = size;
        }
        else if (tokens.Count == 5)
        {
            // account subject reply hdr_size total_size
            c.Pa.Reply = tokens[2];
            if (!TryParseSize(tokens[3], out var hdr))
                return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad hdr size");
            if (!TryParseSize(tokens[4], out var size))
                return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad size");
            if (hdr > size) return ServerErrors.ErrBadMsgHeader;
            c.Pa.HeaderSize = hdr;
            c.Pa.SizeBytes = tokens[4];
            c.Pa.Size = size;
        }
        else if (tokens.Count == 4)
        {
            // account subject hdr_size total_size
            c.Pa.Reply = null;
            if (!TryParseSize(tokens[2], out var hdr))
                return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad hdr size");
            if (!TryParseSize(tokens[3], out var size))
                return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad size");
            if (hdr > size) return ServerErrors.ErrBadMsgHeader;
            c.Pa.HeaderSize = hdr;
            c.Pa.SizeBytes = tokens[3];
            c.Pa.Size = size;
        }
        else
        {
            return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad args");
        }

        c.Pa.Arg = arg;
        return null;
    }

    // =====================================================================
    // ProcessLeafMsgArgs / ProcessLeafHeaderMsgArgs — stubs
    // =====================================================================

    /// <summary>
    /// Parses leaf MSG arguments:
    /// <c>subject size</c>,
    /// <c>subject reply size</c>,
    /// <c>subject + reply queues... size</c>,
    /// <c>subject | queues... size</c>.
    /// Mirrors Go <c>client.processLeafMsgArgs</c>.
    /// </summary>
    public static Exception? ProcessLeafMsgArgs(ParseContext c, byte[] arg)
    {
        var routedErr = ProcessRoutedMsgArgs(c, arg);
        if (routedErr is null)
            return null;

        var tokens = SplitArgs(arg);
        if (tokens.Count < 2)
            return new InvalidOperationException($"processLeafMsgArgs Parse Error: '{Encoding.ASCII.GetString(arg)}'");

        c.Pa.Account = null;
        c.Pa.Subject = tokens[0];
        c.Pa.Arg = arg;
        c.Pa.Queues = null;

        if (tokens.Count == 2)
        {
            c.Pa.Reply = null;
            c.Pa.SizeBytes = tokens[1];
            if (!TryParseSize(tokens[1], out var size))
                return new InvalidOperationException($"processLeafMsgArgs Bad or Missing Size: '{Encoding.ASCII.GetString(arg)}'");
            c.Pa.Size = size;
            return null;
        }

        if (tokens.Count == 3)
        {
            c.Pa.Reply = tokens[1];
            c.Pa.SizeBytes = tokens[2];
            if (!TryParseSize(tokens[2], out var size))
                return new InvalidOperationException($"processLeafMsgArgs Bad or Missing Size: '{Encoding.ASCII.GetString(arg)}'");
            c.Pa.Size = size;
            return null;
        }

        if (tokens[1].Length != 1 || (tokens[1][0] != (byte)'+' && tokens[1][0] != (byte)'|'))
            return new InvalidOperationException($"processLeafMsgArgs Bad or Missing Reply Indicator: '{Encoding.ASCII.GetString(tokens[1])}'");

        c.Pa.Reply = tokens[1][0] == (byte)'+' ? tokens[2] : null;
        c.Pa.SizeBytes = tokens[^1];
        if (!TryParseSize(tokens[^1], out var msgSize))
            return new InvalidOperationException($"processLeafMsgArgs Bad or Missing Size: '{Encoding.ASCII.GetString(arg)}'");
        c.Pa.Size = msgSize;

        c.Pa.Queues = [];
        var queueStart = c.Pa.Reply is null ? 2 : 3;
        for (var i = queueStart; i < tokens.Count - 1; i++)
            c.Pa.Queues.Add(tokens[i]);

        return null;
    }

    /// <summary>
    /// Parses leaf HMSG arguments:
    /// <c>subject hdr_size total_size</c>,
    /// <c>subject reply hdr_size total_size</c>,
    /// <c>subject + reply queues... hdr_size total_size</c>,
    /// <c>subject | queues... hdr_size total_size</c>.
    /// Mirrors Go <c>client.processLeafHeaderMsgArgs</c>.
    /// </summary>
    public static Exception? ProcessLeafHeaderMsgArgs(ParseContext c, byte[] arg)
    {
        var routedErr = ProcessRoutedHeaderMsgArgs(c, arg);
        if (routedErr is null)
            return null;

        var tokens = SplitArgs(arg);
        if (tokens.Count < 3)
            return new InvalidOperationException($"processLeafHeaderMsgArgs Parse Error: '{Encoding.ASCII.GetString(arg)}'");

        c.Pa.Account = null;
        c.Pa.Subject = tokens[0];
        c.Pa.Arg = arg;
        c.Pa.Queues = null;

        if (tokens.Count == 3)
        {
            c.Pa.Reply = null;
            if (!TryParseSize(tokens[1], out var hdr) || !TryParseSize(tokens[2], out var size))
                return new InvalidOperationException($"processLeafHeaderMsgArgs Bad sizes: '{Encoding.ASCII.GetString(arg)}'");
            if (hdr > size) return ServerErrors.ErrBadMsgHeader;
            c.Pa.HeaderSize = hdr;
            c.Pa.SizeBytes = tokens[2];
            c.Pa.Size = size;
            return null;
        }

        if (tokens.Count == 4)
        {
            c.Pa.Reply = tokens[1];
            if (!TryParseSize(tokens[2], out var hdr) || !TryParseSize(tokens[3], out var size))
                return new InvalidOperationException($"processLeafHeaderMsgArgs Bad sizes: '{Encoding.ASCII.GetString(arg)}'");
            if (hdr > size) return ServerErrors.ErrBadMsgHeader;
            c.Pa.HeaderSize = hdr;
            c.Pa.SizeBytes = tokens[3];
            c.Pa.Size = size;
            return null;
        }

        if (tokens[1].Length != 1 || (tokens[1][0] != (byte)'+' && tokens[1][0] != (byte)'|'))
            return new InvalidOperationException($"processLeafHeaderMsgArgs Bad or Missing Reply Indicator: '{Encoding.ASCII.GetString(tokens[1])}'");

        c.Pa.Reply = tokens[1][0] == (byte)'+' ? tokens[2] : null;
        if (!TryParseSize(tokens[^2], out var hdrSize) || !TryParseSize(tokens[^1], out var totalSize))
            return new InvalidOperationException($"processLeafHeaderMsgArgs Bad sizes: '{Encoding.ASCII.GetString(arg)}'");
        if (hdrSize > totalSize) return ServerErrors.ErrBadMsgHeader;
        c.Pa.HeaderSize = hdrSize;
        c.Pa.SizeBytes = tokens[^1];
        c.Pa.Size = totalSize;

        c.Pa.Queues = [];
        var queueStart = c.Pa.Reply is null ? 2 : 3;
        for (var i = queueStart; i < tokens.Count - 2; i++)
            c.Pa.Queues.Add(tokens[i]);

        return null;
    }

    /// <summary>
    /// Parses LMSG arguments (origin cluster routed messages).
    /// Stub — will be fully implemented with cluster routing support.
    /// </summary>
    public static Exception? ProcessRoutedOriginClusterMsgArgs(ParseContext c, byte[] arg) =>
        ProcessRoutedMsgArgs(c, arg);

    // =====================================================================
    // ClonePubArg
    // =====================================================================

    /// <summary>
    /// Clones the pub arg by re-processing the original arg buffer.
    /// Used in split buffer scenarios.
    /// Mirrors Go <c>client.clonePubArg</c>.
    /// </summary>
    public static Exception? ClonePubArg(ParseContext c, IProtocolHandler handler, bool lmsg)
    {
        if (c.Pa.Arg == null)
            return null;

        c.ArgBuf = (byte[])c.Pa.Arg.Clone();
        var kind = c.Kind;

        if (kind == ClientKind.Router || kind == ClientKind.Gateway)
        {
            if (lmsg)
                return ProcessRoutedOriginClusterMsgArgs(c, c.ArgBuf);
            return c.Pa.HeaderSize < 0
                ? ProcessRoutedMsgArgs(c, c.ArgBuf)
                : ProcessRoutedHeaderMsgArgs(c, c.ArgBuf);
        }

        if (kind == ClientKind.Leaf)
        {
            return c.Pa.HeaderSize < 0
                ? ProcessLeafMsgArgs(c, c.ArgBuf)
                : ProcessLeafHeaderMsgArgs(c, c.ArgBuf);
        }

        return c.Pa.HeaderSize < 0
            ? ProcessPub(c, c.ArgBuf)
            : ProcessHeaderPub(c, c.ArgBuf, null);
    }

    // =====================================================================
    // GetHeader
    // =====================================================================

    /// <summary>
    /// Parses message headers from the message buffer.
    /// Mirrors Go <c>parseState.getHeader</c>.
    /// </summary>
    public static Dictionary<string, List<string>>? GetHeader(ParseContext c)
    {
        if (c.Pa.HeaderSize <= 0 || c.MsgBuf == null)
            return null;

        var headerBytes = c.MsgBuf.AsSpan(0, c.Pa.HeaderSize);
        var result = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
        var text = Encoding.ASCII.GetString(headerBytes);
        var lines = text.Split('\n');

        // Skip first line (contains version, e.g. "NATS/1.0\r")
        for (var i = 1; i < lines.Length; i++)
        {
            var line = lines[i].TrimEnd('\r');
            if (string.IsNullOrEmpty(line))
                break;
            var colonIdx = line.IndexOf(':');
            if (colonIdx <= 0) continue;
            var key = line[..colonIdx].Trim();
            var value = line[(colonIdx + 1)..].Trim();
            if (!result.TryGetValue(key, out var values))
            {
                values = [];
                result[key] = values;
            }
            values.Add(value);
        }

        return result;
    }

    // =====================================================================
    // Internal helpers
    // =====================================================================

    /// <summary>Collects the argument bytes, using argBuf if available (split buffer case).</summary>
    private static byte[] CollectArg(ParseContext c, byte[] buf, int i)
    {
        if (c.ArgBuf != null)
        {
            var arg = c.ArgBuf;
            c.ArgBuf = null;
            return arg;
        }

        var end = i - c.Drop;
        var len = end - c.ArgStart;
        if (len <= 0) return [];
        var result = new byte[len];
        Array.Copy(buf, c.ArgStart, result, 0, len);
        return result;
    }

    /// <summary>Appends a byte to the arg buffer.</summary>
    private static void AppendToArgBuf(ParseContext c, byte b)
    {
        var old = c.ArgBuf!;
        var newBuf = new byte[old.Length + 1];
        Array.Copy(old, newBuf, old.Length);
        newBuf[^1] = b;
        c.ArgBuf = newBuf;
    }

    /// <summary>Appends a byte to the msg buffer.</summary>
    private static void AppendToMsgBuf(ParseContext c, byte b)
    {
        var old = c.MsgBuf!;
        var newBuf = new byte[old.Length + 1];
        Array.Copy(old, newBuf, old.Length);
        newBuf[^1] = b;
        c.MsgBuf = newBuf;
    }

    /// <summary>
    /// Splits argument bytes into tokens separated by spaces/tabs.
    /// Mirrors the "unrolled splitArgs" pattern in Go client.go.
    /// </summary>
    internal static List<byte[]> SplitArgs(byte[] arg)
    {
        var tokens = new List<byte[]>(6);
        var i = 0;
        var len = arg.Length;

        while (i < len)
        {
            // Skip whitespace (including \r and \n, matching Go's splitArg)
            while (i < len && (arg[i] == ' ' || arg[i] == '\t' || arg[i] == '\r' || arg[i] == '\n'))
                i++;
            if (i >= len) break;

            // Collect token
            var start = i;
            while (i < len && arg[i] != ' ' && arg[i] != '\t' && arg[i] != '\r' && arg[i] != '\n')
                i++;
            tokens.Add(arg[start..i]);
        }

        return tokens;
    }

    /// <summary>
    /// Parses a size value from ASCII digit bytes. Returns false on overflow or non-digit chars.
    /// </summary>
    internal static bool TryParseSize(byte[] bytes, out int size)
    {
        size = 0;
        if (bytes.Length == 0) return false;

        foreach (var b in bytes)
        {
            if (b < '0' || b > '9') return false;
            var prev = size;
            size = size * 10 + (b - '0');
            if (size < prev) return false; // overflow
        }

        return true;
    }

    /// <summary>Helper to convert nullable byte[] to string.</summary>
    private static string AsString(byte[]? bytes) =>
        bytes == null ? "" : Encoding.ASCII.GetString(bytes);
}
