// Copyright 2019 Yunion
//
// 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.

package icmp6

import (
	"fmt"
	"net"

	"github.com/google/gopacket"
	"github.com/google/gopacket/layers"

	"yunion.io/x/pkg/errors"
)

type TPreference int

const (
	PreferenceLow    TPreference = 0
	PreferenceMedium TPreference = 1
	PreferenceHigh   TPreference = 2
)

func (pref TPreference) String() string {
	switch pref {
	case PreferenceLow:
		return "low"
	case PreferenceMedium:
		return "medium"
	case PreferenceHigh:
		return "high"
	}
	return "unknown"
}

type SBaseICMP6Message struct {
	SrcMac net.HardwareAddr
	SrcIP  net.IP
	DstMac net.HardwareAddr
	DstIP  net.IP
	Vlan   uint16
}

func (msg SBaseICMP6Message) String() string {
	var vlanStr string
	if msg.Vlan > 1 {
		vlanStr += fmt.Sprintf("(vlan %d)", msg.Vlan)
	}
	return fmt.Sprintf("%s[%s]->%s[%s]%s", msg.SrcIP.String(), msg.SrcMac.String(), msg.DstIP.String(), msg.DstMac.String(), vlanStr)
}

type IICMP6Message interface {
	fmt.Stringer

	Payload() gopacket.SerializableLayer
	ICMP6TypeCode() layers.ICMPv6TypeCode

	SourceMac() net.HardwareAddr
	SourceIP() net.IP
	DestinationMac() net.HardwareAddr
	DestinationIP() net.IP

	VlanId() uint16
}

func (msg SBaseICMP6Message) SourceIP() net.IP {
	return msg.SrcIP
}

func (msg SBaseICMP6Message) SourceMac() net.HardwareAddr {
	return msg.SrcMac
}

func (msg SBaseICMP6Message) DestinationIP() net.IP {
	return msg.DstIP
}

func (msg SBaseICMP6Message) DestinationMac() net.HardwareAddr {
	return msg.DstMac
}

func (msg SBaseICMP6Message) VlanId() uint16 {
	return msg.Vlan
}

type SRouterSolicitation struct {
	SBaseICMP6Message
}

type SPrefixInfoOption struct {
	IsOnlink          bool
	IsAutoconf        bool
	Prefix            net.IP
	PrefixLen         uint8
	ValidLifetime     uint32
	PreferredLifetime uint32
}

func (opt SPrefixInfoOption) String() string {
	return fmt.Sprintf("%s/%d (IsOnlink: %t, IsAutoconf: %t) ValidLifetime: %d, PreferredLifetime: %d", opt.Prefix.String(), opt.PrefixLen, opt.IsOnlink, opt.IsAutoconf, opt.ValidLifetime, opt.PreferredLifetime)
}

type SRouteInfoOption struct {
	RouteLifetime uint32
	Prefix        net.IP
	PrefixLen     uint8
	Preference    TPreference
}

func (opt SRouteInfoOption) String() string {
	return fmt.Sprintf("%s/%d (Pref: %s) RouteLifetime: %d", opt.Prefix.String(), opt.PrefixLen, opt.Preference.String(), opt.RouteLifetime)
}

type SRouterAdvertisement struct {
	SBaseICMP6Message

	CurHopLimit uint8

	IsManaged   bool
	IsOther     bool
	IsHomeAgent bool

	Preference TPreference

	RouterLifetime uint16
	ReachableTime  uint32
	RetransTimer   uint32

	MTU        uint32
	PrefixInfo []SPrefixInfoOption
	RouteInfo  []SRouteInfoOption
}

type SNeighborSolicitation struct {
	SBaseICMP6Message

	/*
		The IP address of the target of the solicitation.
		It MUST NOT be a multicast address.
	*/
	TargetAddr net.IP
}

type SNeighborAdvertisement struct {
	SBaseICMP6Message

	/*
		For solicited advertisements, the Target Address
		field in the Neighbor Solicitation message that
		prompted this advertisement.  For an unsolicited
		advertisement, the address whose link-layer address
		has changed.  The Target Address MUST NOT be a
		multicast address.
	*/
	TargetAddr net.IP

	IsRouter    bool
	IsSolicited bool
}

func (msg SNeighborSolicitation) ICMP6TypeCode() layers.ICMPv6TypeCode {
	return layers.CreateICMPv6TypeCode(layers.ICMPv6TypeNeighborSolicitation, 0)
}

