/*  Copyright (C) CZ.NIC, z.s.p.o. and contributors
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  For more information, see <https://www.knot-dns.cz/>
 */

#include <assert.h>

#include "knot/common/log.h"
#include "knot/conf/confdb.h"
#include "knot/conf/confio.h"
#include "knot/conf/module.h"
#include "knot/conf/tools.h"

#define FCN(io)	(io->fcn != NULL) ? io->fcn(io) : KNOT_EOK;

static void io_reset_val(
	conf_io_t *io,
	const yp_item_t *key0,
	const yp_item_t *key1,
	const uint8_t *id,
	size_t id_len,
	bool id_as_data,
	conf_val_t *val)
{
	io->key0 = key0;
	io->key1 = key1;
	io->id = id;
	io->id_len = id_len;
	io->id_as_data = id_as_data;
	io->data.val = val;
	io->data.bin = NULL;
}

static void io_reset_bin(
	conf_io_t *io,
	const yp_item_t *key0,
	const yp_item_t *key1,
	const uint8_t *id,
	size_t id_len,
	const uint8_t *bin,
	size_t bin_len)
{
	io_reset_val(io, key0, key1, id, id_len, false, NULL);
	io->data.bin = bin;
	io->data.bin_len = bin_len;
}

static bool same_thread(void)
{
	return conf()->io.txn == NULL ||
	       pthread_equal(conf()->io.thread_id, pthread_self()) != 0;
}
#define CHECK_SAME_THREAD if (!same_thread()) return KNOT_TXN_ETHREAD;

int conf_io_begin(
	bool child)
{
	assert(conf() != NULL);

	if (conf()->io.txn != NULL && !child) {
		return KNOT_TXN_EEXISTS;
	} else if (conf()->io.txn == NULL && child) {
		return KNOT_TXN_ENOTEXISTS;
	}
	CHECK_SAME_THREAD;

	knot_db_txn_t *parent = conf()->io.txn;
	knot_db_txn_t *txn = (parent == NULL) ? conf()->io.txn_stack : parent + 1;
	if (txn >= conf()->io.txn_stack + CONF_MAX_TXN_DEPTH) {
		return KNOT_TXN_EEXISTS;
	}

	if (conf()->filename != NULL && !child) {
		log_ctl_notice("control, persistent configuration database "
		               "not available");
	}

	// Start the writing transaction.
	int ret = knot_db_lmdb_txn_begin(conf()->db, txn, parent, 0);
	if (ret != KNOT_EOK) {
		return ret;
	}

	conf()->io.txn = txn;
	conf()->io.thread_id = pthread_self();

	// Reset master transaction flags.
	if (!child) {
		conf()->io.flags = CONF_IO_FACTIVE;
		if (conf()->io.zones != NULL) {
			trie_clear(conf()->io.zones);
		}
	}

	return KNOT_EOK;
}

int conf_io_commit(
	bool child)
{
	assert(conf() != NULL);

	if (conf()->io.txn == NULL ||
	    (child && conf()->io.txn == conf()->io.txn_stack)) {
		return KNOT_TXN_ENOTEXISTS;
	}
	CHECK_SAME_THREAD;

	knot_db_txn_t *txn = child ? conf()->io.txn : conf()->io.txn_stack;

	// Commit the writing transaction.
	int ret = conf()->api->txn_commit(txn);

	conf()->io.txn = child ? txn - 1 : NULL;

	return ret;
}

int conf_io_abort(
	bool child)
{
	assert(conf() != NULL);

	if (conf()->io.txn == NULL ||
	    (child && conf()->io.txn == conf()->io.txn_stack)) {
		return KNOT_EOK;
	}
	if (!same_thread()) {
		return KNOT_TXN_ETHREAD;
	}

	knot_db_txn_t *txn = child ? conf()->io.txn : conf()->io.txn_stack;

	// Abort the writing transaction.
	conf()->api->txn_abort(txn);
	conf()->io.txn = child ? txn - 1 : NULL;

	// Reset master transaction flags.
	if (!child) {
		conf()->io.flags = YP_FNONE;
		if (conf()->io.zones != NULL) {
			trie_clear(conf()->io.zones);
		}
	}

	return KNOT_EOK;
}

static int list_section(
	const yp_item_t *items,
	const yp_item_t **item,
	conf_io_t *io)
{
	for (*item = items; (*item)->name != NULL; (*item)++) {
		int ret = FCN(io);
		if (ret != KNOT_EOK) {
			return ret;
		}
	}

	return KNOT_EOK;
}

