// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2022 mochi-co
// SPDX-FileContributor: mochi-co

package packets

import (
	"bytes"
	"fmt"
	"strings"
)

const (
	PropPayloadFormat          byte = 1
	PropMessageExpiryInterval  byte = 2
	PropContentType            byte = 3
	PropResponseTopic          byte = 8
	PropCorrelationData        byte = 9
	PropSubscriptionIdentifier byte = 11
	PropSessionExpiryInterval  byte = 17
	PropAssignedClientID       byte = 18
	PropServerKeepAlive        byte = 19
	PropAuthenticationMethod   byte = 21
	PropAuthenticationData     byte = 22
	PropRequestProblemInfo     byte = 23
	PropWillDelayInterval      byte = 24
	PropRequestResponseInfo    byte = 25
	PropResponseInfo           byte = 26
	PropServerReference        byte = 28
	PropReasonString           byte = 31
	PropReceiveMaximum         byte = 33
	PropTopicAliasMaximum      byte = 34
	PropTopicAlias             byte = 35
	PropMaximumQos             byte = 36
	PropRetainAvailable        byte = 37
	PropUser                   byte = 38
	PropMaximumPacketSize      byte = 39
	PropWildcardSubAvailable   byte = 40
	PropSubIDAvailable         byte = 41
	PropSharedSubAvailable     byte = 42
)

// validPacketProperties indicates which properties are valid for which packet types.
var validPacketProperties = map[byte]map[byte]byte{
	PropPayloadFormat:          {Publish: 1},
	PropMessageExpiryInterval:  {Publish: 1},
	PropContentType:            {Publish: 1},
	PropResponseTopic:          {Publish: 1},
	PropCorrelationData:        {Publish: 1},
	PropSubscriptionIdentifier: {Publish: 1, Subscribe: 1},
	PropSessionExpiryInterval:  {Connect: 1, Connack: 1, Disconnect: 1},
	PropAssignedClientID:       {Connack: 1},
	PropServerKeepAlive:        {Connack: 1},
	PropAuthenticationMethod:   {Connect: 1, Connack: 1, Auth: 1},
	PropAuthenticationData:     {Connect: 1, Connack: 1, Auth: 1},
	PropRequestProblemInfo:     {Connect: 1},
	PropWillDelayInterval:      {Connect: 1},
	PropRequestResponseInfo:    {Connect: 1},
	PropResponseInfo:           {Connack: 1},
	PropServerReference:        {Connack: 1, Disconnect: 1},
	PropReasonString:           {Connack: 1, Puback: 1, Pubrec: 1, Pubrel: 1, Pubcomp: 1, Suback: 1, Unsuback: 1, Disconnect: 1, Auth: 1},
	PropReceiveMaximum:         {Connect: 1, Connack: 1},
	PropTopicAliasMaximum:      {Connect: 1, Connack: 1},
	PropTopicAlias:             {Publish: 1},
	PropMaximumQos:             {Connack: 1},
	PropRetainAvailable:        {Connack: 1},
	PropUser:                   {Connect: 1, Connack: 1, Publish: 1, Puback: 1, Pubrec: 1, Pubrel: 1, Pubcomp: 1, Subscribe: 1, Suback: 1, Unsubscribe: 1, Unsuback: 1, Disconnect: 1, Auth: 1},
	PropMaximumPacketSize:      {Connect: 1, Connack: 1},
	PropWildcardSubAvailable:   {Connack: 1},
	PropSubIDAvailable:         {Connack: 1},
	PropSharedSubAvailable:     {Connack: 1},
}

// UserProperty is an arbitrary key-value pair for a packet user properties array.
type UserProperty struct { // [MQTT-1.5.7-1]
	Key string `json:"k"`
	Val string `json:"v"`
}

