/* 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 MODULE "infix-dhcpv6-client"
#define XPATH  "/ietf-interfaces:interfaces/interface/ietf-ip:ipv6/infix-dhcpv6-client:dhcp"

static char *fallback_options_v6(const char *ifname)
{
	/* dns-server, domain-search, client-fqdn, ntp-server */
	/* odhcp6c format: -R -r <comma-separated list> */
	const char *defaults = "-R -r 23,24,39,56 ";

	return strdup(defaults);
}

static char *dhcp_options_v6(const char *ifname, struct lyd_node *cfg, bool *request_pd)
{
	struct lyd_node *option;
	char opt_list[512] = { 0 };
	char *options = NULL;
	bool has_options = false;

	*request_pd = false;

	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");
		const char *name = lyd_get_value(id);
		int num = dhcp_option_lookup(id);

		/* ia-pd option requires -P flag, not -r 25 */
		if (num == 25 || (name && !strcmp(name, "ia-pd"))) {
			*request_pd = true;
			continue;  /* Don't add to options string */
		}

		/* Options with values not yet supported with odhcp6c */
		if (val || hex) {
			WARN("DHCPv6 client %s: option values not yet supported with odhcp6c, ignoring %s",
			     ifname, name);
			continue;
		}

		/* Build comma-separated list for -r flag */
		if (has_options)
			strlcat(opt_list, ",", sizeof(opt_list));

		char num_str[8];
		snprintf(num_str, sizeof(num_str), "%d", num);
		strlcat(opt_list, num_str, sizeof(opt_list));
		has_options = true;
	}

	if (has_options) {
		size_t len = strlen(opt_list) + 16;
		options = malloc(len);
		if (options)
			snprintf(options, len, "-R -r %s ", opt_list);
	}

	return options ?: fallback_options_v6(ifname);
}

static void add_v6(const char *ifname, struct lyd_node *cfg)
{
	const char *metric = lydx_get_cattr(cfg, "route-preference");
	const char *duid = lydx_get_cattr(cfg, "duid");
	char *client_duid = NULL, *options = NULL;
	const char *action = "disable";
	const char *addr_mode = "-N try";  /* Default: stateful mode */
	char prefix_del[16] = { 0 };
	bool request_pd = false;
	FILE *fp;

	/* Information-only mode (stateless DHCPv6) */
	if (lydx_is_enabled(cfg, "information-only"))
		addr_mode = "-N none";

	/* Custom DUID: odhcp6c uses -c instead of -x 1: */
	if (duid && duid[0]) {
		size_t len = strlen(duid) + 16;

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

		snprintf(client_duid, len, "-c %s ", duid);
	}

	options = dhcp_options_v6(ifname, cfg, &request_pd);

	/* Prefix delegation: use -P 0 for auto */
	if (request_pd)
		strlcpy(prefix_del, "-P 0 ", sizeof(prefix_del));

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

	fprintf(fp, "# Generated by Infix confd\n");
	fprintf(fp, "metric=%s\n", metric);
	fprintf(fp, "service <!> name:dhcpv6-client :%s <net/%s/running> \\\n"
		"	[2345] odhcp6c -e -p /run/dhcpv6-client-%s.pid \\\n"
		"		-s /usr/libexec/odhcp6c.sh \\\n"
		"		%s %s%s%s \\\n"
		"		%s \\\n"
		"		-- DHCPv6 client @%s\n",
		ifname, ifname, ifname,
		addr_mode, prefix_del,
		options ?: "", client_duid ?: "",
		ifname, ifname);
	fclose(fp);
	action = "enable";
err:
	systemf("initctl -bfqn %s dhcpv6-client-%s", action, ifname);
	if (options)
		free(options);
	if (client_duid)
		free(client_duid);
}

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

int dhcpv6_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, *ipv6, *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 *dipv6;

		dipv6 = lydx_get_descendant(lyd_child(diface), "ipv6", NULL);
		if (!dipv6)
			continue;

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

		/* Check if dhcp container was deleted */
		if (lydx_get_op(ddhcp) == LYDX_OP_DELETE) {
			del_v6(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;

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

			dhcp = lydx_get_descendant(lyd_child(ipv6), "dhcp", NULL);
			if (!dhcp)
				del_v6(ifname);
			else
				add_v6(ifname, dhcp);
			break;
		}
	}

	return err;
}
