<?php
/*
** Copyright (C) 2001-2024 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/>.
**/


/**
 * Class containing common methods for operations with triggers.
 */
abstract class CTriggerGeneral extends CApiService {

	protected const FLAGS = null;

	/**
	 * @abstract
	 *
	 * @param array $options
	 *
	 * @return array
	 */
	abstract public function get(array $options = []);

	/**
	 * Prepares and returns an array of child triggers, inherited from triggers $tpl_triggers on the given hosts.
	 *
	 * @param array  $tpl_triggers
	 * @param string $tpl_triggers[<tnum>]['triggerid']
	 */
	private function prepareInheritedTriggers(array $tpl_triggers, array $hostids = null, array &$ins_triggers = null,
			array &$upd_triggers = null, array &$db_triggers = null) {
		$ins_triggers = [];
		$upd_triggers = [];
		$db_triggers = [];

		$result = DBselect(
			'SELECT DISTINCT t.triggerid,h.hostid'.
			' FROM triggers t,functions f,items i,hosts h'.
			' WHERE t.triggerid=f.triggerid'.
				' AND f.itemid=i.itemid'.
				' AND i.hostid=h.hostid'.
				' AND '.dbConditionInt('t.triggerid', zbx_objectValues($tpl_triggers, 'triggerid')).
				' AND '.dbConditionInt('h.status', [HOST_STATUS_TEMPLATE])
		);

		$tpl_hostids_by_triggerid = [];
		$tpl_hostids = [];

		while ($row = DBfetch($result)) {
			$tpl_hostids_by_triggerid[$row['triggerid']][] = $row['hostid'];
			$tpl_hostids[$row['hostid']] = true;
		}

		// Unset host-level triggers.
		foreach ($tpl_triggers as $tnum => $tpl_trigger) {
			if (!array_key_exists($tpl_trigger['triggerid'], $tpl_hostids_by_triggerid)) {
				unset($tpl_triggers[$tnum]);
			}
		}

		if (!$tpl_triggers) {
			// Nothing to inherit, just exit.
			return;
		}

		$hosts_by_tpl_hostid = self::getLinkedHosts(array_keys($tpl_hostids), $hostids);
		$chd_triggers_tpl = $this->getHostTriggersByTemplateId(array_keys($tpl_hostids_by_triggerid), $hostids);
		$tpl_triggers_by_description = [];

		// Preparing list of missing triggers on linked hosts.
		foreach ($tpl_triggers as $tpl_trigger) {
			$hostids = [];

			foreach ($tpl_hostids_by_triggerid[$tpl_trigger['triggerid']] as $tpl_hostid) {
				if (array_key_exists($tpl_hostid, $hosts_by_tpl_hostid)) {
					foreach ($hosts_by_tpl_hostid[$tpl_hostid] as $host) {
						if (array_key_exists($host['hostid'], $chd_triggers_tpl)
								&& array_key_exists($tpl_trigger['triggerid'], $chd_triggers_tpl[$host['hostid']])) {
							continue;
						}

						$hostids[$host['hostid']] = true;
					}
				}
			}

			if ($hostids) {
				$tpl_triggers_by_description[$tpl_trigger['description']][] = [
					'triggerid' => $tpl_trigger['triggerid'],
					'expression' => $tpl_trigger['expression'],
					'recovery_mode' => $tpl_trigger['recovery_mode'],
					'recovery_expression' => $tpl_trigger['recovery_expression'],
					'hostids' => $hostids
				];
			}
		}

		$chd_triggers_all = array_replace_recursive($chd_triggers_tpl,
			$this->getHostTriggersByDescription($tpl_triggers_by_description)
		);

		$expression_parser = new CExpressionParser([
			'usermacros' => true,
			'lldmacros' => $this instanceof CTriggerPrototype
		]);

		$recovery_expression_parser = new CExpressionParser([
			'usermacros' => true,
			'lldmacros' => $this instanceof CTriggerPrototype
		]);

		// List of triggers to check for duplicates. Grouped by description.
		$descriptions = [];
		$triggerids = [];

		$output = ['url_name', 'url', 'status', 'priority', 'comments', 'type', 'correlation_mode', 'correlation_tag',
			'manual_close', 'opdata', 'event_name'
		];
		if ($this instanceof CTriggerPrototype) {
			$output[] = 'discover';
		}

		$db_tpl_triggers = DB::select('triggers', [
			'output' => $output,
			'triggerids' => array_keys($tpl_hostids_by_triggerid),
			'preservekeys' => true
		]);

		foreach ($tpl_triggers as $tpl_trigger) {
			$db_tpl_trigger = $db_tpl_triggers[$tpl_trigger['triggerid']];

			$tpl_hostid = $tpl_hostids_by_triggerid[$tpl_trigger['triggerid']][0];

			// expression: func(/template/item) => func(/host/item)
			if ($expression_parser->parse($tpl_trigger['expression']) != CParser::PARSE_SUCCESS) {
				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
					'expression', $expression_parser->getError()
				));
			}

