/* SPDX-License-Identifier: BSD-3-Clause */

#include <srx/common.h>
#include <srx/lyx.h>
#include <srx/srx_val.h>

#include "core.h"

#define XPATH_BASE_ "/ietf-routing:routing/control-plane-protocols/control-plane-protocol"
#define XPATH_OSPF_ XPATH_BASE_ "/ietf-ospf:ospf"
#define NETD_CONF "/etc/net.d/confd.conf"
#define NETD_CONF_NEXT NETD_CONF "+"
#define NETD_CONF_PREV NETD_CONF "-"
#define OSPFD_CONF "/etc/frr/ospfd.conf"
#define OSPFD_CONF_NEXT OSPFD_CONF "+"
#define OSPFD_CONF_PREV OSPFD_CONF "-"
#define RIPD_SIGNAL "/run/ripd_enabled"
#define RIPD_SIGNAL_NEXT RIPD_SIGNAL "+"
#define BFDD_SIGNAL "/run/bfd_enabled" /* Just signal that bfd should be enabled*/
#define BFDD_SIGNAL_NEXT BFDD_SIGNAL "+"
#define FRR_DAEMONS "/etc/frr/daemons"

#define FRR_STATIC_CONFIG "! Generated by Infix confd\n\
frr defaults traditional\n\
hostname Router\n\
password zebra \n\
enable password zebra\n\
no log unique-id\n\
log syslog warnings\n\
log facility local2\n"

int parse_rip(sr_session_ctx_t *session, struct lyd_node *rip, FILE *fp)
{
	struct lyd_node *interfaces, *timers, *default_route, *interface, *tmp;
	const char *default_metric, *distance;
	int num_interfaces = 0;

	/* Generate libconfuse format for RIP */
	fputs("\nrip {\n", fp);
	fputs("\tenabled = true\n", fp);

	/* Global RIP parameters */
	default_metric = lydx_get_cattr(rip, "default-metric");
	if (default_metric)
		fprintf(fp, "\tdefault-metric = %s\n", default_metric);

	distance = lydx_get_cattr(rip, "distance");
	if (distance)
		fprintf(fp, "\tdistance = %s\n", distance);

	/* Timers */
	timers = lydx_get_child(rip, "timers");
	if (timers) {
		const char *update, *invalid, *holddown, *flush;

		update = lydx_get_cattr(timers, "update-interval");
		invalid = lydx_get_cattr(timers, "invalid-interval");
		holddown = lydx_get_cattr(timers, "holddown-interval");
		flush = lydx_get_cattr(timers, "flush-interval");

		DEBUG("Ignoring 'holddown interval %s' for now", holddown);
		if (update || invalid || flush) {
			fputs("\ttimers {\n", fp);
			fprintf(fp, "\t\tupdate = %s\n", update ? update : "30");
			fprintf(fp, "\t\tinvalid = %s\n", invalid ? invalid : "180");
			fprintf(fp, "\t\tflush = %s\n", flush ? flush : "240");
			fputs("\t}\n", fp);
		}
	}

	/* Default route origination */
	default_route = lydx_get_child(rip, "originate-default-route");
	if (default_route && lydx_get_bool(default_route, "enabled"))
		fputs("\tdefault-route = true\n", fp);

	/* Debug options - use system commands since FRR doesn't support via northbound */
	struct lyd_node *debug = lydx_get_child(rip, "debug");
	if (debug) {
		if (lydx_get_bool(debug, "events"))
			fputs("\tsystem = \"vtysh -c 'debug rip events'\"\n", fp);
		if (lydx_get_bool(debug, "packet"))
			fputs("\tsystem = \"vtysh -c 'debug rip packet'\"\n", fp);
		if (lydx_get_bool(debug, "kernel"))
			fputs("\tsystem = \"vtysh -c 'debug rip zebra'\"\n", fp);
	}

	/* Networks (interfaces) - output as list */
	interfaces = lydx_get_child(rip, "interfaces");
	if (interfaces) {
		int first = 1;
		fputs("\tnetwork = { ", fp);
		LY_LIST_FOR(lyd_child(interfaces), interface) {
			const char *name = lydx_get_cattr(interface, "interface");
			if (name) {
				if (!first)
					fputs(", ", fp);
				fprintf(fp, "\"%s\"", name);
				first = 0;
				num_interfaces++;
			}
		}
		fputs(" }\n", fp);

		/* Passive interfaces - output as list */
		first = 1;
		int has_passive = 0;
		LY_LIST_FOR(lyd_child(interfaces), interface) {
			if (lydx_get_bool(interface, "passive")) {
				if (!has_passive) {
					fputs("\tpassive = { ", fp);
					has_passive = 1;
				}
				if (!first)
					fputs(", ", fp);
				fprintf(fp, "\"%s\"", lydx_get_cattr(interface, "interface"));
				first = 0;
			}
		}
		if (has_passive)
			fputs(" }\n", fp);

		/* Neighbors */
		LY_LIST_FOR(lyd_child(interfaces), interface) {
			struct lyd_node *neighbors_node = lydx_get_child(interface, "neighbors");
			if (neighbors_node) {
				LY_LIST_FOR(lyd_child(neighbors_node), tmp) {
					const char *address = lydx_get_cattr(tmp, "address");
					if (address)
						fprintf(fp, "\tneighbor = \"%s\"\n", address);
				}
			}
		}
	}

	/* Redistribution - output as list */
	tmp = lydx_get_child(rip, "redistribute");
	if (tmp && lyd_child(tmp)) {
		int first = 1;
		fputs("\tredistribute = { ", fp);
		LY_LIST_FOR(lyd_child(tmp), tmp) {
			const char *protocol = lydx_get_cattr(tmp, "protocol");
			if (protocol) {
				if (!first)
					fputs(", ", fp);
				fprintf(fp, "\"%s\"", protocol);
				first = 0;
			}
		}
		fputs(" }\n", fp);
	}

	fputs("}\n", fp);

	return num_interfaces;
}

