/*
** Copyright (C) 2001-2025 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/

#include "zbxlog.h"
#include "zbxgetopt.h"
#include "zbxembed.h"
#include "zbxmutexs.h"
#include "zbxstr.h"
#include "zbxnix.h"
#include "zbxbincommon.h"

ZBX_GET_CONFIG_VAR2(const char *, const char *, zbx_progname, NULL)
static const char	title_message[] = "zabbix_js";
static const char	syslog_app_name[] = "zabbix_js";
static const char	*usage_message[] = {
	"-s script-file", "-p input-param", "[-l log-level]", "[-t timeout]", NULL,
	"-s script-file", "-i input-file", "[-l log-level]", "[-t timeout]", NULL,
	"-h", NULL,
	"-V", NULL,
	NULL	/* end of text */
};

#define ZBX_SERVICE_NAME_LEN	64
char	zabbix_event_source[ZBX_SERVICE_NAME_LEN] = APPLICATION_NAME;
#undef ZBX_SERVICE_NAME_LEN

#define JS_TIMEOUT_MIN		1
#define JS_TIMEOUT_MAX		60
#define JS_TIMEOUT_DEF		ZBX_ES_TIMEOUT
#define JS_TIMEOUT_MIN_STR	ZBX_STR(JS_TIMEOUT_MIN)
#define JS_TIMEOUT_MAX_STR	ZBX_STR(JS_TIMEOUT_MAX)
#define JS_TIMEOUT_DEF_STR	ZBX_STR(JS_TIMEOUT_DEF)

static const char	*help_message[] = {
	"Execute script using Zabbix embedded scripting engine.",
	"",
	"General options:",
	"  -s,--script script-file      Specify the filename of script to execute. Specify - for",
	"                               standard input.",
	"  -i,--input input-file        Specify input parameter file name. Specify - for",
	"                               standard input.",
	"  -p,--param input-param       Specify input parameter",
	"  -w,--webdriver url           Specify webdriver URL",
	"  -l,--loglevel log-level      Specify log level",
	"  -t --timeout timeout         Specify the timeout in seconds. Valid range: " JS_TIMEOUT_MIN_STR "-"
			JS_TIMEOUT_MAX_STR " seconds",
	"                               (default: " JS_TIMEOUT_DEF_STR " seconds)",
	"  -h --help                    Display this help message",
	"  -V --version                 Display version number",
	"",
	"Example:",
	"  zabbix_js -s script-file.js -p example",
	NULL	/* end of text */
};

/* long options */
struct zbx_option	longopts[] =
{
	{"script",			1,	NULL,	's'},
	{"input",			1,	NULL,	'i'},
	{"param",			1,	NULL,	'p'},
	{"webdriver",			1,	NULL,	'w'},
	{"loglevel",			1,	NULL,	'l'},
	{"timeout",			1,	NULL,	't'},
	{"help",			0,	NULL,	'h'},
	{"version",			0,	NULL,	'V'},
	{0}
};

/* short options */
static char	shortopts[] = "s:i:p:hVl:t:w:";

/* end of COMMAND LINE OPTIONS */

static char	*read_file(const char *filename, char **error)
{
	char	buffer[4096];
	int	fd;
	ssize_t	n;
	char	*data = NULL;
	size_t	data_alloc = 0, data_offset = 0;

	if (0 != strcmp(filename, "-"))
	{
		if (-1 == (fd = open(filename, O_RDONLY)))
		{
			*error = zbx_strdup(NULL, zbx_strerror(errno));
			return NULL;
		}
	}
	else
		fd = STDIN_FILENO;

	while (0 != (n = read(fd, buffer, sizeof(buffer))))
	{
		if (-1 == n)
		{
			if (fd != STDIN_FILENO)
				close(fd);
			zbx_free(data);
			*error = zbx_strdup(NULL, zbx_strerror(errno));
			return NULL;
		}
		zbx_strncpy_alloc(&data, &data_alloc, &data_offset, buffer, (size_t)n);
	}

	if (fd != STDIN_FILENO)
		close(fd);

	return data;
}

/******************************************************************************
 *                                                                            *
 * Purpose: executes command (script in form of a text)                       *
 *                                                                            *
 * Parameters: command          - [IN] command in form of a text              *
 *             param            - [IN] script parameters                      *
 *             timeout          - [IN] timeout for the execution (seconds)    *
 *             config_source_ip - [IN]                                        *
 *             webdriver        - [IN] webdriver URL (optional)
 *             result           - [OUT] result of an execution                *
 *             error            - [OUT] error message                         *
 *             max_error_len    - [IN] maximum length of an error             *
 *                                                                            *
 * Return value: SUCCEED                                                      *
 *               FAIL                                                         *
 *                                                                            *
 ******************************************************************************/
static int	execute_script(const char *command, const char *param, int timeout, const char *config_source_ip,
		const char *webdriver, char **result, char *error, size_t max_error_len)
{
	int		size, ret = SUCCEED;
	char		*code = NULL, *errmsg = NULL;
	zbx_es_t	es;

	zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __func__);

	zbx_es_init(&es);
	if (FAIL == zbx_es_init_env(&es, config_source_ip, &errmsg))
	{
		zbx_snprintf(error, max_error_len, "cannot initialize scripting environment: %s", errmsg);
		zbx_free(errmsg);
		ret = FAIL;

		goto failure;
	}

	if (NULL != webdriver && FAIL == zbx_es_init_browser_env(&es, webdriver, &errmsg))
	{
		zbx_snprintf(error, max_error_len, "cannot initialize Browser object: %s", errmsg);
		zbx_free(errmsg);
		ret = FAIL;

		goto failure;
	}

	if (FAIL == zbx_es_compile(&es, command, &code, &size, &errmsg))
	{
		zbx_snprintf(error, max_error_len, "cannot compile script: %s", errmsg);
		zbx_free(errmsg);
		ret = FAIL;

		goto out;
	}

	if (0 != timeout)
		zbx_es_set_timeout(&es, timeout);

	if (FAIL == zbx_es_execute(&es, NULL, code, size, param, result, &errmsg))
	{
		zbx_snprintf(error, max_error_len, "cannot execute script: %s", errmsg);
		zbx_free(errmsg);
		ret = FAIL;

		goto out;
	}