int conf_io_list(
	const char *key0,
	const char *key1,
	const char *id,
	bool list_schema,
	bool get_current,
	conf_io_t *io)
{
	if (io == NULL) {
		return KNOT_EINVAL;
	}

	assert(conf() != NULL);

	if (conf()->io.txn == NULL && !get_current) {
		return KNOT_TXN_ENOTEXISTS;
	}
	CHECK_SAME_THREAD;

	// List schema sections by default.
	if (key0 == NULL) {
		io_reset_val(io, NULL, NULL, NULL, 0, false, NULL);

		return list_section(conf()->schema, &io->key0, io);
	}

	yp_check_ctx_t *ctx = yp_schema_check_init(&conf()->schema);
	if (ctx == NULL) {
		return KNOT_ENOMEM;
	}

	// Check the input.
	int ret = yp_schema_check_str(ctx, key0, key1, id, NULL);
	if (ret != KNOT_EOK) {
		goto list_error;
	}

	knot_db_txn_t *txn = get_current ? &conf()->read_txn : conf()->io.txn;

	yp_node_t *node = &ctx->nodes[ctx->current];
	yp_node_t *parent = node->parent;

	// List group items.
	if (list_schema && key1 == NULL) {
		if (node->item->type != YP_TGRP) { // Ignore non-group section.
			ret = KNOT_EOK;
			goto list_error;
		}

		io_reset_val(io, node->item, NULL, NULL, 0, false, NULL);

		ret = list_section(node->item->sub_items, &io->key1, io);
	// List option item values.
	} else if (list_schema) {
		if (node->item->type != YP_TOPT) { // Ignore non-option item.
			ret = KNOT_EOK;
			goto list_error;
		}

		for (const knot_lookup_t *o = node->item->var.o.opts; o->name != NULL; o++) {
			uint8_t val = o->id;
			io_reset_bin(io, parent->item, node->item, parent->id,
			             parent->id_len, &val, sizeof(val));
			ret = FCN(io);
			if (ret != KNOT_EOK) {
				goto list_error;
			}
		}
	// List group identifiers.
	} else if (parent == NULL) {
		if (node->item->type != YP_TGRP) { // Ignore non-group section.
			ret = KNOT_EOK;
			goto list_error;
		}

		// If key1 != NULL, it's used for a value completion (zone.domain).
		io_reset_val(io, node->item, NULL, NULL, 0, key1 != NULL, NULL);

		conf_iter_t iter;
		ret = conf_db_iter_begin(conf(), txn, io->key0->name, &iter);
		switch (ret) {
		case KNOT_EOK:
			break;
		case KNOT_ENOENT:
			ret = KNOT_EOK;
			goto list_error;
		default:
			goto list_error;
		}

		while (ret == KNOT_EOK) {
			// Set the section identifier.
			ret = conf_db_iter_id(conf(), &iter, &io->id, &io->id_len);
			if (ret != KNOT_EOK) {
				conf_db_iter_finish(conf(), &iter);
				goto list_error;
			}

			ret = FCN(io);
			if (ret != KNOT_EOK) {
				conf_db_iter_finish(conf(), &iter);
				goto list_error;
			}

			ret = conf_db_iter_next(conf(), &iter);
		}
		ret = KNOT_EOK;
	// List item values.
	} else {
		io_reset_val(io, parent->item, node->item, parent->id,
		             parent->id_len, false, NULL);

		// Get the item value.
		conf_val_t data;
		ret = conf_db_get(conf(), txn, io->key0->name, io->key1->name,
		                  io->id, io->id_len, &data);
		switch (ret) {
		case KNOT_EOK:
			break;
		case KNOT_ENOENT:
			ret = KNOT_EOK;
			goto list_error;
		default:
			goto list_error;
		}

		io->data.val = &data;

		ret = FCN(io);
		if (ret != KNOT_EOK) {
			goto list_error;
		}
	}
list_error:
	yp_schema_check_deinit(ctx);

	return ret;
}

static int diff_item(
	conf_io_t *io)
{
	// Process an identifier item.
	if ((io->key0->flags & YP_FMULTI) != 0 && io->key0->var.g.id == io->key1) {
		bool old_id, new_id;

		// Check if a removed identifier.
		int ret = conf_db_get(conf(), &conf()->read_txn, io->key0->name,
		                      NULL, io->id, io->id_len, NULL);
		switch (ret) {
		case KNOT_EOK:
			old_id = true;
			break;
		case KNOT_ENOENT:
		case KNOT_YP_EINVAL_ID:
			old_id = false;
			break;
		default:
			return ret;
		}

		// Check if an added identifier.
		ret = conf_db_get(conf(), conf()->io.txn, io->key0->name, NULL,
		                  io->id, io->id_len, NULL);
		switch (ret) {
		case KNOT_EOK:
			new_id = true;
			break;
		case KNOT_ENOENT:
		case KNOT_YP_EINVAL_ID:
			new_id = false;
			break;
		default:
			return ret;
		}

		// Check if valid identifier.
		if (!old_id && !new_id) {
			return KNOT_YP_EINVAL_ID;
		}

		if (old_id != new_id) {
			io->id_as_data = true;
			io->type = old_id ? OLD : NEW;

			// Process the callback.
			ret = FCN(io);

			// Reset the modified parameters.
			io->id_as_data = false;
			io->type = NONE;

			if (ret != KNOT_EOK) {
				return ret;
			}
		}

		return KNOT_EOK;
	}

	conf_val_t old_val, new_val;

	// Get the old item value.
	conf_db_get(conf(), &conf()->read_txn, io->key0->name, io->key1->name,
	            io->id, io->id_len, &old_val);
	switch (old_val.code) {
	case KNOT_EOK:
		break;
	case KNOT_ENOENT:
	case KNOT_YP_EINVAL_ID:
		break;
	default:
		return old_val.code;
	}

	// Get the new item value.
	conf_db_get(conf(), conf()->io.txn, io->key0->name, io->key1->name,
	            io->id, io->id_len, &new_val);
	switch (new_val.code) {
	case KNOT_EOK:
		break;
	case KNOT_ENOENT:
	case KNOT_YP_EINVAL_ID:
		if (old_val.code != KNOT_EOK) {
			return KNOT_EOK;
		}
		break;
	default:
		return new_val.code;
	}

	// Process the value difference.
	if (old_val.code != KNOT_EOK) {
		io->data.val = &new_val;
		io->type = NEW;
		int ret = FCN(io);
		if (ret != KNOT_EOK) {
			return ret;
		}
	} else if (new_val.code != KNOT_EOK) {
		io->data.val = &old_val;
		io->type = OLD;
		int ret = FCN(io);
		if (ret != KNOT_EOK) {
			return ret;
		}
	} else if (!conf_val_equal(&old_val, &new_val)) {
		io->data.val = &old_val;
		io->type = OLD;
		int ret = FCN(io);
		if (ret != KNOT_EOK) {
			return ret;
		}

		io->data.val = &new_val;
		io->type = NEW;
		ret = FCN(io);
		if (ret != KNOT_EOK) {
			return ret;
		}
	}

	// Reset the modified parameters.
	io->data.val = NULL;
	io->type = NONE;

	return KNOT_EOK;
}

