package dhcpsvc

import (
	"context"
	"fmt"
	"net/netip"

	"github.com/AdguardTeam/golibs/errors"
	"github.com/google/gopacket"
	"github.com/google/gopacket/layers"
)

// serveV4 handles the ethernet packet of IPv4 type.  rw and pkt must not be
// nil.
func (srv *DHCPServer) serveV4(
	ctx context.Context,
	rw responseWriter4,
	pkt gopacket.Packet,
) (err error) {
	defer func() { err = errors.Annotate(err, "serving dhcpv4: %w") }()

	req, ok := pkt.Layer(layers.LayerTypeDHCPv4).(*layers.DHCPv4)
	if !ok {
		// TODO(e.burkov):  Consider adding some debug information about the
		// actual received packet.
		srv.logger.DebugContext(ctx, "skipping non-dhcpv4 packet")

		return nil
	}

	// TODO(e.burkov):  Handle duplicate Xid.

	if req.Operation != layers.DHCPOpRequest {
		srv.logger.DebugContext(ctx, "skipping non-request dhcpv4 packet", "op", req.Operation)

		return nil
	}

	typ, ok := msg4Type(req)
	if !ok {
		// The "DHCP message type" option - must be included in every DHCP
		// message.
		//
		// See https://datatracker.ietf.org/doc/html/rfc2131#section-3.
		return fmt.Errorf("dhcpv4: message type: %w", errors.ErrNoValue)
	}

	return srv.handleDHCPv4(ctx, rw, typ, req)
}

// handleDHCPv4 handles the DHCPv4 message of the given type.
func (srv *DHCPServer) handleDHCPv4(
	ctx context.Context,
	rw responseWriter4,
	typ layers.DHCPMsgType,
	req *layers.DHCPv4,
) (err error) {
	// Each interface should handle the DISCOVER and REQUEST messages offer and
	// allocate the available leases.  The RELEASE and DECLINE messages should
	// be handled by the server itself as it should remove the lease.
	switch typ {
	case layers.DHCPMsgTypeDiscover:
		srv.handleDiscover(ctx, rw, req)
	case layers.DHCPMsgTypeRequest:
		srv.handleRequest(ctx, rw, req)
	case layers.DHCPMsgTypeRelease:
		// TODO(e.burkov):  Remove the lease, either allocated or offered.
	case layers.DHCPMsgTypeDecline:
		// TODO(e.burkov):  Remove the allocated lease.  RFC tells it only
		// possible if the client found the address already in use.
	default:
		// TODO(e.burkov):  Handle DHCPINFORM.
		return fmt.Errorf("dhcpv4: request type: %w: %v", errors.ErrBadEnumValue, typ)
	}

	return nil
}

// handleDiscover handles the DHCPv4 message of discover type.
func (srv *DHCPServer) handleDiscover(ctx context.Context, rw responseWriter4, req *layers.DHCPv4) {
	// TODO(e.burkov):  Check existing leases, either allocated or offered.

	for _, iface := range srv.interfaces4 {
		go iface.handleDiscover(ctx, rw, req)
	}
}

// handleRequest handles the DHCPv4 message of request type.
func (srv *DHCPServer) handleRequest(ctx context.Context, rw responseWriter4, req *layers.DHCPv4) {
	srvID, hasSrvID := serverID4(req)
	reqIP, hasReqIP := requestedIPv4(req)

	switch {
	case hasSrvID && !srvID.IsUnspecified():
		// If the DHCPREQUEST message contains a server identifier option, the
		// message is in response to a DHCPOFFER message.  Otherwise, the
		// message is a request to verify or extend an existing lease.
		iface, hasIface := srv.interfaces4.findInterface(srvID)
		if !hasIface {
			srv.logger.DebugContext(ctx, "skipping selecting request", "serverid", srvID)

			return
		}

		iface.handleSelecting(ctx, rw, req, reqIP)
	case hasReqIP && !reqIP.IsUnspecified():
		// Requested IP address option MUST be filled in with client's notion of
		// its previously assigned address.
		iface, hasIface := srv.interfaces4.findInterface(reqIP)
		if !hasIface {
			srv.logger.DebugContext(ctx, "skipping init-reboot request", "requestedip", reqIP)

			return
		}

		iface.handleInitReboot(ctx, rw, req, reqIP)
	default:
		// Server identifier MUST NOT be filled in, requested IP address option
		// MUST NOT be filled in.
		ip, _ := netip.AddrFromSlice(req.ClientIP.To4())
		iface, hasIface := srv.interfaces4.findInterface(ip)
		if !hasIface {
			srv.logger.DebugContext(ctx, "skipping init-reboot request", "clientip", ip)

			return
		}

		iface.handleRenew(ctx, rw, req)
	}
}