out:
	if (FAIL == zbx_es_destroy_env(&es, &errmsg))
	{
		zabbix_log(LOG_LEVEL_WARNING, "cannot destroy embedded scripting engine environment: %s", errmsg);
		zbx_free(errmsg);
	}

	zbx_free(code);
	zbx_free(errmsg);
failure:
	zabbix_log(LOG_LEVEL_DEBUG, "End of %s():%s", __func__, zbx_result_string(ret));

	return ret;
}

int	main(int argc, char **argv)
{
	int			ret = FAIL, loglevel = LOG_LEVEL_WARNING, timeout = 0;
	char			*script_file = NULL, *input_file = NULL, *param = NULL, ch, *script = NULL,
				*error = NULL, *result = NULL, script_error[MAX_STRING_LEN], *webdriver = NULL;
	zbx_config_log_t	log_file_cfg = {NULL, NULL, ZBX_LOG_TYPE_UNDEFINED, 0};

	/* see description of 'optarg' in 'man 3 getopt' */
	char			*zbx_optarg = NULL;

	/* see description of 'optind' in 'man 3 getopt' */
	int			zbx_optind = 0;

	const char		*config_source_ip = NULL;

	zbx_progname = get_program_name(argv[0]);

	zbx_init_library_common(zbx_log_impl, get_zbx_progname, zbx_backtrace);
#ifndef _WINDOWS
	zbx_init_library_nix(get_zbx_progname, NULL);
#endif
	/* parse the command-line */
	while ((char)EOF != (ch = (char)zbx_getopt_long(argc, argv, shortopts, longopts, NULL, &zbx_optarg,
			&zbx_optind)))
	{
		switch (ch)
		{
			case 's':
				if (NULL == script_file)
					script_file = zbx_strdup(NULL, zbx_optarg);
				break;
			case 'i':
				if (NULL == input_file)
					input_file = zbx_strdup(NULL, zbx_optarg);
				break;
			case 'p':
				if (NULL == param)
					param = zbx_strdup(NULL, zbx_optarg);
				break;
			case 'w':
				if (NULL == webdriver)
					webdriver = zbx_strdup(NULL, zbx_optarg);
				break;
			case 'l':
				loglevel = atoi(zbx_optarg);
				break;
			case 't':
				if (FAIL == zbx_is_uint_n_range(zbx_optarg, ZBX_MAX_UINT64_LEN, &timeout,
						sizeof(timeout), JS_TIMEOUT_MIN, JS_TIMEOUT_MAX))
				{
					zbx_error("Invalid timeout, valid range [" JS_TIMEOUT_MIN_STR ":"
							JS_TIMEOUT_MAX_STR "] seconds");
					exit(EXIT_FAILURE);
				}

				break;
			case 'h':
				zbx_print_help(zbx_progname, help_message, usage_message, NULL);
				ret = SUCCEED;
				goto clean;
			case 'V':
				zbx_print_version(title_message);
				ret = SUCCEED;
				goto clean;
			default:
				zbx_print_usage(zbx_progname, usage_message);
				goto clean;
		}
	}

	if (SUCCEED != zbx_locks_create(&error))
	{
		zbx_error("cannot create locks: %s", error);
		goto clean;
	}

	if (SUCCEED != zbx_open_log(&log_file_cfg, loglevel, syslog_app_name, zabbix_event_source, &error))
	{
		zbx_error("cannot open log: %s", error);
		goto clean;
	}

	if (NULL == script_file || (NULL == input_file && NULL == param))
	{
		zbx_print_usage(zbx_progname, usage_message);
		goto close;
	}

	if (NULL != input_file && NULL != param)
	{
		zbx_error("input and script options are mutually exclusive");
		goto close;
	}

	if (0 == strcmp(script_file, "-") && NULL != input_file && 0 == strcmp(input_file, "-"))
	{
		zbx_error("cannot read script and input parameters from standard input at the same time");
		goto close;
	}

	if (NULL == (script = read_file(script_file, &error)))
	{
		if (NULL != error)
			zbx_error("cannot read script file: %s", error);
		else
			zbx_error("cannot use empty script file");

		goto close;
	}

	if (NULL != input_file)
	{
		if (NULL == (param = read_file(input_file, &error)))
		{
			if (NULL != error)
				zbx_error("cannot read input file: %s", error);
			else
				zbx_error("cannot use empty input file");

			goto close;
		}
	}

	if (FAIL == execute_script(script, param, timeout, config_source_ip, webdriver, &result, script_error,
			sizeof(script_error)))
	{
		zbx_error("error executing script:\n%s", script_error);
		goto close;
	}
	ret = SUCCEED;
	printf("\n%s\n", result);
close:
	zbx_close_log();
#ifndef _WINDOWS
	zbx_locks_destroy();
#endif
clean:
	zbx_free(result);
	zbx_free(error);
	zbx_free(script);
	zbx_free(script_file);
	zbx_free(input_file);
	zbx_free(param);
	zbx_free(webdriver);

	return SUCCEED == ret ? EXIT_SUCCESS : EXIT_FAILURE;
}