static const char *ospf_network_type(const char *yang_type)
{
	if (!strcmp(yang_type, "hybrid"))
		return "point-to-multipoint";
	if (!strcmp(yang_type, "point-to-multipoint"))
		return "point-to-multipoint non-broadcast";

	/* broadcast, non-broadcast, point-to-point pass through unchanged */
	return yang_type;
}

int parse_ospf_interfaces(sr_session_ctx_t *session, struct lyd_node *areas, FILE *fp)
{
	struct lyd_node *interface, *interfaces, *area;
	int num_bfd_enabled = 0;

	LY_LIST_FOR(lyd_child(areas), area) {
		const char *area_id;

		interfaces = lydx_get_child(area, "interfaces");
		area_id = lydx_get_cattr(area, "area-id");

		LY_LIST_FOR(lyd_child(interfaces), interface) {
			const char *hello, *dead, *retransmit, *transmit, *interface_type, *cost, *priority;

			if (lydx_get_bool(interface, "enabled")) {
				int passive = 0, bfd_enabled = 0;
				struct lyd_node *bfd;

				bfd = lydx_get_child(interface, "bfd");
				bfd_enabled = lydx_get_bool(bfd, "enabled");
				num_bfd_enabled += bfd_enabled;

				passive = lydx_get_bool(interface, "passive");
				fprintf(fp, "interface %s\n", lydx_get_cattr(interface, "name"));

				hello = lydx_get_cattr(interface, "hello-interval");
				dead = lydx_get_cattr(interface, "dead-interval");
				retransmit = lydx_get_cattr(interface, "retransmit-interval");
				transmit = lydx_get_cattr(interface, "transmit-delay");
				interface_type = lydx_get_cattr(interface, "interface-type");
				cost = lydx_get_cattr(interface, "cost");
				priority = lydx_get_cattr(interface, "priority");

				fprintf(fp, "  ip ospf area %s\n", area_id);
				if (dead)
					fprintf(fp, "  ip ospf dead-interval %s\n", dead);
				if (hello)
					fprintf(fp, "  ip ospf hello-interval %s\n", hello);
				if (retransmit)
					fprintf(fp, "  ip ospf retransmit-interval %s\n", retransmit);
				if (transmit)
					fprintf(fp, "  ip ospf transmit-delay %s\n", transmit);
				if (priority)
					fprintf(fp, "  ip ospf priority %s\n", priority);
				if (bfd_enabled)
					fputs("  ip ospf bfd\n", fp);
				if (passive)
					fputs("  ip ospf passive\n", fp);
				if (interface_type)
					fprintf(fp, "  ip ospf network %s\n", ospf_network_type(interface_type));
				if (cost)
					fprintf(fp, "  ip ospf cost %s\n", cost);
			}
		}
	}

	return num_bfd_enabled;
}

