package mqtt

import (
	"bufio"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
)

// MQTT 3.1.1 control packet types (§2.2.1). Only the ones this client needs to
// send or recognize are named; anything else on the wire is skipped.
const (
	pktConnect     byte = 1
	pktConnack     byte = 2
	pktPublish     byte = 3
	pktPuback      byte = 4
	pktPubrec      byte = 5
	pktPubrel      byte = 6
	pktPubcomp     byte = 7
	pktSubscribe   byte = 8
	pktSuback      byte = 9
	pktUnsubscribe byte = 10
	pktUnsuback    byte = 11
	pktPingreq     byte = 12
	pktPingresp    byte = 13
	pktDisconnect  byte = 14
)

// protocolName / protocolLevel identify MQTT 3.1.1 in the CONNECT packet (§3.1.2).
const (
	protocolName  = "MQTT"
	protocolLevel = 4
)

// maxRemainingLength is the largest value the four-byte varint can express
// (§2.2.3). maxPacketBytes is the much tighter ceiling this client accepts from a
// broker: Greencell telemetry frames are a few dozen bytes, so a megabyte is
// generous while still bounding a hostile or broken peer.
const (
	maxRemainingLength = 268435455
	maxPacketBytes     = 1 << 20
)

// errPacketTooLarge is returned when a broker announces a packet beyond
// maxPacketBytes; the connection is unusable afterwards because the stream can no
// longer be framed, so the read loop treats it as fatal.
var errPacketTooLarge = errors.New("mqtt: packet exceeds size limit")

// packet is one decoded control packet: its type, the four header flag bits, and
// the variable header plus payload as a single buffer for the caller to parse.
type packet struct {
	typ   byte
	flags byte
	body  []byte
}

// encodeUint16 appends a two-byte big-endian integer, the encoding MQTT uses for
// packet identifiers and the keep-alive.
func encodeUint16(b []byte, v uint16) []byte {
	return binary.BigEndian.AppendUint16(b, v)
}

// encodeString appends an MQTT UTF-8 string: a two-byte length followed by the
// bytes (§1.5.3). Strings longer than 65535 bytes cannot be represented and are
// rejected by the callers that build packets.
func encodeString(b []byte, s string) []byte {
	b = encodeUint16(b, uint16(len(s)))
	return append(b, s...)
}

// encodeRemainingLength appends the variable-length integer that gives the number
// of bytes after the fixed header (§2.2.3).
func encodeRemainingLength(b []byte, n int) []byte {
	for {
		digit := byte(n % 128)
		n /= 128
		if n > 0 {
			digit |= 0x80
		}
		b = append(b, digit)
		if n == 0 {
			return b
		}
	}
}

// buildPacket frames a body as a complete control packet.
func buildPacket(typ, flags byte, body []byte) []byte {
	out := make([]byte, 0, len(body)+5)
	out = append(out, typ<<4|flags)
	out = encodeRemainingLength(out, len(body))
	return append(out, body...)
}

// readRemainingLength decodes the fixed header's variable-length integer.
func readRemainingLength(br *bufio.Reader) (int, error) {
	var (
		value      int
		multiplier = 1
	)
	for i := 0; i < 4; i++ {
		digit, err := br.ReadByte()
		if err != nil {
			return 0, err
		}
		value += int(digit&0x7F) * multiplier
		if digit&0x80 == 0 {
			return value, nil
		}
		multiplier *= 128
	}
	return 0, errors.New("mqtt: malformed remaining length")
}

// readPacket reads one whole control packet off the wire.
func readPacket(br *bufio.Reader) (packet, error) {
	head, err := br.ReadByte()
	if err != nil {
		return packet{}, err
	}
	n, err := readRemainingLength(br)
	if err != nil {
		return packet{}, err
	}
	if n > maxPacketBytes {
		return packet{}, fmt.Errorf("%w: %d bytes", errPacketTooLarge, n)
	}
	body := make([]byte, n)
	if _, err := io.ReadFull(br, body); err != nil {
		return packet{}, err
	}
	return packet{typ: head >> 4, flags: head & 0x0F, body: body}, nil
}

// readString decodes an MQTT UTF-8 string from the front of b and returns it with
// the remainder.
func readString(b []byte) (string, []byte, error) {
	if len(b) < 2 {
		return "", nil, errors.New("mqtt: truncated string length")
	}
	n := int(binary.BigEndian.Uint16(b))
	if len(b) < 2+n {
		return "", nil, errors.New("mqtt: truncated string")
	}
	return string(b[2 : 2+n]), b[2+n:], nil
}

// connackError maps a CONNACK return code (§3.2.2.3) to an error, or nil when the
// broker accepted the connection.
func connackError(code byte) error {
	switch code {
	case 0:
		return nil
	case 1:
		return errors.New("mqtt: broker refused the connection: unacceptable protocol version")
	case 2:
		return errors.New("mqtt: broker refused the connection: client identifier rejected")
	case 3:
		return errors.New("mqtt: broker refused the connection: server unavailable")
	case 4:
		return errors.New("mqtt: broker refused the connection: bad username or password")
	case 5:
		return errors.New("mqtt: broker refused the connection: not authorized")
	default:
		return fmt.Errorf("mqtt: broker refused the connection: code %d", code)
	}
}