// Properties contains all of the mqtt v5 properties available for a packet.
// Some properties have valid values of 0 or not-present. In this case, we opt for
// property flags to indicate the usage of property.
// Refer to mqtt v5 2.2.2.2 Property spec for more information.
type Properties struct {
	CorrelationData           []byte         `json:"cd"`
	SubscriptionIdentifier    []int          `json:"si"`
	AuthenticationData        []byte         `json:"ad"`
	User                      []UserProperty `json:"user"`
	ContentType               string         `json:"ct"`
	ResponseTopic             string         `json:"rt"`
	AssignedClientID          string         `json:"aci"`
	AuthenticationMethod      string         `json:"am"`
	ResponseInfo              string         `json:"ri"`
	ServerReference           string         `json:"sr"`
	ReasonString              string         `json:"rs"`
	MessageExpiryInterval     uint32         `json:"me"`
	SessionExpiryInterval     uint32         `json:"sei"`
	WillDelayInterval         uint32         `json:"wdi"`
	MaximumPacketSize         uint32         `json:"mps"`
	ServerKeepAlive           uint16         `json:"ska"`
	ReceiveMaximum            uint16         `json:"rm"`
	TopicAliasMaximum         uint16         `json:"tam"`
	TopicAlias                uint16         `json:"ta"`
	PayloadFormat             byte           `json:"pf"`
	PayloadFormatFlag         bool           `json:"fpf"`
	SessionExpiryIntervalFlag bool           `json:"fsei"`
	ServerKeepAliveFlag       bool           `json:"fska"`
	RequestProblemInfo        byte           `json:"rpi"`
	RequestProblemInfoFlag    bool           `json:"frpi"`
	RequestResponseInfo       byte           `json:"rri"`
	TopicAliasFlag            bool           `json:"fta"`
	MaximumQos                byte           `json:"mqos"`
	MaximumQosFlag            bool           `json:"fmqos"`
	RetainAvailable           byte           `json:"ra"`
	RetainAvailableFlag       bool           `json:"fra"`
	WildcardSubAvailable      byte           `json:"wsa"`
	WildcardSubAvailableFlag  bool           `json:"fwsa"`
	SubIDAvailable            byte           `json:"sida"`
	SubIDAvailableFlag        bool           `json:"fsida"`
	SharedSubAvailable        byte           `json:"ssa"`
	SharedSubAvailableFlag    bool           `json:"fssa"`
}

// Copy creates a new Properties struct with copies of the values.
func (p *Properties) Copy(allowTransfer bool) Properties {
	pr := Properties{
		PayloadFormat:             p.PayloadFormat, // [MQTT-3.3.2-4]
		PayloadFormatFlag:         p.PayloadFormatFlag,
		MessageExpiryInterval:     p.MessageExpiryInterval,
		ContentType:               p.ContentType,   // [MQTT-3.3.2-20]
		ResponseTopic:             p.ResponseTopic, // [MQTT-3.3.2-15]
		SessionExpiryInterval:     p.SessionExpiryInterval,
		SessionExpiryIntervalFlag: p.SessionExpiryIntervalFlag,
		AssignedClientID:          p.AssignedClientID,
		ServerKeepAlive:           p.ServerKeepAlive,
		ServerKeepAliveFlag:       p.ServerKeepAliveFlag,
		AuthenticationMethod:      p.AuthenticationMethod,
		RequestProblemInfo:        p.RequestProblemInfo,
		RequestProblemInfoFlag:    p.RequestProblemInfoFlag,
		WillDelayInterval:         p.WillDelayInterval,
		RequestResponseInfo:       p.RequestResponseInfo,
		ResponseInfo:              p.ResponseInfo,
		ServerReference:           p.ServerReference,
		ReasonString:              p.ReasonString,
		ReceiveMaximum:            p.ReceiveMaximum,
		TopicAliasMaximum:         p.TopicAliasMaximum,
		TopicAlias:                0, // NB; do not copy topic alias [MQTT-3.3.2-7] + we do not send to clients (currently) [MQTT-3.1.2-26] [MQTT-3.1.2-27]
		MaximumQos:                p.MaximumQos,
		MaximumQosFlag:            p.MaximumQosFlag,
		RetainAvailable:           p.RetainAvailable,
		RetainAvailableFlag:       p.RetainAvailableFlag,
		MaximumPacketSize:         p.MaximumPacketSize,
		WildcardSubAvailable:      p.WildcardSubAvailable,
		WildcardSubAvailableFlag:  p.WildcardSubAvailableFlag,
		SubIDAvailable:            p.SubIDAvailable,
		SubIDAvailableFlag:        p.SubIDAvailableFlag,
		SharedSubAvailable:        p.SharedSubAvailable,
		SharedSubAvailableFlag:    p.SharedSubAvailableFlag,
	}

	if allowTransfer {
		pr.TopicAlias = p.TopicAlias
		pr.TopicAliasFlag = p.TopicAliasFlag
	}

	if len(p.CorrelationData) > 0 {
		pr.CorrelationData = append([]byte{}, p.CorrelationData...) // [MQTT-3.3.2-16]
	}

	if len(p.SubscriptionIdentifier) > 0 {
		pr.SubscriptionIdentifier = append([]int{}, p.SubscriptionIdentifier...)
	}

	if len(p.AuthenticationData) > 0 {
		pr.AuthenticationData = append([]byte{}, p.AuthenticationData...)
	}

	if len(p.User) > 0 {
		pr.User = []UserProperty{}
		for _, v := range p.User {
			pr.User = append(pr.User, UserProperty{ // [MQTT-3.3.2-17]
				Key: v.Key,
				Val: v.Val,
			})
		}
	}

	return pr
}

