package dhcpsvc

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

	"github.com/AdguardTeam/golibs/errors"
	"github.com/AdguardTeam/golibs/logutil/slogutil"
	"github.com/gopacket/gopacket"
	"github.com/gopacket/gopacket/layers"
)

// serveEther6 handles the incoming ethernet packets and dispatches them to the
// appropriate handler.  It's used to run in a separate goroutine as it blocks
// until packets channel is closed.  iface and nd must not be nil.  nd must have
// at least a single address returned by its Addresses method.
func (srv *DHCPServer) serveEther6(ctx context.Context, iface *dhcpInterfaceV6, nd NetworkDevice) {
	defer slogutil.RecoverAndLog(ctx, srv.logger)

	src := gopacket.NewPacketSource(nd, nd.LinkType())
	srvDUID := newServerDUID(nd.HardwareAddr())

	// TODO(e.burkov):  Use [gopacket.PacketSource.PacketsCtx] and cancel
	// context on shutdown.
	for pkt := range src.Packets() {
		fd, err := newFrameData6(pkt, nd, srvDUID)
		if err != nil {
			srv.logger.DebugContext(ctx, "parsing frame data", slogutil.KeyError, err)

			continue
		}

		err = srv.serveV6(ctx, iface, pkt, fd)
		if err != nil {
			srv.logger.ErrorContext(ctx, "serving", slogutil.KeyError, err)
		}
	}
}

// serveV6 handles the ethernet packet of IPv6 type. iface and pkt must not be
// nil.  iface and fd must be valid.  pkt must be an IPv6 packet.
func (srv *DHCPServer) serveV6(
	ctx context.Context,
	iface *dhcpInterfaceV6,
	pkt gopacket.Packet,
	fd *frameData6,
) (err error) {
	defer func() { err = errors.Annotate(err, "serving dhcpv6: %w") }()

	msg, ok := pkt.Layer(layers.LayerTypeDHCPv6).(*layers.DHCPv6)
	if !ok {
		srv.logger.DebugContext(ctx, "skipping non-dhcpv6 packet", "pkt", pkt)

		return nil
	}

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

	return iface.handleDHCPv6(ctx, msg.MsgType, fd, msg)
}

// handleDHCPv6 handles the DHCPv6 message of the given type.
func (iface *dhcpInterfaceV6) handleDHCPv6(
	ctx context.Context,
	typ layers.DHCPv6MsgType,
	fd *frameData6,
	req *layers.DHCPv6,
) (err error) {
	switch typ {
	case layers.DHCPv6MsgTypeSolicit:
		return iface.handleSolicit(ctx, fd, req)
	case layers.DHCPv6MsgTypeRequest:
		return iface.handleRequest(ctx, fd, req)
	case layers.DHCPv6MsgTypeConfirm:
		return iface.handleConfirm(ctx, fd, req)
	case layers.DHCPv6MsgTypeRenew:
		return iface.handleRenew(ctx, fd, req)
	case layers.DHCPv6MsgTypeRebind:
		return iface.handleRebind(ctx, fd, req)
	case layers.DHCPv6MsgTypeInformationRequest:
		return iface.handleInfo(ctx, fd, req)
	case layers.DHCPv6MsgTypeRelease:
		return iface.handleRelease(ctx, fd, req)
	case layers.DHCPv6MsgTypeDecline:
		return iface.handleDecline(ctx, fd, req)
	default:
		return fmt.Errorf("dhcpv6: request type: %w: %d", errors.ErrBadEnumValue, typ)
	}
}

