/*  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 <signal.h>
#include <unistd.h>
#include <urcu.h>

#include "knot/catalog/interpret.h"
#include "knot/common/dbus.h"
#include "knot/common/log.h"
#include "knot/dnssec/zone-events.h"
#include "knot/server/server.h"
#include "knot/updates/zone-update.h"
#include "knot/zone/adds_tree.h"
#include "knot/zone/adjust.h"
#include "knot/zone/digest.h"
#include "knot/zone/redis.h"
#include "knot/zone/serial.h"
#include "knot/zone/zone-diff.h"
#include "knot/zone/zonefile.h"
#include "contrib/trim.h"
#include "contrib/ucw/lists.h"

// Call mem_trim() whenever accumulated size of updated zones reaches this size.
#define UPDATE_MEMTRIM_AT (10 * 1024 * 1024)

static int init_incremental(zone_update_t *update, zone_t *zone, zone_contents_t *old_contents)
{
	if (old_contents == NULL) {
		return KNOT_EINVAL;
	}

	int ret = changeset_init(&update->change, zone->name);
	if (ret != KNOT_EOK) {
		return ret;
	}

	if (update->flags & UPDATE_HYBRID) {
		update->new_cont = old_contents;
	} else {
		ret = zone_contents_cow(old_contents, &update->new_cont);
		if (ret != KNOT_EOK) {
			changeset_clear(&update->change);
			return ret;
		}
	}

	uint32_t apply_flags = (update->flags & UPDATE_STRICT) ? APPLY_STRICT : 0;
	apply_flags |= (update->flags & UPDATE_HYBRID) ? APPLY_UNIFY_FULL : 0;
	ret = apply_init_ctx(update->a_ctx, update->new_cont, apply_flags);
	if (ret != KNOT_EOK) {
		changeset_clear(&update->change);
		return ret;
	}

	/* Copy base SOA RR. */
	update->change.soa_from =
		node_create_rrset(old_contents->apex, KNOT_RRTYPE_SOA);
	if (update->change.soa_from == NULL) {
		zone_contents_free(update->new_cont);
		changeset_clear(&update->change);
		return KNOT_ENOMEM;
	}

	return KNOT_EOK;
}

static int init_full(zone_update_t *update, zone_t *zone)
{
	update->new_cont = zone_contents_new(zone->name, true);
	if (update->new_cont == NULL) {
		return KNOT_ENOMEM;
	}

	int ret = apply_init_ctx(update->a_ctx, update->new_cont, APPLY_UNIFY_FULL);
	if (ret != KNOT_EOK) {
		zone_contents_free(update->new_cont);
		return ret;
	}

	return KNOT_EOK;
}

static int replace_soa(zone_contents_t *contents, const knot_rrset_t *rr)
{
	/* SOA possible only within apex. */
	if (!knot_dname_is_equal(rr->owner, contents->apex->owner)) {
		return KNOT_EDENIED;
	}

	knot_rrset_t old_soa = node_rrset(contents->apex, KNOT_RRTYPE_SOA);
	zone_node_t *n = contents->apex;
	int ret = zone_contents_remove_rr(contents, &old_soa, &n);
	if (ret != KNOT_EOK && ret != KNOT_EINVAL) {
		return ret;
	}

	ret = zone_contents_add_rr(contents, rr, &n);
	if (ret == KNOT_ETTL) {
		return KNOT_EOK;
	}

	return ret;
}

static int init_base(zone_update_t *update, zone_t *zone, zone_contents_t *old_contents,
                     zone_update_flags_t flags)
{
	if (update == NULL || zone == NULL) {
		return KNOT_EINVAL;
	}

	memset(update, 0, sizeof(*update));
	update->zone = zone;
	update->flags = flags;

	update->a_ctx = calloc(1, sizeof(*update->a_ctx));
	if (update->a_ctx == NULL) {
		return KNOT_ENOMEM;
	}

	if (zone->control_update != NULL && zone->control_update != update) {
		log_zone_warning(zone->name, "blocked zone update due to open control transaction");
	}

	knot_sem_wait(&zone->cow_lock);
	update->a_ctx->cow_mutex = &zone->cow_lock;

	if (old_contents == NULL) {
		old_contents = zone->contents; // don't obtain this pointer before any other zone_update ceased to exist!
	}

	int ret = KNOT_EINVAL;
	if (flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
		ret = init_incremental(update, zone, old_contents);
	} else if (flags & UPDATE_FULL) {
		ret = init_full(update, zone);
	}
	if (ret != KNOT_EOK) {
		knot_sem_post(&zone->cow_lock);
		free(update->a_ctx);
	}

	return ret;
}

/* ------------------------------- API -------------------------------------- */

int zone_update_init(zone_update_t *update, zone_t *zone, zone_update_flags_t flags)
{
	return init_base(update, zone, NULL, flags);
}

int zone_update_from_differences(zone_update_t *update, zone_t *zone, zone_contents_t *old_cont,
                                 zone_contents_t *new_cont, zone_update_flags_t flags,
                                 zone_skip_t *skip)
{
	if (update == NULL || zone == NULL || new_cont == NULL ||
	    !(flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) || (flags & UPDATE_FULL)) {
		return KNOT_EINVAL;
	}

	changeset_t diff;
	int ret = changeset_init(&diff, zone->name);
	if (ret != KNOT_EOK) {
		return ret;
	}

	ret = init_base(update, zone, old_cont, flags);
	if (ret != KNOT_EOK) {
		changeset_clear(&diff);
		return ret;
	}

	if (old_cont == NULL) {
		old_cont = zone->contents;
	}

	ret = zone_contents_diff(old_cont, new_cont, &diff, skip);
	switch (ret) {
	case KNOT_ENODIFF:
	case KNOT_ESEMCHECK:
	case KNOT_EOK:
		break;
	case KNOT_ERANGE:
		additionals_tree_free(update->new_cont->adds_tree);
		update->new_cont->adds_tree = NULL;
		update->new_cont = NULL; // Prevent deep_free as old_cont will be used later.
		update->a_ctx->flags &= ~APPLY_UNIFY_FULL; // Prevent Unify of old_cont that will be used later.
		// FALLTHROUGH
	default:
		changeset_clear(&diff);
		zone_update_clear(update);
		return ret;
	}

	ret = zone_update_apply_changeset(update, &diff);
	changeset_clear(&diff);
	if (ret != KNOT_EOK) {
		zone_update_clear(update);
		return ret;
	}

	update->init_cont = new_cont;
	return KNOT_EOK;
}

