/*
 * 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 "config.h"
#include "rspamadm.h"
#include "libserver/http/http_connection.h"
#include "libserver/http/http_private.h"
#include "libserver/http/http_router.h"
#include "printf.h"
#include "lua/lua_common.h"
#include "lua/lua_thread_pool.h"
#include "message.h"
#include "unix-std.h"
#ifdef WITH_LUA_REPL
#include "replxx.h"
#endif
#include "worker_util.h"
#ifdef WITH_LUAJIT
#include <luajit.h>
#endif

static gchar **paths = NULL;
static gchar **scripts = NULL;
static gchar **lua_args = NULL;
static gchar *histfile = NULL;
static guint max_history = 2000;
static gchar *serve = NULL;
static gchar *exec_line = NULL;
static gint batch = -1;
extern struct rspamd_async_session *rspamadm_session;

static const char *default_history_file = ".rspamd_repl.hist";

#ifdef WITH_LUA_REPL
static Replxx *rx_instance = NULL;
#endif

#ifdef WITH_LUAJIT
#define MAIN_PROMPT LUAJIT_VERSION "> "
#else
#define MAIN_PROMPT LUA_VERSION "> "
#endif
#define MULTILINE_PROMPT "... "

static void rspamadm_lua(gint argc, gchar **argv,
						 const struct rspamadm_command *cmd);
static const char *rspamadm_lua_help(gboolean full_help,
									 const struct rspamadm_command *cmd);

struct rspamadm_command lua_command = {
	.name = "lua",
	.flags = 0,
	.help = rspamadm_lua_help,
	.run = rspamadm_lua,
	.lua_subrs = NULL,
};

/*
 * Dot commands
 */
typedef void (*rspamadm_lua_dot_handler)(lua_State *L, gint argc, gchar **argv);
struct rspamadm_lua_dot_command {
	const gchar *name;
	const gchar *description;
	rspamadm_lua_dot_handler handler;
};

static void rspamadm_lua_help_handler(lua_State *L, gint argc, gchar **argv);
static void rspamadm_lua_load_handler(lua_State *L, gint argc, gchar **argv);
static void rspamadm_lua_exec_handler(lua_State *L, gint argc, gchar **argv);
static void rspamadm_lua_message_handler(lua_State *L, gint argc, gchar **argv);

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

static struct rspamadm_lua_dot_command cmds[] = {
	{.name = "help",
	 .description = "shows help for commands",
	 .handler = rspamadm_lua_help_handler},
	{.name = "load",
	 .description = "load lua file",
	 .handler = rspamadm_lua_load_handler},
	{.name = "exec",
	 .description = "exec lua file",
	 .handler = rspamadm_lua_exec_handler},
	{.name = "message",
	 .description = "scans message using specified callback: .message <callback_name> <file>...",
	 .handler = rspamadm_lua_message_handler},
};

static GHashTable *cmds_hash = NULL;

