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

#include "zbxcommon.h"
#include "zbxdbhigh.h"
#include "audit/zbxaudit.h"
#include "zbxdb.h"
#include "zbxnum.h"
#include "../server_constants.h"

ZBX_VECTOR_DECL(id_name_pair, zbx_id_name_pair_t)
ZBX_VECTOR_IMPL(id_name_pair, zbx_id_name_pair_t)
ZBX_VECTOR_IMPL(lld_discovery_ptr, zbx_lld_discovery_t *)

int	lld_ids_names_compare_func(const void *d1, const void *d2)
{
	const zbx_id_name_pair_t	*id_name_pair_entry_1 = (const zbx_id_name_pair_t *)d1;
	const zbx_id_name_pair_t	*id_name_pair_entry_2 = (const zbx_id_name_pair_t *)d2;

	ZBX_RETURN_IF_NOT_EQUAL(id_name_pair_entry_1->id, id_name_pair_entry_2->id);

	return 0;
}

void	lld_field_str_rollback(char **field, char **field_orig, zbx_uint64_t *flags, zbx_uint64_t flag)
{
	if (0 == (*flags & flag))
		return;

	zbx_free(*field);
	*field = *field_orig;
	*field_orig = NULL;
	*flags &= ~flag;
}

/******************************************************************************
 *                                                                            *
 * Purpose: calculates when to delete lost resources in overflow-safe way     *
 *                                                                            *
 ******************************************************************************/
int	lld_end_of_life(int lastcheck, int lifetime)
{
	return ZBX_JAN_2038 - lastcheck > lifetime ? lastcheck + lifetime : ZBX_JAN_2038;
}

static int	lld_get_lifetime_ts(int obj_lastcheck, const zbx_lld_lifetime_t *lifetime)
{
	int	ts;

	if (ZBX_LLD_LIFETIME_TYPE_AFTER == lifetime->type)
		ts = lld_end_of_life(obj_lastcheck, lifetime->duration);
	else if (ZBX_LLD_LIFETIME_TYPE_IMMEDIATELY == lifetime->type)
		ts = 1;
	else
		ts = 0;

	return ts;
}

static int	lld_check_lifetime_elapsed(int lastcheck, int ts)
{
	if (0 == ts || lastcheck <= ts)
		return FAIL;

	return SUCCEED;
}

/******************************************************************************
 *                                                                            *
 * Purpose: add new discovery record                                          *
 *                                                                            *
 * Parameters: discoveries - [OUT] discovery records                          *
 *             id          - [IN] object id                                   *
 *             name        - [IN] object name                                 *
 *                                                                            *
 * Return value: added discovery record                                       *
 *                                                                            *
 ******************************************************************************/
zbx_lld_discovery_t	*lld_add_discovery(zbx_hashset_t *discoveries, zbx_uint64_t id, const char *name)
{
	zbx_lld_discovery_t	discovery_local = {.id = id, .name = name, .flags = ZBX_LLD_DISCOVERY_UPDATE_NONE};

	return (zbx_lld_discovery_t *)zbx_hashset_insert(discoveries, &discovery_local, sizeof(discovery_local));
}

/******************************************************************************
 *                                                                            *
 * Purpose: update fields for discovered objects                              *
 *                                                                            *
 * Parameters: discovery        - [IN] object discovery record                *
 *             discovery_status - [IN] current discovery status               *
 *             ts_delete        - [IN] current object removal time            *
 *                                                                            *
 ******************************************************************************/
void	lld_process_discovered_object(zbx_lld_discovery_t *discovery, unsigned char discovery_status, int ts_delete,
		int lastcheck, int now)
{
	if (lastcheck != now)
		discovery->flags |= ZBX_LLD_DISCOVERY_UPDATE_LASTCHECK;

	if (ZBX_LLD_DISCOVERY_STATUS_NORMAL != discovery_status)
	{
		discovery->flags |= ZBX_LLD_DISCOVERY_UPDATE_DISCOVERY_STATUS;
		discovery->discovery_status = ZBX_LLD_DISCOVERY_STATUS_NORMAL;
	}

	if (0 != ts_delete)
	{
		discovery->flags |= ZBX_LLD_DISCOVERY_UPDATE_TS_DELETE;
		discovery->ts_delete = 0;
	}
}

