/*  Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
 *  SPDX-License-Identifier: GPL-3.0-or-later
 */

#include "daemon/bindings/impl.h"

/** @internal return cache, or throw lua error if not open */
static struct kr_cache * cache_assert_open(lua_State *L)
{
	struct kr_cache *cache = &the_worker->engine->resolver.cache;
	if (kr_fails_assert(cache) || !kr_cache_is_open(cache))
		lua_error_p(L, "no cache is open yet, use cache.open() or cache.size, etc.");
	return cache;
}

/** Return available cached backends. */
static int cache_backends(lua_State *L)
{
	struct engine *engine = the_worker->engine;

	lua_newtable(L);
	for (unsigned i = 0; i < engine->backends.len; ++i) {
		const struct kr_cdb_api *api = engine->backends.at[i];
		lua_pushboolean(L, api == engine->resolver.cache.api);
		lua_setfield(L, -2, api->name);
	}
	return 1;
}

/** Return number of cached records. */
static int cache_count(lua_State *L)
{
	struct kr_cache *cache = cache_assert_open(L);

	int count = cache->api->count(cache->db, &cache->stats);
	if (count >= 0) {
		/* First key is a version counter, omit it if nonempty. */
		lua_pushinteger(L, count ? count - 1 : 0);
		return 1;
	}
	return 0;
}

/** Return time of last checkpoint, or re-set it if passed `true`. */
static int cache_checkpoint(lua_State *L)
{
	struct kr_cache *cache = cache_assert_open(L);

	if (lua_gettop(L) == 0) { /* Return the current value. */
		lua_newtable(L);
		lua_pushnumber(L, cache->checkpoint_monotime);
		lua_setfield(L, -2, "monotime");
		lua_newtable(L);
		lua_pushnumber(L, cache->checkpoint_walltime.tv_sec);
		lua_setfield(L, -2, "sec");
		lua_pushnumber(L, cache->checkpoint_walltime.tv_usec);
		lua_setfield(L, -2, "usec");
		lua_setfield(L, -2, "walltime");
		return 1;
	}

	if (lua_gettop(L) != 1 || !lua_isboolean(L, 1) || !lua_toboolean(L, 1))
		lua_error_p(L, "cache.checkpoint() takes no parameters or a true value");

	kr_cache_make_checkpoint(cache);
	return 1;
}

