/*
** Zabbix
** Copyright (C) 2001-2021 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** 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 General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/

#include "common.h"
#include "db.h"
#include "zbxtasks.h"
#include "log.h"
#include "zbxserver.h"
#include "postinit.h"
#include "valuecache.h"

#define ZBX_HIST_MACRO_NONE		(-1)
#define ZBX_HIST_MACRO_ITEM_VALUE	0
#define ZBX_HIST_MACRO_ITEM_LASTVALUE	1

/******************************************************************************
 *                                                                            *
 * Function: get_trigger_count                                                *
 *                                                                            *
 * Purpose: gets the total number of triggers on system                       *
 *                                                                            *
 * Parameters:                                                                *
 *                                                                            *
 * Return value: The total number of triggers on system.                      *
 *                                                                            *
 ******************************************************************************/
static int	get_trigger_count(void)
{
	DB_RESULT	result;
	DB_ROW		row;
	int		triggers_num;

	result = DBselect("select count(*) from triggers");
	if (NULL != (row = DBfetch(result)))
		triggers_num = atoi(row[0]);
	else
		triggers_num = 0;
	DBfree_result(result);

	return triggers_num;
}

/******************************************************************************
 *                                                                            *
 * Function: is_historical_macro                                              *
 *                                                                            *
 * Purpose: checks if this is historical macro that cannot be expanded for    *
 *          bulk event name update                                            *
 *                                                                            *
 * Parameters: macro      - [IN] the macro name                               *
 *                                                                            *
 * Return value: ZBX_HIST_MACRO_* defines                                     *
 *                                                                            *
 ******************************************************************************/
static int	is_historical_macro(const char *macro)
{
	if (0 == strncmp(macro, "ITEM.VALUE", ZBX_CONST_STRLEN("ITEM.VALUE")))
		return ZBX_HIST_MACRO_ITEM_VALUE;

	if (0 == strncmp(macro, "ITEM.LASTVALUE", ZBX_CONST_STRLEN("ITEM.LASTVALUE")))
		return ZBX_HIST_MACRO_ITEM_LASTVALUE;

	return ZBX_HIST_MACRO_NONE;
}

/******************************************************************************
 *                                                                            *
 * Function: convert_historical_macro                                         *
 *                                                                            *
 * Purpose: translates historical macro to lld macro format                   *
 *                                                                            *
 * Parameters: macro - [IN] the macro type (see ZBX_HIST_MACRO_* defines)     *
 *                                                                            *
 * Return value: the macro                                                    *
 *                                                                            *
 * Comments: Some of the macros can be converted to different name.           *
 *                                                                            *
 ******************************************************************************/
static const char	*convert_historical_macro(int macro)
{
	/* When expanding macros for old events ITEM.LASTVALUE macro would */
	/* always expand to one (latest) value. Expanding it as ITEM.VALUE */
	/* makes more sense in this case.                                  */
	const char	*macros[] = {"#ITEM.VALUE", "#ITEM.VALUE"};

	return macros[macro];
}

/******************************************************************************
 *                                                                            *
 * Function: preprocess_trigger_name                                          *
 *                                                                            *
 * Purpose: pre-process trigger name(description) by expanding non historical *
 *          macros                                                            *
 *                                                                            *
 * Parameters: trigger    - [IN] the trigger                                  *
 *             historical - [OUT] 1 - trigger name contains historical macros *
 *                                0 - otherwise                               *
 *                                                                            *
 * Comments: Some historical macros might be replaced with other macros to    *
 *           better match the trigger name at event creation time.            *
 *                                                                            *
 ******************************************************************************/