int zone_update_from_contents(zone_update_t *update, zone_t *zone_without_contents,
                              zone_contents_t *new_cont, zone_update_flags_t flags)
{
	if (update == NULL || zone_without_contents == NULL || new_cont == NULL) {
		return KNOT_EINVAL;
	}

	memset(update, 0, sizeof(*update));
	update->zone = zone_without_contents;
	update->flags = flags;
	update->new_cont = new_cont;

	update->a_ctx = calloc(1, sizeof(*update->a_ctx));
	if (update->a_ctx == NULL) {
		return KNOT_ENOMEM;
	}

	if (zone_without_contents->control_update != NULL) {
		log_zone_warning(zone_without_contents->name,
		                 "blocked zone update due to open control transaction");
	}

	knot_sem_wait(&update->zone->cow_lock);
	update->a_ctx->cow_mutex = &update->zone->cow_lock;

	if (flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
		int ret = changeset_init(&update->change, zone_without_contents->name);
		if (ret != KNOT_EOK) {
			free(update->a_ctx);
			update->a_ctx = NULL;
			knot_sem_post(&update->zone->cow_lock);
			return ret;
		}

		update->change.soa_from = node_create_rrset(new_cont->apex, KNOT_RRTYPE_SOA);
		if (update->change.soa_from == NULL) {
			changeset_clear(&update->change);
			free(update->a_ctx);
			update->a_ctx = NULL;
			knot_sem_post(&update->zone->cow_lock);
			return KNOT_ENOMEM;
		}
	}

	uint32_t apply_flags = (update->flags & UPDATE_STRICT) ? APPLY_STRICT : 0;
	int ret = apply_init_ctx(update->a_ctx, update->new_cont, apply_flags | APPLY_UNIFY_FULL);
	if (ret != KNOT_EOK) {
		changeset_clear(&update->change);
		free(update->a_ctx);
		update->a_ctx = NULL;
		knot_sem_post(&update->zone->cow_lock);
		return ret;
	}

	return KNOT_EOK;
}

int zone_update_start_extra(zone_update_t *update, conf_t *conf)
{
	assert((update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)));

	int ret = changeset_init(&update->extra_ch, update->new_cont->apex->owner);
	if (ret != KNOT_EOK) {
		return ret;
	}

	if (update->init_cont != NULL) {
		ret = zone_update_increment_soa(update, conf);
		if (ret != KNOT_EOK) {
			return ret;
		}

		ret = zone_contents_diff(update->init_cont, update->new_cont,
		                         &update->extra_ch, NULL);
		if (ret != KNOT_EOK) {
			return ret;
		}
	} else {
		update->extra_ch.soa_from = node_create_rrset(update->new_cont->apex, KNOT_RRTYPE_SOA);
		if (update->extra_ch.soa_from == NULL) {
			return KNOT_ENOMEM;
		}

		ret = zone_update_increment_soa(update, conf);
		if (ret != KNOT_EOK) {
			return ret;
		}

		update->extra_ch.soa_to = node_create_rrset(update->new_cont->apex, KNOT_RRTYPE_SOA);
		if (update->extra_ch.soa_to == NULL) {
			return KNOT_ENOMEM;
		}
	}

	update->flags |= UPDATE_EXTRA_CHSET;
	return KNOT_EOK;
}

const zone_node_t *zone_update_get_node(zone_update_t *update, const knot_dname_t *dname)
{
	if (update == NULL || dname == NULL) {
		return NULL;
	}

	return zone_contents_node_or_nsec3(update->new_cont, dname);
}

uint32_t zone_update_current_serial(zone_update_t *update)
{
	const zone_node_t *apex = update->new_cont->apex;
	if (apex != NULL) {
		return knot_soa_serial(node_rdataset(apex, KNOT_RRTYPE_SOA)->rdata);
	} else {
		return 0;
	}
}

bool zone_update_changed_nsec3param(const zone_update_t *update)
{
	if (update->zone->contents == NULL) {
		return true;
	}

	dnssec_nsec3_params_t *orig = &update->zone->contents->nsec3_params;
	dnssec_nsec3_params_t *upd = &update->new_cont->nsec3_params;
	return !dnssec_nsec3_params_match(orig, upd);
}

const knot_rdataset_t *zone_update_from(zone_update_t *update)
{
	if (update == NULL) {
		return NULL;
	}

	if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
		const zone_node_t *apex = update->zone->contents->apex;
		return node_rdataset(apex, KNOT_RRTYPE_SOA);
	}

	return NULL;
}

uint32_t zone_update_from_serial(const zone_update_t *update)
{
	assert(update != NULL);
	assert(update->new_cont->apex != NULL);

	if (update->flags & UPDATE_NO_CHSET) {
		const knot_rdataset_t *old_soa = node_rdataset(binode_counterpart(update->new_cont->apex), KNOT_RRTYPE_SOA);
		assert(old_soa != NULL && old_soa->count > 0);
		return knot_soa_serial(old_soa->rdata);
	} else if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
		return changeset_from(&update->change);
	} else {
		assert(0);
		return 0;
	}
}

const knot_rdataset_t *zone_update_to(zone_update_t *update)
{
	if (update == NULL) {
		return NULL;
	}

	if (update->flags & UPDATE_NO_CHSET) {
		zone_diff_t diff = { .apex = update->new_cont->apex };
		return zone_diff_to(&diff) == zone_diff_from(&diff) ?
		       NULL : node_rdataset(update->new_cont->apex, KNOT_RRTYPE_SOA);
	} else if (update->flags & UPDATE_FULL) {
		const zone_node_t *apex = update->new_cont->apex;
		return node_rdataset(apex, KNOT_RRTYPE_SOA);
	} else {
		if (update->change.soa_to == NULL) {
			return NULL;
		}
		return &update->change.soa_to->rrs;
	}

	return NULL;
}

void zone_update_clear(zone_update_t *update)
{
	if (update == NULL || update->zone == NULL) {
		return;
	}

	if (update->new_cont != NULL) {
		additionals_tree_free(update->new_cont->adds_tree);
		update->new_cont->adds_tree = NULL;
	}

	if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
		changeset_clear(&update->change);
		changeset_clear(&update->extra_ch);
	}

	zone_contents_deep_free(update->init_cont);

	if (update->flags & (UPDATE_FULL | UPDATE_HYBRID)) {
		apply_cleanup(update->a_ctx);
		zone_contents_deep_free(update->new_cont);
	} else {
		apply_rollback(update->a_ctx);
	}

	free(update->a_ctx);
	memset(update, 0, sizeof(*update));
}

inline static void update_affected_rrtype(zone_update_t *update, uint16_t rrtype)
{
	switch (rrtype) {
	case KNOT_RRTYPE_NSEC:
	case KNOT_RRTYPE_NSEC3:
		update->flags |= UPDATE_CHANGED_NSEC;
		break;
	}
}

