package dhcpv4

import (
	"encoding/binary"
	"errors"

	"github.com/soypat/lneto"
)

const (
	maxHostSize  = 16  // max size for hostname.
	sizeSName    = 64  // Server name, part of BOOTP too.
	sizeBootFile = 128 // Boot file name, Legacy.
	sizeHeader   = 44
	// Magic Cookie offset measured from the start of the UDP payload.
	magicCookieOffset = sizeHeader + sizeSName + sizeBootFile
	// Expected Magic Cookie value.
	MagicCookie uint32 = 0x63825363
	// DHCP Options offset measured from the start of the UDP payload.
	OptionsOffset = magicCookieOffset + 4

	DefaultClientPort = 68
	DefaultServerPort = 67
)

// NewFrame returns a new DHCPv4 Frame with data set to buf.
// An error is returned if the buffer size is smaller than 240.
func NewFrame(buf []byte) (Frame, error) {
	if len(buf) < OptionsOffset {
		return Frame{}, errSmallFrame
	}
	return Frame{buf: buf}, nil
}

func PayloadIsDHCPv4(payload []byte) bool {
	return len(payload) >= OptionsOffset && binary.BigEndian.Uint32(payload[magicCookieOffset:]) == MagicCookie
}

// Frame encapsulates the raw data of a DHCP packet
// and provides methods for manipulating, validating and
// retrieving fields and payload data. See [RFC2131].
//
// [RFC2131]: https://tools.ietf.org/html/rfc2131
type Frame struct {
	buf []byte
}

// OptionsPayload returns the options portion of the DHCP frame. May be zero lengthed.
func (frm Frame) OptionsPayload() []byte {
	return frm.buf[OptionsOffset:]
}

func (frm Frame) Op() Op      { return Op(frm.buf[0]) }
func (frm Frame) SetOp(op Op) { frm.buf[0] = byte(op) }

func (frm Frame) Hardware() (Type, Len, Ops uint8) {
	return frm.buf[1], frm.buf[2], frm.buf[3]
}

func (frm Frame) SetHardware(Type, Len, Ops uint8) {
	frm.buf[1], frm.buf[2], frm.buf[3] = Type, Len, Ops
}

// XID is the transaction ID. Is unique and constant for a DHCP request/response exchange of packets.
func (frm Frame) XID() uint32       { return binary.BigEndian.Uint32(frm.buf[4:8]) }
func (frm Frame) SetXID(xid uint32) { binary.BigEndian.PutUint32(frm.buf[4:8], xid) }

// Secs is seconds elapsed.
func (frm Frame) Secs() uint16        { return binary.BigEndian.Uint16(frm.buf[8:10]) }
func (frm Frame) SetSecs(secs uint16) { binary.BigEndian.PutUint16(frm.buf[8:10], secs) }

func (frm Frame) Flags() Flags         { return Flags(binary.BigEndian.Uint16(frm.buf[10:12])) }
func (frm Frame) SetFlags(flags Flags) { binary.BigEndian.PutUint16(frm.buf[10:12], uint16(flags)) }

// CIAddr is the client IP address. If the client has not obtained an IP
// address yet, this field is set to 0.
func (frm Frame) CIAddr() *[4]byte {
	return (*[4]byte)(frm.buf[12:16])
}

// YIAddr is the IP address offered by the server to the client. Your (client) IP Address.
func (frm Frame) YIAddr() *[4]byte {
	return (*[4]byte)(frm.buf[16:20])
}

// SIAddr is the IP address of the next server to use in bootstrap. This
// field is used in DHCPOFFER and DHCPACK messages.
func (frm Frame) SIAddr() *[4]byte {
	return (*[4]byte)(frm.buf[20:24])
}

// GIAddr is the gateway IP address. Is also known as the Relay Agent IP Address.
func (frm Frame) GIAddr() *[4]byte {
	return (*[4]byte)(frm.buf[24:28])
}

// CHAddrAs6 returns [Frame.CHAddr] but limited to first 6 bytes.
func (frm Frame) CHAddrAs6() *[6]byte {
	return (*[6]byte)(frm.buf[28 : 28+6])
}

// CHAddr is the client hardware address. Can be up to 16 bytes in length but
// is usually 6 bytes for Ethernet.
func (frm Frame) CHAddr() *[16]byte {
	return (*[16]byte)(frm.buf[28:44])
}

// MagicCookie returns the magic cookie of the header. Expect this to always be [MagicCookie].
func (frm Frame) MagicCookie() uint32 { return binary.BigEndian.Uint32(frm.buf[magicCookieOffset:]) }

// SetMagicCookie sets the MagicCookie. Call this with [MagicCookie] to create a valid DHCP header.
func (frm Frame) SetMagicCookie(cookie uint32) {
	binary.BigEndian.PutUint32(frm.buf[magicCookieOffset:], cookie)
}

// ClearHeader zeros out the header contents.
func (frm Frame) ClearHeader() {
	for i := range frm.buf[:OptionsOffset] {
		frm.buf[i] = 0
	}
}

// ForEachOption iterates over all DHCPv4 options returning an error on a malformed option or when user provided callback returns an error.
// If the user provided callback is nil then only option buffer validation is performed.
func (frm Frame) ForEachOption(fn func(off int, op OptNum, data []byte) error) error {
	// Parse DHCP options.
	ptr := OptionsOffset
	if ptr > len(frm.buf) {
		return errSmallFrame
	} else if len(frm.buf[ptr:]) == 0 {
		return errNoOptions
	}
	callback := fn != nil
	for ptr+1 < len(frm.buf) {
		if int(frm.buf[ptr+1]) >= len(frm.buf) {
			return errDHCPBadOption
		}
		optnum := OptNum(frm.buf[ptr])
		if optnum == 0xff {
			break
		} else if optnum == OptWordAligned {
			ptr++
			continue
		}
		optlen := frm.buf[ptr+1]
		if callback {
			optionData := frm.buf[ptr+2 : ptr+2+int(optlen)]
			if err := fn(ptr, optnum, optionData); err != nil {
				return err
			}
		}
		ptr += int(optlen) + 2
	}
	return nil
}

//
// Validation API.
//

var (
	errSmallFrame    = errors.New("DHCPv4: frame size <240")
	errDHCPBadOption = errors.New("DHCPv4: opt length exceeds payload")
	errNoOptions     = errors.New("DHCPv4: no options")
	errOptionNotFit  = errors.New("DHCPv4: options dont fit")
)

func (frm Frame) ValidateSize(vld *lneto.Validator) {
	err := frm.ForEachOption(nil) // Does all necessary validation.
	if err != nil {
		vld.AddError(errDHCPBadOption)
	}
}