func (msg SNeighborSolicitation) Payload() gopacket.SerializableLayer {
	pkt := layers.ICMPv6NeighborSolicitation{}
	pkt.TargetAddress = msg.TargetAddr

	pkt.Options = layers.ICMPv6Options{
		layers.ICMPv6Option{
			Type: layers.ICMPv6OptSourceAddress,
			Data: NewIcmpv6OptSourceTargetAddress(msg.SrcMac).Bytes(),
		},
	}

	return &pkt
}

func (msg *SNeighborSolicitation) Unmarshal(data *layers.ICMPv6NeighborSolicitation) error {
	msg.TargetAddr = data.TargetAddress
	return nil
}

func (msg SNeighborSolicitation) String() string {
	return fmt.Sprintf("NeighborSolicitation %s target %s", msg.SBaseICMP6Message.String(), msg.TargetAddr.String())
}

func (msg SNeighborAdvertisement) ICMP6TypeCode() layers.ICMPv6TypeCode {
	return layers.CreateICMPv6TypeCode(layers.ICMPv6TypeNeighborAdvertisement, 0)
}

func (msg SNeighborAdvertisement) Payload() gopacket.SerializableLayer {
	pkt := layers.ICMPv6NeighborAdvertisement{}
	pkt.TargetAddress = msg.TargetAddr
	pkt.Flags = 0x00
	if msg.IsRouter {
		pkt.Flags |= 0x80
	}
	if msg.IsSolicited {
		pkt.Flags |= 0x40
	}

	pkt.Options = layers.ICMPv6Options{
		layers.ICMPv6Option{
			Type: layers.ICMPv6OptSourceAddress,
			Data: NewIcmpv6OptSourceTargetAddress(msg.SrcMac).Bytes(),
		},
	}

	return &pkt
}

func (msg *SNeighborAdvertisement) Unmarshal(data *layers.ICMPv6NeighborAdvertisement) error {
	msg.TargetAddr = data.TargetAddress
	msg.IsRouter = data.Flags&0x80 != 0
	msg.IsSolicited = data.Flags&0x40 != 0
	return nil
}

func (msg SNeighborAdvertisement) String() string {
	return fmt.Sprintf("NeighborAdvertisement %s target %s", msg.SBaseICMP6Message.String(), msg.TargetAddr.String())
}

func (msg SRouterSolicitation) ICMP6TypeCode() layers.ICMPv6TypeCode {
	return layers.CreateICMPv6TypeCode(layers.ICMPv6TypeRouterSolicitation, 0)
}

func (msg SRouterSolicitation) Payload() gopacket.SerializableLayer {
	pkt := layers.ICMPv6RouterSolicitation{}

	pkt.Options = layers.ICMPv6Options{
		layers.ICMPv6Option{
			Type: layers.ICMPv6OptSourceAddress,
			Data: NewIcmpv6OptSourceTargetAddress(msg.SrcMac).Bytes(),
		},
	}

	return &pkt
}

func (msg *SRouterSolicitation) Unmarshal(data *layers.ICMPv6RouterSolicitation) error {
	return nil
}

func (msg SRouterSolicitation) String() string {
	return fmt.Sprintf("RouterSolicitation %s", msg.SBaseICMP6Message.String())
}

func (msg SRouterAdvertisement) ICMP6TypeCode() layers.ICMPv6TypeCode {
	return layers.CreateICMPv6TypeCode(layers.ICMPv6TypeRouterAdvertisement, 0)
}

func (msg SRouterAdvertisement) Payload() gopacket.SerializableLayer {
	pkt := layers.ICMPv6RouterAdvertisement{}
	// https://datatracker.ietf.org/doc/html/rfc4861#section-4.2
	// https://datatracker.ietf.org/doc/html/rfc4191
	pkt.Flags = 0x00 // M=1, O=1, stateful DHCPv6, pref=00
	if msg.IsManaged {
		pkt.Flags |= 0x80
	}
	if msg.IsOther {
		pkt.Flags |= 0x40
	}
	if msg.IsHomeAgent {
		pkt.Flags |= 0x20
	}

	switch msg.Preference {
	case PreferenceLow:
		pkt.Flags |= 0x18
	case PreferenceMedium:
		// do nothing
	case PreferenceHigh:
		pkt.Flags |= 0x08
	}

	pkt.HopLimit = msg.CurHopLimit
	pkt.RouterLifetime = msg.RouterLifetime
	pkt.ReachableTime = msg.ReachableTime
	pkt.RetransTimer = msg.RetransTimer

	pkt.Options = layers.ICMPv6Options{}

	for i := range msg.PrefixInfo {
		pref := msg.PrefixInfo[i]
		pkt.Options = append(pkt.Options, layers.ICMPv6Option{
			Type: layers.ICMPv6OptPrefixInfo,
			Data: NewIcmpv6OptPrefixInfo(pref).Bytes(),
		})
	}
	for i := range msg.RouteInfo {
		rt := msg.RouteInfo[i]
		pkt.Options = append(pkt.Options, layers.ICMPv6Option{
			Type: 24,
			Data: NewIcmpv6OptRouteInfo(rt).Bytes(),
		})
	}
	if msg.MTU > 0 {
		pkt.Options = append(pkt.Options, layers.ICMPv6Option{
			Type: layers.ICMPv6OptMTU,
			Data: NewIcmpV6OptMtu(msg.MTU).Bytes(),
		})
	}
	pkt.Options = append(pkt.Options, layers.ICMPv6Option{
		Type: layers.ICMPv6OptSourceAddress,
		Data: NewIcmpv6OptSourceTargetAddress(msg.SrcMac).Bytes(),
	})

	return &pkt
}

