// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2018  NetDEF, Inc.
 *                     Renato Westphal
 */

#include <zebra.h>

#include "log.h"
#include "lib_errors.h"
#include "command.h"
#include "debug.h"
#include "libfrr.h"
#include "lib/version.h"
#include "northbound.h"

#include <confd_lib.h>
#include <confd_cdb.h>
#include <confd_dp.h>
#include <confd_maapi.h>

DEFINE_MTYPE_STATIC(LIB, CONFD, "ConfD module");

static struct debug nb_dbg_client_confd = {0, "Northbound client: ConfD"};

static struct event_loop *master;
static struct sockaddr confd_addr;
static int cdb_sub_sock, dp_ctl_sock, dp_worker_sock;
static struct event *t_cdb_sub, *t_dp_ctl, *t_dp_worker;
static struct confd_daemon_ctx *dctx;
static struct confd_notification_ctx *live_ctx;
static bool confd_connected;
static struct list *confd_spoints;
static struct nb_transaction *transaction;

static void frr_confd_finish_cdb(void);
static void frr_confd_finish_dp(void);
static int frr_confd_finish(void);

#define flog_err_confd(funcname)                                               \
	flog_err(EC_LIB_LIBCONFD, "%s: %s() failed: %s (%d): %s", __func__,    \
		 (funcname), confd_strerror(confd_errno), confd_errno,         \
		 confd_lasterr())


/* ------------ Utils ------------ */

/* Get XPath string from ConfD hashed keypath. */
static void frr_confd_get_xpath(const confd_hkeypath_t *kp, char *xpath,
				size_t len)
{
	char *p;

	confd_xpath_pp_kpath(xpath, len, 0, kp);

	/*
	 * Replace double quotes by single quotes (the format accepted by the
	 * northbound API).
	 */
	p = xpath;
	while ((p = strchr(p, '"')) != NULL)
		*p++ = '\'';
}

/* Convert ConfD binary value to a string. */
static int frr_confd_val2str(const char *xpath, const confd_value_t *value,
			     char *string, size_t string_size)
{
	struct confd_cs_node *csp;

	csp = confd_cs_node_cd(NULL, xpath);
	if (!csp) {
		flog_err_confd("confd_cs_node_cd");
		return -1;
	}
	if (confd_val2str(csp->info.type, value, string, string_size)
	    == CONFD_ERR) {
		flog_err_confd("confd_val2str");
		return -1;
	}

	return 0;
}

/* Obtain list entry from ConfD hashed keypath. */
static int frr_confd_hkeypath_get_list_entry(const confd_hkeypath_t *kp,
					     struct nb_node *nb_node,
					     const void **list_entry)
{
	struct nb_node *nb_node_list;
	int parent_lists = 0;
	int curr_list = 0;

	*list_entry = NULL;

	/*
	 * Count the number of YANG lists in the path, disconsidering the
	 * last element.
	 */
	nb_node_list = nb_node;
	while (nb_node_list->parent_list) {
		nb_node_list = nb_node_list->parent_list;
		parent_lists++;
	}
	if (nb_node->snode->nodetype != LYS_LIST && parent_lists == 0)
		return 0;

	/* Start from the beginning and move down the tree. */
	for (int i = kp->len; i >= 0; i--) {
		struct yang_list_keys keys;

		/* Not a YANG list. */
		if (kp->v[i][0].type != C_BUF)
			continue;

		/* Obtain list keys. */
		memset(&keys, 0, sizeof(keys));
		for (int j = 0; kp->v[i][j].type != C_NOEXISTS; j++) {
			strlcpy(keys.key[keys.num],
				(char *)kp->v[i][j].val.buf.ptr,
				sizeof(keys.key[keys.num]));
			keys.num++;
		}

		/* Obtain northbound node associated to the YANG list. */
		nb_node_list = nb_node;
		for (int j = curr_list; j < parent_lists; j++)
			nb_node_list = nb_node_list->parent_list;

		/* Obtain list entry. */
		if (!CHECK_FLAG(nb_node_list->flags, F_NB_NODE_KEYLESS_LIST)) {
			*list_entry = nb_callback_lookup_entry(
				nb_node, *list_entry, &keys);
			if (*list_entry == NULL)
				return -1;
		} else {
			unsigned long ptr_ulong;

			/* Retrieve list entry from pseudo-key (string). */
			if (sscanf(keys.key[0], "%lu", &ptr_ulong) != 1)
				return -1;
			*list_entry = (const void *)ptr_ulong;
		}

		curr_list++;
	}

	return 0;
}

/* Fill the current date and time into a confd_datetime structure. */
static void getdatetime(struct confd_datetime *datetime)
{
	struct tm tm;
	struct timeval tv;

	gettimeofday(&tv, NULL);
	gmtime_r(&tv.tv_sec, &tm);

	memset(datetime, 0, sizeof(*datetime));
	datetime->year = 1900 + tm.tm_year;
	datetime->month = tm.tm_mon + 1;
	datetime->day = tm.tm_mday;
	datetime->sec = tm.tm_sec;
	datetime->micro = tv.tv_usec;
	datetime->timezone = 0;
	datetime->timezone_minutes = 0;
	datetime->hour = tm.tm_hour;
	datetime->min = tm.tm_min;
}

/* ------------ CDB code ------------ */

struct cdb_iter_args {
	struct nb_config *candidate;
	bool error;
};

