package dhcpv4

import (
	"encoding/binary"
	"errors"
	"fmt"
	"net/netip"

	"github.com/soypat/lneto"
	"github.com/soypat/lneto/internal"
)

type Server struct {
	connID   uint64
	nextAddr netip.Addr
	prefix   netip.Prefix
	hosts    map[[36]byte]serverEntry
	vld      lneto.Validator
	pending  int
	port     uint16
	siaddr   [4]byte
	gwaddr   [4]byte
}

type serverEntry struct {
	hostname    string
	xid         uint32
	port        uint16
	addr        [4]byte
	requestlist [10]byte
	hwaddr      [6]byte
	clientIdlen uint8
	// Possible states:
	//  - 0: No entry/uninitialized
	//  - Init: Server received discover, pending Offer sent out.
	//  - Selecting: Server sent out offer, request not received.
	//  - Requesting: Request received, pending Ack sent out.
	//  - Bound: Request sent out, no more pending data to be sent.
	state ClientState
}

func (sv *Server) Reset(serverAddr [4]byte, port uint16) {
	*sv = Server{
		connID:   sv.connID + 1,
		siaddr:   serverAddr,
		port:     port,
		hosts:    sv.hosts,
		nextAddr: netip.AddrFrom4(serverAddr),
	}
	if sv.hosts == nil {
		sv.hosts = make(map[[36]byte]serverEntry)
	} else {
		for k := range sv.hosts {
			delete(sv.hosts, k)
		}
	}
}

func (sv *Server) ConnectionID() *uint64 { return &sv.connID }
func (sv *Server) Protocol() uint64      { return uint64(lneto.IPProtoUDP) }
func (sv *Server) Port() uint16          { return sv.port }

func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
	isIPLayer := frameOffset >= 28
	dhcpData := carrierData[frameOffset:]
	dfrm, err := NewFrame(dhcpData)
	if err != nil {
		return err
	}
	dfrm.ValidateSize(&sv.vld)
	if sv.vld.HasError() {
		return sv.vld.ErrPop()
	}

	var msgType MessageType
	var clientID []byte
	var reqlist []byte
	var reqAddr []byte
	var hostname []byte
	err = dfrm.ForEachOption(func(off int, op OptNum, data []byte) error {
		switch op {
		case OptMessageType:
			if len(data) == 1 {
				msgType = MessageType(data[0])
			}
		case OptHostName:
			if len(data) <= 36 {
				hostname = data
			}
		case OptClientIdentifier:
			if len(data) <= 36 {
				clientID = data
			}
		case OptParameterRequestList:
			if len(data) > 36 {
				return errors.New("too many request options")
			}
			reqlist = data
		case OptRequestedIPaddress:
			if len(data) == 4 {
				reqAddr = data
			}
		}
		return nil
	})
	var clientIDRaw [36]byte
	var client serverEntry
	var clientExists bool
	if len(clientID) == 0 {
		client, clientIDRaw, clientExists = sv.getClientByIP(*dfrm.CIAddr())
	} else {
		copy(clientIDRaw[:], clientID)
		client, clientExists = sv.getClient(clientIDRaw)
	}

	switch msgType {
	case MsgDiscover:
		if clientExists {
			err = errors.New("DHCP Discover on initialized client")
			break
		}
		if len(reqAddr) == 4 {
			println("requested", reqAddr[0], reqAddr[1], reqAddr[2], reqAddr[3])
		}
		sv.nextAddr = sv.nextAddr.Next()
		copy(client.requestlist[:], reqlist)
		client.addr = sv.nextAddr.As4()
		client.state = StateInit
		client.hostname = string(hostname)
		client.xid = dfrm.XID()
		client.hwaddr = *dfrm.CHAddrAs6()
		if isIPLayer {
			_, client.port, _ = getSrcIPPort(carrierData)
		}
		client.clientIdlen = uint8(len(clientID))
		sv.pending++

	case MsgRequest:
		if !clientExists {
			err = errors.New("request for non existing client?")
		} else if dfrm.XID() != client.xid {
			err = errors.New("unexpected XID for client")
		} else if client.state != StateSelecting && client.state != StateRequesting {
			err = errors.New("DHCP request unexpected state")
		}
		if err != nil {
			break
		}
		client.state = StateRequesting
		sv.pending++

	default:
		err = errors.New("unhandled message type")
	}
	if err != nil {
		return fmt.Errorf("msgtype=%s client=%+v: %w", msgType.String(), client, err)
	}
	sv.hosts[clientIDRaw] = client
	return nil
}

