/*  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 "knot/zone/timers.h"

#include "contrib/wire_ctx.h"
#include "knot/zone/zonedb.h"

/*
 * # Timer database
 *
 * Timer database stores timestamps of events which need to be retained
 * across server restarts. The key in the database is the zone name in
 * wire format. The value contains serialized timers.
 *
 * # Serialization format
 *
 * The value is a sequence of timers. Each timer consists of the timer
 * identifier (1 byte, unsigned integer) and timer value (8 bytes, unsigned
 * integer, network order).
 *
 * For example, the following byte sequence:
 *
 *     81 00 00 00 00 57 e3 e8 0a 82 00 00 00 00 57 e3 e9 a1
 *
 * Encodes the following timers:
 *
 *     last_flush = 1474553866
 *     last_refresh = 1474554273
 */

/*!
 * \brief Timer database fields identifiers.
 *
 * Valid ID starts with '1' in MSB to avoid conflicts with "old timers".
 */
enum timer_id {
	TIMER_INVALID        = 0,
	TIMER_LAST_FLUSH     = 0x81,
	TIMER_NEXT_REFRESH   = 0x83,
	TIMER_NEXT_DS_CHECK  = 0x85,
	TIMER_NEXT_DS_PUSH   = 0x86,
	TIMER_CATALOG_MEMBER = 0x87,
	TIMER_LAST_NOTIFIED  = 0x88,
	TIMER_LAST_REFR_OK   = 0x89,
	TIMER_NEXT_EXPIRE    = 0x8a,
	TIMER_LAST_MASTER    = 0x8b,
	TIMER_MASTER_PIN_HIT = 0x8c,
	TIMER_LAST_SIGNED    = 0x8d,
};

#define TIMER_SIZE (sizeof(uint8_t) + sizeof(uint64_t))

inline static void set_flag(zone_timers_t *t, uint32_t flag, uint32_t on)
{
	if (on) {
		t->flags |= flag;
	} else {
		t->flags &= ~flag;
	}
}

/*!
 * \brief Deserialize timers from a binary buffer.
 *
 * \note Unknown timers are ignored.
 */
static int deserialize_timers(zone_timers_t *timers_ptr,
                              const uint8_t *data, size_t size)
{
	if (!timers_ptr || !data) {
		return KNOT_EINVAL;
	}

	zone_timers_t timers = { 0 };

	wire_ctx_t wire = wire_ctx_init_const(data, size);
	while (wire_ctx_available(&wire) >= TIMER_SIZE) {
		uint8_t id = wire_ctx_read_u8(&wire);
		if (id == TIMER_LAST_MASTER) {
			wire_ctx_read(&wire, &timers.last_master, sizeof(timers.last_master));
			continue;
		}
		uint64_t value = wire_ctx_read_u64(&wire);
		switch (id) {
		case TIMER_LAST_FLUSH:     timers.last_flush = value; break;
		case TIMER_NEXT_REFRESH:   timers.next_refresh = value; break;
		case TIMER_LAST_REFR_OK:   set_flag(&timers, LAST_REFRESH_OK, value); break;
		case TIMER_LAST_NOTIFIED:
			timers.last_notified_serial = (value & 0xffffffffLLU);
			set_flag(&timers, LAST_NOTIFIED_SERIAL_VALID, (value >> 32));
			break;
		case TIMER_NEXT_DS_CHECK:  timers.next_ds_check = value; break;
		case TIMER_NEXT_DS_PUSH:   timers.next_ds_push = value; break;
		case TIMER_CATALOG_MEMBER: timers.catalog_member = value; break;
		case TIMER_NEXT_EXPIRE:    timers.next_expire = value; break;
		case TIMER_MASTER_PIN_HIT: timers.master_pin_hit = value; break;
		case TIMER_LAST_SIGNED:
			timers.last_signed_serial = (value & 0xffffffffLLU);
			timers.flags |= LAST_SIGNED_SERIAL_FOUND;
			set_flag(&timers, LAST_SIGNED_SERIAL_VALID, (value >> 32));
			break;
		default:                   break; // ignore
		}
	}

	if (wire_ctx_available(&wire) != 0) {
		return KNOT_EMALF;
	}

	assert(wire.error == KNOT_EOK);

	*timers_ptr = timers;
	return KNOT_EOK;
}