static int solve_add_different_ttl(zone_update_t *update, const knot_rrset_t *add)
{
	if (add->type == KNOT_RRTYPE_RRSIG || add->type == KNOT_RRTYPE_SOA) {
		return KNOT_EOK;
	}

	const zone_node_t *exist_node = zone_contents_find_node(update->new_cont, add->owner);
	const knot_rrset_t exist_rr = node_rrset(exist_node, add->type);
	if (knot_rrset_empty(&exist_rr) || exist_rr.ttl == add->ttl) {
		return KNOT_EOK;
	}

	knot_dname_txt_storage_t buff;
	char *owner = knot_dname_to_str(buff, add->owner, sizeof(buff));
	if (owner == NULL) {
		owner = "";
	}
	char type[16] = "";
	knot_rrtype_to_string(add->type, type, sizeof(type));
	log_zone_notice(update->zone->name, "TTL mismatch, owner %s, type %s, "
	                "TTL set to %u", owner, type, add->ttl);

	knot_rrset_t *exist_copy = knot_rrset_copy(&exist_rr, NULL);
	if (exist_copy == NULL) {
		return KNOT_ENOMEM;
	}
	int ret = zone_update_remove(update, exist_copy);
	if (ret == KNOT_EOK) {
		exist_copy->ttl = add->ttl;
		ret = zone_update_add(update, exist_copy);
	}
	knot_rrset_free(exist_copy, NULL);
	return ret;
}

int zone_update_add(zone_update_t *update, const knot_rrset_t *rrset)
{
	if (update == NULL || rrset == NULL) {
		return KNOT_EINVAL;
	}
	if (knot_rrset_empty(rrset)) {
		return KNOT_EOK;
	}

	// apply_add_rr might modify given rrset to what was really added (in non-strict mode)
	knot_rrset_t *rrset_copy = knot_rrset_copy(rrset, NULL);
	if (rrset_copy == NULL) {
		return KNOT_ENOMEM;
	}

	int ret = KNOT_EOK;

	if (update->flags & UPDATE_INCREMENTAL) {
		if (rrset->type == KNOT_RRTYPE_SOA) {
			// replace previous SOA
			ret = apply_replace_soa(update->a_ctx, rrset);
		} else {
			ret = apply_add_rr(update->a_ctx, rrset_copy);
			if (ret == KNOT_EOK) {
				update_affected_rrtype(update, rrset->type);
			}
		}
	} else if (update->flags & (UPDATE_FULL | UPDATE_HYBRID)) {
		if (rrset->type == KNOT_RRTYPE_SOA) {
			/* replace previous SOA */
			ret = replace_soa(update->new_cont, rrset);
			goto chset_add;
		}

		zone_node_t *n = NULL;
		ret = zone_contents_add_rr(update->new_cont, rrset, &n);
		if (ret == KNOT_ETTL) {
			knot_dname_txt_storage_t buff;
			char *owner = knot_dname_to_str(buff, rrset->owner, sizeof(buff));
			if (owner == NULL) {
				owner = "";
			}
			char type[16] = "";
			knot_rrtype_to_string(rrset->type, type, sizeof(type));
			log_zone_notice(update->new_cont->apex->owner,
			                "TTL mismatch, owner %s, type %s, "
			                "TTL set to %u", owner, type, rrset->ttl);
			ret = KNOT_EOK;
		}
	} else {
		ret = KNOT_EINVAL;
	}

chset_add:
	if ((update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) && ret == KNOT_EOK) {
		ret = solve_add_different_ttl(update, rrset_copy);
		if (ret == KNOT_EOK && !(update->flags & UPDATE_NO_CHSET)) {
			ret = changeset_add_addition(&update->change, rrset_copy, CHANGESET_CHECK);
		}
		if (ret == KNOT_EOK && (update->flags & UPDATE_EXTRA_CHSET)) {
			ret = changeset_add_addition(&update->extra_ch, rrset_copy, CHANGESET_CHECK);
		}
	}

	knot_rrset_free(rrset_copy, NULL);
	return ret;
}

int zone_update_remove(zone_update_t *update, const knot_rrset_t *rrset)
{
	if (update == NULL || rrset == NULL) {
		return KNOT_EINVAL;
	}
	if (knot_rrset_empty(rrset)) {
		return KNOT_EOK;
	}

	/* Copy the rrset as sometimes rrset may be what is actually being removed thus
	 * its contents might be already freed memory when we get to changeset removal. */
	knot_rrset_t *rrset_copy = knot_rrset_copy(rrset, NULL);
	if (rrset_copy == NULL) {
		return KNOT_ENOMEM;
	}

	int ret = KNOT_EOK;

	if (update->flags & UPDATE_INCREMENTAL) {
		if (rrset->type == KNOT_RRTYPE_SOA) {
			/* SOA is replaced with addition */
		} else {
			ret = apply_remove_rr(update->a_ctx, rrset_copy);
			if (ret == KNOT_EOK) {
				update_affected_rrtype(update, rrset->type);
			}
		}
	} else if (update->flags & (UPDATE_FULL | UPDATE_HYBRID)) {
		zone_node_t *n = NULL;
		ret = zone_contents_remove_rr(update->new_cont, rrset, &n);
	} else {
		ret = KNOT_EINVAL;
	}

	if ((update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) &&
	    rrset_copy->type != KNOT_RRTYPE_SOA) {
		if (ret == KNOT_EOK && !(update->flags & UPDATE_NO_CHSET)) {
			ret = changeset_add_removal(&update->change, rrset_copy, CHANGESET_CHECK);
		}
		if (ret == KNOT_EOK && (update->flags & UPDATE_EXTRA_CHSET)) {
			ret = changeset_add_removal(&update->extra_ch, rrset_copy, CHANGESET_CHECK);
		}
	}

	knot_rrset_free(rrset_copy, NULL);
	return ret;
}

int zone_update_remove_rrset(zone_update_t *update, knot_dname_t *owner, uint16_t type)
{
	if (update == NULL || owner == NULL) {
		return KNOT_EINVAL;
	}

	const zone_node_t *node = zone_contents_node_or_nsec3(update->new_cont, owner);
	if (node == NULL) {
		return KNOT_ENONODE;
	}

	knot_rrset_t rrset = node_rrset(node, type);
	if (rrset.owner == NULL) {
		return KNOT_ENOENT;
	}

	return zone_update_remove(update, &rrset);
}

int zone_update_remove_node(zone_update_t *update, const knot_dname_t *owner)
{
	if (update == NULL || owner == NULL) {
		return KNOT_EINVAL;
	}

	const zone_node_t *node = zone_contents_node_or_nsec3(update->new_cont, owner);
	if (node == NULL) {
		return KNOT_ENONODE;
	}

	size_t rrset_count = node->rrset_count;
	for (int i = 0; i < rrset_count; ++i) {
		knot_rrset_t rrset = node_rrset_at(node, rrset_count - 1 - i);
		int ret = zone_update_remove(update, &rrset);
		if (ret != KNOT_EOK) {
			return ret;
		}
	}

	return KNOT_EOK;
}