// handleSolicit handles messages of type SOLICIT.  req must not be nil and must
// be a valid DHCPv6 message of type SOLICIT.  fd must be valid.
func (iface *dhcpInterfaceV6) handleSolicit(
	ctx context.Context,
	fd *frameData6,
	req *layers.DHCPv6,
) (err error) {
	cliID, err := clientIDNoServer(req.Options)
	if err != nil {
		return fmt.Errorf("dhcpv6: %s: %w", req.MsgType, err)
	}

	l := iface.common.logger
	l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)

	iface.common.indexMu.Lock()
	defer iface.common.indexMu.Unlock()

	lease, iaid := iface.allocateForSolicit(ctx, fd.ether.SrcMAC, req)

	resp := &layers.DHCPv6{
		MsgType:       layers.DHCPv6MsgTypeAdvertise,
		TransactionID: req.TransactionID,
	}

	if lease == nil {
		resp.Options = iface.newSolicitRespOpts(fd, req, cliID, iaid, nil, false)

		return respond6(fd, resp)
	}

	_, isRapidCommit := findOption6(req.Options, layers.DHCPv6OptRapidCommit)

	if !isRapidCommit {
		resp.Options = iface.newSolicitRespOpts(fd, req, cliID, iaid, lease, false)

		return respond6(fd, resp)
	}

	err = iface.commit(ctx, req, lease)
	if err != nil {
		l.WarnContext(ctx, "committing rapid leases", slogutil.KeyError, err)
		isRapidCommit = false
	} else {
		// The server will also send a Reply in response to a Solicit with a
		// Rapid Commit option.
		//
		// See RFC 9915 Section 18.3.
		resp.MsgType = layers.DHCPv6MsgTypeReply
	}

	resp.Options = iface.newSolicitRespOpts(fd, req, cliID, iaid, lease, isRapidCommit)

	return respond6(fd, resp)
}

// handleRequest handles messages of type REQUEST.  req must not be nil and must
// be a valid DHCPv6 message of type REQUEST.  fd must be valid.
func (iface *dhcpInterfaceV6) handleRequest(
	ctx context.Context,
	fd *frameData6,
	req *layers.DHCPv6,
) (err error) {
	cliID, err := clientIDMatchingServer(req.Options, fd.duidData)
	if err != nil {
		return fmt.Errorf("dhcpv6: %s: %w", req.MsgType, err)
	}

	l := iface.common.logger
	l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)

	iface.common.indexMu.Lock()
	defer iface.common.indexMu.Unlock()

	resp := &layers.DHCPv6{
		MsgType:       layers.DHCPv6MsgTypeReply,
		TransactionID: req.TransactionID,
	}

	iana, ok := iface.firstIANA(ctx, req)
	if !ok {
		// In practice, the DHCPv6 REQUEST with no IA_NA options is not useful,
		// since the standard explicitly defines the INFORMATION-REQUEST message
		// type for that purpose.  However, there are no requirements which
		// invalidate such messages, so the server must respond.
		resp.Options = iface.newRequestRespOpts(fd, req, cliID, layers.DHCPv6Option{})

		return respond6(fd, resp)
	}

	reqIP, hasReqIP := iana.requestedAddr()
	if hasReqIP && !iface.subnetPrefix.Contains(reqIP) {
		respIANA := newIANAWithStatus(iana.ID, layers.DHCPv6StatusCodeNotOnLink)
		resp.Options = iface.newRequestRespOpts(fd, req, cliID, respIANA)

		return respond6(fd, resp)
	}

	ianaOpt := iface.ianaForRequest(ctx, req, iana.ID, fd.ether.SrcMAC)

	resp.Options = iface.newRequestRespOpts(fd, req, cliID, ianaOpt)

	return respond6(fd, resp)
}

// handleConfirm handles messages of type CONFIRM.  req must not be nil and must
// be a valid DHCPv6 message of type CONFIRM.  fd must be valid.
func (iface *dhcpInterfaceV6) handleConfirm(
	ctx context.Context,
	fd *frameData6,
	req *layers.DHCPv6,
) (err error) {
	cliID, err := clientIDNoServer(req.Options)
	if err != nil {
		return fmt.Errorf("dhcpv6: %s: %w", req.MsgType, err)
	}

	l := iface.common.logger
	l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)

	// Collect all addresses from IA_NA options and check if they are
	// appropriate for the link to which the client is attached.
	//
	// See RFC 9915 Section 18.3.3.
	allOnLink, hasAddrs := iface.confirmAddrsOnLink(ctx, req)
	if !hasAddrs {
		// If the server is unable to perform this test (for example, the server
		// does not have information about prefixes on the link to which the
		// client is connected) or there were no addresses in any of the IAs
		// sent by the client, the server MUST NOT send a Reply to the client.
		//
		// See RFC 9915 Section 18.3.3.
		l.DebugContext(ctx, "no addresses in IA_NA options")

		return nil
	}

	status := layers.DHCPv6StatusCodeSuccess
	if !allOnLink {
		status = layers.DHCPv6StatusCodeNotOnLink
	}

	resp := &layers.DHCPv6{
		MsgType:       layers.DHCPv6MsgTypeReply,
		TransactionID: req.TransactionID,
		Options:       iface.newConfirmRespOpts(fd, req, cliID, status),
	}

	return respond6(fd, resp)
}