static enum cdb_iter_ret
frr_confd_cdb_diff_iter(confd_hkeypath_t *kp, enum cdb_iter_op cdb_op,
			confd_value_t *oldv, confd_value_t *newv, void *args)
{
	char xpath[XPATH_MAXLEN];
	struct nb_node *nb_node;
	enum nb_operation nb_op;
	struct cdb_iter_args *iter_args = args;
	char value_str[YANG_VALUE_MAXLEN];
	struct yang_data *data;
	char *sb1, *sb2;
	int ret;

	frr_confd_get_xpath(kp, xpath, sizeof(xpath));

	/*
	 * HACK: obtain value of leaf-list elements from the XPath due to
	 * a bug in the ConfD API.
	 */
	value_str[0] = '\0';
	sb1 = strrchr(xpath, '[');
	sb2 = strrchr(xpath, ']');
	if (sb1 && sb2 && !strchr(sb1, '=')) {
		*sb2 = '\0';
		strlcpy(value_str, sb1 + 1, sizeof(value_str));
		*sb1 = '\0';
	}

	nb_node = nb_node_find(xpath);
	if (!nb_node) {
		flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
			  "%s: unknown data path: %s", __func__, xpath);
		iter_args->error = true;
		return ITER_STOP;
	}

	/* Map operation values. */
	switch (cdb_op) {
	case MOP_CREATED:
		nb_op = NB_OP_CREATE;
		break;
	case MOP_DELETED:
		nb_op = NB_OP_DESTROY;
		break;
	case MOP_VALUE_SET:
		if (nb_operation_is_valid(NB_OP_MODIFY, nb_node->snode))
			nb_op = NB_OP_MODIFY;
		else
			/* Ignore list keys modifications. */
			return ITER_RECURSE;
		break;
	case MOP_MOVED_AFTER:
		nb_op = NB_OP_MOVE;
		break;
	case MOP_MODIFIED:
		/* We're not interested on this. */
		return ITER_RECURSE;
	default:
		flog_err(EC_LIB_DEVELOPMENT,
			 "%s: unexpected operation %u [xpath %s]", __func__,
			 cdb_op, xpath);
		iter_args->error = true;
		return ITER_STOP;
	}

	/* Convert ConfD value to a string. */
	if (nb_node->snode->nodetype != LYS_LEAFLIST && newv
	    && frr_confd_val2str(nb_node->xpath, newv, value_str,
				 sizeof(value_str))
		       != 0) {
		flog_err(EC_LIB_CONFD_DATA_CONVERT,
			 "%s: failed to convert ConfD value to a string",
			 __func__);
		iter_args->error = true;
		return ITER_STOP;
	}

	/* Edit the candidate configuration. */
	data = yang_data_new(xpath, value_str);
	ret = nb_candidate_edit(iter_args->candidate, nb_node, nb_op, xpath,
				NULL, data);
	yang_data_free(data);
	if (ret != NB_OK) {
		flog_warn(
			EC_LIB_NB_CANDIDATE_EDIT_ERROR,
			"%s: failed to edit candidate configuration: operation [%s] xpath [%s]",
			__func__, nb_operation_name(nb_op), xpath);
		iter_args->error = true;
		return ITER_STOP;
	}

	return ITER_RECURSE;
}

static void frr_confd_cdb_read_cb_prepare(int fd, int *subp, int reslen)
{
	struct nb_context context = {};
	struct nb_config *candidate;
	struct cdb_iter_args iter_args;
	char errmsg[BUFSIZ] = {0};
	int ret;

	candidate = nb_config_dup(running_config);

	/* Iterate over all configuration changes. */
	iter_args.candidate = candidate;
	iter_args.error = false;
	for (int i = 0; i < reslen; i++) {
		if (cdb_diff_iterate(fd, subp[i], frr_confd_cdb_diff_iter,
				     ITER_WANT_PREV, &iter_args)
		    != CONFD_OK) {
			flog_err_confd("cdb_diff_iterate");
		}
	}
	free(subp);

	if (iter_args.error) {
		nb_config_free(candidate);

		if (cdb_sub_abort_trans(
			    cdb_sub_sock, CONFD_ERRCODE_APPLICATION_INTERNAL, 0,
			    0, "Couldn't apply configuration changes")
		    != CONFD_OK) {
			flog_err_confd("cdb_sub_abort_trans");
			return;
		}
		return;
	}

	/*
	 * Validate the configuration changes and allocate all resources
	 * required to apply them.
	 */
	transaction = NULL;
	context.client = NB_CLIENT_CONFD;
	ret = nb_candidate_commit_prepare(context, candidate, NULL,
					  &transaction, false, false, errmsg,
					  sizeof(errmsg));
	if (ret != NB_OK && ret != NB_ERR_NO_CHANGES) {
		enum confd_errcode errcode;

		switch (ret) {
		case NB_ERR_LOCKED:
			errcode = CONFD_ERRCODE_IN_USE;
			break;
		case NB_ERR_RESOURCE:
			errcode = CONFD_ERRCODE_RESOURCE_DENIED;
			break;
		default:
			errcode = CONFD_ERRCODE_APPLICATION;
			break;
		}

		/* Reject the configuration changes. */
		if (cdb_sub_abort_trans(cdb_sub_sock, errcode, 0, 0, "%s",
					errmsg)
		    != CONFD_OK) {
			flog_err_confd("cdb_sub_abort_trans");
			return;
		}
	} else {
		/* Acknowledge the notification. */
		if (cdb_sync_subscription_socket(fd, CDB_DONE_PRIORITY)
		    != CONFD_OK) {
			flog_err_confd("cdb_sync_subscription_socket");
			return;
		}

		/* No configuration changes. */
		if (!transaction)
			nb_config_free(candidate);
	}
}

static void frr_confd_cdb_read_cb_commit(int fd, int *subp, int reslen)
{
	/*
	 * No need to process the configuration changes again as we're already
	 * keeping track of them in the "transaction" variable.
	 */
	free(subp);

	/* Apply the transaction. */
	if (transaction) {
		struct nb_config *candidate = transaction->config;
		char errmsg[BUFSIZ] = {0};

		nb_candidate_commit_apply(transaction, true, NULL, errmsg,
					  sizeof(errmsg));
		nb_config_free(candidate);
	}

	/* Acknowledge the notification. */
	if (cdb_sync_subscription_socket(fd, CDB_DONE_PRIORITY) != CONFD_OK) {
		flog_err_confd("cdb_sync_subscription_socket");
		return;
	}
}