int parse_ospf_redistribute(sr_session_ctx_t *session, struct lyd_node *redistributes, FILE *fp)
{
	struct lyd_node *tmp;

	LY_LIST_FOR(lyd_child(redistributes), tmp) {
		const char *protocol = lydx_get_cattr(tmp, "protocol");

		fprintf(fp, "  redistribute %s\n", protocol);
	}

	return 0;
}

static void parse_ospf_static_neighbors(struct lyd_node *areas, FILE *fp)
{
	struct lyd_node *area, *interface, *interfaces, *neighbors, *neighbor;

	LY_LIST_FOR(lyd_child(areas), area) {
		interfaces = lydx_get_child(area, "interfaces");

		LY_LIST_FOR(lyd_child(interfaces), interface) {
			neighbors = lydx_get_child(interface, "static-neighbors");
			if (!neighbors)
				continue;

			LY_LIST_FOR(lyd_child(neighbors), neighbor) {
				const char *id = lydx_get_cattr(neighbor, "identifier");

				if (id)
					fprintf(fp, "  neighbor %s\n", id);
			}
		}
	}
}

int parse_ospf_areas(sr_session_ctx_t *session, struct lyd_node *areas, FILE *fp)
{
	int areas_configured = 0;
	struct lyd_node *area;

	LY_LIST_FOR(lyd_child(areas), area) {
		const char *area_id, *area_type, *default_cost;
		int summary;

		area_id = lydx_get_cattr(area, "area-id");
		area_type = lydx_get_cattr(area, "area-type");
		default_cost = lydx_get_cattr(area, "default-cost");
		summary = lydx_get_bool(area, "summary");

		if (area_type) {
			int stub_or_nssa = 0;

			if (!strcmp(area_type, "ietf-ospf:nssa-area")) {
				stub_or_nssa = 1;
				fprintf(fp, "  area %s nssa %s\n", area_id, !summary ? "no-summary" : "");
			} else if (!strcmp(area_type, "ietf-ospf:stub-area")) {
				stub_or_nssa = 1;
				fprintf(fp, "  area %s stub %s\n", area_id, !summary ? "no-summary" : "");
			}
			if (stub_or_nssa && default_cost)
				fprintf(fp, "  area %s default-cost %s\n", area_id, default_cost);
		}
		areas_configured++;
	}

	return areas_configured;
}

