/*
** Copyright (C) 2001-2026 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 "zbxpinger.h"

#include "zbxlog.h"
#include "zbxcacheconfig.h"
#include "zbxicmpping.h"
#include "zbxnix.h"
#include "zbxself.h"
#include "zbxtime.h"
#include "zbxnum.h"
#include "zbxsysinfo.h"
#include "zbx_item_constants.h"
#include "zbx_host_constants.h"
#include "zbxpreproc.h"
#include "zbxdbhigh.h"
#include "zbxthreads.h"
#include "zbxtimekeeper.h"
#include "zbxalgo.h"
#include "zbxexpr.h"

typedef struct
{
	zbx_uint64_t		itemid;
	icmpping_t		icmpping;
	icmppingsec_type_t	type;
	char			*addr;
}
zbx_pinger_item_t;

ZBX_VECTOR_DECL(pinger_item, zbx_pinger_item_t)
ZBX_VECTOR_IMPL(pinger_item, zbx_pinger_item_t)

typedef struct
{
	unsigned char			allow_redirect;
	int				count;
	int				interval;
	int				size;
	int				timeout;
	int				retries;
	double				backoff;

	zbx_vector_pinger_item_t	items;
}
zbx_pinger_t;

static zbx_hash_t	pinger_hash(const void *d)
{
	const zbx_pinger_t	*pinger = (const zbx_pinger_t *)d;
	zbx_hash_t		hash = 0;

	hash = ZBX_DEFAULT_HASH_ALGO(&pinger->allow_redirect, sizeof(pinger->allow_redirect), hash);
	hash = ZBX_DEFAULT_HASH_ALGO(&pinger->count, sizeof(pinger->count), hash);
	hash = ZBX_DEFAULT_HASH_ALGO(&pinger->interval, sizeof(pinger->interval), hash);
	hash = ZBX_DEFAULT_HASH_ALGO(&pinger->size, sizeof(pinger->size), hash);
	hash = ZBX_DEFAULT_HASH_ALGO(&pinger->timeout, sizeof(pinger->timeout), hash);
	hash = ZBX_DEFAULT_HASH_ALGO(&pinger->retries, sizeof(pinger->retries), hash);
	hash = ZBX_DEFAULT_HASH_ALGO(&pinger->backoff, sizeof(pinger->backoff), hash);

	return hash;
}

static int	pinger_compare(const void *d1, const void *d2)
{
	const zbx_pinger_t	*pinger1 = (const zbx_pinger_t *)d1;
	const zbx_pinger_t	*pinger2 = (const zbx_pinger_t *)d2;

	ZBX_RETURN_IF_NOT_EQUAL(pinger1->allow_redirect, pinger2->allow_redirect);
	ZBX_RETURN_IF_NOT_EQUAL(pinger1->count, pinger2->count);
	ZBX_RETURN_IF_NOT_EQUAL(pinger1->interval, pinger2->interval);
	ZBX_RETURN_IF_NOT_EQUAL(pinger1->size, pinger2->size);
	ZBX_RETURN_IF_NOT_EQUAL(pinger1->timeout, pinger2->timeout);
	ZBX_RETURN_IF_NOT_EQUAL(pinger1->retries, pinger2->retries);
	ZBX_RETURN_IF_DBL_NOT_EQUAL(pinger1->backoff, pinger2->backoff);

	return 0;
}

static void	pinger_clear(void *d)
{
	zbx_pinger_t	*pinger = (zbx_pinger_t *)d;

	for (int i = 0; i < pinger->items.values_num; i++)
		zbx_free(pinger->items.values[i].addr);

	zbx_vector_pinger_item_destroy(&pinger->items);
}

/******************************************************************************
 *                                                                            *
 * Purpose: processes new item value                                          *
 *                                                                            *
 ******************************************************************************/
