// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2025 Maxime Leroy, Free Mobile

#include "if_map.h"
#include "l3vni_map.h"
#include "log_grout.h"
#include "rt_grout.h"

#include <gr_l2.h>
#include <gr_srv6.h>

#include <lib/libfrr.h>
#include <lib/srv6.h>
#include <lib/version.h>
#include <linux/neighbour.h>
#include <zebra/rib.h>
#include <zebra/table_manager.h>
#include <zebra_dplane_grout.h>

static inline bool is_selfroute(gr_nh_origin_t origin) {
	switch (origin) {
	case GR_NH_ORIGIN_ZEBRA:
	case GR_NH_ORIGIN_BABEL:
	case GR_NH_ORIGIN_BGP:
	case GR_NH_ORIGIN_ISIS:
	case GR_NH_ORIGIN_OSPF:
	case GR_NH_ORIGIN_RIP:
	case GR_NH_ORIGIN_RIPNG:
	case GR_NH_ORIGIN_NHRP:
	case GR_NH_ORIGIN_EIGRP:
	case GR_NH_ORIGIN_LDP:
	case GR_NH_ORIGIN_SHARP:
	case GR_NH_ORIGIN_PBR:
	case GR_NH_ORIGIN_ZSTATIC:
	case GR_NH_ORIGIN_OPENFABRIC:
	case GR_NH_ORIGIN_SRTE:
		return true;
	default:
		return false;
	}
}

static inline gr_nh_origin_t zebra2origin(int proto) {
	gr_nh_origin_t origin;

	switch (proto) {
	case ZEBRA_ROUTE_BABEL:
		origin = GR_NH_ORIGIN_BABEL;
		break;
	case ZEBRA_ROUTE_BGP:
		origin = GR_NH_ORIGIN_BGP;
		break;
	case ZEBRA_ROUTE_OSPF:
	case ZEBRA_ROUTE_OSPF6:
		origin = GR_NH_ORIGIN_OSPF;
		break;
	case ZEBRA_ROUTE_STATIC:
		origin = GR_NH_ORIGIN_ZSTATIC;
		break;
	case ZEBRA_ROUTE_ISIS:
		origin = GR_NH_ORIGIN_ISIS;
		break;
	case ZEBRA_ROUTE_RIP:
		origin = GR_NH_ORIGIN_RIP;
		break;
	case ZEBRA_ROUTE_RIPNG:
		origin = GR_NH_ORIGIN_RIPNG;
		break;
	case ZEBRA_ROUTE_NHRP:
		origin = GR_NH_ORIGIN_NHRP;
		break;
	case ZEBRA_ROUTE_EIGRP:
		origin = GR_NH_ORIGIN_EIGRP;
		break;
	case ZEBRA_ROUTE_LDP:
		origin = GR_NH_ORIGIN_LDP;
		break;
	case ZEBRA_ROUTE_SHARP:
		origin = GR_NH_ORIGIN_SHARP;
		break;
	case ZEBRA_ROUTE_PBR:
		origin = GR_NH_ORIGIN_PBR;
		break;
	case ZEBRA_ROUTE_OPENFABRIC:
		origin = GR_NH_ORIGIN_OPENFABRIC;
		break;
	case ZEBRA_ROUTE_SRTE:
		origin = GR_NH_ORIGIN_SRTE;
		break;
	case ZEBRA_ROUTE_TABLE:
	case ZEBRA_ROUTE_NHG:
		origin = GR_NH_ORIGIN_ZEBRA;
		break;
	case ZEBRA_ROUTE_CONNECT:
	case ZEBRA_ROUTE_LOCAL:
	case ZEBRA_ROUTE_KERNEL:
		origin = GR_NH_ORIGIN_LINK;
		break;
	default:
		// When a user adds a new protocol this will show up
		// to let them know to do something about it.  This
		// is intentionally a warn because we should see
		// this as part of development of a new protocol.
		gr_log_debug("unhandled protocol %d, defaulting to GR_NH_ORIGIN_ZEBRA", proto);
		origin = GR_NH_ORIGIN_ZEBRA;
		break;
	}

	return origin;
}

static inline int origin2zebra(gr_nh_origin_t origin, int family, bool is_nexthop) {
	int proto;

	switch (origin) {
	case GR_NH_ORIGIN_BABEL:
		proto = ZEBRA_ROUTE_BABEL;
		break;
	case GR_NH_ORIGIN_BGP:
		proto = ZEBRA_ROUTE_BGP;
		break;
	case GR_NH_ORIGIN_OSPF:
		proto = (family == AF_INET) ? ZEBRA_ROUTE_OSPF : ZEBRA_ROUTE_OSPF6;
		break;
	case GR_NH_ORIGIN_ISIS:
		proto = ZEBRA_ROUTE_ISIS;
		break;
	case GR_NH_ORIGIN_RIP:
		proto = ZEBRA_ROUTE_RIP;
		break;
	case GR_NH_ORIGIN_RIPNG:
		proto = ZEBRA_ROUTE_RIPNG;
		break;
	case GR_NH_ORIGIN_NHRP:
		proto = ZEBRA_ROUTE_NHRP;
		break;
	case GR_NH_ORIGIN_EIGRP:
		proto = ZEBRA_ROUTE_EIGRP;
		break;
	case GR_NH_ORIGIN_LDP:
		proto = ZEBRA_ROUTE_LDP;
		break;
	case GR_NH_ORIGIN_ZSTATIC:
		proto = ZEBRA_ROUTE_STATIC;
		break;
	case GR_NH_ORIGIN_SHARP:
		proto = ZEBRA_ROUTE_SHARP;
		break;
	case GR_NH_ORIGIN_PBR:
		proto = ZEBRA_ROUTE_PBR;
		break;
	case GR_NH_ORIGIN_OPENFABRIC:
		proto = ZEBRA_ROUTE_OPENFABRIC;
		break;
	case GR_NH_ORIGIN_SRTE:
		proto = ZEBRA_ROUTE_SRTE;
		break;
	case GR_NH_ORIGIN_STATIC:
	case GR_NH_ORIGIN_UNSPEC:
	case GR_NH_ORIGIN_REDIRECT:
	case GR_NH_ORIGIN_LINK:
	case GR_NH_ORIGIN_LEARN:
	case GR_NH_ORIGIN_GATED:
	case GR_NH_ORIGIN_RA:
	case GR_NH_ORIGIN_MRT:
	case GR_NH_ORIGIN_BIRD:
	case GR_NH_ORIGIN_DNROUTED:
	case GR_NH_ORIGIN_XORP:
	case GR_NH_ORIGIN_NTK:
	case GR_NH_ORIGIN_MROUTED:
	case GR_NH_ORIGIN_KEEPALIVED:
	case GR_NH_ORIGIN_OPENR:
		proto = ZEBRA_ROUTE_KERNEL;
		break;
	case GR_NH_ORIGIN_ZEBRA:
		if (is_nexthop) {
			proto = ZEBRA_ROUTE_NHG;
			break;
		}
		proto = ZEBRA_ROUTE_KERNEL;
		break;
	default:
		// When a user adds a new protocol this will show up
		// to let them know to do something about it.  This
		// is intentionally a warn because we should see
		// this as part of development of a new protocol
		gr_log_debug("unhandled origin %d, defaulting to ZEBRA_ROUTE_KERNEL", origin);
		proto = ZEBRA_ROUTE_KERNEL;
		break;
	}
	return proto;
}