static int diff_section(
	conf_io_t *io)
{
	// Get the value for the specified item.
	if (io->key1 != NULL) {
		return diff_item(io);
	}

	// Get the values for all items.
	for (yp_item_t *i = io->key0->sub_items; i->name != NULL; i++) {
		io->key1 = i;

		int ret = diff_item(io);

		// Reset the modified parameters.
		io->key1 = NULL;

		if (ret != KNOT_EOK) {
			return ret;
		}
	}

	return KNOT_EOK;
}

static int diff_iter_section(
	conf_io_t *io)
{
	// First compare the section with the old and common identifiers.
	conf_iter_t iter;
	int ret = conf_db_iter_begin(conf(), &conf()->read_txn, io->key0->name,
	                             &iter);
	switch (ret) {
	case KNOT_EOK:
		break;
	case KNOT_ENOENT:
		// Continue to the second step.
		ret = KNOT_EOF;
		break;
	default:
		return ret;
	}

	while (ret == KNOT_EOK) {
		ret = conf_db_iter_id(conf(), &iter, &io->id, &io->id_len);
		if (ret != KNOT_EOK) {
			conf_db_iter_finish(conf(), &iter);
			return ret;
		}

		ret = diff_section(io);
		if (ret != KNOT_EOK) {
			conf_db_iter_finish(conf(), &iter);
			return ret;
		}

		ret = conf_db_iter_next(conf(), &iter);
	}
	if (ret != KNOT_EOF) {
		return ret;
	}

	// Second compare the section with the new identifiers.
	ret = conf_db_iter_begin(conf(), conf()->io.txn, io->key0->name, &iter);
	switch (ret) {
	case KNOT_EOK:
		break;
	case KNOT_ENOENT:
		return KNOT_EOK;
	default:
		return ret;
	}

	while (ret == KNOT_EOK) {
		ret = conf_db_iter_id(conf(), &iter, &io->id, &io->id_len);
		if (ret != KNOT_EOK) {
			conf_db_iter_finish(conf(), &iter);
			return ret;
		}

		// Ignore old and common identifiers.
		ret = conf_db_get(conf(), &conf()->read_txn, io->key0->name,
		                  NULL, io->id, io->id_len, NULL);
		switch (ret) {
		case KNOT_EOK:
			ret = conf_db_iter_next(conf(), &iter);
			continue;
		case KNOT_ENOENT:
		case KNOT_YP_EINVAL_ID:
			break;
		default:
			conf_db_iter_finish(conf(), &iter);
			return ret;
		}

		ret = diff_section(io);
		if (ret != KNOT_EOK) {
			conf_db_iter_finish(conf(), &iter);
			return ret;
		}

		ret = conf_db_iter_next(conf(), &iter);
	}
	if (ret != KNOT_EOF) {
		return ret;
	}

	return KNOT_EOK;
}

static int diff_zone_section(
	conf_io_t *io)
{
	assert(io->key0->flags & CONF_IO_FZONE);

	if (conf()->io.zones == NULL) {
		return KNOT_EOK;
	}

	trie_it_t *it = trie_it_begin(conf()->io.zones);
	for (; !trie_it_finished(it); trie_it_next(it)) {
		io->id = (const uint8_t *)trie_it_key(it, &io->id_len);

		// Get the difference for specific zone.
		int ret = diff_section(io);
		if (ret != KNOT_EOK) {
			trie_it_free(it);
			return ret;
		}
	}
	trie_it_free(it);

	return KNOT_EOK;
}