/******************************************************************************
 *                                                                            *
 * Purpose: update fields for discovered objects that were disabled because   *
 *          being lost in last discovery processing                           *
 *                                                                            *
 * Parameters: discovery      - [IN] object discovery record                  *
 *             object_status  - [IN] current object status (enabled/disabled) *
 *             disable_source - [IN] lld object disabling status              *
 *             ts_disable     - [IN] current object disable time              *
 *                                                                            *
 ******************************************************************************/
void	lld_enable_discovered_object(zbx_lld_discovery_t *discovery, unsigned char object_status,
		unsigned char disable_source, int ts_disable)
{
	if (ZBX_LLD_OBJECT_STATUS_DISABLED == object_status && ZBX_DISABLE_SOURCE_LLD_LOST == disable_source)
	{
		discovery->flags |= ZBX_LLD_DISCOVERY_UPDATE_DISABLE_SOURCE | ZBX_LLD_DISCOVERY_UPDATE_OBJECT_STATUS;
		discovery->disable_source = ZBX_DISABLE_SOURCE_DEFAULT;
		discovery->object_status = ZBX_LLD_OBJECT_STATUS_ENABLED;
	}

	if (0 != ts_disable)
	{
		discovery->flags |= ZBX_LLD_DISCOVERY_UPDATE_TS_DISABLE;
		discovery->ts_disable = 0;
	}
}

/******************************************************************************
 *                                                                            *
 * Purpose: update fields for lost objects                                    *
 *                                                                            *
 * Parameters: discovery        - [IN] object discovery record                *
 *             object_status    - [IN] current object status                  *
 *             lastcheck        - [IN] last time object was discovered        *
 *             now              - [IN] current timestamp                      *
 *             lifetime         - [IN] period how long lost resources are     *
 *                                     kept                                   *
 *             discovery_status - [IN] current discovery status               *
 *             disable_source   - [IN] lld object disabling status            *
 *             ts_delete        - [IN] current object removal time            *
 *                                                                            *
 ******************************************************************************/
void	lld_process_lost_object(zbx_lld_discovery_t *discovery, unsigned char object_status, int lastcheck, int now,
		const zbx_lld_lifetime_t *lifetime, unsigned char discovery_status, int disable_source, int ts_delete)
{
	int	ts;

	if (0 == discovery->id)
		return;

	ts = lld_get_lifetime_ts(lastcheck, lifetime);

	if (ts != ts_delete)
	{
		discovery->flags |= ZBX_LLD_DISCOVERY_UPDATE_TS_DELETE;
		discovery->ts_delete = ts;
	}

	if (ZBX_LLD_DISCOVERY_STATUS_LOST != discovery_status)
	{
		discovery->flags |= ZBX_LLD_DISCOVERY_UPDATE_DISCOVERY_STATUS;
		discovery->discovery_status = ZBX_LLD_DISCOVERY_STATUS_LOST;
	}

	if (SUCCEED == lld_check_lifetime_elapsed(now, ts))
	{
		if (ZBX_LLD_OBJECT_STATUS_ENABLED == object_status || ZBX_DISABLE_SOURCE_LLD_LOST == disable_source)
			discovery->flags |= ZBX_LLD_DISCOVERY_DELETE_OBJECT;
	}
}

/******************************************************************************
 *                                                                            *
 * Purpose: update fields for lost objects that must be disabled              *
 *                                                                            *
 * Parameters: discovery        - [IN] object discovery record                *
 *             object_status    - [IN] current object status                  *
 *             lastcheck        - [IN] last time object was discovered        *
 *             now              - [IN] current timestamp                      *
 *             lifetime         - [IN] period how long lost resources are     *
 *                                     kept enabled                           *
 *             ts_disable       - [IN] current object removal time            *
 *                                                                            *
 ******************************************************************************/