static void	preprocess_trigger_name(DB_TRIGGER *trigger, int *historical)
{
	int		pos = 0, macro_len, macro_type;
	zbx_token_t	token;
	size_t		name_alloc, name_len, replace_alloc = 64, replace_offset, r, l;
	char		*replace;
	const char	*macro;
	DB_EVENT	event;

	*historical = FAIL;

	replace = (char *)zbx_malloc(NULL, replace_alloc);

	name_alloc = name_len = strlen(trigger->description) + 1;

	while (SUCCEED == zbx_token_find(trigger->description, pos, &token, ZBX_TOKEN_SEARCH_BASIC))
	{
		if (ZBX_TOKEN_MACRO == token.type || ZBX_TOKEN_FUNC_MACRO == token.type)
		{
			/* the macro excluding the opening and closing brackets {}, for example: ITEM.VALUE */
			if (ZBX_TOKEN_MACRO == token.type)
			{
				l = token.data.macro.name.l;
				r = token.data.macro.name.r;
			}
			else
			{
				l = token.data.func_macro.macro.l + 1;
				r = token.data.func_macro.macro.r - 1;
			}

			macro = trigger->description + l;

			if (ZBX_HIST_MACRO_NONE != (macro_type = is_historical_macro(macro)))
			{
				if (0 != isdigit(*(trigger->description + r)))
					macro_len = r - l;
				else
					macro_len = r - l + 1;

				macro = convert_historical_macro(macro_type);

				token.loc.r += zbx_replace_mem_dyn(&trigger->description, &name_alloc, &name_len, l,
						macro_len, macro, strlen(macro));
				*historical = SUCCEED;
			}
		}
		pos = token.loc.r;
	}

	memset(&event, 0, sizeof(DB_EVENT));
	event.object = EVENT_OBJECT_TRIGGER;
	event.objectid = trigger->triggerid;
	event.trigger = *trigger;

	substitute_simple_macros(NULL, &event, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &trigger->description,
			MACRO_TYPE_TRIGGER_DESCRIPTION, NULL, 0);

	if (SUCCEED == *historical)
	{
		pos = 0;
		name_alloc = name_len = strlen(trigger->description) + 1;

		while (SUCCEED == zbx_token_find(trigger->description, pos, &token, ZBX_TOKEN_SEARCH_BASIC))
		{
			if (ZBX_TOKEN_LLD_MACRO == token.type || ZBX_TOKEN_LLD_FUNC_MACRO == token.type)
			{
				if (ZBX_TOKEN_LLD_MACRO == token.type)
				{
					l = token.data.lld_macro.name.l;
					r = token.data.lld_macro.name.r;
				}
				else
				{
					l = token.data.lld_func_macro.macro.l + 2;
					r = token.data.lld_func_macro.macro.r - 1;
				}

				macro = trigger->description + l;

				if (ZBX_HIST_MACRO_NONE != is_historical_macro(macro))
				{
					macro_len = r - l + 1;
					replace_offset = 0;
					zbx_strncpy_alloc(&replace, &replace_alloc, &replace_offset, macro, macro_len);

					token.loc.r += zbx_replace_mem_dyn(&trigger->description, &name_alloc,
							&name_len, l - 1, macro_len + 1, replace, replace_offset);
				}
			}
			pos = token.loc.r;
		}
	}

	zbx_free(replace);
}

/******************************************************************************
 *                                                                            *
 * Function: process_event_bulk_update                                        *
 *                                                                            *
 * Purpose: update event/problem names for a trigger with bulk request        *
 *                                                                            *
 * Parameters: trigger    - [IN] the trigger                                  *
 *             sql        - [IN/OUT] the sql query                            *
 *             sql_alloc  - [IN/OUT] the sql query size                       *
 *             sql_offset - [IN/OUT] the sql query length                     *
 *                                                                            *
 * Return value: SUCCEED - the update was successful                          *
 *               FAIL - otherwise                                             *
 *                                                                            *
 * Comments: Event names for triggers without historical macros will be the   *
 *           same and can be updated with a single sql query.                 *
 *                                                                            *
 ******************************************************************************/