static void	process_value(zbx_uint64_t itemid, zbx_uint64_t *value_ui64, double *value_dbl,	zbx_timespec_t *ts,
		int ping_result, char *error)
{
	zbx_dc_item_t	item;
	int		errcode;

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

	zbx_dc_config_get_items_by_itemids(&item, &itemid, &errcode, 1);

	if (SUCCEED != errcode)
		goto clean;

	if (ITEM_STATUS_ACTIVE != item.status)
		goto clean;

	if (HOST_STATUS_MONITORED != item.host.status)
		goto clean;

	if (NOTSUPPORTED == ping_result)
	{
		item.state = ITEM_STATE_NOTSUPPORTED;
		zbx_preprocess_item_value(item.itemid, item.host.hostid, item.value_type, item.flags,
			item.preprocessing, NULL, ts, item.state, error);
	}
	else
	{
		AGENT_RESULT	value;

		zbx_init_agent_result(&value);

		if (NULL != value_ui64)
			SET_UI64_RESULT(&value, *value_ui64);
		else
			SET_DBL_RESULT(&value, *value_dbl);

		item.state = ITEM_STATE_NORMAL;
		zbx_preprocess_item_value(item.itemid, item.host.hostid, item.value_type, item.flags,
				item.preprocessing, &value, ts, item.state, NULL);

		zbx_free_agent_result(&value);
	}
clean:
	zbx_dc_requeue_items(&item.itemid, &ts->sec, &errcode, 1);

	zbx_dc_config_clean_items(&item, &errcode, 1);

	zabbix_log(LOG_LEVEL_DEBUG, "End of %s()", __func__);
}

/******************************************************************************
 *                                                                            *
 * Purpose: processes new item values                                         *
 *                                                                            *
 ******************************************************************************/
static void	process_values(const zbx_vector_pinger_item_t *items, zbx_fping_host_t *hosts, int hosts_count,
		zbx_timespec_t *ts, int ping_result, char *error)
{
	zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __func__);

	for (int h = 0; h < hosts_count; h++)
	{
		const zbx_fping_host_t	*host = &hosts[h];

		if (NOTSUPPORTED == ping_result)
		{
			zabbix_log(LOG_LEVEL_DEBUG, "host [%s] %s", host->addr, error);
		}
		else
		{
			zabbix_log(LOG_LEVEL_DEBUG, "host [%s] cnt=%d rcv=%d"
					" min=" ZBX_FS_DBL " max=" ZBX_FS_DBL " sum=" ZBX_FS_DBL,
					host->addr, host->cnt, host->rcv, host->min, host->max, host->sum);
		}

		for (int i = 0; i < items->values_num; i++)
		{
			zbx_uint64_t		value_uint64;
			double			value_dbl;
			const zbx_pinger_item_t	*item = &items->values[i];

			if (0 != strcmp(item->addr, host->addr))
				continue;

			if (NOTSUPPORTED == ping_result)
			{
				process_value(item->itemid, NULL, NULL, ts, NOTSUPPORTED, error);
				continue;
			}

			if (0 == host->cnt)
			{
				process_value(item->itemid, NULL, NULL, ts, NOTSUPPORTED,
						(char *)"Cannot send ICMP ping packets to this host.");
				continue;
			}

			switch (item->icmpping)
			{
				case ICMPPING:
				case ICMPPINGRETRY:
					value_uint64 = (0 != host->rcv ? 1 : 0);
					process_value(item->itemid, &value_uint64, NULL, ts, SUCCEED, NULL);
					break;
				case ICMPPINGSEC:
					switch (item->type)
					{
						case ICMPPINGSEC_MIN:
							value_dbl = host->min;
							break;
						case ICMPPINGSEC_MAX:
							value_dbl = host->max;
							break;
						case ICMPPINGSEC_AVG:
							value_dbl = (0 != host->rcv ? host->sum / host->rcv : 0);
							break;
					}

					if (0 < value_dbl && zbx_get_float_epsilon() > value_dbl)
						value_dbl = zbx_get_float_epsilon();

					process_value(item->itemid, NULL, &value_dbl, ts, SUCCEED, NULL);
					break;
				case ICMPPINGLOSS:
					value_dbl = (100 * (host->cnt - host->rcv)) / (double)host->cnt;
					process_value(item->itemid, NULL, &value_dbl, ts, SUCCEED, NULL);
					break;
			}
		}
	}

	zbx_preprocessor_flush();

	zabbix_log(LOG_LEVEL_DEBUG, "End of %s()", __func__);
}