func (sv *Server) Encapsulate(carrierData []byte, frameOffset int) (int, error) {
	carrierIsIP := frameOffset >= 28
	dfrm, err := NewFrame(carrierData[frameOffset:])
	optBuf := dfrm.OptionsPayload()[:]
	if err != nil {
		return 0, err
	} else if len(optBuf) < 255 {
		return 0, errOptionNotFit
	}
	if sv.pending == 0 {
		return 0, nil // No pending outgoing frames.a
	}

	var client serverEntry
	var clientID [36]byte
	for k, v := range sv.hosts {
		pending := v.state == StateInit || v.state == StateRequesting
		if pending {
			client = v
			clientID = k
			break
		}
	}
	if client.state == 0 {
		return 0, nil // Nothing to do.
	}
	futureState := ClientState(0)
	var nopt int
	switch client.state {
	case StateInit:
		futureState = StateSelecting
		nopt, err = EncodeOption(optBuf[nopt:], OptMessageType, byte(MsgOffer))
	case StateRequesting:
		futureState = StateBound
		nopt, err = EncodeOption(optBuf[nopt:], OptMessageType, byte(MsgAck))
		*dfrm.CIAddr() = client.addr
	}
	if err != nil {
		return 0, err
	}
	n, _ := EncodeOption(optBuf[nopt:], OptServerIdentification, sv.siaddr[:]...)
	nopt += n
	if sv.gwaddr != [4]byte{} {
		n, _ = EncodeOption(optBuf[nopt:], OptRouter, sv.gwaddr[:]...)
		nopt += n
	}
	optBuf[nopt] = byte(OptEnd)
	nopt++

	dfrm.ClearHeader()
	dfrm.SetOp(OpReply)
	dfrm.SetHardware(1, 6, 0)
	dfrm.SetXID(client.xid)
	dfrm.SetSecs(0)
	dfrm.SetFlags(0)
	*dfrm.YIAddr() = client.addr // Offer here.
	*dfrm.SIAddr() = sv.siaddr
	*dfrm.GIAddr() = sv.gwaddr
	copy(dfrm.CHAddrAs6()[:], client.hwaddr[:])
	dfrm.SetMagicCookie(MagicCookie)
	if carrierIsIP {
		err = internal.SetIPAddrs(carrierData, 0, sv.siaddr[:], client.addr[:])
		if err != nil {
			return 0, err
		}
	}

	client.state = futureState

	// Set server state.
	sv.hosts[clientID] = client
	sv.pending--
	return OptionsOffset + nopt, nil
}

func (sv *Server) getClient(clientID [36]byte) (serverEntry, bool) {
	entry, ok := sv.hosts[clientID]
	return entry, ok
}

func (sv *Server) getClientByIP(ip [4]byte) (serverEntry, [36]byte, bool) {
	for k, v := range sv.hosts {
		if v.addr == ip {
			return v, k, true
		}
	}
	return serverEntry{}, [36]byte{}, false
}

func getSrcIPPort(ipCarrier []byte) (srcaddr []byte, port uint16, err error) {
	srcaddr, _, _, off, err := internal.GetIPAddr(ipCarrier)
	if err != nil {
		return srcaddr, port, err
	} else if len(ipCarrier[off:]) < 2 {
		return srcaddr, port, errors.New("getSrcIPPort got only IP layer")
	}
	port = binary.BigEndian.Uint16(ipCarrier[off:]) // TCP and UDP share same port offsets.
	return srcaddr, port, nil
}