static int	process_event_bulk_update(const DB_TRIGGER *trigger, char **sql, size_t *sql_alloc, size_t *sql_offset)
{
	char	*name_esc;
	int	ret;

	name_esc = DBdyn_escape_string_len(trigger->description, EVENT_NAME_LEN);

	zbx_snprintf_alloc(sql, sql_alloc, sql_offset,
			"update events"
			" set name='%s'"
			" where source=%d"
				" and object=%d"
				" and objectid=" ZBX_FS_UI64 ";\n",
			name_esc, EVENT_SOURCE_TRIGGERS, EVENT_OBJECT_TRIGGER, trigger->triggerid);

	if (SUCCEED == (ret = DBexecute_overflowed_sql(sql, sql_alloc, sql_offset)))
	{
		zbx_snprintf_alloc(sql, sql_alloc, sql_offset,
				"update problem"
				" set name='%s'"
				" where source=%d"
					" and object=%d"
					" and objectid=" ZBX_FS_UI64 ";\n",
				name_esc, EVENT_SOURCE_TRIGGERS, EVENT_OBJECT_TRIGGER, trigger->triggerid);

		ret = DBexecute_overflowed_sql(sql, sql_alloc, sql_offset);
	}

	zbx_free(name_esc);

	return ret;
}

/******************************************************************************
 *                                                                            *
 * Function: process_event_update                                             *
 *                                                                            *
 * Purpose: update event/problem names for a trigger with separate requests   *
 *          for each event                                                    *
 *                                                                            *
 * Parameters: trigger    - [IN] the trigger                                  *
 *             sql        - [IN/OUT] the sql query                            *
 *             sql_alloc  - [IN/OUT] the sql query size                       *
 *             sql_offset - [IN/OUT] the sql query length                     *
 *                                                                            *
 * Return value: SUCCEED - the update was successful                          *
 *               FAIL - otherwise                                             *
 *                                                                            *
 * Comments: Event names for triggers with historical macros might differ and *
 *           historical macros in trigger name must be expanded for each      *
 *           event.                                                           *
 *                                                                            *
 ******************************************************************************/
static int	process_event_update(const DB_TRIGGER *trigger, char **sql, size_t *sql_alloc, size_t *sql_offset)
{
	DB_RESULT	result;
	DB_ROW		row;
	DB_EVENT	event;
	char		*name, *name_esc;
	int		ret = SUCCEED;

	memset(&event, 0, sizeof(DB_EVENT));

	result = DBselect("select eventid,source,object,objectid,clock,value,acknowledged,ns,name"
			" from events"
			" where source=%d"
				" and object=%d"
				" and objectid=" ZBX_FS_UI64
			" order by eventid",
			EVENT_SOURCE_TRIGGERS, EVENT_OBJECT_TRIGGER, trigger->triggerid);

	while (SUCCEED == ret && NULL != (row = DBfetch(result)))
	{
		ZBX_STR2UINT64(event.eventid, row[0]);
		event.source = atoi(row[1]);
		event.object = atoi(row[2]);
		ZBX_STR2UINT64(event.objectid, row[3]);
		event.clock = atoi(row[4]);
		event.value = atoi(row[5]);
		event.acknowledged = atoi(row[6]);
		event.ns = atoi(row[7]);
		event.name = row[8];

		event.trigger = *trigger;

		name = zbx_strdup(NULL, trigger->description);

		substitute_simple_macros(NULL, &event, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &name,
				MACRO_TYPE_TRIGGER_DESCRIPTION, NULL, 0);

		name_esc = DBdyn_escape_string_len(name, EVENT_NAME_LEN);

		zbx_snprintf_alloc(sql, sql_alloc, sql_offset,
				"update events"
				" set name='%s'"
				" where eventid=" ZBX_FS_UI64 ";\n",
				name_esc, event.eventid);

		if (SUCCEED == (ret = DBexecute_overflowed_sql(sql, sql_alloc, sql_offset)))
		{
			zbx_snprintf_alloc(sql, sql_alloc, sql_offset,
					"update problem"
					" set name='%s'"
					" where eventid=" ZBX_FS_UI64 ";\n",
					name_esc, event.eventid);

			ret = DBexecute_overflowed_sql(sql, sql_alloc, sql_offset);
		}

		zbx_free(name_esc);
		zbx_free(name);
	}

	DBfree_result(result);

	return ret;
}