// handleRenew handles messages of type RENEW.  req must not be nil and must be
// a valid DHCPv6 message of type RENEW.  fd must be valid.
//
// TODO(e.burkov):  The current implementation renews only the first valid IA_NA
// option.  It does not verify that the addresses in the IA match the stored
// lease, since clients are identified by MAC address rather than DUID+IAID.
func (iface *dhcpInterfaceV6) handleRenew(
	ctx context.Context,
	fd *frameData6,
	req *layers.DHCPv6,
) (err error) {
	cliID, err := clientIDMatchingServer(req.Options, fd.duidData)
	if err != nil {
		return fmt.Errorf("dhcpv6: %s: %w", req.MsgType, err)
	}

	l := iface.common.logger
	l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)

	iface.common.indexMu.Lock()
	defer iface.common.indexMu.Unlock()

	resp := &layers.DHCPv6{
		MsgType:       layers.DHCPv6MsgTypeReply,
		TransactionID: req.TransactionID,
	}

	iana, ok := iface.firstIANA(ctx, req)
	if !ok {
		// With no IA_NA options and no requested addresses there's nothing to
		// renew.  Respond with no IA options similarly to how the Request
		// handler does.
		//
		// See RFC 9915 Section 18.3.4.
		resp.Options = iface.newUpdateRespOpts(fd, req, cliID, layers.DHCPv6Option{})

		return respond6(fd, resp)
	}

	ianaOpt := iface.ianaForUpdate(ctx, req, iana, fd.ether.SrcMAC)
	resp.Options = iface.newUpdateRespOpts(fd, req, cliID, ianaOpt)

	return respond6(fd, resp)
}

// handleRebind handles messages of type REBIND.  req must not be nil and must
// be a valid DHCPv6 message of type REBIND, fd must be valid.
//
// TODO(e.burkov):  The current implementation rebinds only the first valid
// IA_NA option.  It does not verify that the addresses in the IA match the
// stored lease, since clients are identified by MAC address rather than
// DUID+IAID.
func (iface *dhcpInterfaceV6) handleRebind(
	ctx context.Context,
	fd *frameData6,
	req *layers.DHCPv6,
) (err error) {
	cliID, err := clientIDNoServer(req.Options)
	if err != nil {
		return fmt.Errorf("dhcpv6: %s: %w", req.MsgType, err)
	}

	l := iface.common.logger
	l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)

	iface.common.indexMu.Lock()
	defer iface.common.indexMu.Unlock()

	resp := &layers.DHCPv6{
		MsgType:       layers.DHCPv6MsgTypeReply,
		TransactionID: req.TransactionID,
	}

	iana, ok := iface.firstIANA(ctx, req)
	if !ok {
		// With no IA_NA options and no requested addresses there's nothing to
		// rebind.  Respond with no IA options similarly to how the Renew
		// handler does.
		//
		// See RFC 9915 Section 18.3.5.
		resp.Options = iface.newUpdateRespOpts(fd, req, cliID, layers.DHCPv6Option{})

		return respond6(fd, resp)
	}

	ianaOpt := iface.ianaForUpdate(ctx, req, iana, fd.ether.SrcMAC)
	resp.Options = iface.newUpdateRespOpts(fd, req, cliID, ianaOpt)

	return respond6(fd, resp)
}