/** Return cache statistics. */
static int cache_stats(lua_State *L)
{
	struct kr_cache *cache = cache_assert_open(L);
	lua_newtable(L);
#define add_stat(name) \
	lua_pushinteger(L, (cache->stats.name)); \
	lua_setfield(L, -2, #name)
	add_stat(open);
	add_stat(close);
	add_stat(count);
	cache->stats.count_entries = cache->api->count(cache->db, &cache->stats);
	add_stat(count_entries);
	add_stat(clear);
	add_stat(commit);
	add_stat(read);
	add_stat(read_miss);
	add_stat(write);
	add_stat(remove);
	add_stat(remove_miss);
	add_stat(match);
	add_stat(match_miss);
	add_stat(read_leq);
	add_stat(read_leq_miss);
	/* usage_percent statistics special case - double */
	cache->stats.usage_percent = cache->api->usage_percent(cache->db);
	lua_pushnumber(L, cache->stats.usage_percent);
	lua_setfield(L, -2, "usage_percent");
#undef add_stat

	return 1;
}

static const struct kr_cdb_api *cache_select(struct engine *engine, const char **conf)
{
	/* Return default backend */
	if (*conf == NULL || !strstr(*conf, "://")) {
		return engine->backends.at[0];
	}

	/* Find storage backend from config prefix */
	for (unsigned i = 0; i < engine->backends.len; ++i) {
		const struct kr_cdb_api *api = engine->backends.at[i];
		if (strncmp(*conf, api->name, strlen(api->name)) == 0) {
			*conf += strlen(api->name) + strlen("://");
			return api;
		}
	}

	return NULL;
}

static int cache_max_ttl(lua_State *L)
{
	struct kr_cache *cache = cache_assert_open(L);

	int n = lua_gettop(L);
	if (n > 0) {
		if (!lua_isnumber(L, 1) || n > 1)
			lua_error_p(L, "expected 'max_ttl(number ttl)'");
		uint32_t min = cache->ttl_min;
		int64_t ttl = lua_tointeger(L, 1);
		if (ttl < 1 || ttl < min || ttl > TTL_MAX_MAX) {
			lua_error_p(L,
				"max_ttl must be larger than minimum TTL, and in range <1, "
				STR(TTL_MAX_MAX) ">'");
		}
		cache->ttl_max = ttl;
	}
	lua_pushinteger(L, cache->ttl_max);
	return 1;
}


static int cache_min_ttl(lua_State *L)
{
	struct kr_cache *cache = cache_assert_open(L);

	int n = lua_gettop(L);
	if (n > 0) {
		if (!lua_isnumber(L, 1))
			lua_error_p(L, "expected 'min_ttl(number ttl)'");
		uint32_t max = cache->ttl_max;
		int64_t ttl = lua_tointeger(L, 1);
		if (ttl < 0 || ttl > max || ttl > TTL_MAX_MAX) {
			lua_error_p(L,
				"min_ttl must be smaller than maximum TTL, and in range <0, "
				STR(TTL_MAX_MAX) ">'");
		}
		cache->ttl_min = ttl;
	}
	lua_pushinteger(L, cache->ttl_min);
	return 1;
}

/** Open cache */
static int cache_open(lua_State *L)
{
	/* Check parameters */
	int n = lua_gettop(L);
	if (n < 1 || !lua_isnumber(L, 1))
		lua_error_p(L, "expected 'open(number max_size, string config = \"\")'");

	/* Select cache storage backend */
	struct engine *engine = the_worker->engine;

	lua_Integer csize_lua = lua_tointeger(L, 1);
	if (!(csize_lua >= 8192 && csize_lua < SIZE_MAX)) { /* min. is basically arbitrary */
		lua_error_p(L, "invalid cache size specified, it must be in range <8192, "
				STR(SIZE_MAX)  ">");
	}
	size_t cache_size = csize_lua;

	const char *conf = n > 1 ? lua_tostring(L, 2) : NULL;
	const char *uri = conf;
	const struct kr_cdb_api *api = cache_select(engine, &conf);
	if (!api)
		lua_error_p(L, "unsupported cache backend");

	/* Close if already open */
	kr_cache_close(&engine->resolver.cache);

	/* Reopen cache */
	struct kr_cdb_opts opts = {
		(conf && strlen(conf)) ? conf : ".",
		cache_size
	};
	int ret = kr_cache_open(&engine->resolver.cache, api, &opts, engine->pool);
	if (ret != 0) {
		char cwd[PATH_MAX];
		get_workdir(cwd, sizeof(cwd));
		return luaL_error(L, "can't open cache path '%s'; working directory '%s'; %s",
				  opts.path, cwd, kr_strerror(ret));
	}
	/* Let's check_health() every five seconds to avoid keeping old cache alive
	 * even in case of not having any work to do. */
	ret = kr_cache_check_health(&engine->resolver.cache, 5000);
	if (ret != 0) {
		kr_log_error(CACHE, "periodic health check failed (ignored): %s\n",
				kr_strerror(ret));
	}

	/* Store current configuration */
	lua_getglobal(L, "cache");
	lua_pushstring(L, "current_size");
	lua_pushnumber(L, cache_size);
	lua_rawset(L, -3);
	lua_pushstring(L, "current_storage");
	lua_pushstring(L, uri);
	lua_rawset(L, -3);
	lua_pop(L, 1);

	lua_pushboolean(L, 1);
	return 1;
}

static int cache_close(lua_State *L)
{
	struct kr_cache *cache = &the_worker->engine->resolver.cache;
	if (!kr_cache_is_open(cache)) {
		return 0;
	}

	kr_cache_close(cache);
	lua_getglobal(L, "cache");
	lua_pushstring(L, "current_size");
	lua_pushnumber(L, 0);
	lua_rawset(L, -3);
	lua_pop(L, 1);
	lua_pushboolean(L, 1);
	return 1;
}

#if 0
/** @internal Prefix walk. */
static int cache_prefixed(struct kr_cache *cache, const char *prefix, bool exact_name,
			  knot_db_val_t keyval[][2], int maxcount)
{
	/* Convert to domain name */
	uint8_t buf[KNOT_DNAME_MAXLEN];
	if (!knot_dname_from_str(buf, prefix, sizeof(buf))) {
		return kr_error(EINVAL);
	}
	/* Start prefix search */
	return kr_cache_match(cache, buf, exact_name, keyval, maxcount);
}
#endif

/** Clear everything. */
static int cache_clear_everything(lua_State *L)
{
	struct kr_cache *cache = cache_assert_open(L);

	/* Clear records and packets. */
	int ret = kr_cache_clear(cache);
	lua_error_maybe(L, ret);

	/* Clear reputation tables */
	struct kr_context *ctx = &the_worker->engine->resolver;
	lru_reset(ctx->cache_cookie);
	lua_pushboolean(L, true);
	return 1;
}

#if 0
/** @internal Dump cache key into table on Lua stack. */
static void cache_dump(lua_State *L, knot_db_val_t keyval[])
{
	knot_dname_t dname[KNOT_DNAME_MAXLEN];
	char name[KNOT_DNAME_TXT_MAXLEN];
	uint16_t type;

	int ret = kr_unpack_cache_key(keyval[0], dname, &type);
	if (ret < 0) {
		return;
	}

	ret = !knot_dname_to_str(name, dname, sizeof(name));
	if (kr_fails_assert(!ret)) return;

	/* If name typemap doesn't exist yet, create it */
	lua_getfield(L, -1, name);
	if (lua_isnil(L, -1)) {
		lua_pop(L, 1);
		lua_newtable(L);
	}
	/* Append to typemap */
	char type_buf[KR_RRTYPE_STR_MAXLEN] = { '\0' };
	knot_rrtype_to_string(type, type_buf, sizeof(type_buf));
	lua_pushboolean(L, true);
	lua_setfield(L, -2, type_buf);
	/* Set name typemap */
	lua_setfield(L, -2, name);
}

/** Query cached records.  TODO: fix caveats in ./README.rst documentation? */
static int cache_get(lua_State *L)
{
	//struct kr_cache *cache = cache_assert_open(L); // to be fixed soon

	/* Check parameters */
	int n = lua_gettop(L);
	if (n < 1 || !lua_isstring(L, 1))
		lua_error_p(L, "expected 'cache.get(string key)'");

	/* Retrieve set of keys */
	const char *prefix = lua_tostring(L, 1);
	knot_db_val_t keyval[100][2];
	int ret = cache_prefixed(cache, prefix, false/*FIXME*/, keyval, 100);
	lua_error_maybe(L, ret);
	/* Format output */
	lua_newtable(L);
	for (int i = 0; i < ret; ++i) {
		cache_dump(L, keyval[i]);
	}
	return 1;
}
#endif
static int cache_get(lua_State *L)
{
	lua_error_maybe(L, ENOSYS);
	return kr_error(ENOSYS); /* doesn't happen */
}

/** Set time interval for cleaning rtt cache.
 * Servers with score >= KR_NS_TIMEOUT will be cleaned after
 * this interval ended up, so that they will be able to participate
 * in NS elections again. */
static int cache_ns_tout(lua_State *L)
{
	struct kr_context *ctx = &the_worker->engine->resolver;

	/* Check parameters */
	int n = lua_gettop(L);
	if (n < 1) {
		lua_pushinteger(L, ctx->cache_rtt_tout_retry_interval);
		return 1;
	}

	if (!lua_isnumber(L, 1))
		lua_error_p(L, "expected 'cache.ns_tout(interval in ms)'");

	lua_Integer interval_lua = lua_tointeger(L, 1);
	if (!(interval_lua > 0 && interval_lua < UINT_MAX)) {
		lua_error_p(L, "invalid interval specified, it must be in range > 0, < "
				STR(UINT_MAX));
	}

	ctx->cache_rtt_tout_retry_interval = interval_lua;
	lua_pushinteger(L, ctx->cache_rtt_tout_retry_interval);
	return 1;
}

int kr_bindings_cache(lua_State *L)
{
	static const luaL_Reg lib[] = {
		{ "backends", cache_backends },
		{ "count",  cache_count },
		{ "stats",  cache_stats },
		{ "checkpoint", cache_checkpoint },
		{ "open",   cache_open },
		{ "close",  cache_close },
		{ "clear_everything", cache_clear_everything },
		{ "get",     cache_get },
		{ "max_ttl", cache_max_ttl },
		{ "min_ttl", cache_min_ttl },
		{ "ns_tout", cache_ns_tout },
		{ NULL, NULL }
	};

	luaL_register(L, "cache", lib);
	return 1;
}