static int grout_gr_nexthop_to_frr_nexthop(
	const struct gr_nexthop *gr_nh,
	struct nexthop *nh,
	int *nh_family
) {
	nh->ifindex = ifindex_grout_to_frr(gr_nh->iface_id);
	nh->vrf_id = vrf_grout_to_frr(gr_nh->vrf_id);
	nh->weight = 1;

	switch (gr_nh->type) {
	case GR_NH_T_BLACKHOLE:
	case GR_NH_T_REJECT:
		nh->type = NEXTHOP_TYPE_BLACKHOLE;
		nh->bh_type = gr_nh->type == GR_NH_T_REJECT ? BLACKHOLE_REJECT : BLACKHOLE_NULL;
		*nh_family = AF_UNSPEC;
		break;
	case GR_NH_T_L3: {
		const struct gr_nexthop_info_l3 *l3;
		l3 = (const struct gr_nexthop_info_l3 *)gr_nh->info;

		switch (l3->af) {
		case GR_AF_IP4:
			if (gr_nh->iface_id)
				nh->type = NEXTHOP_TYPE_IPV4_IFINDEX;
			else
				nh->type = NEXTHOP_TYPE_IPV4;

			*nh_family = AF_INET;
			memcpy(&nh->gate.ipv4, &l3->ipv4, sizeof(nh->gate.ipv4));
			break;
		case GR_AF_IP6:
			if (gr_nh->iface_id)
				nh->type = NEXTHOP_TYPE_IPV6_IFINDEX;
			else
				nh->type = NEXTHOP_TYPE_IPV6;

			*nh_family = AF_INET6;
			memcpy(&nh->gate.ipv6, &l3->ipv6, sizeof(nh->gate.ipv6));
			break;
		case GR_AF_UNSPEC:
			nh->type = NEXTHOP_TYPE_IFINDEX;
			*nh_family = AF_UNSPEC;
			break;
		default:
			gr_log_err("unsupported nexthop family %u", l3->af);
			return -1;
		}
		break;
	}
	case GR_NH_T_SR6_LOCAL: {
		enum seg6local_action_t action = ZEBRA_SEG6_LOCAL_ACTION_UNSPEC;
		const struct gr_nexthop_info_srv6_local *sr6;
		struct seg6local_context ctx;

		memset(&ctx, 0, sizeof(ctx));

		sr6 = (const struct gr_nexthop_info_srv6_local *)gr_nh->info;

		SET_SRV6_FLV_OP(ctx.flv.flv_ops, ZEBRA_SEG6_LOCAL_FLV_OP_USP);
		if (sr6->flags & GR_SR_FL_FLAVOR_PSP)
			SET_SRV6_FLV_OP(ctx.flv.flv_ops, ZEBRA_SEG6_LOCAL_FLV_OP_PSP);
		if (sr6->flags & GR_SR_FL_FLAVOR_USD)
			SET_SRV6_FLV_OP(ctx.flv.flv_ops, ZEBRA_SEG6_LOCAL_FLV_OP_USD);
		if (sr6->flags & GR_SR_FL_FLAVOR_NEXT_CSID) {
			SET_SRV6_FLV_OP(ctx.flv.flv_ops, ZEBRA_SEG6_LOCAL_FLV_OP_NEXT_CSID);
			ctx.flv.lcblock_len = sr6->block_bits;
			ctx.flv.lcnode_func_len = sr6->csid_bits;
		}

		switch (sr6->behavior) {
		case SR_BEHAVIOR_END:
			action = ZEBRA_SEG6_LOCAL_ACTION_END;
			break;
		case SR_BEHAVIOR_END_X:
#if CURRENT_FRR_VERSION >= MAKE_FRRVERSION(10, 6, 0)
			action = ZEBRA_SEG6_LOCAL_ACTION_END_X;
			memcpy(&ctx.nh6, &sr6->endx_addr, sizeof(ctx.nh6));
			ctx.ifindex = ifindex_grout_to_frr(gr_nh->iface_id);
			break;
#else
			gr_log_err("unsupported srv6 behavior end.x");
			return -1;
#endif
		case SR_BEHAVIOR_END_T:
			action = ZEBRA_SEG6_LOCAL_ACTION_END_T;
			break;
		case SR_BEHAVIOR_END_DT6:
			action = ZEBRA_SEG6_LOCAL_ACTION_END_DT6;
			break;
		case SR_BEHAVIOR_END_DT4:
			action = ZEBRA_SEG6_LOCAL_ACTION_END_DT4;
			break;
		case SR_BEHAVIOR_END_DT46:
			action = ZEBRA_SEG6_LOCAL_ACTION_END_DT46;
			break;
		}

		ctx.table = vrf_grout_to_frr(sr6->out_vrf_id);
		nexthop_add_srv6_seg6local(nh, action, &ctx);
		nh->type = nh->ifindex ? NEXTHOP_TYPE_IPV6_IFINDEX : NEXTHOP_TYPE_IPV6;
		*nh_family = AF_INET6;
		break;
	}
	case GR_NH_T_SR6_OUTPUT: {
		enum srv6_headend_behavior encap_behavior = SRV6_HEADEND_BEHAVIOR_H_ENCAPS;
		const struct gr_nexthop_info_srv6 *sr6;

		sr6 = (const struct gr_nexthop_info_srv6 *)gr_nh->info;

		switch (sr6->encap_behavior) {
		case SR_H_ENCAPS:
			encap_behavior = SRV6_HEADEND_BEHAVIOR_H_ENCAPS;
			break;
		case SR_H_ENCAPS_RED:
			encap_behavior = SRV6_HEADEND_BEHAVIOR_H_ENCAPS_RED;
			break;
		}

		nexthop_add_srv6_seg6(
			nh,
			(void *)sr6->seglist,
			sr6->n_seglist,
			encap_behavior
#if CURRENT_FRR_VERSION >= MAKE_FRRVERSION(10, 8, 0)
			,
			rte_ipv6_addr_is_unspec(&sr6->encap_src) ? NULL : (void *)&sr6->encap_src
#endif
		);
		nh->type = nh->ifindex ? NEXTHOP_TYPE_IPV6_IFINDEX : NEXTHOP_TYPE_IPV6;
		*nh_family = AF_INET6;
		break;
	}
	case GR_NH_T_GROUP:
		nh->ifindex = ifindex_grout_to_frr(gr_nh->iface_id);
		nh->vrf_id = vrf_grout_to_frr(gr_nh->vrf_id);
		nh->type = NEXTHOP_TYPE_IFINDEX;
		*nh_family = AF_UNSPEC;
		nh->weight = 1;
		break;
	default:
		gr_log_err("unsupported nexthop type %s", gr_nh_type_name(gr_nh->type));
		return -1;
	}

	return 0;
}

