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

/*
 * FRR vtysh backend for netd - applies incremental route changes
 * by generating a diff (negations of old state + new state) and
 * feeding it to running FRR daemons via `vtysh -b`.
 *
 * Old state is persisted in NETD_CONF so we can recover after a
 * crash — on startup, any routes/config in that file are negated
 * before new config is applied.
 */

#include <errno.h>
#include <stdio.h>
#include <string.h>

#include "netd.h"
#include "vtysh_backend.h"

#define FRR_CONF      "/etc/frr/frr.conf"
#define FRR_CONF_NEXT FRR_CONF "+"
#define NETD_CONF     "/etc/frr/netd.conf"
#define NETD_CONF_NEXT NETD_CONF "+"
#define OSPFD_CONF    "/etc/frr/ospfd.conf"

static const char *frr_header =
	"! Generated by netd\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"
	"!\n";

int vtysh_backend_init(void)
{
	INFO("Using FRR vtysh backend");
	return 0;
}

void vtysh_backend_cleanup(void)
{
	/* Nothing to clean up */
}

static void write_route(FILE *fp, struct route *r)
{
	char prefix_str[INET6_ADDRSTRLEN];
	char gw_str[INET6_ADDRSTRLEN];
	const char *cmd;

	if (r->family == AF_INET) {
		cmd = "ip";
		inet_ntop(AF_INET, &r->prefix.ip4, prefix_str, sizeof(prefix_str));
	} else {
		cmd = "ipv6";
		inet_ntop(AF_INET6, &r->prefix.ip6, prefix_str, sizeof(prefix_str));
	}

	fprintf(fp, "%s route %s/%u ", cmd, prefix_str, r->prefixlen);

	switch (r->nh_type) {
	case NH_ADDR:
		if (r->family == AF_INET)
			inet_ntop(AF_INET, &r->gateway.gw4, gw_str, sizeof(gw_str));
		else
			inet_ntop(AF_INET6, &r->gateway.gw6, gw_str, sizeof(gw_str));
		fputs(gw_str, fp);
		break;
	case NH_IFNAME:
		fputs(r->ifname, fp);
		break;
	case NH_BLACKHOLE:
		switch (r->bh_type) {
		case BH_NULL:
			fputs("Null0", fp);
			break;
		case BH_REJECT:
			fputs("reject", fp);
			break;
		case BH_DROP:
			fputs("blackhole", fp);
			break;
		}
		break;
	}

	if (r->distance)
		fprintf(fp, " %u", r->distance);

	fputc('\n', fp);
}

static const char *redist_name(enum rip_redist_type type)
{
	switch (type) {
	case RIP_REDIST_CONNECTED: return "connected";
	case RIP_REDIST_STATIC:    return "static";
	case RIP_REDIST_KERNEL:    return "kernel";
	case RIP_REDIST_OSPF:      return "ospf";
	}
	return "unknown";
}

static void write_rip_config(FILE *fp, struct rip_config *rip)
{
	struct rip_redistribute *redist;
	struct rip_neighbor *nbr;
	struct rip_network *net;

	if (!rip->enabled)
		return;

	fputs("router rip\n", fp);
	fprintf(fp, " default-metric %u\n", rip->default_metric);
	fprintf(fp, " distance %u\n", rip->distance);
	fprintf(fp, " timers basic %u %u %u\n",
		rip->timers.update, rip->timers.invalid, rip->timers.flush);

	if (rip->default_route)
		fputs(" default-information originate\n", fp);

	TAILQ_FOREACH(net, &rip->networks, entries) {
		fprintf(fp, " network %s\n", net->ifname);
		if (net->passive)
			fprintf(fp, " passive-interface %s\n", net->ifname);
	}

	TAILQ_FOREACH(nbr, &rip->neighbors, entries) {
		char addr[INET_ADDRSTRLEN];

		inet_ntop(AF_INET, &nbr->addr, addr, sizeof(addr));
		fprintf(fp, " neighbor %s\n", addr);
	}

	TAILQ_FOREACH(redist, &rip->redistributes, entries)
		fprintf(fp, " redistribute %s\n", redist_name(redist->type));

	fputs("!\n", fp);
}

