// Binary MQTT packet body decoder.
// Go reference: golang/nats-server/server/mqtt.go
//   CONNECT parsing  — mqttParseSub / mqttParseConnect  (lines ~700–850)
//   PUBLISH parsing  — mqttParsePublish                 (lines ~1200–1300)
//   SUBSCRIBE parsing — mqttParseSub                    (lines ~1400–1500)
//   Wildcard translation — mqttToNATSSubjectConversion   (lines ~2200–2250)

namespace NATS.Server.Mqtt;

/// <summary>
/// Decoded fields from an MQTT CONNECT packet body.
/// Go reference: server/mqtt.go mqttParseConnect ~line 700.
/// </summary>
public readonly record struct MqttConnectInfo(
    string ProtocolName,
    byte ProtocolLevel,
    bool CleanSession,
    ushort KeepAlive,
    string ClientId,
    string? WillTopic,
    byte[]? WillMessage,
    byte WillQoS,
    bool WillRetain,
    string? Username,
    string? Password);

/// <summary>
/// Decoded fields from an MQTT PUBLISH packet body.
/// Go reference: server/mqtt.go mqttParsePublish ~line 1200.
/// </summary>
public readonly record struct MqttPublishInfo(
    string Topic,
    ushort PacketId,
    byte QoS,
    bool Dup,
    bool Retain,
    ReadOnlyMemory<byte> Payload);

/// <summary>
/// Decoded fields from an MQTT SUBSCRIBE packet body.
/// Go reference: server/mqtt.go mqttParseSub ~line 1400.
/// </summary>
public readonly record struct MqttSubscribeInfo(
    ushort PacketId,
    IReadOnlyList<(string TopicFilter, byte QoS)> Filters);

/// <summary>
/// Decodes the variable-header and payload of CONNECT, PUBLISH, and SUBSCRIBE
/// MQTT 3.1.1 control packets, and translates MQTT wildcards to NATS subjects.
/// </summary>
public static class MqttBinaryDecoder
{
    // -------------------------------------------------------------------------
    // CONNECT parsing
    // Go reference: server/mqtt.go mqttParseConnect ~line 700
    // -------------------------------------------------------------------------

    /// <summary>
    /// Parses the payload bytes of an MQTT CONNECT packet (everything after the
    /// fixed header and remaining-length bytes, i.e. the value of
    /// <see cref="MqttControlPacket.Payload"/>).
    /// </summary>
    /// <param name="payload">
    /// The payload bytes as returned by <see cref="MqttPacketReader.Read"/>.
    /// </param>
    /// <returns>A populated <see cref="MqttConnectInfo"/>.</returns>
    /// <exception cref="FormatException">
    /// Thrown when the packet is malformed or the protocol name is not "MQTT".
    /// </exception>
    public static MqttConnectInfo ParseConnect(ReadOnlySpan<byte> payload)
    {
        // Variable header layout (MQTT 3.1.1 spec §3.1):
        //   2-byte length prefix + protocol name bytes ("MQTT")
        //   1 byte  protocol level  (4 = 3.1.1, 5 = 5.0)
        //   1 byte  connect flags
        //   2 bytes keepalive (big-endian)
        // Payload:
        //   2+N  client ID
        //   if will flag: 2+N will topic, 2+N will message
        //   if username:  2+N username
        //   if password:  2+N password

        var pos = 0;

        // Protocol name
        var protocolName = ReadUtf8String(payload, ref pos);
        if (protocolName != "MQTT" && protocolName != "MQIsdp")
            throw new FormatException($"Unknown MQTT protocol name: '{protocolName}'");

        if (pos + 4 > payload.Length)
            throw new FormatException("MQTT CONNECT packet too short for variable header.");

        var protocolLevel = payload[pos++];

        // Connect flags byte
        // Bit 1 = CleanSession, Bit 2 = WillFlag, Bits 3-4 = WillQoS, Bit 5 = WillRetain,
        // Bit 6 = PasswordFlag, Bit 7 = UsernameFlag
        var connectFlags = payload[pos++];
        var cleanSession = (connectFlags & 0x02) != 0;
        var willFlag     = (connectFlags & 0x04) != 0;
        var willQoS      = (byte)((connectFlags >> 3) & 0x03);
        var willRetain   = (connectFlags & 0x20) != 0;
        var passwordFlag = (connectFlags & 0x40) != 0;
        var usernameFlag = (connectFlags & 0x80) != 0;

        // Keep-alive (big-endian uint16)
        var keepAlive = ReadUInt16BigEndian(payload, ref pos);

        // Payload fields
        var clientId = ReadUtf8String(payload, ref pos);

        string? willTopic = null;
        byte[]? willMessage = null;
        if (willFlag)
        {
            willTopic   = ReadUtf8String(payload, ref pos);
            willMessage = ReadBinaryField(payload, ref pos);
        }

        string? username = null;
        if (usernameFlag)
            username = ReadUtf8String(payload, ref pos);

        string? password = null;
        if (passwordFlag)
            password = ReadUtf8String(payload, ref pos);

        return new MqttConnectInfo(
            ProtocolName:  protocolName,
            ProtocolLevel: protocolLevel,
            CleanSession:  cleanSession,
            KeepAlive:     keepAlive,
            ClientId:      clientId,
            WillTopic:     willTopic,
            WillMessage:   willMessage,
            WillQoS:       willQoS,
            WillRetain:    willRetain,
            Username:      username,
            Password:      password);
    }