static int	pinger_parse_key_params(const char *key, const char *host_addr, zbx_pinger_t *pinger,
		icmpping_t *icmpping, char **addr, icmppingsec_type_t *type, char **error)
{
/* defines for `fping' and `fping6' to successfully process pings */
#define MIN_COUNT	1
#define MAX_COUNT	10000
#define MIN_INTERVAL	20
#define MIN_SIZE	24
#define MAX_SIZE	65507
#define MIN_TIMEOUT	50
#define DEFAULT_RETRIES	1
#define MIN_BACKOFF	1.0
#define MAX_BACKOFF	5.0
#define DEFAULT_BACKOFF	1.0

	const char	*tmp;
	int		ret = NOTSUPPORTED;
	AGENT_REQUEST	request;

	zbx_init_agent_request(&request);

	if (SUCCEED != zbx_parse_item_key(key, &request))
	{
		*error = zbx_strdup(NULL, "Invalid item key format.");
		goto out;
	}

	if (0 == strcmp(get_rkey(&request), ZBX_SERVER_ICMPPING_KEY))
	{
		*icmpping = ICMPPING;
	}
	else if (0 == strcmp(get_rkey(&request), ZBX_SERVER_ICMPPINGLOSS_KEY))
	{
		*icmpping = ICMPPINGLOSS;
	}
	else if (0 == strcmp(get_rkey(&request), ZBX_SERVER_ICMPPINGSEC_KEY))
	{
		*icmpping = ICMPPINGSEC;
	}
	else if (0 == strcmp(get_rkey(&request), ZBX_SERVER_ICMPPINGRETRY_KEY))
	{
		*icmpping = ICMPPINGRETRY;
	}
	else
	{
		*error = zbx_strdup(NULL, "Unsupported pinger key.");
		goto out;
	}

	if (7 < get_rparams_num(&request) || (ICMPPINGSEC != *icmpping && 6 < get_rparams_num(&request)))
	{
		*error = zbx_strdup(NULL, "Too many arguments.");
		goto out;
	}

	if (ICMPPINGRETRY != *icmpping)
	{
		if (NULL == (tmp = get_rparam(&request, 1)) || '\0' == *tmp)
		{
			pinger->count = 3;
		}
		else if (FAIL == zbx_is_uint31(tmp, &pinger->count) || MIN_COUNT > pinger->count ||
				pinger->count > MAX_COUNT)
		{
			*error = zbx_dsprintf(NULL, "Number of packets \"%s\" is not between %d and %d.",
					tmp, MIN_COUNT, MAX_COUNT);
			goto out;
		}

		if (NULL == (tmp = get_rparam(&request, 2)) || '\0' == *tmp)
		{
			pinger->interval = 0;
		}
		else if (FAIL == zbx_is_uint31(tmp, &pinger->interval) || MIN_INTERVAL > pinger->interval)
		{
			*error = zbx_dsprintf(NULL, "Interval \"%s\" should be at least %d.", tmp, MIN_INTERVAL);
			goto out;
		}

		pinger->retries = -1;
		pinger->backoff = -1;
	}
	else
	{
		if (NULL == (tmp = get_rparam(&request, 1)) || '\0' == *tmp)
		{
			pinger->retries = DEFAULT_RETRIES;
		}
		else if (FAIL == zbx_is_uint31(tmp, &pinger->retries))
		{
			*error = zbx_dsprintf(NULL, "Number of retries \"%s\" must be greater or equal to zero.", tmp);
			goto out;
		}

		if (NULL == (tmp = get_rparam(&request, 2)) || '\0' == *tmp)
		{
			pinger->backoff = DEFAULT_BACKOFF;
		}
		else if (SUCCEED != zbx_is_double(tmp, &pinger->backoff) || MIN_BACKOFF > pinger->backoff ||
				MAX_BACKOFF < pinger->backoff)
		{
			*error = zbx_dsprintf(NULL, "Backoff \"%s\" is not between %.1f and %.1f.", tmp,
					MIN_BACKOFF, MAX_BACKOFF);
			goto out;
		}

		pinger->count = -1;
		pinger->interval = -1;
	}

	if (NULL == (tmp = get_rparam(&request, 3)) || '\0' == *tmp)
	{
		pinger->size = 0;
	}
	else if (FAIL == zbx_is_uint31(tmp, &pinger->size) || MIN_SIZE > pinger->size || pinger->size > MAX_SIZE)
	{
		*error = zbx_dsprintf(NULL, "Packet size \"%s\" is not between %d and %d.",
				tmp, MIN_SIZE, MAX_SIZE);
		goto out;
	}

	if (NULL == (tmp = get_rparam(&request, 4)) || '\0' == *tmp)
	{
		pinger->timeout = 0;
	}
	else if (FAIL == zbx_is_uint31(tmp, &pinger->timeout) || MIN_TIMEOUT > pinger->timeout)
	{
		*error = zbx_dsprintf(NULL, "Timeout \"%s\" should be at least %d.", tmp, MIN_TIMEOUT);
		goto out;
	}


	if (ICMPPINGSEC != *icmpping || NULL == (tmp = get_rparam(&request, 5)) || '\0' == *tmp)
	{
		*type = ICMPPINGSEC_AVG;
	}
	else
	{
		if (0 == strcmp(tmp, "min"))
		{
			*type = ICMPPINGSEC_MIN;
		}
		else if (0 == strcmp(tmp, "avg"))
		{
			*type = ICMPPINGSEC_AVG;
		}
		else if (0 == strcmp(tmp, "max"))
		{
			*type = ICMPPINGSEC_MAX;
		}
		else
		{
			*error = zbx_dsprintf(NULL, "Mode \"%s\" is not supported.", tmp);
			goto out;
		}
	}

	if (NULL == (tmp = get_rparam(&request, ((ICMPPINGSEC == *icmpping) ? 6 : 5))) || '\0' == *tmp)
	{
		pinger->allow_redirect = 0;
	}
	else if (0 == strcmp(tmp, "allow_redirect"))
	{
		pinger->allow_redirect = 1;
	}
	else
	{
		*error = zbx_dsprintf(NULL, "\"%s\" is not supported as the \"options\" parameter value"
				".", tmp);
		goto out;
	}

	if (NULL == (tmp = get_rparam(&request, 0)) || '\0' == *tmp)
	{
		if (NULL == host_addr || '\0' == *host_addr)
		{
			*error = zbx_strdup(NULL, "Ping item must have target or host interface specified.");
			goto out;
		}
		*addr = strdup(host_addr);
	}
	else
		*addr = strdup(tmp);

	ret = SUCCEED;
out:
	zbx_free_agent_request(&request);

	return ret;
#undef MIN_COUNT
#undef MAX_COUNT
#undef MIN_INTERVAL
#undef MIN_SIZE
#undef MAX_SIZE
#undef MIN_TIMEOUT
#undef DEFAULT_RETRIES
#undef MIN_BACKOFF
#undef MAX_BACKOFF
#undef DEFAULT_BACKOFF
}