static int update_chset_step(const knot_rrset_t *rrset, bool addition, void *ctx)
{
	zone_update_t *update = ctx;
	if (addition) {
		return zone_update_add(update, rrset);
	} else {
		return zone_update_remove(update, rrset);
	}
}

int zone_update_apply_changeset(zone_update_t *update, const changeset_t *changes)
{
	return changeset_walk(changes, update_chset_step, update);
}

int zone_update_apply_changeset_reverse(zone_update_t *update, const changeset_t *changes)
{
	changeset_t reverse;
	reverse.remove = changes->add;
	reverse.add = changes->remove;
	reverse.soa_from = changes->soa_to;
	reverse.soa_to = changes->soa_from;
	return zone_update_apply_changeset(update, &reverse);
}

int zone_update_set_soa(zone_update_t *update, uint32_t new_serial, bool semcheck)
{
	if (update == NULL) {
		return KNOT_EINVAL;
	}

	knot_rrset_t *soa_cpy = node_create_rrset(update->new_cont->apex,
	                                          KNOT_RRTYPE_SOA);
	if (soa_cpy == NULL) {
		return KNOT_ENOMEM;
	}

	uint32_t old_serial = knot_soa_serial(soa_cpy->rrs.rdata);
	if (semcheck && serial_compare(old_serial, new_serial) != SERIAL_LOWER) {
		knot_rrset_free(soa_cpy, NULL);
		return KNOT_ESOAINVAL;
	}

	int ret = zone_update_remove(update, soa_cpy);
	if (ret == KNOT_EOK) {
		knot_soa_serial_set(soa_cpy->rrs.rdata, new_serial);
		ret = zone_update_add(update, soa_cpy);
	}
	knot_rrset_free(soa_cpy, NULL);

	return ret;
}

int zone_update_increment_soa(zone_update_t *update, conf_t *conf)
{
	if (update == NULL || conf == NULL) {
		return KNOT_EINVAL;
	}

	uint32_t old_serial = zone_update_current_serial(update);
	uint32_t new_serial = serial_next(old_serial, conf, update->zone->name,
	                                  SERIAL_POLICY_AUTO, 1);

	int ret = zone_update_set_soa(update, new_serial, true);
	if (ret == KNOT_ESOAINVAL) {
		log_zone_warning(update->zone->name, "updated SOA serial is lower "
		                 "than current, serial %u -> %u",
		                 old_serial, new_serial);
	}

	return ret;
}

static void get_zone_diff(zone_diff_t *zdiff, zone_update_t *up)
{
	zdiff->nodes = *up->a_ctx->node_ptrs;
	zdiff->nsec3s = *up->a_ctx->nsec3_ptrs;
	zdiff->apex = up->new_cont->apex;
}

static int commit_journal(conf_t *conf, zone_update_t *update)
{
	conf_val_t val = conf_zone_get(conf, C_JOURNAL_CONTENT, update->zone->name);
	unsigned content = conf_opt(&val);

	if (zone_includes_configured(conf, update->zone)){
		content = JOURNAL_CONTENT_ALL;
	}

	int ret = KNOT_EOK;
	changeset_t *extra = (update->flags & UPDATE_EXTRA_CHSET) ? &update->extra_ch : NULL;

	if (update->flags & UPDATE_NO_CHSET) {
		zone_diff_t diff;
		get_zone_diff(&diff, update);
		assert(!zone_update_no_change(update) || !(update->flags & UPDATE_EXTRA_CHSET));
		if (content != JOURNAL_CONTENT_NONE && !zone_update_no_change(update)) {
			ret = zone_diff_store(conf, update->zone, &diff, extra);
		}
	} else if ((update->flags & UPDATE_INCREMENTAL) ||
	           (update->flags & UPDATE_HYBRID)) {
		if (content != JOURNAL_CONTENT_NONE && !zone_update_no_change(update)) {
			ret = zone_change_store(conf, update->zone, &update->change, extra);
		}
	} else {
		if (content == JOURNAL_CONTENT_ALL) {
			return zone_in_journal_store(conf, update->zone, update->new_cont);
		} else if (content != JOURNAL_CONTENT_NONE) { // zone_in_journal_store does this automatically
			return zone_changes_clear(conf, update->zone);
		}
	}
	return ret;
}

static int redis_wr_rr(const knot_rrset_t *rr, void *ctx)
{
	return zone_redis_write_rrset(ctx, rr);
}

static int redis_wr_node(zone_node_t *node, void *ctx)
{
	return zone_redis_write_node(ctx, node);
}

static int commit_redis(conf_t *conf, zone_update_t *update)
{
	uint8_t db_instance = 0;
	bool db_enabled = conf_zone_rdb_enabled(conf, update->zone->name, false, &db_instance);
	if (!db_enabled) {
		return KNOT_EOK;
	}

	struct redisContext *db_ctx = zone_redis_connect(conf, true);
	if (db_ctx == NULL) {
		return KNOT_ECONN;
	}

	bool incremental = ((update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) && update->zone->contents != NULL);
	if (incremental) {
		zone_redis_err_t err;
		uint32_t redis_soa = 0, chset_soa = zone_update_from_serial(update);
		int soa_ret = zone_redis_serial(db_ctx, db_instance, update->zone->name, &redis_soa, err);
		incremental = (soa_ret == KNOT_EOK && redis_soa == chset_soa);
	}

	zone_redis_txn_t txn;
	int ret = zone_redis_txn_begin(&txn, db_ctx, db_instance, update->zone->name, incremental);
	if (ret != KNOT_EOK) {
		zone_redis_disconnect(db_ctx, true);
		return ret;
	}

	if (incremental) {
		txn.removals = true;
		ret = zone_update_foreach(update, false, redis_wr_rr, &txn);
		if (ret == KNOT_EOK) {
			txn.removals = false;
			ret = zone_update_foreach(update, true, redis_wr_rr, &txn);
		}
	} else {
		ret = zone_contents_apply(update->new_cont, redis_wr_node, &txn);
		if (ret == KNOT_EOK) {
			ret = zone_contents_nsec3_apply(update->new_cont, redis_wr_node, &txn);
		}
	}

	if (ret == KNOT_EOK) {
		ret = zone_redis_txn_commit(&txn);
	}
	if (ret != KNOT_EOK) {
		if (ret == KNOT_ERDB) {
			log_zone_error(update->zone->name, "rdb, update aborted (%s)", txn.err);
		}
		(void)zone_redis_txn_abort(&txn);
	} else {
		log_zone_info(update->zone->name, "database updated, instance %u, serial %u",
		              db_instance, zone_contents_serial(update->new_cont));
	}

	zone_redis_disconnect(db_ctx, true);
	return ret;
}