int parse_ospf(sr_session_ctx_t *session, struct lyd_node *ospf)
{
	struct lyd_node *areas, *default_route, *debug;
	const char *router_id;
	int bfd_enabled = 0;
	int num_areas = 0;
	FILE *fp;

	fp = fopen(OSPFD_CONF_NEXT, "w");
	if (!fp) {
		ERROR("Failed to open %s", OSPFD_CONF_NEXT);
		return SR_ERR_INTERNAL;
	}

	/* Handle OSPF debug configuration */
	debug = lydx_get_child(ospf, "debug");
	if (debug) {
		int any_debug = 0;

		if (lydx_get_bool(debug, "bfd")) {
			fputs("debug ospf bfd\n", fp);
			any_debug = 1;
		}
		if (lydx_get_bool(debug, "packet")) {
			fputs("debug ospf packet all detail\n", fp);
			any_debug = 1;
		}
		if (lydx_get_bool(debug, "ism")) {
			fputs("debug ospf ism\n", fp);
			any_debug = 1;
		}
		if (lydx_get_bool(debug, "nsm")) {
			fputs("debug ospf nsm\n", fp);
			any_debug = 1;
		}
		if (lydx_get_bool(debug, "default-information")) {
			fputs("debug ospf default-information\n", fp);
			any_debug = 1;
		}
		if (lydx_get_bool(debug, "nssa")) {
			fputs("debug ospf nssa\n", fp);
			any_debug = 1;
		}

		if (any_debug) {
			fputs("log syslog debugging\n", fp);
			fputs("!\n", fp);
		}
	}

	areas = lydx_get_child(ospf, "areas");
	router_id = lydx_get_cattr(ospf, "explicit-router-id");
	bfd_enabled = parse_ospf_interfaces(session, areas, fp);
	fputs("router ospf\n", fp);
	num_areas = parse_ospf_areas(session, areas, fp);
	parse_ospf_redistribute(session, lydx_get_child(ospf, "redistribute"), fp);
	parse_ospf_static_neighbors(areas, fp);
	default_route = lydx_get_child(ospf, "default-route-advertise");
	if (default_route) {
		/* enable is obsolete in favor for enabled. */
		if ((lydx_get_child(default_route, "enable") && lydx_get_bool(default_route, "enable"))
		    || lydx_get_bool(default_route, "enabled")) {
			fputs("  default-information originate", fp);
			if (lydx_get_bool(default_route, "always"))
				fputs(" always", fp);
			fputs("\n", fp);
		}
	}

	if (router_id)
		fprintf(fp, "  ospf router-id %s\n", router_id);
	fclose(fp);

	if (!num_areas) {
		(void)remove(OSPFD_CONF_NEXT);
		return 0;
	}

	if (bfd_enabled)
		(void)touch(BFDD_SIGNAL_NEXT);
	else
		(void)remove(BFDD_SIGNAL_NEXT);

	return 0;
}

static int parse_route(struct lyd_node *parent, FILE *fp, const char *ip)
{
	const char *outgoing_interface, *next_hop_address, *special_next_hop,
		*destination_prefix, *route_preference;
	struct lyd_node *next_hop;

	destination_prefix = lydx_get_cattr(parent, "destination-prefix");
	route_preference = lydx_get_cattr(parent, "route-preference");
	next_hop = lydx_get_child(parent, "next-hop");
	outgoing_interface = lydx_get_cattr(next_hop, "outgoing-interface");
	next_hop_address = lydx_get_cattr(next_hop, "next-hop-address");
	special_next_hop = lydx_get_cattr(next_hop, "special-next-hop");

	/* Generate libconfuse format: route { prefix = "..." nexthop = "..." distance = ... } */
	fputs("route {\n", fp);
	fprintf(fp, "\tprefix = \"%s\"\n", destination_prefix);

	/* Nexthop - there can only be one */
	if (outgoing_interface)
		fprintf(fp, "\tnexthop = \"%s\"\n", outgoing_interface);
	else if (next_hop_address)
		fprintf(fp, "\tnexthop = \"%s\"\n", next_hop_address);
	else if (strcmp(special_next_hop, "blackhole") == 0)
		fputs("\tnexthop = \"blackhole\"\n", fp);
	else if (strcmp(special_next_hop, "unreachable") == 0)
		fputs("\tnexthop = \"reject\"\n", fp);
	else if (strcmp(special_next_hop, "receive") == 0)
		fputs("\tnexthop = \"Null0\"\n", fp);

	if (route_preference)
		fprintf(fp, "\tdistance = %s\n", route_preference);

	fputs("}\n", fp);

	return SR_ERR_OK;
}