static int frr_confd_cdb_read_cb_abort(int fd, int *subp, int reslen)
{
	/*
	 * No need to process the configuration changes again as we're already
	 * keeping track of them in the "transaction" variable.
	 */
	free(subp);

	/* Abort the transaction. */
	if (transaction) {
		struct nb_config *candidate = transaction->config;
		char errmsg[BUFSIZ] = {0};

		nb_candidate_commit_abort(transaction, errmsg, sizeof(errmsg));
		nb_config_free(candidate);
	}

	/* Acknowledge the notification. */
	if (cdb_sync_subscription_socket(fd, CDB_DONE_PRIORITY) != CONFD_OK) {
		flog_err_confd("cdb_sync_subscription_socket");
		return -1;
	}

	return 0;
}

static void frr_confd_cdb_read_cb(struct event *thread)
{
	int fd = EVENT_FD(thread);
	enum cdb_sub_notification cdb_ev;
	int flags;
	int *subp = NULL;
	int reslen = 0;

	event_add_read(master, frr_confd_cdb_read_cb, NULL, fd, &t_cdb_sub);

	if (cdb_read_subscription_socket2(fd, &cdb_ev, &flags, &subp, &reslen)
	    != CONFD_OK) {
		flog_err_confd("cdb_read_subscription_socket2");
		return;
	}

	switch (cdb_ev) {
	case CDB_SUB_PREPARE:
		frr_confd_cdb_read_cb_prepare(fd, subp, reslen);
		break;
	case CDB_SUB_COMMIT:
		frr_confd_cdb_read_cb_commit(fd, subp, reslen);
		break;
	case CDB_SUB_ABORT:
		frr_confd_cdb_read_cb_abort(fd, subp, reslen);
		break;
	default:
		flog_err_confd("unknown CDB event");
		break;
	}
}

/* Trigger CDB subscriptions to read the startup configuration. */
static void *thread_cdb_trigger_subscriptions(void *data)
{
	int sock;
	int *sub_points = NULL, len = 0;
	struct listnode *node;
	int *spoint;
	int i = 0;

	/* Create CDB data socket. */
	sock = socket(PF_INET, SOCK_STREAM, 0);
	if (sock < 0) {
		flog_err(EC_LIB_SOCKET, "%s: failed to create socket: %s",
			 __func__, safe_strerror(errno));
		return NULL;
	}

	if (cdb_connect(sock, CDB_DATA_SOCKET, &confd_addr,
			sizeof(struct sockaddr_in))
	    != CONFD_OK) {
		flog_err_confd("cdb_connect");
		return NULL;
	}

	/*
	 * Fill array containing the subscription point of all loaded YANG
	 * modules.
	 */
	len = listcount(confd_spoints);
	sub_points = XCALLOC(MTYPE_CONFD, len * sizeof(int));
	for (ALL_LIST_ELEMENTS_RO(confd_spoints, node, spoint))
		sub_points[i++] = *spoint;

	if (cdb_trigger_subscriptions(sock, sub_points, len) != CONFD_OK) {
		flog_err_confd("cdb_trigger_subscriptions");
		return NULL;
	}

	/* Cleanup and exit thread. */
	XFREE(MTYPE_CONFD, sub_points);
	cdb_close(sock);

	return NULL;
}

static int frr_confd_subscribe(const struct lysc_node *snode, void *arg)
{
	struct yang_module *module = arg;
	struct nb_node *nb_node;
	int *spoint;
	int ret;

	switch (snode->nodetype) {
	case LYS_CONTAINER:
	case LYS_LEAF:
	case LYS_LEAFLIST:
	case LYS_LIST:
		break;
	default:
		return YANG_ITER_CONTINUE;
	}

	if (CHECK_FLAG(snode->flags, LYS_CONFIG_R))
		return YANG_ITER_CONTINUE;

	nb_node = snode->priv;
	if (!nb_node)
		return YANG_ITER_CONTINUE;

	DEBUGD(&nb_dbg_client_confd, "%s: subscribing to '%s'", __func__,
	       nb_node->xpath);

	spoint = XMALLOC(MTYPE_CONFD, sizeof(*spoint));
	ret = cdb_subscribe2(cdb_sub_sock, CDB_SUB_RUNNING_TWOPHASE,
			     CDB_SUB_WANT_ABORT_ON_ABORT, 3, spoint,
			     module->confd_hash, nb_node->xpath);
	if (ret != CONFD_OK) {
		flog_err_confd("cdb_subscribe2");
		XFREE(MTYPE_CONFD, spoint);
		return YANG_ITER_CONTINUE;
	}

	listnode_add(confd_spoints, spoint);
	return YANG_ITER_CONTINUE;
}

static int frr_confd_init_cdb(void)
{
	struct yang_module *module;
	pthread_t cdb_trigger_thread;

	/* Create CDB subscription socket. */
	cdb_sub_sock = socket(PF_INET, SOCK_STREAM, 0);
	if (cdb_sub_sock < 0) {
		flog_err(EC_LIB_SOCKET, "%s: failed to create socket: %s",
			 __func__, safe_strerror(errno));
		return -1;
	}

	if (cdb_connect(cdb_sub_sock, CDB_SUBSCRIPTION_SOCKET, &confd_addr,
			sizeof(struct sockaddr_in))
	    != CONFD_OK) {
		flog_err_confd("cdb_connect");
		goto error;
	}

	/* Subscribe to all loaded YANG data modules. */
	confd_spoints = list_new();
	RB_FOREACH (module, yang_modules, &yang_modules) {
		module->confd_hash = confd_str2hash(module->info->ns);
		if (module->confd_hash == 0) {
			flog_err(
				EC_LIB_LIBCONFD,
				"%s: failed to find hash value for namespace %s",
				__func__, module->info->ns);
			goto error;
		}

		/*
		 * The CDB API doesn't provide a mechanism to subscribe to an
		 * entire YANG module. So we have to find the top level
		 * nodes ourselves and subscribe to their paths.
		 */
		yang_snodes_iterate(module->info, frr_confd_subscribe, 0,
				    module);
	}

	if (cdb_subscribe_done(cdb_sub_sock) != CONFD_OK) {
		flog_err_confd("cdb_subscribe_done");
		goto error;
	}

	/* Create short lived pthread to trigger the CDB subscriptions. */
	if (pthread_create(&cdb_trigger_thread, NULL,
			   thread_cdb_trigger_subscriptions, NULL)) {
		flog_err(EC_LIB_SYSTEM_CALL, "%s: error creating pthread: %s",
			 __func__, safe_strerror(errno));
		goto error;
	}
	pthread_detach(cdb_trigger_thread);

	event_add_read(master, frr_confd_cdb_read_cb, NULL, cdb_sub_sock,
		       &t_cdb_sub);

	return 0;

error:
	frr_confd_finish_cdb();

	return -1;
}