static int commit_incremental(conf_t *conf, zone_update_t *update)
{
	assert(update);

	if (zone_update_to(update) == NULL && !zone_update_no_change(update)) {
		/* No SOA in the update, create one according to the current policy */
		int ret = zone_update_increment_soa(update, conf);
		if (ret != KNOT_EOK) {
			return ret;
		}
	}

	return KNOT_EOK;
}

static int commit_full(conf_t *conf, zone_update_t *update)
{
	assert(update);

	/* Check if we have SOA. We might consider adding full semantic check here.
	 * But if we wanted full sem-check I'd consider being it controlled by a flag
	 * - to enable/disable it on demand. */
	if (!node_rrtype_exists(update->new_cont->apex, KNOT_RRTYPE_SOA)) {
		return KNOT_ESEMCHECK;
	}

	return KNOT_EOK;
}

static int update_catalog(conf_t *conf, zone_update_t *update)
{
	conf_val_t val = conf_zone_get(conf, C_CATALOG_TPL, update->zone->name);
	if (val.code != KNOT_EOK) {
		return (val.code == KNOT_ENOENT || val.code == KNOT_YP_EINVAL_ID) ? KNOT_EOK : val.code;
	}

	int ret = catalog_zone_verify(update->new_cont);
	if (ret != KNOT_EOK) {
		return ret;
	}

	ssize_t upd_count = 0;
	if ((update->flags & UPDATE_NO_CHSET)) {
		zone_diff_t diff;
		get_zone_diff(&diff, update);
		ret = catalog_update_from_zone(zone_catalog_upd(update->zone),
		                               NULL, &diff, update->new_cont,
		                               false, zone_catalog(update->zone), &upd_count);
	} else if ((update->flags & UPDATE_INCREMENTAL)) {
		ret = catalog_update_from_zone(zone_catalog_upd(update->zone),
		                               update->change.remove, NULL, update->new_cont,
		                               true, zone_catalog(update->zone), &upd_count);
		if (ret == KNOT_EOK) {
			ret = catalog_update_from_zone(zone_catalog_upd(update->zone),
			                               update->change.add, NULL, update->new_cont,
			                               false, NULL, &upd_count);
		}
	} else {
		ret = catalog_update_del_all(zone_catalog_upd(update->zone),
		                             zone_catalog(update->zone),
		                             update->zone->name, &upd_count);
		if (ret == KNOT_EOK) {
			ret = catalog_update_from_zone(zone_catalog_upd(update->zone),
			                               update->new_cont, NULL, update->new_cont,
			                               false, NULL, &upd_count);
		}
	}

	if (ret == KNOT_EOK) {
		log_zone_info(update->zone->name, "enqueued %zd catalog updates", upd_count);
		ATOMIC_SET(update->zone->server->catalog_upd_signal, true);
		if (kill(getpid(), SIGUSR1) != 0) {
			ret = knot_map_errno();
		}
	} else {
		// this cant normally happen, just some ENOMEM or so
		(void)catalog_update_del_all(zone_catalog_upd(update->zone),
		                             zone_catalog(update->zone),
		                             update->zone->name, &upd_count);
	}

	return ret;
}

/*! \brief Struct for what needs to be cleared after RCU.
 *
 * This can't be zone_update_t structure as this might be already freed at that time.
 */
typedef struct {
	struct rcu_head rcuhead;
	pthread_t *retry_thread;

	zone_contents_t *free_contents;
	void (*free_method)(zone_contents_t *);

	apply_ctx_t *cleanup_apply;

	size_t new_cont_size;
} update_clear_ctx_t;

static void call_mem_trim(update_clear_ctx_t *ctx)
{
	static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

	static bool running = false;
	static size_t counter = 0;
	static knot_millis_t next = 0;

	knot_millis_t now = knot_millis_now();
	pthread_mutex_lock(&lock);
	counter += ctx->new_cont_size;
	if (running || counter < UPDATE_MEMTRIM_AT || now < next) {
		pthread_mutex_unlock(&lock);
		return;
	}
	running = true;
	pthread_mutex_unlock(&lock);

	mem_trim();
	knot_millis_t diff = knot_millis_now() - now;

	pthread_mutex_lock(&lock);
	running = false;
	counter = 0;
	next = now + 2 * diff;
	pthread_mutex_unlock(&lock);
}

static void *update_clear_retry(void *arg);

static void update_clear(struct rcu_head *param)
{
	update_clear_ctx_t *ctx = (update_clear_ctx_t *)param;

	if (ctx->free_contents != NULL) {
		if (pthread_rwlock_trywrlock(&ctx->free_contents->xfrout_lock) != 0) {
			if (*ctx->retry_thread == 0 &&
			    pthread_create(ctx->retry_thread, NULL, update_clear_retry, ctx) == 0) {
				return;
			}
			log_zone_debug(ctx->free_contents->apex->owner,
			               "disposal of old contents blocked by outstanding zone transfer");
			pthread_rwlock_wrlock(&ctx->free_contents->xfrout_lock);
		}
		pthread_rwlock_unlock(&ctx->free_contents->xfrout_lock);
	}

	ctx->free_method(ctx->free_contents);
	apply_cleanup(ctx->cleanup_apply);
	free(ctx->cleanup_apply);

	call_mem_trim(ctx);

	free(ctx);
}

static void *update_clear_retry(void *arg)
{
	update_clear(arg);
	return NULL;
}

static void discard_adds_tree(zone_update_t *update)
{
	additionals_tree_free(update->new_cont->adds_tree);
	update->new_cont->adds_tree = NULL;
}

int zone_update_semcheck(conf_t *conf, zone_update_t *update)
{
	if (update == NULL) {
		return KNOT_EINVAL;
	}

	zone_tree_t *node_ptrs = (update->flags & UPDATE_INCREMENTAL) ?
	                         update->a_ctx->node_ptrs : NULL;

	// adjust_cb_nsec3_pointer not needed as we don't check DNSSEC here
	int ret = zone_adjust_contents(update->new_cont, adjust_cb_flags, NULL,
	                               false, false, 1, node_ptrs);
	if (ret != KNOT_EOK) {
		return ret;
	}

	sem_handler_t handler = {
		.cb = err_handler_logger
	};

	conf_val_t val = conf_zone_get(conf, C_SEM_CHECKS, update->zone->name);
	semcheck_optional_t mode = (conf_opt(&val) == SEMCHECKS_SOFT) ?
	                           SEMCHECK_MANDATORY_SOFT : SEMCHECK_MANDATORY_ONLY;

	ret = sem_checks_process(update->new_cont, mode, &handler, time(NULL), 0);
	if (ret != KNOT_EOK) {
		// error is logged by the error handler
		return ret;
	}

	return KNOT_EOK;
}