// canEncode returns true if the property type is valid for the packet type.
func (p *Properties) canEncode(pkt byte, k byte) bool {
	return validPacketProperties[k][pkt] == 1
}

// Encode encodes properties into a bytes buffer.
func (p *Properties) Encode(pk *Packet, b *bytes.Buffer, n int) {
	if p == nil {
		return
	}

	var buf bytes.Buffer
	pkt := pk.FixedHeader.Type

	if p.canEncode(pkt, PropPayloadFormat) && p.PayloadFormatFlag {
		buf.WriteByte(PropPayloadFormat)
		buf.WriteByte(p.PayloadFormat)
	}

	if p.canEncode(pkt, PropMessageExpiryInterval) && p.MessageExpiryInterval > 0 {
		buf.WriteByte(PropMessageExpiryInterval)
		buf.Write(encodeUint32(p.MessageExpiryInterval))
	}

	if p.canEncode(pkt, PropContentType) && p.ContentType != "" {
		buf.WriteByte(PropContentType)
		buf.Write(encodeString(p.ContentType)) // [MQTT-3.3.2-19]
	}

	if pk.Mods.AllowResponseInfo && p.canEncode(pkt, PropResponseTopic) && //  [MQTT-3.3.2-14]
		p.ResponseTopic != "" && !strings.ContainsAny(p.ResponseTopic, "+#") { // [MQTT-3.1.2-28]
		buf.WriteByte(PropResponseTopic)
		buf.Write(encodeString(p.ResponseTopic)) // [MQTT-3.3.2-13]
	}

	if pk.Mods.AllowResponseInfo && p.canEncode(pkt, PropCorrelationData) && len(p.CorrelationData) > 0 { // [MQTT-3.1.2-28]
		buf.WriteByte(PropCorrelationData)
		buf.Write(encodeBytes(p.CorrelationData))
	}

	if p.canEncode(pkt, PropSubscriptionIdentifier) && len(p.SubscriptionIdentifier) > 0 {
		for _, v := range p.SubscriptionIdentifier {
			if v > 0 {
				buf.WriteByte(PropSubscriptionIdentifier)
				encodeLength(&buf, int64(v))
			}
		}
	}

	if p.canEncode(pkt, PropSessionExpiryInterval) && p.SessionExpiryIntervalFlag { // [MQTT-3.14.2-2]
		buf.WriteByte(PropSessionExpiryInterval)
		buf.Write(encodeUint32(p.SessionExpiryInterval))
	}

	if p.canEncode(pkt, PropAssignedClientID) && p.AssignedClientID != "" {
		buf.WriteByte(PropAssignedClientID)
		buf.Write(encodeString(p.AssignedClientID))
	}

	if p.canEncode(pkt, PropServerKeepAlive) && p.ServerKeepAliveFlag {
		buf.WriteByte(PropServerKeepAlive)
		buf.Write(encodeUint16(p.ServerKeepAlive))
	}

	if p.canEncode(pkt, PropAuthenticationMethod) && p.AuthenticationMethod != "" {
		buf.WriteByte(PropAuthenticationMethod)
		buf.Write(encodeString(p.AuthenticationMethod))
	}

	if p.canEncode(pkt, PropAuthenticationData) && len(p.AuthenticationData) > 0 {
		buf.WriteByte(PropAuthenticationData)
		buf.Write(encodeBytes(p.AuthenticationData))
	}

	if p.canEncode(pkt, PropRequestProblemInfo) && p.RequestProblemInfoFlag {
		buf.WriteByte(PropRequestProblemInfo)
		buf.WriteByte(p.RequestProblemInfo)
	}

	if p.canEncode(pkt, PropWillDelayInterval) && p.WillDelayInterval > 0 {
		buf.WriteByte(PropWillDelayInterval)
		buf.Write(encodeUint32(p.WillDelayInterval))
	}

	if p.canEncode(pkt, PropRequestResponseInfo) && p.RequestResponseInfo > 0 {
		buf.WriteByte(PropRequestResponseInfo)
		buf.WriteByte(p.RequestResponseInfo)
	}

	if pk.Mods.AllowResponseInfo && p.canEncode(pkt, PropResponseInfo) && len(p.ResponseInfo) > 0 { // [MQTT-3.1.2-28]
		buf.WriteByte(PropResponseInfo)
		buf.Write(encodeString(p.ResponseInfo))
	}

	if p.canEncode(pkt, PropServerReference) && len(p.ServerReference) > 0 {
		buf.WriteByte(PropServerReference)
		buf.Write(encodeString(p.ServerReference))
	}

	// [MQTT-3.2.2-19] [MQTT-3.14.2-3] [MQTT-3.4.2-2] [MQTT-3.5.2-2]
	// [MQTT-3.6.2-2] [MQTT-3.9.2-1] [MQTT-3.11.2-1] [MQTT-3.15.2-2]
	if !pk.Mods.DisallowProblemInfo && p.canEncode(pkt, PropReasonString) && p.ReasonString != "" {
		b := encodeString(p.ReasonString)
		if pk.Mods.MaxSize == 0 || uint32(n+len(b)+1) < pk.Mods.MaxSize {
			buf.WriteByte(PropReasonString)
			buf.Write(b)
		}
	}

	if p.canEncode(pkt, PropReceiveMaximum) && p.ReceiveMaximum > 0 {
		buf.WriteByte(PropReceiveMaximum)
		buf.Write(encodeUint16(p.ReceiveMaximum))
	}

	if p.canEncode(pkt, PropTopicAliasMaximum) && p.TopicAliasMaximum > 0 {
		buf.WriteByte(PropTopicAliasMaximum)
		buf.Write(encodeUint16(p.TopicAliasMaximum))
	}

	if p.canEncode(pkt, PropTopicAlias) && p.TopicAliasFlag && p.TopicAlias > 0 { // [MQTT-3.3.2-8]
		buf.WriteByte(PropTopicAlias)
		buf.Write(encodeUint16(p.TopicAlias))
	}

	if p.canEncode(pkt, PropMaximumQos) && p.MaximumQosFlag && p.MaximumQos < 2 {
		buf.WriteByte(PropMaximumQos)
		buf.WriteByte(p.MaximumQos)
	}

	if p.canEncode(pkt, PropRetainAvailable) && p.RetainAvailableFlag {
		buf.WriteByte(PropRetainAvailable)
		buf.WriteByte(p.RetainAvailable)
	}

	if !pk.Mods.DisallowProblemInfo && p.canEncode(pkt, PropUser) {
		pb := bytes.NewBuffer([]byte{})
		for _, v := range p.User {
			pb.WriteByte(PropUser)
			pb.Write(encodeString(v.Key))
			pb.Write(encodeString(v.Val))
		}
		// [MQTT-3.2.2-20] [MQTT-3.14.2-4] [MQTT-3.4.2-3] [MQTT-3.5.2-3]
		// [MQTT-3.6.2-3] [MQTT-3.9.2-2] [MQTT-3.11.2-2] [MQTT-3.15.2-3]
		if pk.Mods.MaxSize == 0 || uint32(n+pb.Len()+1) < pk.Mods.MaxSize {
			buf.Write(pb.Bytes())
		}
	}

	if p.canEncode(pkt, PropMaximumPacketSize) && p.MaximumPacketSize > 0 {
		buf.WriteByte(PropMaximumPacketSize)
		buf.Write(encodeUint32(p.MaximumPacketSize))
	}

	if p.canEncode(pkt, PropWildcardSubAvailable) && p.WildcardSubAvailableFlag {
		buf.WriteByte(PropWildcardSubAvailable)
		buf.WriteByte(p.WildcardSubAvailable)
	}

	if p.canEncode(pkt, PropSubIDAvailable) && p.SubIDAvailableFlag {
		buf.WriteByte(PropSubIDAvailable)
		buf.WriteByte(p.SubIDAvailable)
	}

	if p.canEncode(pkt, PropSharedSubAvailable) && p.SharedSubAvailableFlag {
		buf.WriteByte(PropSharedSubAvailable)
		buf.WriteByte(p.SharedSubAvailable)
	}

	encodeLength(b, int64(buf.Len()))
	buf.WriteTo(b) // [MQTT-3.1.3-10]
}