static void frr_confd_finish_cdb(void)
{
	if (cdb_sub_sock > 0) {
		EVENT_OFF(t_cdb_sub);
		cdb_close(cdb_sub_sock);
	}
}

/* ------------ DP code ------------ */

static int frr_confd_transaction_init(struct confd_trans_ctx *tctx)
{
	confd_trans_set_fd(tctx, dp_worker_sock);

	return CONFD_OK;
}

#define CONFD_MAX_CHILD_NODES 32

static int frr_confd_data_get_elem(struct confd_trans_ctx *tctx,
				   confd_hkeypath_t *kp)
{
	struct nb_node *nb_node;
	char xpath[XPATH_MAXLEN];
	struct yang_data *data;
	confd_value_t v;
	const void *list_entry = NULL;

	frr_confd_get_xpath(kp, xpath, sizeof(xpath));

	nb_node = nb_node_find(xpath);
	if (!nb_node) {
		flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
			  "%s: unknown data path: %s", __func__, xpath);
		confd_data_reply_not_found(tctx);
		return CONFD_OK;
	}

	if (frr_confd_hkeypath_get_list_entry(kp, nb_node, &list_entry) != 0) {
		confd_data_reply_not_found(tctx);
		return CONFD_OK;
	}

	data = nb_callback_get_elem(nb_node, xpath, list_entry);
	if (data) {
		if (data->value) {
			CONFD_SET_STR(&v, data->value);
			confd_data_reply_value(tctx, &v);
		} else
			confd_data_reply_found(tctx);
		yang_data_free(data);
	} else
		confd_data_reply_not_found(tctx);

	return CONFD_OK;
}

static int frr_confd_data_get_next(struct confd_trans_ctx *tctx,
				   confd_hkeypath_t *kp, long next)
{
	struct nb_node *nb_node;
	char xpath[XPATH_MAXLEN];
	struct yang_data *data;
	const void *parent_list_entry, *nb_next;
	confd_value_t v[LIST_MAXKEYS];

	frr_confd_get_xpath(kp, xpath, sizeof(xpath));

	nb_node = nb_node_find(xpath);
	if (!nb_node) {
		flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
			  "%s: unknown data path: %s", __func__, xpath);
		confd_data_reply_next_key(tctx, NULL, -1, -1);
		return CONFD_OK;
	}

	if (frr_confd_hkeypath_get_list_entry(kp, nb_node, &parent_list_entry)
	    != 0) {
		/* List entry doesn't exist anymore. */
		confd_data_reply_next_key(tctx, NULL, -1, -1);
		return CONFD_OK;
	}

	nb_next = nb_callback_get_next(nb_node, parent_list_entry,
				       (next == -1) ? NULL : (void *)next);
	if (!nb_next) {
		/* End of the list or leaf-list. */
		confd_data_reply_next_key(tctx, NULL, -1, -1);
		return CONFD_OK;
	}

	switch (nb_node->snode->nodetype) {
	case LYS_LIST:
		if (!CHECK_FLAG(nb_node->flags, F_NB_NODE_KEYLESS_LIST)) {
			struct yang_list_keys keys;

			memset(&keys, 0, sizeof(keys));
			if (nb_callback_get_keys(nb_node, nb_next, &keys)
			    != NB_OK) {
				flog_warn(EC_LIB_NB_CB_STATE,
					  "%s: failed to get list keys",
					  __func__);
				confd_data_reply_next_key(tctx, NULL, -1, -1);
				return CONFD_OK;
			}

			/* Feed keys to ConfD. */
			for (size_t i = 0; i < keys.num; i++)
				CONFD_SET_STR(&v[i], keys.key[i]);
			confd_data_reply_next_key(tctx, v, keys.num,
						  (long)nb_next);
		} else {
			char pointer_str[32];

			/*
			 * ConfD 6.6 user guide, chapter 6.11 (Operational data
			 * lists without keys):
			 * "To support this without having completely separate
			 * APIs, we use a "pseudo" key in the ConfD APIs for
			 * this type of list. This key is not part of the data
			 * model, and completely hidden in the northbound agent
			 * interfaces, but is used with e.g. the get_next() and
			 * get_elem() callbacks as if it were a normal key. This
			 * "pseudo" key is always a single signed 64-bit
			 * integer, i.e. the confd_value_t type is C_INT64. The
			 * values can be chosen arbitrarily by the application,
			 * as long as a key value returned by get_next() can be
			 * used to get the data for the corresponding list entry
			 * with get_elem() or get_object() as usual. It could
			 * e.g. be an index into an array that holds the data,
			 * or even a memory address in integer form".
			 *
			 * Since we're using the CONFD_DAEMON_FLAG_STRINGSONLY
			 * option, we must convert our pseudo-key (a void
			 * pointer) to a string before sending it to confd.
			 */
			snprintf(pointer_str, sizeof(pointer_str), "%lu",
				 (unsigned long)nb_next);
			CONFD_SET_STR(&v[0], pointer_str);
			confd_data_reply_next_key(tctx, v, 1, (long)nb_next);
		}
		break;
	case LYS_LEAFLIST:
		data = nb_callback_get_elem(nb_node, xpath, nb_next);
		if (data) {
			if (data->value) {
				CONFD_SET_STR(&v[0], data->value);
				confd_data_reply_next_key(tctx, v, 1,
							  (long)nb_next);
			}
			yang_data_free(data);
		} else
			confd_data_reply_next_key(tctx, NULL, -1, -1);
		break;
	default:
		break;
	}

	return CONFD_OK;
}