int zone_update_verify_digest(conf_t *conf, zone_update_t *update)
{
	conf_val_t val = conf_zone_get(conf, C_ZONEMD_VERIFY, update->zone->name);
	if (!conf_bool(&val)) {
		return KNOT_EOK;
	}
	val = conf_zone_get(conf, C_DNSSEC_SIGNING, update->zone->name);

	int ret = zone_contents_digest_verify(update->new_cont, conf_bool(&val));
	if (ret != KNOT_EOK) {
		log_zone_error(update->zone->name, "ZONEMD, verification failed (%s)",
		               knot_strerror(ret));
		if (conf->cache.srv_dbus_event & DBUS_EVENT_ZONE_INVALID) {
			dbus_emit_zone_invalid(update->zone->name, 0);
		}
	} else {
		log_zone_info(update->zone->name, "ZONEMD, verification successful");
	}

	return ret;
}

typedef struct {
	FILE *f;
	char *buf;
	size_t buflen;
	knot_dump_style_t style;
} dump_changeset_ctx_t;

static int dump_rrset(const knot_rrset_t *rr, void *ctx_void)
{
	dump_changeset_ctx_t *ctx = ctx_void;

	int ret = knot_rrset_txt_dump(rr, &ctx->buf, &ctx->buflen, &ctx->style);
	if (ret > 0) {
		ret = (fprintf(ctx->f, "%s", ctx->buf) < 0 ? knot_map_errno() : 1);
	}
	return ret < 0 ? ret : KNOT_EOK;
}

static int dump_changeset_part(zone_update_t *up, bool additions,
                               const char *fname, const char *mode)
{
	dump_changeset_ctx_t ctx;
	ctx.f = fopen(fname, mode);
	if (ctx.f == NULL) {
		return knot_map_errno();
	}
	ctx.buflen = 1024;
	ctx.buf = malloc(ctx.buflen);
	if (ctx.buf == NULL) {
		(void)fclose(ctx.f);
		return KNOT_ENOMEM;
	}
	ctx.style = KNOT_DUMP_STYLE_DEFAULT;
	ctx.style.now = knot_time();

	int ret;
	if (fprintf(ctx.f, ";; %s records\n", additions ? "Added" : "Removed") > 0) {
		ret = zone_update_foreach(up, additions, dump_rrset, &ctx);
	} else {
		ret = knot_map_errno();
	}
	if (fclose(ctx.f) == -1 && ret == KNOT_EOK) {
		ret = knot_map_errno();
	}
	free(ctx.buf);
	return ret;
}

int zone_update_external(conf_t *conf, zone_update_t *update, conf_val_t *ev_id)
{
	/* First: dump zone/diff files as/if configured. */
	conf_val_t val = conf_id_get(conf, C_EXTERNAL, C_DUMP_NEW, ev_id);
	char *f_new = conf_get_filename(conf, update->zone->name, conf_str(&val));
	val = conf_id_get(conf, C_EXTERNAL, C_DUMP_REM, ev_id);
	char *f_rem = conf_get_filename(conf, update->zone->name, conf_str(&val));
	val = conf_id_get(conf, C_EXTERNAL, C_DUMP_ADD, ev_id);
	char *f_add = conf_get_filename(conf, update->zone->name, conf_str(&val));

	int ret = KNOT_EOK;
	if (f_new != NULL && ret == KNOT_EOK) {
		ret = zonefile_write(f_new, update->new_cont, NULL);
	}
	if (f_rem != NULL && ret == KNOT_EOK) {
		ret = dump_changeset_part(update, false, f_rem, "w");
	}
	if (f_add != NULL && ret == KNOT_EOK) {
		const char *mode = "w";
		if (f_rem != NULL && strcmp(f_rem, f_add) == 0) {
			mode = "a";
		}
		ret = dump_changeset_part(update, true, f_add, mode);
	}
	free(f_new);
	free(f_rem);
	free(f_add);
	if (ret != KNOT_EOK) {
		log_zone_error(update->zone->name,
		               "failed to dump new zone version or zone diff for external validation (%s)",
		               knot_strerror(ret));
	}

	/* Second: wait on semaphore on user's interaction. */
	pthread_mutex_lock(&update->zone->cu_lock);

	assert(update->zone->control_update == NULL);

	/* Don't start external validation if shutting down. */
	if (update->zone->server->state & ServerShutting) {
		pthread_mutex_unlock(&update->zone->cu_lock);
		return KNOT_EEXTERNAL;
	}

	update->zone->control_update = update;
	update->flags |= UPDATE_WFEV;
	knot_sem_init(&update->external, 0);
	pthread_mutex_unlock(&update->zone->cu_lock);

	log_zone_notice(update->zone->name, "waiting for external validation");

	if (conf->cache.srv_dbus_event & DBUS_EVENT_EXTERNAL) {
		dbus_emit_external_verify(update->zone->name, zone_contents_serial(update->new_cont));
	}

	val = conf_id_get(conf, C_EXTERNAL, C_TIMEOUT, ev_id);
	knot_sem_timedwait(&update->external, conf_int(&val) * 1000);

	pthread_mutex_lock(&update->zone->cu_lock);
	update->zone->control_update = NULL;
	pthread_mutex_unlock(&update->zone->cu_lock);

	knot_sem_post(&update->external);
	knot_sem_destroy(&update->external);

	return (update->flags & UPDATE_EVOK) ? KNOT_EOK : KNOT_EEXTERNAL;
}

