/*
** 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 "discoverer_taskprep.h"

#include "zbxexpression.h"
#include "zbx_discoverer_constants.h"
#include "discoverer_int.h"
#include "zbxdbhigh.h"
#include "zbxip.h"

#define ZBX_DISCOVERER_IPRANGE_LIMIT	(1 << 16)

static void	dcheck_copy(const zbx_dc_dcheck_t *src, zbx_dc_dcheck_t *dst)
{
	dst->dcheckid = src->dcheckid;
	dst->druleid = src->druleid;
	dst->key_ = zbx_strdup(NULL, src->key_);
	dst->ports = zbx_strdup(NULL, src->ports);
	dst->uniq = src->uniq;
	dst->type = src->type;
	dst->allow_redirect = src->allow_redirect;
	dst->timeout = src->timeout;

	if (SVC_SNMPv1 == src->type || SVC_SNMPv2c == src->type || SVC_SNMPv3 == src->type)
	{
		dst->snmp_community = zbx_strdup(NULL, src->snmp_community);
		dst->snmpv3_securityname = zbx_strdup(NULL, src->snmpv3_securityname);
		dst->snmpv3_securitylevel = src->snmpv3_securitylevel;
		dst->snmpv3_authpassphrase = zbx_strdup(NULL, src->snmpv3_authpassphrase);
		dst->snmpv3_privpassphrase = zbx_strdup(NULL, src->snmpv3_privpassphrase);
		dst->snmpv3_authprotocol = src->snmpv3_authprotocol;
		dst->snmpv3_privprotocol = src->snmpv3_privprotocol;
		dst->snmpv3_contextname = zbx_strdup(NULL, src->snmpv3_contextname);
	}
}

static zbx_ds_dcheck_t	*dcheck_clone_get(zbx_dc_dcheck_t *dcheck, zbx_vector_ds_dcheck_ptr_t *ds_dchecks_common)
{
	zbx_ds_dcheck_t	*ds_dcheck, ds_dcheck_cmp = {.dcheck.dcheckid = dcheck->dcheckid};
	zbx_dc_dcheck_t	*dcheck_ptr;
	int		idx;

	if (FAIL != (idx = zbx_vector_ds_dcheck_ptr_search(ds_dchecks_common, &ds_dcheck_cmp,
			ZBX_DEFAULT_UINT64_PTR_COMPARE_FUNC)))
	{
		return ds_dchecks_common->values[idx];
	}

	ds_dcheck = (zbx_ds_dcheck_t*)zbx_malloc(NULL, sizeof(zbx_ds_dcheck_t));
	dcheck_ptr = &ds_dcheck->dcheck;
	dcheck_copy(dcheck, dcheck_ptr);

	zbx_vector_portrange_create(&ds_dcheck->portranges);
	dcheck_port_ranges_get(ds_dcheck->dcheck.ports, &ds_dcheck->portranges);

	if (SVC_SNMPv1 == dcheck_ptr->type || SVC_SNMPv2c == dcheck_ptr->type ||
			SVC_SNMPv3 == dcheck_ptr->type)
	{
		zbx_substitute_simple_macros_unmasked(NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
				NULL, NULL, NULL, NULL, &dcheck_ptr->snmp_community,
				ZBX_MACRO_TYPE_COMMON, NULL, 0);
		zbx_substitute_key_macros(&dcheck_ptr->key_, NULL, NULL, NULL, NULL,
				ZBX_MACRO_TYPE_SNMP_OID, NULL, 0);

		if (SVC_SNMPv3 == dcheck_ptr->type)
		{
			zbx_substitute_simple_macros_unmasked(NULL, NULL, NULL, NULL, NULL, NULL,
					NULL, NULL, NULL, NULL, NULL, NULL,
					&dcheck_ptr->snmpv3_securityname, ZBX_MACRO_TYPE_COMMON, NULL,
					0);
			zbx_substitute_simple_macros_unmasked(NULL, NULL, NULL, NULL, NULL, NULL,
					NULL, NULL, NULL, NULL, NULL, NULL,
					&dcheck_ptr->snmpv3_authpassphrase, ZBX_MACRO_TYPE_COMMON, NULL,
					0);
			zbx_substitute_simple_macros_unmasked(NULL, NULL, NULL, NULL, NULL, NULL,
					NULL, NULL, NULL, NULL, NULL, NULL,
					&dcheck_ptr->snmpv3_privpassphrase, ZBX_MACRO_TYPE_COMMON, NULL,
					0);
			zbx_substitute_simple_macros_unmasked(NULL, NULL, NULL, NULL, NULL, NULL,
					NULL, NULL, NULL, NULL, NULL, NULL,
					&dcheck_ptr->snmpv3_contextname, ZBX_MACRO_TYPE_COMMON, NULL,
					0);
		}
	}

	zbx_vector_ds_dcheck_ptr_append(ds_dchecks_common, ds_dcheck);

	return ds_dcheck;
}

static zbx_uint64_t	process_check_range(const zbx_dc_drule_t *drule, zbx_ds_dcheck_t *ds_dcheck,
		zbx_vector_iprange_t *ipranges, zbx_hashset_t *tasks)
{
	zbx_discoverer_task_t	task_local, *task;
	int			port = ZBX_PORTRANGE_INIT_PORT;
	unsigned int		checks_count = 0;

	if (SVC_ICMPPING != ds_dcheck->dcheck.type)
	{
		zbx_vector_portrange_t	*port_ranges = &ds_dcheck->portranges;

		while (SUCCEED == zbx_portrange_uniq_next(port_ranges->values, port_ranges->values_num, &port))
			checks_count++;

		if (0 != port_ranges->values_num)
			port = port_ranges->values[0].from;	/* get value of first port in range */
	}
	else
		checks_count = 1;

	task_local.range.id = 0;
	zbx_vector_ds_dcheck_ptr_create(&task_local.ds_dchecks);
	zbx_vector_ds_dcheck_ptr_append(&task_local.ds_dchecks, ds_dcheck);

	/* The net-snmplib limitation associated with the internal EnginID cache requires that the net-snmplib cache */
	/* be reset after each dcheck. That's why we put each snmpv3 dcheck into a separate task. */

	if (SVC_SNMPv3 != ds_dcheck->dcheck.type && NULL != (task = zbx_hashset_search(tasks, &task_local)))
	{
		zbx_vector_ds_dcheck_ptr_destroy(&task_local.ds_dchecks);

		if (FAIL == zbx_vector_ds_dcheck_ptr_search(&task->ds_dchecks, ds_dcheck,
				ZBX_DEFAULT_UINT64_PTR_COMPARE_FUNC))
		{
			zbx_vector_ds_dcheck_ptr_append(&task->ds_dchecks, ds_dcheck);
			task->range.state.checks_per_ip += checks_count;
		}
	}
	else
	{
		memset(&task_local.range, 0, sizeof(zbx_task_range_t));
		task_local.range.id = SVC_SNMPv3 == ds_dcheck->dcheck.type ? ds_dcheck->dcheck.dcheckid : 0;
		task_local.range.ipranges = ipranges;
		task_local.range.state.checks_per_ip = checks_count;
		task_local.unique_dcheckid = drule->unique_dcheckid;
		task_local.range.state.port = port;
		zbx_iprange_first(task_local.range.ipranges->values, task_local.range.state.ipaddress);

		zbx_hashset_insert(tasks, &task_local, sizeof(zbx_discoverer_task_t));
	}

	return (zbx_uint64_t)checks_count;
}

