/*  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 "contrib/macros.h"
#include "knot/common/dbus.h"
#include "knot/common/log.h"
#include "knot/dnssec/kasp/keystate.h"
#include "knot/dnssec/key-events.h"
#include "knot/dnssec/policy.h"
#include "knot/dnssec/zone-keys.h"
#include "knot/zone/serial.h"
#include "libdnssec/random.h"

static bool not_removed(const knot_kasp_key_t *key, knot_time_t now)
{
	key_state_t state = get_key_state(key, now);
	return state != DNSSEC_KEY_STATE_REMOVED && state != DNSSEC_KEY_STATE_INVALID;
}

static bool key_present(const kdnssec_ctx_t *ctx, bool ksk, bool zsk)
{
	assert(ctx);
	assert(ctx->zone);
	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		const knot_kasp_key_t *key = &ctx->zone->keys[i];
		if (key->is_ksk == ksk && key->is_zsk == zsk && !key->is_pub_only &&
		    not_removed(key, ctx->now)) {
			return true;
		}
	}
	return false;
}

static bool key_id_present(const kdnssec_ctx_t *ctx, const char *keyid, bool want_ksk)
{
	assert(ctx);
	assert(ctx->zone);
	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		const knot_kasp_key_t *key = &ctx->zone->keys[i];
		if (strcmp(keyid, key->id) == 0 &&
		    key->is_ksk == want_ksk && not_removed(key, ctx->now)) {
			return true;
		}
	}
	return false;
}

static unsigned algorithm_present(const kdnssec_ctx_t *ctx, uint8_t alg)
{
	assert(ctx);
	assert(ctx->zone);
	unsigned ret = 0;
	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		const knot_kasp_key_t *key = &ctx->zone->keys[i];
		knot_time_t activated = knot_time_min(key->timing.pre_active, key->timing.ready);
		if (knot_time_cmp(knot_time_min(activated, key->timing.active), ctx->now) <= 0 &&
		    not_removed(key, ctx->now) &&
		    dnssec_key_get_algorithm(key->key) == alg && !key->is_pub_only) {
			ret++;
		}
	}
	return ret;
}

static bool signing_scheme_present(const kdnssec_ctx_t *ctx)
{
	if (ctx->policy->single_type_signing) {
		return (!key_present(ctx, true, false) || !key_present(ctx, false, true) || key_present(ctx, true, true));
	} else {
		return (key_present(ctx, true, false) && key_present(ctx, false, true));
	}
}

static knot_kasp_key_t *key_get_by_id(kdnssec_ctx_t *ctx, const char *keyid)
{
	assert(ctx);
	assert(ctx->zone);
	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		knot_kasp_key_t *key = &ctx->zone->keys[i];
		if (strcmp(keyid, key->id) == 0) {
			return key;
		}
	}
	return NULL;
}

static int clear_future_timer(knot_time_t *timer, knot_time_t now)
{
	if (*timer > now) { // includes the fact that *timer != 0
		*timer = 0;
		return 1;
	}
	return 0;
}

// this is intended for automatic key management with keys imported from Bind
static void clear_future_timers(knot_kasp_key_t *key, kdnssec_ctx_t *ctx)
{
	int change = 0;
	// untouched timer created, as it should be never in the future
	change += clear_future_timer(&key->timing.pre_active, ctx->now);
	change += clear_future_timer(&key->timing.publish, ctx->now);
	change += clear_future_timer(&key->timing.ready, ctx->now);
	change += clear_future_timer(&key->timing.active, ctx->now);
	change += clear_future_timer(&key->timing.retire_active, ctx->now);
	if (key->timing.retire_active == 0) { // otherwise those timers are in the future normally
		change += clear_future_timer(&key->timing.retire, ctx->now);
		change += clear_future_timer(&key->timing.remove, ctx->now);
	}
	// untouched timer post_active, as it's normally in the future and it's not importable from Bind anyway
	// untouched timer revoke as that is another topic

	if (change > 0) {
		log_zone_notice(ctx->zone->dname , "DNSSEC, cleared future timers of auto-managed key %hu",
		                dnssec_key_get_keytag(key->key));
		(void)kdnssec_ctx_commit(ctx);
	}
}

static int generate_key(kdnssec_ctx_t *ctx, kdnssec_generate_flags_t flags,
			knot_time_t when_active, bool pre_active)
{
	assert(!pre_active || when_active == 0);

	knot_kasp_key_t *key = NULL;
	int ret = kdnssec_generate_key(ctx, flags, &key);
	if (ret != KNOT_EOK) {
		return ret;
	}

	key->timing.remove = 0;
	key->timing.retire = 0;
	key->timing.active = ((flags & DNSKEY_GENERATE_KSK) ? 0 : when_active);
	key->timing.ready  = ((flags & DNSKEY_GENERATE_KSK) ? when_active : 0);
	key->timing.publish    = (pre_active ? 0 : ctx->now);
	key->timing.pre_active = (pre_active ? ctx->now : 0);

	return KNOT_EOK;
}

static int share_or_generate_key(kdnssec_ctx_t *ctx, kdnssec_generate_flags_t flags,
				 knot_time_t when_active, bool pre_active)
{
	assert(!pre_active || when_active == 0);

	knot_dname_t *borrow_zone = NULL;
	char *borrow_key = NULL;

	if (!(flags & DNSKEY_GENERATE_KSK)) {
		return KNOT_EINVAL;
	} // for now not designed for rotating shared ZSK

	int ret = kasp_db_get_policy_last(ctx->kasp_db, ctx->policy->string,
	                                  &borrow_zone, &borrow_key);
	if (ret != KNOT_EOK && ret != KNOT_ENOENT) {
		free(borrow_zone);
		free(borrow_key);
		return ret;
	}

	// if we already have the policy-last key, we have to generate new one
	if (ret == KNOT_ENOENT || key_id_present(ctx, borrow_key, true) ||
	    kasp_db_get_key_algorithm(ctx->kasp_db, borrow_zone, borrow_key) != (int)ctx->policy->algorithm) {
		knot_kasp_key_t *key = NULL;
		ret = kdnssec_generate_key(ctx, flags, &key);
		if (ret != KNOT_EOK) {
			return ret;
		}
		key->timing.remove = 0;
		key->timing.retire = 0;
		key->timing.active = ((flags & DNSKEY_GENERATE_KSK) ? 0 : when_active);
		key->timing.ready  = ((flags & DNSKEY_GENERATE_KSK) ? when_active : 0);
		key->timing.publish    = (pre_active ? 0 : ctx->now);
		key->timing.pre_active = (pre_active ? ctx->now : 0);

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

		ret = kasp_db_set_policy_last(ctx->kasp_db, ctx->policy->string,
		                              borrow_key, ctx->zone->dname, key->id);
		free(borrow_zone);
		free(borrow_key);
		borrow_zone = NULL;
		borrow_key = NULL;
		if (ret != KNOT_ESEMCHECK) {
			// all ok, we generated new kay and updated policy-last
			return ret;
		} else {
			// another zone updated policy-last key in the meantime
			ret = kdnssec_delete_key(ctx, key);
			if (ret == KNOT_EOK) {
				ret = kdnssec_ctx_commit(ctx);
			}
			if (ret != KNOT_EOK) {
				return ret;
			}

			ret = kasp_db_get_policy_last(ctx->kasp_db, ctx->policy->string,
			                              &borrow_zone, &borrow_key);
		}
	}

	if (ret == KNOT_EOK) {
		ret = kdnssec_share_key(ctx, borrow_zone, borrow_key);
		if (ret == KNOT_EOK) {
			knot_kasp_key_t *newkey = key_get_by_id(ctx, borrow_key);
			assert(newkey != NULL);
			newkey->timing.remove = 0;
			newkey->timing.retire = 0;
			newkey->timing.active = ((flags & DNSKEY_GENERATE_KSK) ? 0 : when_active);
			newkey->timing.ready  = ((flags & DNSKEY_GENERATE_KSK) ? when_active : 0);
			newkey->timing.publish    = (pre_active ? 0 : ctx->now);
			newkey->timing.pre_active = (pre_active ? ctx->now : 0);
			newkey->is_ksk = (flags & DNSKEY_GENERATE_KSK);
			newkey->is_zsk = (flags & DNSKEY_GENERATE_ZSK);
		}
	}
	free(borrow_zone);
	free(borrow_key);
	return ret;
}

#define GEN_KSK_FLAGS (DNSKEY_GENERATE_KSK | (ctx->policy->single_type_signing ? DNSKEY_GENERATE_ZSK : 0))

static int generate_ksk(kdnssec_ctx_t *ctx, knot_time_t when_active, bool pre_active)
{
	if (ctx->policy->ksk_shared) {
		return share_or_generate_key(ctx, GEN_KSK_FLAGS, when_active, pre_active);
	} else {
		return generate_key(ctx, GEN_KSK_FLAGS, when_active, pre_active);
	}
}

static bool running_rollover(const kdnssec_ctx_t *ctx)
{
	bool res = false;
	int ready_ksk = 0, active_ksk = 0, active_zsk = 0;

	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		knot_kasp_key_t *key = &ctx->zone->keys[i];
		if (key->is_pub_only) {
			continue;
		}
		switch (get_key_state(key, ctx->now)) {
		case DNSSEC_KEY_STATE_PRE_ACTIVE:
			res = true;
			break;
		case DNSSEC_KEY_STATE_PUBLISHED:
			res = true;
			break;
		case DNSSEC_KEY_STATE_READY:
			ready_ksk += (key->is_ksk ? 1 : 0);
			break;
		case DNSSEC_KEY_STATE_ACTIVE:
			active_ksk += (key->is_ksk ? 1 : 0);
			active_zsk += (key->is_zsk ? 1 : 0);
			break;
		case DNSSEC_KEY_STATE_RETIRE_ACTIVE:
		case DNSSEC_KEY_STATE_POST_ACTIVE:
			res = true;
			break;
		case DNSSEC_KEY_STATE_RETIRED:
		case DNSSEC_KEY_STATE_REMOVED:
		default:
			break;
		}
	}
	if (ready_ksk + active_ksk > 1 || active_zsk > 1) {
		res = true;
	}
	return res;
}

typedef enum {
	INVALID = 0,
	GENERATE = 1,
	PUBLISH,
	SUBMIT,
	REPLACE,
	RETIRE,
	REMOVE,
	REALLY_REMOVE,
} roll_action_type_t;

typedef struct {
	roll_action_type_t type;
	bool ksk; // These flags seem redundant, but are needed to avoid ASAN
	bool zsk; // heap-use-after-free if the key is accessed directly during key generation.
	knot_time_t time;
	knot_kasp_key_t *key;
	uint16_t ready_keytag;
	const char *ready_keyid;
} roll_action_t;

static const char *roll_action_name(roll_action_type_t type)
{
	switch (type) {
	case GENERATE: return "generate";
	case PUBLISH:  return "publish";
	case SUBMIT:   return "submit";
	case REPLACE:  return "replace";
	case RETIRE:   return "retire";
	case REMOVE:   return "remove";
	case INVALID:
		// FALLTHROUGH
	default:       return "invalid";
	}
}

static knot_time_t zsk_rollover_time(knot_time_t active_time, const kdnssec_ctx_t *ctx)
{
	if (active_time <= 0 || ctx->policy->zsk_lifetime == 0) {
		return 0;
	}
	return knot_time_plus(active_time, ctx->policy->zsk_lifetime);
}

static knot_time_t zsk_active_time(knot_time_t publish_time, const kdnssec_ctx_t *ctx)
{
	if (publish_time <= 0) {
		return 0;
	}
	return knot_time_add(publish_time, ctx->policy->propagation_delay + ctx->policy->saved_key_ttl);
}

static knot_time_t zsk_remove_time(knot_time_t retire_time, const kdnssec_ctx_t *ctx)
{
	if (retire_time <= 0) {
		return 0;
	}
	return knot_time_add(retire_time, ctx->policy->propagation_delay + ctx->policy->saved_max_ttl);
}

static knot_time_t ksk_rollover_time(knot_time_t created_time, const kdnssec_ctx_t *ctx)
{
	if (created_time <= 0 || ctx->policy->ksk_lifetime == 0) {
		return 0;
	}
	return knot_time_plus(created_time, ctx->policy->ksk_lifetime);
}

static knot_time_t ksk_ready_time(knot_time_t publish_time, const kdnssec_ctx_t *ctx)
{
	if (publish_time <= 0) {
		return 0;
	}
	return knot_time_add(publish_time, ctx->policy->propagation_delay + ctx->policy->saved_key_ttl);
}

static knot_time_t ksk_sbm_max_time(knot_time_t ready_time, const kdnssec_ctx_t *ctx)
{
	if (ready_time <= 0 || ctx->policy->ksk_sbm_timeout == 0) {
		return 0;
	}
	return knot_time_plus(ready_time, ctx->policy->ksk_sbm_timeout);
}

static knot_time_t ksk_retire_time(knot_time_t retire_active_time, const kdnssec_ctx_t *ctx)
{
	if (retire_active_time <= 0) {
		return 0;
	}
	// this is not correct! It should be parent DS TTL.
	return knot_time_add(retire_active_time, ctx->policy->propagation_delay + ctx->policy->saved_key_ttl);
}

static knot_time_t ksk_remove_time(knot_time_t retire_time, bool is_csk, const kdnssec_ctx_t *ctx)
{
	if (retire_time <= 0) {
		return 0;
	}
	knot_timediff_t use_ttl = ctx->policy->saved_key_ttl;
	if (is_csk) {
		use_ttl = ctx->policy->saved_max_ttl;
	}
	return knot_time_add(retire_time, ctx->policy->propagation_delay + use_ttl);
}

static knot_time_t ksk_really_remove_time(knot_time_t remove_time, const kdnssec_ctx_t *ctx)
{
	if (ctx->keep_deleted_keys) {
		return 0;
	}
	return knot_time_add(remove_time, ctx->policy->delete_delay);
}

static knot_time_t zsk_really_remove_time(knot_time_t remove_time, const kdnssec_ctx_t *ctx)
{
	if (ctx->keep_deleted_keys) {
		return 0;
	}
	return knot_time_add(remove_time, ctx->policy->delete_delay);
}

// algorithm rollover related timers must be the same for KSK and ZSK

static knot_time_t alg_publish_time(knot_time_t pre_active_time, const kdnssec_ctx_t *ctx)
{
	if (pre_active_time <= 0) {
		return 0;
	}
	return knot_time_add(pre_active_time, ctx->policy->propagation_delay + ctx->policy->saved_max_ttl);
}

static knot_time_t alg_remove_time(knot_time_t post_active_time, const kdnssec_ctx_t *ctx)
{
	return knot_time_add(post_active_time, ctx->policy->propagation_delay + ctx->policy->saved_key_ttl);
}

static roll_action_t next_action(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flags)
{
	roll_action_t res = { 0 };

	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		knot_kasp_key_t *key = &ctx->zone->keys[i];
		knot_time_t keytime = 0;
		roll_action_type_t restype = INVALID;
		if (key->is_pub_only ||
		    (key->is_ksk && !(flags & KEY_ROLL_ALLOW_KSK_ROLL)) ||
		    (key->is_zsk && !(flags & KEY_ROLL_ALLOW_ZSK_ROLL))) {
			continue;
		}
		if (!(flags & KEY_ROLL_PRESERVE_FUTURE)) {
			clear_future_timers(key, ctx);
		}
		if (key->is_ksk) {
			switch (get_key_state(key, ctx->now)) {
			case DNSSEC_KEY_STATE_PRE_ACTIVE:
				keytime = alg_publish_time(key->timing.pre_active, ctx);
				restype = PUBLISH;
				break;
			case DNSSEC_KEY_STATE_PUBLISHED:
				keytime = ksk_ready_time(key->timing.publish, ctx);
				restype = SUBMIT;
				break;
			case DNSSEC_KEY_STATE_READY:
				keytime = ksk_sbm_max_time(key->timing.ready, ctx);
				restype = REPLACE;
				res.ready_keyid = key->id;
				res.ready_keytag = dnssec_key_get_keytag(key->key);
				break;
			case DNSSEC_KEY_STATE_ACTIVE:
				if (!running_rollover(ctx) &&
				    dnssec_key_get_algorithm(key->key) == ctx->policy->algorithm) {
					knot_time_t ksk_created = key->timing.created == 0 ?
					                          key->timing.active :
					                          key->timing.created;
					keytime = ksk_rollover_time(ksk_created, ctx);
					restype = GENERATE;
				}
				break;
			case DNSSEC_KEY_STATE_RETIRE_ACTIVE:
				if (key->timing.retire == 0 && key->timing.post_active == 0 && key->timing.remove == 0) { // this shouldn't normally happen
					// when a KSK is retire_active, it has already some following timer set
					keytime = ksk_retire_time(key->timing.retire_active, ctx);
					restype = RETIRE;
				}
				break;
			case DNSSEC_KEY_STATE_POST_ACTIVE:
				keytime = alg_remove_time(key->timing.post_active, ctx);
				restype = REMOVE;
				break;
			case DNSSEC_KEY_STATE_RETIRED:
				keytime = knot_time_min(key->timing.retire, key->timing.remove);
				keytime = ksk_remove_time(keytime, key->is_zsk, ctx);
				restype = REMOVE;
				break;
			case DNSSEC_KEY_STATE_REMOVED:
				keytime = ksk_really_remove_time(key->timing.remove, ctx);
				if (knot_time_cmp(keytime, ctx->now) > 0) {
					keytime = 0;
				}
				restype = REALLY_REMOVE;
				break;
			default:
				continue;
			}
		} else {
			switch (get_key_state(key, ctx->now)) {
			case DNSSEC_KEY_STATE_PRE_ACTIVE:
				keytime = alg_publish_time(key->timing.pre_active, ctx);
				restype = PUBLISH;
				break;
			case DNSSEC_KEY_STATE_PUBLISHED:
				keytime = zsk_active_time(key->timing.publish, ctx);
				restype = REPLACE;
				break;
			case DNSSEC_KEY_STATE_ACTIVE:
				if (!running_rollover(ctx) &&
				    dnssec_key_get_algorithm(key->key) == ctx->policy->algorithm) {
					keytime = zsk_rollover_time(key->timing.active, ctx);
					restype = GENERATE;
				}
				break;
			case DNSSEC_KEY_STATE_RETIRE_ACTIVE:
				// simply waiting for submitted KSK to retire me.
				break;
			case DNSSEC_KEY_STATE_POST_ACTIVE:
				keytime = alg_remove_time(key->timing.post_active, ctx);
				restype = REMOVE;
				break;
			case DNSSEC_KEY_STATE_RETIRED:
				keytime = knot_time_min(key->timing.retire, key->timing.remove);
				keytime = zsk_remove_time(keytime, ctx);
				restype = REMOVE;
				break;
			case DNSSEC_KEY_STATE_REMOVED:
				keytime = zsk_really_remove_time(key->timing.remove, ctx);
				if (knot_time_cmp(keytime, ctx->now) > 0) {
					keytime = 0;
				}
				restype = REALLY_REMOVE;
				break;
			case DNSSEC_KEY_STATE_READY:
			default:
				continue;
			}
		}
		if (knot_time_cmp(keytime, res.time) < 0) {
			res.key = key;
			res.ksk = key->is_ksk;
			res.zsk = key->is_zsk;
			res.time = keytime;
			res.type = restype;
		}
	}

	return res;
}

static int submit_key(kdnssec_ctx_t *ctx, knot_kasp_key_t *newkey)
{
	assert(get_key_state(newkey, ctx->now) == DNSSEC_KEY_STATE_PUBLISHED);
	assert(newkey->is_ksk);

	// pushing from READY into ACTIVE decreases the other key's cds_priority
	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		knot_kasp_key_t *key = &ctx->zone->keys[i];
		if (key->is_ksk && !key->is_pub_only &&
		    get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_READY) {
			key->timing.active = ctx->now;
		}
	}

	newkey->timing.ready = ctx->now;
	return KNOT_EOK;
}

knot_kasp_key_t *knot_dnssec_key2retire(kdnssec_ctx_t *ctx, knot_kasp_key_t *newkey)
{
	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		knot_kasp_key_t *key = &ctx->zone->keys[i];
		key_state_t keystate = get_key_state(key, ctx->now);
		if (((newkey->is_ksk && key->is_ksk) || (!newkey->is_ksk && !key->is_ksk))
		    && (keystate == DNSSEC_KEY_STATE_ACTIVE)) {
			return key;
		}
	}
	return NULL;
}

static knot_kasp_key_t *zsk2retire(kdnssec_ctx_t *ctx, knot_kasp_key_t *newksk)
{
	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		knot_kasp_key_t *key = &ctx->zone->keys[i];
		key_state_t keystate = get_key_state(key, ctx->now);
		uint8_t keyalg = dnssec_key_get_algorithm(key->key);
		bool algdiff = (keyalg != dnssec_key_get_algorithm(newksk->key));

		if (key->is_zsk && !key->is_ksk &&
		    (algdiff || newksk->is_zsk) &&
		    (keystate == DNSSEC_KEY_STATE_ACTIVE ||
		     keystate == DNSSEC_KEY_STATE_RETIRE_ACTIVE)) {
			return key;
		}
	}
	return NULL;
}

static void log_next_event(kdnssec_ctx_t *ctx, roll_action_t *next)
{
	char time_str[64] = "";
	struct tm time_gm = { 0 };
	time_t nt = next->time;
	localtime_r(&nt, &time_gm);
	strftime(time_str, sizeof(time_str), KNOT_LOG_TIME_FORMAT, &time_gm);

	if (next->type == GENERATE) {
		const char *key_type = ctx->policy->single_type_signing ?
			"CSK" : (next->ksk ? "KSK" : "ZSK");
		log_zone_info(ctx->zone->dname, "DNSSEC, next key action, %s, generate at %s",
		              key_type, time_str);
	} else {
		const char *key_type = next->ksk ?
			(next->zsk ? "CSK" : "KSK") : "ZSK";
		log_zone_info(ctx->zone->dname, "DNSSEC, next key action, %s tag %hu, %s at %s",
		              key_type, dnssec_key_get_keytag(next->key->key),
		              roll_action_name(next->type), time_str);
	}
}


static int exec_new_signatures(kdnssec_ctx_t *ctx, knot_kasp_key_t *newkey, uint32_t active_retire_delay)
{
	if (newkey->is_ksk) {
		log_zone_notice(ctx->zone->dname, "DNSSEC, KSK submission, confirmed");
	}

	knot_kasp_key_t *oldkey = knot_dnssec_key2retire(ctx, newkey), *oldzsk = NULL;
	if (oldkey != NULL) {
		uint8_t keyalg = dnssec_key_get_algorithm(oldkey->key);
		bool algdiff = (keyalg != dnssec_key_get_algorithm(newkey->key));
		roll_action_t log_action = {
			.key = oldkey,
			.ksk = oldkey->is_ksk,
			.zsk = oldkey->is_zsk,
			.time = ctx->now + active_retire_delay
		};

		if (algdiff) {
			oldkey->timing.retire_active = ctx->now;
			if (oldkey->is_ksk) {
				oldkey->timing.post_active = ctx->now + active_retire_delay;
				log_action.type = RETIRE;
			}
		} else if (oldkey->is_ksk) {
			oldkey->timing.retire_active = ctx->now;
			if (oldkey->is_zsk) { // CSK
				oldkey->timing.retire = ctx->now + active_retire_delay;
				log_action.type = RETIRE;
			} else {
				oldkey->timing.remove = ctx->now + active_retire_delay;
				log_action.type = REMOVE;
			}
		} else {
			oldkey->timing.retire = ctx->now;
		}

		if (newkey->is_ksk && (oldzsk = zsk2retire(ctx, newkey)) != NULL) {
			if (algdiff) {
				oldzsk->timing.post_active = ctx->now + active_retire_delay;
			} else {
				oldzsk->timing.retire = ctx->now;
			}
		}

		if (log_action.type != INVALID) {
			log_next_event(ctx, &log_action);
		}
	}

	if (newkey->is_ksk) {
		assert(get_key_state(newkey, ctx->now) == DNSSEC_KEY_STATE_READY);
	} else {
		assert(get_key_state(newkey, ctx->now) == DNSSEC_KEY_STATE_PUBLISHED);
	}
	newkey->timing.active = knot_time_min(ctx->now, newkey->timing.active);

	return KNOT_EOK;
}

static int exec_publish(kdnssec_ctx_t *ctx, knot_kasp_key_t *key)
{
	assert(get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_PRE_ACTIVE);
	key->timing.publish = ctx->now;

	return KNOT_EOK;
}

static int exec_ksk_retire(kdnssec_ctx_t *ctx, knot_kasp_key_t *key)
{
	bool alg_rollover = false;
	knot_kasp_key_t *alg_rollover_friend = NULL;

	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		knot_kasp_key_t *k = &ctx->zone->keys[i];
		int magic = (k->is_ksk && k->is_zsk ? 2 : 3); // :(
		if (k->is_zsk && get_key_state(k, ctx->now) == DNSSEC_KEY_STATE_RETIRE_ACTIVE &&
		    algorithm_present(ctx, dnssec_key_get_algorithm(k->key)) < magic) {
			alg_rollover = true;
			alg_rollover_friend = k;
		}
	}

	assert(get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_RETIRE_ACTIVE);

	if (alg_rollover) {
		key->timing.post_active = ctx->now;
		alg_rollover_friend->timing.post_active = ctx->now;
	} else {
		key->timing.retire = ctx->now;
	}

	return KNOT_EOK;
}

static int exec_remove_old_key(kdnssec_ctx_t *ctx, knot_kasp_key_t *key)
{
	assert(get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_RETIRED ||
	       get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_POST_ACTIVE ||
	       get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_REMOVED);
	key->timing.remove = ctx->now;
	return KNOT_EOK;
}

static int exec_really_remove(kdnssec_ctx_t *ctx, knot_kasp_key_t *key)
{
	assert(get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_REMOVED);
	assert(!ctx->keep_deleted_keys);
	return kdnssec_delete_key(ctx, key);
}

int knot_dnssec_key_rollover(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flags,
                             zone_sign_reschedule_t *reschedule)
{
	if (ctx == NULL || reschedule == NULL) {
		return KNOT_EINVAL;
	}
	if (ctx->policy->manual) {
		if ((flags & (KEY_ROLL_FORCE_KSK_ROLL | KEY_ROLL_FORCE_ZSK_ROLL))) {
			log_zone_notice(ctx->zone->dname, "DNSSEC, ignoring forced key rollover "
							  "due to manual policy");
		}
		return KNOT_EOK;
	}
	int ret = KNOT_EOK;
	uint16_t ready_keytag = 0;
	const char *ready_keyid = NULL;
	bool allowed_general_roll = ((flags & KEY_ROLL_ALLOW_KSK_ROLL) && (flags & KEY_ROLL_ALLOW_ZSK_ROLL));
	bool allowed_zsk_only = (!(flags & KEY_ROLL_ALLOW_KSK_ROLL) && (flags & KEY_ROLL_ALLOW_ZSK_ROLL));
	// generate initial keys if missing
	if ((!key_present(ctx, true, false) && !key_present(ctx, true, true)) ||
	    (allowed_zsk_only && !key_present(ctx, false, true) && !key_present(ctx, true, true))) {
		if ((flags & KEY_ROLL_ALLOW_KSK_ROLL)) {
			if (ctx->policy->ksk_shared) {
				ret = share_or_generate_key(ctx, GEN_KSK_FLAGS, ctx->now, false);
			} else {
				ret = generate_key(ctx, GEN_KSK_FLAGS, ctx->now, false);
			}
			if (ret == KNOT_EOK) {
				reschedule->plan_ds_check = true;
				ready_keyid = ctx->zone->keys[0].id;
				ready_keytag = dnssec_key_get_keytag(ctx->zone->keys[0].key);
			}
		}
		if (ret == KNOT_EOK && (flags & KEY_ROLL_ALLOW_ZSK_ROLL)) {
			reschedule->keys_changed = true;
			if (!ctx->policy->single_type_signing &&
			    !key_present(ctx, false, true)) {
				ret = generate_key(ctx, DNSKEY_GENERATE_ZSK, ctx->now, false);
			}
		}
	}
	// forced KSK rollover
	if ((flags & KEY_ROLL_FORCE_KSK_ROLL) && ret == KNOT_EOK && (flags & KEY_ROLL_ALLOW_KSK_ROLL)) {
		flags &= ~KEY_ROLL_FORCE_KSK_ROLL;
		if (running_rollover(ctx)) {
			log_zone_warning(ctx->zone->dname, "DNSSEC, ignoring forced KSK rollover "
			                                   "due to running rollover");
		} else {
			ret = generate_ksk(ctx, 0, false);
			if (ret == KNOT_EOK) {
				reschedule->keys_changed = true;
				log_zone_info(ctx->zone->dname, "DNSSEC, KSK rollover started");
			}
		}
	}
	// forced ZSK rollover
	if ((flags & KEY_ROLL_FORCE_ZSK_ROLL) && ret == KNOT_EOK && (flags & KEY_ROLL_ALLOW_ZSK_ROLL)) {
		flags &= ~KEY_ROLL_FORCE_ZSK_ROLL;
		if (running_rollover(ctx)) {
			log_zone_warning(ctx->zone->dname, "DNSSEC, ignoring forced ZSK rollover "
			                                   "due to running rollover");
		} else {
			ret = generate_key(ctx, DNSKEY_GENERATE_ZSK, 0, false);
			if (ret == KNOT_EOK) {
				reschedule->keys_changed = true;
				log_zone_info(ctx->zone->dname, "DNSSEC, ZSK rollover started");
			}
		}
	}
	// algorithm rollover
	if (algorithm_present(ctx, ctx->policy->algorithm) == 0 &&
	    !running_rollover(ctx) && allowed_general_roll && ret == KNOT_EOK) {
		ret = generate_ksk(ctx, 0, true);
		if (!ctx->policy->single_type_signing && ret == KNOT_EOK) {
			ret = generate_key(ctx, DNSKEY_GENERATE_ZSK, 0, true);
		}
		log_zone_info(ctx->zone->dname, "DNSSEC, algorithm rollover started");
		if (ret == KNOT_EOK) {
			reschedule->keys_changed = true;
		}
	}
	// scheme rollover
	if (!signing_scheme_present(ctx) && allowed_general_roll &&
	    !running_rollover(ctx) && ret == KNOT_EOK) {
		ret = generate_ksk(ctx, 0, false);
		if (!ctx->policy->single_type_signing && ret == KNOT_EOK) {
			ret = generate_key(ctx, DNSKEY_GENERATE_ZSK, 0, false);
		}
		log_zone_info(ctx->zone->dname, "DNSSEC, signing scheme rollover started");
		if (ret == KNOT_EOK) {
			reschedule->keys_changed = true;
		}
	}
	if (ret != KNOT_EOK) {
		return ret;
	}

	roll_action_t next = next_action(ctx, flags);

	reschedule->next_rollover = next.time;

	if (knot_time_cmp(reschedule->next_rollover, ctx->now) <= 0) {
		bool log_keytag = true;
		switch (next.type) {
		case GENERATE:
			if (next.ksk) {
				ret = generate_ksk(ctx, 0, false);
			} else {
				ret = generate_key(ctx, DNSKEY_GENERATE_ZSK, 0, false);
			}
			if (ret == KNOT_EOK) {
				log_zone_info(ctx->zone->dname, "DNSSEC, %cSK rollover started",
				              (next.ksk ? 'K' : 'Z'));
			}
			log_keytag = false;
			break;
		case PUBLISH:
			ret = exec_publish(ctx, next.key);
			break;
		case SUBMIT:
			ret = submit_key(ctx, next.key);
			if (ret == KNOT_EOK) {
				reschedule->plan_ds_check = true;
				ready_keyid = next.key->id;
				ready_keytag = dnssec_key_get_keytag(next.key->key);
			}
			break;
		case REPLACE:
			ret = exec_new_signatures(ctx, next.key, 0);
			break;
		case RETIRE:
			ret = exec_ksk_retire(ctx, next.key);
			break;
		case REMOVE:
			ret = exec_remove_old_key(ctx, next.key);
			break;
		case REALLY_REMOVE:
			ret = exec_really_remove(ctx, next.key);
			break;
		default:
			log_keytag = false;
			ret = KNOT_EINVAL;
		}

		if (ret == KNOT_EOK) {
			reschedule->keys_changed = true;
			next = next_action(ctx, flags);
			reschedule->next_rollover = next.time;
		} else {
			if (log_keytag) {
				log_zone_warning(ctx->zone->dname, "DNSSEC, key rollover, tag %5d, action %s (%s)",
				                 dnssec_key_get_keytag(next.key->key),
				                 roll_action_name(next.type), knot_strerror(ret));
			} else {
				log_zone_warning(ctx->zone->dname, "DNSSEC, key rollover, action %s (%s)",
				                 roll_action_name(next.type), knot_strerror(ret));
			}
		}
	}

	if (ret == KNOT_EOK && next.ready_keyid != NULL) {
		// just to make sure DS check is scheduled
		reschedule->plan_ds_check = true;
		ready_keyid = next.ready_keyid;
		ready_keytag = next.ready_keytag;
	}

	if (ret == KNOT_EOK && knot_time_cmp(reschedule->next_rollover, ctx->now) <= 0) {
		return knot_dnssec_key_rollover(ctx, flags, reschedule);
	}

	if (ret == KNOT_EOK && next.time > 0) {
		log_next_event(ctx, &next);
	}

	if (ret == KNOT_EOK && reschedule->keys_changed) {
		ret = kdnssec_ctx_commit(ctx);
		if (ret == KNOT_EOK && (ctx->dbus_event & DBUS_EVENT_KEYS_UPDATED)) {
			dbus_emit_keys_updated(ctx->zone->dname);
		}
	}

	if (ret == KNOT_EOK && reschedule->plan_ds_check) {
		char param[32];
		(void)snprintf(param, sizeof(param), "KEY_SUBMISSION=%hu", ready_keytag);
		log_fmt_zone(LOG_NOTICE, LOG_SOURCE_ZONE, ctx->zone->dname, param,
		             "DNSSEC, KSK submission, waiting for confirmation");
		if (ctx->dbus_event & DBUS_EVENT_ZONE_SUBMISSION) {
			dbus_emit_zone_submission(ctx->zone->dname, ready_keytag, ready_keyid);
		}
	}

	return ret;
}

int knot_dnssec_ksk_sbm_confirm(kdnssec_ctx_t *ctx, uint32_t retire_delay)
{
	if (ctx->policy->manual) {
		log_zone_warning(ctx->zone->dname, "DNSSEC, ignoring KSK submission "
		                                   "due to manual policy");
		return KNOT_NO_READY_KEY;
	}

	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		knot_kasp_key_t *key = &ctx->zone->keys[i];
		if (key->is_ksk && !key->is_pub_only &&
		    get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_READY) {
			int ret = exec_new_signatures(ctx, key, retire_delay);
			if (ret == KNOT_EOK) {
				ret = kdnssec_ctx_commit(ctx);
			}
			return ret;
		}
	}
	return KNOT_NO_READY_KEY;
}

bool zone_has_key_sbm(const kdnssec_ctx_t *ctx)
{
	assert(ctx->zone);

	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		knot_kasp_key_t *key = &ctx->zone->keys[i];
		if (key->is_ksk && !key->is_pub_only &&
		    (get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_READY ||
		     get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_ACTIVE)) {
			return true;
		}
	}
	return false;
}

unsigned dnskey_sync_jitter(conf_t *conf, zone_t *zone)
{
	conf_val_t id = conf_zone_get(conf, C_DNSSEC_POLICY, zone->name);
	conf_id_fix_default(&id);
	conf_val_t val = conf_id_get(conf, C_POLICY, C_KEYTAG_MODULO, &id);
	if (val.code == KNOT_EOK) {
		int zero;
		uint32_t rem, mod;
		int ret = serial_modulo_parse(conf_str(&val), &rem, &mod, &zero);
		if (ret == KNOT_EOK) {
			return 2 * rem;
		}
	}
	return dnssec_random_uint16_t() % 5;
}