    // -------------------------------------------------------------------------
    // PUBLISH parsing
    // Go reference: server/mqtt.go mqttParsePublish ~line 1200
    // -------------------------------------------------------------------------

    /// <summary>
    /// Parses the payload bytes of an MQTT PUBLISH packet.
    /// The <paramref name="flags"/> nibble comes from
    /// <see cref="MqttControlPacket.Flags"/> of the fixed header.
    /// </summary>
    /// <param name="payload">The payload bytes from <see cref="MqttControlPacket.Payload"/>.</param>
    /// <param name="flags">The lower nibble of the fixed header byte (DUP/QoS/RETAIN flags).</param>
    /// <returns>A populated <see cref="MqttPublishInfo"/>.</returns>
    public static MqttPublishInfo ParsePublish(ReadOnlySpan<byte> payload, byte flags)
    {
        // Fixed-header flags nibble layout (MQTT 3.1.1 spec §3.3.1):
        //   Bit 3 = DUP
        //   Bits 2-1 = QoS (0, 1, or 2)
        //   Bit 0 = RETAIN
        var dup    = (flags & 0x08) != 0;
        var qos    = (byte)((flags >> 1) & 0x03);
        var retain = (flags & 0x01) != 0;

        var pos = 0;

        // Variable header: topic name (2-byte length prefix + UTF-8)
        var topic = ReadUtf8String(payload, ref pos);

        // Packet identifier — only present for QoS > 0
        ushort packetId = 0;
        if (qos > 0)
            packetId = ReadUInt16BigEndian(payload, ref pos);

        // Remaining bytes are the application payload
        var messagePayload = payload[pos..].ToArray();

        return new MqttPublishInfo(
            Topic:   topic,
            PacketId: packetId,
            QoS:     qos,
            Dup:     dup,
            Retain:  retain,
            Payload: messagePayload);
    }

    // -------------------------------------------------------------------------
    // SUBSCRIBE parsing
    // Go reference: server/mqtt.go mqttParseSub ~line 1400
    // -------------------------------------------------------------------------

    /// <summary>
    /// Parses the payload bytes of an MQTT SUBSCRIBE packet.
    /// </summary>
    /// <param name="payload">The payload bytes from <see cref="MqttControlPacket.Payload"/>.</param>
    /// <param name="flags">
    /// Optional fixed-header flags nibble. When provided, must match SUBSCRIBE flags (0x02).
    /// </param>
    /// <returns>A populated <see cref="MqttSubscribeInfo"/>.</returns>
    public static MqttSubscribeInfo ParseSubscribe(ReadOnlySpan<byte> payload, byte? flags = null)
    {
        if (flags.HasValue && flags.Value != MqttProtocolConstants.SubscribeFlags)
            throw new FormatException("MQTT SUBSCRIBE packet has invalid fixed-header flags.");

        // Variable header: packet identifier (2 bytes, big-endian)
        // Payload: one or more topic-filter entries, each:
        //   2-byte length prefix + UTF-8 filter string + 1-byte requested QoS

        var pos = 0;
        var packetId = ReadUInt16BigEndian(payload, ref pos);

        var filters = new List<(string, byte)>();
        while (pos < payload.Length)
        {
            var topicFilter = ReadUtf8String(payload, ref pos);
            if (pos >= payload.Length)
                throw new FormatException("MQTT SUBSCRIBE packet missing QoS byte after topic filter.");
            var filterQoS = payload[pos++];
            filters.Add((topicFilter, filterQoS));
        }

        return new MqttSubscribeInfo(packetId, filters);
    }

