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

#include <assert.h>
#include <jansson.h>

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

#include "core.h"

#define XPATH_NTP_ "/ietf-ntp:ntp"
#define NTP_CONF   "/etc/chrony/conf.d/ntp-server.conf"
#define NTP_PREV   NTP_CONF "-"
#define NTP_NEXT   NTP_CONF "+"

static int change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff,
		  sr_event_t event, struct confd *confd)
{
	struct lyd_node *ntp, *entry, *makestep, *refclock;
	const char *port;
	FILE *fp;

	if (diff && !lydx_get_xpathf(diff, XPATH_NTP_))
		return SR_ERR_OK;

	switch (event) {
	case SR_EV_ENABLED:	/* first time, on register. */
	case SR_EV_CHANGE:	/* regular change (copy cand running) */
		/* Generate next config */
		break;

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

	case SR_EV_DONE:
		/* Check if passed validation in previous event */
		if (!fexist(NTP_NEXT)) {
			(void)remove(NTP_CONF);
			return SR_ERR_OK;
		}
		(void)remove(NTP_PREV);
		(void)rename(NTP_CONF, NTP_PREV);
		(void)rename(NTP_NEXT, NTP_CONF);

		/* Reload chronyd to pick up new config */
		if (systemf("chronyc reload sources >/dev/null 2>&1"))
			ERRNO("Failed reloading chronyd sources");

		systemf("initctl -nbq touch chronyd");
		return SR_ERR_OK;

	default:
		return SR_ERR_OK;
	}

	ntp = lydx_get_xpathf(config, XPATH_NTP_);
	if (!ntp)
		return SR_ERR_OK;

	fp = fopen(NTP_NEXT, "w");
	if (!fp) {
		ERROR("Failed creating %s: %s", NTP_NEXT, strerror(errno));
		return SR_ERR_SYS;
	}

	fprintf(fp, "# Generated by confd\n");
	fprintf(fp, "# This file configures chronyd as an NTP server\n\n");

	/* Port configuration (optional) */
	port = lydx_get_cattr(ntp, "port");
	if (port) {
		fprintf(fp, "# Custom NTP port\n");
		fprintf(fp, "port %d\n\n", atoi(port));
	}

	/* makestep configuration - allow clock stepping for fast initial sync */
	makestep = lydx_get_child(ntp, "makestep");
	if (makestep) {
		const char *threshold = lydx_get_cattr(makestep, "threshold");
		const char *limit = lydx_get_cattr(makestep, "limit");

		fprintf(fp, "# Allow clock stepping for fast initial sync\n");
		fprintf(fp, "makestep %.1f %d\n\n", atof(threshold), atoi(limit));
	}

	fprintf(fp, "# Upstream NTP servers and peers\n");
	LYX_LIST_FOR_EACH(lyd_child(ntp), entry, "unicast-configuration") {
		const char *address, *type, *minpoll, *maxpoll, *version, *srcport;
		const char *directive = NULL;
		bool prefer, burst, iburst;

		address = lydx_get_cattr(entry, "address");
		type    = lydx_get_cattr(entry, "type");
		minpoll = lydx_get_cattr(entry, "minpoll");
		maxpoll = lydx_get_cattr(entry, "maxpoll");
		version = lydx_get_cattr(entry, "version");
		srcport = lydx_get_cattr(entry, "port");
		prefer  = lydx_get_bool(entry, "prefer");
		burst   = lydx_get_bool(entry, "burst");
		iburst  = lydx_get_bool(entry, "iburst");

		if (type && strstr(type, "uc-server"))
			directive = "server";
		else if (type && strstr(type, "uc-peer"))
			directive = "peer";

		if (directive && address) {
			fprintf(fp, "%s %s", directive, address);
			if (srcport)
				fprintf(fp, " port %s", srcport);
			if (iburst)
				fprintf(fp, " iburst");
			if (burst)
				fprintf(fp, " burst");
			if (prefer)
				fprintf(fp, " prefer");
			if (minpoll)
				fprintf(fp, " minpoll %s", minpoll);
			if (maxpoll)
				fprintf(fp, " maxpoll %s", maxpoll);
			if (version)
				fprintf(fp, " version %s", version);
			fprintf(fp, "\n");
		}
	}
	fprintf(fp, "\n");

	/* Reference clock configuration */
	refclock = lydx_get_child(ntp, "refclock-master");
	if (refclock) {
		struct lyd_node *source;
		int stratum = atoi(lydx_get_cattr(refclock, "master-stratum"));
		bool has_gps_sources = false;

		/*
		 * GPS/GNSS reference clock sources via gpsd SHM
		 *
		 * Each GPS hardware component (gps0, gps1, ...) maps to:
		 * - SHM unit 2*N for GPS time (coarse, ~100ms)
		 * - SHM unit 2*N+1 for PPS time (precise, ~1us) if enabled
		 *
		 * The mapping is based on gpsd's convention.
		 */
		LYX_LIST_FOR_EACH(lyd_child(refclock), source, "source") {
			const char *receiver, *refid, *poll_str, *precision_str;
			const char *offset_str, *delay_str;
			bool prefer, pps;
			int unit;

			receiver = lydx_get_cattr(source, "receiver");
			if (!receiver)
				continue;

			/* Extract unit number from receiver name (gps0 -> 0, gps1 -> 1, ...) */
			if (sscanf(receiver, "gps%d", &unit) != 1) {
				ERROR("Invalid GPS receiver name: %s (expected gpsN)", receiver);
				continue;
			}

			refid         = lydx_get_cattr(source, "refid");
			poll_str      = lydx_get_cattr(source, "poll");
			precision_str = lydx_get_cattr(source, "precision");
			offset_str    = lydx_get_cattr(source, "offset");
			delay_str     = lydx_get_cattr(source, "delay");
			prefer        = lydx_get_bool(source, "prefer");
			pps           = lydx_get_bool(source, "pps");

			if (!has_gps_sources) {
				fprintf(fp, "# GPS/GNSS reference clock sources (via gpsd SHM)\n");
				has_gps_sources = true;
			}

			/* GPS time source: SHM unit 2*N */
			fprintf(fp, "refclock SHM %d", unit * 2);
			if (poll_str)
				fprintf(fp, " poll %s", poll_str);
			if (refid)
				fprintf(fp, " refid %s", refid);
			if (precision_str)
				fprintf(fp, " precision %s", precision_str);
			if (offset_str && atof(offset_str) != 0.0)
				fprintf(fp, " offset %s", offset_str);
			if (delay_str && atof(delay_str) != 0.0)
				fprintf(fp, " delay %s", delay_str);
			if (prefer)
				fprintf(fp, " prefer");
			fprintf(fp, "\n");

			/* PPS time source: SHM unit 2*N+1 (if enabled) */
			if (pps) {
				fprintf(fp, "refclock SHM %d", unit * 2 + 1);
				fprintf(fp, " poll %s", poll_str ? poll_str : "-6");
				fprintf(fp, " refid PPS");
				fprintf(fp, " precision 0.000001");
				if (prefer)
					fprintf(fp, " prefer");
				/* PPS needs a time source for initial lock */
				fprintf(fp, " lock %s", refid ? refid : "GPS");
				fprintf(fp, "\n");
			}
		}

		if (has_gps_sources)
			fprintf(fp, "\n");

		/* Local clock fallback - only if stratum is valid (1-15) */
		if (stratum >= 1 && stratum <= 15) {
			fprintf(fp, "# Local reference clock - fallback stratum %d source\n", stratum);
			fprintf(fp, "local stratum %d orphan\n\n", stratum);
		}
	}

	/*
	 * Enable NTP server mode - allow clients to query us
	 *
	 * Using 'allow' without arguments permits all clients.
	 * In a future version with access-rules support, we could
	 * restrict to specific subnets.
	 */
	fprintf(fp, "# Allow NTP clients to query this server\n");
	fprintf(fp, "allow\n\n");

	/*
	 * Enable RTC synchronization
	 *
	 * On Linux, the kernel will copy system time to the hardware RTC
	 * every 11 minutes when the clock is synchronized. This keeps the
	 * RTC accurate for the next boot, which is important for embedded
	 * systems without continuous network connectivity.
	 */
	fprintf(fp, "# Synchronize system time to hardware RTC\n");
	fprintf(fp, "rtcsync\n");

	fclose(fp);
	return SR_ERR_OK;
}