static void grout_route_change(
	bool new,
	gr_nh_origin_t origin,
	uint16_t family,
	void *dest_addr,
	uint8_t dest_prefixlen,
	uint16_t gr_route_vrf_id,
	struct gr_nexthop *gr_nh,
	bool startup
) {
	uint32_t vrf_id = vrf_grout_to_frr(gr_route_vrf_id);
	bool selfroute = is_selfroute(origin);
	int proto = ZEBRA_ROUTE_KERNEL;
	uint32_t nh_id = gr_nh->nh_id;
	// Grout has no per‑VRF routing tables; table_id always equals vrf_id
	uint32_t tableid = vrf_id;
	struct nexthop *nh = NULL;
	uint32_t flags = 0;
	struct prefix p;
	size_t sz;
	afi_t afi;

	// Echo of our own install: skip. Mirrors the !startup && selfroute
	// guard in netlink_route_change (zebra/rt_netlink.c).
	if (new && !startup && selfroute) {
		gr_log_debug(
			"self-originated %s route notification, skip", gr_nh_origin_name(origin)
		);
		return;
	}

	if (origin == GR_NH_ORIGIN_LINK) {
		gr_log_debug("link route, skip");
		return;
	}

	// if no nh_id, parse nexthop
	if (nh_id == 0) {
		int nh_family;

		nh = nexthop_new();

		if (grout_gr_nexthop_to_frr_nexthop(gr_nh, nh, &nh_family) < 0) {
			gr_log_debug("invalid nexthop, skip route");
			nexthop_free(nh);
			return;
		}

		if (nh_family != AF_UNSPEC && nh_family != family) {
			gr_log_debug(
				"nexthop family %u != route family %u, skip", nh_family, family
			);
			nexthop_free(nh);
			return;
		}
	}

	if (family == AF_INET) {
		afi = AFI_IP;
		p.family = AF_INET;
		sz = 4;

		memcpy(&p.u.prefix4, dest_addr, sz);
		p.prefixlen = dest_prefixlen;
	} else {
		afi = AFI_IP6;
		p.family = AF_INET6;
		sz = 16;

		memcpy(&p.u.prefix6, dest_addr, sz);
		p.prefixlen = dest_prefixlen;
	}

	proto = origin2zebra(origin, family, false);

	// Route inserted by Zebra: mark SELFROUTE so rib_compare_routes can
	// swap with a client's re-push and rib_sweep_route can purge.
	if (selfroute)
		flags |= ZEBRA_FLAG_SELFROUTE;

	if (new) {
		struct route_entry *re;
		struct nexthop_group *ng = NULL;

		re = zebra_rib_route_entry_new(vrf_id, proto, 0, flags, nh_id, tableid, 0, 0, 0, 0);
		if (nh) {
			ng = nexthop_group_new();
			nexthop_group_add_sorted(ng, nh);
			assert(nh_id == 0);
		}

#if CURRENT_FRR_VERSION >= MAKE_FRRVERSION(10, 6, 0)
		rib_add_multipath(afi, SAFI_UNICAST, &p, NULL, re, ng, false, false);
#else
		rib_add_multipath(afi, SAFI_UNICAST, &p, NULL, re, ng, false);
#endif

		if (ng)
			nexthop_group_delete(&ng);
	} else {
		rib_delete(
			afi,
			SAFI_UNICAST,
			vrf_id,
			proto,
			0,
			flags,
			&p,
			NULL,
			nh,
			nh_id,
			tableid,
			0,
			0,
			true
		);
		if (nh)
			nexthop_free(nh);
	}
}

void grout_route4_change(bool new, struct gr_ip4_route *gr_r4, bool startup) {
	gr_log_debug(
		"%s %pI4/%u origin %s nh_id %u route_vrf %u nh_vrf %u",
		new ? "add" : "del",
		&gr_r4->dest.ip,
		gr_r4->dest.prefixlen,
		gr_nh_origin_name(gr_r4->origin),
		gr_r4->nh.nh_id,
		gr_r4->vrf_id,
		gr_r4->nh.vrf_id
	);
	grout_route_change(
		new,
		gr_r4->origin,
		AF_INET,
		(void *)&gr_r4->dest.ip,
		gr_r4->dest.prefixlen,
		gr_r4->vrf_id,
		&gr_r4->nh,
		startup
	);
}

void grout_route6_change(bool new, struct gr_ip6_route *gr_r6, bool startup) {
	gr_log_debug(
		"%s %pI6/%u origin %s nh_id %u route_vrf %u nh_vrf %u",
		new ? "add" : "del",
		&gr_r6->dest.ip,
		gr_r6->dest.prefixlen,
		gr_nh_origin_name(gr_r6->origin),
		gr_r6->nh.nh_id,
		gr_r6->vrf_id,
		gr_r6->nh.vrf_id
	);
	grout_route_change(
		new,
		gr_r6->origin,
		AF_INET6,
		(void *)&gr_r6->dest.ip,
		gr_r6->dest.prefixlen,
		gr_r6->vrf_id,
		&gr_r6->nh,
		startup
	);
}

enum zebra_dplane_result grout_add_del_route(struct zebra_dplane_ctx *ctx) {
	bool new = dplane_ctx_get_op(ctx) != DPLANE_OP_ROUTE_DELETE;
	union {
		struct gr_ip4_route_add_req r4_add;
		struct gr_ip4_route_del_req r4_del;
		struct gr_ip6_route_add_req r6_add;
		struct gr_ip6_route_del_req r6_del;
	} req;
	uint32_t nh_id = dplane_ctx_get_nhe_id(ctx);
	uint32_t vrf_id = vrf_frr_to_grout(dplane_ctx_get_vrf(ctx));
	const struct prefix *p;
	gr_nh_origin_t origin;
	uint32_t req_type;
	size_t req_len;

	origin = zebra2origin(dplane_ctx_get_type(ctx));
	p = dplane_ctx_get_dest(ctx);

	gr_log_debug(
		"%s %pFX origin %s nh_id %u vrf %u",
		new ? "add" : "del",
		p,
		gr_nh_origin_name(origin),
		nh_id,
		vrf_id
	);

	if (!is_selfroute(origin)) {
		gr_log_debug("non-frr origin, skip");
		return ZEBRA_DPLANE_REQUEST_SUCCESS;
	}

	// Route was read from grout at startup and injected into zebra's RIB
	// with ZEBRA_FLAG_SELFROUTE. Grout already has it, do not round-trip
	// the install/update back to grout. Deletes must still flow so that
	// rib_sweep_route can purge unreclaimed orphans.
	if (new && (dplane_ctx_get_flags(ctx) & ZEBRA_FLAG_SELFROUTE)) {
		gr_log_debug("selfroute install/update, already in grout, skip");
		return ZEBRA_DPLANE_REQUEST_SUCCESS;
	}