    // -------------------------------------------------------------------------
    // MQTT wildcard → NATS subject translation
    // Go reference: server/mqtt.go mqttToNATSSubjectConversion ~line 2200
    //
    // Simple translation (filter → NATS, wildcards permitted):
    //   '+'  →  '*'   (single-level wildcard)
    //   '#'  →  '>'   (multi-level wildcard)
    //   '/'  →  '.'   (topic separator)
    //
    // NOTE: This method implements the simple/naïve translation that the task
    // description specifies.  The full Go implementation also handles dots,
    // leading/trailing slashes, and empty levels differently (see
    // MqttTopicMappingParityTests for the complete behavior).  This method is
    // intentionally limited to the four rules requested by the task spec.
    // -------------------------------------------------------------------------

    /// <summary>
    /// Translates an MQTT topic filter to a NATS subject using the simple rules:
    /// <list type="bullet">
    ///   <item><c>+</c> → <c>*</c> (single-level wildcard)</item>
    ///   <item><c>#</c> → <c>&gt;</c> (multi-level wildcard)</item>
    ///   <item><c>/</c> → <c>.</c> (separator)</item>
    /// </list>
    /// Go reference: server/mqtt.go mqttToNATSSubjectConversion ~line 2200.
    /// </summary>
    /// <param name="mqttFilter">An MQTT topic filter string.</param>
    /// <returns>The equivalent NATS subject string.</returns>
    public static string TranslateFilterToNatsSubject(string mqttFilter)
    {
        if (mqttFilter.Length == 0)
            return string.Empty;

        var result = new char[mqttFilter.Length];
        for (var i = 0; i < mqttFilter.Length; i++)
        {
            result[i] = mqttFilter[i] switch
            {
                '+' => '*',
                '#' => '>',
                '/' => '.',
                var c => c,
            };
        }

        return new string(result);
    }

    // -------------------------------------------------------------------------
    // Internal helpers
    // -------------------------------------------------------------------------

    /// <summary>
    /// Reads a 2-byte big-endian length-prefixed UTF-8 string from
    /// <paramref name="data"/> starting at <paramref name="pos"/>, advancing
    /// <paramref name="pos"/> past the consumed bytes.
    /// </summary>
    private static string ReadUtf8String(ReadOnlySpan<byte> data, ref int pos)
    {
        if (pos + 2 > data.Length)
            throw new FormatException("MQTT packet truncated reading string length prefix.");

        var length = (data[pos] << 8) | data[pos + 1];
        pos += 2;

        if (pos + length > data.Length)
            throw new FormatException("MQTT packet truncated reading string body.");

        var value = System.Text.Encoding.UTF8.GetString(data.Slice(pos, length));
        pos += length;
        return value;
    }

    /// <summary>
    /// Reads a 2-byte big-endian length-prefixed binary field (e.g. will
    /// message, password) from <paramref name="data"/>, advancing
    /// <paramref name="pos"/> past the consumed bytes.
    /// </summary>
    private static byte[] ReadBinaryField(ReadOnlySpan<byte> data, ref int pos)
    {
        if (pos + 2 > data.Length)
            throw new FormatException("MQTT packet truncated reading binary field length prefix.");

        var length = (data[pos] << 8) | data[pos + 1];
        pos += 2;

        if (pos + length > data.Length)
            throw new FormatException("MQTT packet truncated reading binary field body.");

        var value = data.Slice(pos, length).ToArray();
        pos += length;
        return value;
    }

    /// <summary>
    /// Reads a big-endian uint16 from <paramref name="data"/> at
    /// <paramref name="pos"/>, advancing <paramref name="pos"/> by 2.
    /// </summary>
    private static ushort ReadUInt16BigEndian(ReadOnlySpan<byte> data, ref int pos)
    {
        if (pos + 2 > data.Length)
            throw new FormatException("MQTT packet truncated reading uint16.");

        var value = (ushort)((data[pos] << 8) | data[pos + 1]);
        pos += 2;
        return value;
    }
}