/*
 * Optional callback - implemented for performance reasons.
 */
static int frr_confd_data_get_object(struct confd_trans_ctx *tctx,
				     confd_hkeypath_t *kp)
{
	struct nb_node *nb_node;
	const struct lysc_node *child;
	char xpath[XPATH_MAXLEN];
	char xpath_child[XPATH_MAXLEN * 2];
	struct list *elements;
	struct yang_data *data;
	const void *list_entry;
	confd_value_t values[CONFD_MAX_CHILD_NODES];
	size_t nvalues = 0;

	frr_confd_get_xpath(kp, xpath, sizeof(xpath));

	nb_node = nb_node_find(xpath);
	if (!nb_node) {
		flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
			  "%s: unknown data path: %s", __func__, xpath);
		confd_data_reply_not_found(tctx);
		return CONFD_ERR;
	}

	if (frr_confd_hkeypath_get_list_entry(kp, nb_node, &list_entry) != 0) {
		confd_data_reply_not_found(tctx);
		return CONFD_OK;
	}

	elements = yang_data_list_new();

	/* Loop through list child nodes. */
	LY_LIST_FOR (lysc_node_child(nb_node->snode), child) {
		struct nb_node *nb_node_child = child->priv;
		confd_value_t *v;

		if (nvalues > CONFD_MAX_CHILD_NODES)
			break;

		v = &values[nvalues++];

		/* Non-presence containers, lists and leaf-lists. */
		if (!nb_node_child->cbs.get_elem) {
			CONFD_SET_NOEXISTS(v);
			continue;
		}

		snprintf(xpath_child, sizeof(xpath_child), "%s/%s", xpath,
			 child->name);
		data = nb_callback_get_elem(nb_node_child, xpath_child,
					    list_entry);
		if (data) {
			if (data->value)
				CONFD_SET_STR(v, data->value);
			else {
				/* Presence containers and empty leafs. */
				CONFD_SET_XMLTAG(
					v, nb_node_child->confd_hash,
					confd_str2hash(nb_node_child->snode
							       ->module->ns));
			}
			listnode_add(elements, data);
		} else
			CONFD_SET_NOEXISTS(v);
	}

	confd_data_reply_value_array(tctx, values, nvalues);

	/* Release memory. */
	list_delete(&elements);

	return CONFD_OK;
}

/*
 * Optional callback - implemented for performance reasons.
 */
static int frr_confd_data_get_next_object(struct confd_trans_ctx *tctx,
					  confd_hkeypath_t *kp, long next)
{
	char xpath[XPATH_MAXLEN];
	struct nb_node *nb_node;
	struct list *elements;
	const void *parent_list_entry;
	const void *nb_next;
#define CONFD_OBJECTS_PER_TIME 100
	struct confd_next_object objects[CONFD_OBJECTS_PER_TIME + 1];
	char pseudo_keys[CONFD_OBJECTS_PER_TIME][32];
	int nobjects = 0;

	frr_confd_get_xpath(kp, xpath, sizeof(xpath));

	nb_node = nb_node_find(xpath);
	if (!nb_node) {
		flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
			  "%s: unknown data path: %s", __func__, xpath);
		confd_data_reply_next_object_array(tctx, NULL, 0, 0);
		return CONFD_OK;
	}

	if (frr_confd_hkeypath_get_list_entry(kp, nb_node, &parent_list_entry)
	    != 0) {
		confd_data_reply_next_object_array(tctx, NULL, 0, 0);
		return CONFD_OK;
	}

	elements = yang_data_list_new();
	nb_next = (next == -1) ? NULL : (void *)next;

	memset(objects, 0, sizeof(objects));
	for (int j = 0; j < CONFD_OBJECTS_PER_TIME; j++) {
		struct confd_next_object *object;
		const struct lysc_node *child;
		struct yang_data *data;
		size_t nvalues = 0;

		object = &objects[j];

		nb_next = nb_callback_get_next(nb_node, parent_list_entry,
					       nb_next);
		if (!nb_next)
			/* End of the list. */
			break;

		object->next = (long)nb_next;

		/* Leaf-lists require special handling. */
		if (nb_node->snode->nodetype == LYS_LEAFLIST) {
			object->v = XMALLOC(MTYPE_CONFD, sizeof(confd_value_t));
			data = nb_callback_get_elem(nb_node, xpath, nb_next);
			assert(data && data->value);
			CONFD_SET_STR(object->v, data->value);
			nvalues++;
			listnode_add(elements, data);
			goto next;
		}

		object->v =
			XMALLOC(MTYPE_CONFD,
				CONFD_MAX_CHILD_NODES * sizeof(confd_value_t));

		/*
		 * ConfD 6.6 user guide, chapter 6.11 (Operational data lists
		 * without keys):
		 * "In the response to the get_next_object() callback, the data
		 * provider is expected to provide the key values along with the
		 * other leafs in an array that is populated according to the
		 * data model. This must be done also for this type of list,
		 * even though the key isn't actually in the data model. The
		 * "pseudo" key must always be the first element in the array".
		 */
		if (CHECK_FLAG(nb_node->flags, F_NB_NODE_KEYLESS_LIST)) {
			confd_value_t *v;

			snprintf(pseudo_keys[j], sizeof(pseudo_keys[j]), "%lu",
				 (unsigned long)nb_next);

			v = &object->v[nvalues++];
			CONFD_SET_STR(v, pseudo_keys[j]);
		}

		/* Loop through list child nodes. */
		LY_LIST_FOR (lysc_node_child(nb_node->snode), child) {
			struct nb_node *nb_node_child = child->priv;
			char xpath_child[XPATH_MAXLEN * 2];
			confd_value_t *v;

			if (nvalues > CONFD_MAX_CHILD_NODES)
				break;

			v = &object->v[nvalues++];

			/* Non-presence containers, lists and leaf-lists. */
			if (!nb_node_child->cbs.get_elem) {
				CONFD_SET_NOEXISTS(v);
				continue;
			}

			snprintf(xpath_child, sizeof(xpath_child), "%s/%s",
				 xpath, child->name);
			data = nb_callback_get_elem(nb_node_child, xpath_child,
						    nb_next);
			if (data) {
				if (data->value)
					CONFD_SET_STR(v, data->value);
				else {
					/*
					 * Presence containers and empty leafs.
					 */
					CONFD_SET_XMLTAG(
						v, nb_node_child->confd_hash,
						confd_str2hash(
							nb_node_child->snode
								->module->ns));
				}
				listnode_add(elements, data);
			} else
				CONFD_SET_NOEXISTS(v);
		}
	next:
		object->n = nvalues;
		nobjects++;
	}

	if (nobjects == 0) {
		confd_data_reply_next_object_array(tctx, NULL, 0, 0);
		list_delete(&elements);
		return CONFD_OK;
	}

	/* Detect end of the list. */
	if (!nb_next) {
		nobjects++;
		objects[nobjects].v = NULL;
	}

	/* Reply to ConfD. */
	confd_data_reply_next_object_arrays(tctx, objects, nobjects, 0);
	if (!nb_next)
		nobjects--;

	/* Release memory. */
	list_delete(&elements);
	for (int j = 0; j < nobjects; j++) {
		struct confd_next_object *object;

		object = &objects[j];
		XFREE(MTYPE_CONFD, object->v);
	}

	return CONFD_OK;
}