int conf_io_diff(
	const char *key0,
	const char *key1,
	const char *id,
	conf_io_t *io)
{
	if (io == NULL) {
		return KNOT_EINVAL;
	}

	assert(conf() != NULL);

	if (conf()->io.txn == NULL) {
		return KNOT_TXN_ENOTEXISTS;
	}
	CHECK_SAME_THREAD;

	// Compare all sections by default.
	if (key0 == NULL) {
		for (yp_item_t *i = conf()->schema; i->name != NULL; i++) {
			// Skip non-group item.
			if (i->type != YP_TGRP) {
				continue;
			}

			int ret = conf_io_diff(i->name + 1, key1, NULL, io);

			// Reset parameters after each section.
			io_reset_val(io, NULL, NULL, NULL, 0, false, NULL);

			if (ret != KNOT_EOK) {
				return ret;
			}
		}

		return KNOT_EOK;
	}

	yp_check_ctx_t *ctx = yp_schema_check_init(&conf()->schema);
	if (ctx == NULL) {
		return KNOT_ENOMEM;
	}

	// Check the input.
	int ret = yp_schema_check_str(ctx, key0, key1, id, NULL);
	if (ret != KNOT_EOK) {
		goto diff_error;
	}

	yp_node_t *node = &ctx->nodes[ctx->current];
	yp_node_t *parent = node->parent;

	// Key1 is not a group identifier.
	if (parent != NULL) {
		io_reset_val(io, parent->item, node->item, parent->id,
		             parent->id_len, false, NULL);
	// Key1 is a group identifier.
	} else if (key1 != NULL && strlen(key1) != 0) {
		assert(node->item->type == YP_TGRP &&
		       (node->item->flags & YP_FMULTI) != 0);

		io_reset_val(io, node->item, node->item->var.g.id, node->id,
		             node->id_len, true, NULL);
	// No key1 specified.
	} else {
		io_reset_val(io, node->item, NULL, node->id, node->id_len,
		             false, NULL);
	}

	// Check for a non-group item.
	if (io->key0->type != YP_TGRP) {
		ret = KNOT_ENOTSUP;
		goto diff_error;
	}

	// Compare the section with all identifiers by default.
	if ((io->key0->flags & YP_FMULTI) != 0 && io->id_len == 0) {
		// The zone section has an optimized diff.
		if (io->key0->flags & CONF_IO_FZONE) {
			// Full diff by default.
			if (!(conf()->io.flags & CONF_IO_FACTIVE)) {
				ret = diff_iter_section(io);
			// Full diff if all zones changed.
			} else if (conf()->io.flags & CONF_IO_FDIFF_ZONES) {
				ret = diff_iter_section(io);
			// Optimized diff for specific zones.
			} else {
				ret = diff_zone_section(io);
			}
		} else {
			ret = diff_iter_section(io);
		}

		goto diff_error;
	}

	// Compare the section with a possible identifier.
	ret = diff_section(io);
diff_error:
	yp_schema_check_deinit(ctx);

	return ret;
}

static int get_section(
	knot_db_txn_t *txn,
	conf_io_t *io)
{
	conf_val_t data;

	// Get the value for the specified item.
	if (io->key1 != NULL) {
		if (!io->id_as_data) {
			// Get the item value.
			conf_db_get(conf(), txn, io->key0->name, io->key1->name,
			            io->id, io->id_len, &data);
			switch (data.code) {
			case KNOT_EOK:
				break;
			case KNOT_ENOENT:
				return KNOT_EOK;
			default:
				return data.code;
			}

			io->data.val = &data;
		}

		// Process the callback.
		int ret = FCN(io);

		// Reset the modified parameters.
		io->data.val = NULL;

		return ret;
	}

	// Get the values for all section items by default.
	for (yp_item_t *i = io->key0->sub_items; i->name != NULL; i++) {
		// Process the (first) identifier item.
		if ((io->key0->flags & YP_FMULTI) != 0 && io->key0->var.g.id == i) {
			// Check if existing identifier.
			conf_db_get(conf(), txn, io->key0->name, NULL, io->id,
			            io->id_len, &data);
			switch (data.code) {
			case KNOT_EOK:
				break;
			case KNOT_ENOENT:
				continue;
			default:
				return data.code;
			}

			io->key1 = i;
			io->id_as_data = true;

			// Process the callback.
			int ret = FCN(io);

			// Reset the modified parameters.
			io->key1 = NULL;
			io->id_as_data = false;

			if (ret != KNOT_EOK) {
				return ret;
			}

			continue;
		}

		// Get the item value.
		conf_db_get(conf(), txn, io->key0->name, i->name, io->id,
		            io->id_len, &data);
		switch (data.code) {
		case KNOT_EOK:
			break;
		case KNOT_ENOENT:
			continue;
		default:
			return data.code;
		}

		io->key1 = i;
		io->data.val = &data;

		// Process the callback.
		int ret = FCN(io);

		// Reset the modified parameters.
		io->key1 = NULL;
		io->data.val = NULL;

		if (ret != KNOT_EOK) {
			return ret;
		}
	}

	return KNOT_EOK;
}

int conf_io_get(
	const char *key0,
	const char *key1,
	const char *id,
	bool get_current,
	conf_io_t *io)
{
	if (io == NULL) {
		return KNOT_EINVAL;
	}

	assert(conf() != NULL);

	if (conf()->io.txn == NULL && !get_current) {
		return KNOT_TXN_ENOTEXISTS;
	}
	CHECK_SAME_THREAD;

	// List all sections by default.
	if (key0 == NULL) {
		for (yp_item_t *i = conf()->schema; i->name != NULL; i++) {
			// Skip non-group item.
			if (i->type != YP_TGRP) {
				continue;
			}

			int ret = conf_io_get(i->name + 1, key1, NULL,
			                      get_current, io);
			// Reset parameters after each section.
			io_reset_val(io, NULL, NULL, NULL, 0, false, NULL);
			if (ret != KNOT_EOK) {
				return ret;
			}
		}

		return KNOT_EOK;
	}

	yp_check_ctx_t *ctx = yp_schema_check_init(&conf()->schema);
	if (ctx == NULL) {
		return KNOT_ENOMEM;
	}

	// Check the input.
	int ret = yp_schema_check_str(ctx, key0, key1, id, NULL);
	if (ret != KNOT_EOK) {
		goto get_error;
	}

	yp_node_t *node = &ctx->nodes[ctx->current];
	yp_node_t *parent = node->parent;

	// Key1 is not a group identifier.
	if (parent != NULL) {
		io_reset_val(io, parent->item, node->item, parent->id,
		             parent->id_len, false, NULL);
	// Key1 is a group identifier.
	} else if (key1 != NULL && strlen(key1) != 0) {
		assert(node->item->type == YP_TGRP &&
		       (node->item->flags & YP_FMULTI) != 0);

		io_reset_val(io, node->item, node->item->var.g.id, node->id,
		             node->id_len, true, NULL);
	// No key1 specified.
	} else {
		io_reset_val(io, node->item, NULL, node->id, node->id_len, false,
		             NULL);
	}

	knot_db_txn_t *txn = get_current ? &conf()->read_txn : conf()->io.txn;

	// Check for a non-group item.
	if (io->key0->type != YP_TGRP) {
		ret = KNOT_ENOTSUP;
		goto get_error;
	}

	// List the section with all identifiers by default.
	if ((io->key0->flags & YP_FMULTI) != 0 && io->id_len == 0) {
		conf_iter_t iter;
		ret = conf_db_iter_begin(conf(), txn, io->key0->name, &iter);
		switch (ret) {
		case KNOT_EOK:
			break;
		case KNOT_ENOENT:
			ret = KNOT_EOK;
			goto get_error;
		default:
			goto get_error;
		}

		while (ret == KNOT_EOK) {
			// Set the section identifier.
			ret = conf_db_iter_id(conf(), &iter, &io->id, &io->id_len);
			if (ret != KNOT_EOK) {
				conf_db_iter_finish(conf(), &iter);
				goto get_error;
			}

			ret = get_section(txn, io);
			if (ret != KNOT_EOK) {
				conf_db_iter_finish(conf(), &iter);
				goto get_error;
			}

			ret = conf_db_iter_next(conf(), &iter);
		}

		ret = KNOT_EOK;
		goto get_error;
	}

	// List the section with a possible identifier.
	ret = get_section(txn, io);
get_error:
	yp_schema_check_deinit(ctx);

	return ret;
}