/*
 * Inference callback for NTP server configuration
 *
 * When a user creates the /ietf-ntp:ntp presence container without
 * any configuration, we infer sensible defaults:
 *
 * - refclock-master container (stratum defaults to 16 per YANG model)
 * - makestep with threshold 1.0 and limit 3 (fast initial sync for embedded)
 *
 * This provides a usable NTP server configuration with just 'edit ntp' + 'leave'.
 */
static int cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
		const char *path, sr_event_t event, unsigned request_id, void *priv)
{
	sr_val_t inferred_container = { .type = SR_CONTAINER_PRESENCE_T };
	size_t cnt = 0;

	if (event != SR_EV_UPDATE && event != SR_EV_CHANGE)
		return SR_ERR_OK;

	/* Check if NTP container exists */
	if (srx_nitems(session, &cnt, XPATH_NTP_) || !cnt)
		return SR_ERR_OK;

	/* Check if refclock-master already configured */
	if (!srx_nitems(session, &cnt, XPATH_NTP_"/refclock-master") && cnt > 0)
		return SR_ERR_OK;

	/* Check if unicast-configuration already configured */
	if (!srx_nitems(session, &cnt, XPATH_NTP_"/unicast-configuration") && cnt > 0)
		return SR_ERR_OK;

	/* Infer refclock-master container (let YANG provide default stratum 16) */
	DEBUG("Inferring NTP refclock-master container");
	srx_set_item(session, &inferred_container, 0, XPATH_NTP_"/refclock-master");

	/* Infer makestep for fast initial sync (critical for embedded systems) */
	if (!srx_nitems(session, &cnt, XPATH_NTP_"/infix-ntp:makestep") || cnt == 0) {
		DEBUG("Inferring NTP makestep container");
		/* Create presence container (let YANG provide defaults: threshold 1.0, limit 3) */
		srx_set_item(session, &inferred_container, 0, XPATH_NTP_"/infix-ntp:makestep");
	}

	return SR_ERR_OK;
}

int ntp_change(sr_session_ctx_t *session, struct lyd_node *config,
	       struct lyd_node *diff, sr_event_t event, struct confd *confd)
{
	return change(session, config, diff, event, confd);
}

int ntp_cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
	     const char *path, sr_event_t event, unsigned request_id, void *priv)
{
	return cand(session, sub_id, module, path, event, request_id, priv);
}

int ntp_candidate_init(struct confd *confd)
{
	int rc;

	REGISTER_CHANGE(confd->cand, "ietf-ntp", XPATH_NTP_ "//.", SR_SUBSCR_UPDATE,
			ntp_cand, confd, &confd->sub);

	return SR_ERR_OK;
fail:
	ERROR("init failed: %s", sr_strerror(rc));
	return rc;
}