static GOptionEntry entries[] = {
	{"script", 's', 0, G_OPTION_ARG_STRING_ARRAY, &scripts,
	 "Load specified scripts", NULL},
	{"path", 'P', 0, G_OPTION_ARG_STRING_ARRAY, &paths,
	 "Add specified paths to lua paths", NULL},
	{"history-file", 'H', 0, G_OPTION_ARG_FILENAME, &histfile,
	 "Load history from the specified file", NULL},
	{"max-history", 'm', 0, G_OPTION_ARG_INT, &max_history,
	 "Store this number of history entries", NULL},
	{"serve", 'S', 0, G_OPTION_ARG_STRING, &serve,
	 "Serve http lua server", NULL},
	{"batch", 'b', 0, G_OPTION_ARG_NONE, &batch,
	 "Batch execution mode", NULL},
	{"exec", 'e', 0, G_OPTION_ARG_STRING, &exec_line,
	 "Execute specified script", NULL},
	{"args", 'a', 0, G_OPTION_ARG_STRING_ARRAY, &lua_args,
	 "Arguments to pass to Lua", NULL},
	{NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};

static const char *
rspamadm_lua_help(gboolean full_help, const struct rspamadm_command *cmd)
{
	const char *help_str;

	if (full_help) {
		help_str = "Run lua read/execute/print loop\n\n"
				   "Usage: rspamadm lua [-P paths] [-s scripts]\n"
				   "Where options are:\n\n"
				   "-P: add additional lua paths (may be repeated)\n"
				   "-p: split input to lines and feed each line to the script\n"
				   "-s: load scripts on start from specified files (may be repeated)\n"
				   "-S: listen on a specified address as HTTP server\n"
				   "-a: pass argument to lua (may be repeated)\n"
				   "-e: execute script specified in command line"
				   "--help: shows available options and commands";
	}
	else {
		help_str = "Run LUA interpreter";
	}

	return help_str;
}

static void
rspamadm_lua_add_path(lua_State *L, const gchar *path)
{
	const gchar *old_path;
	gsize len;
	GString *new_path;

	lua_getglobal(L, "package");
	lua_getfield(L, -1, "path");
	old_path = luaL_checklstring(L, -1, &len);

	new_path = g_string_sized_new(len + strlen(path) + sizeof("/?.lua"));

	if (strstr(path, "?.lua") == NULL) {
		rspamd_printf_gstring(new_path, "%s/?.lua;%s", path, old_path);
	}
	else {
		rspamd_printf_gstring(new_path, "%s;%s", path, old_path);
	}

	lua_pushlstring(L, new_path->str, new_path->len);
	lua_setfield(L, -2, "path");
	lua_settop(L, 0);
	g_string_free(new_path, TRUE);
}


static void
lua_thread_finish_cb(struct thread_entry *thread, int ret)
{
	struct lua_call_data *cd = thread->cd;

	cd->ret = ret;
}

static void
lua_thread_error_cb(struct thread_entry *thread, int ret, const char *msg)
{
	struct lua_call_data *cd = thread->cd;

	rspamd_fprintf(stderr, "call failed: %s\n", msg);

	cd->ret = ret;
}

static void
lua_thread_str_error_cb(struct thread_entry *thread, int ret, const char *msg)
{
	struct lua_call_data *cd = thread->cd;
	const char *what = cd->ud;

	rspamd_fprintf(stderr, "call to %s failed: %s\n", what, msg);

	cd->ret = ret;
}

static gboolean
rspamadm_lua_load_script(lua_State *L, const gchar *path)
{
	struct thread_entry *thread = lua_thread_pool_get_for_config(rspamd_main->cfg);
	L = thread->lua_state;

	if (luaL_loadfile(L, path) != 0) {
		rspamd_fprintf(stderr, "cannot load script %s: %s\n",
					   path, lua_tostring(L, -1));
		lua_settop(L, 0);

		return FALSE;
	}

	if (lua_repl_thread_call(thread, 0, (void *) path, lua_thread_str_error_cb) != 0) {
		return FALSE;
	}

	lua_settop(L, 0);

	return TRUE;
}

static void
rspamadm_exec_input(lua_State *L, const gchar *input)
{
	GString *tb;
	gint i, cbref;
	int top = 0;
	gchar outbuf[8192];
	struct lua_logger_trace tr;

	struct thread_entry *thread = lua_thread_pool_get_for_config(rspamd_main->cfg);
	L = thread->lua_state;

	/* First try return + input */
	tb = g_string_sized_new(strlen(input) + sizeof("return "));
	rspamd_printf_gstring(tb, "return %s", input);

	int r = luaL_loadstring(L, tb->str);
	if (r != 0) {
		/* Reset stack */
		lua_settop(L, 0);
		/* Try with no return */
		if (luaL_loadstring(L, input) != 0) {
			rspamd_fprintf(stderr, "cannot load string %s\n",
						   input);
			g_string_free(tb, TRUE);
			lua_settop(L, 0);

			lua_thread_pool_return(rspamd_main->cfg->lua_thread_pool, thread);
			return;
		}
	}

	g_string_free(tb, TRUE);


	top = lua_gettop(L);

	if (lua_repl_thread_call(thread, 0, NULL, NULL) == 0) {
		/* Print output */
		for (i = top; i <= lua_gettop(L); i++) {
			if (lua_isfunction(L, i)) {
				lua_pushvalue(L, i);
				cbref = luaL_ref(L, LUA_REGISTRYINDEX);

				rspamd_printf("local function: %d\n", cbref);
			}
			else {
				memset(&tr, 0, sizeof(tr));
				lua_logger_out_type(L, i, outbuf, sizeof(outbuf) - 1, &tr,
									LUA_ESCAPE_UNPRINTABLE);
				rspamd_printf("%s\n", outbuf);
			}
		}
	}
}

static void
wait_session_events(void)
{
	/* XXX: it's probably worth to add timeout here - not to wait forever */
	while (rspamd_session_events_pending(rspamadm_session) > 0) {
		ev_loop(rspamd_main->event_loop, EVRUN_ONCE);
	}

	msg_debug("finished events waiting, terminating session");
}

gint lua_repl_thread_call(struct thread_entry *thread, gint narg, gpointer ud, lua_thread_error_t error_func)
{
	int ret;
	struct lua_call_data *cd = g_new0(struct lua_call_data, 1);
	cd->top = lua_gettop(thread->lua_state);
	cd->ud = ud;

	thread->finish_callback = lua_thread_finish_cb;
	if (error_func) {
		thread->error_callback = error_func;
	}
	else {
		thread->error_callback = lua_thread_error_cb;
	}
	thread->cd = cd;

	lua_thread_call(thread, narg);

	wait_session_events();

	ret = cd->ret;

	g_free(cd);

	return ret;
}

static void
rspamadm_lua_help_handler(lua_State *L, gint argc, gchar **argv)
{
	guint i;
	struct rspamadm_lua_dot_command *cmd;

	if (argv[1] == NULL) {
		/* Print all commands */
		for (i = 0; i < G_N_ELEMENTS(cmds); i++) {
			rspamd_printf("%s: %s\n", cmds[i].name, cmds[i].description);
		}

		rspamd_printf("{{: start multiline input\n");
		rspamd_printf("}}: end multiline input\n");
	}
	else {
		for (i = 1; argv[i] != NULL; i++) {
			cmd = g_hash_table_lookup(cmds_hash, argv[i]);

			if (cmd) {
				rspamd_printf("%s: %s\n", cmds->name, cmds->description);
			}
			else {
				rspamd_printf("%s: no such command\n", argv[i]);
			}
		}
	}
}

static void
rspamadm_lua_load_handler(lua_State *L, gint argc, gchar **argv)
{
	guint i;
	gboolean ret;

	for (i = 1; argv[i] != NULL; i++) {
		ret = rspamadm_lua_load_script(L, argv[i]);
		rspamd_printf("%s: %sloaded\n", argv[i], ret ? "" : "NOT ");
	}
}

static void
rspamadm_lua_exec_handler(lua_State *L, gint argc, gchar **argv)
{
	gint i;

	struct thread_entry *thread = lua_thread_pool_get_for_config(rspamd_main->cfg);
	L = thread->lua_state;

	for (i = 1; argv[i] != NULL; i++) {

		if (luaL_loadfile(L, argv[i]) != 0) {
			rspamd_fprintf(stderr, "cannot load script %s: %s\n",
						   argv[i], lua_tostring(L, -1));
			lua_settop(L, 0);

			return;
		}

		if (lua_repl_thread_call(thread, 0, argv[i], lua_thread_str_error_cb) != 0) {
			return;
		}
	}
}

static void
rspamadm_lua_message_handler(lua_State *L, gint argc, gchar **argv)
{
	gulong cbref;
	gint old_top, func_idx, i, j;
	struct rspamd_task *task, **ptask;
	gpointer map;
	gsize len;
	gchar outbuf[8192];
	struct lua_logger_trace tr;

	if (argv[1] == NULL) {
		rspamd_printf("no callback is specified\n");
		return;
	}

	for (i = 2; argv[i] != NULL; i++) {
		struct thread_entry *thread = lua_thread_pool_get_for_config(rspamd_main->cfg);
		L = thread->lua_state;

		if (rspamd_strtoul(argv[1], strlen(argv[1]), &cbref)) {
			lua_rawgeti(L, LUA_REGISTRYINDEX, cbref);
		}
		else {
			lua_getglobal(L, argv[1]);
		}

		if (lua_type(L, -1) != LUA_TFUNCTION) {
			rspamd_printf("bad callback type: %s\n", lua_typename(L, lua_type(L, -1)));
			lua_thread_pool_return(rspamd_main->cfg->lua_thread_pool, thread);
			return;
		}

		/* Save index to reuse */
		func_idx = lua_gettop(L);

		map = rspamd_file_xmap(argv[i], PROT_READ, &len, TRUE);

		if (map == NULL) {
			rspamd_printf("cannot open %s: %s\n", argv[i], strerror(errno));
		}
		else {
			task = rspamd_task_new(NULL, rspamd_main->cfg, NULL, NULL, NULL, FALSE);

			if (!rspamd_task_load_message(task, NULL, map, len)) {
				rspamd_printf("cannot load %s\n", argv[i]);
				rspamd_task_free(task);
				munmap(map, len);
				continue;
			}

			if (!rspamd_message_parse(task)) {
				rspamd_printf("cannot parse %s: %e\n", argv[i], task->err);
				rspamd_task_free(task);
				munmap(map, len);
				continue;
			}

			rspamd_message_process(task);
			old_top = lua_gettop(L);

			lua_pushvalue(L, func_idx);
			ptask = lua_newuserdata(L, sizeof(*ptask));
			*ptask = task;
			rspamd_lua_setclass(L, "rspamd{task}", -1);


			if (lua_repl_thread_call(thread, 1, argv[i], lua_thread_str_error_cb) == 0) {
				rspamd_printf("lua callback for %s returned:\n", argv[i]);

				for (j = old_top + 1; j <= lua_gettop(L); j++) {
					memset(&tr, 0, sizeof(tr));
					lua_logger_out_type(L, j, outbuf, sizeof(outbuf), &tr,
										LUA_ESCAPE_UNPRINTABLE);
					rspamd_printf("%s\n", outbuf);
				}
			}

			rspamd_task_free(task);
			munmap(map, len);
			/* Pop all but the original function */
			lua_settop(L, func_idx);
		}
	}

	lua_settop(L, 0);
}


static gboolean
rspamadm_lua_try_dot_command(lua_State *L, const gchar *input)
{
	struct rspamadm_lua_dot_command *cmd;
	gchar **argv;

	argv = g_strsplit_set(input + 1, " ", -1);

	if (argv == NULL || argv[0] == NULL) {
		if (argv) {
			g_strfreev(argv);
		}

		return FALSE;
	}

	cmd = g_hash_table_lookup(cmds_hash, argv[0]);

	if (cmd) {
		cmd->handler(L, g_strv_length(argv), argv);
		g_strfreev(argv);

		return TRUE;
	}

	g_strfreev(argv);

	return FALSE;
}

#ifdef WITH_LUA_REPL
static gint lex_ref_idx = -1;

static void
lua_syntax_highlighter(const char *str, ReplxxColor *colours, int size, void *ud)
{
	lua_State *L = (lua_State *) ud;

	if (lex_ref_idx == -1) {
		if (!rspamd_lua_require_function(L, "lua_lexer", "lex_to_table")) {
			fprintf(stderr, "cannot require lua_lexer!\n");

			exit(EXIT_FAILURE);
		}

		lex_ref_idx = luaL_ref(L, LUA_REGISTRYINDEX);
	}

	lua_rawgeti(L, LUA_REGISTRYINDEX, lex_ref_idx);
	lua_pushstring(L, str);

	if (lua_pcall(L, 1, 1, 0) != 0) {
		fprintf(stderr, "cannot lex a string!\n");
	}
	else {
		/* Process what we have after lexing */
		gsize nelts = rspamd_lua_table_size(L, -1);

		for (gsize i = 0; i < nelts; i++) {
			/*
			 * Indexes in the table:
			 * 1 - type of element (string)
			 * 2 - text (string)
			 * 3 - line num (int), always 1...
			 * 4 - column num (must be less than size)
			 */
			const gchar *what;
			gsize column, tlen, cur_top, elt_pos;
			ReplxxColor elt_color = REPLXX_COLOR_DEFAULT;

			cur_top = lua_gettop(L);
			lua_rawgeti(L, -1, i + 1);
			elt_pos = lua_gettop(L);
			lua_rawgeti(L, elt_pos, 1);
			what = lua_tostring(L, -1);
			lua_rawgeti(L, elt_pos, 2);
			lua_tolstring(L, -1, &tlen);
			lua_rawgeti(L, elt_pos, 4);
			column = lua_tointeger(L, -1);

			g_assert(column > 0);
			column--; /* Start from 0 */

			if (column + tlen > size) {
				/* Likely utf8 case, too complicated to match */
				lua_settop(L, cur_top);
				continue;
			}

			/* Check what and adjust color */
			if (strcmp(what, "identifier") == 0) {
				elt_color = REPLXX_COLOR_NORMAL;
			}
			else if (strcmp(what, "number") == 0) {
				elt_color = REPLXX_COLOR_BLUE;
			}
			else if (strcmp(what, "string") == 0) {
				elt_color = REPLXX_COLOR_GREEN;
			}
			else if (strcmp(what, "keyword") == 0) {
				elt_color = REPLXX_COLOR_WHITE;
			}
			else if (strcmp(what, "constant") == 0) {
				elt_color = REPLXX_COLOR_WHITE;
			}
			else if (strcmp(what, "operator") == 0) {
				elt_color = REPLXX_COLOR_CYAN;
			}
			else if (strcmp(what, "comment") == 0) {
				elt_color = REPLXX_COLOR_BRIGHTGREEN;
			}
			else if (strcmp(what, "error") == 0) {
				elt_color = REPLXX_COLOR_ERROR;
			}

			for (gsize j = column; j < column + tlen; j++) {
				colours[j] = elt_color;
			}

			/* Restore stack */
			lua_settop(L, cur_top);
		}
	}

	lua_settop(L, 0);
}
#endif

static void
rspamadm_lua_run_repl(lua_State *L, bool is_batch)
{
	gchar *input;
#ifdef WITH_LUA_REPL
	gboolean is_multiline = FALSE;
	GString *tb = NULL;
	gsize i;
#else
	/* Always set is_batch */
	is_batch = TRUE;
#endif

	for (;;) {
		if (is_batch) {
			size_t linecap = 0;
			ssize_t linelen;

			linelen = getline(&input, &linecap, stdin);

			if (linelen > 0) {
				if (input[linelen - 1] == '\n') {
					input[linelen - 1] = '\0';
					linelen--;
				}

				if (linelen > 0) {
					if (input[0] == '.') {
						if (rspamadm_lua_try_dot_command(L, input)) {
							continue;
						}
					}

					rspamadm_exec_input(L, input);
				}
			}
			else {
				break;
			}

			lua_settop(L, 0);
		}
		else {
#ifdef WITH_LUA_REPL
			replxx_set_highlighter_callback(rx_instance, lua_syntax_highlighter,
											L);

			if (!is_multiline) {
				input = (gchar *) replxx_input(rx_instance, MAIN_PROMPT);

				if (input == NULL) {
					return;
				}

				if (input[0] == '.') {
					if (rspamadm_lua_try_dot_command(L, input)) {
						if (!is_batch) {
							replxx_history_add(rx_instance, input);
						}
						continue;
					}
				}

				if (strcmp(input, "{{") == 0) {
					is_multiline = TRUE;
					tb = g_string_sized_new(8192);
					continue;
				}

				rspamadm_exec_input(L, input);
				if (!is_batch) {
					replxx_history_add(rx_instance, input);
				}
				lua_settop(L, 0);
			}
			else {
				input = (gchar *) replxx_input(rx_instance, MULTILINE_PROMPT);

				if (input == NULL) {
					g_string_free(tb, TRUE);
					return;
				}

				if (strcmp(input, "}}") == 0) {
					is_multiline = FALSE;
					rspamadm_exec_input(L, tb->str);

					/* Replace \n with ' ' for sanity */
					for (i = 0; i < tb->len; i++) {
						if (tb->str[i] == '\n') {
							tb->str[i] = ' ';
						}
					}

					if (!is_batch) {
						replxx_history_add(rx_instance, tb->str);
					}
					g_string_free(tb, TRUE);
				}
				else {
					g_string_append(tb, input);
					g_string_append(tb, " \n");
				}
			}
		}
#endif
	}
}

struct rspamadm_lua_repl_context {
	struct rspamd_http_connection_router *rt;
	lua_State *L;
};

struct rspamadm_lua_repl_session {
	struct rspamd_http_connection_router *rt;
	rspamd_inet_addr_t *addr;
	struct rspamadm_lua_repl_context *ctx;
	gint sock;
};

static void
rspamadm_lua_accept_cb(EV_P_ ev_io *w, int revents)
{
	struct rspamadm_lua_repl_context *ctx =
		(struct rspamadm_lua_repl_context *) w->data;
	rspamd_inet_addr_t *addr = NULL;
	struct rspamadm_lua_repl_session *session;
	gint nfd;

	if ((nfd =
			 rspamd_accept_from_socket(w->fd, &addr, NULL, NULL)) == -1) {
		rspamd_fprintf(stderr, "accept failed: %s", strerror(errno));
		return;
	}
	/* Check for EAGAIN */
	if (nfd == 0) {
		rspamd_inet_address_free(addr);
		return;
	}

	session = g_malloc0(sizeof(*session));
	session->rt = ctx->rt;
	session->ctx = ctx;
	session->addr = addr;
	session->sock = nfd;

	rspamd_http_router_handle_socket(ctx->rt, nfd, session);
}

static void
rspamadm_lua_error_handler(struct rspamd_http_connection_entry *conn_ent,
						   GError *err)
{
	rspamd_fprintf(stderr, "http error occurred: %s\n", err->message);
}

static void
rspamadm_lua_finish_handler(struct rspamd_http_connection_entry *conn_ent)
{
	struct rspamadm_lua_repl_session *session = conn_ent->ud;

	g_free(session);
}

static void
lua_thread_http_error_cb(struct thread_entry *thread, int ret, const char *msg)
{
	struct lua_call_data *cd = thread->cd;
	struct rspamd_http_connection_entry *conn_ent = cd->ud;

	rspamd_controller_send_error(conn_ent, 500, "call failed: %s\n", msg);

	cd->ret = ret;
}


/*
 * Exec command handler:
 * request: /exec
 * body: lua script
 * reply: json {"status": "ok", "reply": {<lua json object>}}
 */
static int
rspamadm_lua_handle_exec(struct rspamd_http_connection_entry *conn_ent,
						 struct rspamd_http_message *msg)
{
	GString *tb;
	gint err_idx, i;
	lua_State *L;
	ucl_object_t *obj, *elt;
	const gchar *body;
	gsize body_len;
	struct thread_entry *thread = lua_thread_pool_get_for_config(rspamd_main->cfg);

	L = thread->lua_state;

	body = rspamd_http_message_get_body(msg, &body_len);

	if (body == NULL) {
		rspamd_controller_send_error(conn_ent, 400, "Empty lua script");

		return 0;
	}

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

	/* First try return + input */
	tb = g_string_sized_new(body_len + sizeof("return "));
	rspamd_printf_gstring(tb, "return %*s", (gint) body_len, body);

	if (luaL_loadstring(L, tb->str) != 0) {
		/* Reset stack */
		lua_settop(L, 0);
		lua_pushcfunction(L, &rspamd_lua_traceback);
		err_idx = lua_gettop(L);
		/* Try with no return */
		if (luaL_loadbuffer(L, body, body_len, "http input") != 0) {
			rspamd_controller_send_error(conn_ent, 400, "Invalid lua script");

			return 0;
		}
	}

	g_string_free(tb, TRUE);

	if (lua_repl_thread_call(thread, 0, conn_ent, lua_thread_http_error_cb) != 0) {
		return 0;
	}

	obj = ucl_object_typed_new(UCL_ARRAY);

	for (i = err_idx + 1; i <= lua_gettop(L); i++) {
		if (lua_isfunction(L, i)) {
			/* XXX: think about API */
		}
		else {
			elt = ucl_object_lua_import(L, i);

			if (elt) {
				ucl_array_append(obj, elt);
			}
		}
	}

	rspamd_controller_send_ucl(conn_ent, obj);
	ucl_object_unref(obj);
	lua_settop(L, 0);

	return 0;
}

static void
rspamadm_lua(gint argc, gchar **argv, const struct rspamadm_command *cmd)
{
	GOptionContext *context;
	GError *error = NULL;
	gchar **elt;
	guint i;
	lua_State *L = rspamd_main->cfg->lua_state;

	context = g_option_context_new("lua - run lua interpreter");
	g_option_context_set_summary(context,
								 "Summary:\n  Rspamd administration utility version " RVERSION
								 "\n  Release id: " RID);
	g_option_context_add_main_entries(context, entries, NULL);

	if (!g_option_context_parse(context, &argc, &argv, &error)) {
		fprintf(stderr, "option parsing failed: %s\n", error->message);
		g_error_free(error);
		g_option_context_free(context);
		exit(EXIT_FAILURE);
	}

	g_option_context_free(context);

	if (batch == -1) {
		if (isatty(STDIN_FILENO)) {
			batch = 0;
		}
		else {
			batch = 1;
		}
	}

	if (paths) {
		for (elt = paths; *elt != NULL; elt++) {
			rspamadm_lua_add_path(L, *elt);
		}
	}

	if (lua_args) {
		i = 1;

		lua_newtable(L);

		for (elt = lua_args; *elt != NULL; elt++) {
			lua_pushinteger(L, i);
			lua_pushstring(L, *elt);
			lua_settable(L, -3);
			i++;
		}

		lua_setglobal(L, "arg");
	}

	if (scripts) {
		for (elt = scripts; *elt != NULL; elt++) {
			if (!rspamadm_lua_load_script(L, *elt)) {
				exit(EXIT_FAILURE);
			}
		}
	}

	if (exec_line) {
		rspamadm_exec_input(L, exec_line);
	}

	if (serve) {
		/* HTTP Server mode */
		GPtrArray *addrs = NULL;
		gchar *name = NULL;
		struct ev_loop *ev_base;
		struct rspamd_http_connection_router *http;
		gint fd;
		struct rspamadm_lua_repl_context *ctx;

		if (rspamd_parse_host_port_priority(serve, &addrs, NULL, &name,
											10000, TRUE, NULL) == RSPAMD_PARSE_ADDR_FAIL) {
			fprintf(stderr, "cannot listen on %s", serve);
			exit(EXIT_FAILURE);
		}

		ev_base = rspamd_main->event_loop;
		ctx = g_malloc0(sizeof(*ctx));
		http = rspamd_http_router_new(rspamadm_lua_error_handler,
									  rspamadm_lua_finish_handler,
									  0.0,
									  NULL,
									  rspamd_main->http_ctx);
		ctx->L = L;
		ctx->rt = http;
		rspamd_http_router_add_path(http,
									"/exec",
									rspamadm_lua_handle_exec);

		for (i = 0; i < addrs->len; i++) {
			rspamd_inet_addr_t *addr = g_ptr_array_index(addrs, i);

			fd = rspamd_inet_address_listen(addr, SOCK_STREAM,
											RSPAMD_INET_ADDRESS_LISTEN_ASYNC, -1);

			if (fd != -1) {
				static ev_io ev;

				ev.data = ctx;
				ev_io_init(&ev, rspamadm_lua_accept_cb, fd, EV_READ);
				ev_io_start(ev_base, &ev);
				rspamd_printf("listen on %s\n",
							  rspamd_inet_address_to_string_pretty(addr));
			}
		}

		ev_loop(ev_base, 0);

		exit(EXIT_SUCCESS);
	}

	if (histfile == NULL) {
		const gchar *homedir;
		GString *hist_path;

		homedir = getenv("HOME");

		if (homedir) {
			hist_path = g_string_sized_new(strlen(homedir) +
										   strlen(default_history_file) + 1);
			rspamd_printf_gstring(hist_path, "%s/%s", homedir,
								  default_history_file);
		}
		else {
			hist_path = g_string_sized_new(strlen(default_history_file) + 2);
			rspamd_printf_gstring(hist_path, "./%s", default_history_file);
		}

		histfile = hist_path->str;
		g_string_free(hist_path, FALSE);
	}

	if (argc > 1) {
		for (i = 1; i < argc; i++) {
			if (!rspamadm_lua_load_script(L, argv[i])) {
				exit(EXIT_FAILURE);
			}
		}

		exit(EXIT_SUCCESS);
	}

	/* Init dot commands */
	cmds_hash = g_hash_table_new(rspamd_strcase_hash, rspamd_strcase_equal);

	for (i = 0; i < G_N_ELEMENTS(cmds); i++) {
		g_hash_table_insert(cmds_hash, (gpointer) cmds[i].name, &cmds[i]);
	}

	if (!batch) {
#ifdef WITH_LUA_REPL
		rx_instance = replxx_init();
		replxx_set_max_history_size(rx_instance, max_history);
		replxx_history_load(rx_instance, histfile);
#endif
		rspamadm_lua_run_repl(L, false);
#ifdef WITH_LUA_REPL
		replxx_history_save(rx_instance, histfile);
		replxx_end(rx_instance);
#endif
	}
	else {
		rspamadm_lua_run_repl(L, true);
	}
}
