/*
 * Copyright 2023 Vsevolod Stakhov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "lua_common.h"
#include "lua_compress.h"
#include "lptree.h"
#include "utlist.h"
#include "unix-std.h"
#include "ottery.h"
#include "lua_thread_pool.h"
#include "libstat/stat_api.h"
#include "libserver/rspamd_control.h"

#include <math.h>


/* Lua module init function */
#define MODULE_INIT_FUNC "module_init"

#ifdef WITH_LUA_TRACE
ucl_object_t *lua_traces;
#endif

const luaL_reg null_reg[] = {
	{"__tostring", rspamd_lua_class_tostring},
	{NULL, NULL}};

static const char rspamd_modules_state_global[] = "rspamd_plugins_state";

static GQuark
lua_error_quark(void)
{
	return g_quark_from_static_string("lua-routines");
}

/*
 * Used to map string to a pointer
 */
KHASH_INIT(lua_class_set, const char *, int, 1, rspamd_str_hash, rspamd_str_equal);
struct rspamd_lua_context {
	lua_State *L;
	khash_t(lua_class_set) * classes;
	struct rspamd_lua_context *prev, *next; /* Expensive but we usually have exactly one lua state */
};
struct rspamd_lua_context *rspamd_lua_global_ctx = NULL;
#define RSPAMD_LUA_NCLASSES 64
static inline struct rspamd_lua_context *
rspamd_lua_ctx_by_state(lua_State *L)
{
	struct rspamd_lua_context *cur;

	DL_FOREACH(rspamd_lua_global_ctx, cur)
	{
		if (cur->L == L) {
			return cur;
		}
	}

	/* When we are using thread pool, this is the case... */
	return rspamd_lua_global_ctx;
}

/* Util functions */
/**
 * Create new class and store metatable on top of the stack (must be popped if not needed)
 * @param L
 * @param classname name of class
 * @param func table of class methods
 */
void rspamd_lua_new_class(lua_State *L,
						  const gchar *classname,
						  const struct luaL_reg *methods)
{
	khiter_t k;
	gint r, nmethods = 0;
	gboolean seen_index = false;
	struct rspamd_lua_context *ctx = rspamd_lua_ctx_by_state(L);

	if (methods) {
		for (;;) {
			if (methods[nmethods].name != NULL) {
				if (strcmp(methods[nmethods].name, "__index") == 0) {
					seen_index = true;
				}
				nmethods++;
			}
			else {
				break;
			}
		}
	}

	lua_createtable(L, 0, 3 + nmethods);

	if (!seen_index) {
		lua_pushstring(L, "__index");
		lua_pushvalue(L, -2); /* pushes the metatable */
		lua_settable(L, -3);  /* metatable.__index = metatable */
	}

	lua_pushstring(L, "class");
	lua_pushstring(L, classname);
	lua_rawset(L, -3);

	if (methods) {
		luaL_register(L, NULL, methods); /* pushes all methods as MT fields */
	}

	lua_pushvalue(L, -1); /* Preserves metatable */
	int offset = luaL_ref(L, LUA_REGISTRYINDEX);
	k = kh_put(lua_class_set, ctx->classes, classname, &r);
	kh_value(ctx->classes, k) = offset;
	/* MT is left on stack ! */
}

static const gchar *
rspamd_lua_class_tostring_buf(lua_State *L, gboolean print_pointer, gint pos)
{
	static gchar buf[64];
	const gchar *ret = NULL;
	gint pop = 0;

	if (!lua_getmetatable(L, pos)) {
		goto err;
	}

	pop++;
	lua_pushstring(L, "class");
	lua_gettable(L, -2);
	pop++;

	if (!lua_isstring(L, -1)) {
		goto err;
	}

	if (print_pointer) {
		rspamd_snprintf(buf, sizeof(buf), "%s(%p)", lua_tostring(L, -1),
						lua_touserdata(L, 1));
	}
	else {
		rspamd_snprintf(buf, sizeof(buf), "%s", lua_tostring(L, -1));
	}

	ret = buf;

err:
	lua_pop(L, pop);

	return ret;
}

gint rspamd_lua_class_tostring(lua_State *L)
{
	const gchar *p;

	p = rspamd_lua_class_tostring_buf(L, TRUE, 1);

	if (!p) {
		lua_pushstring(L, "invalid object passed to 'lua_common.c:__tostring'");
		return lua_error(L);
	}

	lua_pushstring(L, p);

	return 1;
}


void rspamd_lua_setclass(lua_State *L, const gchar *classname, gint objidx)
{
	khiter_t k;
	struct rspamd_lua_context *ctx = rspamd_lua_ctx_by_state(L);

	k = kh_get(lua_class_set, ctx->classes, classname);

	g_assert(k != kh_end(ctx->classes));
	lua_rawgeti(L, LUA_REGISTRYINDEX, kh_value(ctx->classes, k));

	if (objidx < 0) {
		objidx--;
	}
	lua_setmetatable(L, objidx);
}

void rspamd_lua_class_metatable(lua_State *L, const gchar *classname)
{
	khiter_t k;
	struct rspamd_lua_context *ctx = rspamd_lua_ctx_by_state(L);

	k = kh_get(lua_class_set, ctx->classes, classname);

	g_assert(k != kh_end(ctx->classes));
	lua_rawgeti(L, LUA_REGISTRYINDEX, kh_value(ctx->classes, k));
}

void rspamd_lua_add_metamethod(lua_State *L, const gchar *classname,
							   luaL_Reg *meth)
{
	khiter_t k;
	struct rspamd_lua_context *ctx = rspamd_lua_ctx_by_state(L);

	k = kh_get(lua_class_set, ctx->classes, classname);

	g_assert(k != kh_end(ctx->classes));
	lua_rawgeti(L, LUA_REGISTRYINDEX, kh_value(ctx->classes, k));

	lua_pushcfunction(L, meth->func);
	lua_setfield(L, -2, meth->name);
	lua_pop(L, 1); /* remove metatable */
}

/* assume that table is at the top */
void rspamd_lua_table_set(lua_State *L, const gchar *index, const gchar *value)
{
	lua_pushstring(L, index);
	if (value) {
		lua_pushstring(L, value);
	}
	else {
		lua_pushnil(L);
	}
	lua_settable(L, -3);
}

const gchar *
rspamd_lua_table_get(lua_State *L, const gchar *index)
{
	const gchar *result;

	lua_pushstring(L, index);
	lua_gettable(L, -2);
	if (!lua_isstring(L, -1)) {
		return NULL;
	}
	result = lua_tostring(L, -1);
	lua_pop(L, 1);
	return result;
}

static void
lua_add_actions_global(lua_State *L)
{
	gint i;

	lua_newtable(L);

	for (i = METRIC_ACTION_REJECT; i <= METRIC_ACTION_NOACTION; i++) {
		lua_pushstring(L, rspamd_action_to_str(i));
		lua_pushinteger(L, i);
		lua_settable(L, -3);
	}
	/* Set global table */
	lua_setglobal(L, "rspamd_actions");
}

#ifndef __APPLE__
#define OS_SO_SUFFIX ".so"
#else
#define OS_SO_SUFFIX ".dylib"
#endif

void rspamd_lua_set_path(lua_State *L, const ucl_object_t *cfg_obj, GHashTable *vars)
{
	const gchar *old_path, *additional_path = NULL;
	const ucl_object_t *opts = NULL;
	const gchar *rulesdir = RSPAMD_RULESDIR,
				*lualibdir = RSPAMD_LUALIBDIR,
				*libdir = RSPAMD_LIBDIR;
	const gchar *t;

	gchar path_buf[PATH_MAX];

	lua_getglobal(L, "package");
	lua_getfield(L, -1, "path");
	old_path = luaL_checkstring(L, -1);

	if (strstr(old_path, RSPAMD_LUALIBDIR) != NULL) {
		/* Path has been already set, do not touch it */
		lua_pop(L, 2);
		return;
	}

	if (cfg_obj) {
		opts = ucl_object_lookup(cfg_obj, "options");
		if (opts != NULL) {
			opts = ucl_object_lookup(opts, "lua_path");
			if (opts != NULL && ucl_object_type(opts) == UCL_STRING) {
				additional_path = ucl_object_tostring(opts);
			}
		}
	}

	if (additional_path) {
		rspamd_snprintf(path_buf, sizeof(path_buf),
						"%s;"
						"%s",
						additional_path, old_path);
	}
	else {
		/* Try environment */
		t = getenv("RULESDIR");
		if (t) {
			rulesdir = t;
		}

		t = getenv("LUALIBDIR");
		if (t) {
			lualibdir = t;
		}

		t = getenv("LIBDIR");
		if (t) {
			libdir = t;
		}

		t = getenv("RSPAMD_LIBDIR");
		if (t) {
			libdir = t;
		}

		if (vars) {
			t = g_hash_table_lookup(vars, "RULESDIR");
			if (t) {
				rulesdir = t;
			}

			t = g_hash_table_lookup(vars, "LUALIBDIR");
			if (t) {
				lualibdir = t;
			}

			t = g_hash_table_lookup(vars, "LIBDIR");
			if (t) {
				libdir = t;
			}

			t = g_hash_table_lookup(vars, "RSPAMD_LIBDIR");
			if (t) {
				libdir = t;
			}
		}

		rspamd_snprintf(path_buf, sizeof(path_buf),
						"%s/lua/?.lua;"
						"%s/?.lua;"
						"%s/?.lua;"
						"%s/?/init.lua;"
						"%s",
						RSPAMD_CONFDIR,
						rulesdir,
						lualibdir, lualibdir,
						old_path);
	}

	lua_pop(L, 1);
	lua_pushstring(L, path_buf);
	lua_setfield(L, -2, "path");

	lua_getglobal(L, "package");
	lua_getfield(L, -1, "cpath");
	old_path = luaL_checkstring(L, -1);

	additional_path = NULL;

	if (opts != NULL) {
		opts = ucl_object_lookup(opts, "lua_cpath");
		if (opts != NULL && ucl_object_type(opts) == UCL_STRING) {
			additional_path = ucl_object_tostring(opts);
		}
	}

	if (additional_path) {
		rspamd_snprintf(path_buf, sizeof(path_buf),
						"%s/?%s;"
						"%s",
						additional_path,
						OS_SO_SUFFIX,
						old_path);
	}
	else {
		rspamd_snprintf(path_buf, sizeof(path_buf),
						"%s/?%s;"
						"%s",
						libdir,
						OS_SO_SUFFIX,
						old_path);
	}

	lua_pop(L, 1);
	lua_pushstring(L, path_buf);
	lua_setfield(L, -2, "cpath");

	lua_pop(L, 1);
}