static int parse_static_routes(sr_session_ctx_t *session, struct lyd_node *parent, FILE *fp)
{
	struct lyd_node *ipv4, *v4routes, *ipv6, *v6routes, *route;
	int num_routes = 0;

	ipv4 = lydx_get_child(parent, "ipv4");
	ipv6 = lydx_get_child(parent, "ipv6");

	v4routes = lydx_get_child(ipv4, "route");
	LY_LIST_FOR(v4routes, route) {
		parse_route(route, fp, "ip");
		num_routes++;
	}

	v6routes = lydx_get_child(ipv6, "route");
	LY_LIST_FOR(v6routes, route) {
		parse_route(route, fp, "ipv6");
		num_routes++;
	}

	DEBUG("Found %d routes in configuration", num_routes);
	return num_routes;
}

/*
 * Generate the complete /etc/frr/daemons file.  Written atomically as a
 * single unit so the file is always consistent and easy to read.
 */
static void frr_daemons_write(int ospfd, int ripd, int bfdd)
{
	const char *next = FRR_DAEMONS "+";
	FILE *fp;

	fp = fopen(next, "w");
	if (!fp) {
		ERROR("Failed to open %s", next);
		return;
	}

	/* Daemon selection - watchfrr, zebra, mgmtd, and staticd are
	 * always started by frrinit.sh regardless of these settings. */
	fprintf(fp,
		"# Generated by Infix confd\n"
		"ospfd=%s\n"
		"ripd=%s\n"
		"bfdd=%s\n"
		"bgpd=no\n"
		"ospf6d=no\n"
		"ripngd=no\n"
		"isisd=no\n"
		"pimd=no\n"
		"pim6d=no\n"
		"ldpd=no\n"
		"eigrpd=no\n"
		"babeld=no\n"
		"vrrpd=no\n"
		"pathd=no\n"
		"\n",
		ospfd ? "yes" : "no",
		ripd  ? "yes" : "no",
		bfdd  ? "yes" : "no");

	/* Global settings and per-daemon options */
	fputs(
		"vtysh_enable=yes\n"
		"watchfrr_options=\"-l 4\"\n"
		"zebra_options=\"  -A 127.0.0.1 -s 90000000\"\n"
		"mgmtd_options=\"  -A 127.0.0.1\"\n"
		"ospfd_options=\"  -A 127.0.0.1\"\n"
		"ripd_options=\"   -A 127.0.0.1\"\n"
		"staticd_options=\"-A 127.0.0.1\"\n"
		"bfdd_options=\"   -A 127.0.0.1\"\n"
		"\n"
		"frr_profile=\"traditional\"\n",
		fp);

	fclose(fp);
	if (rename(next, FRR_DAEMONS))
		ERROR("Failed to rename %s to %s: %m", next, FRR_DAEMONS);
}

