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

#include <assert.h>
#include <ctype.h>
#include <pwd.h>
#include <sys/utsname.h>
#include <sys/sysinfo.h>
#include <sys/types.h>

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

#include "core.h"
#include "dhcp-common.h"

#define  ARPING_MSEC    1000
#define  MODULE         "infix-dhcp-client"
#define  XPATH          "/ietf-interfaces:interfaces/interface/ietf-ip:ipv4/infix-dhcp-client:dhcp"
#define  CACHE_TEMPLATE "/var/lib/misc/%s.cache"

static char *ip_cache(const char *ifname, char *str, size_t len)
{

	struct in_addr ina;
	char buf[128];
	FILE *fp;

	if (!fexistf(CACHE_TEMPLATE, ifname))
		return NULL;

	fp = fopenf("r", CACHE_TEMPLATE, ifname);
	if (!fp)
		return NULL;

	if (!fgets(buf, sizeof(buf), fp)) {
		fclose(fp);
		return NULL;
	}
	fclose(fp);
	chomp(buf);

	if (!inet_aton(buf, &ina)) {
		erasef(CACHE_TEMPLATE, ifname);
		return NULL;
	}

	snprintf(str, len, "-r %.15s ", buf);

	return str;
}

static const char *get_vendor_class(struct lyd_node *cfg)
{
	struct lyd_node *option;

	LYX_LIST_FOR_EACH(lyd_child(cfg), option, "option") {
		struct lyd_node *id = lydx_get_child(option, "id");
		const char *name = lyd_get_value(id);

		if (strcmp(name, "vendor-class") == 0 || strcmp(name, "60") == 0)
			return lydx_get_cattr(option, "value");
	}

	return NULL;
}

static char *fallback_options(const char *ifname)
{
	const char *defaults = "-O subnet -O broadcast -O router -O domain -O search "
		"-O dns -O ntpsrv -O staticroutes -O msstaticroutes ";
	char address[32] = { 0 };
	char *options;

	ip_cache(ifname, address, sizeof(address));
	asprintf(&options, "%s %s", defaults, address);

	return options;
}

static char *dhcp_options(const char *ifname, struct lyd_node *cfg)
{
	struct lyd_node *option;
	char *options = NULL;

	LYX_LIST_FOR_EACH(lyd_child(cfg), option, "option") {
		struct lyd_node *id = lydx_get_child(option, "id");
		const char *val = lydx_get_cattr(option, "value");
		const char *hex = lydx_get_cattr(option, "hex");

		options = dhcp_compose_options(cfg, ifname, &options, id, val, hex, ip_cache);
	}

	return options ?: fallback_options(ifname);
}

static void add(const char *ifname, struct lyd_node *cfg)
{
	const char *metric = lydx_get_cattr(cfg, "route-preference");
	const char *client_id = lydx_get_cattr(cfg, "client-id");
	char *cid = NULL, *options = NULL;
	const char *action = "disable";
	const char *vendor_class;
	char vendor[128] = { 0 };
	char do_arp[20] = { 0 };
	bool arping;
	FILE *fp;

	arping = lydx_is_enabled(cfg, "arping");
	if (arping)
		snprintf(do_arp, sizeof(do_arp), "-a%d", ARPING_MSEC);

	if (client_id && client_id[0]) {
		size_t len = 3 * strlen(client_id) + 16;

		cid = malloc(len);
		if (!cid)
			goto generr;

		strlcpy(cid, "-C -x 61:00", len);
		for (size_t i = 0; client_id[i]; i++) {
			char hex[5];

			snprintf(hex, sizeof(hex), ":%02x", client_id[i]);
			strlcat(cid, hex, len);
		}
	}

	options = dhcp_options(ifname, cfg);

	/* Check for vendor-class option (option 60) */
	vendor_class = get_vendor_class(cfg);
	if (vendor_class)
		snprintf(vendor, sizeof(vendor), "-V \"%s\"", vendor_class);
	else
		snprintf(vendor, sizeof(vendor), "-V \"\"");

	fp = fopenf("w", "/etc/finit.d/available/dhcp-client-%s.conf", ifname);
	if (!fp) {
	generr:
		ERRNO("failed creating DHCP client %s: %s", ifname, strerror(errno));
		goto err;
	}

	fprintf(fp, "# Generated by Infix confd\n");
	fprintf(fp, "metric=%s\n", metric);
	fprintf(fp, "service <!pid/netd> name:dhcp-client :%s <net/%s/running> \\\n"
		"	[2345] udhcpc -f -p /run/dhcp-client-%s.pid -t 3 -T 5 -A 30 %s -S -R \\\n"
		"		%s%s \\\n"
		"		-i %s %s %s \\\n"
		"		-- DHCP client @%s\n",
		ifname, ifname, ifname, do_arp,
		options ? "-o " : "", options,
		ifname, cid ?: "", vendor, ifname);
	fclose(fp);
	action = "enable";
err:
	systemf("initctl -bfqn %s dhcp-client-%s", action, ifname);
	if (options)
		free(options);
	if (cid)
		free(cid);
}

static void del(const char *ifname)
{
	systemf("initctl -bfq delete dhcp-client-%s", ifname);
}

int dhcp_client_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff,
			     sr_event_t event, struct confd *confd)
{
	struct lyd_node *ifaces, *difaces, *iface, *diface, *ipv4, *dhcp, *ddhcp;
	sr_error_t err = 0;

	if (event != SR_EV_DONE)
		return SR_ERR_OK;

	ifaces  = lydx_get_descendant(config, "interfaces", "interface", NULL);
	difaces = lydx_get_descendant(diff, "interfaces", "interface", NULL);

	/* find the modified interfaces */
	LYX_LIST_FOR_EACH(difaces, diface, "interface") {
		const char *ifname = lydx_get_cattr(diface, "name");
		struct lyd_node *dipv4;

		dipv4 = lydx_get_descendant(lyd_child(diface), "ipv4", NULL);
		if (!dipv4)
			continue;

		ddhcp = lydx_get_descendant(lyd_child(dipv4), "dhcp", NULL);
		if (!ddhcp)
			continue;

		/* Check if dhcp container was deleted */
		if (lydx_get_op(ddhcp) == LYDX_OP_DELETE) {
			del(ifname);
			continue;
		}

		/* Find corresponding interface in config to check if dhcp is present */
		LYX_LIST_FOR_EACH(ifaces, iface, "interface") {
			if (strcmp(ifname, lydx_get_cattr(iface, "name")))
				continue;

			ipv4 = lydx_get_descendant(lyd_child(iface), "ipv4", NULL);
			if (!ipv4) {
				del(ifname);
				break;
			}

			dhcp = lydx_get_descendant(lyd_child(ipv4), "dhcp", NULL);
			if (!dhcp)
				del(ifname);
			else
				add(ifname, dhcp);
			break;
		}
	}

	return err;
}