static zbx_uint64_t	process_checks(const zbx_dc_drule_t *drule, int unique, zbx_hashset_t *tasks,
		zbx_vector_ds_dcheck_ptr_t *ds_dchecks_common, zbx_vector_iprange_t *ipranges)
{
	int		i;
	zbx_uint64_t	checks_count = 0;

	for (i = 0; i < drule->dchecks.values_num; i++)
	{
		zbx_dc_dcheck_t	*dcheck = (zbx_dc_dcheck_t*)drule->dchecks.values[i];
		zbx_ds_dcheck_t	*ds_dcheck_common;

		if (0 != drule->unique_dcheckid &&
				((1 == unique && drule->unique_dcheckid != dcheck->dcheckid) ||
				(0 == unique && drule->unique_dcheckid == dcheck->dcheckid)))
		{
			continue;
		}

		ds_dcheck_common = dcheck_clone_get(dcheck, ds_dchecks_common);
		checks_count += process_check_range(drule, ds_dcheck_common, ipranges, tasks);
	}

	return checks_count;
}

static void	process_task_range_count(zbx_hashset_t *tasks, unsigned int ips_num)
{
	zbx_discoverer_task_t	*task;
	zbx_hashset_iter_t	iter;

	if (0 == ips_num)
		return;

	zbx_hashset_iter_reset(tasks, &iter);

	while (NULL != (task = (zbx_discoverer_task_t*)zbx_hashset_iter_next(&iter)))
	{
		task->range.state.count = task->range.state.checks_per_ip * ips_num;
	}
}