int zone_update_commit(conf_t *conf, zone_update_t *update)
{
	if (conf == NULL || update == NULL) {
		return KNOT_EINVAL;
	}

	int ret = KNOT_EOK;

	conf_val_t val = conf_zone_get(conf, C_DNSSEC_SIGNING, update->zone->name);
	bool dnssec = conf_bool(&val);

	if ((update->flags & UPDATE_INCREMENTAL) && zone_update_no_change(update)) {
		if (dnssec && (update->flags & UPDATE_SIGNED_FULL)) {
			zone_set_flag(update->zone, ZONE_LAST_SIGN_OK);
		}
		zone_update_clear(update);
		return KNOT_EOK;
	}

	if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
		ret = commit_incremental(conf, update);
	} else {
		ret = commit_full(conf, update);
	}
	if (ret != KNOT_EOK) {
		return ret;
	}

	conf_val_t thr = conf_zone_get(conf, C_ADJUST_THR, update->zone->name);
	if ((update->flags & (UPDATE_HYBRID | UPDATE_FULL))) {
		ret = zone_adjust_full(update->new_cont, conf_int(&thr));
	} else {
		ret = zone_adjust_incremental_update(update, conf_int(&thr));
	}
	if (ret != KNOT_EOK) {
		discard_adds_tree(update);
		return ret;
	}

	/* Check the zone size. */
	val = conf_zone_get(conf, C_ZONE_MAX_SIZE, update->zone->name);
	size_t size_limit = conf_int(&val);

	if (update->new_cont->size > size_limit) {
		discard_adds_tree(update);
		return KNOT_EZONESIZE;
	}

	val = conf_zone_get(conf, C_DNSSEC_VALIDATION, update->zone->name);
	if (conf_bool(&val)) {
		validation_conf_t val_conf = {
			.conf = conf,
			.incremental = update->flags & UPDATE_INCREMENTAL,
			.log_plan = true,
		};
		ret = knot_dnssec_validate_zone(update, &val_conf);
		if (ret != KNOT_EOK) {
			discard_adds_tree(update);
			return ret;
		}
	}

	val = conf_zone_get(conf, C_EXTERNAL_VLDT, update->zone->name);
	if (val.code == KNOT_EOK && (update->flags & UPDATE_EVREQ)) {
		ret = zone_update_external(conf, update, &val);
		if (ret != KNOT_EOK) {
			discard_adds_tree(update);
			return ret;
		}
	}

	ret = update_catalog(conf, update);
	if (ret != KNOT_EOK) {
		log_zone_error(update->zone->name, "failed to process catalog zone (%s)", knot_strerror(ret));
		discard_adds_tree(update);
		return ret;
	}

	ret = commit_redis(conf, update);
	if (ret != KNOT_EOK) {
		log_zone_error(update->zone->name, "zone database update failed (%s)", knot_strerror(ret));
		discard_adds_tree(update);
		return ret;
	}

	ret = commit_journal(conf, update);
	if (ret != KNOT_EOK) {
		log_zone_error(update->zone->name, "journal update failed (%s)", knot_strerror(ret));
		discard_adds_tree(update);
		return ret;
	}

	if (dnssec) {
		zone_set_lastsigned_serial(conf, update->zone, zone_contents_serial(update->new_cont));

		if ((update->flags & UPDATE_SIGNED_FULL)) {
			zone_set_flag(update->zone, ZONE_LAST_SIGN_OK);
		}
	}

	/* Switch zone contents. */
	zone_contents_t *old_contents;
	old_contents = zone_switch_contents(update->zone, update->new_cont);

	if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
		changeset_clear(&update->change);
		changeset_clear(&update->extra_ch);
	}
	zone_contents_deep_free(update->init_cont);

	update_clear_ctx_t *clear_ctx = calloc(1, sizeof(*clear_ctx));
	if (clear_ctx != NULL) {
		clear_ctx->retry_thread = &update->zone->update_clear_thr;
		clear_ctx->free_contents = old_contents;
		clear_ctx->free_method = (
			(update->flags & (UPDATE_FULL | UPDATE_HYBRID)) ?
			zone_contents_deep_free : update_free_zone
		);
		clear_ctx->cleanup_apply = update->a_ctx;
		clear_ctx->new_cont_size = update->new_cont->size;

		call_rcu((struct rcu_head *)clear_ctx, update_clear);
	} else {
		log_zone_error(update->zone->name, "failed to deallocate unused memory");
	}

	zone_local_notify(conf, update->zone);

	/* Sync zonefile immediately if configured. */
	val = conf_zone_get(conf, C_ZONEFILE_SYNC, update->zone->name);
	if (conf_int(&val) == 0) {
		zone_events_schedule_now(update->zone, ZONE_EVENT_FLUSH);
	}

	if (conf->cache.srv_dbus_event & DBUS_EVENT_ZONE_UPDATED) {
		dbus_emit_zone_updated(update->zone->name, true,
		                       zone_contents_serial(update->zone->contents));
	}

	memset(update, 0, sizeof(*update));

	return KNOT_EOK;
}

static int no_change(const knot_rrset_t *rr, void *ctx)
{
	(void)ctx;
	return knot_rrset_empty(rr) ? KNOT_EOK : 1;
}

bool zone_update_no_change(zone_update_t *update)
{
	if (update == NULL) {
		return true;
	}

	if (update->flags & UPDATE_NO_CHSET) {
		return zone_update_foreach(update, false, no_change, NULL) == KNOT_EOK &&
		       zone_update_foreach(update, true, no_change, NULL) == KNOT_EOK;
	} else if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
		return changeset_empty(&update->change);
	} else {
		return zone_contents_is_empty(update->new_cont);
	}
}

static int rrset_foreach(zone_node_t *n, bool subtract_counterpart,
                         int idx, uint16_t rrtype, // two exclusive possibilities how to define which rrset in the node
                         rrset_cb_t cb, void *ctx)
{
	knot_rrset_t rr = (rrtype == KNOT_RRTYPE_ANY) ? node_rrset_at(n, idx) : node_rrset(n, rrtype);
	knot_rrset_t rrc = subtract_counterpart ? node_rrset(binode_counterpart(n), rr.type) : (knot_rrset_t){ 0 };
	if (rrtype == KNOT_RRTYPE_ANY && rr.type == KNOT_RRTYPE_SOA) {
		return KNOT_EOK; // ignore SOA if rrset specified by idx
	} else if (knot_rrset_empty(&rrc)) {
		return cb(&rr, ctx);
	} else if (knot_rdataset_subset(&rr.rrs, &rrc.rrs)) {
		return KNOT_EOK;
	} else {
		knot_rdataset_t rd_copy = { 0 };
		int ret = knot_rdataset_copy(&rd_copy, &rr.rrs, NULL);
		if (ret == KNOT_EOK) {
			ret = knot_rdataset_subtract(&rd_copy, &rrc.rrs, NULL);
		}
		if (ret == KNOT_EOK) {
			rr.rrs = rd_copy;
			ret = cb(&rr, ctx);
		}
		knot_rdataset_clear(&rd_copy, NULL);
		return ret;
	}
}

static int trees_foreach(zone_tree_t *nodes, zone_tree_t *nsec3_nodes, bool subtract_counterparts,
                         rrset_cb_t cb, void *ctx)
{
	zone_tree_it_t it = { 0 };
	int ret = zone_tree_it_double_begin(nodes, nsec3_nodes, &it);
	while (!zone_tree_it_finished(&it) && ret == KNOT_EOK) {
		zone_node_t *n = zone_tree_it_val(&it);
		for (int i = 0; i < n->rrset_count && ret == KNOT_EOK; i++) {
			ret = rrset_foreach(n, subtract_counterparts, i, KNOT_RRTYPE_ANY, cb, ctx);
		}
		zone_tree_it_next(&it);
	}
	zone_tree_it_free(&it);
	return ret;
}

