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

/*
 * FRR frr.conf backend for netd - assembles a single /etc/frr/frr.conf
 * from static routes, RIP config, and any existing OSPF config snippet,
 * then signals FRR to reload via Finit.
 */

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

#include "netd.h"
#include "frrconf_backend.h"

#define FRR_CONF      "/etc/frr/frr.conf"
#define FRR_CONF_NEXT FRR_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 frrconf_backend_init(void)
{
	INFO("Using FRR frr.conf backend");
	return 0;
}

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

static void write_static_routes(FILE *fp, struct route_head *routes)
{
	char prefix_str[INET6_ADDRSTRLEN];
	char gw_str[INET6_ADDRSTRLEN];
	struct route *r;
	int count = 0;

	TAILQ_FOREACH(r, routes, entries) {
		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);
		count++;
	}

	if (count)
		fputs("!\n", fp);

	DEBUG("frrconf: wrote %d static routes", count);
}

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);
	DEBUG("frrconf: wrote RIP configuration");
}

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("frrconf: appended %s", path);
}

int frrconf_backend_apply(struct route_head *routes, struct rip_config *rip)
{
	FILE *fp;

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

	fputs(frr_header, fp);
	write_static_routes(fp, routes);
	write_rip_config(fp, rip);
	append_file(fp, OSPFD_CONF);

	fclose(fp);

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

	INFO("frrconf: updated %s", FRR_CONF);
	return 0;
}