static void	add_icmpping_item(zbx_hashset_t *pinger_items, zbx_pinger_t *pinger_local, zbx_uint64_t itemid,
		char *addr, icmpping_t icmpping, icmppingsec_type_t type)
{
	int			num;
	zbx_pinger_item_t	item;
	zbx_pinger_t		*pinger;

	zabbix_log(LOG_LEVEL_DEBUG, "In %s() addr:'%s' count:%d interval:%d size:%d timeout:%d retries:%d backoff:%.1f"
			" allow_redirect:%u",
			__func__, addr, pinger_local->count, pinger_local->interval, pinger_local->size,
			pinger_local->timeout, pinger_local->retries, pinger_local->backoff,
			pinger_local->allow_redirect);

	num = pinger_items->num_data;

	pinger = (zbx_pinger_t *)zbx_hashset_insert(pinger_items, pinger_local, sizeof(zbx_pinger_t));

	if (pinger_items->num_data != num)
	{
		/* new entry was added, initialize */
		zbx_vector_pinger_item_create(&pinger->items);
	}

	item.itemid = itemid;
	item.addr = addr;
	item.icmpping = icmpping;
	item.type = type;
	zbx_vector_pinger_item_append_ptr(&pinger->items, &item);

	zabbix_log(LOG_LEVEL_DEBUG, "End of %s()", __func__);
}

