/*  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 <string.h>

#include "knot/catalog/generate.h"
#include "contrib/openbsd/siphash.h"
#include "contrib/wire_ctx.h"

knot_dname_t *catalog_member_owner(const knot_dname_t *member, const knot_dname_t *catzone,
                                   time_t member_time)
{
	assert(member);
	assert(catzone);

	SIPHASH_CTX hash;
	SIPHASH_KEY shkey = { 0 }; // only used for hashing -> zero key
	SipHash24_Init(&hash, &shkey);
	SipHash24_Update(&hash, member, knot_dname_size(member));
	uint64_t u64time = htobe64(member_time);
	SipHash24_Update(&hash, &u64time, sizeof(u64time));
	uint64_t hashres = SipHash24_End(&hash);

	char *hexhash = bin_to_hex((uint8_t *)&hashres, sizeof(hashres), false);
	if (hexhash == NULL) {
		return NULL;
	}
	size_t hexlen = strlen(hexhash);
	assert(hexlen == 16);
	size_t zoneslen = knot_dname_size((uint8_t *)CATALOG_ZONES_LABEL);
	assert(hexlen <= KNOT_DNAME_MAXLABELLEN && zoneslen <= KNOT_DNAME_MAXLABELLEN);
	size_t catzlen = knot_dname_size(catzone);

	size_t outlen = hexlen + zoneslen + catzlen;
	knot_dname_t *out;
	if (outlen > KNOT_DNAME_MAXLEN || (out = malloc(outlen)) == NULL) {
		free(hexhash);
		return NULL;
	}

	wire_ctx_t wire = wire_ctx_init(out, outlen);
	wire_ctx_write_u8(&wire, hexlen);
	wire_ctx_write(&wire, hexhash, hexlen);
	wire_ctx_write(&wire, CATALOG_ZONES_LABEL, zoneslen);
	wire_ctx_skip(&wire, -1);
	wire_ctx_write(&wire, catzone, catzlen);
	assert(wire.error == KNOT_EOK);

	free(hexhash);
	return out;
}

static void set_rdata(knot_rrset_t *rrset, uint8_t *data, uint16_t len)
{
	knot_rdata_init(rrset->rrs.rdata, len, data);
	rrset->rrs.size = knot_rdata_size(len);
}

#define def_txt_owner(ptr_owner) \
	knot_dname_storage_t txt_owner = "\x05""group"; \
	size_t _ptr_ow_len = knot_dname_size(ptr_owner); \
	size_t _ptr_ow_ind = strlen((const char *)txt_owner); \
	if (_ptr_ow_ind + _ptr_ow_len > sizeof(txt_owner)) { \
		return KNOT_ERANGE; \
	} \
	memcpy(txt_owner + _ptr_ow_ind, (ptr_owner), _ptr_ow_len);

static int add_group_txt(const knot_dname_t *ptr_owner, const char *group,
                         zone_contents_t *conts, zone_update_t *up)
{
	assert((conts == NULL) != (up == NULL));
	size_t group_len;
	if (group == NULL || (group_len = strlen(group)) < 1) {
		return KNOT_EOK;
	}
	assert(group_len <= 255);

	def_txt_owner(ptr_owner);

	uint8_t data[256] = { group_len };
	memcpy(data + 1, group, group_len);

	knot_rrset_t txt;
	knot_rrset_init(&txt, txt_owner, KNOT_RRTYPE_TXT, KNOT_CLASS_IN, 0);
	uint8_t txt_rd[256] = { 0 };
	txt.rrs.rdata = (knot_rdata_t *)txt_rd;
	txt.rrs.count = 1;
	set_rdata(&txt, data, 1 + group_len );

	int ret;
	if (conts != NULL) {
		zone_node_t *unused = NULL;
		ret = zone_contents_add_rr(conts, &txt, &unused);
	} else {
		ret = zone_update_add(up, &txt);
	}

	return ret;
}

static int rem_group_txt(const knot_dname_t *ptr_owner, zone_update_t *up)
{
	def_txt_owner(ptr_owner);

	int ret = zone_update_remove_rrset(up, txt_owner, KNOT_RRTYPE_TXT);
	if (ret == KNOT_ENOENT || ret == KNOT_ENONODE) {
		ret = KNOT_EOK;
	}

	return ret;
}

int catalog_update_to_zone(struct zone_contents **conts, catalog_update_t *u,
                           const knot_dname_t *catzone, uint32_t soa_serial)
{
	zone_contents_t *c = zone_contents_new(catzone, true);
	if (c == NULL) {
		return KNOT_ENOMEM;
	}

	zone_node_t *unused = NULL;
	uint8_t invalid[9] = "\x07""invalid";
	uint8_t version[9] = "\x07""version";
	uint8_t cat_version[2] = "\x01" CATALOG_ZONE_VERSION;

	// prepare common rrset with one rdata item
	uint8_t rdata[256] = { 0 };
	knot_rrset_t rrset;
	knot_rrset_init(&rrset, (knot_dname_t *)catzone, KNOT_RRTYPE_SOA, KNOT_CLASS_IN, 0);
	rrset.rrs.rdata = (knot_rdata_t *)rdata;
	rrset.rrs.count = 1;

	// set catalog zone's SOA
	uint8_t data[250];
	assert(sizeof(knot_rdata_t) + sizeof(data) <= sizeof(rdata));
	wire_ctx_t wire = wire_ctx_init(data, sizeof(data));
	wire_ctx_write(&wire, invalid, sizeof(invalid));
	wire_ctx_write(&wire, invalid, sizeof(invalid));
	wire_ctx_write_u32(&wire, soa_serial);
	wire_ctx_write_u32(&wire, CATALOG_SOA_REFRESH);
	wire_ctx_write_u32(&wire, CATALOG_SOA_RETRY);
	wire_ctx_write_u32(&wire, CATALOG_SOA_EXPIRE);
	wire_ctx_write_u32(&wire, 0);
	set_rdata(&rrset, data, wire_ctx_offset(&wire));
	int ret = zone_contents_add_rr(c, &rrset, &unused);
	if (ret != KNOT_EOK) {
		goto fail;
	}

	// set catalog zone's NS
	unused = NULL;
	rrset.type = KNOT_RRTYPE_NS;
	set_rdata(&rrset, invalid, sizeof(invalid));
	ret = zone_contents_add_rr(c, &rrset, &unused);
	if (ret != KNOT_EOK) {
		goto fail;
	}

	// set catalog zone's version TXT
	unused = NULL;
	knot_dname_storage_t owner;
	if (knot_dname_store(owner, version) == 0 || catalog_dname_append(owner, catzone) == 0) {
		ret = KNOT_ERROR;
		goto fail;
	}
	rrset.owner = owner;
	rrset.type = KNOT_RRTYPE_TXT;
	set_rdata(&rrset, cat_version, sizeof(cat_version));
	ret = zone_contents_add_rr(c, &rrset, &unused);
	if (ret != KNOT_EOK) {
		goto fail;
	}

	// insert member zone PTR records
	rrset.type = KNOT_RRTYPE_PTR;
	catalog_it_t *it = catalog_it_begin(u);
	while (!catalog_it_finished(it)) {
		catalog_upd_val_t *val = catalog_it_val(it);
		if (val->add_owner == NULL) {
			continue;
		}
		rrset.owner = val->add_owner;
		set_rdata(&rrset, val->member, knot_dname_size(val->member));
		unused = NULL;
		if ((ret = zone_contents_add_rr(c, &rrset, &unused)) != KNOT_EOK ||
		    (ret = add_group_txt(val->add_owner, val->new_group, c, NULL)) != KNOT_EOK) {
			catalog_it_free(it);
			goto fail;
		}
		catalog_it_next(it);
	}
	catalog_it_free(it);

	*conts = c;
	return KNOT_EOK;
fail:
	zone_contents_deep_free(c);
	return ret;;
}

int catalog_update_to_update(catalog_update_t *u, struct zone_update *zu)
{
	knot_rrset_t ptr;
	knot_rrset_init(&ptr, NULL, KNOT_RRTYPE_PTR, KNOT_CLASS_IN, 0);
	uint8_t tmp[KNOT_DNAME_MAXLEN + sizeof(knot_rdata_t)];
	ptr.rrs.rdata = (knot_rdata_t *)tmp;
	ptr.rrs.count = 1;

	int ret = KNOT_EOK;
	catalog_it_t *it = catalog_it_begin(u);
	while (!catalog_it_finished(it) && ret == KNOT_EOK) {
		catalog_upd_val_t *val = catalog_it_val(it);
		if (val->type == CAT_UPD_INVALID) {
			catalog_it_next(it);
			continue;
		}

		if (val->type == CAT_UPD_PROP && knot_dname_is_equal(zu->zone->name, val->add_catz)) {
			ret = rem_group_txt(val->add_owner, zu);
			if (ret == KNOT_EOK) {
				ret = add_group_txt(val->add_owner, val->new_group, NULL, zu);
			}
			catalog_it_next(it);
			continue;
		}

		set_rdata(&ptr, val->member, knot_dname_size(val->member));
		if (val->type == CAT_UPD_REM && knot_dname_is_equal(zu->zone->name, val->rem_catz)) {
			ptr.owner = val->rem_owner;
			ret = zone_update_remove(zu, &ptr);
			if (ret == KNOT_EOK) {
				ret = rem_group_txt(val->rem_owner, zu);
			}
		}
		if (val->type == CAT_UPD_ADD && knot_dname_is_equal(zu->zone->name, val->add_catz)) {
			ptr.owner = val->add_owner;
			ret = zone_update_add(zu, &ptr);
			if (ret == KNOT_EOK) {
				ret = add_group_txt(val->add_owner, val->new_group, NULL, zu);
			}
		}
		catalog_it_next(it);
	}
	catalog_it_free(it);
	return ret;
}