void	lld_disable_lost_object(zbx_lld_discovery_t *discovery, unsigned char object_status, int lastcheck, int now,
		const zbx_lld_lifetime_t *lifetime, int ts_disable)
{
	int	ts;

	if (0 == discovery->id)
		return;

	ts = lld_get_lifetime_ts(lastcheck, lifetime);

	if (ts != ts_disable)
	{
		discovery->flags |= ZBX_LLD_DISCOVERY_UPDATE_TS_DISABLE;
		discovery->ts_disable = ts;
	}

	if (SUCCEED != lld_check_lifetime_elapsed(now, ts))
		return;

	if (ZBX_LLD_OBJECT_STATUS_ENABLED == object_status)
	{
		discovery->flags |= ZBX_LLD_DISCOVERY_UPDATE_DISABLE_SOURCE | ZBX_LLD_DISCOVERY_UPDATE_OBJECT_STATUS;
		discovery->disable_source = ZBX_DISABLE_SOURCE_LLD_LOST;
		discovery->object_status = ZBX_LLD_OBJECT_STATUS_DISABLED;
	}
}

/******************************************************************************
 *                                                                            *
 * Purpose: lock objects with pending status updates in database and check    *
 *          their actual statuses there                                       *
 *                                                                            *
 * Parameters: discoveries      - [IN] object discoveries                     *
 *             upd_ids          - [IN] ids of objects to be updated           *
 *             id_field         - [IN] object id field name                   *
 *             object_table     - [IN] object table name                      *
 *                                                                            *
 * Comments: If object doesn't need to be updated or has been removed the     *
 *           corresponding discovery flags are reset.                         *
 *                                                                            *
 ******************************************************************************/
static void	lld_check_objects_in_db(zbx_hashset_t *discoveries, zbx_vector_uint64_t *upd_ids,
				const char *id_field, const char *object_table)
{
	zbx_db_result_t		result;
	zbx_db_row_t		row;
	char			*sql = NULL;
	size_t			sql_alloc = 0, sql_offset = 0;
	zbx_lld_discovery_t	*discovery;

	zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, "select %s,status from %s where",
			id_field, object_table );

	zbx_db_add_condition_alloc(&sql, &sql_alloc, &sql_offset, id_field, upd_ids->values, upd_ids->values_num);
	zbx_strcpy_alloc(&sql, &sql_alloc, &sql_offset, ZBX_FOR_UPDATE);

	result = zbx_db_select("%s", sql);
	zbx_free(sql);

	while (NULL != (row = zbx_db_fetch(result)))
	{
		zbx_uint64_t	id;
		unsigned char	value;

		ZBX_STR2UINT64(id, row[0]);

		if (NULL == (discovery = (zbx_lld_discovery_t *)zbx_hashset_search(discoveries, &id)))
		{
			THIS_SHOULD_NEVER_HAPPEN;
			continue;
		}

		discovery->flags |= ZBX_LLD_DISCOVERY_UPDATE_OBJECT_EXISTS;

		if (0 != (discovery->flags & ZBX_LLD_DISCOVERY_UPDATE_OBJECT_STATUS))
		{
			ZBX_STR2UCHAR(value, row[1]);
			if (value == discovery->object_status)
				discovery->flags &= ~ZBX_LLD_DISCOVERY_UPDATE_OBJECT_STATUS;
		}
	}
	zbx_db_free_result(result);

	/* reset discovery flags for already removed objects */
	for (int i = 0; i < upd_ids->values_num; i++)
	{
		if (NULL == (discovery = (zbx_lld_discovery_t *)zbx_hashset_search(discoveries, &upd_ids->values[i])))
		{
			THIS_SHOULD_NEVER_HAPPEN;
			continue;
		}

		if (0 == (discovery->flags & ZBX_LLD_DISCOVERY_UPDATE_OBJECT_EXISTS))
			discovery->flags = 0;
	}
}