/******************************************************************************
 *                                                                            *
 * Purpose: process single discovery rule                                     *
 *                                                                            *
 ******************************************************************************/
void	process_rule(zbx_dc_drule_t *drule, zbx_hashset_t *tasks, zbx_hashset_t *check_counts,
		zbx_vector_ds_dcheck_ptr_t *ds_dchecks_common, zbx_vector_iprange_t *ipranges,
		zbx_vector_discoverer_drule_error_t *drule_errors, zbx_vector_uint64_t *err_druleids)
{
	zbx_uint64_t	checks_count = 0;
	char		ip[ZBX_INTERFACE_IP_LEN_MAX], *comma, *start = drule->iprange;
	unsigned int	uniq_ips_num = 0;
	int		i;

	zabbix_log(LOG_LEVEL_DEBUG, "In %s() rule:'%s' range:'%s'", __func__, drule->name, drule->iprange);

	/* i = 1 to guarantee at least 1 iprange */
	for (i = 1; NULL != (start = strchr(start, ',')); i++, start++);

	zbx_vector_iprange_reserve(ipranges, (size_t)i);

	for (start = drule->iprange; '\0' != *start;)
	{
		zbx_iprange_t	ipr;
		int		res, ip_first[ZBX_IPRANGE_GROUPS_V6], z[ZBX_IPRANGE_GROUPS_V6] = {0};

		if (NULL != (comma = strchr(start, ',')))
			*comma = '\0';

		zabbix_log(LOG_LEVEL_DEBUG, "%s() range:'%s'", __func__, start);

		if (SUCCEED == (res = zbx_iprange_parse(&ipr, start)))
			zbx_iprange_first(&ipr, ip_first);

		if (SUCCEED != res || 0 == memcmp(ip_first, z, sizeof(int) *
				(ZBX_IPRANGE_V4 == ipr.type ? ZBX_IPRANGE_GROUPS_V4 : ZBX_IPRANGE_GROUPS_V6)))
		{
			char	err[MAX_STRING_LEN];

			zbx_snprintf(err, sizeof(err), "Wrong format of IP range \"%s\"", start);
			discoverer_queue_append_error(drule_errors, drule->druleid, err);
			zbx_vector_uint64_append(err_druleids, drule->druleid);
			goto out;
		}

		if (ZBX_DISCOVERER_IPRANGE_LIMIT < zbx_iprange_volume(&ipr))
		{
			char	err[MAX_STRING_LEN];

			zbx_snprintf(err, sizeof(err), "IP range \"%s\" exceeds %d address limit", start,
					ZBX_DISCOVERER_IPRANGE_LIMIT);
			discoverer_queue_append_error(drule_errors, drule->druleid, err);
			zbx_vector_uint64_append(err_druleids, drule->druleid);
			goto out;
		}
#ifndef HAVE_IPV6
		if (ZBX_IPRANGE_V6 == ipr.type)
		{
			char	err[MAX_STRING_LEN];

			zbx_snprintf(err, sizeof(err), "Encountered IP range \"%s\","
					" but IPv6 support not compiled in", start);
			discoverer_queue_append_error(drule_errors, drule->druleid, err);
			zbx_vector_uint64_append(err_druleids, drule->druleid);
			goto out;
		}
#endif
		zbx_vector_iprange_append(ipranges, ipr);

		if (NULL != comma)
		{
			*comma = ',';
			start = comma + 1;
		}
		else
			break;
	}

	if (0 != drule->unique_dcheckid)
		checks_count = process_checks(drule, 1, tasks, ds_dchecks_common, ipranges);

	checks_count += process_checks(drule, 0, tasks, ds_dchecks_common, ipranges);

	if (0 == checks_count)
		goto out;

	*ip = '\0';

	while (SUCCEED == zbx_iprange_uniq_next(ipranges->values, ipranges->values_num, ip, sizeof(ip)))
	{
		zbx_discoverer_check_count_t	dcc;

		dcc.druleid = drule->druleid;
		zbx_strlcpy(dcc.ip, ip, sizeof(dcc.ip));
		dcc.count = checks_count;
		zbx_hashset_insert(check_counts, &dcc, sizeof(zbx_discoverer_check_count_t));
		uniq_ips_num++;
	}

	process_task_range_count(tasks, uniq_ips_num);
out:
	zabbix_log(LOG_LEVEL_DEBUG, "End of %s() drule:" ZBX_FS_UI64 " tasks:%d check_counts(ip):%d checks_count:"
			ZBX_FS_UI64, __func__, drule->druleid, tasks->num_data, check_counts->num_data, checks_count);
}