	if (new && nh_id == 0) {
		gr_log_err("no nh_id, skip");
		return ZEBRA_DPLANE_REQUEST_FAILURE;
	}
	if (p->family != AF_INET && p->family != AF_INET6) {
		gr_log_err("unsupported family %u, skip", p->family);
		return ZEBRA_DPLANE_REQUEST_FAILURE;
	}
	if (dplane_ctx_get_src(ctx) != NULL) {
		gr_log_err("src routes not supported, skip");
		return ZEBRA_DPLANE_REQUEST_FAILURE;
	}
	// TODO: other check for metric, distance, and so-on

	if (p->family == AF_INET) {
		struct ip4_net *dest;

		if (new) {
			req.r4_add = (struct gr_ip4_route_add_req) {
				.exist_ok = true, .vrf_id = vrf_id
			};

			req_type = GR_IP4_ROUTE_ADD;
			req_len = sizeof(struct gr_ip4_route_add_req);

			req.r4_add.vrf_id = vrf_id;
			req.r4_add.nh_id = nh_id;
			req.r4_add.origin = origin;
			dest = &req.r4_add.dest;
		} else {
			req.r4_del = (struct gr_ip4_route_del_req) {
				.missing_ok = true, .vrf_id = vrf_id
			};
			req_type = GR_IP4_ROUTE_DEL;
			req_len = sizeof(struct gr_ip4_route_del_req);

			dest = &req.r4_del.dest;
			new = false;
		}

		dest->ip = p->u.prefix4.s_addr;
		dest->prefixlen = p->prefixlen;
	} else {
		struct ip6_net *dest;

		if (new) {
			req.r6_add = (struct gr_ip6_route_add_req) {
				.exist_ok = true, .vrf_id = vrf_id
			};

			req_type = GR_IP6_ROUTE_ADD;
			req_len = sizeof(struct gr_ip6_route_add_req);

			req.r6_add.nh_id = nh_id;
			req.r6_add.origin = origin;
			dest = &req.r6_add.dest;
		} else {
			req.r6_del = (struct gr_ip6_route_del_req) {
				.missing_ok = true, .vrf_id = vrf_id
			};

			req_type = GR_IP6_ROUTE_DEL;
			req_len = sizeof(struct gr_ip6_route_del_req);

			dest = &req.r6_del.dest;
			new = false;
		}

		memcpy(dest->ip.a, p->u.prefix6.s6_addr, sizeof(dest->ip.a));
		dest->prefixlen = p->prefixlen;
	}

	if (grout_client_send_recv(req_type, req_len, &req, NULL) < 0)
		return ZEBRA_DPLANE_REQUEST_FAILURE;

	return ZEBRA_DPLANE_REQUEST_SUCCESS;
}

static enum zebra_dplane_result grout_add_nexthop_group(struct zebra_dplane_ctx *ctx) {
	enum zebra_dplane_result ret = ZEBRA_DPLANE_REQUEST_SUCCESS;
	uint32_t nh_id = dplane_ctx_get_nhe_id(ctx);
	struct gr_nexthop_info_group *group;
	struct gr_nh_add_req *req = NULL;
	size_t len;

	gr_log_debug("nh_id %u", nh_id);

	len = sizeof(*req) + sizeof(*group)
		+ dplane_ctx_get_nhe_nh_grp_count(ctx) * sizeof(group->members[0]);
	if ((req = calloc(1, len)) == NULL) {
		gr_log_debug("calloc: %s", strerror(errno));
		return ZEBRA_DPLANE_REQUEST_FAILURE;
	}

	group = (struct gr_nexthop_info_group *)req->nh.info;
	group->n_members = dplane_ctx_get_nhe_nh_grp_count(ctx);

	req->exist_ok = true;
	req->nh.nh_id = nh_id;
	req->nh.type = GR_NH_T_GROUP;
	req->nh.origin = zebra2origin(dplane_ctx_get_nhe_type(ctx));

	const struct nh_grp *nhs = dplane_ctx_get_nhe_nh_grp(ctx);
	for (size_t i = 0; i < group->n_members; i++) {
		group->members[i].nh_id = nhs[i].id;
		group->members[i].weight = nhs[i].weight;
	}

	if (grout_client_send_recv(GR_NH_ADD, len, req, NULL) < 0)
		ret = ZEBRA_DPLANE_REQUEST_FAILURE;

	free(req);
	return ret;
}

static enum zebra_dplane_result grout_del_nexthop(uint32_t nh_id) {
	gr_log_debug("nh_id %u", nh_id);

	struct gr_nh_del_req req = {.missing_ok = true, .nh = {.nh_id = nh_id}};

	if (grout_client_send_recv(GR_NH_DEL, sizeof(req), &req, NULL) < 0)
		return ZEBRA_DPLANE_REQUEST_FAILURE;
	return ZEBRA_DPLANE_REQUEST_SUCCESS;
}