static void append_file(FILE *fp, const char *path)
{
	char buf[1024];
	FILE *src;
	size_t n;

	src = fopen(path, "r");
	if (!src)
		return;

	while ((n = fread(buf, 1, sizeof(buf), src)) > 0)
		fwrite(buf, 1, n, fp);

	fclose(src);
	DEBUG("vtysh: appended %s", path);
}

/*
 * Negate old state by reading previous netd.conf and prefixing
 * each config line with "no ".  Comment and blank lines are skipped.
 *
 * For "router rip" blocks we emit a single "no router rip" instead
 * of negating each sub-command individually.
 */
static void negate_old_conf(FILE *fp)
{
	char line[256];
	int in_rip = 0;
	int count = 0;
	FILE *old;

	old = fopen(NETD_CONF, "r");
	if (!old) {
		DEBUG("vtysh: no previous %s, first boot", NETD_CONF);
		return;
	}

	while (fgets(line, sizeof(line), old)) {
		size_t len = strlen(line);

		/* Strip trailing newline */
		if (len > 0 && line[len - 1] == '\n')
			line[--len] = '\0';

		/* Skip empty lines and comments */
		if (len == 0 || line[0] == '!' || line[0] == '#')
			continue;

		/* Track router rip block */
		if (strcmp(line, "router rip") == 0) {
			in_rip = 1;
			fputs("no router rip\n", fp);
			count++;
			continue;
		}

		/* Skip sub-commands inside router rip block */
		if (in_rip) {
			/* Sub-commands are indented with space */
			if (line[0] == ' ')
				continue;
			in_rip = 0;
		}

		/* Negate route lines (ip route ..., ipv6 route ...) */
		fprintf(fp, "no %s\n", line);
		count++;
	}

	fclose(old);
	DEBUG("vtysh: negated %d old config lines", count);
}

/*
 * Save current config to netd.conf for next reload or crash recovery.
 * Written atomically via rename.
 */
static int save_conf(struct route_head *routes, struct rip_config *rip)
{
	struct route *r;
	FILE *fp;

	fp = fopen(NETD_CONF_NEXT, "w");
	if (!fp) {
		ERROR("vtysh: failed to open %s: %s", NETD_CONF_NEXT, strerror(errno));
		return -1;
	}

	TAILQ_FOREACH(r, routes, entries)
		write_route(fp, r);

	write_rip_config(fp, rip);

	fclose(fp);

	if (rename(NETD_CONF_NEXT, NETD_CONF)) {
		ERROR("vtysh: failed to rename %s to %s: %s",
		      NETD_CONF_NEXT, NETD_CONF, strerror(errno));
		return -1;
	}

	return 0;
}

int vtysh_backend_apply(struct route_head *routes, struct rip_config *rip)
{
	struct route *r;
	int rc, count = 0;
	FILE *fp;

	fp = fopen(FRR_CONF_NEXT, "w");
	if (!fp) {
		ERROR("vtysh: failed to open %s: %s", FRR_CONF_NEXT, strerror(errno));
		return -1;
	}

	fputs(frr_header, fp);

	/* Negate old state from previous netd.conf */
	negate_old_conf(fp);

	/* Write new routes */
	TAILQ_FOREACH(r, routes, entries) {
		write_route(fp, r);
		count++;
	}
	DEBUG("vtysh: wrote %d new routes", count);

	/* Write new RIP config */
	write_rip_config(fp, rip);

	/* Append OSPF config if present (written by confd) */
	append_file(fp, OSPFD_CONF);

	fclose(fp);

	if (rename(FRR_CONF_NEXT, FRR_CONF)) {
		ERROR("vtysh: failed to rename %s to %s: %s",
		      FRR_CONF_NEXT, FRR_CONF, strerror(errno));
		return -1;
	}

	rc = system("vtysh -b");
	if (rc) {
		ERROR("vtysh: vtysh -b failed with status %d", rc);
		return -1;
	}

	/* Persist new state for next reload / crash recovery */
	if (save_conf(routes, rip))
		ERROR("vtysh: failed to save %s, next reload may be inconsistent", NETD_CONF);

	INFO("vtysh: applied config via vtysh -b");
	return 0;
}