static int frr_confd_notification_send(const char *xpath,
				       struct list *arguments)
{
	struct nb_node *nb_node;
	struct yang_module *module;
	struct confd_datetime now;
	confd_tag_value_t *values;
	int nvalues;
	int i = 0;
	struct yang_data *data;
	struct listnode *node;
	int ret;

	nb_node = nb_node_find(xpath);
	if (!nb_node) {
		flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
			  "%s: unknown data path: %s", __func__, xpath);
		return -1;
	}
	module = yang_module_find(nb_node->snode->module->name);
	assert(module);

	nvalues = 2;
	if (arguments)
		nvalues += listcount(arguments);

	values = XMALLOC(MTYPE_CONFD, nvalues * sizeof(*values));

	CONFD_SET_TAG_XMLBEGIN(&values[i++], nb_node->confd_hash,
			       module->confd_hash);
	for (ALL_LIST_ELEMENTS_RO(arguments, node, data)) {
		struct nb_node *nb_node_arg;

		nb_node_arg = nb_node_find(data->xpath);
		if (!nb_node_arg) {
			flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
				  "%s: unknown data path: %s", __func__,
				  data->xpath);
			XFREE(MTYPE_CONFD, values);
			return NB_ERR;
		}

		CONFD_SET_TAG_STR(&values[i++], nb_node_arg->confd_hash,
				  data->value);
	}
	CONFD_SET_TAG_XMLEND(&values[i++], nb_node->confd_hash,
			     module->confd_hash);

	getdatetime(&now);
	ret = confd_notification_send(live_ctx, &now, values, nvalues);

	/* Release memory. */
	XFREE(MTYPE_CONFD, values);

	/* Map ConfD return code to northbound return code. */
	switch (ret) {
	case CONFD_OK:
		return NB_OK;
	default:
		return NB_ERR;
	}
}

static int frr_confd_action_init(struct confd_user_info *uinfo)
{
	confd_action_set_fd(uinfo, dp_worker_sock);

	return CONFD_OK;
}

static int frr_confd_action_execute(struct confd_user_info *uinfo,
				    struct xml_tag *name, confd_hkeypath_t *kp,
				    confd_tag_value_t *params, int nparams)
{
	char xpath[XPATH_MAXLEN];
	struct nb_node *nb_node;
	struct list *input;
	struct list *output;
	struct yang_data *data;
	confd_tag_value_t *reply;
	int ret = CONFD_OK;
	char errmsg[BUFSIZ] = {0};

	/* Getting the XPath is tricky. */
	if (kp) {
		/* This is a YANG RPC. */
		frr_confd_get_xpath(kp, xpath, sizeof(xpath));
		strlcat(xpath, "/", sizeof(xpath));
		strlcat(xpath, confd_hash2str(name->tag), sizeof(xpath));
	} else {
		/* This is a YANG action. */
		snprintf(xpath, sizeof(xpath), "/%s:%s",
			 confd_ns2prefix(name->ns), confd_hash2str(name->tag));
	}

	nb_node = nb_node_find(xpath);
	if (!nb_node) {
		flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
			  "%s: unknown data path: %s", __func__, xpath);
		return CONFD_ERR;
	}

	input = yang_data_list_new();
	output = yang_data_list_new();

	/* Process input nodes. */
	for (int i = 0; i < nparams; i++) {
		char xpath_input[XPATH_MAXLEN * 2];
		char value_str[YANG_VALUE_MAXLEN];

		snprintf(xpath_input, sizeof(xpath_input), "%s/%s", xpath,
			 confd_hash2str(params[i].tag.tag));

		if (frr_confd_val2str(xpath_input, &params[i].v, value_str,
				      sizeof(value_str))
		    != 0) {
			flog_err(
				EC_LIB_CONFD_DATA_CONVERT,
				"%s: failed to convert ConfD value to a string",
				__func__);
			ret = CONFD_ERR;
			goto exit;
		}

		data = yang_data_new(xpath_input, value_str);
		listnode_add(input, data);
	}

	/* Execute callback registered for this XPath. */
	if (nb_callback_rpc(nb_node, xpath, input, output, errmsg,
			    sizeof(errmsg))
	    != NB_OK) {
		flog_warn(EC_LIB_NB_CB_RPC, "%s: rpc callback failed: %s",
			  __func__, xpath);
		ret = CONFD_ERR;
		goto exit;
	}

	/* Process output nodes. */
	if (listcount(output) > 0) {
		struct listnode *node;
		int i = 0;

		reply = XMALLOC(MTYPE_CONFD,
				listcount(output) * sizeof(*reply));

		for (ALL_LIST_ELEMENTS_RO(output, node, data)) {
			struct nb_node *nb_node_output;
			int hash;

			nb_node_output = nb_node_find(data->xpath);
			if (!nb_node_output) {
				flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
					  "%s: unknown data path: %s", __func__,
					  data->xpath);
				goto exit;
			}

			hash = confd_str2hash(nb_node_output->snode->name);
			CONFD_SET_TAG_STR(&reply[i++], hash, data->value);
		}
		confd_action_reply_values(uinfo, reply, listcount(output));
		XFREE(MTYPE_CONFD, reply);
	}