static enum zebra_dplane_result
grout_add_nexthop(uint32_t nh_id, gr_nh_origin_t origin, const struct nexthop *nh) {
	enum zebra_dplane_result ret = ZEBRA_DPLANE_REQUEST_FAILURE;
	struct gr_nexthop_info_srv6_local *sr6_local;
	struct gr_nexthop_info_srv6 *sr6;
	struct gr_nh_add_req *req = NULL;
	struct gr_nexthop_info_l3 *l3;
	const struct ethaddr *rmac;
	size_t len = sizeof(*req);
	uint16_t vxlan_iface_id;
	struct ipaddr vtep;
	gr_nh_type_t type;

	switch (nh->type) {
	case NEXTHOP_TYPE_IPV4:
	case NEXTHOP_TYPE_IPV4_IFINDEX:
	case NEXTHOP_TYPE_IPV6:
	case NEXTHOP_TYPE_IPV6_IFINDEX:
	case NEXTHOP_TYPE_IFINDEX:
		if (nh->nh_srv6 != NULL
		    && nh->nh_srv6->seg6local_action != ZEBRA_SEG6_LOCAL_ACTION_UNSPEC) {
			len += sizeof(*sr6_local);
			type = GR_NH_T_SR6_LOCAL;
		} else if (nh->nh_srv6 != NULL && nh->nh_srv6->seg6_segs->num_segs > 0) {
			assert(nh->nh_srv6->seg6_segs != NULL);
			len += sizeof(*sr6)
				+ nh->nh_srv6->seg6_segs->num_segs * sizeof(sr6->seglist[0]);
			type = GR_NH_T_SR6_OUTPUT;
		} else {
			len += sizeof(*l3);
			type = GR_NH_T_L3;
		}
		break;
	case NEXTHOP_TYPE_BLACKHOLE:
		type = nh->bh_type == BLACKHOLE_REJECT ? GR_NH_T_REJECT : GR_NH_T_BLACKHOLE;
		break;
	default:
		gr_log_err("unsupported nexthop type %u", nh->type);
		goto out;
	}

	gr_log_debug("nh_id %u type %s", nh_id, gr_nh_type_name(type));

	req = calloc(1, len);
	if (req == NULL) {
		gr_log_err("calloc: %s", strerror(errno));
		goto out;
	}

	req->exist_ok = true;
	req->nh.nh_id = nh_id;
	req->nh.origin = origin;
	req->nh.type = type;
	req->nh.vrf_id = vrf_frr_to_grout(nh->vrf_id);
	req->nh.iface_id = ifindex_frr_to_grout(nh->ifindex);

	switch (type) {
	case GR_NH_T_L3:
		// For L3 nexthops in VRFs with an L3VNI, redirect the iface from
		// the VRF (SVI in FRR's model) to the VXLAN interface. Grout
		// routes packets directly through the VXLAN tunnel.
		vxlan_iface_id = l3vni_get_vxlan(req->nh.vrf_id);

		switch (nh->type) {
		case NEXTHOP_TYPE_IPV4:
		case NEXTHOP_TYPE_IPV4_IFINDEX:
		case NEXTHOP_TYPE_IPV6:
		case NEXTHOP_TYPE_IPV6_IFINDEX: {
			struct l3_addr gate;

			l3 = (struct gr_nexthop_info_l3 *)req->nh.info;

			if (nh->type == NEXTHOP_TYPE_IPV4
			    || nh->type == NEXTHOP_TYPE_IPV4_IFINDEX) {
				vtep.ipa_type = IPADDR_V4;
				vtep.ipaddr_v4 = nh->gate.ipv4;
			} else {
				vtep.ipa_type = IPADDR_V6;
				vtep.ipaddr_v6 = nh->gate.ipv6;
			}

			// EVPN may advertise an IPv4 VTEP as an IPv4-mapped IPv6
			// address; ipaddr_to_l3_addr() canonicalizes it so that
			// vxlan_output builds IPv4 encapsulation.
			ipaddr_to_l3_addr(&gate, &vtep);
			l3->af = gate.af;
			if (gate.af == GR_AF_IP4)
				l3->ipv4 = gate.ipv4;
			else
				l3->ipv6 = gate.ipv6;

			// Apply cached RMAC from EVPN NEIGH install if available.
			rmac = l3vni_rmac_get(req->nh.vrf_id, &vtep);
			if (rmac != NULL) {
				memcpy(&l3->mac, rmac, sizeof(l3->mac));
				l3->flags |= GR_NH_F_REMOTE;
				// Only redirect to VXLAN for remote nexthops
				if (vxlan_iface_id != GR_IFACE_ID_UNDEF)
					req->nh.iface_id = vxlan_iface_id;
			}
			break;
		}
		case NEXTHOP_TYPE_IFINDEX:
			l3 = (struct gr_nexthop_info_l3 *)req->nh.info;
			l3->af = GR_AF_UNSPEC;
			break;
		default:
			break;
		}
		break;
	case GR_NH_T_SR6_LOCAL:
		sr6_local = (struct gr_nexthop_info_srv6_local *)req->nh.info;

		switch (nh->nh_srv6->seg6local_action) {
		case ZEBRA_SEG6_LOCAL_ACTION_END:
			sr6_local->behavior = SR_BEHAVIOR_END;
			sr6_local->out_vrf_id = GR_VRF_ID_UNDEF;
			break;
		case ZEBRA_SEG6_LOCAL_ACTION_END_T:
			sr6_local->behavior = SR_BEHAVIOR_END_T;
			sr6_local->out_vrf_id = vrf_frr_to_grout(nh->nh_srv6->seg6local_ctx.table);
			break;
		case ZEBRA_SEG6_LOCAL_ACTION_END_DT6:
			sr6_local->behavior = SR_BEHAVIOR_END_DT6;
			sr6_local->out_vrf_id = vrf_frr_to_grout(nh->nh_srv6->seg6local_ctx.table);
			break;
		case ZEBRA_SEG6_LOCAL_ACTION_END_DT4:
			sr6_local->behavior = SR_BEHAVIOR_END_DT4;
			sr6_local->out_vrf_id = vrf_frr_to_grout(nh->nh_srv6->seg6local_ctx.table);
			break;
		case ZEBRA_SEG6_LOCAL_ACTION_END_DT46:
			sr6_local->behavior = SR_BEHAVIOR_END_DT46;
			sr6_local->out_vrf_id = vrf_frr_to_grout(nh->nh_srv6->seg6local_ctx.table);
			break;
#if CURRENT_FRR_VERSION >= MAKE_FRRVERSION(10, 6, 0)
		case ZEBRA_SEG6_LOCAL_ACTION_END_X:
			sr6_local->behavior = SR_BEHAVIOR_END_X;
			sr6_local->out_vrf_id = GR_VRF_ID_UNDEF;
			req->nh.iface_id = ifindex_frr_to_grout(nh->nh_srv6->seg6local_ctx.ifindex);
			memcpy(&sr6_local->endx_addr,
			       &nh->nh_srv6->seg6local_ctx.nh6,
			       sizeof(sr6_local->endx_addr));
			break;
#endif
		default:
			gr_log_err(
				"unsupported seg6local action %u", nh->nh_srv6->seg6local_action
			);
			goto out;
		}

		uint32_t flv = nh->nh_srv6->seg6local_ctx.flv.flv_ops;

		if (CHECK_SRV6_FLV_OP(flv, ZEBRA_SEG6_LOCAL_FLV_OP_PSP))
			sr6_local->flags |= GR_SR_FL_FLAVOR_PSP;
		if (CHECK_SRV6_FLV_OP(flv, ZEBRA_SEG6_LOCAL_FLV_OP_USP))
			gr_log_debug("USP always enabled, ignoring flag");
		if (CHECK_SRV6_FLV_OP(flv, ZEBRA_SEG6_LOCAL_FLV_OP_USD))
			sr6_local->flags |= GR_SR_FL_FLAVOR_USD;
		if (CHECK_SRV6_FLV_OP(flv, ZEBRA_SEG6_LOCAL_FLV_OP_NEXT_CSID)) {
			sr6_local->flags |= GR_SR_FL_FLAVOR_NEXT_CSID;
			sr6_local->block_bits = nh->nh_srv6->seg6local_ctx.flv.lcblock_len;
			sr6_local->csid_bits = nh->nh_srv6->seg6local_ctx.flv.lcnode_func_len;
		}

		break;
	case GR_NH_T_SR6_OUTPUT:
		sr6 = (struct gr_nexthop_info_srv6 *)req->nh.info;

		switch (nh->nh_srv6->seg6_segs->encap_behavior) {
		case SRV6_HEADEND_BEHAVIOR_H_ENCAPS:
			sr6->encap_behavior = SR_H_ENCAPS;
			break;
		case SRV6_HEADEND_BEHAVIOR_H_ENCAPS_RED:
			sr6->encap_behavior = SR_H_ENCAPS_RED;
			break;
		default:
			gr_log_err(
				"unsupported encap behavior %s",
				srv6_headend_behavior2str(
					nh->nh_srv6->seg6_segs->encap_behavior, true
				)
			);
			goto out;
		}

		sr6->n_seglist = nh->nh_srv6->seg6_segs->num_segs;
		for (unsigned i = 0; i < nh->nh_srv6->seg6_segs->num_segs; i++)
			memcpy(&sr6->seglist[i],
			       &nh->nh_srv6->seg6_segs->seg[i],
			       sizeof(sr6->seglist[i]));
#if CURRENT_FRR_VERSION >= MAKE_FRRVERSION(10, 8, 0)
		memcpy(&sr6->encap_src,
		       &nh->nh_srv6->seg6_segs->encap_source,
		       sizeof(sr6->encap_src));
#endif

		break;
	case GR_NH_T_BLACKHOLE:
	case GR_NH_T_REJECT:
		req->nh.iface_id = GR_IFACE_ID_UNDEF;
		break;
	default:
		gr_log_err("unsupported nh type %s", gr_nh_type_name(type));
		goto out;
	}

	if (grout_client_send_recv(GR_NH_ADD, len, req, NULL) < 0)
		goto out;

	ret = ZEBRA_DPLANE_REQUEST_SUCCESS;
out:
	free(req);
	return ret;
}