int zone_update_foreach(zone_update_t *update, bool additions, rrset_cb_t cb, void *ctx)
{
	if (update == NULL) {
		return KNOT_EINVAL;
	}

	if (update->flags & UPDATE_NO_CHSET) {
		zone_diff_t diff;
		get_zone_diff(&diff, update);
		if (!additions) {
			zone_diff_reverse(&diff);
		}

		int ret = rrset_foreach(diff.apex, true, 0, KNOT_RRTYPE_SOA, cb, ctx);
		if (ret == KNOT_EOK) {
			ret = trees_foreach(&diff.nodes, &diff.nsec3s, true, cb, ctx);
		}
		return ret;
	} else if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
		knot_rrset_t *soa = additions ? update->change.soa_to : update->change.soa_from;
		zone_contents_t *c = additions ? update->change.add : update->change.remove;
		int ret = (soa == NULL || update->change.soa_to == NULL) ? KNOT_EOK : cb(soa, ctx);
		if (ret == KNOT_EOK) {
			ret = trees_foreach(c->nodes, c->nsec3_nodes, false, cb, ctx);
		}
		return ret;
	} else if (additions) {
		knot_rrset_t soa = node_rrset(update->new_cont->apex, KNOT_RRTYPE_SOA);
		int ret = knot_rrset_empty(&soa) ? KNOT_EOK : cb(&soa, ctx);
		if (ret == KNOT_EOK) {
			ret = trees_foreach(update->new_cont->nodes, update->new_cont->nsec3_nodes, false, cb, ctx);
		}
		return ret;
	} else {
		return KNOT_EOK;
	}
}

static bool zone_diff_rdataset(const zone_contents_t *c, uint16_t rrtype)
{
	const knot_rdataset_t *a = node_rdataset(binode_counterpart(c->apex), rrtype);
	const knot_rdataset_t *b = node_rdataset(c->apex, rrtype);
	if ((a == NULL && b == NULL) || (a != NULL && b != NULL && a->rdata == b->rdata)) {
		return false;
	} else {
		return !knot_rdataset_eq(a, b);
	}
}

static bool contents_have_dnskey(const zone_contents_t *contents)
{
	if (contents == NULL) {
		return false;
	}
	assert(contents->apex != NULL);
	return (node_rrtype_exists(contents->apex, KNOT_RRTYPE_DNSKEY) ||
	        node_rrtype_exists(contents->apex, KNOT_RRTYPE_CDNSKEY) ||
		node_rrtype_exists(contents->apex, KNOT_RRTYPE_CDS));
}

bool zone_update_changes_dnskey(zone_update_t *update)
{
	if (update->flags & UPDATE_NO_CHSET) {
		return (zone_diff_rdataset(update->new_cont, KNOT_RRTYPE_DNSKEY) ||
		        zone_diff_rdataset(update->new_cont, KNOT_RRTYPE_CDNSKEY) ||
		        zone_diff_rdataset(update->new_cont, KNOT_RRTYPE_CDS));
	} else if (update->flags & UPDATE_FULL) {
		return contents_have_dnskey(update->new_cont);
	} else {
		return (contents_have_dnskey(update->change.remove) ||
		        contents_have_dnskey(update->change.add));
	}
}

static bool soa_differs_just_serial(const knot_rrset_t *soa1, const knot_rrset_t *soa2)
{
	return soa1 != NULL && soa2 != NULL && soa1->rrs.count == 1 && soa2->rrs.count == 1 &&
	       knot_dname_is_equal(knot_soa_primary(soa1->rrs.rdata), knot_soa_primary(soa2->rrs.rdata)) &&
	       knot_dname_is_equal(knot_soa_mailbox(soa1->rrs.rdata), knot_soa_mailbox(soa2->rrs.rdata)) &&
	       knot_soa_refresh(soa1->rrs.rdata) == knot_soa_refresh(soa2->rrs.rdata) &&
	       knot_soa_retry(soa1->rrs.rdata)   == knot_soa_retry(soa2->rrs.rdata) &&
	       knot_soa_minimum(soa1->rrs.rdata) == knot_soa_minimum(soa2->rrs.rdata) &&
	       knot_soa_expire(soa1->rrs.rdata)  == knot_soa_expire(soa2->rrs.rdata);
}

static int diff_just_ser_cb(const knot_rrset_t *rr, void *ctx)
{
	bool ignore_zonemd = *((bool *)ctx);
	if (rr->type == KNOT_RRTYPE_SOA || (ignore_zonemd && rr->type == KNOT_RRTYPE_ZONEMD)) {
		return KNOT_EOK;
	}
	if (rr->type != KNOT_RRTYPE_RRSIG) {
		return 1;
	}
	knot_rdata_t *rd = rr->rrs.rdata;
	for (int i = 0; i < rr->rrs.count; i++) {
		uint16_t type = knot_rrsig_type_covered(rd);
		if (type != KNOT_RRTYPE_SOA && (!ignore_zonemd || type != KNOT_RRTYPE_ZONEMD)) {
			return 1;
		}
		rd = knot_rdataset_next(rd);
	}
	return KNOT_EOK;
}

int zone_update_differs_just_serial(zone_update_t *update, bool ignore_zonemd)
{
	int ret = zone_update_foreach(update, false, diff_just_ser_cb, &ignore_zonemd);
	if (ret == KNOT_EOK) {
		ret = zone_update_foreach(update, true, diff_just_ser_cb, &ignore_zonemd);
	}
	if (ret != KNOT_EOK || (update->flags & UPDATE_FULL)) {
		return ret;
	}
	if (!(update->flags & UPDATE_NO_CHSET)) {
		return soa_differs_just_serial(update->change.soa_from, update->change.soa_to) ? KNOT_EOK : 1;
	}
	knot_rrset_t soa1 = node_rrset(binode_counterpart(update->new_cont->apex), KNOT_RRTYPE_SOA);
	knot_rrset_t soa2 = node_rrset(update->new_cont->apex, KNOT_RRTYPE_SOA);
	return soa_differs_just_serial(&soa1, &soa2) ? KNOT_EOK : 1;
}

static int ch_rem_cb(const knot_rrset_t *rr, void *ctx)
{
	return changeset_add_removal(ctx, rr, 0);
}

static int ch_add_cb(const knot_rrset_t *rr, void *ctx)
{
	return changeset_add_addition(ctx, rr, 0);
}

int zone_update_to_changeset(zone_update_t *update, changeset_t **ch)
{
	if (update == NULL || ch == NULL || (update->flags & UPDATE_FULL)) {
		return KNOT_EINVAL;
	}

	if (!(update->flags & UPDATE_NO_CHSET)) {
		*ch = changeset_clone(&update->change);
		return *ch == NULL ? KNOT_ENOMEM : KNOT_EOK;
	}

	*ch = changeset_new(update->zone->name);
	if (*ch == NULL) {
		return KNOT_ENOMEM;
	}

	int ret = zone_update_foreach(update, false, ch_rem_cb, *ch);
	if (ret == KNOT_EOK) {
		ret = zone_update_foreach(update, true, ch_add_cb, *ch);
	}
	if (ret != KNOT_EOK) {
		changeset_free(*ch);
		*ch = NULL;
	}
	return ret;
}