			// recovery_expression: func(/template/item) => func(/host/item)
			if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
				if ($recovery_expression_parser->parse($tpl_trigger['recovery_expression']) != CParser::PARSE_SUCCESS) {
					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
						'recovery_expression', $recovery_expression_parser->getError()
					));
				}
			}

			$new_trigger = $tpl_trigger;
			$new_trigger['uuid'] = '';
			unset($new_trigger['triggerid'], $new_trigger['templateid']);

			if (array_key_exists($tpl_hostid, $hosts_by_tpl_hostid)) {
				foreach ($hosts_by_tpl_hostid[$tpl_hostid] as $host) {
					$new_trigger['expression'] = $tpl_trigger['expression'];
					$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
						[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
					);
					$hist_function = end($hist_functions);
					do {
						$query_parameter = $hist_function['data']['parameters'][0];
						$new_trigger['expression'] = substr_replace($new_trigger['expression'],
							'/'.$host['host'].'/'.$query_parameter['data']['item'], $query_parameter['pos'],
							$query_parameter['length']
						);
					}
					while ($hist_function = prev($hist_functions));

					if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
						$new_trigger['recovery_expression'] = $tpl_trigger['recovery_expression'];
						$hist_functions = $recovery_expression_parser->getResult()->getTokensOfTypes(
							[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
						);
						$hist_function = end($hist_functions);
						do {
							$query_parameter = $hist_function['data']['parameters'][0];
							$new_trigger['recovery_expression'] = substr_replace($new_trigger['recovery_expression'],
								'/'.$host['host'].'/'.$query_parameter['data']['item'], $query_parameter['pos'],
								$query_parameter['length']
							);
						}
						while ($hist_function = prev($hist_functions));
					}

					if (array_key_exists($host['hostid'], $chd_triggers_all)
							&& array_key_exists($tpl_trigger['triggerid'], $chd_triggers_all[$host['hostid']])) {
						$chd_trigger = $chd_triggers_all[$host['hostid']][$tpl_trigger['triggerid']];

						$upd_triggers[] = $new_trigger + [
							'triggerid' => $chd_trigger['triggerid'],
							'templateid' => $tpl_trigger['triggerid']
						];
						$db_triggers[$chd_trigger['triggerid']] = $chd_trigger;
						$triggerids[] = $chd_trigger['triggerid'];

						$check_duplicates = ($chd_trigger['description'] !== $new_trigger['description']
							|| $chd_trigger['expression'] !== $new_trigger['expression']
							|| $chd_trigger['recovery_expression'] !== $new_trigger['recovery_expression']);

						if ($check_duplicates) {
							$descriptions[$new_trigger['description']][] = [
								'expression' => $new_trigger['expression'],
								'recovery_expression' => $new_trigger['recovery_expression'],
								'host' => [
									'hostid' => $host['hostid'],
									'status' => $host['status']
								]
							];
						}
					}
					else {
						$ins_triggers[] = $new_trigger + $db_tpl_trigger + ['templateid' => $tpl_trigger['triggerid']];
					}

				}
			}
		}

		if ($triggerids) {
			// Add trigger tags.
			$result = DBselect(
				'SELECT tt.triggertagid,tt.triggerid,tt.tag,tt.value'.
				' FROM trigger_tag tt'.
				' WHERE '.dbConditionInt('tt.triggerid', $triggerids)
			);

			$trigger_tags = [];

			while ($row = DBfetch($result)) {
				$trigger_tags[$row['triggerid']][] = [
					'triggertagid' => $row['triggertagid'],
					'tag' => $row['tag'],
					'value' => $row['value']
				];
			}

			foreach ($db_triggers as &$db_trigger) {
				$db_trigger['tags'] = array_key_exists($db_trigger['triggerid'], $trigger_tags)
					? $trigger_tags[$db_trigger['triggerid']]
					: [];
			}
			unset($db_trigger);

			// Add discovery rule IDs.
			if ($this instanceof CTriggerPrototype) {
				$result = DBselect(
					'SELECT id.parent_itemid,f.triggerid'.
						' FROM item_discovery id,functions f'.
						' WHERE '.dbConditionInt('f.triggerid', $triggerids).
						' AND f.itemid=id.itemid'
				);

				$drule_by_triggerid = [];

				while ($row = DBfetch($result)) {
					$drule_by_triggerid[$row['triggerid']] = $row['parent_itemid'];
				}

				foreach ($db_triggers as &$db_trigger) {
					$db_trigger['discoveryRule']['itemid'] = $drule_by_triggerid[$db_trigger['triggerid']];
				}
				unset($db_trigger);
			}
		}

		$this->checkDuplicates($descriptions);
	}

	/**
	 * Returns list of linked hosts.
	 *
	 * Output format:
	 *   [
	 *     <tpl_hostid> => [
	 *       [
	 *         'hostid' => <hostid>,
	 *         'host' => <host>
	 *       ],
	 *       ...
	 *     ],
	 *     ...
	 *   ]
	 *
	 * @param array  $tpl_hostids
	 * @param array  $hostids      The function will return a list of all linked hosts if no hostids are specified.
	 *
	 * @return array
	 */
	private static function getLinkedHosts(array $tpl_hostids, array $hostids = null) {
		// Fetch all child hosts and templates
		$sql = 'SELECT ht.hostid,ht.templateid,h.host,h.status'.
			' FROM hosts_templates ht,hosts h'.
			' WHERE ht.hostid=h.hostid'.
				' AND '.dbConditionInt('ht.templateid', $tpl_hostids).
				' AND '.dbConditionInt('h.flags', [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED]);
		if ($hostids !== null) {
			$sql .= ' AND '.dbConditionInt('ht.hostid', $hostids);
		}
		$result = DBselect($sql);

		$hosts_by_tpl_hostid = [];

		while ($row = DBfetch($result)) {
			$hosts_by_tpl_hostid[$row['templateid']][] = [
				'hostid' => $row['hostid'],
				'host' => $row['host'],
				'status' => $row['status']
			];
		}

		return $hosts_by_tpl_hostid;
	}

	/**
	 * Returns list of already linked triggers.
	 *
	 * Output format:
	 *   [
	 *     <hostid> => [
	 *       <tpl_triggerid> => ['triggerid' => <triggerid>],
	 *       ...
	 *     ],
	 *     ...
	 *   ]
	 *
	 * @param array  $tpl_triggerids
	 * @param array  $hostids         The function will return a list of all linked triggers if no hosts are specified.
	 *
	 * @return array
	 */
	private function getHostTriggersByTemplateId(array $tpl_triggerids, array $hostids = null) {
		$output = 't.triggerid,t.expression,t.description,t.url_name,t.url,t.status,t.priority,t.comments,t.type,'.
			't.recovery_mode,t.recovery_expression,t.correlation_mode,t.correlation_tag,t.manual_close,t.opdata,'.
			't.templateid,t.event_name,i.hostid';
		if ($this instanceof CTriggerPrototype) {
			$output .= ',t.discover';
		}

		// Preparing list of triggers by templateid.
		$sql = 'SELECT DISTINCT '.$output.
			' FROM triggers t,functions f,items i'.
			' WHERE t.triggerid=f.triggerid'.
				' AND f.itemid=i.itemid'.
				' AND '.dbConditionInt('t.templateid', $tpl_triggerids);
		if ($hostids !== null) {
			$sql .= ' AND '.dbConditionInt('i.hostid', $hostids);
		}

		$chd_triggers = DBfetchArray(DBselect($sql));
		$chd_triggers = CMacrosResolverHelper::resolveTriggerExpressions($chd_triggers,
			['sources' => ['expression', 'recovery_expression']]
		);

		$chd_triggers_tpl = [];

		foreach ($chd_triggers as $chd_trigger) {
			$hostid = $chd_trigger['hostid'];
			unset($chd_trigger['hostid']);

			$chd_triggers_tpl[$hostid][$chd_trigger['templateid']] = $chd_trigger;
		}

		return $chd_triggers_tpl;
	}

	/**
	 * Returns list of not inherited triggers with same name and expression.
	 *
	 * Output format:
	 *   [
	 *     <hostid> => [
	 *       <tpl_triggerid> => ['triggerid' => <triggerid>],
	 *       ...
	 *     ],
	 *     ...
	 *   ]
	 *
	 * @param array $tpl_triggers_by_description  The list of hostids, grouped by trigger description and expression.
	 *
	 * @return array
	 */
	private function getHostTriggersByDescription(array $tpl_triggers_by_description) {
		$chd_triggers_description = [];

		$expression_parser = new CExpressionParser([
			'usermacros' => true,
			'lldmacros' => $this instanceof CTriggerPrototype
		]);

		$recovery_expression_parser = new CExpressionParser([
			'usermacros' => true,
			'lldmacros' => $this instanceof CTriggerPrototype
		]);

		$output = 't.triggerid,t.expression,t.description,t.url_name,t.url,t.status,t.priority,t.comments,t.type,'.
			't.recovery_mode,t.recovery_expression,t.correlation_mode,t.correlation_tag,t.manual_close,t.opdata,'.
			't.event_name,i.hostid,h.host';
		if ($this instanceof CTriggerPrototype) {
			$output .= ',t.discover';
		}

		foreach ($tpl_triggers_by_description as $description => $tpl_triggers) {
			$hostids = [];

			foreach ($tpl_triggers as $tpl_trigger) {
				$hostids += $tpl_trigger['hostids'];
			}

			$chd_triggers = DBfetchArray(DBselect(
				'SELECT DISTINCT '.$output.
				' FROM triggers t,functions f,items i,hosts h'.
				' WHERE t.triggerid=f.triggerid'.
					' AND f.itemid=i.itemid'.
					' AND i.hostid=h.hostid'.
					' AND '.dbConditionString('t.description', [$description]).
					' AND '.dbConditionInt('i.hostid', array_keys($hostids))
			));

			$chd_triggers = CMacrosResolverHelper::resolveTriggerExpressions($chd_triggers,
				['sources' => ['expression', 'recovery_expression']]
			);

			foreach ($tpl_triggers as $tpl_trigger) {
				// expression: func(/template/item) => func(/host/item)
				if ($expression_parser->parse($tpl_trigger['expression']) != CParser::PARSE_SUCCESS) {
					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
						'expression', $expression_parser->getError()
					));
				}

				// recovery_expression: func(/template/item) => func(/host/item)
				if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
					if ($recovery_expression_parser->parse($tpl_trigger['recovery_expression']) !=
							CParser::PARSE_SUCCESS) {
						self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
							'recovery_expression', $recovery_expression_parser->getError()
						));
					}
				}

				foreach ($chd_triggers as $chd_trigger) {
					if (!array_key_exists($chd_trigger['hostid'], $tpl_trigger['hostids'])) {
						continue;
					}

					if ($chd_trigger['recovery_mode'] != $tpl_trigger['recovery_mode']) {
						continue;
					}

					// Replace template name in /host/key reference to target host name.
					$expression = $tpl_trigger['expression'];
					$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
						[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
					);
					$hist_function = end($hist_functions);
					do {
						$query_parameter = $hist_function['data']['parameters'][0];
						$expression = substr_replace($expression,
							'/'.$chd_trigger['host'].'/'.$query_parameter['data']['item'], $query_parameter['pos'],
							$query_parameter['length']
						);
					}
					while ($hist_function = prev($hist_functions));

					if ($chd_trigger['expression'] !== $expression) {
						continue;
					}

					if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
						$recovery_expression = $tpl_trigger['recovery_expression'];
						$hist_functions = $recovery_expression_parser->getResult()->getTokensOfTypes(
							[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
						);
						$hist_function = end($hist_functions);
						do {
							$query_parameter = $hist_function['data']['parameters'][0];
							$recovery_expression = substr_replace($recovery_expression,
								'/'.$chd_trigger['host'].'/'.$query_parameter['data']['item'], $query_parameter['pos'],
								$query_parameter['length']
							);
						}
						while ($hist_function = prev($hist_functions));

						if ($chd_trigger['recovery_expression'] !== $recovery_expression) {
							continue;
						}
					}

					$hostid = $chd_trigger['hostid'];
					unset($chd_trigger['hostid'], $chd_trigger['host']);
					$chd_triggers_description[$hostid][$tpl_trigger['triggerid']] = $chd_trigger + ['templateid' => 0];
				}
			}
		}

		return $chd_triggers_description;
	}

	/**
	 * Updates children of triggers on the given hosts and propagates the inheritance to all child hosts.
	 * All of the child triggers that became obsolete will be deleted if the given triggers were assigned to a different
	 * template or host.
	 *
	 * @param array  $triggers
	 * @param string $triggers[]['triggerid']
	 * @param string $triggers[]['description']
	 * @param string $triggers[]['expression']
	 * @param int    $triggers[]['recovery mode']
	 * @param string $triggers[]['recovery_expression']
	 * @param array  $hostids
	 */
	protected function inherit(array $triggers, array $hostids = null) {
		$this->prepareInheritedTriggers($triggers, $hostids, $ins_triggers, $upd_triggers, $db_triggers);

		if ($ins_triggers) {
			$this->createReal($ins_triggers, true);
		}

		if ($upd_triggers) {
			$this->updateReal($upd_triggers, $db_triggers, true);
		}

		if ($ins_triggers || $upd_triggers) {
			$this->inherit(array_merge($ins_triggers + $upd_triggers));
		}
	}

	/**
	 * Populate an array by "hostid" keys.
	 *
	 * @param array  $descriptions
	 * @param string $descriptions[<description>][]['expression']
	 *
	 * @throws APIException  If host or template does not exists.
	 *
	 * @return array
	 */
	protected function populateHostIds($descriptions) {
		$expression_parser = new CExpressionParser([
			'usermacros' => true,
			'lldmacros' => $this instanceof CTriggerPrototype
		]);

		$hosts = [];

		foreach ($descriptions as $description => $triggers) {
			foreach ($triggers as $index => $trigger) {
				$expression_parser->parse($trigger['expression']);
				$hosts[$expression_parser->getResult()->getHosts()[0]][$description][] = $index;
			}
		}

		$db_hosts = DBselect(
			'SELECT h.hostid,h.host,h.status'.
			' FROM hosts h'.
			' WHERE '.dbConditionString('h.host', array_keys($hosts)).
				' AND '.dbConditionInt('h.status',
					[HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED, HOST_STATUS_TEMPLATE]
				)
		);

		while ($db_host = DBfetch($db_hosts)) {
			foreach ($hosts[$db_host['host']] as $description => $indexes) {
				foreach ($indexes as $index) {
					$descriptions[$description][$index]['host'] = [
						'hostid' => $db_host['hostid'],
						'status' => $db_host['status']
					];
				}
			}
			unset($hosts[$db_host['host']]);
		}

		if ($hosts) {
			$error_wrong_host = ($this instanceof CTrigger)
				? _('Incorrect trigger expression. Host "%1$s" does not exist or you have no access to this host.')
				: _('Incorrect trigger prototype expression. Host "%1$s" does not exist or you have no access to this host.');
			self::exception(ZBX_API_ERROR_PARAMETERS, _params($error_wrong_host, [key($hosts)]));
		}

		return $descriptions;
	}

	/**
	 * @param array $triggers
	 * @param array $descriptions
	 *
	 * @throws APIException
	 */
	private static function validateUuid(array $triggers, array $descriptions): void {
		foreach ($descriptions as $_triggers) {
			foreach ($_triggers as $_trigger) {
				$triggers[$_trigger['index']]['host_status'] = $_trigger['host']['status'];
			}
		}

		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_ALLOW_UNEXPECTED, 'uniq' => [['uuid']], 'fields' => [
			'host_status' =>	['type' => API_ANY],
			'uuid' =>			['type' => API_MULTIPLE, 'rules' => [
									['if' => ['field' => 'host_status', 'in' => HOST_STATUS_TEMPLATE], 'type' => API_UUID],
									['else' => true, 'type' => API_STRING_UTF8, 'in' => DB::getDefault('triggers', 'uuid'), 'unset' => true]
			]]
		]];

		if (!CApiInputValidator::validate($api_input_rules, $triggers, '/', $error)) {
			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
		}
	}

	/**
	 * Add the UUID to those of the given triggers that belong to a template and don't have the 'uuid' parameter set.
	 *
	 * @param array $triggers
	 * @param array $descriptions
	 */
	private static function addUuid(array &$triggers, array $descriptions): void {
		foreach ($descriptions as $_triggers) {
			foreach ($_triggers as $_trigger) {
				if ($_trigger['host']['status'] == HOST_STATUS_TEMPLATE
						&& !array_key_exists('uuid', $triggers[$_trigger['index']])) {
					$triggers[$_trigger['index']]['uuid'] = generateUuidV4();
				}
			}
		}
	}

	/**
	 * Verify trigger UUIDs are not repeated.
	 *
	 * @param array      $triggers
	 * @param array|null $db_triggers
	 *
	 * @throws APIException
	 */
	private static function checkUuidDuplicates(array $triggers, array $db_triggers = null): void {
		$trigger_indexes = [];

		foreach ($triggers as $i => $trigger) {
			if (!array_key_exists('uuid', $trigger) || $trigger['uuid'] === '') {
				continue;
			}

			if ($db_triggers === null || $trigger['uuid'] !== $db_triggers[$trigger['triggerid']]['uuid']) {
				$trigger_indexes[$trigger['uuid']] = $i;
			}
		}

		if (!$trigger_indexes) {
			return;
		}

		$duplicates = DB::select('triggers', [
			'output' => ['uuid'],
			'filter' => [
				'flags' => static::FLAGS,
				'uuid' => array_keys($trigger_indexes)
			],
			'limit' => 1
		]);

		if ($duplicates) {
			switch (static::FLAGS) {
				case ZBX_FLAG_DISCOVERY_NORMAL:
					$error = _s('Invalid parameter "%1$s": %2$s.', '/'.($trigger_indexes[$duplicates[0]['uuid']] + 1),
						_('trigger with the same UUID already exists')
					);
					break;

				case ZBX_FLAG_DISCOVERY_PROTOTYPE:
					$error = _s('Invalid parameter "%1$s": %2$s.', '/'.($trigger_indexes[$duplicates[0]['uuid']] + 1),
						_('trigger prototype with the same UUID already exists')
					);
					break;
			}

			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
		}
	}

	/**
	 * Checks triggers for duplicates.
	 *
	 * @param array  $descriptions
	 * @param string $descriptions[<description>][]['expression']
	 * @param string $descriptions[<description>][]['recovery_expression']
	 * @param string $descriptions[<description>][]['hostid']
	 *
	 * @throws APIException if at least one trigger exists
	 */
	protected function checkDuplicates(array $descriptions) {
		foreach ($descriptions as $description => $triggers) {
			$hostids = [];
			$expressions = [];

			foreach ($triggers as $trigger) {
				if (array_key_exists('name_updated', $trigger) && !$trigger['name_updated']) {
					continue;
				}

				$hostids[$trigger['host']['hostid']] = true;
				$expressions[$trigger['expression']][$trigger['recovery_expression']] = $trigger['host']['hostid'];
			}

			if (!$hostids) {
				continue;
			}

			$db_triggers = DBfetchArray(DBselect(
				'SELECT DISTINCT t.expression,t.recovery_expression'.
				' FROM triggers t,functions f,items i,hosts h'.
				' WHERE t.triggerid=f.triggerid'.
					' AND f.itemid=i.itemid'.
					' AND i.hostid=h.hostid'.
					' AND '.dbConditionString('t.description', [$description]).
					' AND '.dbConditionInt('i.hostid', array_keys($hostids))
			));

			$db_triggers = CMacrosResolverHelper::resolveTriggerExpressions($db_triggers,
				['sources' => ['expression', 'recovery_expression']]
			);

			foreach ($db_triggers as $db_trigger) {
				$expression = $db_trigger['expression'];
				$recovery_expression = $db_trigger['recovery_expression'];

				if (array_key_exists($expression, $expressions)
						&& array_key_exists($recovery_expression, $expressions[$expression])) {
					$error_already_exists = ($this instanceof CTrigger)
						? _('Trigger "%1$s" already exists on "%2$s".')
						: _('Trigger prototype "%1$s" already exists on "%2$s".');

					$db_hosts = DB::select('hosts', [
						'output' => ['name'],
						'hostids' => $expressions[$expression][$recovery_expression]
					]);

					self::exception(ZBX_API_ERROR_PARAMETERS,
						_params($error_already_exists, [$description, $db_hosts[0]['name']])
					);
				}
			}
		}
	}

	protected function addRelatedObjects(array $options, array $result) {
		$result = parent::addRelatedObjects($options, $result);

		$triggerids = array_keys($result);

		$this->addRelatedHostGroups($options, $result);
		$this->addRelatedTemplateGroups($options, $result);

		// adding hosts
		if ($options['selectHosts'] !== null && $options['selectHosts'] != API_OUTPUT_COUNT) {
			$res = DBselect(
				'SELECT f.triggerid,i.hostid'.
					' FROM functions f,items i'.
					' WHERE '.dbConditionInt('f.triggerid', $triggerids).
					' AND f.itemid=i.itemid'
			);
			$relationMap = new CRelationMap();
			while ($relation = DBfetch($res)) {
				$relationMap->addRelation($relation['triggerid'], $relation['hostid']);
			}

			$hosts = API::Host()->get([
				'output' => $options['selectHosts'],
				'hostids' => $relationMap->getRelatedIds(),
				'templated_hosts' => true,
				'nopermissions' => true,
				'preservekeys' => true
			]);
			if (!is_null($options['limitSelects'])) {
				order_result($hosts, 'host');
			}
			$result = $relationMap->mapMany($result, $hosts, 'hosts', $options['limitSelects']);
		}

		// adding functions
		if ($options['selectFunctions'] !== null && $options['selectFunctions'] != API_OUTPUT_COUNT) {
			$functions = API::getApiService()->select('functions', [
				'output' => $this->outputExtend($options['selectFunctions'], ['triggerid', 'functionid']),
				'filter' => ['triggerid' => $triggerids],
				'preservekeys' => true
			]);

			// Rename column 'name' to 'function'.
			$function = reset($functions);
			if ($function && array_key_exists('name', $function)) {
				$functions = CArrayHelper::renameObjectsKeys($functions, ['name' => 'function']);
			}

			$relationMap = $this->createRelationMap($functions, 'triggerid', 'functionid');

			$functions = $this->unsetExtraFields($functions, ['triggerid', 'functionid'], $options['selectFunctions']);
			$result = $relationMap->mapMany($result, $functions, 'functions');
		}

		// Adding trigger tags.
		if ($options['selectTags'] !== null && $options['selectTags'] != API_OUTPUT_COUNT) {
			$tags = API::getApiService()->select('trigger_tag', [
				'output' => $this->outputExtend($options['selectTags'], ['triggerid']),
				'filter' => ['triggerid' => $triggerids],
				'preservekeys' => true
			]);

			$relationMap = $this->createRelationMap($tags, 'triggerid', 'triggertagid');
			$tags = $this->unsetExtraFields($tags, ['triggertagid', 'triggerid'], []);
			$result = $relationMap->mapMany($result, $tags, 'tags');
		}

		return $result;
	}

	private function addRelatedHostGroups(array $options, array &$result): void {
		if ($options['selectHostGroups'] === null || $options['selectHostGroups'] === API_OUTPUT_COUNT) {
			return;
		}

		$resource = DBselect(
			'SELECT f.triggerid,hg.groupid'.
			' FROM functions f'.
			' JOIN items i ON f.itemid=i.itemid'.
			' JOIN hosts_groups hg ON i.hostid=hg.hostid'.
			' JOIN hstgrp hgg ON hg.groupid=hgg.groupid'.
				' AND '.dbConditionInt('hgg.type', [HOST_GROUP_TYPE_HOST_GROUP]).
			' WHERE '.dbConditionId('f.triggerid', array_keys($result))
		);
		$relation_map = new CRelationMap();

		while ($relation = DBfetch($resource)) {
			$relation_map->addRelation($relation['triggerid'], $relation['groupid']);
		}

		$related_ids = $relation_map->getRelatedIds();
		$groups = $related_ids
			? API::HostGroup()->get([
				'output' => $options['selectHostGroups'],
				'groupids' => $related_ids,
				'preservekeys' => true
			])
			: [];

		$result = $relation_map->mapMany($result, $groups, 'hostgroups');
	}

	private function addRelatedTemplateGroups(array $options, array &$result): void {
		if ($options['selectTemplateGroups'] === null || $options['selectTemplateGroups'] === API_OUTPUT_COUNT) {
			return;
		}

		$resource = DBselect(
			'SELECT f.triggerid,hg.groupid'.
			' FROM functions f'.
			' JOIN items i ON f.itemid=i.itemid'.
			' JOIN hosts_groups hg ON i.hostid=hg.hostid'.
			' JOIN hstgrp hgg ON hg.groupid=hgg.groupid'.
				' AND '.dbConditionInt('hgg.type', [HOST_GROUP_TYPE_TEMPLATE_GROUP]).
			' WHERE '.dbConditionId('f.triggerid', array_keys($result))
		);
		$relation_map = new CRelationMap();

		while ($relation = DBfetch($resource)) {
			$relation_map->addRelation($relation['triggerid'], $relation['groupid']);
		}

		$related_ids = $relation_map->getRelatedIds();
		$groups = $related_ids
			? API::TemplateGroup()->get([
				'output' => $options['selectTemplateGroups'],
				'groupids' => $related_ids,
				'preservekeys' => true
			])
			: [];

		$result = $relation_map->mapMany($result, $groups, 'templategroups');
	}

	/**
	 * Validate integrity of trigger recovery properties.
	 *
	 * @param array  $trigger
	 * @param int    $trigger['recovery_mode']
	 * @param string $trigger['recovery_expression']
	 *
	 * @throws APIException if validation failed.
	 */
	private static function checkTriggerRecoveryMode(array $trigger) {
		if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
			if ($trigger['recovery_expression'] === '') {
				self::exception(ZBX_API_ERROR_PARAMETERS,
					_s('Incorrect value for field "%1$s": %2$s.', 'recovery_expression', _('cannot be empty'))
				);
			}
		}
		elseif ($trigger['recovery_expression'] !== '') {
			self::exception(ZBX_API_ERROR_PARAMETERS,
				_s('Incorrect value for field "%1$s": %2$s.', 'recovery_expression', _('should be empty'))
			);
		}
	}

	/**
	 * Validate trigger correlation mode and related properties.
	 *
	 * @param array  $trigger
	 * @param int    $trigger['correlation_mode']
	 * @param string $trigger['correlation_tag']
	 * @param int    $trigger['recovery_mode']
	 *
	 * @throws APIException if validation failed.
	 */
	private static function checkTriggerCorrelationMode(array $trigger) {
		if ($trigger['correlation_mode'] == ZBX_TRIGGER_CORRELATION_TAG) {
			if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_NONE) {
				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
					'correlation_mode', _s('unexpected value "%1$s"', $trigger['correlation_mode'])
				));
			}

			if ($trigger['correlation_tag'] === '') {
				self::exception(ZBX_API_ERROR_PARAMETERS,
					_s('Incorrect value for field "%1$s": %2$s.', 'correlation_tag', _('cannot be empty'))
				);
			}
		}
		elseif ($trigger['correlation_tag'] !== '') {
			self::exception(ZBX_API_ERROR_PARAMETERS,
				_s('Incorrect value for field "%1$s": %2$s.', 'correlation_tag', _('should be empty'))
			);
		}
	}

	/**
	 * Validate trigger to be created.
	 *
	 * @param array  $triggers                                   [IN/OUT]
	 * @param array  $triggers[]['description']                  [IN]
	 * @param string $triggers[]['expression']                   [IN]
	 * @param string $triggers[]['opdata']                       [IN]
	 * @param string $triggers[]['event_name']                   [IN]
	 * @param string $triggers[]['comments']                     [IN] (optional)
	 * @param int    $triggers[]['priority']                     [IN] (optional)
	 * @param int    $triggers[]['status']                       [IN] (optional)
	 * @param int    $triggers[]['type']                         [IN] (optional)
	 * @param string $triggers[]['url_name']                     [IN] (optional)
	 * @param string $triggers[]['url']                          [IN] (optional)
	 * @param int    $triggers[]['recovery_mode']                [IN/OUT] (optional)
	 * @param string $triggers[]['recovery_expression']          [IN/OUT] (optional)
	 * @param int    $triggers[]['correlation_mode']             [IN/OUT] (optional)
	 * @param string $triggers[]['correlation_tag']              [IN/OUT] (optional)
	 * @param int    $triggers[]['manual_close']                 [IN] (optional)
	 * @param int    $triggers[]['discover']                     [IN] (optional) for trigger prototypes only
	 * @param array  $triggers[]['tags']                         [IN] (optional)
	 * @param string $triggers[]['tags'][]['tag']                [IN]
	 * @param string $triggers[]['tags'][]['value']              [IN/OUT] (optional)
	 * @param array  $triggers[]['dependencies']                 [IN] (optional)
	 * @param string $triggers[]['dependencies'][]['triggerid']  [IN]
	 *
	 * @throws APIException if validation failed.
	 */
	protected function validateCreate(array &$triggers) {
		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['description', 'expression']], 'fields' => [
			'uuid' =>					['type' => API_ANY],
			'description' =>			['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('triggers', 'description')],
			'expression' =>				['type' => API_TRIGGER_EXPRESSION, 'flags' => API_REQUIRED | API_NOT_EMPTY | API_ALLOW_LLD_MACRO],
			'event_name' =>				['type' => API_EVENT_NAME, 'length' => DB::getFieldLength('triggers', 'event_name')],
			'opdata' =>					['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'opdata')],
			'comments' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'comments')],
			'priority' =>				['type' => API_INT32, 'in' => implode(',', range(TRIGGER_SEVERITY_NOT_CLASSIFIED, TRIGGER_SEVERITY_COUNT - 1))],
			'status' =>					['type' => API_INT32, 'in' => implode(',', [TRIGGER_STATUS_ENABLED, TRIGGER_STATUS_DISABLED])],
			'type' =>					['type' => API_INT32, 'in' => implode(',', [TRIGGER_MULT_EVENT_DISABLED, TRIGGER_MULT_EVENT_ENABLED])],
			'url_name' =>				['type' => API_STRING_UTF8, 'flags' => API_ALLOW_USER_MACRO, 'length' => DB::getFieldLength('triggers', 'url_name')],
			'url' =>					['type' => API_URL, 'flags' => API_ALLOW_USER_MACRO, 'length' => DB::getFieldLength('triggers', 'url')],
			'recovery_mode' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_RECOVERY_MODE_EXPRESSION, ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION, ZBX_RECOVERY_MODE_NONE]), 'default' => DB::getDefault('triggers', 'recovery_mode')],
			'recovery_expression' =>	['type' => API_TRIGGER_EXPRESSION, 'flags' => API_ALLOW_LLD_MACRO, 'default' => DB::getDefault('triggers', 'recovery_expression')],
			'correlation_mode' =>		['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_CORRELATION_NONE, ZBX_TRIGGER_CORRELATION_TAG]), 'default' => DB::getDefault('triggers', 'correlation_mode')],
			'correlation_tag' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'correlation_tag'), 'default' => DB::getDefault('triggers', 'correlation_tag')],
			'manual_close' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_MANUAL_CLOSE_NOT_ALLOWED, ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED])],
			'tags' =>					['type' => API_OBJECTS, 'uniq' => [['tag', 'value']], 'fields' => [
				'tag' =>					['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('trigger_tag', 'tag')],
				'value' =>					['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('trigger_tag', 'value'), 'default' => DB::getDefault('trigger_tag', 'value')]
			]],
			'dependencies' =>			['type' => API_OBJECTS, 'uniq' => [['triggerid']], 'fields'=> [
				'triggerid' =>				['type' => API_ID, 'flags' => API_REQUIRED]
			]]
		]];
		if ($this instanceof CTriggerPrototype) {
			$api_input_rules['fields']['discover'] = ['type' => API_INT32, 'in' => implode(',', [TRIGGER_DISCOVER, TRIGGER_NO_DISCOVER])];
		}
		else {
			$api_input_rules['fields']['expression']['flags'] &= ~API_ALLOW_LLD_MACRO;
			$api_input_rules['fields']['recovery_expression']['flags'] &= ~API_ALLOW_LLD_MACRO;
		}
		if (!CApiInputValidator::validate($api_input_rules, $triggers, '/', $error)) {
			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
		}

		$descriptions = [];
		foreach ($triggers as $index => $trigger) {
			self::checkTriggerRecoveryMode($trigger);
			self::checkTriggerCorrelationMode($trigger);

			$descriptions[$trigger['description']][] = [
				'index' => $index,
				'expression' => $trigger['expression'],
				'recovery_expression' => $trigger['recovery_expression']
			];
		}

		$descriptions = $this->populateHostIds($descriptions);

		self::validateUuid($triggers, $descriptions);

		self::addUuid($triggers, $descriptions);

		self::checkUuidDuplicates($triggers);
		$this->checkDuplicates($descriptions);
		$this->checkDependencies($triggers);
	}

	/**
	 * Validate trigger to be updated.
	 *
	 * @param array  $triggers                                   [IN/OUT]
	 * @param array  $triggers[]['triggerid']                    [IN]
	 * @param array  $triggers[]['description']                  [IN/OUT] (optional)
	 * @param string $triggers[]['expression']                   [IN/OUT] (optional)
	 * @param string $triggers[]['event_name']                   [IN] (optional)
	 * @param string $triggers[]['opdata']                       [IN] (optional)
	 * @param string $triggers[]['comments']                     [IN] (optional)
	 * @param int    $triggers[]['priority']                     [IN] (optional)
	 * @param int    $triggers[]['status']                       [IN] (optional)
	 * @param int    $triggers[]['type']                         [IN] (optional)
	 * @param string $triggers[]['url_name']                     [IN] (optional)
	 * @param string $triggers[]['url']                          [IN] (optional)
	 * @param int    $triggers[]['recovery_mode']                [IN/OUT] (optional)
	 * @param string $triggers[]['recovery_expression']          [IN/OUT] (optional)
	 * @param int    $triggers[]['correlation_mode']             [IN/OUT] (optional)
	 * @param string $triggers[]['correlation_tag']              [IN/OUT] (optional)
	 * @param int    $triggers[]['manual_close']                 [IN] (optional)
	 * @param int    $triggers[]['discover']                     [IN] (optional) for trigger prototypes only
	 * @param array  $triggers[]['tags']                         [IN] (optional)
	 * @param string $triggers[]['tags'][]['tag']                [IN]
	 * @param string $triggers[]['tags'][]['value']              [IN/OUT] (optional)
	 * @param array  $triggers[]['dependencies']                 [IN] (optional)
	 * @param string $triggers[]['dependencies'][]['triggerid']  [IN]
	 * @param array  $db_triggers                                [OUT]
	 * @param array  $db_triggers[<tnum>]['triggerid']           [OUT]
	 * @param array  $db_triggers[<tnum>]['description']         [OUT]
	 * @param string $db_triggers[<tnum>]['expression']          [OUT]
	 * @param string $db_triggers[<tnum>]['event_name']          [OUT]
	 * @param string $db_triggers[<tnum>]['opdata']              [OUT]
	 * @param int    $db_triggers[<tnum>]['recovery_mode']       [OUT]
	 * @param string $db_triggers[<tnum>]['recovery_expression'] [OUT]
	 * @param string $db_triggers[<tnum>]['url_name']            [OUT]
	 * @param string $db_triggers[<tnum>]['url']                 [OUT]
	 * @param int    $db_triggers[<tnum>]['status']              [OUT]
	 * @param int    $db_triggers[<tnum>]['discover']            [OUT]
	 * @param int    $db_triggers[<tnum>]['priority']            [OUT]
	 * @param string $db_triggers[<tnum>]['comments']            [OUT]
	 * @param int    $db_triggers[<tnum>]['type']                [OUT]
	 * @param string $db_triggers[<tnum>]['templateid']          [OUT]
	 * @param int    $db_triggers[<tnum>]['correlation_mode']    [OUT]
	 * @param string $db_triggers[<tnum>]['correlation_tag']     [OUT]
	 * @param int    $db_triggers[<tnum>]['discover']            [OUT] for trigger prototypes only
	 *
	 * @throws APIException if validation failed.
	 */
	protected function validateUpdate(array &$triggers, array &$db_triggers = null) {
		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['description', 'expression']], 'fields' => [
			'uuid' => 					['type' => API_ANY],
			'triggerid' =>				['type' => API_ID, 'flags' => API_REQUIRED],
			'description' =>			['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('triggers', 'description')],
			'expression' =>				['type' => API_TRIGGER_EXPRESSION, 'flags' => API_NOT_EMPTY | API_ALLOW_LLD_MACRO],
			'event_name' =>				['type' => API_EVENT_NAME, 'length' => DB::getFieldLength('triggers', 'event_name')],
			'opdata' =>					['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'opdata')],
			'comments' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'comments')],
			'priority' =>				['type' => API_INT32, 'in' => implode(',', range(TRIGGER_SEVERITY_NOT_CLASSIFIED, TRIGGER_SEVERITY_COUNT - 1))],
			'status' =>					['type' => API_INT32, 'in' => implode(',', [TRIGGER_STATUS_ENABLED, TRIGGER_STATUS_DISABLED])],
			'type' =>					['type' => API_INT32, 'in' => implode(',', [TRIGGER_MULT_EVENT_DISABLED, TRIGGER_MULT_EVENT_ENABLED])],
			'url_name' =>				['type' => API_STRING_UTF8, 'flags' => API_ALLOW_USER_MACRO, 'length' => DB::getFieldLength('triggers', 'url_name')],
			'url' =>					['type' => API_URL, 'flags' => API_ALLOW_USER_MACRO, 'length' => DB::getFieldLength('triggers', 'url')],
			'recovery_mode' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_RECOVERY_MODE_EXPRESSION, ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION, ZBX_RECOVERY_MODE_NONE])],
			'recovery_expression' =>	['type' => API_TRIGGER_EXPRESSION, 'flags' => API_ALLOW_LLD_MACRO],
			'correlation_mode' =>		['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_CORRELATION_NONE, ZBX_TRIGGER_CORRELATION_TAG])],
			'correlation_tag' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'correlation_tag')],
			'manual_close' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_MANUAL_CLOSE_NOT_ALLOWED, ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED])],
			'tags' =>					['type' => API_OBJECTS, 'uniq' => [['tag', 'value']], 'fields' => [
				'tag' =>					['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('trigger_tag', 'tag')],
				'value' =>					['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('trigger_tag', 'value'), 'default' => DB::getDefault('trigger_tag', 'value')]
			]],
			'dependencies' =>			['type' => API_OBJECTS, 'uniq' => [['triggerid']], 'fields'=> [
				'triggerid' =>				['type' => API_ID, 'flags' => API_REQUIRED]
			]]
		]];

		if ($this instanceof CTriggerPrototype) {
			$api_input_rules['fields']['discover'] = ['type' => API_INT32, 'in' => implode(',', [TRIGGER_DISCOVER, TRIGGER_NO_DISCOVER])];
		}
		else {
			$api_input_rules['fields']['expression']['flags'] &= ~API_ALLOW_LLD_MACRO;
			$api_input_rules['fields']['recovery_expression']['flags'] &= ~API_ALLOW_LLD_MACRO;
		}
		if (!CApiInputValidator::validate($api_input_rules, $triggers, '/', $error)) {
			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
		}

		$options = [
			'output' => ['uuid', 'triggerid', 'description', 'expression', 'url_name', 'url', 'status', 'priority',
				'comments', 'type', 'templateid', 'recovery_mode', 'recovery_expression', 'correlation_mode',
				'correlation_tag', 'manual_close', 'opdata', 'event_name'
			],
			'triggerids' => zbx_objectValues($triggers, 'triggerid'),
			'editable' => true,
			'preservekeys' => true
		];

		$class = get_class($this);

		switch ($class) {
			case 'CTrigger':
				$error_cannot_update = _('Cannot update "%1$s" for templated trigger "%2$s".');
				$options['output'][] = 'flags';

				// Discovered fields, except status, cannot be updated.
				$update_discovered_validator = new CUpdateDiscoveredValidator([
					'allowed' => ['triggerid', 'status'],
					'messageAllowedField' => _('Cannot update "%2$s" for a discovered trigger "%1$s".')
				]);
				break;

			case 'CTriggerPrototype':
				$error_cannot_update = _('Cannot update "%1$s" for templated trigger prototype "%2$s".');
				$options['output'][] = 'discover';
				$options['selectDiscoveryRule'] = ['itemid'];
				break;

			default:
				self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.'));
		}

		$db_triggers = $this->get($options);

		if (count($db_triggers) != count($triggers)) {
			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
		}

		$db_triggers = CMacrosResolverHelper::resolveTriggerExpressions($db_triggers, [
			'sources' => ['expression', 'recovery_expression']
		]);

		$this->addAffectedObjects($triggers, $db_triggers);

		$read_only_fields = ['uuid', 'description', 'expression', 'recovery_mode', 'recovery_expression',
			'correlation_mode', 'correlation_tag', 'manual_close'
		];

		$descriptions = [];

		foreach ($triggers as $index => &$trigger) {
			$db_trigger = $db_triggers[$trigger['triggerid']];
			$description = array_key_exists('description', $trigger)
				? $trigger['description']
				: $db_trigger['description'];

			if ($class === 'CTrigger') {
				$update_discovered_validator->setObjectName($description);
				$this->checkPartialValidator($trigger, $update_discovered_validator, $db_trigger);
			}

			if ($db_trigger['templateid'] != 0) {
				$this->checkNoParameters($trigger, $read_only_fields, $error_cannot_update, $description);
			}

			$field_names = ['description', 'expression', 'recovery_mode', 'manual_close'];
			foreach ($field_names as $field_name) {
				if (!array_key_exists($field_name, $trigger)) {
					$trigger[$field_name] = $db_trigger[$field_name];
				}
			}

			if (!array_key_exists('recovery_expression', $trigger)) {
				$trigger['recovery_expression'] = ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION)
					? $db_trigger['recovery_expression']
					: '';
			}
			if (!array_key_exists('correlation_mode', $trigger)) {
				$trigger['correlation_mode'] = ($trigger['recovery_mode'] != ZBX_RECOVERY_MODE_NONE)
					? $db_trigger['correlation_mode']
					: ZBX_TRIGGER_CORRELATION_NONE;
			}
			if (!array_key_exists('correlation_tag', $trigger)) {
				$trigger['correlation_tag'] = ($trigger['correlation_mode'] == ZBX_TRIGGER_CORRELATION_TAG)
					? $db_trigger['correlation_tag']
					: '';
			}

			self::checkTriggerRecoveryMode($trigger);
			self::checkTriggerCorrelationMode($trigger);

			$name_updated = $trigger['expression'] !== $db_trigger['expression']
				|| $trigger['recovery_expression'] !== $db_trigger['recovery_expression']
				|| $trigger['description'] !== $db_trigger['description'];

			if ($name_updated || array_key_exists('uuid', $trigger)) {
				$descriptions[$trigger['description']][] = [
					'index' => $index,
					'expression' => $trigger['expression'],
					'recovery_expression' => $trigger['recovery_expression'],
					'name_updated' => $name_updated
				];
			}
		}
		unset($trigger);

		if ($descriptions) {
			$descriptions = $this->populateHostIds($descriptions);

			self::validateUuid($triggers, $descriptions);

			self::checkUuidDuplicates($triggers, $db_triggers);
			$this->checkDuplicates($descriptions);
		}

		$this->checkDependencies($triggers, $db_triggers);
		$this->checkDependenciesLinks($triggers, $db_triggers);
	}

	/**
	 * @param array $triggers
	 * @param array $db_triggers
	 */
	private function addAffectedObjects(array $triggers, array &$db_triggers): void {
		if ($this instanceof CTrigger) {
			self::addAffectedHosts($triggers, $db_triggers);
		}

		self::addAffectedTags($triggers, $db_triggers);
		self::addAffectedDependencies($triggers, $db_triggers);
	}

	/**
	 * @param array $triggers
	 * @param array $db_triggers
	 */
	protected static function addAffectedHosts(array $triggers, array &$db_triggers): void {
		$triggerids = [];

		foreach ($triggers as $trigger) {
			$db_trigger = $db_triggers[$trigger['triggerid']];

			if ((array_key_exists('expression', $trigger) && $trigger['expression'] !== $db_trigger['expression'])
					|| (array_key_exists('recovery_expression', $trigger)
						&& $trigger['recovery_expression'] !== $db_trigger['recovery_expression'])) {
				$triggerids[] = $trigger['triggerid'];
				$db_triggers[$trigger['triggerid']]['hosts'] = [];
			}
		}

		if (!$triggerids) {
			return;
		}

		$result = DBselect(
			'SELECT DISTINCT f.triggerid,i.hostid'.
			' FROM functions f,items i'.
			' WHERE f.itemid=i.itemid'.
				' AND '.dbConditionId('f.triggerid', $triggerids)
		);

		while ($row = DBfetch($result)) {
			$db_triggers[$row['triggerid']]['hosts'][$row['hostid']] = true;
		}
	}

	/**
	 * @param array $triggers
	 * @param array $db_triggers
	 */
	private static function addAffectedTags(array $triggers, array &$db_triggers): void {
		$triggerids = [];

		foreach ($triggers as $trigger) {
			if (array_key_exists('tags', $trigger)) {
				$triggerids[] = $trigger['triggerid'];
				$db_triggers[$trigger['triggerid']]['tags'] = [];
			}
		}

		if (!$triggerids) {
			return;
		}

		$options = [
			'output' => ['triggertagid', 'triggerid', 'tag', 'value'],
			'filter' => ['triggerid' => $triggerids]
		];
		$db_tags = DBselect(DB::makeSql('trigger_tag', $options));

		while ($db_tag = DBfetch($db_tags)) {
			$db_triggers[$db_tag['triggerid']]['tags'][$db_tag['triggertagid']] =
				array_diff_key($db_tag, array_flip(['triggerid']));
		}
	}

	/**
	 * @param array $triggers
	 * @param array $db_triggers
	 */
	protected static function addAffectedDependencies(array $triggers, array &$db_triggers): void {
		$triggerids = [];

		foreach ($triggers as $trigger) {
			$db_trigger = $db_triggers[$trigger['triggerid']];

			if (array_key_exists('dependencies', $trigger) || array_key_exists('hosts', $db_trigger)) {
				$triggerids[] = $trigger['triggerid'];
				$db_triggers[$trigger['triggerid']]['dependencies'] = [];
			}
		}

		if (!$triggerids) {
			return;
		}

		$options = [
			'output' => ['triggerdepid', 'triggerid_down', 'triggerid_up'],
			'filter' => ['triggerid_down' => $triggerids]
		];
		$db_deps = DBselect(DB::makeSql('trigger_depends', $options));

		while ($db_dep = DBfetch($db_deps)) {
			$db_triggers[$db_dep['triggerid_down']]['dependencies'][$db_dep['triggerdepid']] = [
				'triggerdepid' => $db_dep['triggerdepid'],
				'triggerid' => $db_dep['triggerid_up']
			];
		}
	}

	/**
	 * Check trigger dependencies of the given triggers.
	 *
	 * @param array      $triggers
	 * @param array|null $db_triggers
	 *
	 * @throws APIException
	 */
	private function checkDependencies(array $triggers, array $db_triggers = null): void {
		$edit_triggerids_up = [];

		foreach ($triggers as $trigger) {
			if (!array_key_exists('dependencies', $trigger)) {
				continue;
			}

			$triggerids_up = array_column($trigger['dependencies'], 'triggerid');

			if ($db_triggers === null) {
				$edit_triggerids_up += array_flip($triggerids_up);
			}
			else {
				$db_triggerids_up = array_column($db_triggers[$trigger['triggerid']]['dependencies'], 'triggerid');

				$ins_triggerids_up = array_flip(array_diff($triggerids_up, $db_triggerids_up));
				$del_triggerids_up = array_flip(array_diff($db_triggerids_up, $triggerids_up));

				$edit_triggerids_up += $ins_triggerids_up + $del_triggerids_up;
			}
		}

		if (!$edit_triggerids_up) {
			return;
		}

		$count = API::Trigger()->get([
			'countOutput' => true,
			'triggerids' => array_keys($edit_triggerids_up)
		]);

		if ($this instanceof CTriggerPrototype) {
			$trigger_prototypes_up = API::TriggerPrototype()->get([
				'output' => [],
				'triggerids' => array_keys($edit_triggerids_up),
				'preservekeys' => true
			]);

			$count += count($trigger_prototypes_up);
		}

		if ($count != count($edit_triggerids_up)) {
			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
		}

		if ($this instanceof CTriggerPrototype) {
			$ins_dependencies = [];
			$triggerids = [];

			foreach ($triggers as $trigger) {
				if (!array_key_exists('dependencies', $triggers)) {
					continue;
				}

				$db_triggers_up = ($db_triggers !== null)
					? array_column($db_triggers[$trigger['triggerid']]['dependencies'], null, 'triggerid')
					: [];

				foreach ($trigger['dependencies'] as $trigger_up) {
					if (!array_key_exists($trigger_up['triggerid'], $db_triggers_up)
							&& array_key_exists($trigger_up['triggerid'], $trigger_prototypes_up)) {
						$ins_dependencies[$trigger_up['triggerid']][$trigger['triggerid']] = true;
						$triggerids[$trigger['triggerid']] = true;
					}
				}
			}

			if (!$ins_dependencies) {
				return;
			}

			$result = DBselect(
				'SELECT DISTINCT f.triggerid,id.parent_itemid'.
				' FROM functions f,item_discovery id'.
				' WHERE f.itemid=id.itemid'.
					' AND '.dbConditionId('f.triggerid', array_keys($ins_dependencies + $triggerids))
			);

			$lld_rules = [];

			while ($row = DBfetch($result)) {
				$lld_rules[$row['triggerid']] = $row['parent_itemid'];
			}

			foreach ($ins_dependencies as $triggerid_up => $triggerids) {
				foreach ($triggerids as $triggerid) {
					if (bccomp($lld_rules[$triggerid_up], $lld_rules[$triggerid]) != 0) {
						self::exception(ZBX_API_ERROR_PARAMETERS,
							_('Trigger prototype "%1$s" cannot depend on the trigger prototype "%2$s", because dependencies on trigger prototypes from another LLD rule are not allowed.')
						);
					}
				}
			}
		}
	}

	/**
	 * Check linkage of dependencies.
	 *
	 * @param array      $triggers
	 * @param array|null $db_triggers
	 */
	protected function checkDependenciesLinks(array $triggers, array $db_triggers = null): void {
		$ins_dependencies = [];
		$del_dependencies = [];

		foreach ($triggers as $trigger) {
			if (!array_key_exists('dependencies', $trigger)) {
				continue;
			}

			$db_triggers_up = ($db_triggers !== null)
				? array_column($db_triggers[$trigger['triggerid']]['dependencies'], null, 'triggerid')
				: [];

			foreach ($trigger['dependencies'] as $trigger_up) {
				if (array_key_exists($trigger_up['triggerid'], $db_triggers_up)) {
					unset($db_triggers_up[$trigger_up['triggerid']]);
				}
				else {
					$ins_dependencies[$trigger_up['triggerid']][$trigger['triggerid']] = true;
				}
			}

			foreach ($db_triggers_up as $db_trigger_up) {
				$del_dependencies[$db_trigger_up['triggerid']][$trigger['triggerid']] = true;
			}
		}

		if ($ins_dependencies) {
			if ($this instanceof CTriggerPrototype) {
				$options = [
					'output' => ['triggerid', 'flags'],
					'triggerids' => array_keys($ins_dependencies)
				];
				$result = DBselect(DB::makeSql('triggers', $options));

				$ins_dependencies_prototypes = [];

				while ($row = DBfetch($result)) {
					if ($row['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {

						/*
						 * Trigger cannot depend on trigger prototypes, so it's enough to check circular dependencies
						 * only among the trigger prototypes.
						 */
						$ins_dependencies_prototypes[$row['triggerid']] = $ins_dependencies[$row['triggerid']];

						/*
						 * Since the trigger prototypes can depend only on trigger prototypes from the same LLD rule,
						 * for such dependencies it is not necessary to check the dependencies of host and template
						 * triggers. Only dependencies on triggers are required to be checked.
						 */
						unset($ins_dependencies[$row['triggerid']]);
					}
				}
			}

			if ($db_triggers !== null) {
				if ($this instanceof CTriggerPrototype && $ins_dependencies_prototypes) {
					self::checkCircularDependencies($ins_dependencies_prototypes, $del_dependencies);
				}
				else {
					self::checkCircularDependencies($ins_dependencies, $del_dependencies);
				}
			}

			$trigger_hosts = self::getTriggerHosts($ins_dependencies);

			self::checkDependenciesOfHostTriggers($ins_dependencies, $trigger_hosts);
			self::checkDependenciesOfTemplateTriggers($ins_dependencies, $trigger_hosts);
		}
	}

	/**
	 * Inserts trigger or trigger prototypes records into the database.
	 *
	 * @param array  $triggers                          [IN/OUT]
	 * @param array  $triggers[]['triggerid']           [OUT]
	 * @param array  $triggers[]['description']         [IN]
	 * @param string $triggers[]['expression']          [IN]
	 * @param int    $triggers[]['recovery_mode']       [IN]
	 * @param string $triggers[]['recovery_expression'] [IN]
	 * @param string $triggers[]['url_name']            [IN] (optional)
	 * @param string $triggers[]['url']                 [IN] (optional)
	 * @param int    $triggers[]['status']              [IN] (optional)
	 * @param int    $triggers[]['priority']            [IN] (optional)
	 * @param string $triggers[]['comments']            [IN] (optional)
	 * @param int    $triggers[]['type']                [IN] (optional)
	 * @param string $triggers[]['templateid']          [IN] (optional)
	 * @param array  $triggers[]['tags']                [IN] (optional)
	 * @param string $triggers[]['tags'][]['tag']       [IN]
	 * @param string $triggers[]['tags'][]['value']     [IN]
	 * @param int    $triggers[]['correlation_mode']    [IN] (optional)
	 * @param string $triggers[]['correlation_tag']     [IN] (optional)
	 * @param bool   $inherited                         [IN] (optional)  If set to true, trigger will be created for
	 *                                                                   non-editable host/template.
	 *
	 * @throws APIException
	 */
	protected function createReal(array &$triggers, $inherited = false) {
		$new_triggers = $triggers;
		$new_functions = [];
		$triggers_functions = [];
		$new_tags = [];
		$this->implode_expressions($new_triggers, null, $triggers_functions, $inherited);

		$triggerid = DB::reserveIds('triggers', count($new_triggers));

		foreach ($new_triggers as $tnum => &$new_trigger) {
			$new_trigger['triggerid'] = $triggerid;
			$triggers[$tnum]['triggerid'] = $triggerid;

			foreach ($triggers_functions[$tnum] as $trigger_function) {
				$trigger_function['triggerid'] = $triggerid;
				$new_functions[] = $trigger_function;
			}

			if ($this instanceof CTriggerPrototype) {
				$new_trigger['flags'] = ZBX_FLAG_DISCOVERY_PROTOTYPE;
			}

			if (array_key_exists('tags', $new_trigger)) {
				foreach ($new_trigger['tags'] as $tag) {
					$tag['triggerid'] = $triggerid;
					$new_tags[] = $tag;
				}
			}

			$triggerid = bcadd($triggerid, 1, 0);
		}
		unset($new_trigger);

		DB::insert('triggers', $new_triggers, false);
		DB::insertBatch('functions', $new_functions, false);

		if ($new_tags) {
			DB::insert('trigger_tag', $new_tags);
		}

		if (!$inherited) {
			$resource = ($this instanceof CTrigger) ? CAudit::RESOURCE_TRIGGER : CAudit::RESOURCE_TRIGGER_PROTOTYPE;
			$this->addAuditBulk(CAudit::ACTION_ADD, $resource, $triggers);
		}
	}

	/**
	 * Update trigger or trigger prototypes records in the database.
	 *
	 * @param array  $triggers                                       [IN] list of triggers to be updated
	 * @param array  $triggers[<tnum>]['triggerid']                  [IN]
	 * @param array  $triggers[<tnum>]['description']                [IN]
	 * @param string $triggers[<tnum>]['expression']                 [IN]
	 * @param int    $triggers[<tnum>]['recovery_mode']              [IN]
	 * @param string $triggers[<tnum>]['recovery_expression']        [IN]
	 * @param string $triggers[<tnum>]['url_name']                   [IN] (optional)
	 * @param string $triggers[<tnum>]['url']                        [IN] (optional)
	 * @param int    $triggers[<tnum>]['status']                     [IN] (optional)
	 * @param int    $triggers[<tnum>]['priority']                   [IN] (optional)
	 * @param string $triggers[<tnum>]['comments']                   [IN] (optional)
	 * @param int    $triggers[<tnum>]['type']                       [IN] (optional)
	 * @param string $triggers[<tnum>]['templateid']                 [IN] (optional)
	 * @param array  $triggers[<tnum>]['tags']                       [IN]
	 * @param string $triggers[<tnum>]['tags'][]['tag']              [IN]
	 * @param string $triggers[<tnum>]['tags'][]['value']            [IN]
	 * @param int    $triggers[<tnum>]['correlation_mode']           [IN]
	 * @param string $triggers[<tnum>]['correlation_tag']            [IN]
	 * @param array  $db_triggers                                    [IN]
	 * @param array  $db_triggers[<tnum>]['triggerid']               [IN]
	 * @param array  $db_triggers[<tnum>]['description']             [IN]
	 * @param string $db_triggers[<tnum>]['expression']              [IN]
	 * @param int    $db_triggers[<tnum>]['recovery_mode']           [IN]
	 * @param string $db_triggers[<tnum>]['recovery_expression']     [IN]
	 * @param string $db_triggers[<tnum>]['url_name']                [IN]
	 * @param string $db_triggers[<tnum>]['url']                     [IN]
	 * @param int    $db_triggers[<tnum>]['status']                  [IN]
	 * @param int    $db_triggers[<tnum>]['priority']                [IN]
	 * @param string $db_triggers[<tnum>]['comments']                [IN]
	 * @param int    $db_triggers[<tnum>]['type']                    [IN]
	 * @param string $db_triggers[<tnum>]['templateid']              [IN]
	 * @param array  $db_triggers[<tnum>]['discoveryRule']           [IN] For trigger prototypes only.
	 * @param string $db_triggers[<tnum>]['discoveryRule']['itemid'] [IN]
	 * @param array  $db_triggers[<tnum>]['tags']                    [IN]
	 * @param string $db_triggers[<tnum>]['tags'][]['tag']           [IN]
	 * @param string $db_triggers[<tnum>]['tags'][]['value']         [IN]
	 * @param int    $db_triggers[<tnum>]['correlation_mode']        [IN]
	 * @param string $db_triggers[<tnum>]['correlation_tag']         [IN]
	 * @param bool   $inherited                                      [IN] (optional)  If set to true, trigger will be
	 *                                                                                created for non-editable
	 *                                                                                host/template.
	 *
	 * @throws APIException
	 */
	protected function updateReal(array $triggers, array $db_triggers, $inherited = false) {
		$upd_triggers = [];
		$new_functions = [];
		$del_functions_triggerids = [];
		$triggers_functions = [];
		$new_tags = [];
		$del_triggertagids = [];
		$upd_discovered_triggers = [];
		$save_triggers = $triggers;
		$this->implode_expressions($triggers, $db_triggers, $triggers_functions, $inherited);

		foreach ($triggers as $tnum => $trigger) {
			$db_trigger = $db_triggers[$trigger['triggerid']];
			$upd_trigger = ['values' => [], 'where' => ['triggerid' => $trigger['triggerid']]];

			if (array_key_exists($tnum, $triggers_functions)) {
				$del_functions_triggerids[] = $trigger['triggerid'];

				foreach ($triggers_functions[$tnum] as $trigger_function) {
					$trigger_function['triggerid'] = $trigger['triggerid'];
					$new_functions[] = $trigger_function;
				}

				$upd_trigger['values']['expression'] = $trigger['expression'];
				$upd_trigger['values']['recovery_expression'] = $trigger['recovery_expression'];
			}

			if (array_key_exists('uuid', $trigger)) {
				$upd_trigger['values']['uuid'] = $trigger['uuid'];
			}
			if ($trigger['description'] !== $db_trigger['description']) {
				$upd_trigger['values']['description'] = $trigger['description'];
			}
			if (array_key_exists('event_name', $trigger) && $trigger['event_name'] !== $db_trigger['event_name']) {
				$upd_trigger['values']['event_name'] = $trigger['event_name'];
			}
			if (array_key_exists('opdata', $trigger) && $trigger['opdata'] !== $db_trigger['opdata']) {
				$upd_trigger['values']['opdata'] = $trigger['opdata'];
			}
			if ($trigger['recovery_mode'] != $db_trigger['recovery_mode']) {
				$upd_trigger['values']['recovery_mode'] = $trigger['recovery_mode'];
			}
			if (array_key_exists('url_name', $trigger) && $trigger['url_name'] !== $db_trigger['url_name']) {
				$upd_trigger['values']['url_name'] = $trigger['url_name'];
			}
			if (array_key_exists('url', $trigger) && $trigger['url'] !== $db_trigger['url']) {
				$upd_trigger['values']['url'] = $trigger['url'];
			}
			if (array_key_exists('status', $trigger) && $trigger['status'] != $db_trigger['status']) {
				$upd_trigger['values']['status'] = $trigger['status'];
			}
			if ($this instanceof CTriggerPrototype
					&& array_key_exists('discover', $trigger) && $trigger['discover'] != $db_trigger['discover']) {
				$upd_trigger['values']['discover'] = $trigger['discover'];
			}
			if (array_key_exists('priority', $trigger) && $trigger['priority'] != $db_trigger['priority']) {
				$upd_trigger['values']['priority'] = $trigger['priority'];
			}
			if (array_key_exists('comments', $trigger) && $trigger['comments'] !== $db_trigger['comments']) {
				$upd_trigger['values']['comments'] = $trigger['comments'];
			}
			if (array_key_exists('type', $trigger) && $trigger['type'] != $db_trigger['type']) {
				$upd_trigger['values']['type'] = $trigger['type'];
			}
			if (array_key_exists('templateid', $trigger) && $trigger['templateid'] != $db_trigger['templateid']) {
				$upd_trigger['values']['templateid'] = $trigger['templateid'];
			}
			if ($trigger['correlation_mode'] != $db_trigger['correlation_mode']) {
				$upd_trigger['values']['correlation_mode'] = $trigger['correlation_mode'];
			}
			if ($trigger['correlation_tag'] !== $db_trigger['correlation_tag']) {
				$upd_trigger['values']['correlation_tag'] = $trigger['correlation_tag'];
			}
			if ($trigger['manual_close'] != $db_trigger['manual_close']) {
				$upd_trigger['values']['manual_close'] = $trigger['manual_close'];
			}

			if ($upd_trigger['values']) {
				$upd_triggers[] = $upd_trigger;
			}

			if (array_key_exists('tags', $trigger)) {
				// Add new trigger tags and replace changed ones.

				CArrayHelper::sort($db_trigger['tags'], ['tag', 'value']);
				CArrayHelper::sort($trigger['tags'], ['tag', 'value']);

				$tags_delete = $db_trigger['tags'];
				$tags_add = $trigger['tags'];

				foreach ($tags_delete as $dt_key => $tag_delete) {
					foreach ($tags_add as $nt_key => $tag_add) {
						if ($tag_delete['tag'] === $tag_add['tag'] && $tag_delete['value'] === $tag_add['value']) {
							unset($tags_delete[$dt_key], $tags_add[$nt_key]);
							continue 2;
						}
					}
				}

				foreach ($tags_delete as $tag_delete) {
					$del_triggertagids[] = $tag_delete['triggertagid'];
				}

				foreach ($tags_add as $tag_add) {
					$tag_add['triggerid'] = $trigger['triggerid'];
					$new_tags[] = $tag_add;
				}
			}

			if (array_key_exists('flags', $db_trigger) && $db_trigger['flags'] == ZBX_FLAG_DISCOVERY_CREATED
					&& array_key_exists('status', $upd_trigger['values'])
					&& $upd_trigger['values']['status'] == TRIGGER_STATUS_DISABLED
					&& $upd_trigger['values']['status'] != $db_trigger['status']) {
				$upd_discovered_triggers[] = $db_trigger['triggerid'];
			}
		}

		if ($upd_triggers) {
			DB::update('triggers', $upd_triggers);
		}
		if ($del_functions_triggerids) {
			DB::delete('functions', ['triggerid' => $del_functions_triggerids]);
		}
		if ($new_functions) {
			DB::insertBatch('functions', $new_functions, false);
		}
		if ($del_triggertagids) {
			DB::delete('trigger_tag', ['triggertagid' => $del_triggertagids]);
		}
		if ($new_tags) {
			DB::insert('trigger_tag', $new_tags);
		}

		if ($upd_discovered_triggers) {
			DB::update('trigger_discovery', [
				'values' => ['disable_source' => ZBX_DISABLE_DEFAULT],
				'where' => ['triggerid' => $upd_discovered_triggers]
			]);
		}

		if (!$inherited) {
			$resource = ($this instanceof CTrigger) ? CAudit::RESOURCE_TRIGGER : CAudit::RESOURCE_TRIGGER_PROTOTYPE;
			$this->addAuditBulk(CAudit::ACTION_UPDATE, $resource, $save_triggers, $db_triggers);
		}
	}

	/**
	 * Implodes expression and recovery_expression for each trigger. Also returns array of functions and
	 * array of hostnames for each trigger.
	 *
	 * For example: last(/host/system.cpu.load)>10 will be translated to {12}>10 and created database representation.
	 *
	 * Note: All expressions must be already validated and exploded.
	 *
	 * @param array      $triggers                                   [IN]
	 * @param string     $triggers[<tnum>]['description']            [IN]
	 * @param string     $triggers[<tnum>]['expression']             [IN/OUT]
	 * @param int        $triggers[<tnum>]['recovery_mode']          [IN]
	 * @param string     $triggers[<tnum>]['recovery_expression']    [IN/OUT]
	 * @param array|null $db_triggers                                [IN]
	 * @param string     $db_triggers[<triggerid>]['triggerid']           [IN]
	 * @param string     $db_triggers[<triggerid>]['expression']          [IN]
	 * @param string     $db_triggers[<triggerid>]['recovery_expression'] [IN]
	 * @param array      $triggers_functions                         [OUT] array of the new functions which must be
	 *                                                                     inserted into DB
	 * @param string     $triggers_functions[<tnum>][]['functionid'] [OUT]
	 * @param null       $triggers_functions[<tnum>][]['triggerid']  [OUT] must be initialized before insertion into DB
	 * @param string     $triggers_functions[<tnum>][]['itemid']     [OUT]
	 * @param string     $triggers_functions[<tnum>][]['name']       [OUT]
	 * @param string     $triggers_functions[<tnum>][]['parameter']  [OUT]
	 * @param bool       $inherited                                  [IN] (optional)  If set to true, triggers will be
	 *                                                                                created for non-editable
	 *                                                                                hosts/templates.
	 *
	 * @throws APIException if error occurred
	 */
	private function implode_expressions(array &$triggers, ?array $db_triggers, array &$triggers_functions, $inherited) {
		$class = get_class($this);

		switch ($class) {
			case 'CTrigger':
				$expression_parser = new CExpressionParser(['usermacros' => true]);
				$error_wrong_host = _('Incorrect trigger expression. Host "%1$s" does not exist or you have no access to this host.');
				$error_host_and_template = _('Incorrect trigger expression. Trigger expression elements should not belong to a template and a host simultaneously.');
				break;

			case 'CTriggerPrototype':
				$expression_parser = new CExpressionParser(['usermacros' => true, 'lldmacros' => true]);
				$error_wrong_host = _('Incorrect trigger prototype expression. Host "%1$s" does not exist or you have no access to this host.');
				$error_host_and_template = _('Incorrect trigger prototype expression. Trigger prototype expression elements should not belong to a template and a host simultaneously.');
				break;

			default:
				self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.'));
		}

		$hist_function_value_types = (new CHistFunctionData())->getValueTypes();

		/*
		 * [
		 *     <host> => [
		 *         'hostid' => <hostid>,
		 *         'host' => <host>,
		 *         'status' => <status>,
		 *         'keys' => [
		 *             <key> => [
		 *                 'itemid' => <itemid>,
		 *                 'key' => <key>,
		 *                 'value_type' => <value_type>,
		 *                 'flags' => <flags>,
		 *                 'lld_ruleid' => <itemid> (CTriggerProrotype only)
		 *             ]
		 *         ]
		 *     ]
		 * ]
		 */
		$hosts_keys = [];
		$functions_num = 0;

		foreach ($triggers as $trigger) {
			$expressions_changed = ($db_triggers === null
				|| ($trigger['expression'] !== $db_triggers[$trigger['triggerid']]['expression']
				|| $trigger['recovery_expression'] !== $db_triggers[$trigger['triggerid']]['recovery_expression']));

			if (!$expressions_changed) {
				continue;
			}

			$expression_parser->parse($trigger['expression']);
			$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
				[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
			);

			if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
				$expression_parser->parse($trigger['recovery_expression']);
				$hist_functions = array_merge($hist_functions, $expression_parser->getResult()->getTokensOfTypes(
					[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
				));
			}

			foreach ($hist_functions as $hist_function) {
				$host = $hist_function['data']['parameters'][0]['data']['host'];
				$item = $hist_function['data']['parameters'][0]['data']['item'];

				if (!array_key_exists($host, $hosts_keys)) {
					$hosts_keys[$host] = [
						'hostid' => null,
						'host' => $host,
						'status' => null,
						'keys' => []
					];
				}

				$hosts_keys[$host]['keys'][$item] = [
					'itemid' => null,
					'key' => $item,
					'value_type' => null,
					'flags' => null
				];
			}
		}

		if (!$hosts_keys) {
			return;
		}

		$permission_check = $inherited
			? ['nopermissions' => true]
			: ['editable' => true];

		$_db_hosts = API::Host()->get([
			'output' => ['hostid', 'host', 'status'],
			'filter' => ['host' => array_keys($hosts_keys)]
		] + $permission_check);

		if (count($hosts_keys) != count($_db_hosts)) {
			$_db_templates = API::Template()->get([
				'output' => ['templateid', 'host', 'status'],
				'filter' => ['host' => array_keys($hosts_keys)]
			] + $permission_check);

			foreach ($_db_templates as &$_db_template) {
				$_db_template['hostid'] = $_db_template['templateid'];
				unset($_db_template['templateid']);
			}
			unset($_db_template);

			$_db_hosts = array_merge($_db_hosts, $_db_templates);
		}

		foreach ($_db_hosts as $_db_host) {
			$host_keys = &$hosts_keys[$_db_host['host']];

			$host_keys['hostid'] = $_db_host['hostid'];
			$host_keys['status'] = $_db_host['status'];

			if ($class === 'CTriggerPrototype') {
				$sql = 'SELECT i.itemid,i.key_,i.value_type,i.flags,id.parent_itemid'.
					' FROM items i'.
						' LEFT JOIN item_discovery id ON i.itemid=id.itemid'.
					' WHERE i.hostid='.$host_keys['hostid'].
						' AND '.dbConditionString('i.key_', array_keys($host_keys['keys'])).
						' AND '.dbConditionInt('i.flags',
							[ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_PROTOTYPE, ZBX_FLAG_DISCOVERY_CREATED]
						);
			}
			else {
				$sql = 'SELECT i.itemid,i.key_,i.value_type,i.flags'.
					' FROM items i'.
					' WHERE i.hostid='.$host_keys['hostid'].
						' AND '.dbConditionString('i.key_', array_keys($host_keys['keys'])).
						' AND '.dbConditionInt('i.flags', [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED]);
			}

			$_db_items = DBselect($sql);

			while ($_db_item = DBfetch($_db_items)) {
				$host_keys['keys'][$_db_item['key_']]['itemid'] = $_db_item['itemid'];
				$host_keys['keys'][$_db_item['key_']]['value_type'] = $_db_item['value_type'];
				$host_keys['keys'][$_db_item['key_']]['flags'] = $_db_item['flags'];

				if ($class === 'CTriggerPrototype' && $_db_item['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
					$host_keys['keys'][$_db_item['key_']]['lld_ruleid'] = $_db_item['parent_itemid'];
				}
			}

			unset($host_keys);
		}

		/*
		 * The list of triggers with multiple templates.
		 *
		 * [
		 *     [
		 *         'description' => <description>,
		 *         'templateids' => [<templateid>, ...]
		 *     ],
		 *     ...
		 * ]
		 */
		$mt_triggers = [];

		if ($class === 'CTrigger') {
			/*
			 * The list of triggers which are moved from one host or template to another.
			 *
			 * [
			 *     <triggerid> => [
			 *         'description' => <description>
			 *     ],
			 *     ...
			 * ]
			 */
			$moved_triggers = [];
		}

		foreach ($triggers as $tnum => &$trigger) {
			$expressions_changed = ($db_triggers === null
				|| ($trigger['expression'] !== $db_triggers[$trigger['triggerid']]['expression']
				|| $trigger['recovery_expression'] !== $db_triggers[$trigger['triggerid']]['recovery_expression']));

			if (!$expressions_changed) {
				continue;
			}

			$expression_parser->parse($trigger['expression']);

			$hist_functions1 = $expression_parser->getResult()->getTokensOfTypes(
				[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
			);

			$hist_functions2 = [];

			if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
				$expression_parser->parse($trigger['recovery_expression']);

				$hist_functions2 = $expression_parser->getResult()->getTokensOfTypes(
					[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
				);
			}

			$triggers_functions[$tnum] = [];
			if ($class === 'CTriggerPrototype') {
				$lld_ruleids = [];
			}

			/*
			 * 0x01 - with templates
			 * 0x02 - with hosts
			 */
			$status_mask = 0x00;
			// The lists of hostids and hosts which are used in the current trigger.
			$hostids = [];
			$hosts = [];

			// Common checks.
			foreach (array_merge($hist_functions1, $hist_functions2) as $hist_function) {
				$host = $hist_function['data']['parameters'][0]['data']['host'];
				$item = $hist_function['data']['parameters'][0]['data']['item'];

				$host_keys = $hosts_keys[$host];
				$key = $host_keys['keys'][$item];

				if ($host_keys['hostid'] === null) {
					self::exception(ZBX_API_ERROR_PARAMETERS, _params($error_wrong_host, [$host_keys['host']]));
				}

				if ($key['itemid'] === null) {
					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
						'Incorrect item key "%1$s" provided for trigger expression on "%2$s".', $key['key'],
						$host_keys['host']
					));
				}

				if (!in_array($key['value_type'], $hist_function_value_types[$hist_function['data']['function']])) {
					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
						'Incorrect item value type "%1$s" provided for trigger function "%2$s".',
						itemValueTypeString($key['value_type']), $hist_function['data']['function']
					));
				}

				if (!array_key_exists($hist_function['match'], $triggers_functions[$tnum])) {
					$query_parameter = $hist_function['data']['parameters'][0];
					$parameter = substr_replace($hist_function['match'], TRIGGER_QUERY_PLACEHOLDER,
						$query_parameter['pos'] - $hist_function['pos'], $query_parameter['length']
					);
					$triggers_functions[$tnum][$hist_function['match']] = [
						'functionid' => null,
						'triggerid' => null,
						'itemid' => $key['itemid'],
						'name' => $hist_function['data']['function'],
						'parameter' => substr($parameter, strlen($hist_function['data']['function']) + 1, -1)
					];
					$functions_num++;
				}

				if ($class === 'CTriggerPrototype' && $key['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
					$lld_ruleids[$key['lld_ruleid']] = true;
				}

				$status_mask |= ($host_keys['status'] == HOST_STATUS_TEMPLATE ? 0x01 : 0x02);

				$hostids[$host_keys['hostid']] = true;
				$hosts[$host] = true;
			}

			// When both templates and hosts are referenced in expressions.
			if ($status_mask == 0x03) {
				self::exception(ZBX_API_ERROR_PARAMETERS, $error_host_and_template);
			}

			// Triggers with children cannot be moved from one template to another host or template.
			if ($class === 'CTrigger' && $db_triggers !== null && $expressions_changed) {
				$expression_parser->parse($db_triggers[$trigger['triggerid']]['expression']);
				$old_hosts1 = $expression_parser->getResult()->getHosts();
				$old_hosts2 = [];

				if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
					$expression_parser->parse($db_triggers[$trigger['triggerid']]['recovery_expression']);
					$old_hosts2 = $expression_parser->getResult()->getHosts();
				}

				$is_moved = true;
				foreach (array_merge($old_hosts1, $old_hosts2) as $old_host) {
					if (array_key_exists($old_host, $hosts)) {
						$is_moved = false;
						break;
					}
				}

				if ($is_moved) {
					$moved_triggers[$trigger['triggerid']] = ['description' => $trigger['description']];
				}
			}

			// The trigger with multiple templates.
			if ($status_mask == 0x01 && count($hostids) > 1) {
				$mt_triggers[] = [
					'description' => $trigger['description'],
					'templateids' => array_keys($hostids)
				];
			}

			if ($class === 'CTriggerPrototype') {
				$lld_ruleids = array_keys($lld_ruleids);

				if (!$lld_ruleids) {
					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
						'Trigger prototype "%1$s" must contain at least one item prototype.', $trigger['description']
					));
				}
				elseif (count($lld_ruleids) > 1) {
					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
						'Trigger prototype "%1$s" contains item prototypes from multiple discovery rules.',
						$trigger['description']
					));
				}
				elseif ($db_triggers !== null
						&& !idcmp($lld_ruleids[0], $db_triggers[$trigger['triggerid']]['discoveryRule']['itemid'])) {
					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot update trigger prototype "%1$s": %2$s.',
						$trigger['description'], _('trigger prototype cannot be moved to another template or host')
					));
				}
			}
		}
		unset($trigger);

		if ($mt_triggers) {
			$this->validateTriggersWithMultipleTemplates($mt_triggers);
		}

		if ($class === 'CTrigger' && $moved_triggers) {
			$this->validateMovedTriggers($moved_triggers);
		}

		$functionid = DB::reserveIds('functions', $functions_num);

		$expression_max_length = DB::getFieldLength('triggers', 'expression');
		$recovery_expression_max_length = DB::getFieldLength('triggers', 'recovery_expression');

		// Replace func(/host/item) macros with {<functionid>}.
		foreach ($triggers as $tnum => &$trigger) {
			$expressions_changed = ($db_triggers === null
				|| ($trigger['expression'] !== $db_triggers[$trigger['triggerid']]['expression']
				|| $trigger['recovery_expression'] !== $db_triggers[$trigger['triggerid']]['recovery_expression']));

			if (!$expressions_changed) {
				continue;
			}

			foreach ($triggers_functions[$tnum] as &$trigger_function) {
				$trigger_function['functionid'] = $functionid;
				$functionid = bcadd($functionid, 1, 0);
			}
			unset($trigger_function);

			$expression_parser->parse($trigger['expression']);
			$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
				[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
			);
			$hist_function = end($hist_functions);
			do {
				$trigger['expression'] = substr_replace($trigger['expression'],
					'{'.$triggers_functions[$tnum][$hist_function['match']]['functionid'].'}',
					$hist_function['pos'], $hist_function['length']
				);
			}
			while ($hist_function = prev($hist_functions));

			if (mb_strlen($trigger['expression']) > $expression_max_length) {
				self::exception(ZBX_API_ERROR_PARAMETERS, _s(
					'Invalid parameter "%1$s": %2$s.', '/'.($tnum + 1).'/expression', _('value is too long')
				));
			}

			if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
				$expression_parser->parse($trigger['recovery_expression']);
				$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
					[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
				);
				$hist_function = end($hist_functions);
				do {
					$trigger['recovery_expression'] = substr_replace($trigger['recovery_expression'],
						'{'.$triggers_functions[$tnum][$hist_function['match']]['functionid'].'}',
						$hist_function['pos'], $hist_function['length']
					);
				}
				while ($hist_function = prev($hist_functions));

				if (mb_strlen($trigger['recovery_expression']) > $recovery_expression_max_length) {
					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
						'/'.($tnum + 1).'/recovery_expression', _('value is too long')
					));
				}
			}
		}
		unset($trigger);
	}

	/**
	 * Check if all templates trigger belongs to are linked to same hosts.
	 *
	 * @param array  $mt_triggers
	 * @param string $mt_triggers[]['description']
	 * @param array  $mt_triggers[]['templateids']
	 *
	 * @throws APIException
	 */
	protected function validateTriggersWithMultipleTemplates(array $mt_triggers) {
		switch (get_class($this)) {
			case 'CTrigger':
				$error_different_linkages = _('Trigger "%1$s" belongs to templates with different linkages.');
				break;

			case 'CTriggerPrototype':
				$error_different_linkages = _('Trigger prototype "%1$s" belongs to templates with different linkages.');
				break;

			default:
				self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.'));
		}

		$templateids = [];

		foreach ($mt_triggers as $mt_trigger) {
			foreach ($mt_trigger['templateids'] as $templateid) {
				$templateids[$templateid] = true;
			}
		}

		$templates = API::Template()->get([
			'output' => [],
			'selectHosts' => ['hostid'],
			'selectTemplates' => ['templateid'],
			'templateids' => array_keys($templateids),
			'nopermissions' => true,
			'preservekeys' => true
		]);

		foreach ($templates as &$template) {
			$template = array_merge(
				zbx_objectValues($template['hosts'], 'hostid'),
				zbx_objectValues($template['templates'], 'templateid')
			);
		}
		unset($template);

		foreach ($mt_triggers as $mt_trigger) {
			$compare_links = null;

			foreach ($mt_trigger['templateids'] as $templateid) {
				if ($compare_links === null) {
					$compare_links = $templates[$templateid];
					continue;
				}

				$linked_to = $templates[$templateid];

				if (array_diff($compare_links, $linked_to) || array_diff($linked_to, $compare_links)) {
					self::exception(ZBX_API_ERROR_PARAMETERS,
						_params($error_different_linkages, [$mt_trigger['description']])
					);
				}
			}
		}
	}

	/**
	 * Check if moved triggers does not have children.
	 *
	 * @param array  $moved_triggers
	 * @param string $moved_triggers[<triggerid>]['description']
	 *
	 * @throws APIException
	 */
	protected function validateMovedTriggers(array $moved_triggers) {
		$_db_triggers = DBselect(
			'SELECT t.templateid'.
			' FROM triggers t'.
			' WHERE '.dbConditionInt('t.templateid', array_keys($moved_triggers)),
			1
		);

		if ($_db_trigger = DBfetch($_db_triggers)) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot update trigger "%1$s": %2$s.',
				$moved_triggers[$_db_trigger['templateid']]['description'],
				_('trigger with linkages cannot be moved to another template or host')
			));
		}
	}

	/**
	 * Adds triggers and trigger prototypes from template to hosts.
	 *
	 * @param array $data
	 */
	public function syncTemplates(array $data) {
		$data['templateids'] = zbx_toArray($data['templateids']);
		$data['hostids'] = zbx_toArray($data['hostids']);

		$output = ['triggerid', 'description', 'expression', 'recovery_mode', 'recovery_expression', 'url_name', 'url',
			'status', 'priority', 'comments', 'type', 'correlation_mode', 'correlation_tag', 'manual_close', 'opdata',
			'event_name'
		];
		if ($this instanceof CTriggerPrototype) {
			$output[] = 'discover';
		}

		$triggers = $this->get([
			'output' => $output,
			'selectTags' => ['tag', 'value'],
			'hostids' => $data['templateids'],
			'preservekeys' => true,
			'nopermissions' => true
		]);

		$triggers = CMacrosResolverHelper::resolveTriggerExpressions($triggers,
			['sources' => ['expression', 'recovery_expression']]
		);

		$this->inherit($triggers, $data['hostids']);
	}

	/**
	 * Check whether circular linkage occurs as a result of the given changes in trigger dependencies.
	 *
	 * @param array $ins_dependencies[<triggerid_up>][<triggerid>]
	 * @param array $del_dependencies[<triggerid_up>][<triggerid>]
	 * @param bool  $inherited  Whether the check gets performed during inherit.
	 *
	 * @throws APIException
	 */
	protected static function checkCircularDependencies(array $ins_dependencies, array $del_dependencies = [],
			bool $inherited = false): void {
		$links = [];
		$_triggerids_down = $ins_dependencies;

		do {
			$options = [
				'output' => ['triggerid_up', 'triggerid_down'],
				'filter' => [
					'triggerid_down' => array_keys($_triggerids_down)
				]
			];
			$result = DBselect(DB::makeSql('trigger_depends', $options));

			$_triggerids_down = [];

			while ($row = DBfetch($result)) {
				if (array_key_exists($row['triggerid_up'], $del_dependencies)
						&& array_key_exists($row['triggerid_down'], $del_dependencies[$row['triggerid_up']])) {
					continue;
				}

				if (!array_key_exists($row['triggerid_up'], $links)) {
					$_triggerids_down[$row['triggerid_up']] = true;
				}

				$links[$row['triggerid_up']][$row['triggerid_down']] = true;
			}
		} while ($_triggerids_down);

		foreach ($ins_dependencies as $triggerid_up => $triggerids) {
			if (array_key_exists($triggerid_up, $links)) {
				$links[$triggerid_up] += $triggerids;
			}
			else {
				$links[$triggerid_up] = $triggerids;
			}
		}

		foreach ($ins_dependencies as $triggerid_up => $triggerids) {
			foreach ($triggerids as $triggerid => $foo) {
				if (array_key_exists($triggerid, $links)) {
					$links_path = [$triggerid => true];

					if (self::circularLinkageExists($links, $triggerid_up, $links[$triggerid], $links_path)) {
						$trigger_up_name = '';

						$triggers = DB::select('triggers', [
							'output' => ['description', 'flags'],
							'triggerids' => array_keys($links_path + [$triggerid_up => true]),
							'preservekeys' => true
						]);

						foreach ($triggers as $_triggerid => $trigger) {
							$description = '"'.$trigger['description'].'"';

							if (bccomp($_triggerid, $triggerid_up) == 0) {
								$trigger_up_name = $description;
							}
							else {
								$links_path[$_triggerid] = $description;
							}
						}

						$circular_linkage = (bccomp($triggerid_up, $triggerid) == 0)
							? $trigger_up_name.' -> '.$trigger_up_name
							: $trigger_up_name.' -> '.implode(' -> ', $links_path).' -> '.$trigger_up_name;

						if ($inherited) {
							$host = DBfetch(DBselect(
								'SELECT DISTINCT h.host,h.status'.
								' FROM functions f,items i,hosts h'.
								' WHERE f.itemid=i.itemid'.
									' AND i.hostid=h.hostid'.
									' AND '.dbConditionId('f.triggerid', [$triggerid_up])
							));

							if ($host['status'] == HOST_STATUS_TEMPLATE) {
								$error = ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL)
									? _('Trigger "%1$s" cannot depend on the trigger "%2$s", because a circular linkage (%3$s) would occur for template "%4$s".')
									: _('Trigger prototype "%1$s" cannot depend on the trigger prototype "%2$s", because a circular linkage (%3$s) would occur for template "%4$s".');
							}
							else {
								$error = ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL)
									? _('Trigger "%1$s" cannot depend on the trigger "%2$s", because a circular linkage (%3$s) would occur for host "%4$s".')
									: _('Trigger prototype "%1$s" cannot depend on the trigger prototype "%2$s", because a circular linkage (%3$s) would occur for host "%4$s".');
							}

							self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error,
								$triggers[$triggerid]['description'], $triggers[$triggerid_up]['description'],
								$circular_linkage, $host['host']
							));
						}
						else {
							$error = ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL)
								? _('Trigger "%1$s" cannot depend on the trigger "%2$s", because a circular linkage (%3$s) would occur.')
								: _('Trigger prototype "%1$s" cannot depend on the trigger prototype "%2$s", because a circular linkage (%3$s) would occur.');

							self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error,
								$triggers[$triggerid]['description'], $triggers[$triggerid_up]['description'],
								$circular_linkage
							));
						}
					}
				}
			}
		}
	}

	/**
	 * Recursively check whether the trigger, which a dependency is being set on, produces a circular linkage.
	 *
	 * @param array  $links[<triggerid_up>][<triggerid>]
	 * @param string $triggerid_up
	 * @param array  $triggerids[<triggerid>]
	 * @param array  $links_path                          Circular linkage path, collected performing the check.
	 *
	 * @return bool
	 */
	private static function circularLinkageExists(array $links, string $triggerid_up, array $triggerids,
			array &$links_path): bool {
		if (array_key_exists($triggerid_up, $triggerids)) {
			return true;
		}

		$_links_path = $links_path;

		foreach ($triggerids as $triggerid => $foo) {
			if (array_key_exists($triggerid, $links)) {
				$links_path = $_links_path;
				$triggerid_links = array_diff_key($links[$triggerid], $links_path);

				if ($triggerid_links) {
					$links_path[$triggerid] = true;

					if (self::circularLinkageExists($links, $triggerid_up, $triggerid_links, $links_path)) {
						return true;
					}
				}
			}
		}

		return false;
	}

	/**
	 * Get hosts data of all triggers given in trigger dependencies array.
	 *
	 * @param array $trigger_dependencies[<triggerid_up>][<triggerid>]
	 *
	 * @return array
	 */
	public static function getTriggerHosts(array $trigger_dependencies): array {
		$all_triggerids = $trigger_dependencies;

		foreach ($trigger_dependencies as $triggerids) {
			$all_triggerids += $triggerids;
		}

		$result = DBselect(
			'SELECT DISTINCT f.triggerid,h.hostid,h.status'.
			' FROM functions f,items i,hosts h'.
			' WHERE f.itemid=i.itemid'.
				' AND i.hostid=h.hostid'.
				' AND '.dbConditionId('f.triggerid', array_keys($all_triggerids))
		);

		$trigger_hosts = [];

		while ($row = DBfetch($result)) {
			// Each trigger can have either only templateids or only hostids.
			if ($row['status'] == HOST_STATUS_TEMPLATE) {
				$trigger_hosts[$row['triggerid']]['templateids'][$row['hostid']] = true;
			}
			else {
				$trigger_hosts[$row['triggerid']]['hostids'][$row['hostid']] = true;
			}
		}

		return $trigger_hosts;
	}

	/**
	 * Check whether the trigger dependencies are correctly set for host triggers.
	 *
	 * @param array $trigger_dependencies[<triggerid_up>][<triggerid>]
	 * @param array $trigger_hosts
	 *
	 * @throws APIException
	 */
	public static function checkDependenciesOfHostTriggers(array $trigger_dependencies, array $trigger_hosts): void {
		foreach ($trigger_dependencies as $triggerid_up => $triggerids) {
			if (array_key_exists('templateids', $trigger_hosts[$triggerid_up])) {
				foreach ($triggerids as $triggerid => $foo) {
					if (array_key_exists('hostids', $trigger_hosts[$triggerid])) {
						$triggers = DB::select('triggers', [
							'output' => ['description', 'flags'],
							'triggerids' => [$triggerid, $triggerid_up],
							'preservekeys' => true
						]);

						$error = ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL)
							? _('Trigger "%1$s" cannot depend on the trigger "%2$s", because dependencies of host triggers on template triggers are not allowed.')
							: _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s", because dependencies of host triggers on template triggers are not allowed.');

						self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $triggers[$triggerid]['description'],
							$triggers[$triggerid_up]['description']
						));
					}
				}
			}
		}
	}

	/**
	 * Check whether the trigger dependencies are correctly set for template triggers.
	 *
	 * @param array $trigger_dependencies[<triggerid_up>][<triggerid>]
	 * @param array $trigger_hosts
	 *
	 * @throws APIException
	 */
	public static function checkDependenciesOfTemplateTriggers(array $trigger_dependencies,
			array $trigger_hosts): void {
		/*
		 * From the given trigger dependencies we should keep only the dependencies of the template triggers. There is
		 * also no need to check the dependencies on triggers from the same template, because that is considered a
		 * valid case. Thus, among the triggers that the template triggers depends on, we should only keep triggers from
		 * other templates and hosts.
		 */
		foreach ($trigger_dependencies as $triggerid_up => $triggerids) {
			$templateids_up = array_key_exists('templateids', $trigger_hosts[$triggerid_up])
				? $trigger_hosts[$triggerid_up]['templateids']
				: [];

			foreach ($triggerids as $triggerid => $foo) {
				/*
				 * If trigger-up and dependent trigger have at least one common template, even if they also have
				 * other templates, it is important to understand that at the moment of the trigger dependency
				 * validation all of those different templates are already linked to all child templates,
				 * so there is no need to check them again.
				 */
				if (!array_key_exists('templateids', $trigger_hosts[$triggerid])
						|| array_intersect_key($templateids_up, $trigger_hosts[$triggerid]['templateids'])) {
					unset($trigger_dependencies[$triggerid_up][$triggerid]);
				}
			}

			if (!$trigger_dependencies[$triggerid_up]) {
				unset($trigger_dependencies[$triggerid_up]);
			}
		}

		if (!$trigger_dependencies) {
			return;
		}

		self::checkTriggersUpNotFromParentTemplates($trigger_dependencies, $trigger_hosts);
		self::checkTriggersUpNotFromChildTemplatesOrHosts($trigger_dependencies, $trigger_hosts);
		self::checkTriggersUpTemplatesAreLinkedToChildTemplates($trigger_dependencies, $trigger_hosts);
	}

	/**
	 * Check that the triggers-up of the given trigger dependencies do not come from parent templates of dependent
	 * triggers.
	 *
	 * @param array $trigger_dependencies[<triggerid_up>][<triggerid>]
	 * @param array $trigger_hosts
	 *
	 * @throws APIException
	 */
	private static function checkTriggersUpNotFromParentTemplates(array $trigger_dependencies, array $trigger_hosts): void {
		$templateids = [];
		$template_triggers_up = [];
		$dependency_templates = [];

		foreach ($trigger_dependencies as $triggerid_up => $triggerids) {
			if (!array_key_exists('templateids', $trigger_hosts[$triggerid_up])) {
				continue;
			}

			$templateids_up = [];

			foreach ($trigger_hosts[$triggerid_up]['templateids'] as $templateid => $foo) {
				$template_triggers_up[$templateid][] = $triggerid_up;
				$templateids_up[$templateid] = true;
			}

			foreach ($triggerids as $triggerid => $foo) {
				foreach ($trigger_hosts[$triggerid]['templateids'] as $templateid => $foo) {
					if (!array_key_exists($templateid, $dependency_templates)) {
						$dependency_templates[$templateid] = [];
					}

					$dependency_templates[$templateid] += $templateids_up;
				}

				$templateids += $trigger_hosts[$triggerid]['templateids'];
			}
		}

		$_templateids = $templateids;
		$template_links = [];

		do {
			$options = [
				'output' => ['hostid', 'templateid'],
				'filter' => ['hostid' => array_keys($_templateids)]
			];
			$result = DBselect(DB::makeSql('hosts_templates', $options));

			$_templateids = [];

			while ($row = DBfetch($result)) {
				$template_links[$row['hostid']][$row['templateid']] = true;

				if (!array_key_exists($row['templateid'], $template_links)) {
					$_templateids[$row['templateid']] = true;
				}
			}
		} while ($_templateids);

		if (!$template_links) {
			return;
		}

		// Check if the trigger-up is a trigger from the parent template of the dependent trigger.
		foreach ($dependency_templates as $templateid => $templateids_up) {
			if (array_key_exists($templateid, $template_links)) {
				foreach ($templateids_up as $templateid_up => $foo) {
					if (self::checkTemplateUpExistsInTemplateLinks($template_links, $templateid, $templateid_up)) {
						foreach ($template_triggers_up[$templateid_up] as $triggerid_up) {
							foreach ($trigger_dependencies[$triggerid_up] as $triggerid => $foo) {
								if (array_key_exists($templateid, $trigger_hosts[$triggerid]['templateids'])) {
									break 2;
								}
							}
						}

						$triggers = DB::select('triggers', [
							'output' => ['description', 'flags'],
							'triggerids' => [$triggerid, $triggerid_up],
							'preservekeys' => true
						]);

						$templates = DB::select('hosts', [
							'output' => ['host'],
							'hostids' => $templateid_up
						]);

						$error = ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL)
							? _('Trigger "%1$s" cannot depend on the trigger "%2$s" from the template "%3$s", because dependencies on triggers from the parent template are not allowed.')
							: _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s" from the template "%3$s", because dependencies on triggers from the parent template are not allowed.');

						self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $triggers[$triggerid]['description'],
							$triggers[$triggerid_up]['description'], $templates[0]['host']
						));
					}
				}
			}
		}
	}

	/**
	 * Check that the triggers-up of the given trigger dependencies are not from the child templates or hosts of the
	 * dependent triggers.
	 *
	 * @param array $trigger_dependencies[<triggerid_up>][<triggerid>]
	 * @param array $trigger_hosts
	 *
	 * @throws APIException
	 */
	private static function checkTriggersUpNotFromChildTemplatesOrHosts(array $trigger_dependencies,
			array $trigger_hosts): void {
		$templateids = [];
		$template_triggers_up = [];
		$dependency_templates = [];

		foreach ($trigger_dependencies as $triggerid_up => $triggerids) {
			$templateids_up = [];

			if (array_key_exists('templateids', $trigger_hosts[$triggerid_up])) {
				foreach ($trigger_hosts[$triggerid_up]['templateids'] as $templateid => $foo) {
					$template_triggers_up[$templateid][] = $triggerid_up;
					$templateids_up[$templateid] = true;
				}
			}
			else {
				foreach ($trigger_hosts[$triggerid_up]['hostids'] as $hostid => $foo) {
					$template_triggers_up[$hostid][] = $triggerid_up;
					$templateids_up[$hostid] = true;
				}
			}

			foreach ($triggerids as $triggerid => $foo) {
				foreach ($trigger_hosts[$triggerid]['templateids'] as $templateid => $foo) {
					if (!array_key_exists($templateid, $dependency_templates)) {
						$dependency_templates[$templateid] = [];
					}

					$dependency_templates[$templateid] += $templateids_up;
				}

				$templateids += $trigger_hosts[$triggerid]['templateids'];
			}
		}

		$template_links = [];

		do {
			$options = [
				'output' => ['hostid', 'templateid'],
				'filter' => ['templateid' => array_keys($templateids)]
			];
			$result = DBselect(DB::makeSql('hosts_templates', $options));

			$templateids = [];

			while ($row = DBfetch($result)) {
				$template_links[$row['templateid']][$row['hostid']] = true;

				if (!array_key_exists($row['hostid'], $template_links)) {
					$templateids[$row['hostid']] = true;
				}
			}
		} while ($templateids);

		if (!$template_links) {
			return;
		}

		// Check if each trigger-up is a trigger from the child template or host of the dependent trigger.
		foreach ($dependency_templates as $templateid => $templateids_up) {
			if (array_key_exists($templateid, $template_links)) {
				foreach ($templateids_up as $templateid_up => $foo) {
					if (self::checkTemplateUpExistsInTemplateLinks($template_links, $templateid, $templateid_up)) {
						foreach ($template_triggers_up[$templateid_up] as $triggerid_up) {
							foreach ($trigger_dependencies[$triggerid_up] as $triggerid => $foo) {
								if (array_key_exists($templateid, $trigger_hosts[$triggerid]['templateids'])) {
									break 2;
								}
							}
						}

						$triggers = DB::select('triggers', [
							'output' => ['description', 'flags'],
							'triggerids' => [$triggerid, $triggerid_up],
							'preservekeys' => true
						]);

						$templates = DB::select('hosts', [
							'output' => ['host', 'status'],
							'hostids' => $templateid_up
						]);

						if ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL) {
							$error = ($templates[0]['status'] == HOST_STATUS_TEMPLATE)
								? _('Trigger "%1$s" cannot depend on the trigger "%2$s" from the template "%3$s", because dependencies on triggers from a child template or host are not allowed.')
								: _('Trigger "%1$s" cannot depend on the trigger "%2$s" from the host "%3$s", because dependencies on triggers from a child template or host are not allowed.');
						}
						else {
							$error = ($templates[0]['status'] == HOST_STATUS_TEMPLATE)
								? _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s" from the template "%3$s", because dependencies on triggers from a child template or host are not allowed.')
								: _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s" from the host "%3$s", because dependencies on triggers from a child template or host are not allowed.');
						}

						self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $triggers[$triggerid]['description'],
							$triggers[$triggerid_up]['description'], $templates[0]['host']
						));
					}
				}
			}
		}
	}

	/**
	 * Recursively check if the given template-up exists in the chain of the given template links.
	 *
	 * @param array  $template_links[<templateid>][<templateid_up>]
	 * @param string $templateid
	 * @param string $templateid_up
	 *
	 * @return bool
	 */
	private static function checkTemplateUpExistsInTemplateLinks(array $template_links, string $templateid,
			string $templateid_up): bool {
		if (array_key_exists($templateid_up, $template_links[$templateid])) {
			return true;
		}

		foreach ($template_links[$templateid] as $_templateid => $foo) {
			if (array_key_exists($_templateid, $template_links)
					&& self::checkTemplateUpExistsInTemplateLinks($template_links, $_templateid, $templateid_up)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Check if the triggers-up templates of the given trigger dependencies are linked to child templates of the
	 * dependent triggers.
	 *
	 * @param array $trigger_dependencies[<triggerid_up>][<triggerid>]
	 * @param array $trigger_hosts
	 *
	 * @throws APIException
	 */
	private static function checkTriggersUpTemplatesAreLinkedToChildTemplates(array $trigger_dependencies,
			array $trigger_hosts): void {
		$templateids_up = [];
		$templateids = [];

		foreach ($trigger_dependencies as $triggerid_up => $triggerids) {
			if (!array_key_exists('templateids', $trigger_hosts[$triggerid_up])) {
				unset($trigger_dependencies[$triggerid_up]);
				continue;
			}

			$templateids_up += $trigger_hosts[$triggerid_up]['templateids'];

			foreach ($triggerids as $triggerid => $foo) {
				$templateids += $trigger_hosts[$triggerid]['templateids'];
			}
		}

		$options = [
			'output' => ['templateid', 'hostid'],
			'filter' => [
				'templateid' => array_keys($templateids)
			]
		];
		$result = DBselect(DB::makeSql('hosts_templates', $options));

		$template_links = [];

		while ($row = DBfetch($result)) {
			$template_links[$row['templateid']][$row['hostid']] = [];
		}

		$result = DBselect(
			'SELECT ht.templateid,ht.hostid,htt.templateid AS host_templateid'.
			' FROM hosts_templates ht,hosts_templates htt'.
			' WHERE ht.hostid=htt.hostid'.
				' AND ht.templateid!=htt.templateid'.
				' AND '.dbConditionId('ht.templateid', array_keys($templateids)).
				' AND '.dbConditionId('htt.templateid', array_keys($templateids_up))
		);

		while ($row = DBfetch($result)) {
			$template_links[$row['templateid']][$row['hostid']][$row['host_templateid']] = true;
		}

		foreach ($trigger_dependencies as $triggerid_up => $triggerids) {
			/*
			 * If trigger belongs to more than one template, then it is not possible to link only part of them to
			 * another host or template. That means each template ID of that trigger would have the same hosts
			 * in template links. And vice versa, if at least one of the trigger templates was not found in template
			 * links, then the trigger is not inherited further.
			 */
			$templateid_up = key($trigger_hosts[$triggerid_up]['templateids']);

			foreach ($triggerids as $triggerid => $foo) {
				$templateid = key($trigger_hosts[$triggerid]['templateids']);

				if (!array_key_exists($templateid, $template_links)) {
					continue;
				}

				foreach ($template_links[$templateid] as $hostid => $host_templateids) {
					if (!array_key_exists($templateid_up, $host_templateids)) {
						$triggers = DB::select('triggers', [
							'output' => ['description', 'flags'],
							'triggerids' => [$triggerid, $triggerid_up],
							'preservekeys' => true
						]);

						$hosts = DB::select('hosts', [
							'output' => ['host', 'status'],
							'hostids' => [$templateid_up, $hostid],
							'preservekeys' => true
						]);

						if ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL) {
							$error = ($hosts[$hostid]['status'] == HOST_STATUS_TEMPLATE)
								? _('Trigger "%1$s" cannot depend on the trigger "%2$s", because the template "%3$s" is not linked to the template "%4$s".')
								: _('Trigger "%1$s" cannot depend on the trigger "%2$s", because the template "%3$s" is not linked to the host "%4$s".');
						}
						else {
							$error = ($hosts[$hostid]['status'] == HOST_STATUS_TEMPLATE)
								? _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s", because the template "%3$s" is not linked to the template "%4$s".')
								: _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s", because the template "%3$s" is not linked to the host "%4$s".');
						}

						self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $triggers[$triggerid]['description'],
							$triggers[$triggerid_up]['description'], $hosts[$templateid_up]['host'],
							$hosts[$hostid]['host']
						));
					}
				}
			}
		}
	}

	/**
	 * Update the trigger dependencies.
	 *
	 * @param array      $triggers
	 * @param array|null $db_triggers
	 */
	public static function updateDependencies(array &$triggers, array $db_triggers = null): void {
		$ins_trigger_deps = [];
		$del_triggerdepids = [];
		$edit_dependencies = [];

		foreach ($triggers as &$trigger) {
			if (!array_key_exists('dependencies', $trigger)) {
				continue;
			}

			$db_triggers_up = ($db_triggers !== null)
				? array_column($db_triggers[$trigger['triggerid']]['dependencies'], null, 'triggerid')
				: [];

			foreach ($trigger['dependencies'] as &$trigger_up) {
				if (array_key_exists($trigger_up['triggerid'], $db_triggers_up)) {
					$trigger_up['triggerdepid'] = $db_triggers_up[$trigger_up['triggerid']]['triggerdepid'];
					unset($db_triggers_up[$trigger_up['triggerid']]);
				}
				else {
					$ins_trigger_deps[] = [
						'triggerid_down' => $trigger['triggerid'],
						'triggerid_up' => $trigger_up['triggerid']
					];

					$edit_dependencies[$trigger['triggerid']][$trigger_up['triggerid']] = true;
				}
			}
			unset($trigger_up);

			foreach ($db_triggers_up as $db_trigger_up) {
				$del_triggerdepids[] = $db_trigger_up['triggerdepid'];

				$edit_dependencies[$trigger['triggerid']][$db_trigger_up['triggerid']] = false;
			}
		}
		unset($trigger);

		if ($del_triggerdepids) {
			DB::delete('trigger_depends', ['triggerdepid' => $del_triggerdepids]);
		}

		if ($ins_trigger_deps) {
			$triggerdepids = DB::insertBatch('trigger_depends', $ins_trigger_deps);
		}

		foreach ($triggers as &$trigger) {
			if (!array_key_exists('dependencies', $trigger)) {
				continue;
			}

			foreach ($trigger['dependencies'] as &$trigger_up) {
				if (!array_key_exists('triggerdepid', $trigger_up)) {
					$trigger_up['triggerdepid'] = array_shift($triggerdepids);
				}
			}
			unset($trigger_up);
		}
		unset($trigger);

		if ($edit_dependencies) {
			$edit_dependencies = self::getTemplatedDependencies($edit_dependencies);

			if ($edit_dependencies) {
				self::inheritDependencies($edit_dependencies);
			}
		}
	}

	/**
	 * Filter for dependencies whose dependent triggers belong to templates that are further linked
	 * to some templates or hosts.
	 *
	 * @param array $edit_dependencies[<triggerid>][<triggerid_up>]
	 *
	 * @return array
	 */
	protected static function getTemplatedDependencies(array $edit_dependencies): array {
		$triggerids = DBfetchColumn(DBselect(
			'SELECT DISTINCT f.triggerid'.
			' FROM functions f,items i,hosts h'.
			' WHERE f.itemid=i.itemid'.
				' AND i.hostid=h.hostid'.
				' AND '.dbConditionId('f.triggerid', array_keys($edit_dependencies)).
				' AND '.dbConditionInt('h.status', [HOST_STATUS_TEMPLATE]).
				' AND EXISTS('.
					'SELECT NULL'.
					' FROM hosts_templates ht,hosts h2'.
					' WHERE i.hostid=ht.templateid'.
						' AND ht.hostid=h2.hostid'.
						' AND '.dbConditionInt('h2.flags', [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED]).
				')'
		), 'triggerid');

		return array_intersect_key($edit_dependencies, array_flip($triggerids));
	}

	/**
	 * Inherit the given trigger dependencies.
	 *
	 * @param array $edit_dependencies[<triggerid>][<triggerid_up>]
	 * @param array $hostids
	 */
	protected static function inheritDependencies(array $edit_dependencies, array $hostids = null): void {
		$all_triggerids = $edit_dependencies;
		$all_triggerids_up = [];

		foreach ($edit_dependencies as $triggerids_up) {
			$all_triggerids += $triggerids_up;
			$all_triggerids_up += $triggerids_up;
		}

		$hostids_condition = ($hostids !== null) ? ' AND '.dbConditionId('i.hostid', $hostids) : '';

		$result = DBselect(
			'SELECT DISTINCT t.templateid,t.triggerid,t.flags,i.hostid'.
			' FROM triggers t,functions f,items i'.
			' WHERE t.triggerid=f.triggerid'.
				' AND f.itemid=i.itemid'.
				' AND '.dbConditionId('t.templateid', array_keys($all_triggerids)).
				$hostids_condition
		);

		$trigger_flags = [];
		$trigger_links = [];
		$tpl_triggerids_up = [];

		while ($row = DBfetch($result)) {
			$trigger_links[$row['templateid']][$row['hostid']] = $row['triggerid'];
			$trigger_flags[$row['triggerid']] = $row['flags'];

			if (array_key_exists($row['templateid'], $all_triggerids_up)) {
				$tpl_triggerids_up[$row['templateid']] = true;
			}
		}

		$del_triggerdepids = [];
		$_edit_dependencies = [];

		if ($tpl_triggerids_up) {
			if ($hostids === null) {
				$result = DBselect(
					'SELECT DISTINCT t.triggerid,td.triggerid_up,td.triggerdepid,i.hostid'.
					' FROM triggers t,trigger_depends td,triggers tt,functions f,items i'.
					' WHERE t.triggerid=td.triggerid_down'.
						' AND td.triggerid_up=tt.triggerid'.
						' AND tt.triggerid=f.triggerid'.
						' AND f.itemid=i.itemid'.
						' AND '.dbConditionId('t.templateid', array_keys($edit_dependencies)).
						' AND '.dbConditionId('tt.templateid', array_keys($tpl_triggerids_up))
				);
			}
			else {
				$result = DBselect(
					'SELECT DISTINCT t.triggerid,td.triggerid_up,td.triggerdepid,ii.hostid'.
					' FROM triggers t,functions f,items i,trigger_depends td,triggers tt,functions ff,items ii'.
					' WHERE t.triggerid=f.triggerid'.
						' AND f.itemid=i.itemid'.
						' AND t.triggerid=td.triggerid_down'.
						' AND td.triggerid_up=tt.triggerid'.
						' AND tt.triggerid=ff.triggerid'.
						' AND ff.itemid=ii.itemid'.
						' AND '.dbConditionId('t.templateid', array_keys($edit_dependencies)).
						' AND '.dbConditionId('i.hostid', $hostids).
						' AND '.dbConditionId('tt.templateid', array_keys($tpl_triggerids_up))
				);
			}

			$tpl_child_dependencies = [];

			while ($row = DBfetch($result)) {
				$tpl_child_dependencies[$row['triggerid']][$row['triggerid_up']][$row['hostid']] = $row['triggerdepid'];
			}

			foreach ($edit_dependencies as $triggerid => $triggerids_up) {
				$triggerids_up = array_intersect_key($triggerids_up, $tpl_triggerids_up);

				if (!$triggerids_up) {
					continue;
				}

				foreach ($trigger_links[$triggerid] as $hostid => $child_triggerid) {
					$upd_child_triggerids_up = [];

					if (array_key_exists($child_triggerid, $tpl_child_dependencies)) {
						foreach ($tpl_child_dependencies[$child_triggerid] as $child_triggerid_up => $hostids_up) {
							$hostid_up = key($hostids_up);
							$triggerdepid = reset($hostids_up);

							if (in_array($child_triggerid_up, $upd_child_triggerids_up)
									|| in_array($triggerdepid, $del_triggerdepids)) {
								continue;
							}

							foreach ($triggerids_up as $triggerid_up => $add) {
								if (array_key_exists($hostid_up, $trigger_links[$triggerid_up])
										&& bccomp($child_triggerid_up, $trigger_links[$triggerid_up][$hostid_up]) == 0) {
									if ($add) {
										$upd_child_triggerids_up[] = $child_triggerid_up;
									}
									else {
										$del_triggerdepids[] = $hostids_up[$hostid_up];

										$_edit_dependencies[$child_triggerid][$child_triggerid_up] = false;
									}
								}
							}
						}
					}

					foreach ($triggerids_up as $triggerid_up => $add) {
						if ($add) {
							$child_triggerid_up = $trigger_links[$triggerid_up][$hostid];

							if (!in_array($child_triggerid_up, $upd_child_triggerids_up)) {
								$_edit_dependencies[$child_triggerid][$child_triggerid_up] = true;
							}
						}
					}
				}
			}
		}

		$host_triggerids_up = array_diff_key($all_triggerids_up, $tpl_triggerids_up);

		if ($host_triggerids_up) {
			if ($hostids === null) {
				$result = DBselect(
					'SELECT DISTINCT t.triggerid,td.triggerid_up,td.triggerdepid'.
					' FROM triggers t,trigger_depends td'.
					' WHERE t.triggerid=td.triggerid_down'.
						' AND '.dbConditionId('t.templateid', array_keys($edit_dependencies)).
						' AND '.dbConditionId('td.triggerid_up', array_keys($host_triggerids_up))
				);
			}
			else {
				$result = DBselect(
					'SELECT DISTINCT t.triggerid,td.triggerid_up,td.triggerdepid'.
					' FROM triggers t,functions f,items i,trigger_depends td'.
					' WHERE t.triggerid=f.triggerid'.
						' AND f.itemid=i.itemid'.
						' AND t.triggerid=td.triggerid_down'.
						' AND '.dbConditionId('t.templateid', array_keys($edit_dependencies)).
						' AND '.dbConditionId('i.hostid', $hostids).
						' AND '.dbConditionId('td.triggerid_up', array_keys($host_triggerids_up))
				);
			}

			$host_child_dependencies = [];

			while ($row = DBfetch($result)) {
				$host_child_dependencies[$row['triggerid']][$row['triggerid_up']] = $row['triggerdepid'];
			}

			foreach ($edit_dependencies as $triggerid => $triggerids_up) {
				$triggerids_up = array_intersect_key($triggerids_up, $host_triggerids_up);

				if (!$triggerids_up) {
					continue;
				}

				foreach ($trigger_links[$triggerid] as $hostid => $child_triggerid) {
					$upd_child_triggerids_up = [];

					if (array_key_exists($child_triggerid, $host_child_dependencies)) {
						foreach ($host_child_dependencies[$child_triggerid] as $child_triggerid_up => $triggerdepid) {
							if (array_key_exists($child_triggerid_up, $triggerids_up)) {
								$add = $triggerids_up[$child_triggerid_up];

								if ($add) {
									$upd_child_triggerids_up[] = $child_triggerid_up;
								}
								else {
									$del_triggerdepids[] = $triggerdepid;

									$_edit_dependencies[$child_triggerid][$child_triggerid_up] = false;
								}
							}
						}
					}

					foreach ($triggerids_up as $triggerid_up => $add) {
						if ($add && !in_array($triggerid_up, $upd_child_triggerids_up)) {
							$_edit_dependencies[$child_triggerid][$triggerid_up] = true;
						}
					}
				}
			}
		}

		$ins_dependencies = [];
		$del_dependencies = [];
		$ins_trigger_deps = [];

		foreach ($_edit_dependencies as $triggerid => $triggerids_up) {
			foreach ($triggerids_up as $triggerid_up => $add) {
				if ($add) {
					if ($trigger_flags[$triggerid] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
						if (array_key_exists($triggerid_up, $trigger_flags)
								&& $trigger_flags[$triggerid_up] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
							$ins_dependencies[$triggerid_up][$triggerid] = true;
						}
					}
					else {
						$ins_dependencies[$triggerid_up][$triggerid] = true;
					}

					$ins_trigger_deps[] = [
						'triggerid_down' => $triggerid,
						'triggerid_up' => $triggerid_up
					];
				}
				else {
					$del_dependencies[$triggerid_up][$triggerid] = true;
				}
			}
		}

		if ($ins_dependencies) {
			self::checkCircularDependencies($ins_dependencies, $del_dependencies, true);
		}

		if ($del_triggerdepids) {
			DB::delete('trigger_depends', ['triggerdepid' => $del_triggerdepids]);
		}

		if ($ins_trigger_deps) {
			DB::insertBatch('trigger_depends', $ins_trigger_deps);
		}

		if ($_edit_dependencies) {
			$_edit_dependencies = self::getTemplatedDependencies($_edit_dependencies);

			if ($_edit_dependencies) {
				self::inheritDependencies($_edit_dependencies);
			}
		}
	}

	/**
	 * Inherit the trigger dependencies of the given templates to the given hosts.
	 *
	 * @param array $templateids
	 * @param array $hostids
	 */
	public static function syncTemplateDependencies(array $templateids, array $hostids): void {
		$result = DBselect(
			'SELECT DISTINCT f.triggerid,td.triggerid_up'.
			' FROM items i,functions f,trigger_depends td'.
			' WHERE i.itemid=f.itemid'.
				' AND f.triggerid=td.triggerid_down'.
				' AND '.dbConditionId('i.hostid', $templateids)
		);

		$edit_dependencies = [];

		while ($row = DBfetch($result)) {
			$edit_dependencies[$row['triggerid']][$row['triggerid_up']] = true;
		}

		if ($edit_dependencies) {
			$edit_dependencies = self::getTemplatedDependencies($edit_dependencies);

			if ($edit_dependencies) {
				self::inheritDependencies($edit_dependencies, $hostids);
			}
		}
	}
}