/******************************************************************************
 *                                                                            *
 * Purpose: creates buffer which contains list of hosts to ping               *
 *                                                                            *
 * Return value: SUCCEED - file was created successfully                      *
 *               FAIL - otherwise                                             *
 *                                                                            *
 ******************************************************************************/
static void	get_pinger_hosts(zbx_hashset_t *pinger_items, int config_timeout)
{
	zbx_dc_item_t		item, *items;
	int			num, errcode = SUCCEED, items_count = 0;
	char			error[MAX_STRING_LEN], *addr = NULL, *errmsg = NULL;
	icmpping_t		icmpping;
	icmppingsec_type_t	type;
	zbx_dc_um_handle_t	*um_handle;

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

	um_handle = zbx_dc_open_user_macros_masked();

	items = &item;
	num = zbx_dc_config_get_poller_items(ZBX_POLLER_TYPE_PINGER, config_timeout, 0, 0, &items);

	for (int i = 0; i < num; i++)
	{
		zbx_pinger_t	pinger_local;

		ZBX_STRDUP(items[i].key, items[i].key_orig);
		int	rc = zbx_substitute_item_key_params(&items[i].key, error, sizeof(error),
				zbx_item_key_subst_cb, um_handle, &items[i]);

		if (SUCCEED == rc)
		{
			rc = pinger_parse_key_params(items[i].key, items[i].interface.addr, &pinger_local, &icmpping,
					&addr, &type, &errmsg);
		}
		else
			errmsg = zbx_strdup(NULL, error);

		if (SUCCEED == rc)
		{
			add_icmpping_item(pinger_items, &pinger_local, items[i].itemid, addr, icmpping, type);
			items_count++;
		}
		else
		{
			zbx_timespec_t	ts;

			zbx_timespec(&ts);

			items[i].state = ITEM_STATE_NOTSUPPORTED;
			zbx_preprocess_item_value(items[i].itemid, items[i].host.hostid, items[i].value_type,
					items[i].flags, items[i].preprocessing, NULL, &ts, items[i].state, errmsg);

			zbx_dc_requeue_items(&items[i].itemid, &ts.sec, &errcode, 1);
			zbx_free(errmsg);
		}

		zbx_free(items[i].key);
	}

	zbx_dc_config_clean_items(items, NULL, num);

	if (items != &item)
		zbx_free(items);

	zbx_preprocessor_flush();

	zbx_dc_close_user_macros(um_handle);

	zabbix_log(LOG_LEVEL_DEBUG, "End of %s():%d", __func__, items_count);
}

static int	fping_host_compare(const void *d1, const void *d2)
{
	const zbx_fping_host_t	*h1 = (const zbx_fping_host_t *)d1;
	const zbx_fping_host_t	*h2 = (const zbx_fping_host_t *)d2;

	return strcmp(h1->addr, h2->addr);
}

static void	add_pinger_host(zbx_vector_fping_host_t *hosts, char *addr)
{
	zbx_fping_host_t	host = {.addr = addr};

	zabbix_log(LOG_LEVEL_DEBUG, "In %s() addr:'%s'", __func__, addr);

	if (FAIL == zbx_vector_fping_host_search(hosts, host, fping_host_compare))
		zbx_vector_fping_host_append_ptr(hosts, &host);

	zabbix_log(LOG_LEVEL_DEBUG, "End of %s()", __func__);
}

