package dhcpv6

import (
	"errors"
	"fmt"
	"net"
	"strings"

	"github.com/insomniacslk/dhcp/iana"
	"github.com/u-root/uio/uio"
)

const RelayHeaderSize = 34

// RelayOptions are the options valid for RelayForw and RelayRepl messages.
//
// RFC 3315 Appendix B defines them to be InterfaceID and RelayMsg options; RFC
// 4649 also adds the RemoteID option.
type RelayOptions struct {
	Options
}

// RelayMessage returns the message embedded.
func (ro RelayOptions) RelayMessage() DHCPv6 {
	opt := ro.Options.GetOne(OptionRelayMsg)
	if opt == nil {
		return nil
	}
	if relayOpt, ok := opt.(*optRelayMsg); ok {
		return relayOpt.Msg
	}
	return nil
}

// InterfaceID returns the interface ID of this relay message.
func (ro RelayOptions) InterfaceID() []byte {
	opt := ro.Options.GetOne(OptionInterfaceID)
	if opt == nil {
		return nil
	}
	if iid, ok := opt.(*optInterfaceID); ok {
		return iid.ID
	}
	return nil
}

// RemoteID returns the remote ID in this relay message.
func (ro RelayOptions) RemoteID() *OptRemoteID {
	opt := ro.Options.GetOne(OptionRemoteID)
	if opt == nil {
		return nil
	}
	if rid, ok := opt.(*OptRemoteID); ok {
		return rid
	}
	return nil
}

// ClientLinkLayerAddress returns the Hardware Type and
// Link Layer Address of the requesting client in this relay message.
func (ro RelayOptions) ClientLinkLayerAddress() (iana.HWType, net.HardwareAddr) {
	opt := ro.Options.GetOne(OptionClientLinkLayerAddr)
	if opt == nil {
		return 0, nil
	}
	if lla, ok := opt.(*optClientLinkLayerAddress); ok {
		return lla.LinkLayerType, lla.LinkLayerAddress
	}
	return 0, nil
}

// RelayMessage is a DHCPv6 relay agent message as defined by RFC 3315 Section
// 7.
type RelayMessage struct {
	MessageType MessageType
	HopCount    uint8
	LinkAddr    net.IP
	PeerAddr    net.IP
	Options     RelayOptions
}

func write16(b *uio.Lexer, ip net.IP) {
	if ip == nil || ip.To16() == nil {
		var zeros [net.IPv6len]byte
		b.WriteBytes(zeros[:])
	} else {
		b.WriteBytes(ip.To16())
	}
}

// Type is this relay message's types.
func (r *RelayMessage) Type() MessageType {
	return r.MessageType
}

// String prints a short human-readable relay message.
func (r *RelayMessage) String() string {
	return fmt.Sprintf("RelayMessage(MessageType=%s, HopCount=%d, LinkAddr=%s, PeerAddr=%s, %d options)",
		r.Type(), r.HopCount, r.LinkAddr, r.PeerAddr, len(r.Options.Options))
}

// Summary prints all options associated with this relay message.
func (r *RelayMessage) Summary() string {
	return r.LongString(0)
}

// LongString prints all options associated with this message.
func (r *RelayMessage) LongString(spaceIndent int) string {
	indent := strings.Repeat(" ", spaceIndent)

	var s strings.Builder
	s.WriteString(indent)
	s.WriteString("RelayMessage{\n")
	s.WriteString(indent)
	s.WriteString(fmt.Sprintf("  MessageType=%s\n", r.MessageType))
	s.WriteString(indent)
	s.WriteString(fmt.Sprintf("  HopCount=%d\n", r.HopCount))
	s.WriteString(indent)
	s.WriteString(fmt.Sprintf("  LinkAddr=%s\n", r.LinkAddr))
	s.WriteString(indent)
	s.WriteString(fmt.Sprintf("  PeerAddr=%s\n", r.PeerAddr))
	s.WriteString(indent)
	s.WriteString("  Options: ")
	s.WriteString(r.Options.Options.LongString(spaceIndent + 2))
	s.WriteString("\n}")

	return s.String()
}

// ToBytes returns the serialized version of this relay message as defined by
// RFC 3315, Section 7.
func (r *RelayMessage) ToBytes() []byte {
	buf := uio.NewBigEndianBuffer(make([]byte, 0, RelayHeaderSize))
	buf.Write8(byte(r.MessageType))
	buf.Write8(r.HopCount)
	write16(buf, r.LinkAddr)
	write16(buf, r.PeerAddr)
	buf.WriteBytes(r.Options.ToBytes())
	return buf.Data()
}

// GetOption returns the options associated with the code.
func (r *RelayMessage) GetOption(code OptionCode) []Option {
	return r.Options.Get(code)
}

// GetOneOption returns the first associated option with the code from this
// message.
func (r *RelayMessage) GetOneOption(code OptionCode) Option {
	return r.Options.GetOne(code)
}

// AddOption adds an option to this message.
func (r *RelayMessage) AddOption(option Option) {
	r.Options.Add(option)
}

// UpdateOption replaces the first option of the same type as the specified one.
func (r *RelayMessage) UpdateOption(option Option) {
	r.Options.Update(option)
}

// IsRelay returns whether this is a relay message or not.
func (r *RelayMessage) IsRelay() bool {
	return true
}

// GetInnerMessage recurses into a relay message and extract and return the
// inner Message. Return nil if none found (e.g. not a relay message).
func (r *RelayMessage) GetInnerMessage() (*Message, error) {
	var (
		p   DHCPv6
		err error
	)
	p = r
	for {
		p, err = DecapsulateRelay(p)
		if err != nil {
			return nil, err
		}
		if m, ok := p.(*Message); ok {
			return m, nil
		}
	}
}

// NewRelayReplFromRelayForw creates a MessageTypeRelayReply based on a
// MessageTypeRelayForward and replaces the inner message with the passed
// DHCPv6 message. It copies the OptionInterfaceID and OptionRemoteID if the
// options are present in the Relay packet.
func NewRelayReplFromRelayForw(relay *RelayMessage, msg *Message) (DHCPv6, error) {
	var (
		err                error
		linkAddr, peerAddr []net.IP
		optiid             []Option
		optrid             []Option
	)
	if relay == nil {
		return nil, errors.New("Relay message cannot be nil")
	}
	if relay.Type() != MessageTypeRelayForward {
		return nil, errors.New("The passed packet is not of type MessageTypeRelayForward")
	}
	if msg == nil {
		return nil, errors.New("The passed message cannot be nil")
	}
	for {
		linkAddr = append(linkAddr, relay.LinkAddr)
		peerAddr = append(peerAddr, relay.PeerAddr)
		optiid = append(optiid, relay.GetOneOption(OptionInterfaceID))
		optrid = append(optrid, relay.GetOneOption(OptionRemoteID))
		decap, err := DecapsulateRelay(relay)
		if err != nil {
			return nil, err
		}
		if decap.IsRelay() {
			relay = decap.(*RelayMessage)
		} else {
			break
		}
	}
	m := DHCPv6(msg)
	for i := len(linkAddr) - 1; i >= 0; i-- {
		m, err = EncapsulateRelay(m, MessageTypeRelayReply, linkAddr[i], peerAddr[i])
		if err != nil {
			return nil, err
		}
		if opt := optiid[i]; opt != nil {
			m.AddOption(opt)
		}
		if opt := optrid[i]; opt != nil {
			m.AddOption(opt)
		}
	}
	return m, nil
}