exit:
	/* Release memory. */
	list_delete(&input);
	list_delete(&output);

	return ret;
}


static int frr_confd_dp_read(struct confd_daemon_ctx *dctx, int fd)
{
	int ret;

	ret = confd_fd_ready(dctx, fd);
	if (ret == CONFD_EOF) {
		flog_err_confd("confd_fd_ready");
		frr_confd_finish();
		return -1;
	} else if (ret == CONFD_ERR && confd_errno != CONFD_ERR_EXTERNAL) {
		flog_err_confd("confd_fd_ready");
		frr_confd_finish();
		return -1;
	}

	return 0;
}

static void frr_confd_dp_ctl_read(struct event *thread)
{
	struct confd_daemon_ctx *dctx = EVENT_ARG(thread);
	int fd = EVENT_FD(thread);

	event_add_read(master, frr_confd_dp_ctl_read, dctx, fd, &t_dp_ctl);

	frr_confd_dp_read(dctx, fd);
}

static void frr_confd_dp_worker_read(struct event *thread)
{
	struct confd_daemon_ctx *dctx = EVENT_ARG(thread);
	int fd = EVENT_FD(thread);

	event_add_read(master, frr_confd_dp_worker_read, dctx, fd,
		       &t_dp_worker);

	frr_confd_dp_read(dctx, fd);
}

static int frr_confd_subscribe_state(const struct lysc_node *snode, void *arg)
{
	struct nb_node *nb_node = snode->priv;
	struct confd_data_cbs *data_cbs = arg;

	if (!nb_node || !CHECK_FLAG(snode->flags, LYS_CONFIG_R))
		return YANG_ITER_CONTINUE;
	/* We only need to subscribe to the root of the state subtrees. */
	if (snode->parent && CHECK_FLAG(snode->parent->flags, LYS_CONFIG_R))
		return YANG_ITER_CONTINUE;

	DEBUGD(&nb_dbg_client_confd,
	       "%s: providing data to '%s' (callpoint %s)", __func__,
	       nb_node->xpath, snode->name);

	strlcpy(data_cbs->callpoint, snode->name, sizeof(data_cbs->callpoint));
	if (confd_register_data_cb(dctx, data_cbs) != CONFD_OK)
		flog_err_confd("confd_register_data_cb");

	return YANG_ITER_CONTINUE;
}

static int frr_confd_init_dp(const char *program_name)
{
	struct confd_trans_cbs trans_cbs;
	struct confd_data_cbs data_cbs;
	struct confd_notification_stream_cbs ncbs;
	struct confd_action_cbs acbs;

	/* Initialize daemon context. */
	dctx = confd_init_daemon(program_name);
	if (!dctx) {
		flog_err_confd("confd_init_daemon");
		goto error;
	}

	/*
	 * Inform we want to receive YANG values as raw strings, and that we
	 * want to provide only strings in the reply functions, regardless of
	 * the YANG type.
	 */
	confd_set_daemon_flags(dctx, CONFD_DAEMON_FLAG_STRINGSONLY);

	/* Establish a control socket. */
	dp_ctl_sock = socket(PF_INET, SOCK_STREAM, 0);
	if (dp_ctl_sock < 0) {
		flog_err(EC_LIB_SOCKET, "%s: failed to create socket: %s",
			 __func__, safe_strerror(errno));
		goto error;
	}

	if (confd_connect(dctx, dp_ctl_sock, CONTROL_SOCKET, &confd_addr,
			  sizeof(struct sockaddr_in))
	    != CONFD_OK) {
		flog_err_confd("confd_connect");
		goto error;
	}

	/*
	 * Establish a worker socket (only one since this plugin runs on a
	 * single thread).
	 */
	dp_worker_sock = socket(PF_INET, SOCK_STREAM, 0);
	if (dp_worker_sock < 0) {
		flog_err(EC_LIB_SOCKET, "%s: failed to create socket: %s",
			 __func__, safe_strerror(errno));
		goto error;
	}
	if (confd_connect(dctx, dp_worker_sock, WORKER_SOCKET, &confd_addr,
			  sizeof(struct sockaddr_in))
	    != CONFD_OK) {
		flog_err_confd("confd_connect");
		goto error;
	}

	/* Register transaction callback functions. */
	memset(&trans_cbs, 0, sizeof(trans_cbs));
	trans_cbs.init = frr_confd_transaction_init;
	confd_register_trans_cb(dctx, &trans_cbs);

	/* Register our read/write callbacks. */
	memset(&data_cbs, 0, sizeof(data_cbs));
	data_cbs.get_elem = frr_confd_data_get_elem;
	data_cbs.exists_optional = frr_confd_data_get_elem;
	data_cbs.get_next = frr_confd_data_get_next;
	data_cbs.get_object = frr_confd_data_get_object;
	data_cbs.get_next_object = frr_confd_data_get_next_object;

	/*
	 * Iterate over all loaded YANG modules and subscribe to the paths
	 * referent to state data.
	 */
	yang_snodes_iterate(NULL, frr_confd_subscribe_state, 0, &data_cbs);

	/* Register notification stream. */
	memset(&ncbs, 0, sizeof(ncbs));
	ncbs.fd = dp_worker_sock;
	/*
	 * RFC 5277 - Section 3.2.3:
	 * A NETCONF server implementation supporting the notification
	 * capability MUST support the "NETCONF" notification event
	 * stream. This stream contains all NETCONF XML event notifications
	 * supported by the NETCONF server.
	 */
	strlcpy(ncbs.streamname, "NETCONF", sizeof(ncbs.streamname));
	if (confd_register_notification_stream(dctx, &ncbs, &live_ctx)
	    != CONFD_OK) {
		flog_err_confd("confd_register_notification_stream");
		goto error;
	}

	/* Register the action handler callback. */
	memset(&acbs, 0, sizeof(acbs));
	strlcpy(acbs.actionpoint, "actionpoint", sizeof(acbs.actionpoint));
	acbs.init = frr_confd_action_init;
	acbs.action = frr_confd_action_execute;
	if (confd_register_action_cbs(dctx, &acbs) != CONFD_OK) {
		flog_err_confd("confd_register_action_cbs");
		goto error;
	}

	/* Notify we registered all callbacks we wanted. */
	if (confd_register_done(dctx) != CONFD_OK) {
		flog_err_confd("confd_register_done");
		goto error;
	}

	event_add_read(master, frr_confd_dp_ctl_read, dctx, dp_ctl_sock,
		       &t_dp_ctl);
	event_add_read(master, frr_confd_dp_worker_read, dctx, dp_worker_sock,
		       &t_dp_worker);

	return 0;

error:
	frr_confd_finish_dp();

	return -1;
}