static int	process_pinger_hosts(zbx_hashset_t *pinger_items, int process_num, int process_type)
{
	int				ping_result, processed_num = 0;
	char				error[ZBX_ITEM_ERROR_LEN_MAX];
	zbx_vector_fping_host_t		hosts;
	zbx_timespec_t			ts;
	zbx_hashset_iter_t		iter;
	zbx_pinger_t			*pinger;

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

	zbx_vector_fping_host_create(&hosts);
	zbx_vector_fping_host_reserve(&hosts, pinger_items->num_data);

	zbx_hashset_iter_reset(pinger_items, &iter);
	while (NULL != (pinger = (zbx_pinger_t *)zbx_hashset_iter_next(&iter)) && ZBX_IS_RUNNING())
	{
		for (int i = 0; i < pinger->items.values_num; i++)
			add_pinger_host(&hosts, pinger->items.values[i].addr);

		processed_num += pinger->items.values_num;

		zbx_setproctitle("%s #%d [pinging hosts]", get_process_type_string(process_type), process_num);
		zbx_timespec(&ts);

		ping_result = zbx_ping(hosts.values, hosts.values_num, pinger->count, pinger->interval, pinger->size,
				pinger->timeout, pinger->retries, pinger->backoff, pinger->allow_redirect, 0, error,
				sizeof(error));

		if (FAIL != ping_result)
			process_values(&pinger->items, hosts.values, hosts.values_num, &ts, ping_result, error);

		zbx_vector_fping_host_clear(&hosts);
	}

	zbx_vector_fping_host_destroy(&hosts);

	zabbix_log(LOG_LEVEL_DEBUG, "End of %s()", __func__);

	return processed_num;
}

/******************************************************************************
 *                                                                            *
 * Purpose: periodically performs ICMP pings                                  *
 *                                                                            *
 * Comments: never returns                                                    *
 *                                                                            *
 ******************************************************************************/
ZBX_THREAD_ENTRY(zbx_pinger_thread, args)
{
	int			nextcheck, sleeptime, itc,
				server_num = ((zbx_thread_args_t *)args)->info.server_num,
				process_num = ((zbx_thread_args_t *)args)->info.process_num;
	double			sec;
	const zbx_thread_info_t	*info = &((zbx_thread_args_t *)args)->info;
	unsigned char		process_type = ((zbx_thread_args_t *)args)->info.process_type;
	zbx_thread_pinger_args	*pinger_args_in = (zbx_thread_pinger_args *)(((zbx_thread_args_t *)args)->args);
	zbx_hashset_t		pinger_items;

	zabbix_log(LOG_LEVEL_INFORMATION, "%s #%d started [%s #%d]", get_program_type_string(info->program_type),
			server_num, get_process_type_string(process_type), process_num);

	zbx_setproctitle("%s #%d [starting]", get_process_type_string(process_type), process_num);

	zbx_update_selfmon_counter(info, ZBX_PROCESS_STATE_BUSY);
	zbx_init_icmpping_env(get_process_type_string(process_type), zbx_get_thread_id());

	zbx_hashset_create_ext(&pinger_items, ZBX_MAX_PINGER_ITEMS, pinger_hash, pinger_compare, pinger_clear,
			ZBX_DEFAULT_MEM_MALLOC_FUNC, ZBX_DEFAULT_MEM_REALLOC_FUNC, ZBX_DEFAULT_MEM_FREE_FUNC);

	while (ZBX_IS_RUNNING())
	{
		sec = zbx_time();
		zbx_update_env(get_process_type_string(process_type), sec);

		if (FAIL == zbx_vps_monitor_capped())
		{
			zbx_setproctitle("%s #%d [getting values]", get_process_type_string(process_type), process_num);

			get_pinger_hosts(&pinger_items, pinger_args_in->config_timeout);
			itc = process_pinger_hosts(&pinger_items, process_num, process_type);
			zbx_hashset_clear(&pinger_items);
			sec = zbx_time() - sec;

			nextcheck = zbx_dc_config_get_poller_nextcheck(ZBX_POLLER_TYPE_PINGER);
			sleeptime = zbx_calculate_sleeptime(nextcheck, POLLER_DELAY);
		}
		else
		{
			sec = 0;
			itc = 0;
			sleeptime = POLLER_DELAY;
		}

		zbx_setproctitle("%s #%d [got %d values in " ZBX_FS_DBL " sec, idle %d sec%s]",
				get_process_type_string(process_type), process_num, itc, sec, sleeptime,
				zbx_vps_monitor_status());

		zbx_sleep_loop(info, sleeptime);
	}

	zbx_setproctitle("%s #%d [terminated]", get_process_type_string(process_type), process_num);

	while (1)
		zbx_sleep(SEC_PER_MIN);
}