static void upd_changes(
	const conf_io_t *io,
	conf_io_type_t type,
	yp_flag_t flags,
	bool any_id)
{
	// Update common flags.
	conf()->io.flags |= flags;

	// Return if not important change.
	if (type == CONF_IO_TNONE) {
		return;
	}

	// Update reference item.
	if (flags & CONF_IO_FREF) {
		// Expected an identifier, which cannot be changed.
		assert(type != CONF_IO_TCHANGE);

		// Re-check and reload all zones if a reference has been removed.
		if (type == CONF_IO_TUNSET) {
			conf()->io.flags |= CONF_IO_FCHECK_ZONES | CONF_IO_FRLD_ZONES;
		}
		return;
	// Return if no specific zone operation.
	} else if (!(flags & CONF_IO_FZONE)) {
		return;
	}

	// Don't process each zone individually, process all instead.
	if (any_id) {
		// Diff all zone changes.
		conf()->io.flags |= CONF_IO_FCHECK_ZONES | CONF_IO_FDIFF_ZONES;

		// Reload just with important changes.
		if (flags & CONF_IO_FRLD_ZONE) {
			conf()->io.flags |= CONF_IO_FRLD_ZONES;
		}
		return;
	}

	// Prepare zone changes storage if it doesn't exist.
	trie_t *zones = conf()->io.zones;
	if (zones == NULL) {
		zones = trie_create(NULL);
		if (zones == NULL) {
			return;
		}
		conf()->io.zones = zones;
	}

	// Get zone status or create new.
	trie_val_t *val = trie_get_ins(zones, io->id, io->id_len);
	conf_io_type_t *current = (conf_io_type_t *)val;

	switch (type) {
	case CONF_IO_TSET:
		// Revert remove zone, but don't remove (probably changed).
		if (*current & CONF_IO_TUNSET) {
			*current &= ~CONF_IO_TUNSET;
		} else {
			// Must be a new zone.
			assert(*current == CONF_IO_TNONE);
			// Mark added zone.
			*current = type;
		}
		break;
	case CONF_IO_TUNSET:
		if (*current & CONF_IO_TSET) {
			// Remove inserted zone -> no change.
			trie_del(zones, io->id, io->id_len, NULL);
		} else {
			// Remove existing zone.
			*current |= type;
		}
		break;
	case CONF_IO_TCHANGE:
		*current |= type;
		// Mark zone to reload if required.
		if (flags & CONF_IO_FRLD_ZONE) {
			*current |= CONF_IO_TRELOAD;
		}
		break;
	case CONF_IO_TRELOAD:
	default:
		assert(0);
	}
}

static int set_item(
	conf_io_t *io)
{
	int ret = conf_db_set(conf(), conf()->io.txn, io->key0->name,
	                      (io->key1 != NULL) ? io->key1->name : NULL,
	                      io->id, io->id_len, io->data.bin, io->data.bin_len);
	if (ret != KNOT_EOK) {
		return ret;
	}

	// Postpone group callbacks to config check.
	if (io->key0->type == YP_TGRP && io->id_len == 0) {
		return KNOT_EOK;
	}

	knotd_conf_check_extra_t extra = {
		.conf = conf(),
		.txn = conf()->io.txn
	};
	knotd_conf_check_args_t args = {
		.item = (io->key1 != NULL) ? io->key1 :
		         ((io->id_len == 0) ? io->key0 : io->key0->var.g.id),
		.id = io->id,
		.id_len = io->id_len,
		.data = io->data.bin,
		.data_len = io->data.bin_len,
		.extra = &extra
	};

	// Call the item callbacks (include, item check, mod-id check).
	ret = conf_exec_callbacks(&args);
	if (ret != KNOT_EOK) {
		CONF_LOG(LOG_DEBUG, "item '%s' (%s)", args.item->name + 1,
		         args.err_str != NULL ? args.err_str : knot_strerror(ret));
	}

	return ret;
}