static gint
rspamd_lua_cmp_version_components(const gchar *comp1, const gchar *comp2)
{
	guint v1, v2;

	v1 = strtoul(comp1, NULL, 10);
	v2 = strtoul(comp2, NULL, 10);

	return v1 - v2;
}

static int
rspamd_lua_rspamd_version_cmp(lua_State *L)
{
	const gchar *ver;
	gchar **components;
	gint ret = 0;

	if (lua_type(L, 2) == LUA_TSTRING) {
		ver = lua_tostring(L, 2);

		components = g_strsplit_set(ver, ".-_", -1);

		if (!components) {
			return luaL_error(L, "invalid arguments to 'cmp': %s", ver);
		}

		if (components[0]) {
			ret = rspamd_lua_cmp_version_components(components[0],
													RSPAMD_VERSION_MAJOR);
		}

		if (ret) {
			goto set;
		}

		if (components[1]) {
			ret = rspamd_lua_cmp_version_components(components[1],
													RSPAMD_VERSION_MINOR);
		}

		if (ret) {
			goto set;
		}

		/*
		 * XXX: we don't compare git releases assuming that it is meaningless
		 */
	}
	else {
		return luaL_error(L, "invalid arguments to 'cmp'");
	}

set:
	g_strfreev(components);
	lua_pushinteger(L, ret);

	return 1;
}

static int
rspamd_lua_rspamd_version_numeric(lua_State *L)
{
	static gint64 version_num = RSPAMD_VERSION_NUM;
	const gchar *type;

	if (lua_gettop(L) >= 2 && lua_type(L, 1) == LUA_TSTRING) {
		type = lua_tostring(L, 1);
		if (g_ascii_strcasecmp(type, "short") == 0) {
			version_num = RSPAMD_VERSION_MAJOR_NUM * 1000 +
						  RSPAMD_VERSION_MINOR_NUM * 100 +
						  RSPAMD_VERSION_PATCH_NUM * 10;
		}
		else if (g_ascii_strcasecmp(type, "main") == 0) {
			version_num = RSPAMD_VERSION_MAJOR_NUM * 1000 +
						  RSPAMD_VERSION_MINOR_NUM * 100 +
						  RSPAMD_VERSION_PATCH_NUM * 10;
		}
		else if (g_ascii_strcasecmp(type, "major") == 0) {
			version_num = RSPAMD_VERSION_MAJOR_NUM;
		}
		else if (g_ascii_strcasecmp(type, "patch") == 0) {
			version_num = RSPAMD_VERSION_PATCH_NUM;
		}
		else if (g_ascii_strcasecmp(type, "minor") == 0) {
			version_num = RSPAMD_VERSION_MINOR_NUM;
		}
	}

	lua_pushinteger(L, version_num);

	return 1;
}

static int
rspamd_lua_rspamd_version(lua_State *L)
{
	const gchar *result = NULL, *type;

	if (lua_gettop(L) == 0) {
		result = RVERSION;
	}
	else if (lua_gettop(L) >= 1 && lua_type(L, 1) == LUA_TSTRING) {
		/* We got something like string */
		type = lua_tostring(L, 1);

		if (g_ascii_strcasecmp(type, "short") == 0) {
			result = RSPAMD_VERSION_MAJOR
				"." RSPAMD_VERSION_MINOR;
		}
		else if (g_ascii_strcasecmp(type, "main") == 0) {
			result = RSPAMD_VERSION_MAJOR "." RSPAMD_VERSION_MINOR "." RSPAMD_VERSION_PATCH;
		}
		else if (g_ascii_strcasecmp(type, "major") == 0) {
			result = RSPAMD_VERSION_MAJOR;
		}
		else if (g_ascii_strcasecmp(type, "minor") == 0) {
			result = RSPAMD_VERSION_MINOR;
		}
		else if (g_ascii_strcasecmp(type, "patch") == 0) {
			result = RSPAMD_VERSION_PATCH;
		}
		else if (g_ascii_strcasecmp(type, "id") == 0) {
			result = RID;
		}
		else if (g_ascii_strcasecmp(type, "num") == 0) {
			return rspamd_lua_rspamd_version_numeric(L);
		}
		else if (g_ascii_strcasecmp(type, "cmp") == 0) {
			return rspamd_lua_rspamd_version_cmp(L);
		}
	}

	lua_pushstring(L, result);

	return 1;
}

static gboolean
rspamd_lua_load_env(lua_State *L, const char *fname, gint tbl_pos, GError **err)
{
	gint orig_top = lua_gettop(L), err_idx;
	gboolean ret = TRUE;

	lua_pushcfunction(L, &rspamd_lua_traceback);
	err_idx = lua_gettop(L);

	if (luaL_loadfile(L, fname) != 0) {
		g_set_error(err, g_quark_from_static_string("lua_env"), errno,
					"cannot load lua file %s: %s",
					fname,
					lua_tostring(L, -1));
		ret = FALSE;
	}

	if (ret && lua_pcall(L, 0, 1, err_idx) != 0) {
		g_set_error(err, g_quark_from_static_string("lua_env"), errno,
					"cannot init lua file %s: %s",
					fname,
					lua_tostring(L, -1));
		ret = FALSE;
	}

	if (ret && lua_type(L, -1) == LUA_TTABLE) {
		for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1)) {
			lua_pushvalue(L, -2); /* Store key */
			lua_pushvalue(L, -2); /* Store value */
			lua_settable(L, tbl_pos);
		}
	}
	else if (ret) {
		g_set_error(err, g_quark_from_static_string("lua_env"), errno,
					"invalid return type when loading env from %s: %s",
					fname,
					lua_typename(L, lua_type(L, -1)));
		ret = FALSE;
	}

	lua_settop(L, orig_top);

	return ret;
}