int routing_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd)
{
	int netd_enabled = 0, ospfd_enabled = 0, bfdd_enabled = 0, ripd_enabled = 0;
	struct lyd_node *cplane, *cplanes;
	int rc = SR_ERR_OK;
	FILE *fp;

	if (diff && !lydx_get_xpathf(diff, "/ietf-routing:routing"))
		return SR_ERR_OK;

	switch (event) {
	case SR_EV_CHANGE: /* regular change (copy cand running) */
		break;

	case SR_EV_ENABLED: /* first time, on register - no SR_EV_DONE follows */
		break;

	case SR_EV_ABORT: /* User abort, or other plugin failed */
		(void)remove(NETD_CONF_NEXT);
		return SR_ERR_OK;

	case SR_EV_DONE:
		/* Check if passed validation in previous event */
		netd_enabled = fexist(NETD_CONF_NEXT);
		ospfd_enabled = fexist(OSPFD_CONF_NEXT);
		bfdd_enabled = fexist(BFDD_SIGNAL_NEXT);
		ripd_enabled = fexist(RIPD_SIGNAL_NEXT);
		goto activate;

	default:
		return SR_ERR_OK;
	}

	cplanes = lydx_get_descendant(config, "routing", "control-plane-protocols", "control-plane-protocol", NULL);

	/* Open netd config file for both static routes and RIP */
	fp = fopen(NETD_CONF_NEXT, "w");
	if (!fp) {
		ERROR("Failed to open %s", NETD_CONF_NEXT);
		return SR_ERR_INTERNAL;
	}
	fputs("# Generated by Infix confd\n", fp);

	LYX_LIST_FOR_EACH(cplanes, cplane, "control-plane-protocol") {
		const char *type;
		int num;

		type = lydx_get_cattr(cplane, "type");
		if (!strcmp(type, "infix-routing:static")) {
			num = parse_static_routes(session, lydx_get_child(cplane, "static-routes"), fp);
			if (num > 0)
				netd_enabled = 1;
		} else if (!strcmp(type, "infix-routing:ospfv2")) {
			parse_ospf(session, lydx_get_child(cplane, "ospf"));
		} else if (!strcmp(type, "infix-routing:ripv2")) {
			num = parse_rip(session, lydx_get_child(cplane, "rip"), fp);
			if (num > 0) {
				touch(RIPD_SIGNAL_NEXT);
				netd_enabled = 1;
			}
		}
	}

	fclose(fp);
	if (!netd_enabled)
		(void)remove(NETD_CONF_NEXT);

	if (event == SR_EV_CHANGE)
		return rc;

	/* For SR_EV_ENABLED we activate immediately (no SR_EV_DONE follows) */
	netd_enabled = fexist(NETD_CONF_NEXT);
	ospfd_enabled = fexist(OSPFD_CONF_NEXT);
	bfdd_enabled = fexist(BFDD_SIGNAL_NEXT);
	ripd_enabled = fexist(RIPD_SIGNAL_NEXT);

activate:
	/* Generate complete /etc/frr/daemons (for watchfrr/frrinit.sh) */
	frr_daemons_write(ospfd_enabled, ripd_enabled, bfdd_enabled);

	if (bfdd_enabled)
		(void)rename(BFDD_SIGNAL_NEXT, BFDD_SIGNAL);
	else
		(void)remove(BFDD_SIGNAL);

	if (ospfd_enabled) {
		(void)remove(OSPFD_CONF_PREV);
		(void)rename(OSPFD_CONF, OSPFD_CONF_PREV);
		(void)rename(OSPFD_CONF_NEXT, OSPFD_CONF);
	} else {
		(void)remove(OSPFD_CONF);
	}

	if (ripd_enabled)
		(void)rename(RIPD_SIGNAL_NEXT, RIPD_SIGNAL);
	else
		(void)remove(RIPD_SIGNAL);

	/* netd handles both static routes and RIP, assembles frr.conf */
	if (netd_enabled) {
		(void)remove(NETD_CONF_PREV);
		(void)rename(NETD_CONF, NETD_CONF_PREV);
		(void)rename(NETD_CONF_NEXT, NETD_CONF);
	} else {
		(void)remove(NETD_CONF);
	}

	/* Enable/disable FRR daemons as standalone finit services.
	 * Harmless no-op when using watchfrr (services not installed). */
	systemf("initctl -nbq %s ospfd", ospfd_enabled ? "enable" : "disable");
	if (ospfd_enabled)
		systemf("initctl -nbq touch ospfd");
	systemf("initctl -nbq %s ripd",  ripd_enabled  ? "enable" : "disable");
	systemf("initctl -nbq %s bfdd",  bfdd_enabled  ? "enable" : "disable");

	/*
	 * Signal netd to reload - it assembles /etc/frr/frr.conf and
	 * Finit propagates the restart to the frr sysv service
	 */
	if (systemf("initctl -bfq touch netd"))
		ERROR("Failed to signal netd for reload");

	return rc;
}
