package reservation

import (
	"context"
	"dhcp/internal/dhcpServer/data"
	"errors"
	"github.com/go-logr/logr"
	"github.com/insomniacslk/dhcp/dhcpv4"
	"github.com/rs/zerolog/log"
	"golang.org/x/net/ipv4"
	"net"
)

// setDefaults will update the Handler struct to have default values so as
// to avoid panic for nil pointers and such.
func (h *Handler) setDefaults() {
	if h.Backend == nil {
		h.Backend = noop{}
	}
	if h.Log.GetSink() == nil {
		h.Log = logr.Discard()
	}
}

// Handle responds to dhcpServer messages with dhcpServer server options.
func (h *Handler) Handle(ctx context.Context, conn *ipv4.PacketConn, p data.Packet) {
	h.setDefaults()
	if p.Pkt == nil {
		h.Log.Error(errors.New("incoming packet is nil"), "not able to respond when the incoming packet is nil")
		return
	}
	upeer, ok := p.Peer.(*net.UDPAddr)
	if !ok {
		h.Log.Error(errors.New("peer is not a UDP connection"), "not able to respond when the peer is not a UDP connection")
		return
	}
	if upeer == nil {
		h.Log.Error(errors.New("peer is nil"), "not able to respond when the peer is nil")
		return
	}
	if conn == nil {
		h.Log.Error(errors.New("connection is nil"), "not able to respond when the connection is nil")
		return
	}

	var ifName string
	if p.Md != nil {
		ifName = p.Md.IfName
	}
	log := h.Log.WithValues("mac", p.Pkt.ClientHWAddr.String(), "xid", p.Pkt.TransactionID.String(), "interface", ifName)

	var reply *dhcpv4.DHCPv4
	switch mt := p.Pkt.MessageType(); mt {
	case dhcpv4.MessageTypeDiscover:

		d, err := h.readBackend(ctx, p.Pkt.ClientHWAddr, ifName)
		if err != nil {
			if hardwareNotFound(err) {
				return
			}
			log.Info("error reading from macAndIPXE", "error", err)

			return
		}
		log.Info("received dhcpServer packet", "type", p.Pkt.MessageType().String())
		reply = h.updateMsg(ctx, &p, d, dhcpv4.MessageTypeOffer)
		log = log.WithValues("type", dhcpv4.MessageTypeOffer.String())
	case dhcpv4.MessageTypeRequest:
		d, err := h.readBackend(ctx, p.Pkt.ClientHWAddr, ifName)
		if err != nil {
			if hardwareNotFound(err) {
				return
			}
			log.Info("error reading from macAndIPXE", "error", err)
			return
		}
		log.Info("received dhcpServer packet", "type", p.Pkt.MessageType().String())
		reply = h.updateMsg(ctx, &p, d, dhcpv4.MessageTypeAck)
		log = log.WithValues("type", dhcpv4.MessageTypeAck.String())
	case dhcpv4.MessageTypeRelease:
		// 固定ip
		// 不需要客户端去主动释放
		log.Info("received dhcpServer release packet, no response required, all IPs are host reservations", "type", p.Pkt.MessageType().String())

		return
	default:
		log.Info("received unknown message type", "type", p.Pkt.MessageType().String())

		return
	}

	if ns := reply.ServerIPAddr; ns != nil {
		log = log.WithValues("nextServer", ns.String())
	}

	dst := replyDestination(p.Peer, p.Pkt.GatewayIPAddr)
	log = log.WithValues("ipAddress", reply.YourIPAddr.String(), "destination", dst.String())
	cm := &ipv4.ControlMessage{}
	if p.Md != nil {
		cm.IfIndex = p.Md.IfIndex
	}

	if _, err := conn.WriteTo(reply.ToBytes(), cm, dst); err != nil {
		log.Error(err, "failed to send dhcpServer")

		return
	}

	log.Info("sent dhcpServer response")
}

// replyDestination determines the destination address for the dhcpServer reply.
// If the giaddr is set, then the reply should be sent to the giaddr.
// Otherwise, the reply should be sent to the direct peer.
//
// From page 22 of https://www.ietf.org/rfc/rfc2131.txt:
// "If the 'giaddr' field in a dhcpServer message from a client is non-zero,
// the server sends any return messages to the 'dhcpServer server' port on
// the BOOTP relay agent whose address appears in 'giaddr'.".
func replyDestination(directPeer net.Addr, giaddr net.IP) net.Addr {
	if !giaddr.IsUnspecified() && giaddr != nil {
		return &net.UDPAddr{IP: giaddr, Port: dhcpv4.ServerPort}
	}

	return directPeer
}

// readBackend encapsulates the macAndIPXE read and opentelemetry handling.
func (h *Handler) readBackend(ctx context.Context, mac net.HardwareAddr, ifName string) (*data.DHCP, error) {
	h.setDefaults()

	d, err := h.Backend.GetByMac(ctx, mac, ifName)
	if err != nil {
		return nil, err
	}
	return d, nil
}

// updateMsg handles updating dhcpServer packets with the data from the macAndIPXE.
func (h *Handler) updateMsg(ctx context.Context, p *data.Packet, d *data.DHCP, msgType dhcpv4.MessageType) *dhcpv4.DHCPv4 {
	h.setDefaults()
	mods := []dhcpv4.Modifier{
		dhcpv4.WithMessageType(msgType),
		dhcpv4.WithGeneric(dhcpv4.OptionServerIdentifier, h.IPAddr.AsSlice()),
		dhcpv4.WithServerIP(d.ServerIP),
		dhcpv4.WithHwAddr(p.Pkt.ClientHWAddr),
	}
	mods = append(mods, h.setDHCPOpts(ctx, p.Pkt, d)...)
	// We ignore the error here because:
	// 1. it's only non-nil if the generation of a transaction id (XID) fails.
	// 2. We always use the clients transaction id (XID) in responses. See dhcpv4.WithReply().
	reply, _ := dhcpv4.NewReplyFromRequest(p.Pkt, mods...)
	log.Printf("%+v", reply)
	return reply
}

// hardwareNotFound returns true if the error is from a hardware record not being found.
func hardwareNotFound(err error) bool {
	type hardwareNotFound interface {
		NotFound() bool
	}
	te, ok := err.(hardwareNotFound)
	return ok && te.NotFound()
}