func (msg *SRouterAdvertisement) Unmarshal(data *layers.ICMPv6RouterAdvertisement) error {
	msg.CurHopLimit = data.HopLimit
	msg.IsManaged = data.Flags&0x80 != 0
	msg.IsOther = data.Flags&0x40 != 0
	msg.IsHomeAgent = data.Flags&0x20 != 0
	switch data.Flags & 0x18 {
	case 0x18:
		msg.Preference = PreferenceLow
	case 0x08:
		msg.Preference = PreferenceHigh
	default:
		msg.Preference = PreferenceMedium
	}
	msg.RouterLifetime = data.RouterLifetime

	for i := range data.Options {
		opt := data.Options[i]
		switch opt.Type {
		case layers.ICMPv6OptMTU:
			msg.MTU = DecodeIcmpV6OptMtu(opt.Data)
		case layers.ICMPv6OptPrefixInfo:
			prefix := DecodeIcmpv6OptPrefixInfo(opt.Data)
			msg.PrefixInfo = append(msg.PrefixInfo, prefix)
		case 24:
			route := DecodeIcmpv6OptRouteInfo(opt.Data)
			msg.RouteInfo = append(msg.RouteInfo, route)
		}
	}

	return nil
}

func (msg SRouterAdvertisement) String() string {
	return fmt.Sprintf(`RouterAdvertisement %s 
    CurHopLimit: %d, IsManaged: %t, IsOther: %t, IsHomeAgent: %t, Preference: %s, RouterLifetime: %d, ReachableTime: %d, RetransTimer: %d
    MTU: %d
    PrefixInfo: %v
    RouteInfo: %v)`,
		msg.SBaseICMP6Message.String(),
		msg.CurHopLimit, msg.IsManaged, msg.IsOther, msg.IsHomeAgent, msg.Preference.String(), msg.RouterLifetime, msg.ReachableTime, msg.RetransTimer,
		msg.MTU, msg.PrefixInfo, msg.RouteInfo)
}

func EncodePacket(msg IICMP6Message) ([]byte, error) {
	var pktLayers []gopacket.SerializableLayer

	var eth = &layers.Ethernet{
		EthernetType: layers.EthernetTypeIPv6,
		SrcMAC:       msg.SourceMac(),
		DstMAC:       msg.DestinationMac(),
	}
	pktLayers = append(pktLayers, eth)

	if msg.VlanId() > 1 {
		eth.EthernetType = layers.EthernetTypeDot1Q
		dot1Q := &layers.Dot1Q{
			VLANIdentifier: msg.VlanId(),
			Priority:       6,
			Type:           layers.EthernetTypeIPv6,
		}
		pktLayers = append(pktLayers, dot1Q)
	}

	var ip = &layers.IPv6{
		Version:      6,
		HopLimit:     0xff,
		TrafficClass: 0xc0,
		SrcIP:        msg.SourceIP(),
		DstIP:        msg.DestinationIP(),
		NextHeader:   layers.IPProtocolICMPv6,
	}
	pktLayers = append(pktLayers, ip)

	var icmp6 = &layers.ICMPv6{
		TypeCode: msg.ICMP6TypeCode(),
	}
	icmp6.SetNetworkLayerForChecksum(ip)
	pktLayers = append(pktLayers, icmp6)

	pktLayers = append(pktLayers, msg.Payload())

	var (
		buf  = gopacket.NewSerializeBuffer()
		opts = gopacket.SerializeOptions{ComputeChecksums: true, FixLengths: true}
	)
	if err := gopacket.SerializeLayers(buf, opts, pktLayers...); err != nil {
		return nil, errors.Wrap(err, "SerializeLayers error")
	}

	return buf.Bytes(), nil
}