enum zebra_dplane_result grout_add_del_nexthop(struct zebra_dplane_ctx *ctx) {
	gr_nh_origin_t origin = zebra2origin(dplane_ctx_get_nhe_type(ctx));
	uint32_t nh_id = dplane_ctx_get_nhe_id(ctx);

	gr_log_debug(
		"%s nh_id %u origin %s",
		dplane_ctx_get_op(ctx) == DPLANE_OP_NH_DELETE ? "del" : "add",
		nh_id,
		gr_nh_origin_name(origin)
	);

	if (!is_selfroute(origin)) {
		gr_log_debug("non-frr origin, skip");
		return ZEBRA_DPLANE_REQUEST_SUCCESS;
	}
	if (!nh_id) {
		// it's supported by grout, but not by the linux kernel
		gr_log_err("no nh_id, skip");
		return ZEBRA_DPLANE_REQUEST_FAILURE;
	}

	if (dplane_ctx_get_op(ctx) == DPLANE_OP_NH_DELETE)
		return grout_del_nexthop(nh_id);

	if (dplane_ctx_get_nhe_nh_grp_count(ctx))
		return grout_add_nexthop_group(ctx);

	return grout_add_nexthop(nh_id, origin, dplane_ctx_get_nhe_ng(ctx)->nexthop);
}

void grout_nexthop_group_add(struct gr_nexthop *gr_nh, bool startup) {
	const struct gr_nexthop_info_group *g = (const struct gr_nexthop_info_group *)gr_nh->info;
	struct nh_grp grp[g->n_members];
	int type;

	gr_log_debug("add group nh_id %u n_members %u", gr_nh->nh_id, g->n_members);

	for (uint32_t i = 0; i < g->n_members; i++) {
		grp[i].id = g->members[i].nh_id;
		grp[i].weight = g->members[i].weight;
	}

	type = origin2zebra(gr_nh->origin, AF_UNSPEC, true);
	zebra_nhg_kernel_find(
		gr_nh->nh_id,
		NULL,
		grp,
		g->n_members,
		vrf_grout_to_frr(gr_nh->vrf_id),
		AFI_UNSPEC,
		type,
		startup,
		NULL
	);
}

static void grout_neigh_notify(bool new, struct gr_nexthop *gr_nh) {
	const struct gr_nexthop_info_l3 *l3;
	static const struct ethaddr zero_mac = {};
	struct zebra_dplane_ctx *ctx;
	struct ethaddr mac;
	struct ipaddr ip;

	if (gr_nh->type != GR_NH_T_L3)
		return;

	l3 = (const struct gr_nexthop_info_l3 *)gr_nh->info;

	if (l3->af != GR_AF_IP4 && l3->af != GR_AF_IP6)
		return;
	if (l3->flags & (GR_NH_F_LOCAL | GR_NH_F_LINK | GR_NH_F_MCAST | GR_NH_F_REMOTE))
		return;
	if (new && memcmp(&l3->mac, &zero_mac, sizeof(zero_mac)) == 0)
		return;

	memset(&ip, 0, sizeof(ip));
	if (l3->af == GR_AF_IP4) {
		ip.ipa_type = IPADDR_V4;
		memcpy(&ip.ipaddr_v4, &l3->ipv4, sizeof(ip.ipaddr_v4));
	} else {
		ip.ipa_type = IPADDR_V6;
		memcpy(&ip.ipaddr_v6, &l3->ipv6, sizeof(ip.ipaddr_v6));
	}
	memcpy(&mac, &l3->mac, sizeof(mac));

	gr_log_debug(
		"%s neigh iface=%u %pIA %pEA", new ? "add" : "del", gr_nh->iface_id, &ip, &mac
	);

	ctx = dplane_ctx_alloc();
	dplane_ctx_set_op(ctx, new ? DPLANE_OP_NEIGH_IP_INSTALL : DPLANE_OP_NEIGH_IP_DELETE);
	dplane_ctx_set_ns_id(ctx, GROUT_NS);
	dplane_ctx_set_ifindex(ctx, ifindex_grout_to_frr(gr_nh->iface_id));
	dplane_ctx_neigh_set_ipaddr(ctx, &ip);
	dplane_ctx_neigh_set_mac(ctx, &mac);
	dplane_ctx_neigh_set_ndm_state(ctx, NUD_REACHABLE);
	dplane_ctx_neigh_set_ndm_family(ctx, l3->af == GR_AF_IP4 ? AF_INET : AF_INET6);
	dplane_ctx_neigh_set_l2_len(ctx, ETH_ALEN);
#if CURRENT_FRR_VERSION >= MAKE_FRRVERSION(10, 6, 0)
	dplane_ctx_neigh_set_is_own(ctx, false);
#else
	dplane_ctx_neigh_set_is_ext(ctx, false);
#endif
	dplane_ctx_neigh_set_is_router(ctx, false);
	dplane_ctx_neigh_set_dp_static(ctx, false);
	dplane_ctx_neigh_set_local_inactive(ctx, false);

	dplane_provider_enqueue_to_zebra(ctx);
}

void grout_nexthop_change(bool new, struct gr_nexthop *gr_nh, bool startup) {
	struct nexthop *nh = NULL;
	afi_t afi = AFI_UNSPEC;
	int family, type;

	gr_log_debug(
		"%s nh_id %u origin %s",
		new ? "add" : "del",
		gr_nh->nh_id,
		gr_nh_origin_name(gr_nh->origin)
	);

	grout_neigh_notify(new, gr_nh);

	if (gr_nh->origin == GR_NH_ORIGIN_LEARN) {
		gr_log_debug("dynamic neighbor learn, skip");
		return;
	}
	if (gr_nh->nh_id == 0) {
		// Nexthop IDs are optional in grout, but mandatory in FRR.
		gr_log_debug("nexthop without ID, skip");
		return;
	}

	if (!new) {
		zebra_nhg_kernel_del(gr_nh->nh_id, vrf_grout_to_frr(gr_nh->vrf_id));
		return;
	}

	nh = nexthop_new();

	if (grout_gr_nexthop_to_frr_nexthop(gr_nh, nh, &family) < 0) {
		nexthop_free(nh);
		return;
	}

	// kernel set INET4 when no gateway, let's do the same
	if (family == AF_UNSPEC)
		family = AF_INET;

	afi = family2afi(family);
	type = origin2zebra(gr_nh->origin, family, true);
	SET_FLAG(nh->flags, NEXTHOP_FLAG_ACTIVE);

	zebra_nhg_kernel_find(
		gr_nh->nh_id, nh, NULL, 0, vrf_grout_to_frr(gr_nh->vrf_id), afi, type, startup, NULL
	);

	// zebra_nhg_kernel_find() makes a *shallow* copy of the allocated nexthop.
	// nexthop_free() must *NOT* be used to preserve the nh_srv6 context.
	free(nh);
}