/******************************************************************************
 *                                                                            *
 * Purpose: flush pending discovery record updates to database                *
 *                                                                            *
 * Parameters: discoveries            - [IN] object discoveries               *
 *             id_field               - [IN] object id field name             *
 *             object_table           - [IN] object table name                *
 *                                           (can be NULL if object status    *
 *                                           is not changed)                  *
 *             discovery_table        - [IN] object discovery record table    *
 *             now                    - [IN] current timestamp                *
 *             cb_status              - [IN] function to convert common lld   *
 *                                           status to object specific status *
 *                                           (can be NULL if object status    *
 *                                           is not changed)                  *
 *             cb_delete_objects      - [IN] function to delete objects by    *
 *                                           their ids                        *
 *             cb_audit_create        - [IN] function to create object audit  *
 *                                           entry                            *
 *             cb_audit_update_status - [IN] function to update object status *
 *                                           change in audit                  *
 *                                           (can be NULL if object status    *
 *                                           is not changed)                  *
 *                                                                            *
 ******************************************************************************/
void	lld_flush_discoveries(zbx_hashset_t *discoveries, const char *id_field, const char *object_table,
		const char *discovery_table, int now, get_object_status_val cb_status, delete_ids_f cb_delete_objects,
		object_audit_entry_create_f cb_audit_create, object_audit_entry_update_status_f cb_audit_update_status)
{
	int				updates_num = 0;
	zbx_vector_uint64_t		upd_ids, del_ids, upd_ts;
	zbx_vector_lld_discovery_ptr_t	object_updates, discovery_updates;
	zbx_hashset_iter_t		iter;
	zbx_lld_discovery_t		*discovery;
	char				*sql = NULL;
	size_t				sql_alloc = 0, sql_offset = 0;

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

	zbx_vector_lld_discovery_ptr_create(&object_updates);
	zbx_vector_lld_discovery_ptr_create(&discovery_updates);
	zbx_vector_uint64_create(&upd_ids);
	zbx_vector_uint64_create(&del_ids);
	zbx_vector_uint64_create(&upd_ts);

	zbx_hashset_iter_reset(discoveries, &iter);
	while (NULL != (discovery = (zbx_lld_discovery_t *)zbx_hashset_iter_next(&iter)))
	{
		if (0 == discovery->flags)
			continue;

		if (0 != (discovery->flags & (ZBX_LLD_DISCOVERY_UPDATE_OBJECT_STATUS)))
			zbx_vector_uint64_append(&upd_ids, discovery->id);

		updates_num++;
	}

	if (0 == updates_num)
		goto out;

	zbx_db_begin();

	/* lock object table rows and double check if they need to be updated */
	if (0 != upd_ids.values_num)
	{
		zbx_vector_uint64_sort(&upd_ids, ZBX_DEFAULT_UINT64_COMPARE_FUNC);
		lld_check_objects_in_db(discoveries, &upd_ids, id_field, object_table);
	}

	/* prepare updates */
	zbx_hashset_iter_reset(discoveries, &iter);
	while (NULL != (discovery = (zbx_lld_discovery_t *)zbx_hashset_iter_next(&iter)))
	{
		if (0 != (discovery->flags & ZBX_LLD_DISCOVERY_DELETE_OBJECT))
		{
			cb_audit_create(ZBX_AUDIT_LLD_CONTEXT, ZBX_AUDIT_ACTION_DELETE, discovery->id, discovery->name,
					(int)ZBX_FLAG_DISCOVERY_CREATED);
			zbx_vector_uint64_append(&del_ids, discovery->id);

			continue;
		}

		if (0 != (discovery->flags & ZBX_LLD_DISCOVERY_UPDATE_OBJECT_STATUS))
			zbx_vector_lld_discovery_ptr_append(&object_updates, discovery);

		if (0 != (discovery->flags & ZBX_LLD_DISCOVERY_UPDATE))
		{
			if (ZBX_LLD_DISCOVERY_UPDATE_LASTCHECK == (discovery->flags & ZBX_LLD_DISCOVERY_UPDATE))
				zbx_vector_uint64_append(&upd_ts, discovery->id);
			else
				zbx_vector_lld_discovery_ptr_append(&discovery_updates, discovery);
		}
	}

	if (0 != del_ids.values_num)
	{
		zbx_vector_uint64_sort(&del_ids, ZBX_DEFAULT_UINT64_COMPARE_FUNC);
		cb_delete_objects(&del_ids, ZBX_AUDIT_LLD_CONTEXT);
	}

	if (0 == object_updates.values_num && 0 == discovery_updates.values_num && 0 == upd_ts.values_num)
		goto commit;

	zbx_db_begin_multiple_update(&sql, &sql_alloc, &sql_offset);

	zbx_vector_lld_discovery_ptr_sort(&object_updates, ZBX_DEFAULT_UINT64_PTR_COMPARE_FUNC);
	for (int i = 0; i < object_updates.values_num; i++)
	{
		discovery = object_updates.values[i];

		zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, "update %s set status=%d where %s=" ZBX_FS_UI64 ";\n",
				object_table, cb_status(discovery->object_status), id_field, discovery->id);
		zbx_db_execute_overflowed_sql(&sql, &sql_alloc, &sql_offset);

		unsigned char	old_status = ZBX_LLD_OBJECT_STATUS_ENABLED == discovery->object_status ?
						ZBX_LLD_OBJECT_STATUS_DISABLED : ZBX_LLD_OBJECT_STATUS_ENABLED;

		cb_audit_create(ZBX_AUDIT_LLD_CONTEXT, ZBX_AUDIT_ACTION_UPDATE, discovery->id,
				discovery->name, (int)ZBX_FLAG_DISCOVERY_CREATED);

		cb_audit_update_status(ZBX_AUDIT_LLD_CONTEXT, discovery->id, (int)ZBX_FLAG_DISCOVERY_CREATED,
				cb_status(old_status), cb_status(discovery->object_status));
	}

	zbx_vector_lld_discovery_ptr_sort(&discovery_updates, ZBX_DEFAULT_UINT64_PTR_COMPARE_FUNC);
	for (int i = 0; i < discovery_updates.values_num; i++)
	{
		char	delim = ' ';

		discovery = discovery_updates.values[i];

		zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, "update %s set", discovery_table);

		if (0 != (discovery->flags & ZBX_LLD_DISCOVERY_UPDATE_LASTCHECK))
		{
			zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, "%clastcheck=%d", delim, now);
			delim = ',';
		}

		if (0 != (discovery->flags & ZBX_LLD_DISCOVERY_UPDATE_DISCOVERY_STATUS))
		{
			zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, "%cstatus=%u", delim,
					discovery->discovery_status);
			delim = ',';
		}

		if (0 != (discovery->flags & ZBX_LLD_DISCOVERY_UPDATE_DISABLE_SOURCE))
		{
			zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, "%cdisable_source=%u", delim,
					discovery->disable_source);
			delim = ',';
		}

		if (0 != (discovery->flags & ZBX_LLD_DISCOVERY_UPDATE_TS_DELETE))
		{
			zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, "%cts_delete=%d", delim,
					discovery->ts_delete);
			delim = ',';
		}

		if (0 != (discovery->flags & ZBX_LLD_DISCOVERY_UPDATE_TS_DISABLE))
		{
			zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, "%cts_disable=%d", delim,
					discovery->ts_disable);
		}

		zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, " where %s=" ZBX_FS_UI64 ";\n",
				id_field, discovery->id);
		zbx_db_execute_overflowed_sql(&sql, &sql_alloc, &sql_offset);
	}

	zbx_db_end_multiple_update(&sql, &sql_alloc, &sql_offset);

	if (16 < sql_offset)	/* in ORACLE always present begin..end; */
		zbx_db_execute("%s", sql);

	if (0 != upd_ts.values_num)
	{
		sql_offset = 0;
		zbx_vector_uint64_sort(&upd_ts, ZBX_DEFAULT_UINT64_COMPARE_FUNC);
		zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, "update %s set lastcheck=%d where",
				discovery_table, now);
		zbx_db_execute_multiple_query(sql, id_field, &upd_ts);
	}

	zbx_free(sql);
commit:
	zbx_db_commit();
out:
	zbx_vector_uint64_destroy(&upd_ts);
	zbx_vector_uint64_destroy(&del_ids);
	zbx_vector_uint64_destroy(&upd_ids);
	zbx_vector_lld_discovery_ptr_destroy(&discovery_updates);
	zbx_vector_lld_discovery_ptr_destroy(&object_updates);

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