static void txn_write_timers(knot_lmdb_txn_t *txn, const knot_dname_t *zone,
                             zone_timers_t *timers)
{
	if (!(timers->flags & TIMERS_MODIFIED)) { // TODO move this conditional to txn_zone_write, as it is also in zone_timers_write. Here temporarily to avoid git conflicts.
		return;
	}
	const char *format = (timers->last_master.sin6_family == AF_INET ||
	                      timers->last_master.sin6_family == AF_INET6) ?
	                     "TTTTTTTTTTBD" :
	                     "TTTTTTTTT";

	MDB_val k = { knot_dname_size(zone), (void *)zone };
	MDB_val v = knot_lmdb_make_key(format,
		TIMER_LAST_FLUSH,    (uint64_t)timers->last_flush,
		TIMER_NEXT_REFRESH,  (uint64_t)timers->next_refresh,
		TIMER_LAST_REFR_OK,  (uint64_t)(bool)(timers->flags & LAST_REFRESH_OK),
		TIMER_LAST_NOTIFIED, (uint64_t)timers->last_notified_serial | (((uint64_t)(bool)(timers->flags & LAST_NOTIFIED_SERIAL_VALID)) << 32),
		TIMER_NEXT_DS_CHECK, (uint64_t)timers->next_ds_check,
		TIMER_NEXT_DS_PUSH,  (uint64_t)timers->next_ds_push,
		TIMER_CATALOG_MEMBER,(uint64_t)timers->catalog_member,
		TIMER_NEXT_EXPIRE,   (uint64_t)timers->next_expire,
		TIMER_LAST_SIGNED,   (uint64_t)timers->last_signed_serial | (((uint64_t)(bool)(timers->flags & LAST_SIGNED_SERIAL_VALID)) << 32),
		TIMER_MASTER_PIN_HIT,(uint64_t)timers->master_pin_hit, // those items should be last two
		TIMER_LAST_MASTER,   &timers->last_master, sizeof(timers->last_master));
	knot_lmdb_insert(txn, &k, &v);
	free(v.mv_data);

	if (txn->ret == KNOT_EOK) {
		timers->flags &= ~TIMERS_MODIFIED;
	}
}


int zone_timers_open(const char *path, knot_db_t **db, size_t mapsize)
{
	if (path == NULL || db == NULL) {
		return KNOT_EINVAL;
	}

	struct knot_db_lmdb_opts opts = KNOT_DB_LMDB_OPTS_INITIALIZER;
	opts.mapsize = mapsize;
	opts.path = path;

	return knot_db_lmdb_api()->init(db, NULL, &opts);
}

void zone_timers_close(knot_db_t *db)
{
	if (db == NULL) {
		return;
	}

	knot_db_lmdb_api()->deinit(db);
}

int zone_timers_read(knot_lmdb_db_t *db, const knot_dname_t *zone,
                     zone_timers_t *timers)
{
	if (knot_lmdb_exists(db) == KNOT_ENODB) {
		return KNOT_ENODB;
	}
	int ret = knot_lmdb_open(db);
	if (ret != KNOT_EOK) {
		return ret;
	}
	knot_lmdb_txn_t txn = { 0 };
	knot_lmdb_begin(db, &txn, false);
	MDB_val k = { knot_dname_size(zone), (void *)zone };
	if (knot_lmdb_find(&txn, &k, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE)) {
		deserialize_timers(timers, txn.cur_val.mv_data, txn.cur_val.mv_size);
	}
	knot_lmdb_abort(&txn);

	return txn.ret;
}

int zone_timers_write(knot_lmdb_db_t *db, const knot_dname_t *zone,
                      zone_timers_t *timers)
{
	int ret = knot_lmdb_open(db);
	if (ret != KNOT_EOK || !(timers->flags & TIMERS_MODIFIED)) {
		return ret;
	}
	knot_lmdb_txn_t txn = { 0 };
	knot_lmdb_begin(db, &txn, true);
	txn_write_timers(&txn, zone, timers);
	knot_lmdb_commit(&txn);
	return txn.ret;
}

static void txn_zone_write(zone_t *z, knot_lmdb_txn_t *txn)
{
	if (z->started) {
                zone_timers_t *t = z->timers_static;
                txn_write_timers(txn, z->name, t);
	}
}

int zone_timers_write_all(knot_lmdb_db_t *db, knot_zonedb_t *zonedb)
{
	int ret = knot_lmdb_open(db);
	if (ret != KNOT_EOK) {
		return ret;
	}
	knot_lmdb_txn_t txn = { 0 };
	knot_lmdb_begin(db, &txn, true);
	knot_zonedb_foreach(zonedb, txn_zone_write, &txn);
	knot_lmdb_commit(&txn);
	return txn.ret;
}

int zone_timers_sweep(knot_lmdb_db_t *db, sweep_cb keep_zone, void *cb_data)
{
	if (knot_lmdb_exists(db) == KNOT_ENODB) {
		return KNOT_EOK;
	}
	int ret = knot_lmdb_open(db);
	if (ret != KNOT_EOK) {
		return ret;
	}
	knot_lmdb_txn_t txn = { 0 };
	knot_lmdb_begin(db, &txn, true);
	knot_lmdb_forwhole(&txn) {
		if (!keep_zone((const knot_dname_t *)txn.cur_key.mv_data, cb_data)) {
			knot_lmdb_del_cur(&txn);
		}
	}
	knot_lmdb_commit(&txn);
	return txn.ret;
}

bool zone_timers_serial_notified(const zone_timers_t *timers, uint32_t serial)
{
	return (timers->flags & LAST_NOTIFIED_SERIAL_VALID) &&
	       (timers->last_notified_serial == serial);
}
