/*  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 <limits.h>
#include <string.h>
#include <strings.h>
#include <time.h>
#include <fcntl.h>

#include "utils/keymgr/functions.h"

#include "utils/common/msg.h"
#include "utils/keymgr/bind_privkey.h"
#include "contrib/base64.h"
#include "contrib/color.h"
#include "contrib/ctype.h"
#include "contrib/json.h"
#include "contrib/string.h"
#include "contrib/strtonum.h"
#include "contrib/tolower.h"
#include "contrib/wire_ctx.h"
#include "libdnssec/error.h"
#include "libdnssec/keyid.h"
#include "libdnssec/shared/shared.h"
#include "knot/dnssec/kasp/policy.h"
#include "knot/dnssec/key-events.h"
#include "knot/dnssec/rrset-sign.h"
#include "knot/dnssec/zone-events.h"
#include "knot/dnssec/zone-keys.h"
#include "knot/dnssec/zone-sign.h"
#include "libzscanner/scanner.h"

inline static bool both_dash_undersc(const char a, const char b)
{
	return (a == '-' || a == '_') && (b == '-' || b == '_');
}

bool same_command(const char *arg, const char *cmd, bool prefix)
{
	unsigned cmd_len = strlen(cmd);
	unsigned arg_len = strnlen(arg, cmd_len + 1);
	if (arg_len != cmd_len && (!prefix || arg_len < cmd_len)) {
		return false;
	}

	for (unsigned i = 0; i < cmd_len; i++) {
		if (knot_tolower(arg[i]) != knot_tolower(cmd[i]) &&
		    !both_dash_undersc(arg[i], cmd[i])) {
			return false;
		}
	}

	return true;
}

int parse_timestamp(char *arg, knot_time_t *stamp)
{
	int ret = knot_time_parse("YMDhms|'now'+-#u|'t'+-#u|+-#u|'t'+-#|+-#|#",
	                          arg, stamp);
	if (ret < 0) {
		ERR2("invalid timestamp: %s", arg);
		return KNOT_EINVAL;
	}
	return KNOT_EOK;
}

static bool init_timestamps(char *arg, knot_kasp_key_timing_t *timing)
{
	knot_time_t *dst = NULL;

	if (same_command(arg, "created=", true)) {
		dst = &timing->created;
	} else if (same_command(arg, "publish=", true)) {
		dst = &timing->publish;
	} else if (same_command(arg, "ready=", true)) {
		dst = &timing->ready;
	} else if (same_command(arg, "active=", true)) {
		dst = &timing->active;
	} else if (same_command(arg, "retire=", true)) {
		dst = &timing->retire;
	} else if (same_command(arg, "remove=", true)) {
		dst = &timing->remove;
	} else if (same_command(arg, "pre_active=", true)) {
		dst = &timing->pre_active;
	} else if (same_command(arg, "post_active=", true)) {
		dst = &timing->post_active;
	} else if (same_command(arg, "retire_active=", true)) {
		dst = &timing->retire_active;
	} else if (same_command(arg, "revoke=", true)) {
		dst = &timing->revoke;
	} else {
		return false;
	}

	knot_time_t stamp;
	int ret = parse_timestamp(strchr(arg, '=') + 1, &stamp);
	if (ret != KNOT_EOK) {
		return true;
	}

	*dst = stamp;

	return true;
}

static bool str2bool(const char *s)
{
	switch (knot_tolower(s[0])) {
	case '1':
	case 'y':
	case 't':
		return true;
	default:
		return false;
	}
}

static void bitmap_set(kdnssec_generate_flags_t *bitmap, int flag, bool onoff)
{
	if (onoff) {
		*bitmap |= flag;
	} else {
		*bitmap &= ~flag;
	}
}

static bool same_command_bool(const char *arg, const char *cmd, bool *res)
{
	char prefix[32];
	(void)snprintf(prefix, sizeof(prefix), "%s=", cmd);
	if (same_command(arg, prefix, true)) {
		*res = str2bool(arg + strlen(prefix));
		return true;
	} else if (same_command(arg, cmd, false)) {
		*res = true;
		return true;
	} else {
		return false;
	}
}

static bool genkeyargs(int argc, char *argv[], bool just_timing,
                       kdnssec_generate_flags_t *flags, dnssec_key_algorithm_t *algorithm,
                       uint16_t *keysize, knot_kasp_key_timing_t *timing,
                       const char **addtopolicy)
{
	// generate algorithms field
	const char *algnames[256] = {
		[DNSSEC_KEY_ALGORITHM_RSA_SHA1] = "rsasha1",
		[DNSSEC_KEY_ALGORITHM_RSA_SHA1_NSEC3] = "rsasha1-nsec3-sha1",
		[DNSSEC_KEY_ALGORITHM_RSA_SHA256] = "rsasha256",
		[DNSSEC_KEY_ALGORITHM_RSA_SHA512] = "rsasha512",
		[DNSSEC_KEY_ALGORITHM_ECDSA_P256_SHA256] = "ecdsap256sha256",
		[DNSSEC_KEY_ALGORITHM_ECDSA_P384_SHA384] = "ecdsap384sha384",
		[DNSSEC_KEY_ALGORITHM_ED25519] = "ed25519",
		[DNSSEC_KEY_ALGORITHM_ED448] = "ed448",
	};

	// parse args
	for (int i = 0; i < argc; i++) {
		bool res;
		if (!just_timing && same_command(argv[i], "algorithm=", true)) {
			int alg = 256; // invalid value
			(void)str_to_int(argv[i] + 10, &alg, 0, 255);
			for (int al = 0; al < 256 && alg > 255; al++) {
				if (algnames[al] != NULL &&
				    same_command(argv[i] + 10, algnames[al], false)) {
					alg = al;
				}
			}
			if (alg > 255) {
				ERR2("unknown algorithm: %s", argv[i] + 10);
				return false;
			}
			*algorithm = alg;
		} else if (!just_timing && same_command(argv[i], "size=", true)) {
			if (str_to_u16(argv[i] + 5, keysize) != KNOT_EOK) {
				ERR2("invalid size: '%s'", argv[i] + 5);
				return false;
			}
		} else if (!just_timing && same_command(argv[i], "addtopolicy=", true) && addtopolicy != NULL) {
			*addtopolicy = argv[i] + 12;
		} else if (same_command_bool(argv[i], "ksk", &res)) {
			bitmap_set(flags, DNSKEY_GENERATE_KSK, res);
		} else if (same_command_bool(argv[i], "zsk", &res)) {
			bitmap_set(flags, DNSKEY_GENERATE_ZSK, res);
		} else if (same_command_bool(argv[i], "sep", &res)) {
			bitmap_set(flags, DNSKEY_GENERATE_SEP_SPEC, true);
			bitmap_set(flags, DNSKEY_GENERATE_SEP_ON, res);
		} else if (same_command_bool(argv[i], "for-later", &res)) {
			bitmap_set(flags, DNSKEY_GENERATE_FOR_LATER, res);
		} else if (!init_timestamps(argv[i], timing)) {
			ERR2("invalid parameter: %s", argv[i]);
			return false;
		}
	}

	if (*flags & DNSKEY_GENERATE_FOR_LATER) {
		*timing = (knot_kasp_key_timing_t){ .created = timing->created };
	}

	return true;
}

static bool genkeyargs_ksk(int argc, char *argv[])
{
	for (int i = 0; i < argc; i++) {
		if (same_command(argv[i], "ksk=", true) && str2bool(argv[i] + 4)) {
			return true;
		}
	}
	return false;
}

static bool _check_lower(knot_time_t a, knot_time_t b,
			 const char *a_name, const char *b_name)
{
	if (knot_time_cmp(a, b) > 0) {
		ERR2("timestamp '%s' must be before '%s'", a_name, b_name);
		return false;
	}
	return true;
}

#define check_lower(t, a, b) if (!_check_lower(t->a, t->b, #a, #b)) return KNOT_ESEMCHECK

static int check_timers(const knot_kasp_key_timing_t *t)
{
	if (t->pre_active != 0) {
		check_lower(t, pre_active, publish);
	}
	check_lower(t, publish, active);
	check_lower(t, active, retire_active);
	check_lower(t, active, retire);
	check_lower(t, active, post_active);
	if (t->post_active == 0) {
		check_lower(t, retire, remove);
	}
	return KNOT_EOK;
}

#undef check_lower

// modifies ctx->policy options, so don't do anything afterwards !
int keymgr_generate_key(kdnssec_ctx_t *ctx, int argc, char *argv[])
{
	knot_time_t now = knot_time(), infinity = 0;
	knot_kasp_key_timing_t gen_timing = { now, infinity, now, infinity, now, infinity, infinity, infinity, infinity };
	kdnssec_generate_flags_t flags = 0;
	uint16_t keysize = 0;
	const char *addtopolicy = NULL;
	if (!genkeyargs(argc, argv, false, &flags, &ctx->policy->algorithm,
			&keysize, &gen_timing, &addtopolicy)) {
		return KNOT_EINVAL;
	}

	int ret = check_timers(&gen_timing);
	if (ret != KNOT_EOK) {
		return ret;
	}

	if ((flags & DNSKEY_GENERATE_KSK) && gen_timing.ready == infinity) {
		gen_timing.ready = gen_timing.active;
	}

	if (keysize == 0) {
		keysize = dnssec_algorithm_key_size_default(ctx->policy->algorithm);
	}
	if ((flags & DNSKEY_GENERATE_KSK)) {
		ctx->policy->ksk_size = keysize;
	} else {
		ctx->policy->zsk_size = keysize;
	}

	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		knot_kasp_key_t *kasp_key = &ctx->zone->keys[i];
		if ((kasp_key->is_ksk && (flags & DNSKEY_GENERATE_KSK)) &&
		    dnssec_key_get_algorithm(kasp_key->key) != ctx->policy->algorithm) {
			WARN2("inconsistent KSK algorithms");
			break;
		}
	}

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

	key->timing = gen_timing;

	if (addtopolicy != NULL) {
		char *last_policy_last = NULL;

		knot_dname_t *unused = NULL;
		ret = kasp_db_get_policy_last(ctx->kasp_db, addtopolicy, &unused,
		                              &last_policy_last);
		knot_dname_free(unused, NULL);
		if (ret != KNOT_EOK && ret != KNOT_ENOENT) {
			free(last_policy_last);
			return ret;
		}

		ret = kasp_db_set_policy_last(ctx->kasp_db, addtopolicy, last_policy_last,
		                              ctx->zone->dname, key->id);
		free(last_policy_last);
		if (ret != KNOT_EOK) {
			return ret;
		}
	}

	ret = kdnssec_ctx_commit(ctx);

	if (ret == KNOT_EOK) {
		printf("%s\n", key->id);
	}

	return ret;
}

static void parse_record(zs_scanner_t *scanner)
{
	dnssec_key_t *key = scanner->process.data;

	if (dnssec_key_get_dname(key) != NULL ||
	    scanner->r_type != KNOT_RRTYPE_DNSKEY) {
		scanner->state = ZS_STATE_STOP;
		return;
	}

	dnssec_binary_t rdata = {
		.data = scanner->r_data,
		.size = scanner->r_data_length
	};
	dnssec_key_set_dname(key, scanner->dname);
	dnssec_key_set_rdata(key, &rdata);
}

int bind_pubkey_parse(const char *filename, dnssec_key_t **key_ptr)
{
	dnssec_key_t *key = NULL;
	int result = dnssec_key_new(&key);
	if (result != DNSSEC_EOK) {
		return KNOT_ENOMEM;
	}

	uint16_t cls = KNOT_CLASS_IN;
	uint32_t ttl = 0;
	zs_scanner_t *scanner = malloc(sizeof(zs_scanner_t));
	if (scanner == NULL) {
		dnssec_key_free(key);
		return KNOT_ENOMEM;
	}

	if (zs_init(scanner, ".", cls, ttl) != 0 ||
	    zs_set_input_file(scanner, filename) != 0 ||
	    zs_set_processing(scanner, parse_record, NULL, key) != 0 ||
	    zs_parse_all(scanner) != 0) {
		int ret;
		switch (scanner->error.code) {
		case ZS_FILE_OPEN:
		case ZS_FILE_INVALID:
			ret = KNOT_EFILE;
			break;
		case ZS_FILE_ACCESS:
			ret = KNOT_EFACCES;
			break;
		default:
			ret = KNOT_EPARSEFAIL;
		}
		zs_deinit(scanner);
		free(scanner);
		dnssec_key_free(key);
		return ret;
	}
	zs_deinit(scanner);
	free(scanner);

	if (dnssec_key_get_dname(key) == NULL) {
		dnssec_key_free(key);
		return KNOT_INVALID_PUBLIC_KEY;
	}

	*key_ptr = key;
	return KNOT_EOK;
}

static char *gen_keyfilename(const char *orig, const char *wantsuff, const char *altsuff)
{
	assert(orig && wantsuff && altsuff);

	const char *dot = strrchr(orig, '.');

	if (dot != NULL && strcmp(dot, wantsuff) == 0) { // Full match.
		return strdup(orig);
	} else if (dot != NULL && strcmp(dot, altsuff) == 0) { // Replace suffix.
		return sprintf_alloc("%.*s%s", (int)(dot - orig), orig, wantsuff);
	} else { // Add wanted suffix.
		return sprintf_alloc("%s%s", orig, wantsuff);
	}
}

int keymgr_import_bind(kdnssec_ctx_t *ctx, const char *import_file, bool pub_only)
{
	if (ctx == NULL || import_file == NULL) {
		return KNOT_EINVAL;
	}

	knot_kasp_key_timing_t timing = { 0 };
	dnssec_key_t *key = NULL;
	char *keyid = NULL;

	char *pubname = gen_keyfilename(import_file, ".key", ".private");
	if (pubname == NULL) {
		return KNOT_EINVAL;
	}

	int ret = bind_pubkey_parse(pubname, &key);
	free(pubname);
	if (ret != KNOT_EOK) {
		goto fail;
	}

	if (!pub_only) {
		bind_privkey_t bpriv = { .time_publish = ctx->now, .time_activate = ctx->now };

		knot_kasp_keystore_t *keystore = knot_store_for_key(ctx->keystores,
		                                 (dnssec_key_get_flags(key) == DNSKEY_FLAGS_KSK));
		if (keystore == NULL) {
			ret = KNOT_DNSSEC_ENOKEYSTORE;
			goto fail;
		}

		char *privname = gen_keyfilename(import_file, ".private", ".key");
		if (privname == NULL) {
			goto fail;
		}

		ret = bind_privkey_parse(privname, &bpriv);
		free(privname);
		if (ret != DNSSEC_EOK) {
			goto fail;
		}

		dnssec_binary_t pem = { 0 };
		ret = bind_privkey_to_pem(key, &bpriv, &pem);
		if (ret != DNSSEC_EOK) {
			bind_privkey_free(&bpriv);
			goto fail;
		}

		bind_privkey_to_timing(&bpriv, &timing);

		bind_privkey_free(&bpriv);

		ret = dnssec_keystore_import(keystore->keystore, &pem, &keyid);
		dnssec_binary_free(&pem);
		if (ret != DNSSEC_EOK) {
			goto fail;
		}
	} else {
		timing.publish = ctx->now;

		ret = dnssec_key_get_keyid(key, &keyid);
		if (ret != DNSSEC_EOK) {
			goto fail;
		}
	}

	// allocate kasp key
	knot_kasp_key_t *kkey = calloc(1, sizeof(*kkey));
	if (!kkey) {
		ret = KNOT_ENOMEM;
		goto fail;
	}

	kkey->id = keyid;
	kkey->key = key;
	kkey->timing = timing;
	kkey->is_pub_only = pub_only;
	kkey->is_ksk = (dnssec_key_get_flags(kkey->key) == DNSKEY_FLAGS_KSK);
	kkey->is_zsk = !kkey->is_ksk;

	// append to zone
	ret = kasp_zone_append(ctx->zone, kkey);
	free(kkey);
	if (ret != KNOT_EOK) {
		goto fail;
	}
	ret = kdnssec_ctx_commit(ctx);
	if (ret == KNOT_EOK) {
		printf("%s\n", keyid);
		return KNOT_EOK;
	}
fail:
	dnssec_key_free(key);
	free(keyid);
	return knot_error_from_libdnssec(ret);
}

static void err_import_key(char *keyid, const char *file)
{
	assert(file != NULL);

	ERR2("failed to get key%s%s from %s%s",
	     (keyid == NULL || *keyid == '\0') ? "" : " ",
	     (keyid == NULL) ? "" : keyid,
	     (*file == '\0') ? "the keystore" : "file ", file );
}

static int import_key(kdnssec_ctx_t *ctx, unsigned backend, const char *param,
                      int argc, char *argv[])
{
	if (ctx == NULL || param == NULL) {
		return KNOT_EINVAL;
	}

	// parse params
	knot_time_t now = knot_time();
	knot_kasp_key_timing_t timing = { .publish = now, .active = now };
	kdnssec_generate_flags_t flags = 0;
	uint16_t keysize = 0;
	if (!genkeyargs(argc, argv, false, &flags, &ctx->policy->algorithm,
	                &keysize, &timing, NULL)) {
		return KNOT_EINVAL;
	}

	knot_kasp_keystore_t *keystore = knot_store_for_key(ctx->keystores,
	                                 (flags & DNSKEY_GENERATE_KSK));
	if (keystore == NULL) {
		return KNOT_DNSSEC_ENOKEYSTORE;
	}

	int ret = check_timers(&timing);
	if (ret != KNOT_EOK) {
		return ret;
	}

	normalize_generate_flags(&flags);

	dnssec_key_t *key = NULL;
	char *keyid = NULL;

	if (backend == KEYSTORE_BACKEND_PEM) {
		// open file
		int fd = open(param, O_RDONLY, 0);
		if (fd == -1) {
			err_import_key("", param);
			return knot_map_errno();
		}

		// determine size
		off_t fsize = lseek(fd, 0, SEEK_END);
		if (fsize == -1) {
			close(fd);
			err_import_key("", param);
			return knot_map_errno();
		}
		if (lseek(fd, 0, SEEK_SET) == -1) {
			close(fd);
			err_import_key("", param);
			return knot_map_errno();
		}

		// alloc memory
		dnssec_binary_t pem = { 0 };
		ret = dnssec_binary_alloc(&pem, fsize);
		if (ret != DNSSEC_EOK) {
			close(fd);
			err_import_key("", param);
			goto fail;
		}

		// read pem
		ssize_t read_count = read(fd, pem.data, pem.size);
		close(fd);
		if (read_count == -1) {
			dnssec_binary_free(&pem);
			ret = knot_map_errno();
			err_import_key("", param);
			goto fail;
		}

		// put pem to keystore
		ret = dnssec_keystore_import(keystore->keystore, &pem, &keyid);
		dnssec_binary_free(&pem);
		if (ret != DNSSEC_EOK) {
			err_import_key(keyid, param);
			goto fail;
		}
	} else {
		assert(backend == KEYSTORE_BACKEND_PKCS11);
		keyid = strdup(param);
	}

	// create dnssec key
	ret = dnssec_key_new(&key);
	if (ret != DNSSEC_EOK) {
		goto fail;
	}
	ret = dnssec_key_set_dname(key, ctx->zone->dname);
	if (ret != DNSSEC_EOK) {
		goto fail;
	}
	dnssec_key_set_flags(key, dnskey_flags(flags & DNSKEY_GENERATE_SEP_ON));
	dnssec_key_set_algorithm(key, ctx->policy->algorithm);

	// fill key structure from keystore (incl. pubkey from privkey computation)
	ret = kdnssec_load_private(ctx->keystores, keyid, key, NULL, NULL);
	if (ret != DNSSEC_EOK) {
		err_import_key(keyid, "");
		goto fail;
	}

	// allocate kasp key
	knot_kasp_key_t *kkey = calloc(1, sizeof(*kkey));
	if (kkey == NULL) {
		ret = KNOT_ENOMEM;
		goto fail;
	}
	kkey->id = keyid;
	kkey->key = key;
	kkey->timing = timing;
	kkey->is_ksk = (flags & DNSKEY_GENERATE_KSK);
	kkey->is_zsk = (flags & DNSKEY_GENERATE_ZSK);
	kkey->is_for_later = (flags & DNSKEY_GENERATE_FOR_LATER);

	// append to zone
	ret = kasp_zone_append(ctx->zone, kkey);
	free(kkey);
	if (ret != KNOT_EOK) {
		goto fail;
	}
	ret = kdnssec_ctx_commit(ctx);
	if (ret == KNOT_EOK) {
		printf("%s\n", keyid);
		return KNOT_EOK;
	}
fail:
	dnssec_key_free(key);
	free(keyid);
	return knot_error_from_libdnssec(ret);
}

int keymgr_import_pem(kdnssec_ctx_t *ctx, const char *import_file, int argc, char *argv[])
{
	return import_key(ctx, KEYSTORE_BACKEND_PEM, import_file, argc, argv);
}

int keymgr_import_pkcs11(kdnssec_ctx_t *ctx, char *key_id, int argc, char *argv[])
{
	if (!dnssec_keyid_is_valid(key_id)) {
		return DNSSEC_INVALID_KEY_ID;
	}

	knot_kasp_keystore_t *keystore = knot_store_for_key(ctx->keystores, genkeyargs_ksk(argc, argv));

	if (keystore == NULL || keystore->backend != KEYSTORE_BACKEND_PKCS11) {
		knot_dname_txt_storage_t dname_str;
		(void)knot_dname_to_str(dname_str, ctx->zone->dname, sizeof(dname_str));
		ERR2("not a PKCS #11 keystore for zone %s", dname_str);
		return KNOT_ERROR;
	}

	dnssec_keyid_normalize(key_id);
	return import_key(ctx, KEYSTORE_BACKEND_PKCS11, key_id, argc, argv);
}

int keymgr_nsec3_salt_print(kdnssec_ctx_t *ctx)
{
	dnssec_binary_t salt_bin;
	knot_time_t created;
	int ret = kasp_db_load_nsec3salt(ctx->kasp_db, ctx->zone->dname,
	                                 &salt_bin, &created);
	switch (ret) {
	case KNOT_EOK:
		printf("Current salt: ");
		if (salt_bin.size == 0) {
			printf("-");
		}
		for (size_t i = 0; i < salt_bin.size; i++) {
			printf("%02X", (unsigned)salt_bin.data[i]);
		}
		printf("\n");
		free(salt_bin.data);
		break;
	case KNOT_ENOENT:
		printf("-- no salt --\n");
		ret = KNOT_EOK;
		break;
	}
	return ret;
}

int keymgr_nsec3_salt_set(kdnssec_ctx_t *ctx, const char *new_salt)
{
	assert(new_salt);

	dnssec_binary_t salt_bin = { 0 };
	if (strcmp(new_salt, "-") != 0) {
		salt_bin.data = hex_to_bin(new_salt, &salt_bin.size);
		if (salt_bin.data == NULL) {
			return KNOT_EMALF;
		}
	}
	if (salt_bin.size != ctx->policy->nsec3_salt_length) {
		WARN2("specified salt doesn't match configured salt length (%d)",
		      (int)ctx->policy->nsec3_salt_length);
	}
	int ret = kasp_db_store_nsec3salt(ctx->kasp_db, ctx->zone->dname,
	                                  &salt_bin, knot_time());
	if (salt_bin.size > 0) {
		free(salt_bin.data);
	}
	return ret;
}

int keymgr_serial_print(kdnssec_ctx_t *ctx, kaspdb_serial_t type)
{
	uint32_t serial = 0;
	int ret = kasp_db_load_serial(ctx->kasp_db, ctx->zone->dname,
	                              type, &serial);
	switch (ret) {
	case KNOT_EOK:
		printf("Current serial: %u\n", serial);
		break;
	case KNOT_ENOENT:
		printf("-- no serial --\n");
		ret = KNOT_EOK;
		break;
	}
	return ret;
}

int keymgr_serial_set(kdnssec_ctx_t *ctx, kaspdb_serial_t type, uint32_t new_serial)
{
	return kasp_db_store_serial(ctx->kasp_db, ctx->zone->dname,
	                            type, new_serial);
}

static void print_tsig(dnssec_tsig_algorithm_t mac, const char *name,
		       const dnssec_binary_t *secret)
{
	assert(name);
	assert(secret);

	const char *mac_name = dnssec_tsig_algorithm_to_name(mac);
	assert(mac_name);

	// client format (as a comment)
	printf("# %s:%s:%.*s\n", mac_name, name, (int)secret->size, secret->data);

	// server format
	printf("key:\n");
	printf("  - id: %s\n", name);
	printf("    algorithm: %s\n", mac_name);
	printf("    secret: %.*s\n", (int)secret->size, secret->data);
}

int keymgr_generate_tsig(const char *tsig_name, const char *alg_name, int bits)
{
	dnssec_tsig_algorithm_t alg = dnssec_tsig_algorithm_from_name(alg_name);
	if (alg == DNSSEC_TSIG_UNKNOWN) {
		return KNOT_INVALID_KEY_ALGORITHM;
	}

	int optimal_bits = dnssec_tsig_optimal_key_size(alg);
	if (bits == 0) {
		bits = optimal_bits;
	}

	// round up bits to bytes
	bits = (bits + CHAR_BIT - 1) / CHAR_BIT * CHAR_BIT;

	if (bits < optimal_bits) {
		WARN2("optimal key size for %s is at least %d bits",
		       dnssec_tsig_algorithm_to_name(alg), optimal_bits);
	}
	assert(bits % CHAR_BIT == 0);

	_cleanup_binary_ dnssec_binary_t key = { 0 };
	int r = dnssec_binary_alloc(&key, bits / CHAR_BIT);
	if (r != DNSSEC_EOK) {
		ERR2("failed to allocate memory");
		return knot_error_from_libdnssec(r);
	}

	r = gnutls_rnd(GNUTLS_RND_KEY, key.data, key.size);
	if (r != 0) {
		ERR2("failed to generate secret the key");
		return knot_error_from_libdnssec(r);
	}

	_cleanup_binary_ dnssec_binary_t key_b64 = { 0 };
	r = dnssec_binary_to_base64(&key, &key_b64);
	if (r != DNSSEC_EOK) {
		ERR2("failed to convert the key to Base64");
		return knot_error_from_libdnssec(r);
	}

	print_tsig(alg, tsig_name, &key_b64);

	return KNOT_EOK;
}

static bool is_hex(const char *string)
{
	for (const char *p = string; *p != '\0'; p++) {
		if (!is_xdigit(*p)) {
			return false;
		}
	}
	return (*string != '\0');
}

int keymgr_get_key(kdnssec_ctx_t *ctx, const char *key_spec, knot_kasp_key_t **key)
{
	// Check if type of key spec is prescribed.
	bool is_keytag = false, is_id = false;
	if (same_command(key_spec, "keytag=", true)) {
		key_spec += 7;
		is_keytag = true;
	} else if (same_command(key_spec, "id=", true)) {
		key_spec += 3;
		is_id = true;
	}

	uint16_t keytag = 0;
	bool can_be_keytag = (str_to_u16(key_spec, &keytag) == KNOT_EOK);
	long spec_len = strlen(key_spec);

	// Check if input is a valid key spec.
	if ((is_keytag && !can_be_keytag) ||
	    (is_id && !is_hex(key_spec)) ||
	    (!can_be_keytag && !is_hex(key_spec))) {
		ERR2("invalid key specification");
		return KNOT_EINVAL;
	}

	*key = NULL;
	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		knot_kasp_key_t *candidate = &ctx->zone->keys[i];

		bool keyid_match = strncmp(candidate->id, key_spec, spec_len) == 0; // May be just a prefix.
		bool keytag_match = can_be_keytag &&
		                    dnssec_key_get_keytag(candidate->key) == keytag;

		// Terminate if found exact key ID match.
		if (keyid_match && !is_keytag && strlen(candidate->id) == spec_len) {
			*key = candidate;
			break;
		}
		// Check for key ID prefix or tag match.
		if ((is_keytag && keytag_match) || // Tag is prescribed.
		    (is_id && keyid_match) ||   // Key ID is prescribed.
		    ((!is_keytag && !is_id) && (keyid_match || keytag_match))) { // Nothing is prescribed.
			if (*key == NULL) {
				*key = candidate;
			} else {
				ERR2("key not specified uniquely, please use id=Full_Key_ID");
				return KNOT_EINVAL;
			}
		}
	}
	if (*key == NULL) {
		ERR2("key not found");
		return KNOT_ENOENT;
	}
	return KNOT_EOK;
}

int keymgr_foreign_key_id(char *argv[], knot_lmdb_db_t *kaspdb, knot_dname_t **key_zone, char **key_id)
{
	*key_zone = knot_dname_from_str_alloc(argv[3]);
	if (*key_zone == NULL) {
		return KNOT_ENOMEM;
	}
	knot_dname_to_lower(*key_zone);

	kdnssec_ctx_t kctx = { 0 };
	int ret = kdnssec_ctx_init(conf(), &kctx, *key_zone, kaspdb, NULL);
	if (ret != KNOT_EOK) {
		ERR2("failed to initialize zone %s (%s)", argv[0], knot_strerror(ret));
		free(*key_zone);
		*key_zone = NULL;
		return KNOT_ENOZONE;
	}
	knot_kasp_key_t *key;
	ret = keymgr_get_key(&kctx, argv[2], &key);
	if (ret == KNOT_EOK) {
		*key_id = strdup(key->id);
		if (*key_id == NULL) {
			ret = KNOT_ENOMEM;
		}
	}
	kdnssec_ctx_deinit(&kctx);
	return ret;
}

int keymgr_set_timing(knot_kasp_key_t *key, int argc, char *argv[])
{
	knot_kasp_key_timing_t temp = key->timing;
	kdnssec_generate_flags_t flags = ((key->is_ksk ? DNSKEY_GENERATE_KSK : 0) |
	                                  (key->is_zsk ? DNSKEY_GENERATE_ZSK : 0) |
	                                  (key->is_for_later ? DNSKEY_GENERATE_FOR_LATER : 0));

	if (genkeyargs(argc, argv, true, &flags, NULL, NULL, &temp, NULL)) {
		int ret = check_timers(&temp);
		if (ret != KNOT_EOK) {
			return ret;
		}
		key->timing = temp;
		key->is_for_later = (flags & DNSKEY_GENERATE_FOR_LATER);
		if (key->is_ksk != (bool)(flags & DNSKEY_GENERATE_KSK) ||
		    key->is_zsk != (bool)(flags & DNSKEY_GENERATE_ZSK) ||
		    flags & DNSKEY_GENERATE_SEP_SPEC) {
			normalize_generate_flags(&flags);
			key->is_ksk = (flags & DNSKEY_GENERATE_KSK);
			key->is_zsk = (flags & DNSKEY_GENERATE_ZSK);
			return dnssec_key_set_flags(key->key, dnskey_flags(flags & DNSKEY_GENERATE_SEP_ON));
		}
		return KNOT_EOK;
	}
	return KNOT_EINVAL;
}

typedef struct {
	const char *name;
	size_t offset;
} timer_ctx_t;

static const timer_ctx_t timers[] = {
	{ "created",       offsetof(knot_kasp_key_timing_t, created) },
	{ "pre-active",    offsetof(knot_kasp_key_timing_t, pre_active) },
	{ "publish",       offsetof(knot_kasp_key_timing_t, publish) },
	{ "ready",         offsetof(knot_kasp_key_timing_t, ready) },
	{ "active",        offsetof(knot_kasp_key_timing_t, active) },
	{ "retire-active", offsetof(knot_kasp_key_timing_t, retire_active) },
	{ "retire",        offsetof(knot_kasp_key_timing_t, retire) },
	{ "post-active",   offsetof(knot_kasp_key_timing_t, post_active) },
	{ "revoke",        offsetof(knot_kasp_key_timing_t, revoke) },
	{ "remove",        offsetof(knot_kasp_key_timing_t, remove) },
	{ NULL }
};

typedef struct {
	const char *ks_name;
	size_t ks_count;
	unsigned backend;
	bool missing;
} key_info_t;

#define KS_TYPE(info) (info->backend == KEYSTORE_BACKEND_PEM) ? "PEM" : "PKCS11"

static void print_key_brief(const knot_kasp_key_t *key, key_info_t *info,
                            keymgr_list_params_t *params)
{
	const bool c = params->color;

	printf("%s %s%5u%s ",
	       key->id, COL_BOLD(c), dnssec_key_get_keytag(key->key), COL_RST(c));

	printf("%s%s%s%s ",
	       COL_BOLD(c),
	       (key->is_ksk ? (key->is_zsk ? COL_YELW(c) : COL_RED(c)) : COL_GRN(c)),
	       (key->is_ksk ? (key->is_zsk ? "CSK" : "KSK") : "ZSK"),
	       COL_RST(c));

	uint8_t alg = dnssec_key_get_algorithm(key->key);
	const knot_lookup_t *alg_info = knot_lookup_by_id(knot_dnssec_alg_names, alg);
	if (alg_info != NULL) {
		printf("%s", alg_info->name);
		if (alg <= DNSSEC_KEY_ALGORITHM_RSA_SHA512) {
			printf("%s/%u%s", COL_DIM(c), dnssec_key_get_size(key->key), COL_RST(c));
		}
	} else {
		printf("ALGORITHM_%u", alg);
	}

	if (key->is_pub_only) {
		printf(" %s%spublic-only%s", COL_BOLD(c), COL_MGNT(c), COL_RST(c));
	}

	if (key->is_for_later) {
		printf(" %s%sfor-later%s", COL_BOLD(c), COL_MGNT(c), COL_RST(c));
	}

	if (info->missing) {
		printf(" %s%smissing%s", COL_BOLD(c), COL_YELW(c), COL_RST(c));
	} else if (info->ks_count > 1 && info->ks_name != NULL) {
		printf(" %s%s/%s%s", COL_YELW(c), KS_TYPE(info), info->ks_name, COL_RST(c));
	}

	static char buf[100];
	knot_time_t now = knot_time();
	for (const timer_ctx_t *t = &timers[0]; t->name != NULL; t++) {
		knot_time_t *val = (void *)(&key->timing) + t->offset;
		if (*val == 0) {
			continue;
		}
		bool past = (knot_time_cmp(*val, now) <= 0);
		const char *UNDR = past ? COL_UNDR(c) : "";
		const char *BOLD = past ? "" : COL_BOLD(c);
		for (const timer_ctx_t *t2 = t + 1; past && t2->name != NULL; t2++) {
			knot_time_t *val2 = (void *)(&key->timing) + t2->offset;
			if (knot_time_cmp(*val2, now) <= 0) {
				UNDR = "";
				break;
			}
		}
		(void)knot_time_print(params->format, *val, buf, sizeof(buf));
		printf(" %s%s%s=%s%s%s", UNDR, t->name, COL_RST(c), BOLD, buf, COL_RST(c));
	}
	printf("\n");
}

static void print_key_full(const knot_kasp_key_t *key, key_info_t *info,
                           knot_time_print_t format)
{
	printf("%s ksk=%s zsk=%s tag=%05d algorithm=%-2d size=%-4u public-only=%s for-later=%s missing=%s",
	       key->id, (key->is_ksk ? "yes" : "no "), (key->is_zsk ? "yes" : "no "),
	       dnssec_key_get_keytag(key->key), (int)dnssec_key_get_algorithm(key->key),
	       dnssec_key_get_size(key->key), (key->is_pub_only ? "yes" : "no "),
	       (key->is_for_later ? "yes" : "no "), (info->missing ? "yes" : "no "));
	if (info->ks_name != NULL) {
		printf(" keystore=%s/%s", KS_TYPE(info), info->ks_name);
	}

	static char buf[100];
	for (const timer_ctx_t *t = &timers[0]; t->name != NULL; t++) {
		knot_time_t *val = (void *)(&key->timing) + t->offset;
		(void)knot_time_print(format, *val, buf, sizeof(buf));
		printf(" %s=%s", t->name, buf);
	}
	printf("\n");
}

static void print_key_json(const knot_kasp_key_t *key, key_info_t *info,
                           knot_time_print_t format, jsonw_t *w, const char *zone_name)
{
	jsonw_str(w,   "zone", zone_name);
	jsonw_str(w,   "id", key->id);
	jsonw_bool(w,  "ksk", key->is_ksk);
	jsonw_bool(w,  "zsk", key->is_zsk);
	jsonw_int(w,   "tag", dnssec_key_get_keytag(key->key));
	jsonw_ulong(w, "algorithm", dnssec_key_get_algorithm(key->key));
	jsonw_int(w,   "size", dnssec_key_get_size(key->key));
	jsonw_bool(w,  "public-only", key->is_pub_only);
	jsonw_bool(w,  "for-later", key->is_for_later);
	jsonw_bool(w,  "missing", info->missing);
	if (info->ks_name != NULL) {
	jsonw_str(w,   "keystore", info->ks_name);
	jsonw_str(w,   "backend", KS_TYPE(info));
	}

	static char buf[100];
	for (const timer_ctx_t *t = &timers[0]; t->name != NULL; t++) {
		knot_time_t *val = (void *)(&key->timing) + t->offset;
		(void)knot_time_print(format, *val, buf, sizeof(buf));

		if (format == TIME_PRINT_UNIX) {
			jsonw_int(w, t->name, *val);
		} else {
			jsonw_str(w, t->name, buf);
		}
	}
}

typedef struct {
	knot_time_t val;
	knot_kasp_key_t *key;
} key_sort_item_t;

static int key_sort(const void *a, const void *b)
{
	const key_sort_item_t *key_a = a;
	const key_sort_item_t *key_b = b;
	return knot_time_cmp(key_a->val, key_b->val);
}

static key_info_t key_missing(kdnssec_ctx_t *ctx, const knot_kasp_key_t *key)
{
	key_info_t out = { .ks_count = ctx->keystores[0].count };
	out.missing = !key->is_pub_only &&
	              DNSSEC_EOK != kdnssec_load_private(ctx->keystores, key->id,
	                                                 key->key, &out.ks_name, &out.backend);
	return out;
}

int keymgr_list_keys(kdnssec_ctx_t *ctx, keymgr_list_params_t *params)
{
	if (ctx->zone->num_keys == 0) {
		return KNOT_EOK;
	}

	if (params->extended) {
		for (size_t i = 0; i < ctx->zone->num_keys; i++) {
			knot_kasp_key_t *key = &ctx->zone->keys[i];
			key_info_t info = key_missing(ctx, key);
			print_key_full(key, &info, params->format);
		}
	} else if (params->json) {
		jsonw_t *w = jsonw_new(stdout, "  ");
		if (w == NULL) {
			return KNOT_ENOMEM;
		}

		knot_dname_txt_storage_t name;
		(void)knot_dname_to_str(name, ctx->zone->dname, sizeof(name));

		jsonw_list(w, NULL);
		for (size_t i = 0; i < ctx->zone->num_keys; i++) {
			knot_kasp_key_t *key = &ctx->zone->keys[i];
			jsonw_object(w, NULL);
			key_info_t info = key_missing(ctx, key);
			print_key_json(key, &info, params->format, w, name);
			jsonw_end(w); // object
		}
		jsonw_end(w); // list
		jsonw_free(&w);
	} else {
		key_sort_item_t items[ctx->zone->num_keys];
		for (size_t i = 0; i < ctx->zone->num_keys; i++) {
			knot_kasp_key_t *key = &ctx->zone->keys[i];
			items[i].key = key;
			if (knot_time_cmp(key->timing.pre_active, key->timing.publish) < 0) {
				items[i].val = key->timing.pre_active;
			} else {
				items[i].val = key->timing.publish;
			}
		}
		qsort(&items, ctx->zone->num_keys, sizeof(items[0]), key_sort);
		for (size_t i = 0; i < ctx->zone->num_keys; i++) {
			knot_kasp_key_t *key = items[i].key;
			key_info_t info = key_missing(ctx, key);
			print_key_brief(key, &info, params);
		}
	}
	return KNOT_EOK;
}

static int print_ds(const knot_dname_t *dname, const dnssec_binary_t *rdata)
{
	wire_ctx_t ctx = wire_ctx_init(rdata->data, rdata->size);
	if (wire_ctx_available(&ctx) < 4) {
		return KNOT_EMALF;
	}

	char *name = knot_dname_to_str_alloc(dname);
	if (!name) {
		return KNOT_ENOMEM;
	}

	uint16_t keytag   = wire_ctx_read_u16(&ctx);
	uint8_t algorithm = wire_ctx_read_u8(&ctx);
	uint8_t digest_type = wire_ctx_read_u8(&ctx);

	size_t digest_size = wire_ctx_available(&ctx);

	printf("%s DS %d %d %d ", name, keytag, algorithm, digest_type);
	for (size_t i = 0; i < digest_size; i++) {
		printf("%02x", ctx.position[i]);
	}
	printf("\n");

	free(name);
	return KNOT_EOK;
}

static int create_and_print_ds(const knot_dname_t *zone_name,
			       const dnssec_key_t *key, dnssec_key_digest_t digest)
{
	_cleanup_binary_ dnssec_binary_t rdata = { 0 };
	int r = dnssec_key_create_ds(key, digest, &rdata);
	if (r != DNSSEC_EOK) {
		return knot_error_from_libdnssec(r);
	}

	return print_ds(zone_name, &rdata);
}

int keymgr_generate_ds(const knot_dname_t *dname, const knot_kasp_key_t *key)
{
	static const dnssec_key_digest_t digests[] = {
		DNSSEC_KEY_DIGEST_SHA256,
		DNSSEC_KEY_DIGEST_SHA384,
		0
	};

	int ret = KNOT_EOK;
	for (int i = 0; digests[i] != 0 && ret == KNOT_EOK; i++) {
		ret = create_and_print_ds(dname, key->key, digests[i]);
	}

	return ret;
}

int keymgr_generate_dnskey(const knot_dname_t *dname, const knot_kasp_key_t *key)
{
	const dnssec_key_t *dnskey = key->key;

	char *name = knot_dname_to_str_alloc(dname);
	if (!name) {
		return KNOT_ENOMEM;
	}

	uint16_t flags = dnssec_key_get_flags(dnskey);
	uint8_t algorithm = dnssec_key_get_algorithm(dnskey);

	dnssec_binary_t pubkey = { 0 };
	int ret = dnssec_key_get_pubkey(dnskey, &pubkey);
	if (ret != DNSSEC_EOK) {
		free(name);
		return knot_error_from_libdnssec(ret);
	}

	uint8_t *base64_output = NULL;
	int len = knot_base64_encode_alloc(pubkey.data, pubkey.size, &base64_output);
	if (len < 0) {
		free(name);
		return len;
	}

	printf("%s DNSKEY %u 3 %u %.*s\n", name, flags, algorithm, len, base64_output);

	free(base64_output);
	free(name);
	return KNOT_EOK;
}

int keymgr_list_zones(knot_lmdb_db_t *kaspdb, bool json)
{
	jsonw_t *w;
	list_t zones;
	init_list(&zones);
	int ret = kasp_db_list_zones(kaspdb, &zones);
	if (ret != KNOT_EOK) {
		ERR2("failed to initialize KASP (%s)", knot_strerror(ret));
		return ret;
	}

	knot_dname_txt_storage_t name;
	ptrnode_t *node;

	if (json) {
		w = jsonw_new(stdout, "  ");
		if (w == NULL) {
			ERR2("failed to allocate memory");
			ptrlist_deep_free(&zones, NULL);
			return KNOT_ENOMEM;
		}
		jsonw_list(w, NULL);
	}
	WALK_LIST(node, zones) {
		(void)knot_dname_to_str(name, node->d, sizeof(name));
		if (json) {
			jsonw_str(w, NULL, name);
		} else {
			printf("%s\n", name);
		}
	}
	if (json) {
		jsonw_end(w); // list
		jsonw_free(&w);
	}

	ptrlist_deep_free(&zones, NULL);
	return KNOT_EOK;
}