// handleInfo handles messages of type INFORMATION-REQUEST.  req must not be nil
// and must be a valid DHCPv6 message of type INFORMATION-REQUEST.  fd must be
// valid.
//
// TODO(e.burkov):  The current implementation does not handle relay-forwarded
// INFORMATION-REQUEST messages.
func (iface *dhcpInterfaceV6) handleInfo(
	ctx context.Context,
	fd *frameData6,
	req *layers.DHCPv6,
) (err error) {
	srvID, ok := findOption6(req.Options, layers.DHCPv6OptServerID)
	if ok && !bytes.Equal(srvID, fd.duidData) {
		return fmt.Errorf(
			"dhcpv6: server id: got %v, want %v: %w",
			srvID,
			fd.duidData,
			errors.ErrNotEqual,
		)
	}

	_, ok = findOption6(req.Options, layers.DHCPv6OptIANA)
	if ok {
		return fmt.Errorf("dhcpv6: %s: ia option: %w", req.MsgType, errors.ErrUnexpectedValue)
	}

	_, ok = findOption6(req.Options, layers.DHCPv6OptIATA)
	if ok {
		return fmt.Errorf("dhcpv6: %s: ia option: %w", req.MsgType, errors.ErrUnexpectedValue)
	}

	l := iface.common.logger
	l.DebugContext(ctx, "handling message", "type", req.MsgType)

	resp := &layers.DHCPv6{
		MsgType:       layers.DHCPv6MsgTypeReply,
		TransactionID: req.TransactionID,
		Options:       iface.newInfoRespOpts(fd, req),
	}

	return respond6(fd, resp)
}

// handleRelease handles messages of type RELEASE.  req must not be nil and must
// be a valid DHCPv6 message of type RELEASE.  fd must be valid.
//
// TODO(e.burkov):  Verify all IA_NA options instead of handling only the first
// one.
func (iface *dhcpInterfaceV6) handleRelease(
	ctx context.Context,
	fd *frameData6,
	req *layers.DHCPv6,
) (err error) {
	cliID, err := clientIDMatchingServer(req.Options, fd.duidData)
	if err != nil {
		return fmt.Errorf("dhcpv6: %s: %w", req.MsgType, err)
	}

	l := iface.common.logger
	l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)

	resp := &layers.DHCPv6{
		MsgType:       layers.DHCPv6MsgTypeReply,
		TransactionID: req.TransactionID,
	}

	iaid, ip := iface.ipForRelease(ctx, req)
	if ip == (netip.Addr{}) {
		resp.Options = iface.newUpdateRespOpts(fd, req, cliID, layers.DHCPv6Option{})

		return respond6(fd, resp)
	}

	key := macToKey(fd.ether.SrcMAC)

	iface.common.indexMu.Lock()
	defer iface.common.indexMu.Unlock()

	lease, hasLease := iface.common.leases[key]
	if !hasLease || lease.IP != ip {
		respIANA := newIANAWithStatus(iaid, layers.DHCPv6StatusCodeNoBinding)
		resp.Options = iface.newUpdateRespOpts(fd, req, cliID, respIANA)

		return respond6(fd, resp)
	}

	err = iface.common.index.remove(ctx, lease, iface.common)
	if err != nil {
		l.ErrorContext(ctx, "removing lease", slogutil.KeyError, err)

		respIANA := newIANAWithStatus(iaid, layers.DHCPv6StatusCodeNoBinding)
		resp.Options = iface.newUpdateRespOpts(fd, req, cliID, respIANA)
	} else {
		respIANA := &IANAOption{ID: iaid}
		resp.Options = iface.newUpdateRespOpts(fd, req, cliID, respIANA.Encode())
	}

	return respond6(fd, resp)
}

// handleDecline handles messages of type DECLINE.  req must not be nil and must
// be a valid DHCPv6 message of type DECLINE.  fd must be valid.
//
// TODO(e.burkov):  Implement.  This is a stub for now.
func (iface *dhcpInterfaceV6) handleDecline(
	ctx context.Context,
	fd *frameData6,
	req *layers.DHCPv6,
) (err error) {
	cliID, err := clientIDMatchingServer(req.Options, fd.duidData)
	if err != nil {
		return fmt.Errorf("dhcpv6: %s: %w", req.MsgType, err)
	}

	l := iface.common.logger
	l.DebugContext(ctx, "handling message", "type", req.MsgType, "cli_id", cliID)

	return nil
}