gboolean
rspamd_lua_set_env(lua_State *L, GHashTable *vars, char **lua_env, GError **err)
{
	gint orig_top = lua_gettop(L);
	gchar **env = g_get_environ();

	/* Set known paths as rspamd_paths global */
	lua_getglobal(L, "rspamd_paths");
	if (lua_isnil(L, -1)) {
		const gchar *confdir = RSPAMD_CONFDIR,
					*local_confdir = RSPAMD_LOCAL_CONFDIR,
					*rundir = RSPAMD_RUNDIR,
					*dbdir = RSPAMD_DBDIR,
					*logdir = RSPAMD_LOGDIR,
					*wwwdir = RSPAMD_WWWDIR,
					*pluginsdir = RSPAMD_PLUGINSDIR,
					*rulesdir = RSPAMD_RULESDIR,
					*lualibdir = RSPAMD_LUALIBDIR,
					*prefix = RSPAMD_PREFIX,
					*sharedir = RSPAMD_SHAREDIR;
		const gchar *t;

		/* Try environment */
		t = g_environ_getenv(env, "SHAREDIR");
		if (t) {
			sharedir = t;
		}

		t = g_environ_getenv(env, "PLUGINSDIR");
		if (t) {
			pluginsdir = t;
		}

		t = g_environ_getenv(env, "RULESDIR");
		if (t) {
			rulesdir = t;
		}

		t = g_environ_getenv(env, "DBDIR");
		if (t) {
			dbdir = t;
		}

		t = g_environ_getenv(env, "RUNDIR");
		if (t) {
			rundir = t;
		}

		t = g_environ_getenv(env, "LUALIBDIR");
		if (t) {
			lualibdir = t;
		}

		t = g_environ_getenv(env, "LOGDIR");
		if (t) {
			logdir = t;
		}

		t = g_environ_getenv(env, "WWWDIR");
		if (t) {
			wwwdir = t;
		}

		t = g_environ_getenv(env, "CONFDIR");
		if (t) {
			confdir = t;
		}

		t = g_environ_getenv(env, "LOCAL_CONFDIR");
		if (t) {
			local_confdir = t;
		}


		if (vars) {
			t = g_hash_table_lookup(vars, "SHAREDIR");
			if (t) {
				sharedir = t;
			}

			t = g_hash_table_lookup(vars, "PLUGINSDIR");
			if (t) {
				pluginsdir = t;
			}

			t = g_hash_table_lookup(vars, "RULESDIR");
			if (t) {
				rulesdir = t;
			}

			t = g_hash_table_lookup(vars, "LUALIBDIR");
			if (t) {
				lualibdir = t;
			}

			t = g_hash_table_lookup(vars, "RUNDIR");
			if (t) {
				rundir = t;
			}

			t = g_hash_table_lookup(vars, "WWWDIR");
			if (t) {
				wwwdir = t;
			}

			t = g_hash_table_lookup(vars, "CONFDIR");
			if (t) {
				confdir = t;
			}

			t = g_hash_table_lookup(vars, "LOCAL_CONFDIR");
			if (t) {
				local_confdir = t;
			}

			t = g_hash_table_lookup(vars, "DBDIR");
			if (t) {
				dbdir = t;
			}

			t = g_hash_table_lookup(vars, "LOGDIR");
			if (t) {
				logdir = t;
			}
		}

		lua_createtable(L, 0, 9);

		rspamd_lua_table_set(L, RSPAMD_SHAREDIR_INDEX, sharedir);
		rspamd_lua_table_set(L, RSPAMD_CONFDIR_INDEX, confdir);
		rspamd_lua_table_set(L, RSPAMD_LOCAL_CONFDIR_INDEX, local_confdir);
		rspamd_lua_table_set(L, RSPAMD_RUNDIR_INDEX, rundir);
		rspamd_lua_table_set(L, RSPAMD_DBDIR_INDEX, dbdir);
		rspamd_lua_table_set(L, RSPAMD_LOGDIR_INDEX, logdir);
		rspamd_lua_table_set(L, RSPAMD_WWWDIR_INDEX, wwwdir);
		rspamd_lua_table_set(L, RSPAMD_PLUGINSDIR_INDEX, pluginsdir);
		rspamd_lua_table_set(L, RSPAMD_RULESDIR_INDEX, rulesdir);
		rspamd_lua_table_set(L, RSPAMD_LUALIBDIR_INDEX, lualibdir);
		rspamd_lua_table_set(L, RSPAMD_PREFIX_INDEX, prefix);

		lua_setglobal(L, "rspamd_paths");
	}

	lua_getglobal(L, "rspamd_env");
	if (lua_isnil(L, -1)) {
		lua_newtable(L);

		if (vars != NULL) {
			GHashTableIter it;
			gpointer k, v;

			g_hash_table_iter_init(&it, vars);

			while (g_hash_table_iter_next(&it, &k, &v)) {
				rspamd_lua_table_set(L, k, v);
			}
		}

		gint hostlen = sysconf(_SC_HOST_NAME_MAX);

		if (hostlen <= 0) {
			hostlen = 256;
		}
		else {
			hostlen++;
		}

		gchar *hostbuf = g_alloca(hostlen);
		memset(hostbuf, 0, hostlen);
		gethostname(hostbuf, hostlen - 1);

		rspamd_lua_table_set(L, "hostname", hostbuf);

		rspamd_lua_table_set(L, "version", RVERSION);
		rspamd_lua_table_set(L, "ver_major", RSPAMD_VERSION_MAJOR);
		rspamd_lua_table_set(L, "ver_minor", RSPAMD_VERSION_MINOR);
		rspamd_lua_table_set(L, "ver_id", RID);
		lua_pushstring(L, "ver_num");
		lua_pushinteger(L, RSPAMD_VERSION_NUM);
		lua_settable(L, -3);

		if (env) {
			gint lim = g_strv_length(env);

			for (gint i = 0; i < lim; i++) {
				if (RSPAMD_LEN_CHECK_STARTS_WITH(env[i], strlen(env[i]), "RSPAMD_")) {
					const char *var = env[i] + sizeof("RSPAMD_") - 1, *value;
					gint varlen;

					varlen = strcspn(var, "=");
					value = var + varlen;

					if (*value == '=') {
						value++;

						lua_pushlstring(L, var, varlen);
						lua_pushstring(L, value);
						lua_settable(L, -3);
					}
				}
			}
		}

		if (lua_env) {
			gint lim = g_strv_length(lua_env);

			for (gint i = 0; i < lim; i++) {
				if (!rspamd_lua_load_env(L, lua_env[i], lua_gettop(L), err)) {
					return FALSE;
				}
			}
		}

		lua_setglobal(L, "rspamd_env");
	}

	lua_settop(L, orig_top);
	g_strfreev(env);

	return TRUE;
}

void rspamd_lua_set_globals(struct rspamd_config *cfg, lua_State *L)
{
	struct rspamd_config **pcfg;
	gint orig_top = lua_gettop(L);

	/* First check for global variable 'config' */
	lua_getglobal(L, "config");
	if (lua_isnil(L, -1)) {
		/* Assign global table to set up attributes */
		lua_newtable(L);
		lua_setglobal(L, "config");
	}

	lua_getglobal(L, "metrics");
	if (lua_isnil(L, -1)) {
		lua_newtable(L);
		lua_setglobal(L, "metrics");
	}

	lua_getglobal(L, "composites");
	if (lua_isnil(L, -1)) {
		lua_newtable(L);
		lua_setglobal(L, "composites");
	}

	lua_getglobal(L, "rspamd_classifiers");
	if (lua_isnil(L, -1)) {
		lua_newtable(L);
		lua_setglobal(L, "rspamd_classifiers");
	}

	lua_getglobal(L, "classifiers");
	if (lua_isnil(L, -1)) {
		lua_newtable(L);
		lua_setglobal(L, "classifiers");
	}

	lua_getglobal(L, "rspamd_version");
	if (lua_isnil(L, -1)) {
		lua_pushcfunction(L, rspamd_lua_rspamd_version);
		lua_setglobal(L, "rspamd_version");
	}

	if (cfg != NULL) {
		pcfg = lua_newuserdata(L, sizeof(struct rspamd_config *));
		rspamd_lua_setclass(L, "rspamd{config}", -1);
		*pcfg = cfg;
		lua_setglobal(L, "rspamd_config");
	}

	lua_settop(L, orig_top);
}

#ifdef WITH_LUA_TRACE
static gint
lua_push_trace_data(lua_State *L)
{
	if (lua_traces) {
		ucl_object_push_lua(L, lua_traces, true);
	}
	else {
		lua_pushnil(L);
	}

	return 1;
}
#endif


static void *
rspamd_lua_wipe_realloc(void *ud,
						void *ptr,
						size_t osize,
						size_t nsize) RSPAMD_ATTR_ALLOC_SIZE(4);
static void *
rspamd_lua_wipe_realloc(void *ud,
						void *ptr,
						size_t osize,
						size_t nsize)
{
	if (nsize == 0) {
		if (ptr) {
			rspamd_explicit_memzero(ptr, osize);
		}

		free(ptr);
	}
	else if (ptr == NULL) {
		return malloc(nsize);
	}
	else {
		if (nsize < osize) {
			/* Wipe on shrinking (actually never used) */
			rspamd_explicit_memzero(((unsigned char *) ptr) + nsize, osize - nsize);
		}

		return realloc(ptr, nsize);
	}

	return NULL;
}

#ifndef WITH_LUAJIT
extern int luaopen_bit(lua_State *L);
#endif

static unsigned int lua_initialized = 0;