/******************************************************************************
 *                                                                            *
 * Function: update_event_names                                               *
 *                                                                            *
 * Purpose: update event names in events and problem tables                   *
 *                                                                            *
 * Return value: SUCCEED - the update was successful                          *
 *               FAIL - otherwise                                             *
 *                                                                            *
 ******************************************************************************/
static int	update_event_names(void)
{
	DB_RESULT	result;
	DB_ROW		row;
	DB_TRIGGER	trigger;
	int		ret = SUCCEED, historical, triggers_num, processed_num = 0, completed, last_completed = 0;
	char		*sql;
	size_t		sql_alloc = 4096, sql_offset = 0;

	zabbix_log(LOG_LEVEL_WARNING, "starting event name update forced by database upgrade");

	if (0 == (triggers_num = get_trigger_count()))
		goto out;

	memset(&trigger, 0, sizeof(DB_TRIGGER));

	sql = (char *)zbx_malloc(NULL, sql_alloc);
	DBbegin_multiple_update(&sql, &sql_alloc, &sql_offset);

	result = DBselect(
			"select triggerid,description,expression,priority,comments,url,recovery_expression,"
				"recovery_mode,value"
			" from triggers"
			" order by triggerid");

	while (SUCCEED == ret && NULL != (row = DBfetch(result)))
	{
		ZBX_STR2UINT64(trigger.triggerid, row[0]);
		trigger.description = zbx_strdup(NULL, row[1]);
		trigger.expression = zbx_strdup(NULL, row[2]);
		ZBX_STR2UCHAR(trigger.priority, row[3]);
		trigger.comments = zbx_strdup(NULL, row[4]);
		trigger.url = zbx_strdup(NULL, row[5]);
		trigger.recovery_expression = zbx_strdup(NULL, row[6]);
		ZBX_STR2UCHAR(trigger.recovery_mode, row[7]);
		ZBX_STR2UCHAR(trigger.value, row[8]);

		preprocess_trigger_name(&trigger, &historical);

		if (FAIL == historical)
			ret = process_event_bulk_update(&trigger, &sql, &sql_alloc, &sql_offset);
		else
			ret = process_event_update(&trigger, &sql, &sql_alloc, &sql_offset);

		zbx_db_trigger_clean(&trigger);

		processed_num++;

		if (last_completed != (completed = 100.0 * processed_num / triggers_num))
		{
			zabbix_log(LOG_LEVEL_WARNING, "completed %d%% of event name update", completed);
			last_completed = completed;
		}
	}

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

	if (SUCCEED == ret && 16 < sql_offset) /* in ORACLE always present begin..end; */
	{
		if (ZBX_DB_OK > DBexecute("%s", sql))
			ret = FAIL;
	}

	DBfree_result(result);

	zbx_free(sql);
out:
	if (SUCCEED == ret)
		zabbix_log(LOG_LEVEL_WARNING, "event name update completed");
	else
		zabbix_log(LOG_LEVEL_WARNING, "event name update failed");

	return ret;
}

/******************************************************************************
 *                                                                            *
 * Function: zbx_check_postinit_tasks                                         *
 *                                                                            *
 * Purpose: process post initialization tasks                                 *
 *                                                                            *
 * Return value: SUCCEED - the update was successful                          *
 *               FAIL - otherwise                                             *
 *                                                                            *
 ******************************************************************************/
int	zbx_check_postinit_tasks(char **error)
{
	DB_RESULT	result;
	DB_ROW		row;
	int		ret = SUCCEED;

	result = DBselect("select taskid from task where type=%d and status=%d", ZBX_TM_TASK_UPDATE_EVENTNAMES,
			ZBX_TM_STATUS_NEW);

	if (NULL != (row = DBfetch(result)))
	{
		DBbegin();

		if (SUCCEED == (ret = update_event_names()))
		{
			DBexecute("delete from task where taskid=%s", row[0]);
			DBcommit();
		}
		else
			DBrollback();
	}

	DBfree_result(result);

	if (SUCCEED != ret)
		*error = zbx_strdup(*error, "cannot update event names");

	return ret;
}