int conf_io_set(
	const char *key0,
	const char *key1,
	const char *id,
	const char *data)
{
	assert(conf() != NULL);

	if (conf()->io.txn == NULL) {
		return KNOT_TXN_ENOTEXISTS;
	}
	CHECK_SAME_THREAD;

	// At least key0 must be specified.
	if (key0 == NULL) {
		return KNOT_EINVAL;
	}

	yp_check_ctx_t *ctx = yp_schema_check_init(&conf()->schema);
	if (ctx == NULL) {
		return KNOT_ENOMEM;
	}

	// Check the input.
	int ret = yp_schema_check_str(ctx, key0, key1, id, data);
	if (ret != KNOT_EOK) {
		goto set_error;
	}

	yp_node_t *node = &ctx->nodes[ctx->current];
	yp_node_t *parent = node->parent;

	yp_flag_t upd_flags = node->item->flags;
	conf_io_type_t upd_type = CONF_IO_TNONE;

	conf_io_t io = { NULL };

	// Key1 is not a group identifier.
	if (parent != NULL) {
		if (node->data_len == 0) {
			ret = KNOT_YP_ENODATA;
			goto set_error;
		}
		upd_type = CONF_IO_TCHANGE;
		upd_flags |= parent->item->flags;
		io_reset_bin(&io, parent->item, node->item, parent->id,
		             parent->id_len, node->data, node->data_len);
	// A group identifier or whole group.
	} else if (node->item->type == YP_TGRP) {
		upd_type = CONF_IO_TSET;
		if ((node->item->flags & YP_FMULTI) != 0) {
			if (node->id_len == 0) {
				ret = KNOT_YP_ENOID;
				goto set_error;
			}
			upd_flags |= node->item->var.g.id->flags;
		} else {
			ret = KNOT_ENOTSUP;
			goto set_error;
		}
		assert(node->data_len == 0);
		io_reset_bin(&io, node->item, NULL, node->id, node->id_len,
		             NULL, 0);
	// A non-group item with data (include).
	} else if (node->data_len > 0) {
		io_reset_bin(&io, node->item, NULL, NULL, 0, node->data,
		             node->data_len);
	} else {
		ret = KNOT_YP_ENODATA;
		goto set_error;
	}

	// Set the item for all identifiers by default.
	if (io.key0->type == YP_TGRP && io.key1 != NULL &&
	    (io.key0->flags & YP_FMULTI) != 0 && io.id_len == 0) {
		conf_iter_t iter;
		ret = conf_db_iter_begin(conf(), conf()->io.txn, io.key0->name,
		                         &iter);
		switch (ret) {
		case KNOT_EOK:
			break;
		case KNOT_ENOENT:
			ret = KNOT_EOK;
			goto set_error;
		default:
			goto set_error;
		}

		uint8_t copied_id[YP_MAX_ID_LEN];
		io.id = copied_id;
		while (ret == KNOT_EOK) {
			// Get the identifier and copy it because of next DB update.
			const uint8_t *tmp_id;
			ret = conf_db_iter_id(conf(), &iter, &tmp_id, &io.id_len);
			if (ret != KNOT_EOK) {
				conf_db_iter_finish(conf(), &iter);
				goto set_error;
			}
			memcpy(copied_id, tmp_id, io.id_len);

			// Set the data.
			ret = set_item(&io);
			if (ret != KNOT_EOK) {
				conf_db_iter_finish(conf(), &iter);
				goto set_error;
			}

			ret = conf_db_iter_next(conf(), &iter);
		}
		if (ret != KNOT_EOF) {
			goto set_error;
		}

		upd_changes(&io, upd_type, upd_flags, true);

		ret = KNOT_EOK;
		goto set_error;
	}

	// Set the item with a possible identifier.
	ret = set_item(&io);

	if (ret == KNOT_EOK) {
		upd_changes(&io, upd_type, upd_flags, false);
	}
set_error:
	yp_schema_check_deinit(ctx);

	return ret;
}

static int unset_section_data(
	conf_io_t *io)
{
	// Unset the value for the specified item.
	if (io->key1 != NULL) {
		return conf_db_unset(conf(), conf()->io.txn, io->key0->name,
		                     io->key1->name, io->id, io->id_len,
		                     io->data.bin, io->data.bin_len, false);
	}

	// Unset the whole section by default.
	for (yp_item_t *i = io->key0->sub_items; i->name != NULL; i++) {
		// Skip the identifier item.
		if ((io->key0->flags & YP_FMULTI) != 0 && io->key0->var.g.id == i) {
			continue;
		}

		int ret = conf_db_unset(conf(), conf()->io.txn, io->key0->name,
		                        i->name, io->id, io->id_len, io->data.bin,
		                        io->data.bin_len, false);
		switch (ret) {
		case KNOT_EOK:
		case KNOT_ENOENT:
			continue;
		default:
			return ret;
		}
	}

	return KNOT_EOK;
}

static int unset_section(
	const yp_item_t *key0)
{
	// Unset the section items.
	for (yp_item_t *i = key0->sub_items; i->name != NULL; i++) {
		// Skip the identifier item.
		if ((key0->flags & YP_FMULTI) != 0 && key0->var.g.id == i) {
			continue;
		}

		int ret = conf_db_unset(conf(), conf()->io.txn, key0->name,
		                        i->name, NULL, 0, NULL, 0, true);
		switch (ret) {
		case KNOT_EOK:
		case KNOT_ENOENT:
			continue;
		default:
			return ret;
		}
	}

	// Unset the section.
	int ret = conf_db_unset(conf(), conf()->io.txn, key0->name, NULL, NULL,
	                        0, NULL, 0, false);
	switch (ret) {
	case KNOT_EOK:
	case KNOT_ENOENT:
		return KNOT_EOK;
	default:
		return ret;
	}
}