// Decode decodes property bytes into a properties struct.
func (p *Properties) Decode(pk byte, b *bytes.Buffer) (n int, err error) {
	if p == nil {
		return 0, nil
	}

	var bu int
	n, bu, err = DecodeLength(b)
	if err != nil {
		return n + bu, err
	}

	if n == 0 {
		return n + bu, nil
	}

	bt := b.Bytes()
	var k byte
	for offset := 0; offset < n; {
		k, offset, err = decodeByte(bt, offset)
		if err != nil {
			return n + bu, err
		}

		if _, ok := validPacketProperties[k][pk]; !ok {
			return n + bu, fmt.Errorf("property type %v not valid for packet type %v: %w", k, pk, ErrProtocolViolationUnsupportedProperty)
		}

		switch k {
		case PropPayloadFormat:
			p.PayloadFormat, offset, err = decodeByte(bt, offset)
			p.PayloadFormatFlag = true
		case PropMessageExpiryInterval:
			p.MessageExpiryInterval, offset, err = decodeUint32(bt, offset)
		case PropContentType:
			p.ContentType, offset, err = decodeString(bt, offset)
		case PropResponseTopic:
			p.ResponseTopic, offset, err = decodeString(bt, offset)
		case PropCorrelationData:
			p.CorrelationData, offset, err = decodeBytes(bt, offset)
		case PropSubscriptionIdentifier:
			if p.SubscriptionIdentifier == nil {
				p.SubscriptionIdentifier = []int{}
			}

			n, bu, err := DecodeLength(bytes.NewBuffer(bt[offset:]))
			if err != nil {
				return n + bu, err
			}
			p.SubscriptionIdentifier = append(p.SubscriptionIdentifier, n)
			offset += bu
		case PropSessionExpiryInterval:
			p.SessionExpiryInterval, offset, err = decodeUint32(bt, offset)
			p.SessionExpiryIntervalFlag = true
		case PropAssignedClientID:
			p.AssignedClientID, offset, err = decodeString(bt, offset)
		case PropServerKeepAlive:
			p.ServerKeepAlive, offset, err = decodeUint16(bt, offset)
			p.ServerKeepAliveFlag = true
		case PropAuthenticationMethod:
			p.AuthenticationMethod, offset, err = decodeString(bt, offset)
		case PropAuthenticationData:
			p.AuthenticationData, offset, err = decodeBytes(bt, offset)
		case PropRequestProblemInfo:
			p.RequestProblemInfo, offset, err = decodeByte(bt, offset)
			p.RequestProblemInfoFlag = true
		case PropWillDelayInterval:
			p.WillDelayInterval, offset, err = decodeUint32(bt, offset)
		case PropRequestResponseInfo:
			p.RequestResponseInfo, offset, err = decodeByte(bt, offset)
		case PropResponseInfo:
			p.ResponseInfo, offset, err = decodeString(bt, offset)
		case PropServerReference:
			p.ServerReference, offset, err = decodeString(bt, offset)
		case PropReasonString:
			p.ReasonString, offset, err = decodeString(bt, offset)
		case PropReceiveMaximum:
			p.ReceiveMaximum, offset, err = decodeUint16(bt, offset)
		case PropTopicAliasMaximum:
			p.TopicAliasMaximum, offset, err = decodeUint16(bt, offset)
		case PropTopicAlias:
			p.TopicAlias, offset, err = decodeUint16(bt, offset)
			p.TopicAliasFlag = true
		case PropMaximumQos:
			p.MaximumQos, offset, err = decodeByte(bt, offset)
			p.MaximumQosFlag = true
		case PropRetainAvailable:
			p.RetainAvailable, offset, err = decodeByte(bt, offset)
			p.RetainAvailableFlag = true
		case PropUser:
			var k, v string
			k, offset, err = decodeString(bt, offset)
			if err != nil {
				return n + bu, err
			}
			v, offset, err = decodeString(bt, offset)
			p.User = append(p.User, UserProperty{Key: k, Val: v})
		case PropMaximumPacketSize:
			p.MaximumPacketSize, offset, err = decodeUint32(bt, offset)
		case PropWildcardSubAvailable:
			p.WildcardSubAvailable, offset, err = decodeByte(bt, offset)
			p.WildcardSubAvailableFlag = true
		case PropSubIDAvailable:
			p.SubIDAvailable, offset, err = decodeByte(bt, offset)
			p.SubIDAvailableFlag = true
		case PropSharedSubAvailable:
			p.SharedSubAvailable, offset, err = decodeByte(bt, offset)
			p.SharedSubAvailableFlag = true
		}

		if err != nil {
			return n + bu, err
		}
	}

	return n + bu, nil
}