void grout_macfdb_change(const struct gr_fdb_entry *fdb, bool new) {
	struct zebra_dplane_ctx *ctx;
	struct ethaddr mac;
	struct ipaddr vtep;

	l3_addr_to_ipaddr(&vtep, &fdb->vtep);

	gr_log_debug(
		"%s bridge=%u iface=%u mac=%pEA vlan=%u vtep=%pIA",
		new ? "add" : "del",
		fdb->bridge_id,
		fdb->iface_id,
		&fdb->mac,
		fdb->vlan_id,
		&vtep
	);

#if CURRENT_FRR_VERSION < MAKE_FRRVERSION(10, 6, 0)
	if (vtep.ipa_type == IPADDR_V6) {
		gr_log_warn("ipv6 vtep addresses are not supported in frr %s, skip", FRR_VER_SHORT);
		return;
	}
#endif

	ctx = dplane_ctx_alloc();
	memcpy(&mac, &fdb->mac, sizeof(mac));

	// Zebra's dplane API is asymmetric for FDB entries:
	//
	// - DPLANE_OP_MAC_INSTALL/DELETE is the downward path (zebra pushing
	//   MACs to dplane providers). The result handler is a no-op.
	// - DPLANE_OP_NEIGH_INSTALL/DELETE is the upward path (dplane providers
	//   notifying zebra of learned MACs). This goes through
	//   zebra_neigh_macfdb_update() which triggers EVPN type-2 routes.
	//
	// It is NOT a bug to use dplane_ctx_mac_set_*() with DPLANE_OP_NEIGH_*
	// ops. The macinfo and neigh fields are separate union members in the
	// dplane context, and zebra's own netlink provider does the same thing
	// (see rt_netlink.c netlink_macfdb_change()).
	dplane_ctx_set_ns_id(ctx, GROUT_NS);
	dplane_ctx_set_ifindex(ctx, ifindex_grout_to_frr(fdb->iface_id));
	dplane_ctx_mac_set_addr(ctx, &mac);
	dplane_ctx_mac_set_nhg_id(ctx, 0);
	dplane_ctx_mac_set_ndm_state(ctx, NUD_REACHABLE);
	dplane_ctx_mac_set_ndm_flags(ctx, NTF_MASTER);
	dplane_ctx_mac_set_dst_present(ctx, fdb->vtep.af != GR_AF_UNSPEC);
#if CURRENT_FRR_VERSION >= MAKE_FRRVERSION(10, 6, 0)
	dplane_ctx_mac_set_vtep_ip(ctx, &vtep);
#else
	dplane_ctx_mac_set_vtep_ip(ctx, &(struct in_addr) {fdb->vtep.ipv4});
#endif
	dplane_ctx_mac_set_vid(ctx, fdb->vlan_id);
	dplane_ctx_mac_set_dp_static(ctx, fdb->flags & GR_FDB_F_STATIC);
	dplane_ctx_mac_set_local_inactive(ctx, false);
	dplane_ctx_mac_set_is_sticky(ctx, false);
	dplane_ctx_set_op(ctx, new ? DPLANE_OP_NEIGH_INSTALL : DPLANE_OP_NEIGH_DELETE);

	dplane_provider_enqueue_to_zebra(ctx);
}

enum zebra_dplane_result grout_macfdb_update_ctx(struct zebra_dplane_ctx *ctx) {
	bool add = dplane_ctx_get_op(ctx) == DPLANE_OP_MAC_INSTALL;
	uint32_t req_type;
	size_t len;
	void *req;
	int ret;

	gr_log_debug(
#if CURRENT_FRR_VERSION >= MAKE_FRRVERSION(10, 6, 0)
		"%s bridge=%u iface=%u mac=%pEA vlan=%u vtep=%pIA",
#else
		"%s bridge=%u iface=%u mac=%pEA vlan=%u vtep=%pI4",
#endif
		add ? "add" : "del",
		ifindex_frr_to_grout(dplane_ctx_get_ifindex(ctx)),
		ifindex_frr_to_grout(dplane_ctx_get_ifindex(ctx)),
		dplane_ctx_mac_get_addr(ctx),
		dplane_ctx_mac_get_vlan(ctx),
		dplane_ctx_mac_get_vtep_ip(ctx)
	);

	len = add ? sizeof(struct gr_fdb_add_req) : sizeof(struct gr_fdb_del_req);
	req = calloc(1, len);
	if (req == NULL) {
		gr_log_err("failed to allocate memory");
		return ZEBRA_DPLANE_REQUEST_FAILURE;
	}

	if (add) {
		struct gr_fdb_add_req *add = req;
		add->exist_ok = true;
		add->fdb.iface_id = ifindex_frr_to_grout(dplane_ctx_get_ifindex(ctx));
		add->fdb.bridge_id = ifindex_frr_to_grout(dplane_ctx_mac_get_br_ifindex(ctx));
		add->fdb.vlan_id = dplane_ctx_mac_get_vlan(ctx);
		add->fdb.flags = GR_FDB_F_EXTERN;
		if (dplane_ctx_mac_get_dp_static(ctx))
			add->fdb.flags |= GR_FDB_F_STATIC;
		memcpy(&add->fdb.mac, dplane_ctx_mac_get_addr(ctx), sizeof(add->fdb.mac));
#if CURRENT_FRR_VERSION >= MAKE_FRRVERSION(10, 6, 0)
		ipaddr_to_l3_addr(&add->fdb.vtep, dplane_ctx_mac_get_vtep_ip(ctx));
#else
		add->fdb.vtep.af = GR_AF_IP4;
		add->fdb.vtep.ipv4 = dplane_ctx_mac_get_vtep_ip(ctx)->s_addr;
#endif
		req_type = GR_FDB_ADD;
	} else {
		struct gr_fdb_del_req *del = req;
		del->missing_ok = true;
		del->bridge_id = ifindex_frr_to_grout(dplane_ctx_mac_get_br_ifindex(ctx));
		del->vlan_id = dplane_ctx_mac_get_vlan(ctx);
		memcpy(&del->mac, dplane_ctx_mac_get_addr(ctx), sizeof(del->mac));
		req_type = GR_FDB_DEL;
	}