int conf_io_unset(
	const char *key0,
	const char *key1,
	const char *id,
	const char *data)
{
	assert(conf() != NULL);

	if (conf()->io.txn == NULL) {
		return KNOT_TXN_ENOTEXISTS;
	}
	CHECK_SAME_THREAD;

	// Unset all sections by default.
	if (key0 == NULL) {
		for (yp_item_t *i = conf()->schema; i->name != NULL; i++) {
			// Skip non-group item.
			if (i->type != YP_TGRP) {
				continue;
			}

			int ret = conf_io_unset(i->name + 1, key1, NULL, NULL);
			switch (ret) {
			case KNOT_EOK:
			case KNOT_ENOENT:
				break;
			default:
				return ret;
			}
		}

		return KNOT_EOK;
	}

	yp_check_ctx_t *ctx = yp_schema_check_init(&conf()->schema);
	if (ctx == NULL) {
		return KNOT_ENOMEM;
	}

	// Check the input.
	int ret = yp_schema_check_str(ctx, key0, key1, id, data);
	if (ret != KNOT_EOK) {
		goto unset_error;
	}

	yp_node_t *node = &ctx->nodes[ctx->current];
	yp_node_t *parent = node->parent;

	yp_flag_t upd_flags = node->item->flags;
	conf_io_type_t upd_type = CONF_IO_TNONE;

	conf_io_t io = { NULL };

	// Key1 is not a group identifier.
	if (parent != NULL) {
		upd_type = CONF_IO_TCHANGE;
		upd_flags |= parent->item->flags;
		io_reset_bin(&io, parent->item, node->item, parent->id,
		             parent->id_len, node->data, node->data_len);
	// A group identifier or whole group.
	} else if (node->item->type == YP_TGRP) {
		upd_type = CONF_IO_TUNSET;
		if ((node->item->flags & YP_FMULTI) != 0) {
			upd_flags |= node->item->var.g.id->flags;
		}
		assert(node->data_len == 0);
		io_reset_bin(&io, node->item, NULL, node->id, node->id_len,
		             NULL, 0);
	// A non-group item (include).
	} else {
		ret = KNOT_ENOTSUP;
		goto unset_error;
	}

	// Unset the section with all identifiers by default.
	if ((io.key0->flags & YP_FMULTI) != 0 && io.id_len == 0) {
		conf_iter_t iter;
		ret = conf_db_iter_begin(conf(), conf()->io.txn, io.key0->name,
		                         &iter);
		switch (ret) {
		case KNOT_EOK:
			break;
		case KNOT_ENOENT:
			ret = KNOT_EOK;
			goto unset_error;
		default:
			goto unset_error;
		}

		uint8_t copied_id[YP_MAX_ID_LEN];
		io.id = copied_id;
		while (ret == KNOT_EOK) {
			// Get the identifier and copy it because of next DB update.
			const uint8_t *tmp_id;
			ret = conf_db_iter_id(conf(), &iter, &tmp_id, &io.id_len);
			if (ret != KNOT_EOK) {
				conf_db_iter_finish(conf(), &iter);
				goto unset_error;
			}
			memcpy(copied_id, tmp_id, io.id_len);

			// Unset the section data.
			ret = unset_section_data(&io);
			switch (ret) {
			case KNOT_EOK:
			case KNOT_ENOENT:
				break;
			default:
				conf_db_iter_finish(conf(), &iter);
				goto unset_error;
			}

			ret = conf_db_iter_next(conf(), &iter);
		}
		if (ret != KNOT_EOF) {
			goto unset_error;
		}

		if (io.key1 == NULL) {
			// Unset all identifiers.
			ret = conf_db_iter_begin(conf(), conf()->io.txn,
			                         io.key0->name, &iter);
			switch (ret) {
			case KNOT_EOK:
				break;
			case KNOT_ENOENT:
				ret = KNOT_EOK;
				goto unset_error;
			default:
				goto unset_error;
			}

			while (ret == KNOT_EOK) {
				ret = conf_db_iter_del(conf(), &iter);
				if (ret != KNOT_EOK) {
					conf_db_iter_finish(conf(), &iter);
					goto unset_error;
				}

				ret = conf_db_iter_next(conf(), &iter);
			}
			if (ret != KNOT_EOF) {
				goto unset_error;
			}

			// Unset the section.
			ret = unset_section(io.key0);
			if (ret != KNOT_EOK) {
				goto unset_error;
			}
		}

		upd_changes(&io, upd_type, upd_flags, true);

		ret = KNOT_EOK;
		goto unset_error;
	}

	// Unset the section data.
	ret = unset_section_data(&io);
	if (ret != KNOT_EOK) {
		goto unset_error;
	}

	if (io.key1 == NULL) {
		// Unset the identifier.
		if (io.id_len != 0) {
			ret = conf_db_unset(conf(), conf()->io.txn, io.key0->name,
			                    NULL, io.id, io.id_len, NULL, 0, false);
			if (ret != KNOT_EOK) {
				goto unset_error;
			}
		// Unset the section.
		} else {
			ret = unset_section(io.key0);
			if (ret != KNOT_EOK) {
				goto unset_error;
			}
		}
	}

	if (ret == KNOT_EOK) {
		upd_changes(&io, upd_type, upd_flags, false);
	}
unset_error:
	yp_schema_check_deinit(ctx);

	return ret;
}