lua_State *
rspamd_lua_init(bool wipe_mem)
{
	lua_State *L;

	if (wipe_mem) {
#ifdef WITH_LUAJIT
		/* TODO: broken on luajit without GC64 */
		L = luaL_newstate();
#else
		L = lua_newstate(rspamd_lua_wipe_realloc, NULL);
#endif
	}
	else {
		L = luaL_newstate();
	}

	struct rspamd_lua_context *ctx;

	ctx = (struct rspamd_lua_context *) g_malloc0(sizeof(*ctx));
	ctx->L = L;
	ctx->classes = kh_init(lua_class_set);
	kh_resize(lua_class_set, ctx->classes, RSPAMD_LUA_NCLASSES);
	DL_APPEND(rspamd_lua_global_ctx, ctx);

	lua_gc(L, LUA_GCSTOP, 0);
	luaL_openlibs(L);
	luaopen_logger(L);
	luaopen_mempool(L);
	luaopen_config(L);
	luaopen_map(L);
	luaopen_trie(L);
	luaopen_task(L);
	luaopen_textpart(L);
	luaopen_mimepart(L);
	luaopen_image(L);
	luaopen_url(L);
	luaopen_classifier(L);
	luaopen_statfile(L);
	luaopen_regexp(L);
	luaopen_cdb(L);
	luaopen_xmlrpc(L);
	luaopen_http(L);
	luaopen_redis(L);
	luaopen_upstream(L);
	lua_add_actions_global(L);
	luaopen_dns_resolver(L);
	luaopen_rsa(L);
	luaopen_ip(L);
	luaopen_expression(L);
	luaopen_text(L);
	luaopen_util(L);
	luaopen_tcp(L);
	luaopen_html(L);
	luaopen_sqlite3(L);
	luaopen_cryptobox(L);
	luaopen_dns(L);
	luaopen_udp(L);
	luaopen_worker(L);
	luaopen_kann(L);
	luaopen_spf(L);
	luaopen_tensor(L);
	luaopen_parsers(L);
	luaopen_compress(L);
#ifndef WITH_LUAJIT
	rspamd_lua_add_preload(L, "bit", luaopen_bit);
	lua_settop(L, 0);
#endif

	rspamd_lua_new_class(L, "rspamd{session}", NULL);
	lua_pop(L, 1);

	rspamd_lua_add_preload(L, "lpeg", luaopen_lpeg);
	luaopen_ucl(L);
	rspamd_lua_add_preload(L, "ucl", luaopen_ucl);

	/* Add plugins global */
	lua_newtable(L);
	lua_setglobal(L, "rspamd_plugins");

	/* Set PRNG */
	lua_getglobal(L, "math");
	lua_pushstring(L, "randomseed"); /* Push math.randomseed function on top of the stack */
	lua_gettable(L, -2);
	lua_pushinteger(L, ottery_rand_uint64());
	g_assert(lua_pcall(L, 1, 0, 0) == 0);
	lua_pop(L, 1); /* math table */

	/* Modules state */
	lua_newtable(L);
	/*
	 * rspamd_plugins_state = {
	 *   enabled = {},
	 *   disabled_unconfigured = {},
	 *   disabled_redis = {},
	 *   disabled_explicitly = {},
	 *   disabled_failed = {},
	 *   disabled_experimental = {},
	 *   disabled_unknown = {},
	 * }
	 */
#define ADD_TABLE(name)           \
	do {                          \
		lua_pushstring(L, #name); \
		lua_newtable(L);          \
		lua_settable(L, -3);      \
	} while (0)

	ADD_TABLE(enabled);
	ADD_TABLE(disabled_unconfigured);
	ADD_TABLE(disabled_redis);
	ADD_TABLE(disabled_explicitly);
	ADD_TABLE(disabled_failed);
	ADD_TABLE(disabled_experimental);
	ADD_TABLE(disabled_unknown);

#undef ADD_TABLE
	lua_setglobal(L, rspamd_modules_state_global);

#ifdef WITH_LUA_TRACE
	lua_pushcfunction(L, lua_push_trace_data);
	lua_setglobal(L, "get_traces");
#endif

	lua_initialized++;

	return L;
}

void rspamd_lua_close(lua_State *L)
{
	struct rspamd_lua_context *ctx = rspamd_lua_ctx_by_state(L);

	/* TODO: we will leak this memory, but I don't know how to resolve
	 * the chicked-egg problem when lua_close calls GC for many
	 * userdata that requires classes metatables to be represented
	 * For now, it is safe to leave it as is, I'm afraid
	 */
#if 0
	int ref;
	kh_foreach_value(ctx->classes, ref, {
		luaL_unref(L, LUA_REGISTRYINDEX, ref);
	});
#endif

	lua_close(L);
	DL_DELETE(rspamd_lua_global_ctx, ctx);
	kh_destroy(lua_class_set, ctx->classes);
	g_free(ctx);

	lua_initialized--;
}

bool rspamd_lua_is_initialised(void)
{
	return lua_initialized != 0;
}

void rspamd_lua_start_gc(struct rspamd_config *cfg)
{
	lua_State *L = (lua_State *) cfg->lua_state;

	lua_settop(L, 0);
	/* Set up GC */
	lua_gc(L, LUA_GCCOLLECT, 0);
	lua_gc(L, LUA_GCSETSTEPMUL, cfg->lua_gc_step);
	lua_gc(L, LUA_GCSETPAUSE, cfg->lua_gc_pause);
	lua_gc(L, LUA_GCRESTART, 0);
}


void rspamd_plugins_table_push_elt(lua_State *L, const gchar *field_name,
								   const gchar *new_elt)
{
	lua_getglobal(L, rspamd_modules_state_global);

	if (lua_istable(L, -1)) {
		lua_pushstring(L, field_name);
		lua_gettable(L, -2);

		if (lua_istable(L, -1)) {
			lua_pushstring(L, new_elt);
			lua_newtable(L);
			lua_settable(L, -3);
			lua_pop(L, 2); /* Global + element */
		}
		else {
			lua_pop(L, 2); /* Global + element */
		}
	}
	else {
		lua_pop(L, 1);
	}
}

gboolean
rspamd_init_lua_filters(struct rspamd_config *cfg, bool force_load, bool strict)
{
	struct rspamd_config **pcfg;
	struct script_module *module;
	lua_State *L = cfg->lua_state;
	gint err_idx, i;

	pcfg = lua_newuserdata(L, sizeof(struct rspamd_config *));
	rspamd_lua_setclass(L, "rspamd{config}", -1);
	*pcfg = cfg;
	lua_setglobal(L, "rspamd_config");

	PTR_ARRAY_FOREACH(cfg->script_modules, i, module)
	{
		if (module->path) {
			if (!force_load) {
				if (!rspamd_config_is_module_enabled(cfg, module->name)) {
					continue;
				}
			}

			lua_pushcfunction(L, &rspamd_lua_traceback);
			err_idx = lua_gettop(L);

			gsize fsize;
			guint8 *data = rspamd_file_xmap(module->path,
											PROT_READ, &fsize, TRUE);
			guchar digest[rspamd_cryptobox_HASHBYTES];
			gchar *lua_fname;

			if (data == NULL) {
				msg_err_config("cannot mmap %s failed: %s", module->path,
							   strerror(errno));

				lua_settop(L, err_idx - 1); /*  Error function */

				rspamd_plugins_table_push_elt(L, "disabled_failed",
											  module->name);

				if (strict) {
					return FALSE;
				}

				continue;
			}

			module->digest = rspamd_mempool_alloc(cfg->cfg_pool,
												  rspamd_cryptobox_HASHBYTES * 2 + 1);
			rspamd_cryptobox_hash(digest, data, fsize, NULL, 0);
			rspamd_encode_hex_buf(digest, sizeof(digest),
								  module->digest, rspamd_cryptobox_HASHBYTES * 2 + 1);
			module->digest[rspamd_cryptobox_HASHBYTES * 2] = '\0';
			lua_fname = g_malloc(strlen(module->path) + 2);
			rspamd_snprintf(lua_fname, strlen(module->path) + 2, "@%s",
							module->path);

			if (luaL_loadbuffer(L, data, fsize, lua_fname) != 0) {
				msg_err_config("load of %s failed: %s", module->path,
							   lua_tostring(L, -1));
				lua_settop(L, err_idx - 1); /*  Error function */

				rspamd_plugins_table_push_elt(L, "disabled_failed",
											  module->name);
				munmap(data, fsize);
				g_free(lua_fname);

				if (strict) {
					return FALSE;
				}

				continue;
			}

			munmap(data, fsize);
			g_free(lua_fname);

			if (lua_pcall(L, 0, 0, err_idx) != 0) {
				msg_err_config("init of %s failed: %s",
							   module->path,
							   lua_tostring(L, -1));

				lua_settop(L, err_idx - 1);
				rspamd_plugins_table_push_elt(L, "disabled_failed",
											  module->name);

				if (strict) {
					return FALSE;
				}

				continue;
			}

			if (!force_load) {
				msg_info_config("init lua module %s from %s; digest: %*s",
								module->name,
								module->path,
								10, module->digest);
			}

			lua_pop(L, 1); /* Error function */
		}
	}

	return TRUE;
}

void rspamd_lua_dumpstack(lua_State *L)
{
	gint i, t, r = 0;
	gint top = lua_gettop(L);
	gchar buf[BUFSIZ];

	r += rspamd_snprintf(buf + r, sizeof(buf) - r, "lua stack: ");
	for (i = 1; i <= top; i++) { /* repeat for each level */
		t = lua_type(L, i);
		switch (t) {
		case LUA_TSTRING: /* strings */
			r += rspamd_snprintf(buf + r,
								 sizeof(buf) - r,
								 "str: %s",
								 lua_tostring(L, i));
			break;

		case LUA_TBOOLEAN: /* booleans */
			r += rspamd_snprintf(buf + r, sizeof(buf) - r,
								 lua_toboolean(L, i) ? "bool: true" : "bool: false");
			break;

		case LUA_TNUMBER: /* numbers */
			r += rspamd_snprintf(buf + r,
								 sizeof(buf) - r,
								 "number: %.2f",
								 lua_tonumber(L, i));
			break;

		default: /* other values */
			r += rspamd_snprintf(buf + r,
								 sizeof(buf) - r,
								 "type: %s",
								 lua_typename(L, t));
			break;
		}
		if (i < top) {
			r += rspamd_snprintf(buf + r, sizeof(buf) - r,
								 " -> "); /* put a separator */
		}
	}

	msg_info("%*s", r, buf);
}

gpointer
rspamd_lua_check_class(lua_State *L, gint index, const gchar *name)
{
	gpointer p;
	khiter_t k;

	if (lua_type(L, index) == LUA_TUSERDATA) {
		p = lua_touserdata(L, index);
		if (p) {
			if (lua_getmetatable(L, index)) {
				struct rspamd_lua_context *ctx = rspamd_lua_ctx_by_state(L);

				k = kh_get(lua_class_set, ctx->classes, name);

				if (k == kh_end(ctx->classes)) {
					lua_pop(L, 1);

					return NULL;
				}

				lua_rawgeti(L, LUA_REGISTRYINDEX, kh_value(ctx->classes, k));

				if (lua_rawequal(L, -1, -2)) { /* does it have the correct mt? */
					lua_pop(L, 2);             /* remove both metatables */
					return p;
				}
				lua_pop(L, 2);
			}
		}
	}
	return NULL;
}

int rspamd_lua_typerror(lua_State *L, int narg, const char *tname)
{
	const char *msg = lua_pushfstring(L, "%s expected, got %s", tname,
									  luaL_typename(L, narg));
	return luaL_argerror(L, narg, msg);
}


void rspamd_lua_add_preload(lua_State *L, const gchar *name, lua_CFunction func)
{
	lua_getglobal(L, "package");
	lua_pushstring(L, "preload");
	lua_gettable(L, -2);
	lua_pushcfunction(L, func);
	lua_setfield(L, -2, name);
	lua_pop(L, 2); /* preload key + global package */
}


gboolean
rspamd_lua_parse_table_arguments(lua_State *L, gint pos,
								 GError **err,
								 enum rspamd_lua_parse_arguments_flags how,
								 const gchar *extraction_pattern, ...)
{
	const gchar *p, *key = NULL, *end, *cls;
	va_list ap;
	gboolean required = FALSE, failed = FALSE, is_table;
	gchar classbuf[128];
	enum {
		read_key = 0,
		read_arg,
		read_class_start,
		read_class,
		read_semicolon
	} state = read_key;
	gsize keylen = 0, *valuelen, clslen;
	gint idx = 0, t, direct_userdata = 0;

	g_assert(extraction_pattern != NULL);

	if (pos < 0) {
		/* Get absolute pos */
		pos = lua_gettop(L) + pos + 1;
	}

	if (lua_type(L, pos) == LUA_TTABLE) {
		is_table = TRUE;
	}
	else {
		is_table = FALSE;
		idx = pos;
	}

	p = extraction_pattern;
	end = p + strlen(extraction_pattern);

	va_start(ap, extraction_pattern);

	while (p <= end) {
		switch (state) {
		case read_key:
			if (*p == '=') {
				if (key == NULL) {
					g_set_error(err, lua_error_quark(), 1, "cannot read key");
					va_end(ap);

					return FALSE;
				}

				state = read_arg;
				keylen = p - key;
			}
			else if (*p == '*' && key == NULL) {
				required = TRUE;
			}
			else if (key == NULL) {
				key = p;
			}
			p++;
			break;
		case read_arg:
			g_assert(keylen != 0);

			if (is_table) {
				lua_pushlstring(L, key, keylen);
				lua_gettable(L, pos);
				idx = -1;
			}

			t = lua_type(L, idx);

			switch (*p) {
			case 'S':
				if (t == LUA_TSTRING) {
					*(va_arg(ap, const gchar **)) = lua_tostring(L, idx);
				}
				else if (t == LUA_TNIL || t == LUA_TNONE) {
					failed = TRUE;

					if (how != RSPAMD_LUA_PARSE_ARGUMENTS_IGNORE_MISSING) {
						*(va_arg(ap, const gchar **)) = NULL;
					}
					else {
						(void) va_arg(ap, gchar **);
					}
				}
				else {
					g_set_error(err,
								lua_error_quark(),
								1,
								"bad type for key:"
								" %.*s: '%s', '%s' is expected",
								(gint) keylen,
								key,
								lua_typename(L, lua_type(L, idx)), "string");
					va_end(ap);

					return FALSE;
				}

				if (is_table) {
					lua_pop(L, 1);
				}
				break;

			case 'I':
				if (t == LUA_TNUMBER) {
					*(va_arg(ap, gint64 *)) = lua_tointeger(L, idx);
				}
				else if (t == LUA_TNIL || t == LUA_TNONE) {
					failed = TRUE;
					if (how != RSPAMD_LUA_PARSE_ARGUMENTS_IGNORE_MISSING) {
						*(va_arg(ap, gint64 *)) = 0;
					}
					else {
						(void) va_arg(ap, gint64 *);
					}
				}
				else {
					g_set_error(err,
								lua_error_quark(),
								1,
								"bad type for key:"
								" %.*s: '%s', '%s' is expected",
								(gint) keylen,
								key,
								lua_typename(L, lua_type(L, idx)),
								"int64");
					va_end(ap);

					return FALSE;
				}
				if (is_table) {
					lua_pop(L, 1);
				}
				break;

			case 'i':
				if (t == LUA_TNUMBER) {
					*(va_arg(ap, gint32 *)) = lua_tointeger(L, idx);
				}
				else if (t == LUA_TNIL || t == LUA_TNONE) {
					failed = TRUE;
					if (how != RSPAMD_LUA_PARSE_ARGUMENTS_IGNORE_MISSING) {
						*(va_arg(ap, gint32 *)) = 0;
					}
					else {
						(void) va_arg(ap, gint32 *);
					}
				}
				else {
					g_set_error(err,
								lua_error_quark(),
								1,
								"bad type for key:"
								" %.*s: '%s', '%s' is expected",
								(gint) keylen,
								key,
								lua_typename(L, lua_type(L, idx)),
								"int64");
					va_end(ap);

					return FALSE;
				}
				if (is_table) {
					lua_pop(L, 1);
				}
				break;

			case 'F':
				if (t == LUA_TFUNCTION) {
					if (!is_table) {
						lua_pushvalue(L, idx);
					}

					*(va_arg(ap, gint *)) = luaL_ref(L, LUA_REGISTRYINDEX);
				}
				else if (t == LUA_TNIL || t == LUA_TNONE) {
					failed = TRUE;

					if (how != RSPAMD_LUA_PARSE_ARGUMENTS_IGNORE_MISSING) {
						*(va_arg(ap, gint *)) = -1;
					}
					else {
						(void) va_arg(ap, gint *);
					}

					if (is_table) {
						lua_pop(L, 1);
					}
				}
				else {
					g_set_error(err,
								lua_error_quark(),
								1,
								"bad type for key:"
								" %.*s: '%s', '%s' is expected",
								(gint) keylen,
								key,
								lua_typename(L, lua_type(L, idx)),
								"function");
					va_end(ap);
					if (is_table) {
						lua_pop(L, 1);
					}

					return FALSE;
				}

				/* luaL_ref pops argument from the stack */
				break;

			case 'B':
				if (t == LUA_TBOOLEAN) {
					*(va_arg(ap, gboolean *)) = lua_toboolean(L, idx);
				}
				else if (t == LUA_TNIL || t == LUA_TNONE) {
					failed = TRUE;

					if (how != RSPAMD_LUA_PARSE_ARGUMENTS_IGNORE_MISSING) {
						*(va_arg(ap, gboolean *)) = 0;
					}
				}
				else {
					g_set_error(err,
								lua_error_quark(),
								1,
								"bad type for key:"
								" %.*s: '%s', '%s' is expected",
								(gint) keylen,
								key,
								lua_typename(L, lua_type(L, idx)),
								"bool");
					va_end(ap);

					return FALSE;
				}

				if (is_table) {
					lua_pop(L, 1);
				}
				break;

			case 'N':
				if (t == LUA_TNUMBER) {
					*(va_arg(ap, gdouble *)) = lua_tonumber(L, idx);
				}
				else if (t == LUA_TNIL || t == LUA_TNONE) {
					failed = TRUE;

					if (how != RSPAMD_LUA_PARSE_ARGUMENTS_IGNORE_MISSING) {
						*(va_arg(ap, gdouble *)) = 0;
					}
					else {
						(void) va_arg(ap, gdouble *);
					}
				}
				else {
					g_set_error(err,
								lua_error_quark(),
								1,
								"bad type for key:"
								" %.*s: '%s', '%s' is expected",
								(gint) keylen,
								key,
								lua_typename(L, lua_type(L, idx)),
								"double");
					va_end(ap);

					return FALSE;
				}

				if (is_table) {
					lua_pop(L, 1);
				}
				break;

			case 'D':
				if (t == LUA_TNUMBER) {
					*(va_arg(ap, gdouble *)) = lua_tonumber(L, idx);
				}
				else if (t == LUA_TNIL || t == LUA_TNONE) {
					failed = TRUE;

					if (how != RSPAMD_LUA_PARSE_ARGUMENTS_IGNORE_MISSING) {
						*(va_arg(ap, gdouble *)) = NAN;
					}
					else {
						(void) va_arg(ap, gdouble *);
					}
				}
				else {
					g_set_error(err,
								lua_error_quark(),
								1,
								"bad type for key:"
								" %.*s: '%s', '%s' is expected",
								(gint) keylen,
								key,
								lua_typename(L, lua_type(L, idx)),
								"double");
					va_end(ap);

					return FALSE;
				}

				if (is_table) {
					lua_pop(L, 1);
				}
				break;

			case 'V':
				valuelen = va_arg(ap, gsize *);

				if (t == LUA_TSTRING) {
					*(va_arg(ap, const gchar **)) = lua_tolstring(L, idx,
																  valuelen);
				}
				else if (t == LUA_TNIL || t == LUA_TNONE) {
					failed = TRUE;

					if (how != RSPAMD_LUA_PARSE_ARGUMENTS_IGNORE_MISSING) {
						*(va_arg(ap, const char **)) = NULL;
						*valuelen = 0;
					}
					else {
						(void) va_arg(ap, const char **);
					}
				}
				else {
					g_set_error(err,
								lua_error_quark(),
								1,
								"bad type for key:"
								" %.*s: '%s', '%s' is expected",
								(gint) keylen,
								key,
								lua_typename(L, lua_type(L, idx)),
								"string");
					va_end(ap);

					return FALSE;
				}

				if (is_table) {
					lua_pop(L, 1);
				}
				break;
			case 'O':
				if (t != LUA_TNONE) {
					*(va_arg(ap, ucl_object_t **)) = ucl_object_lua_import(L,
																		   idx);
				}
				else {
					failed = TRUE;

					if (how != RSPAMD_LUA_PARSE_ARGUMENTS_IGNORE_MISSING) {
						*(va_arg(ap, ucl_object_t **)) = NULL;
					}
					else {
						(void) va_arg(ap, ucl_object_t **);
					}
				}

				if (is_table) {
					lua_pop(L, 1);
				}
				break;
			case 'U':
				if (t == LUA_TNIL || t == LUA_TNONE) {
					failed = TRUE;

					if (how != RSPAMD_LUA_PARSE_ARGUMENTS_IGNORE_MISSING) {
						*(va_arg(ap, void **)) = NULL;
					}
					else {
						(void) va_arg(ap, void **);
					}
				}
				else if (t != LUA_TUSERDATA) {
					g_set_error(err,
								lua_error_quark(),
								1,
								"bad type for key:"
								" %.*s: '%s', '%s' is expected",
								(gint) keylen,
								key,
								lua_typename(L, lua_type(L, idx)),
								"int64");
					va_end(ap);

					return FALSE;
				}

				state = read_class_start;
				clslen = 0;
				direct_userdata = 0;
				cls = NULL;
				p++;
				continue;
			case 'u':
				if (t == LUA_TNIL || t == LUA_TNONE) {
					failed = TRUE;

					if (how != RSPAMD_LUA_PARSE_ARGUMENTS_IGNORE_MISSING) {
						*(va_arg(ap, void **)) = NULL;
					}
					else {
						(void) va_arg(ap, void **);
					}
				}
				else if (t != LUA_TUSERDATA) {
					g_set_error(err,
								lua_error_quark(),
								1,
								"bad type for key:"
								" %.*s: '%s', '%s' is expected",
								(gint) keylen,
								key,
								lua_typename(L, lua_type(L, idx)),
								"int64");
					va_end(ap);

					return FALSE;
				}

				state = read_class_start;
				clslen = 0;
				direct_userdata = 1;
				cls = NULL;
				p++;
				continue;
			default:
				g_assert(0);
				break;
			}

			if (failed && required) {
				g_set_error(err, lua_error_quark(), 2, "required parameter "
													   "%.*s is missing",
							(gint) keylen, key);
				va_end(ap);

				return FALSE;
			}

			if (!is_table) {
				idx++;
			}

			/* Reset read params */
			state = read_semicolon;
			failed = FALSE;
			required = FALSE;
			keylen = 0;
			key = NULL;
			p++;
			break;

		case read_class_start:
			if (*p == '{') {
				cls = p + 1;
				state = read_class;
			}
			else {
				if (is_table) {
					lua_pop(L, 1);
				}

				g_set_error(err, lua_error_quark(), 2, "missing classname for "
													   "%.*s",
							(gint) keylen, key);
				va_end(ap);

				return FALSE;
			}
			p++;
			break;

		case read_class:
			if (*p == '}') {
				clslen = p - cls;
				if (clslen == 0) {
					if (is_table) {
						lua_pop(L, 1);
					}

					g_set_error(err,
								lua_error_quark(),
								2,
								"empty classname for "
								"%*.s",
								(gint) keylen,
								key);
					va_end(ap);

					return FALSE;
				}

				rspamd_snprintf(classbuf, sizeof(classbuf), "rspamd{%*s}",
								(gint) clslen, cls);


				/*
				 * We skip class check here for speed in non-table mode
				 */
				if (!failed && (!is_table ||
								rspamd_lua_check_class(L, idx, classbuf))) {
					if (direct_userdata) {
						void **arg_p = (va_arg(ap, void **));
						*arg_p = lua_touserdata(L, idx);
					}
					else {
						*(va_arg(ap,
								 void **)) = *(void **) lua_touserdata(L, idx);
					}
				}
				else {
					if (!failed) {
						g_set_error(err,
									lua_error_quark(),
									2,
									"invalid class for key %.*s, expected %s, got %s",
									(gint) keylen,
									key,
									classbuf,
									rspamd_lua_class_tostring_buf(L, FALSE, idx));
						va_end(ap);

						return FALSE;
					}
				}

				if (is_table) {
					lua_pop(L, 1);
				}
				else {
					idx++;
				}

				if (failed && required) {
					g_set_error(err,
								lua_error_quark(),
								2,
								"required parameter "
								"%.*s is missing",
								(gint) keylen,
								key);
					va_end(ap);

					return FALSE;
				}

				/* Reset read params */
				state = read_semicolon;
				failed = FALSE;
				required = FALSE;
				keylen = 0;
				key = NULL;
			}
			p++;
			break;

		case read_semicolon:
			if (*p == ';' || p == end) {
				state = read_key;
				key = NULL;
				keylen = 0;
				failed = FALSE;
			}
			else {
				g_set_error(err, lua_error_quark(), 2, "bad format string: %s,"
													   " at char %c, position %d",
							extraction_pattern, *p, (int) (p - extraction_pattern));
				va_end(ap);

				return FALSE;
			}

			p++;
			break;
		}
	}

	va_end(ap);

	return TRUE;
}

static void
rspamd_lua_traceback_string(lua_State *L, luaL_Buffer *buf)
{
	gint i = 1, r;
	lua_Debug d;
	gchar tmp[256];

	while (lua_getstack(L, i++, &d)) {
		lua_getinfo(L, "nSl", &d);
		r = rspamd_snprintf(tmp, sizeof(tmp), " [%d]:{%s:%d - %s [%s]};",
							i - 1, d.short_src, d.currentline,
							(d.name ? d.name : "<unknown>"), d.what);
		luaL_addlstring(buf, tmp, r);
	}
}

gint rspamd_lua_traceback(lua_State *L)
{
	luaL_Buffer b;

	luaL_buffinit(L, &b);
	rspamd_lua_get_traceback_string(L, &b);
	luaL_pushresult(&b);

	return 1;
}

void rspamd_lua_get_traceback_string(lua_State *L, luaL_Buffer *buf)
{
	const gchar *msg = lua_tostring(L, -1);

	if (msg) {
		luaL_addstring(buf, msg);
		lua_pop(L, 1); /* Error string */
	}
	else {
		luaL_addstring(buf, "unknown error");
	}

	luaL_addstring(buf, "; trace:");
	rspamd_lua_traceback_string(L, buf);
}

guint rspamd_lua_table_size(lua_State *L, gint tbl_pos)
{
	guint tbl_size = 0;

	if (!lua_istable(L, tbl_pos)) {
		return 0;
	}

#if LUA_VERSION_NUM >= 502
	tbl_size = lua_rawlen(L, tbl_pos);
#else
	tbl_size = lua_objlen(L, tbl_pos);
#endif

	return tbl_size;
}

static void *
rspamd_lua_check_udata_common(lua_State *L, gint pos, const gchar *classname,
							  gboolean fatal)
{
	void *p = lua_touserdata(L, pos);
	guint i, top = lua_gettop(L);
	khiter_t k;

	if (p == NULL) {
		goto err;
	}
	else {
		/* Match class */
		if (lua_getmetatable(L, pos)) {
			struct rspamd_lua_context *ctx = rspamd_lua_ctx_by_state(L);

			k = kh_get(lua_class_set, ctx->classes, classname);

			if (k == kh_end(ctx->classes)) {
				goto err;
			}

			lua_rawgeti(L, LUA_REGISTRYINDEX, kh_value(ctx->classes, k));

			if (!lua_rawequal(L, -1, -2)) {
				goto err;
			}
		}
		else {
			goto err;
		}
	}

	lua_settop(L, top);

	return p;

err:
	if (fatal) {
		const gchar *actual_classname = NULL;

		if (lua_type(L, pos) == LUA_TUSERDATA && lua_getmetatable(L, pos)) {
			lua_pushstring(L, "__index");
			lua_gettable(L, -2);
			lua_pushstring(L, "class");
			lua_gettable(L, -2);
			actual_classname = lua_tostring(L, -1);
		}
		else {
			actual_classname = lua_typename(L, lua_type(L, pos));
		}

		luaL_Buffer buf;
		gchar tmp[512];
		gint r;

		luaL_buffinit(L, &buf);
		r = rspamd_snprintf(tmp, sizeof(tmp),
							"expected %s at position %d, but userdata has "
							"%s metatable; trace: ",
							classname, pos, actual_classname);
		luaL_addlstring(&buf, tmp, r);
		rspamd_lua_traceback_string(L, &buf);
		r = rspamd_snprintf(tmp, sizeof(tmp), " stack(%d): ", top);
		luaL_addlstring(&buf, tmp, r);

		for (i = 1; i <= MIN(top, 10); i++) {
			if (lua_type(L, i) == LUA_TUSERDATA) {
				const char *clsname;

				if (lua_getmetatable(L, i)) {
					lua_pushstring(L, "__index");
					lua_gettable(L, -2);
					lua_pushstring(L, "class");
					lua_gettable(L, -2);
					clsname = lua_tostring(L, -1);
				}
				else {
					clsname = lua_typename(L, lua_type(L, i));
				}

				r = rspamd_snprintf(tmp, sizeof(tmp), "[%d: ud=%s] ", i,
									clsname);
				luaL_addlstring(&buf, tmp, r);
			}
			else {
				r = rspamd_snprintf(tmp, sizeof(tmp), "[%d: %s] ", i,
									lua_typename(L, lua_type(L, i)));
				luaL_addlstring(&buf, tmp, r);
			}
		}

		luaL_pushresult(&buf);
		msg_err("lua type error: %s", lua_tostring(L, -1));
	}

	lua_settop(L, top);

	return NULL;
}

void *
rspamd_lua_check_udata(lua_State *L, gint pos, const gchar *classname)
{
	return rspamd_lua_check_udata_common(L, pos, classname, TRUE);
}

void *
rspamd_lua_check_udata_maybe(lua_State *L, gint pos, const gchar *classname)
{
	return rspamd_lua_check_udata_common(L, pos, classname, FALSE);
}

struct rspamd_async_session *
lua_check_session(lua_State *L, gint pos)
{
	void *ud = rspamd_lua_check_udata(L, pos, "rspamd{session}");
	luaL_argcheck(L, ud != NULL, pos, "'session' expected");
	return ud ? *((struct rspamd_async_session **) ud) : NULL;
}

struct ev_loop *
lua_check_ev_base(lua_State *L, gint pos)
{
	void *ud = rspamd_lua_check_udata(L, pos, "rspamd{ev_base}");
	luaL_argcheck(L, ud != NULL, pos, "'event_base' expected");
	return ud ? *((struct ev_loop **) ud) : NULL;
}

static void rspamd_lua_run_postloads_error(struct thread_entry *thread, int ret, const char *msg);

void rspamd_lua_run_postloads(lua_State *L, struct rspamd_config *cfg,
							  struct ev_loop *ev_base, struct rspamd_worker *w)
{
	struct rspamd_config_cfg_lua_script *sc;
	struct rspamd_config **pcfg;
	struct ev_loop **pev_base;
	struct rspamd_worker **pw;

	/* Execute post load scripts */
	LL_FOREACH(cfg->on_load_scripts, sc)
	{
		struct thread_entry *thread = lua_thread_pool_get_for_config(cfg);
		thread->error_callback = rspamd_lua_run_postloads_error;
		L = thread->lua_state;

		lua_rawgeti(L, LUA_REGISTRYINDEX, sc->cbref);
		pcfg = lua_newuserdata(L, sizeof(*pcfg));
		*pcfg = cfg;
		rspamd_lua_setclass(L, "rspamd{config}", -1);

		pev_base = lua_newuserdata(L, sizeof(*pev_base));
		*pev_base = ev_base;
		rspamd_lua_setclass(L, "rspamd{ev_base}", -1);

		pw = lua_newuserdata(L, sizeof(*pw));
		*pw = w;
		rspamd_lua_setclass(L, "rspamd{worker}", -1);

		lua_thread_call(thread, 3);
	}
}


void rspamd_lua_run_config_post_init(lua_State *L, struct rspamd_config *cfg)
{
	struct rspamd_config_cfg_lua_script *sc;
	struct rspamd_config **pcfg;

	LL_FOREACH(cfg->post_init_scripts, sc)
	{
		lua_pushcfunction(L, &rspamd_lua_traceback);
		gint err_idx = lua_gettop(L);

		lua_rawgeti(L, LUA_REGISTRYINDEX, sc->cbref);
		pcfg = lua_newuserdata(L, sizeof(*pcfg));
		*pcfg = cfg;
		rspamd_lua_setclass(L, "rspamd{config}", -1);

		if (lua_pcall(L, 1, 0, err_idx) != 0) {
			msg_err_config("cannot run config post init script: %s; priority = %d",
						   lua_tostring(L, -1), sc->priority);
		}

		lua_settop(L, err_idx - 1);
	}
}


void rspamd_lua_run_config_unload(lua_State *L, struct rspamd_config *cfg)
{
	struct rspamd_config_cfg_lua_script *sc;
	struct rspamd_config **pcfg;

	LL_FOREACH(cfg->config_unload_scripts, sc)
	{
		lua_pushcfunction(L, &rspamd_lua_traceback);
		gint err_idx = lua_gettop(L);

		lua_rawgeti(L, LUA_REGISTRYINDEX, sc->cbref);
		pcfg = lua_newuserdata(L, sizeof(*pcfg));
		*pcfg = cfg;
		rspamd_lua_setclass(L, "rspamd{config}", -1);

		if (lua_pcall(L, 1, 0, err_idx) != 0) {
			msg_err_config("cannot run config post init script: %s",
						   lua_tostring(L, -1));
		}

		lua_settop(L, err_idx - 1);
	}
}

static void
rspamd_lua_run_postloads_error(struct thread_entry *thread, int ret, const char *msg)
{
	struct rspamd_config *cfg = thread->cfg;

	msg_err_config("error executing post load code: %s", msg);
}


struct rspamd_lua_ref_cbdata {
	lua_State *L;
	gint cbref;
};

static void
rspamd_lua_ref_dtor(gpointer p)
{
	struct rspamd_lua_ref_cbdata *cbdata = p;

	luaL_unref(cbdata->L, LUA_REGISTRYINDEX, cbdata->cbref);
}

void rspamd_lua_add_ref_dtor(lua_State *L, rspamd_mempool_t *pool,
							 gint ref)
{
	struct rspamd_lua_ref_cbdata *cbdata;

	if (ref != -1) {
		cbdata = rspamd_mempool_alloc(pool, sizeof(*cbdata));
		cbdata->cbref = ref;
		cbdata->L = L;

		rspamd_mempool_add_destructor(pool, rspamd_lua_ref_dtor, cbdata);
	}
}

gboolean
rspamd_lua_require_function(lua_State *L, const gchar *modname,
							const gchar *funcname)
{
	gint table_pos, err_pos;

	lua_pushcfunction(L, &rspamd_lua_traceback);
	err_pos = lua_gettop(L);
	lua_getglobal(L, "require");

	if (lua_isnil(L, -1)) {
		lua_remove(L, err_pos);
		lua_pop(L, 1);

		return FALSE;
	}

	lua_pushstring(L, modname);

	/* Now try to call */
	if (lua_pcall(L, 1, 1, 0) != 0) {
		lua_remove(L, err_pos);
		msg_warn("require of %s.%s failed: %s", modname,
				 funcname, lua_tostring(L, -1));
		lua_pop(L, 1);

		return FALSE;
	}

	lua_remove(L, err_pos);

	/* Now we should have a table with results */
	if (funcname) {
		if (!lua_istable(L, -1)) {
			msg_warn("require of %s.%s failed: not a table but %s", modname,
					 funcname, lua_typename(L, lua_type(L, -1)));

			lua_pop(L, 1);

			return FALSE;
		}

		table_pos = lua_gettop(L);
		lua_pushstring(L, funcname);
		lua_gettable(L, -2);

		if (lua_type(L, -1) == LUA_TFUNCTION) {
			/* Remove table, preserve just a function */
			lua_remove(L, table_pos);

			return TRUE;
		}
		else {
			msg_warn("require of %s.%s failed: not a function but %s", modname,
					 funcname, lua_typename(L, lua_type(L, -1)));
		}

		lua_pop(L, 2);

		return FALSE;
	}
	else if (lua_isfunction(L, -1)) {
		return TRUE;
	}
	else {
		msg_warn("require of %s failed: not a function but %s", modname,
				 lua_typename(L, lua_type(L, -1)));
		lua_pop(L, 1);

		return FALSE;
	}
}

gint rspamd_lua_function_ref_from_str(lua_State *L, const gchar *str, gsize slen,
									  const gchar *modname, GError **err)
{
	gint err_idx, ref_idx;

	lua_pushcfunction(L, &rspamd_lua_traceback);
	err_idx = lua_gettop(L);

	/* Load file */
	if (luaL_loadbuffer(L, str, slen, modname) != 0) {
		g_set_error(err,
					lua_error_quark(),
					EINVAL,
					"%s: cannot load lua script: %s",
					modname,
					lua_tostring(L, -1));
		lua_settop(L, err_idx - 1); /* Error function */

		return LUA_NOREF;
	}

	/* Now call it */
	if (lua_pcall(L, 0, 1, err_idx) != 0) {
		g_set_error(err,
					lua_error_quark(),
					EINVAL,
					"%s: cannot init lua script: %s",
					modname,
					lua_tostring(L, -1));
		lua_settop(L, err_idx - 1);

		return LUA_NOREF;
	}

	if (!lua_isfunction(L, -1)) {
		g_set_error(err,
					lua_error_quark(),
					EINVAL,
					"%s: cannot init lua script: "
					"must return function not %s",
					modname,
					lua_typename(L, lua_type(L, -1)));
		lua_settop(L, err_idx - 1);

		return LUA_NOREF;
	}

	ref_idx = luaL_ref(L, LUA_REGISTRYINDEX);
	lua_settop(L, err_idx - 1);

	return ref_idx;
}


gboolean
rspamd_lua_try_load_redis(lua_State *L, const ucl_object_t *obj,
						  struct rspamd_config *cfg, gint *ref_id)
{
	gint err_idx;
	struct rspamd_config **pcfg;

	lua_pushcfunction(L, &rspamd_lua_traceback);
	err_idx = lua_gettop(L);

	/* Obtain function */
	if (!rspamd_lua_require_function(L, "lua_redis", "try_load_redis_servers")) {
		msg_err_config("cannot require lua_redis");
		lua_pop(L, 2);

		return FALSE;
	}

	/* Function arguments */
	ucl_object_push_lua(L, obj, false);
	pcfg = lua_newuserdata(L, sizeof(*pcfg));
	rspamd_lua_setclass(L, "rspamd{config}", -1);
	*pcfg = cfg;
	lua_pushboolean(L, false); /* no_fallback */

	if (lua_pcall(L, 3, 1, err_idx) != 0) {
		msg_err_config("cannot call lua try_load_redis_servers script: %s",
					   lua_tostring(L, -1));
		lua_settop(L, 0);

		return FALSE;
	}

	if (lua_istable(L, -1)) {
		if (ref_id) {
			/* Ref table */
			lua_pushvalue(L, -1);
			*ref_id = luaL_ref(L, LUA_REGISTRYINDEX);
			lua_settop(L, 0);
		}
		else {
			/* Leave it on the stack */
			lua_insert(L, err_idx);
			lua_settop(L, err_idx);
		}

		return TRUE;
	}
	else {
		lua_settop(L, 0);
	}

	return FALSE;
}

void rspamd_lua_push_full_word(lua_State *L, rspamd_stat_token_t *w)
{
	gint fl_cnt;

	lua_createtable(L, 4, 0);

	if (w->stemmed.len > 0) {
		lua_pushlstring(L, w->stemmed.begin, w->stemmed.len);
		lua_rawseti(L, -2, 1);
	}
	else {
		lua_pushstring(L, "");
		lua_rawseti(L, -2, 1);
	}

	if (w->normalized.len > 0) {
		lua_pushlstring(L, w->normalized.begin, w->normalized.len);
		lua_rawseti(L, -2, 2);
	}
	else {
		lua_pushstring(L, "");
		lua_rawseti(L, -2, 2);
	}

	if (w->original.len > 0) {
		lua_pushlstring(L, w->original.begin, w->original.len);
		lua_rawseti(L, -2, 3);
	}
	else {
		lua_pushstring(L, "");
		lua_rawseti(L, -2, 3);
	}

	/* Flags part */
	fl_cnt = 1;
	lua_createtable(L, 4, 0);

	if (w->flags & RSPAMD_STAT_TOKEN_FLAG_NORMALISED) {
		lua_pushstring(L, "normalised");
		lua_rawseti(L, -2, fl_cnt++);
	}
	if (w->flags & RSPAMD_STAT_TOKEN_FLAG_BROKEN_UNICODE) {
		lua_pushstring(L, "broken_unicode");
		lua_rawseti(L, -2, fl_cnt++);
	}
	if (w->flags & RSPAMD_STAT_TOKEN_FLAG_UTF) {
		lua_pushstring(L, "utf");
		lua_rawseti(L, -2, fl_cnt++);
	}
	if (w->flags & RSPAMD_STAT_TOKEN_FLAG_TEXT) {
		lua_pushstring(L, "text");
		lua_rawseti(L, -2, fl_cnt++);
	}
	if (w->flags & RSPAMD_STAT_TOKEN_FLAG_HEADER) {
		lua_pushstring(L, "header");
		lua_rawseti(L, -2, fl_cnt++);
	}
	if (w->flags & (RSPAMD_STAT_TOKEN_FLAG_META | RSPAMD_STAT_TOKEN_FLAG_LUA_META)) {
		lua_pushstring(L, "meta");
		lua_rawseti(L, -2, fl_cnt++);
	}
	if (w->flags & RSPAMD_STAT_TOKEN_FLAG_STOP_WORD) {
		lua_pushstring(L, "stop_word");
		lua_rawseti(L, -2, fl_cnt++);
	}
	if (w->flags & RSPAMD_STAT_TOKEN_FLAG_INVISIBLE_SPACES) {
		lua_pushstring(L, "invisible_spaces");
		lua_rawseti(L, -2, fl_cnt++);
	}
	if (w->flags & RSPAMD_STAT_TOKEN_FLAG_STEMMED) {
		lua_pushstring(L, "stemmed");
		lua_rawseti(L, -2, fl_cnt++);
	}

	lua_rawseti(L, -2, 4);
}

gint rspamd_lua_push_words(lua_State *L, GArray *words,
						   enum rspamd_lua_words_type how)
{
	rspamd_stat_token_t *w;
	guint i, cnt;

	lua_createtable(L, words->len, 0);

	for (i = 0, cnt = 1; i < words->len; i++) {
		w = &g_array_index(words, rspamd_stat_token_t, i);

		switch (how) {
		case RSPAMD_LUA_WORDS_STEM:
			if (w->stemmed.len > 0) {
				lua_pushlstring(L, w->stemmed.begin, w->stemmed.len);
				lua_rawseti(L, -2, cnt++);
			}
			break;
		case RSPAMD_LUA_WORDS_NORM:
			if (w->normalized.len > 0) {
				lua_pushlstring(L, w->normalized.begin, w->normalized.len);
				lua_rawseti(L, -2, cnt++);
			}
			break;
		case RSPAMD_LUA_WORDS_RAW:
			if (w->original.len > 0) {
				lua_pushlstring(L, w->original.begin, w->original.len);
				lua_rawseti(L, -2, cnt++);
			}
			break;
		case RSPAMD_LUA_WORDS_FULL:
			rspamd_lua_push_full_word(L, w);
			/* Push to the resulting vector */
			lua_rawseti(L, -2, cnt++);
			break;
		default:
			break;
		}
	}

	return 1;
}

gchar *
rspamd_lua_get_module_name(lua_State *L)
{
	lua_Debug d;
	gchar *p;
	gchar func_buf[128];

	if (lua_getstack(L, 1, &d) == 1) {
		(void) lua_getinfo(L, "Sl", &d);
		if ((p = strrchr(d.short_src, '/')) == NULL) {
			p = d.short_src;
		}
		else {
			p++;
		}

		if (strlen(p) > 20) {
			rspamd_snprintf(func_buf, sizeof(func_buf), "%10s...]:%d", p,
							d.currentline);
		}
		else {
			rspamd_snprintf(func_buf, sizeof(func_buf), "%s:%d", p,
							d.currentline);
		}

		return g_strdup(func_buf);
	}

	return NULL;
}

bool rspamd_lua_universal_pcall(lua_State *L, gint cbref, const gchar *strloc,
								gint nret, const gchar *args, GError **err, ...)
{
	va_list ap;
	const gchar *argp = args, *classname;
	gint err_idx, nargs = 0;
	gpointer *cls_ptr;
	gsize sz;

	/* Error function */
	lua_pushcfunction(L, &rspamd_lua_traceback);
	err_idx = lua_gettop(L);

	va_start(ap, err);
	/* Called function */
	if (cbref > 0) {
		lua_rawgeti(L, LUA_REGISTRYINDEX, cbref);
	}
	else {
		/* Assume that function was on top of the stack */
		lua_pushvalue(L, err_idx - 1);
	}
	/*
	 * Possible arguments
	 * - i - lua_integer, argument - gint64
	 * - n - lua_number, argument - gdouble
	 * - s - lua_string, argument - const gchar * (zero terminated)
	 * - l - lua_lstring, argument - (size_t + const gchar *) pair
	 * - u - lua_userdata, argument - (const char * + void *) - classname + pointer
	 * - b - lua_boolean, argument - gboolean (not bool due to varargs promotion)
	 * - f - lua_function, argument - int - position of the function on stack (not lua_registry)
	 * - t - lua_text, argument - int - position of the lua_text on stack (not lua_registry)
	 */
	while (*argp) {
		switch (*argp) {
		case 'i':
			lua_pushinteger(L, va_arg(ap, gint64));
			nargs++;
			break;
		case 'n':
			lua_pushnumber(L, va_arg(ap, gdouble));
			nargs++;
			break;
		case 's':
			lua_pushstring(L, va_arg(ap, const gchar *));
			nargs++;
			break;
		case 'l':
			sz = va_arg(ap, gsize);
			lua_pushlstring(L, va_arg(ap, const gchar *), sz);
			nargs++;
			break;
		case 'b':
			lua_pushboolean(L, va_arg(ap, gboolean));
			nargs++;
			break;
		case 'u':
			classname = va_arg(ap, const gchar *);
			cls_ptr = (gpointer *) lua_newuserdata(L, sizeof(gpointer));
			*cls_ptr = va_arg(ap, gpointer);
			rspamd_lua_setclass(L, classname, -1);
			nargs++;
			break;
		case 'f':
		case 't':
			lua_pushvalue(L, va_arg(ap, gint));
			nargs++;
			break;
		default:
			lua_settop(L, err_idx - 1);
			g_set_error(err, lua_error_quark(), EINVAL,
						"invalid argument character: %c at %s",
						*argp, argp);
			va_end(ap);

			return false;
		}

		argp++;
	}

	if (lua_pcall(L, nargs, nret, err_idx) != 0) {
		g_set_error(err, lua_error_quark(), EBADF,
					"error when calling lua function from %s: %s",
					strloc, lua_tostring(L, -1));
		lua_settop(L, err_idx - 1);
		va_end(ap);

		return false;
	}

	lua_remove(L, err_idx);
	va_end(ap);

	return true;
}

#if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM <= 502
gint rspamd_lua_geti(lua_State *L, int pos, int i)
{
	pos = lua_absindex(L, pos);
	lua_pushinteger(L, i);
	lua_gettable(L, pos);

	return lua_type(L, -1);
}
#endif