	ret = grout_client_send_recv(req_type, len, req, NULL);

	free(req);

	return ret == 0 ? ZEBRA_DPLANE_REQUEST_SUCCESS : ZEBRA_DPLANE_REQUEST_FAILURE;
}

static void neigh_install_nexthop(struct zebra_dplane_ctx *ctx, const struct ipaddr *addr) {
	uint16_t iface_id = ifindex_frr_to_grout(dplane_ctx_get_ifindex(ctx));
	const struct ethaddr *mac = dplane_ctx_neigh_get_mac(ctx);
	struct gr_nexthop_info_l3 *l3;
	struct gr_nh_add_req *req;
	size_t len;

	if (iface_id == GR_IFACE_ID_UNDEF)
		return;

	len = sizeof(*req) + sizeof(*l3);
	req = calloc(1, len);
	if (req == NULL) {
		gr_log_err("calloc: %s", strerror(errno));
		return;
	}

	req->exist_ok = true;
	req->nh.type = GR_NH_T_L3;
	req->nh.origin = zebra2origin(dplane_ctx_get_type(ctx));
	req->nh.iface_id = iface_id;
	l3 = (struct gr_nexthop_info_l3 *)req->nh.info;
	l3->flags = GR_NH_F_REMOTE | GR_NH_F_NEIGH;

	switch (addr->ipa_type) {
	case IPADDR_V4:
		l3->af = GR_AF_IP4;
		memcpy(&l3->ipv4, &addr->ipaddr_v4, sizeof(l3->ipv4));
		break;
	case IPADDR_V6:
		l3->af = GR_AF_IP6;
		memcpy(&l3->ipv6, &addr->ipaddr_v6, sizeof(l3->ipv6));
		break;
	default:
		free(req);
		return;
	}
	memcpy(&l3->mac, mac, sizeof(l3->mac));

	gr_log_debug("install remote nh iface=%u %pIA %pEA", iface_id, addr, mac);
	grout_client_send_recv(GR_NH_ADD, len, req, NULL);
	free(req);
}

static void neigh_delete_nexthop(struct zebra_dplane_ctx *ctx, const struct ipaddr *addr) {
	uint16_t iface_id = ifindex_frr_to_grout(dplane_ctx_get_ifindex(ctx));
	struct gr_nexthop_info_l3 *l3;
	struct gr_nh_del_req *req;
	size_t len;

	if (iface_id == GR_IFACE_ID_UNDEF)
		return;

	len = sizeof(*req) + sizeof(*l3);
	req = calloc(1, len);
	if (req == NULL) {
		gr_log_err("calloc: %s", strerror(errno));
		return;
	}

	req->missing_ok = true;
	req->nh.type = GR_NH_T_L3;
	req->nh.iface_id = iface_id;
	l3 = (struct gr_nexthop_info_l3 *)req->nh.info;

	switch (addr->ipa_type) {
	case IPADDR_V4:
		l3->af = GR_AF_IP4;
		memcpy(&l3->ipv4, &addr->ipaddr_v4, sizeof(l3->ipv4));
		break;
	case IPADDR_V6:
		l3->af = GR_AF_IP6;
		memcpy(&l3->ipv6, &addr->ipaddr_v6, sizeof(l3->ipv6));
		break;
	default:
		free(req);
		return;
	}

	gr_log_debug("delete remote nh iface=%u %pIA", iface_id, addr);
	grout_client_send_recv(GR_NH_DEL, len, req, NULL);
	free(req);
}

enum zebra_dplane_result grout_neigh_update_ctx(struct zebra_dplane_ctx *ctx) {
	const struct ipaddr *addr = dplane_ctx_neigh_get_ipaddr(ctx);
	bool add = dplane_ctx_get_op(ctx) != DPLANE_OP_NEIGH_DELETE;
	uint16_t vrf_id = vrf_frr_to_grout(dplane_ctx_get_vrf(ctx));

#if CURRENT_FRR_VERSION < MAKE_FRRVERSION(10, 6, 0)
	if (addr->ipa_type != IPADDR_V4) {
		gr_log_debug("only IPv4 VTEP addresses supported, skip");
		return ZEBRA_DPLANE_REQUEST_SUCCESS;
	}
#endif

	// Cache the RMAC for later use by grout_add_nexthop. We cannot
	// create a separate nexthop here because grout's L3 nexthop hash
	// keys on (vrf, addr) without iface_id, so it would collide with
	// the route nexthop that FRR installs right after.
	if (add) {
		const struct ethaddr *mac = dplane_ctx_neigh_get_mac(ctx);
		gr_log_debug("cache rmac vrf=%u %pIA %pEA", vrf_id, addr, mac);
		l3vni_rmac_set(vrf_id, addr, mac);
		// Install a remote nexthop for ARP/ND suppression.
		neigh_install_nexthop(ctx, addr);
	} else {
		gr_log_debug("uncache rmac vrf=%u %pIA", vrf_id, addr);
		l3vni_rmac_del(vrf_id, addr);
		neigh_delete_nexthop(ctx, addr);
	}

	return ZEBRA_DPLANE_REQUEST_SUCCESS;
}

enum zebra_dplane_result grout_vxlan_flood_update_ctx(struct zebra_dplane_ctx *ctx) {
	const struct ipaddr *addr = dplane_ctx_neigh_get_ipaddr(ctx);
	bool add = dplane_ctx_get_op(ctx) == DPLANE_OP_VTEP_ADD;
	struct gr_flood_entry *entry;
	uint32_t req_type;
	size_t len;
	void *req;
	int ret;

	gr_log_debug(
		"%s %pIA vni=%u vrf=%u",
		add ? "add" : "del",
		addr,
		dplane_ctx_neigh_get_vni(ctx),
		vrf_frr_to_grout(dplane_ctx_get_vrf(ctx))
	);

	len = add ? sizeof(struct gr_flood_add_req) : sizeof(struct gr_flood_del_req);

	req = calloc(1, len);
	if (req == NULL) {
		gr_log_err("failed to allocate memory");
		return ZEBRA_DPLANE_REQUEST_FAILURE;
	}

	if (add) {
		struct gr_flood_add_req *a = req;
		entry = &a->entry;
		a->exist_ok = true;
		req_type = GR_FLOOD_ADD;
	} else {
		struct gr_flood_del_req *d = req;
		entry = &d->entry;
		d->missing_ok = true;
		req_type = GR_FLOOD_DEL;
	}

	entry->type = GR_FLOOD_T_VTEP;
	entry->vrf_id = vrf_frr_to_grout(dplane_ctx_get_vrf(ctx));
	entry->vtep.vni = dplane_ctx_neigh_get_vni(ctx);
	ipaddr_to_l3_addr(&entry->vtep.addr, addr);

	ret = grout_client_send_recv(req_type, len, req, NULL);

	free(req);

	return ret == 0 ? ZEBRA_DPLANE_REQUEST_SUCCESS : ZEBRA_DPLANE_REQUEST_FAILURE;
}