static int check_section(
	const yp_item_t *group,
	const uint8_t *id,
	size_t id_len,
	conf_io_t *io)
{
	knotd_conf_check_extra_t extra = {
		.conf = conf(),
		.txn = conf()->io.txn,
		.check = true
	};
	knotd_conf_check_args_t args = {
		.id = id,
		.id_len = id_len,
		.extra = &extra
	};

	bool non_empty = false;

	conf_val_t bin; // Must be in the scope of the error processing.
	for (yp_item_t *item = group->sub_items; item->name != NULL; item++) {
		args.item = item;

		// Check the identifier.
		if ((group->flags & YP_FMULTI) != 0 && group->var.g.id == item) {
			io->error.code = conf_exec_callbacks(&args);
			if (io->error.code != KNOT_EOK) {
				io_reset_val(io, group, item, NULL, 0, false, NULL);
				goto check_section_error;
			}
			continue;
		}

		// Get the item value.
		conf_db_get(conf(), conf()->io.txn, group->name, item->name, id,
		            id_len, &bin);
		if (bin.code == KNOT_ENOENT) {
			continue;
		} else if (bin.code != KNOT_EOK) {
			return bin.code;
		}

		non_empty = true;

		// Check the item value(s).
		size_t values = conf_val_count(&bin);
		for (size_t i = 1; i <= values; i++) {
			conf_val(&bin);
			args.data = bin.data;
			args.data_len = bin.len;

			io->error.code = conf_exec_callbacks(&args);
			if (io->error.code != KNOT_EOK) {
				io_reset_val(io, group, item, id, id_len, false,
				             &bin);
				io->data.index = i;
				goto check_section_error;
			}

			if (values > 1) {
				conf_val_next(&bin);
			}
		}
	}

	// Check the whole section if not empty.
	if (id != NULL || non_empty) {
		args.item = group;
		args.data = NULL;
		args.data_len = 0;

		io->error.code = conf_exec_callbacks(&args);
		if (io->error.code != KNOT_EOK) {
			io_reset_val(io, group, NULL, id, id_len, false, NULL);
			goto check_section_error;
		}
	}

	return KNOT_EOK;

check_section_error:
	io->error.str = args.err_str;
	int ret = FCN(io);
	if (ret == KNOT_EOK) {
		return io->error.code;
	}
	return ret;
}

static int check_iter_section(
	const yp_item_t *item,
	conf_io_t *io)
{
	// Iterate over all identifiers.
	conf_iter_t iter;
	int ret = conf_db_iter_begin(conf(), conf()->io.txn, item->name, &iter);
	switch (ret) {
	case KNOT_EOK:
		break;
	case KNOT_ENOENT:
		return KNOT_EOK;
	default:
		return ret;
	}

	while (ret == KNOT_EOK) {
		size_t id_len;
		const uint8_t *id;
		ret = conf_db_iter_id(conf(), &iter, &id, &id_len);
		if (ret != KNOT_EOK) {
			conf_db_iter_finish(conf(), &iter);
			return ret;
		}

		// Check specific section item.
		ret = check_section(item, id, id_len, io);
		if (ret != KNOT_EOK) {
			conf_db_iter_finish(conf(), &iter);
			return ret;
		}

		ret = conf_db_iter_next(conf(), &iter);
	}
	if (ret != KNOT_EOF) {
		return ret;
	}

	return KNOT_EOK;
}

static int check_zone_section(
	const yp_item_t *item,
	conf_io_t *io)
{
	assert(item->flags & CONF_IO_FZONE);

	if (conf()->io.zones == NULL) {
		return KNOT_EOK;
	}

	trie_it_t *it = trie_it_begin(conf()->io.zones);
	for (; !trie_it_finished(it); trie_it_next(it)) {
		size_t id_len;
		const uint8_t *id = (const uint8_t *)trie_it_key(it, &id_len);

		conf_io_type_t type = conf_io_trie_val(it);
		if (type == CONF_IO_TUNSET) {
			// Nothing to check.
			continue;
		}

		// Check specific zone.
		int ret = check_section(item, id, id_len, io);
		if (ret != KNOT_EOK) {
			trie_it_free(it);
			return ret;
		}
	}
	trie_it_free(it);

	return KNOT_EOK;
}

int conf_io_check(
	conf_io_t *io)
{
	if (io == NULL) {
		return KNOT_EINVAL;
	}

	assert(conf() != NULL);

	if (conf()->io.txn == NULL) {
		return KNOT_TXN_ENOTEXISTS;
	}
	CHECK_SAME_THREAD;

	int ret;

	// Iterate over the schema.
	for (yp_item_t *item = conf()->schema; item->name != NULL; item++) {
		// Skip non-group items (include).
		if (item->type != YP_TGRP) {
			continue;
		}

		// Check simple group without identifiers.
		if ((item->flags & YP_FMULTI) == 0) {
			ret = check_section(item, NULL, 0, io);
			if (ret != KNOT_EOK) {
				goto check_error;
			}
			continue;
		}

		// The zone section has an optimized check.
		if (item->flags & CONF_IO_FZONE) {
			// Full check by default.
			if (!(conf()->io.flags & CONF_IO_FACTIVE)) {
				ret = check_iter_section(item, io);
			// Full check if all zones changed.
			} else if (conf()->io.flags & CONF_IO_FCHECK_ZONES) {
				ret = check_iter_section(item, io);
			// Optimized check for specific zones.
			} else {
				ret = check_zone_section(item, io);
			}
		} else {
			ret = check_iter_section(item, io);
		}
		if (ret != KNOT_EOK) {
			goto check_error;
		}
	}

	ret = KNOT_EOK;
check_error:
	conf_mod_load_purge(conf(), true);

	return ret;
}