static void frr_confd_finish_dp(void)
{
	if (dp_worker_sock > 0) {
		EVENT_OFF(t_dp_worker);
		close(dp_worker_sock);
	}
	if (dp_ctl_sock > 0) {
		EVENT_OFF(t_dp_ctl);
		close(dp_ctl_sock);
	}
	if (dctx != NULL)
		confd_release_daemon(dctx);
}

/* ------------ CLI ------------ */

DEFUN (debug_nb_confd,
       debug_nb_confd_cmd,
       "[no] debug northbound client confd",
       NO_STR
       DEBUG_STR
       "Northbound debugging\n"
       "Client\n"
       "ConfD\n")
{
	uint32_t mode = DEBUG_NODE2MODE(vty->node);
	bool no = strmatch(argv[0]->text, "no");

	DEBUG_MODE_SET(&nb_dbg_client_confd, mode, !no);

	return CMD_SUCCESS;
}

static int frr_confd_debug_config_write(struct vty *vty)
{
	if (DEBUG_MODE_CHECK(&nb_dbg_client_confd, DEBUG_MODE_CONF))
		vty_out(vty, "debug northbound client confd\n");

	return 0;
}

static int frr_confd_debug_set_all(uint32_t flags, bool set)
{
	DEBUG_FLAGS_SET(&nb_dbg_client_confd, flags, set);

	/* If all modes have been turned off, don't preserve options. */
	if (!DEBUG_MODE_CHECK(&nb_dbg_client_confd, DEBUG_MODE_ALL))
		DEBUG_CLEAR(&nb_dbg_client_confd);

	return 0;
}

static void frr_confd_cli_init(void)
{
	hook_register(nb_client_debug_config_write,
		      frr_confd_debug_config_write);
	hook_register(nb_client_debug_set_all, frr_confd_debug_set_all);

	install_element(ENABLE_NODE, &debug_nb_confd_cmd);
	install_element(CONFIG_NODE, &debug_nb_confd_cmd);
}

/* ------------ Main ------------ */

static int frr_confd_calculate_snode_hash(const struct lysc_node *snode,
					  void *arg)
{
	struct nb_node *nb_node = snode->priv;

	if (nb_node)
		nb_node->confd_hash = confd_str2hash(snode->name);

	return YANG_ITER_CONTINUE;
}

static int frr_confd_init(const char *program_name)
{
	struct sockaddr_in *confd_addr4 = (struct sockaddr_in *)&confd_addr;
	int debuglevel = CONFD_SILENT;
	int ret = -1;

	/* Initialize ConfD library. */
	confd_init(program_name, stderr, debuglevel);

	confd_addr4->sin_family = AF_INET;
	confd_addr4->sin_addr.s_addr = inet_addr("127.0.0.1");
	confd_addr4->sin_port = htons(CONFD_PORT);
	if (confd_load_schemas(&confd_addr, sizeof(struct sockaddr_in))
	    != CONFD_OK) {
		flog_err_confd("confd_load_schemas");
		return -1;
	}

	ret = frr_confd_init_cdb();
	if (ret != 0)
		goto error;

	ret = frr_confd_init_dp(program_name);
	if (ret != 0) {
		frr_confd_finish_cdb();
		goto error;
	}

	yang_snodes_iterate(NULL, frr_confd_calculate_snode_hash, 0, NULL);

	hook_register(nb_notification_send, frr_confd_notification_send);

	confd_connected = true;
	return 0;

error:
	confd_free_schemas();

	return ret;
}

static int frr_confd_finish(void)
{
	if (!confd_connected)
		return 0;

	frr_confd_finish_cdb();
	frr_confd_finish_dp();

	confd_free_schemas();

	confd_connected = false;

	return 0;
}

static int frr_confd_module_late_init(struct event_loop *tm)
{
	master = tm;

	if (frr_confd_init(frr_get_progname()) < 0) {
		flog_err(EC_LIB_CONFD_INIT,
			 "failed to initialize the ConfD module");
		return -1;
	}

	hook_register(frr_fini, frr_confd_finish);
	frr_confd_cli_init();

	return 0;
}

static int frr_confd_module_init(void)
{
	hook_register(frr_late_init, frr_confd_module_late_init);

	return 0;
}

FRR_MODULE_SETUP(.name = "frr_confd", .version = FRR_VERSION,
		 .description = "FRR ConfD integration module",
		 .init = frr_confd_module_init,
);
