// Copyright 2020-2026 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/mqtt.go mqttParse() in the NATS server Go source.

namespace ZB.MOM.NatsNet.Server.Mqtt;

/// <summary>
/// MQTT binary packet parser and dispatch.
/// Reads packets from a byte buffer using <see cref="MqttReader"/> and dispatches
/// to the appropriate handler based on packet type.
/// Mirrors Go <c>mqttParse()</c> in server/mqtt.go.
/// </summary>
internal static class MqttParser
{
    /// <summary>PINGRESP packet bytes: 0xD0 0x00.</summary>
    private static readonly byte[] PingRespPacket = [MqttPacket.PingResp, 0x00];

    /// <summary>
    /// Parses MQTT packets from <paramref name="buf"/> and dispatches to handlers.
    /// Returns null on success, or an exception describing the parse/dispatch error.
    /// Handles partial packets by saving state in the client's <see cref="MqttReader"/>.
    /// Mirrors Go <c>mqttParse(r *mqttReader, c *client, ...)</c>.
    /// </summary>
    public static Exception? Parse(ClientConnection c, byte[] buf, int len)
    {
        var mqtt = c.Mqtt!;
        var r = mqtt.Reader;

        // Slice buffer to actual length if needed.
        if (len < buf.Length)
        {
            var tmp = new byte[len];
            Buffer.BlockCopy(buf, 0, tmp, 0, len);
            buf = tmp;
        }

        r.Reset(buf);

        var connected = (c.Flags & ClientFlags.ConnectReceived) != 0;
        Exception? err = null;

        while (err == null && r.HasMore())
        {
            r.PacketStart = r.Position;

            // Read packet type + flags byte.
            byte b;
            try { b = r.ReadByte("packet type"); }
            catch (Exception ex) { err = ex; break; }

            var pt = (byte)(b & MqttPacket.Mask);

            // CONNECT must be the first packet.
            if (!connected && pt != MqttPacket.Connect)
            {
                err = new InvalidOperationException(
                    $"the first packet should be a CONNECT (0x{MqttPacket.Connect:X2}), got 0x{pt:X2}");
                break;
            }

            // Read remaining length (variable-length encoding).
            int pl;
            bool complete;
            try
            {
                (pl, complete) = r.ReadPacketLen();
            }
            catch (Exception ex) { err = ex; break; }

            if (!complete)
                break; // Partial packet — state saved in reader.

            // Dispatch based on packet type.
            switch (pt)
            {
                case MqttPacket.Pub:
                    var pubFlags = (byte)(b & MqttPacket.FlagMask);
                    var (pp, pubErr) = MqttPacketHandlers.ParsePub(r, pl, pubFlags, mqtt.RejectQoS2Pub);
                    if (pubErr != null) { err = pubErr; break; }
                    err = MqttPacketHandlers.ProcessPub(c, pp!);
                    break;

                case MqttPacket.PubAck:
                {
                    var (ackPi, ackErr) = MqttPacketHandlers.ParsePiPacket(r);
                    if (ackErr != null) { err = ackErr; break; }
                    err = MqttPacketHandlers.ProcessPubAck(c, ackPi);
                    break;
                }

                case MqttPacket.PubRec:
                {
                    var (recPi, recErr) = MqttPacketHandlers.ParsePiPacket(r);
                    if (recErr != null) { err = recErr; break; }
                    err = MqttPacketHandlers.ProcessPubRec(c, recPi);
                    break;
                }

                case MqttPacket.PubRel:
                {
                    var (relPi, relErr) = MqttPacketHandlers.ParsePiPacket(r);
                    if (relErr != null) { err = relErr; break; }
                    err = MqttPacketHandlers.ProcessPubRel(c, relPi);
                    break;
                }

                case MqttPacket.PubComp:
                {
                    var (compPi, compErr) = MqttPacketHandlers.ParsePiPacket(r);
                    if (compErr != null) { err = compErr; break; }
                    err = MqttPacketHandlers.ProcessPubComp(c, compPi);
                    break;
                }

                case MqttPacket.Sub:
                    var (subPi, subFilters, subErr) = MqttPacketHandlers.ParseSubsOrUnsubs(r, b, pl, isSub: true);
                    if (subErr != null) { err = subErr; break; }
                    err = MqttPacketHandlers.ProcessSubs(c, subPi, subFilters!);
                    break;

                case MqttPacket.Unsub:
                    var (unsubPi, unsubFilters, unsubErr) = MqttPacketHandlers.ParseSubsOrUnsubs(r, b, pl, isSub: false);
                    if (unsubErr != null) { err = unsubErr; break; }
                    err = MqttPacketHandlers.ProcessUnsubs(c, unsubPi, unsubFilters!);
                    break;

                case MqttPacket.Ping:
                    HandlePingReq(c);
                    break;

                case MqttPacket.Connect:
                    if (connected)
                    {
                        err = new InvalidOperationException("second CONNECT packet not allowed");
                        break;
                    }
                    var (rc, cp, parseErr) = MqttPacketHandlers.ParseConnect(r);
                    if (parseErr != null)
                    {
                        // Send CONNACK with error code if we have one, then close.
                        if (rc != MqttConnAckRc.Accepted)
                            MqttPacketHandlers.EnqueueConnAck(c, rc, false);
                        err = parseErr;
                        break;
                    }
                    if (rc != MqttConnAckRc.Accepted)
                    {
                        MqttPacketHandlers.EnqueueConnAck(c, rc, false);
                        err = new InvalidOperationException($"CONNECT rejected with code 0x{rc:X2}");
                        break;
                    }
                    err = MqttPacketHandlers.ProcessConnect(c, cp!);
                    if (err == null)
                        connected = true;
                    break;

                case MqttPacket.Disconnect:
                    MqttPacketHandlers.HandleDisconnect(c);
                    return null; // Connection closed, exit parse loop.

                default:
                    err = new InvalidOperationException($"unknown MQTT packet type: 0x{pt:X2}");
                    break;
            }
        }

        return err;
    }

    /// <summary>
    /// Handles PINGREQ by enqueueing a PINGRESP packet.
    /// Mirrors Go <c>mqttEnqueuePingResp()</c>.
    /// </summary>
    private static void HandlePingReq(ClientConnection c)
    {
        lock (c)
        {
            c.EnqueueProto(PingRespPacket);
        }
    }
}
