/*
Package proxy implements a dhcpServer handler that provides proxyDHCP functionality.

"[A] Proxy dhcpServer server behaves much like a dhcpServer server by listening for ordinary
dhcpServer client traffic and responding to certain client requests. However, unlike the
dhcpServer server, the PXE Proxy dhcpServer server does not administer network addresses, and
it only responds to clients that identify themselves as PXE clients. The responses
given by the PXE Proxy dhcpServer server contain the mechanism by which the client locates
the boot servers or the network addresses and descriptions of the supported,
compatible boot servers."

Reference: https://www.ibm.com/docs/en/aix/7.1?topic=protocol-preboot-execution-environment-proxy-dhcp-daemon
*/
package proxy

import (
	"context"
	"dhcp/internal/dhcpServer/data"
	"dhcp/internal/dhcpServer/handler"
	"errors"
	"fmt"
	"net"
	"net/netip"

	"github.com/go-logr/logr"
	"github.com/insomniacslk/dhcp/dhcpv4"
	"golang.org/x/net/ipv4"
)

// Handler holds the configuration details for the running the dhcpServer server.
type Handler struct {
	// Backend is the macAndIPXE to use for getting dhcpServer data.
	Backend handler.BackendReader

	// IPAddr is the IP address to use in dhcpServer responses.
	// Option 54 and the sname dhcpServer header.
	// This could be a load balancer IP address or an ingress IP address or a local IP address.
	IPAddr netip.Addr

	// Log is used to log messages.
	// `logr.Discard()` can be used if no logging is desired.
	Log logr.Logger
}

// Redirection name comes from section 2.5 of http://www.pix.net/software/pxeboot/archive/pxespec.pdf
func (h *Handler) Handle(ctx context.Context, conn *ipv4.PacketConn, dp data.Packet) {
	// validations
	if dp.Pkt == nil {
		h.Log.Error(errors.New("incoming packet is nil"), "not able to respond when the incoming packet is nil")
		return
	}
	upeer, ok := dp.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 dp.Md != nil {
		ifName = dp.Md.IfName
	}
	log := h.Log.WithValues("mac", dp.Pkt.ClientHWAddr.String(), "xid", dp.Pkt.TransactionID.String(), "interface", ifName)

	// 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(dp.Pkt)

	if dp.Pkt.OpCode != dhcpv4.OpcodeBootRequest { // TODO(jacobweinstock): dont understand this, found it in an example here: https://github.com/insomniacslk/dhcp/blob/c51060810aaab9c8a0bd1b0fcbf72bc0b91e6427/dhcpv4/server4/server_test.go#L31
		log.V(1).Info("Ignoring packet", "OpCode", dp.Pkt.OpCode)
		return
	}

	if err := setMessageType(reply, dp.Pkt.MessageType()); err != nil {
		log.V(1).Info("Ignoring packet", "error", err.Error())

		return
	}

	// Set option 97
	reply.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionClientMachineIdentifier, dp.Pkt.GetOneOption(dhcpv4.OptionClientMachineIdentifier)))

	log.Info(
		"received dhcpServer packet",
		"type", dp.Pkt.MessageType().String(),
	)

	dst := replyDestination(dp.Peer, dp.Pkt.GatewayIPAddr)
	cm := &ipv4.ControlMessage{}
	if dp.Md != nil {
		cm.IfIndex = dp.Md.IfIndex
	}
	log = log.WithValues(
		"destination", dst.String(),
		"bootFileName", reply.BootFileName,
		"nextServer", reply.ServerIPAddr.String(),
		"messageType", reply.MessageType().String(),
		"serverHostname", reply.ServerHostName,
	)
	// send the dhcpServer packet
	if _, err := conn.WriteTo(reply.ToBytes(), cm, dst); err != nil {
		log.Error(err, "failed to send ProxyDHCP response")
		return
	}
	log.Info("Sent ProxyDHCP response")
}

func setMessageType(reply *dhcpv4.DHCPv4, reqMsg dhcpv4.MessageType) error {
	switch mt := reqMsg; mt {
	case dhcpv4.MessageTypeDiscover:
		reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
	case dhcpv4.MessageTypeRequest:
		reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
	default:
		return IgnorePacketError{PacketType: mt, Details: "proxyDHCP only responds to Discover or Request message types"}
	}
	return nil
}

// IgnorePacketError is for when a dhcpServer packet should be ignored.
type IgnorePacketError struct {
	PacketType dhcpv4.MessageType
	Details    string
}

// Error returns the string representation of ErrIgnorePacket.
func (e IgnorePacketError) Error() string {
	return fmt.Sprintf("Ignoring packet: message type %s: details %s", e.PacketType, e.Details)
}

// 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
}