func DecodePacket(data []byte) (IICMP6Message, error) {
	packet := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.Default)
	if packet.ErrorLayer() != nil {
		return nil, errors.Wrap(packet.ErrorLayer().Error(), "DecodePacket error")
	}

	var baseMsg SBaseICMP6Message

	{
		ethLayer := packet.Layer(layers.LayerTypeEthernet)
		if ethLayer != nil {
			eth := ethLayer.(*layers.Ethernet)
			baseMsg.SrcMac = eth.SrcMAC
			baseMsg.DstMac = eth.DstMAC
		} else {
			return nil, errors.Wrap(packet.ErrorLayer().Error(), "Expect Ethernet layer")
		}
	}

	{
		// optional vlan layer
		dot1qLayer := packet.Layer(layers.LayerTypeDot1Q)
		if dot1qLayer != nil {
			dot1q := dot1qLayer.(*layers.Dot1Q)
			baseMsg.Vlan = dot1q.VLANIdentifier
		}
	}

	{
		ipLayer := packet.Layer(layers.LayerTypeIPv6)
		if ipLayer != nil {
			// ipv6
			ip6 := ipLayer.(*layers.IPv6)
			baseMsg.SrcIP = ip6.SrcIP
			baseMsg.DstIP = ip6.DstIP
		} else {
			return nil, errors.Wrap(packet.ErrorLayer().Error(), "Expect IPv6 packet")
		}
	}

	{
		icmpLayer := packet.Layer(layers.LayerTypeICMPv6)
		if icmpLayer != nil {
			icmp6 := icmpLayer.(*layers.ICMPv6)
			if icmp6 == nil {
				return nil, errors.Wrap(packet.ErrorLayer().Error(), "Expect IPv6 packet")
			}

			switch icmp6.TypeCode.Type() {
			case 133:
				payload := packet.Layer(layers.LayerTypeICMPv6RouterSolicitation)
				if payload != nil {
					msg := &SRouterSolicitation{
						SBaseICMP6Message: baseMsg,
					}
					err := msg.Unmarshal(payload.(*layers.ICMPv6RouterSolicitation))
					if err != nil {
						return nil, errors.Wrap(err, "Unmarshal Router Solicitation packet")
					}
					return msg, nil
				} else {
					return nil, errors.Wrap(packet.ErrorLayer().Error(), "Expect ICMPv6 Router Solicitation packet")
				}
			case 134:
				payload := packet.Layer(layers.LayerTypeICMPv6RouterAdvertisement)
				if payload != nil {
					msg := &SRouterAdvertisement{
						SBaseICMP6Message: baseMsg,
					}
					err := msg.Unmarshal(payload.(*layers.ICMPv6RouterAdvertisement))
					if err != nil {
						return nil, errors.Wrap(err, "Unmarshal Router Advertisement packet")
					}
					return msg, nil
				} else {
					return nil, errors.Wrap(packet.ErrorLayer().Error(), "Expect ICMPv6 Router Advertisement packet")
				}
			case 135:
				payload := packet.Layer(layers.LayerTypeICMPv6NeighborSolicitation)
				if payload != nil {
					msg := &SNeighborSolicitation{
						SBaseICMP6Message: baseMsg,
					}
					err := msg.Unmarshal(payload.(*layers.ICMPv6NeighborSolicitation))
					if err != nil {
						return nil, errors.Wrap(err, "Unmarshal Neighbor Solicitation packet")
					}
					return msg, nil
				} else {
					return nil, errors.Wrap(packet.ErrorLayer().Error(), "Expect ICMPv6 Neighbor Solicitation packet")
				}
			case 136:
				payload := packet.Layer(layers.LayerTypeICMPv6NeighborAdvertisement)
				if payload != nil {
					msg := &SNeighborAdvertisement{
						SBaseICMP6Message: baseMsg,
					}
					err := msg.Unmarshal(payload.(*layers.ICMPv6NeighborAdvertisement))
					if err != nil {
						return nil, errors.Wrap(err, "Unmarshal Neighbor Advertisement packet")
					}
					return msg, nil
				} else {
					return nil, errors.Wrap(packet.ErrorLayer().Error(), "Expect ICMPv6 Neighbor Advertisement packet")
				}
			default:
				return nil, errors.Wrap(packet.ErrorLayer().Error(), "Expect ICMPv6 packet")
			}
		} else {
			return nil, errors.Wrap(packet.ErrorLayer().Error(), "Expect IPv6 packet")
		}
	}
}
