<?php
/*
** Copyright (C) 2001-2026 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/


abstract class CHostBase extends CApiService {

	public const ACCESS_RULES = [
		'get' => ['min_user_type' => USER_TYPE_ZABBIX_USER],
		'create' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
		'update' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
		'delete' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN]
	];

	public const DISCOVERY_DATA_OUTPUT_FIELDS = ['host', 'parent_hostid', 'status', 'ts_delete', 'ts_disable',
		'disable_source'
	];

	protected $tableName = 'hosts';
	protected $tableAlias = 'h';

	protected static function isHost(): bool {
		return static::class === 'CHost';
	}

	protected static function isTemplate(): bool {
		return static::class === 'CTemplate';
	}

	private static function isHostPrototype(): bool {
		return static::class === 'CHostPrototype';
	}

	protected function checkTemplates(array &$hosts, ?array &$db_hosts = null, ?string $path = null,
			?array $template_indexes = null, ?string $path_clear = null, ?array $template_clear_indexes = null): void {
		$id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid';

		$ins_template_indexes = [];
		$clear_template_indexes = [];

		foreach ($hosts as $i1 => &$host) {
			if (array_key_exists('templates', $host) && array_key_exists('templates_clear', $host)) {
				foreach ($host['templates_clear'] as $i2_clear => $template_clear) {
					foreach ($host['templates'] as $i2 => $template) {
						if (bccomp($template['templateid'], $template_clear['templateid']) != 0) {
							continue;
						}

						if ($path === null) {
							$path_clear = '/'.($i1 + 1).'/templates_clear/'.($i2_clear + 1);
							$path = '/'.($i1 + 1).'/templates/'.($i2 + 1);
						}
						else {
							$path_clear .= '/'.($template_clear_indexes[$template['templateid']] + 1);
							$path .= '/'.($template_indexes[$template['templateid']] + 1);
						}

						$error = _s('cannot be specified the value of parameter "%1$s"', $path.'/templateid');

						self::exception(ZBX_API_ERROR_PARAMETERS,
							_s('Invalid parameter "%1$s": %2$s.', $path_clear.'/templateid', $error)
						);
					}
				}
			}

			if (array_key_exists('templates', $host)) {
				$db_templates = $db_hosts !== null
					? array_column($db_hosts[$host[$id_field_name]]['templates'], null, 'templateid')
					: [];

				foreach ($host['templates'] as $i2 => $template) {
					if (array_key_exists($template['templateid'], $db_templates)) {
						if ($db_templates[$template['templateid']]['link_type'] != TEMPLATE_LINK_MANUAL) {
							unset($host['templates'][$i2]);

							$db_hosttemplateid = $db_templates[$template['templateid']]['hosttemplateid'];
							unset($db_hosts[$host[$id_field_name]]['templates'][$db_hosttemplateid]);
						}

						unset($db_templates[$template['templateid']]);
					}
					else {
						$ins_template_indexes[$template['templateid']][$i1] = $i2;
					}
				}

				if ($db_templates) {
					$templateids_clear = array_key_exists('templates_clear', $host)
						? array_column($host['templates_clear'], 'templateid')
						: [];

					foreach ($db_templates as $db_template) {
						if ($db_template['link_type'] != TEMPLATE_LINK_MANUAL
								&& !in_array($db_template['templateid'], $templateids_clear)) {
							unset($db_hosts[$host[$id_field_name]]['templates'][$db_template['hosttemplateid']]);
						}
					}
				}
			}

			if (array_key_exists('templates_clear', $host)) {
				$db_templates = array_column($db_hosts[$host[$id_field_name]]['templates'], null, 'templateid');

				foreach ($host['templates_clear'] as $i2 => $template) {
					if (array_key_exists($template['templateid'], $db_templates)) {
						if ($db_templates[$template['templateid']]['link_type'] != TEMPLATE_LINK_MANUAL) {
							unset($host['templates_clear'][$i2]);

							$db_hosttemplateid = $db_templates[$template['templateid']]['hosttemplateid'];
							unset($db_hosts[$host[$id_field_name]]['templates'][$db_hosttemplateid]);
						}

						unset($db_templates[$template['templateid']]);
					}
					else {
						$clear_template_indexes[$template['templateid']][$i1] = $i2;
					}
				}
			}
		}
		unset($host);

		if ($ins_template_indexes) {
			$db_templates = API::Template()->get([
				'output' => [],
				'templateids' => array_keys($ins_template_indexes),
				'preservekeys' => true
			]);

			foreach ($ins_template_indexes as $templateid => $indexes) {
				if (!array_key_exists($templateid, $db_templates)) {
					if ($path === null) {
						$i1 = key($indexes);
						$i2 = $indexes[$i1];

						$path = '/'.($i1 + 1).'/templates/'.($i2 + 1);
					}
					else {
						$i = $template_indexes[$templateid];

						$path .= '/'.($i + 1);
					}

					self::exception(ZBX_API_ERROR_PERMISSIONS, _s('Invalid parameter "%1$s": %2$s.', $path,
						_('object does not exist, or you have no permissions to it')
					));
				}
			}
		}

		if ($clear_template_indexes) {
			$db_templates = API::Template()->get([
				'output' => [],
				'templateids' => array_keys($clear_template_indexes),
				'preservekeys' => true
			]);

			foreach ($clear_template_indexes as $templateid => $indexes) {
				if (!array_key_exists($templateid, $db_templates)) {
					if ($path_clear === null) {
						$i1 = key($indexes);
						$i2 = $indexes[$i1];

						$path_clear = '/'.($i1 + 1).'/templates_clear/'.($i2 + 1);
					}
					else {
						$i = $template_clear_indexes[$template['templateid']];

						$path_clear .= '/'.($i + 1);
					}

					self::exception(ZBX_API_ERROR_PERMISSIONS, _s('Invalid parameter "%1$s": %2$s.', $path_clear,
						_('object does not exist, or you have no permissions to it')
					));
				}
				else {
					foreach ($indexes as $i1 => $i2) {
						unset($hosts[$i1]['templates_clear'][$i2]);

						if (!$hosts[$i1]['templates_clear']) {
							unset($hosts[$i1]['templates_clear']);
						}
					}
				}
			}
		}
	}

	/**
	 * Check templates links.
	 *
	 * @param array      $hosts
	 * @param array|null $db_hosts
	 */
	protected function checkTemplatesLinks(array $hosts, ?array $db_hosts = null): void {
		$id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid';

		$ins_templates = [];
		$del_links = [];
		$is_template_update = $this instanceof CTemplate && $db_hosts !== null;
		$double_linkage_scope = $is_template_update ? null : [];
		$del_templates = [];

		foreach ($hosts as $host) {
			if (array_key_exists('templates', $host)) {
				$db_templates = ($db_hosts !== null)
					? array_column($db_hosts[$host[$id_field_name]]['templates'], null, 'templateid')
					: [];
				$templateids = array_column($host['templates'], 'templateid');
				$templates_count = count($host['templates']);
				$upd_templateids = [];

				if ($db_hosts !== null
						&& array_key_exists('nopermissions_templates', $db_hosts[$host[$id_field_name]])) {
					foreach ($db_hosts[$host[$id_field_name]]['nopermissions_templates'] as $db_template) {
						$templateids[] = $db_template['templateid'];
						$templates_count++;
						$upd_templateids[] = $db_template['templateid'];
					}
				}

				foreach ($host['templates'] as $template) {
					if (array_key_exists($template['templateid'], $db_templates)) {
						$upd_templateids[] = $template['templateid'];
						unset($db_templates[$template['templateid']]);
					}
					else {
						$ins_templates[$template['templateid']][$host[$id_field_name]] = $templateids;

						if (!$is_template_update && $templates_count > 1) {
							$double_linkage_scope[$template['templateid']][$host[$id_field_name]] = true;
						}
					}
				}

				foreach ($db_templates as $db_template) {
					$del_links[$db_template['templateid']][$host[$id_field_name]] = true;

					if (($this instanceof CHost || $this instanceof CTemplate) && $upd_templateids) {
						$del_templates[$db_template['templateid']][$host[$id_field_name]] = $upd_templateids;
					}
				}
			}
			elseif (array_key_exists('templates_clear', $host)) {
				$templateids = array_column($host['templates_clear'], 'templateid');
				$upd_templateids = [];

				foreach ($db_hosts[$host[$id_field_name]]['templates'] as $db_template) {
					if (!in_array($db_template['templateid'], $templateids)) {
						$upd_templateids[] = $db_template['templateid'];
					}
				}

				foreach ($host['templates_clear'] as $template) {
					$del_links[$template['templateid']][$host[$id_field_name]] = true;

					if (($this instanceof CHost || $this instanceof CTemplate) && $upd_templateids) {
						$del_templates[$template['templateid']][$host[$id_field_name]] = $upd_templateids;
					}
				}
			}
		}

		if ($del_templates) {
			$this->checkTriggerExpressionsOfDelTemplates($del_templates);
		}

		if ($ins_templates) {
			if ($this instanceof CTemplate && $db_hosts !== null) {
				self::checkCircularLinkageNew($ins_templates, $del_links);
			}

			if ($is_template_update || $double_linkage_scope) {
				$this->checkDoubleLinkageNew($ins_templates, $del_links, $double_linkage_scope);
			}

			$this->checkTriggerDependenciesOfInsTemplates($ins_templates);
			$this->checkTriggerExpressionsOfInsTemplates($ins_templates);
		}
	}

	/**
	 * Check whether all templates of triggers of unlinking templates are unlinked from target hosts or templates.
	 *
	 * @param array $del_templates
	 * @param array $del_templates[<templateid>][<hostid>]  Array of IDs of existing templates.
	 *
	 * @throws APIException if not linked template is found.
	 */
	protected function checkTriggerExpressionsOfDelTemplates(array $del_templates): void {
		$result = DBselect(
			'SELECT DISTINCT i.hostid AS del_templateid,f.triggerid,ii.hostid'.
			' FROM items i,functions f,functions ff,items ii'.
			' WHERE i.itemid=f.itemid'.
				' AND f.triggerid=ff.triggerid'.
				' AND ff.itemid=ii.itemid'.
				' AND '.dbConditionId('i.hostid', array_keys($del_templates))
		);

		while ($row = DBfetch($result)) {
			foreach ($del_templates[$row['del_templateid']] as $hostid => $upd_templateids) {
				if (in_array($row['hostid'], $upd_templateids)) {
					$objects = DB::select('hosts', [
						'output' => ['host', 'status'],
						'hostids' => [$row['del_templateid'], $row['hostid'], $hostid],
						'preservekeys' => true
					]);

					$triggers = DB::select('triggers', [
						'output' => ['description'],
						'triggerids' => $row['triggerid']
					]);

					$error = ($objects[$hostid]['status'] == HOST_STATUS_TEMPLATE)
						? _('Cannot unlink template "%1$s" without template "%2$s" from template "%3$s" due to expression of trigger "%4$s".')
						: _('Cannot unlink template "%1$s" without template "%2$s" from host "%3$s" due to expression of trigger "%4$s".');

					self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $objects[$row['del_templateid']]['host'],
						$objects[$row['hostid']]['host'], $objects[$hostid]['host'], $triggers[0]['description']
					));
				}
			}
		}
	}

	/**
	 * Check whether circular linkage occurs as a result of the given changes in templates links.
	 *
	 * @param array $ins_links[<templateid>][<hostid>]
	 * @param array $del_links[<templateid>][<hostid>]
	 *
	 * @throws APIException
	 */
	protected static function checkCircularLinkageNew(array $ins_links, array $del_links): void {
		$links = [];
		$_hostids = $ins_links;

		do {
			$result = DBselect(
				'SELECT ht.templateid,ht.hostid'.
				' FROM hosts_templates ht'.
				' WHERE '.dbConditionId('ht.hostid', array_keys($_hostids))
			);

			$_hostids = [];

			while ($row = DBfetch($result)) {
				if (array_key_exists($row['templateid'], $del_links)
						&& array_key_exists($row['hostid'], $del_links[$row['templateid']])) {
					continue;
				}

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

				$links[$row['templateid']][$row['hostid']] = true;
			}
		}
		while ($_hostids);

		foreach ($ins_links as $templateid => $hostids) {
			if (array_key_exists($templateid, $links)) {
				$links[$templateid] += $hostids;
			}
			else {
				$links[$templateid] = $ins_links[$templateid];
			}
		}

		foreach ($ins_links as $templateid => $hostids) {
			foreach ($hostids as $hostid => $foo) {
				if (array_key_exists($hostid, $links)){
					$links_path = [$hostid => true];

					if (self::circularLinkageExists($links, $templateid, $links[$hostid], $links_path)) {
						$template_name = '';

						$templates = DB::select('hosts', [
							'output' => ['hostid', 'host'],
							'hostids' => array_keys($links_path + [$templateid => true]),
							'preservekeys' => true
						]);

						foreach ($templates as $template) {
							$description = '"'.$template['host'].'"';

							if (bccomp($template['hostid'], $templateid) == 0) {
								$template_name = $description;
							}
							else {
								$links_path[$template['hostid']] = $description;
							}
						}

						$circular_linkage = (bccomp($templateid, $hostid) == 0)
							? $template_name.' -> '.$template_name
							: $template_name.' -> '.implode(' -> ', $links_path).' -> '.$template_name;

						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
							'Cannot link template "%1$s" to template "%2$s", because a circular linkage (%3$s) would occur.',
							$templates[$templateid]['host'], $templates[$hostid]['host'], $circular_linkage
						));
					}
				}
			}
		}
	}

	/**
	 * Recursively check whether given template to link forms a circular linkage.
	 *
	 * @param array  $links[<templateid>][<hostid>]
	 * @param string $templateid
	 * @param array  $hostids[<hostid>]
	 * @param array  $links_path                     Circular linkage path, collected performing the check.
	 *
	 * @return bool
	 */
	private static function circularLinkageExists(array $links, string $templateid, array $hostids,
			array &$links_path): bool {
		if (array_key_exists($templateid, $hostids)) {
			return true;
		}

		$_links_path = $links_path;

		foreach ($hostids as $hostid => $foo) {
			if (array_key_exists($hostid, $links)) {
				$links_path = $_links_path;
				$hostid_links = array_diff_key($links[$hostid], $links_path);

				if ($hostid_links) {
					$links_path[$hostid] = true;

					if (self::circularLinkageExists($links, $templateid, $hostid_links, $links_path)) {
						return true;
					}
				}
			}
		}

		return false;
	}

	/**
	 * Check whether double linkage occurs as a result of the given changes in template links.
	 *
	 * @param array      $ins_templates
	 * @param array      $ins_templates[<templateid>][<hostid>]  Array of template IDs to replace on target object.
	 * @param array      $del_links[<templateid>][<hostid>]
	 * @param array|null $scope[<templateid>][<hostid>]          The scope of template links to perform the double
	 *                                                           linkage check for. If null, all of $ins_templates
	 *                                                           links will be checked.
	 *
	 * @throws APIException
	 */
	protected static function checkDoubleLinkageNew(array $ins_templates, array $del_links, ?array $scope): void {
		$ins_hosts = self::getInsHosts($ins_templates, $scope);
		$scoped_ins_templates = self::getScopedInsTemplates($ins_templates, $scope, $db_templates);

		$targetids = $scoped_ins_templates + $db_templates;

		if ($scope === null) {
			$children = self::getChildren($ins_hosts + $ins_templates, $del_links, $ins_templates);

			$targetids += $ins_hosts + self::getTemplateOrTargetRelatedIds($children, $ins_hosts);
		}

		$parents = self::getParents($targetids, $del_links, $ins_hosts);

		self::checkParentsOfDbTemplatesLinkedTwice($db_templates, $parents);
		self::checkParentsOfInsTemplatesLinkedTwice($scoped_ins_templates, $parents);

		if ($scope === null) {
			self::addInsHostsParentsAndChildren($ins_hosts, $parents, $children);
			$children_parents = self::getChildrenParents($children, $parents);

			self::checkInsTemplatesLinkedTwiceOnTargetChildren($ins_hosts, $children_parents);
		}
	}

	/**
	 * Get an array indexed by targets of the given $ins_templates and their templates. If the given scope is partial,
	 * returns null.
	 *
	 * @param array      $ins_templates
	 * @param array      $ins_templates[<templateid>][<hostid>]  Array of template IDs to replace on target object.
	 * @param array|null $scope[<templateid>][<hostid>]          The scope of template links to perform the double
	 *                                                           linkage check for.
	 *
	 * @return array|null
	 */
	private static function getInsHosts(array $ins_templates, ?array $scope): ?array {
		if ($scope !== null) {
			return null;
		}

		$ins_hosts = [];

		foreach ($ins_templates as $templateid => $host_templates) {
			foreach ($host_templates as $hostid => $foo) {
				$ins_hosts[$hostid][$templateid] = [];
			}
		}

		return $ins_hosts;
	}

	/**
	 * Get an array of template links from the given $ins_templates to check for double linkage.
	 * The same target object will be referenced to a common array of template IDs to replace (to be updated later).
	 * Skip template links out of the given scope.
	 *
	 * @param array      $ins_templates
	 * @param array      $ins_templates[<templateid>][<hostid>]  Array of template IDs to replace on target object.
	 * @param array|null $scope[<templateid>][<hostid>]          The scope of template links to perform the double
	 *                                                           linkage check for. If null, all of $ins_templates
	 *                                                           links will be processed.
	 * @param array      $db_templates
	 * @param array      $db_templates[<templateid>][<hostid>]   Reference to a common array of template IDs to replace.
	 *
	 * @return array|null
	 */
	private static function getScopedInsTemplates(array $ins_templates, ?array $scope,
			?array &$db_templates = null): array {
		$scoped_ins_templates = [];
		$db_templates = [];

		foreach ($ins_templates as $templateid => $host_templates) {
			if ($scope !== null && !array_key_exists($templateid, $scope)) {
				continue;
			}

			foreach ($host_templates as $hostid => &$templateids) {
				if (($scope !== null && !array_key_exists($hostid, $scope[$templateid]))
						|| (array_key_exists($templateid, $scoped_ins_templates)
							&& array_key_exists($hostid, $scoped_ins_templates[$templateid]))) {
					continue;
				}

				$scoped_ins_templates[$templateid][$hostid] = &$templateids;

				foreach ($templateids as $_templateid) {
					if (bccomp($_templateid, $templateid) == 0) {
						continue;
					}

					if (array_key_exists($_templateid, $ins_templates)
							&& array_key_exists($hostid, $ins_templates[$_templateid])) {
						$scoped_ins_templates[$_templateid][$hostid] = &$templateids;
					}
					else {
						$db_templates[$_templateid][$hostid] = &$templateids;
					}
				}
			}
			unset($templateids);
		}

		return $scoped_ins_templates;
	}

	/**
	 * Recursively get children of the given template IDs.
	 *
	 * @param array      $templateids[<templateid>]
	 * @param array      $del_links
	 * @param array|null $ins_templates
	 *
	 * @return array
	 */
	private static function getChildren(array $templateids, array $del_links, ?array $ins_templates): array {
		$processed_templateids = $templateids;
		$children = [];

		do {
			$links = DB::select('hosts_templates', [
				'output' => ['templateid', 'hostid'],
				'filter' => ['templateid' => array_keys($templateids)]
			]);

			if ($ins_templates !== null) {
				foreach (array_intersect_key($ins_templates, $templateids) as $templateid => $hostids) {
					foreach ($hostids as $hostid => $foo) {
						$links[] = ['templateid' => $templateid, 'hostid' => $hostid];
					}
				}
			}

			$templateids = [];

			foreach ($links as $link) {
				if ($ins_templates !== null) {
					if (array_key_exists($link['templateid'], $del_links)
							&& array_key_exists($link['hostid'], $del_links[$link['templateid']])) {
						continue;
					}
				}

				if (!array_key_exists($link['hostid'], $processed_templateids)) {
					$templateids[$link['hostid']] = true;
					$processed_templateids[$link['hostid']] = true;
				}

				$children[$link['templateid']][] = $link['hostid'];
			}
		} while ($templateids);

		return $children;
	}

	/**
	 * Recursively get parents of the given target IDs.
	 *
	 * @param array      $targetids[<targetid>]
	 * @param array      $del_links
	 * @param array|null $ins_hosts
	 *
	 * @return array
	 */
	private static function getParents(array $targetids, array $del_links, ?array $ins_hosts): array {
		$processed_targetids = $targetids;
		$parents = [];

		do {
			$links = DB::select('hosts_templates', [
				'output' => ['templateid', 'hostid'],
				'filter' => ['hostid' => array_keys($targetids)]
			]);

			if ($ins_hosts !== null) {
				foreach (array_intersect_key($ins_hosts, $targetids) as $hostid => $templateids) {
					foreach ($templateids as $templateid => $foo) {
						$links[] = ['templateid' => $templateid, 'hostid' => $hostid];
					}
				}
			}

			$targetids = [];

			foreach ($links as $link) {
				if ($ins_hosts !== null) {
					if (array_key_exists($link['templateid'], $del_links)
							&& array_key_exists($link['hostid'], $del_links[$link['templateid']])) {
						continue;
					}
				}

				if (!array_key_exists($link['templateid'], $processed_targetids)) {
					$targetids[$link['templateid']] = true;
					$processed_targetids[$link['templateid']] = true;
				}

				$parents[$link['hostid']][] = $link['templateid'];
			}
		} while ($targetids);

		return $parents;
	}

	/**
	 * Check whether parents of already linked templates would be linked twice to target hosts or templates through new
	 * template linkage.
	 * Populate the referenced arrays of target object template IDs with the parents of the given $db_templates.
	 *
	 * @param array $db_templates
	 * @param array $parents
	 *
	 * @throws APIException
	 */
	private static function checkParentsOfDbTemplatesLinkedTwice(array $db_templates, array $parents): void {
		$_templateids = $db_templates;
		$children = [];

		do {
			$links = array_intersect_key($parents, $_templateids);

			$_templateids = [];

			foreach ($links as $link_templateid => $link_parent_templateids) {
				$db_templateids = self::getRootTemplateIds([$link_templateid => true], $children);

				foreach ($db_templateids as $templateid => $foo) {
					foreach ($db_templates[$templateid] as $hostid => &$templateids) {
						$double_templateids = array_intersect($link_parent_templateids, $templateids);

						if ($double_templateids) {
							$double_templateid = reset($double_templateids);

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

							if ($objects[$hostid]['status'] == HOST_STATUS_TEMPLATE) {
								$error = _('Cannot link template "%1$s" to template "%2$s", because it would be linked twice through template "%3$s".');
							}
							elseif ($objects[$hostid]['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
								$error = _('Cannot link template "%1$s" to host prototype "%2$s", because it would be linked twice through template "%3$s".');
							}
							else {
								$error = _('Cannot link template "%1$s" to host "%2$s", because it would be linked twice through template "%3$s".');
							}

							self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error,
								$objects[$double_templateid]['host'], $objects[$hostid]['host'],
								$objects[$templateid]['host']
							));
						}

						$templateids = array_merge($templateids, $link_parent_templateids);
					}
					unset($templateids);
				}

				foreach ($link_parent_templateids as $link_parent_templateid) {
					if (!array_key_exists($link_parent_templateid, $children)) {
						$_templateids[$link_parent_templateid] = true;
					}

					$children[$link_parent_templateid][] = $link_templateid;
				}
			}
		} while ($_templateids);
	}

	/**
	 * Check whether parents of templates to link would be linked twice to target hosts or templates.
	 * Populate the referenced arrays of target object template IDs with the parents of the given $ins_templates.
	 *
	 * @param array      $ins_templates
	 * @param array      $ins_templates[<templateid>][<hostid>]  Referenced array of target object template IDs.
	 * @param array      $parents
	 *
	 * @throws APIException
	 */
	private static function checkParentsOfInsTemplatesLinkedTwice(array $ins_templates, array $parents): void {
		$_templateids = $ins_templates;
		$children = [];

		do {
			$links = array_intersect_key($parents, $_templateids);

			$_templateids = [];

			foreach ($links as $link_templateid => $link_parent_templateids) {
				$ins_templateids = self::getRootTemplateIds([$link_templateid => true], $children);

				foreach ($ins_templateids as $ins_templateid => $foo) {
					foreach ($ins_templates[$ins_templateid] as $hostid => &$templateids) {
						$double_templateids = array_intersect($link_parent_templateids, $templateids);

						if ($double_templateids) {
							$double_templateid = reset($double_templateids);
							$objects = DB::select('hosts', [
								'output' => ['host', 'status', 'flags'],
								'hostids' => [$ins_templateid, $hostid, $double_templateid],
								'preservekeys' => true
							]);

							if ($objects[$hostid]['status'] == HOST_STATUS_TEMPLATE) {
								$error = _('Cannot link template "%1$s" to template "%2$s", because its parent template "%3$s" would be linked twice.');
							}
							elseif ($objects[$hostid]['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
								$error = _('Cannot link template "%1$s" to host prototype "%2$s", because its parent template "%3$s" would be linked twice.');
							}
							else {
								$error = _('Cannot link template "%1$s" to host "%2$s", because its parent template "%3$s" would be linked twice.');
							}

							self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $objects[$ins_templateid]['host'],
								$objects[$hostid]['host'], $objects[$double_templateid]['host']
							));
						}
						else {
							$templateids = array_merge($templateids, $link_parent_templateids);
						}
					}
					unset($templateids);
				}

				foreach ($link_parent_templateids as $link_parent_templateid) {
					if (!array_key_exists($link_parent_templateid, $children)) {
						$_templateids[$link_parent_templateid] = true;
					}

					$children[$link_parent_templateid][] = $link_templateid;
				}
			}
		} while ($_templateids);
	}

	/**
	 * Add the parent and children relations of each template to link to the given $ins_hosts.
	 *
	 * @param array $ins_hosts
	 * @param array $parents
	 * @param array $children
	 */
	private static function addInsHostsParentsAndChildren(array &$ins_hosts, array $parents, array $children): void {
		foreach ($ins_hosts as &$template_data) {
			foreach ($template_data as $templateid => &$data) {
				$data['parents'] = self::getTemplateOrTargetRelatedIds($parents, [$templateid => true]);
				$data['children'] = self::getTemplateOrTargetRelatedIds($children, [$templateid => true]);
			}
			unset($data);
		}
		unset($template_data);
	}

	/**
	 * Get the direct parents of each given children template.
	 *
	 * @param array $children
	 * @param array $parents
	 *
	 * @return array
	 */
	private static function getChildrenParents(array $children, array $parents): array {
		$children_parents = [];

		foreach ($children as $templateid => $targetids) {
			foreach ($targetids as $targetid) {
				$children_parents[$templateid][$targetid] =
					self::getTemplateOrTargetRelatedIds($parents, [$targetid => true], $templateid);
			}
		}

		return $children_parents;
	}

	/**
	 * Check whether templates to link, its parents, or children are encountered between parents of target templates'
	 * children.
	 *
	 * @param array $ins_hosts
	 * @param array $children_templates
	 *
	 * @throws APIException
	 */
	private static function checkInsTemplatesLinkedTwiceOnTargetChildren(array $ins_hosts,
			array $children_parents): void {
		$_templateids = $ins_hosts;
		$_parents = [];

		do {
			$links = array_intersect_key($children_parents, $_templateids);

			$_templateids = [];

			foreach ($links as $link_templateid => $host_parent_templates) {
				$ins_hostids = self::getRootTemplateIds([$link_templateid => true], $_parents);

				foreach ($ins_hostids as $ins_hostid => $foo) {
					foreach ($ins_hosts[$ins_hostid] as $templateid => $data) {
						foreach ($host_parent_templates as $hostid => $parent_templateids) {
							if (array_key_exists($templateid, $parent_templateids)
									|| array_intersect_key($data['children'], $parent_templateids)) {
								$objects = DB::select('hosts', [
									'output' => ['host', 'status', 'flags'],
									'hostids' => [$templateid, $ins_hostid, $hostid],
									'preservekeys' => true
								]);

								if ($objects[$hostid]['status'] == HOST_STATUS_TEMPLATE) {
									$error = _('Cannot link template "%1$s" to template "%2$s", because it would be linked to template "%3$s" twice.');
								}
								elseif ($objects[$hostid]['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
									$error = _('Cannot link template "%1$s" to template "%2$s", because it would be linked to host prototype "%3$s" twice.');
								}
								else {
									$error = _('Cannot link template "%1$s" to template "%2$s", because it would be linked to host "%3$s" twice.');
								}

								self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error,
									$objects[$templateid]['host'], $objects[$ins_hostid]['host'],
									$objects[$hostid]['host']
								));
							}

							$double_templateids = array_intersect_key($data['parents'], $parent_templateids);

							if ($double_templateids) {
								$double_templateid = key($double_templateids);

								$objects = DB::select('hosts', [
									'output' => ['host', 'status', 'flags'],
									'hostids' => [$templateid, $ins_hostid, $double_templateid, $hostid],
									'preservekeys' => true
								]);

								if ($objects[$hostid]['status'] == HOST_STATUS_TEMPLATE) {
									$error = _('Cannot link template "%1$s" to template "%2$s", because its parent template "%3$s" would be linked to template "%4$s" twice.');
								}
								elseif ($objects[$hostid]['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
									$error = _('Cannot link template "%1$s" to template "%2$s", because its parent template "%3$s" would be linked to host prototype "%4$s" twice.');
								}
								else {
									$error = _('Cannot link template "%1$s" to template "%2$s", because its parent template "%3$s" would be linked to host "%4$s" twice.');
								}

								self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error,
									$objects[$templateid]['host'], $objects[$ins_hostid]['host'],
									$objects[$double_templateid]['host'], $objects[$hostid]['host']
								));
							}
						}
					}
				}

				foreach ($host_parent_templates as $hostid => $foo) {
					if (!array_key_exists($hostid, $_parents)) {
						$_templateids[$hostid] = true;
						$_parents[$hostid][] = $link_templateid;
					}
				}
			}
		} while ($_templateids);
	}

	/**
	 * Get IDs of targets linked to given templates or IDs of templates linked to given targets.
	 *
	 * @param array       $links
	 * @param array       $sourceids
	 * @param string|null $ignore_relatedid
	 *
	 * @return array
	 */
	private static function getTemplateOrTargetRelatedIds(array $links, array $sourceids,
			?string $ignore_relatedid = null): array {
		$processed_sourceids = $sourceids;
		$relatedids = [];

		do {
			$scoped_links = array_intersect_key($links, $sourceids);

			$sourceids = [];

			foreach ($scoped_links as $_relatedids) {
				foreach ($_relatedids as $relatedid) {
					if ($ignore_relatedid !== null && bccomp($relatedid, $ignore_relatedid) == 0) {
						continue;
					}

					if (!array_key_exists($relatedid, $processed_sourceids)) {
						$sourceids[$relatedid] = true;
						$processed_sourceids[$relatedid] = true;
					}

					$relatedids[$relatedid] = true;
				}
			}

			$ignore_relatedid = null;
		} while ($sourceids);

		return $relatedids;
	}

	/**
	 * Recursively collects the roots of the given children or parent templates.
	 *
	 * @param array $templateids
	 * @param array $template_links
	 *
	 * @return array
	 */
	private static function getRootTemplateIds(array $templateids, array $template_links): array {
		$root_templateids = $templateids;

		foreach ($templateids as $templateid => $foo) {
			if (array_key_exists($templateid, $template_links)) {
				unset($root_templateids[$templateid]);

				$root_templateids +=
					self::getRootTemplateIds(array_flip($template_links[$templateid]), $template_links);
			}
		}

		return $root_templateids;
	}

	/**
	 * Check whether all templates of triggers, from which depends the triggers of linking templates, are linked to
	 * target hosts or templates.
	 *
	 * @param array  $ins_templates
	 * @param array  $ins_templates[<templateid>][<hostid>]  Array of template IDs to replace on target object.
	 *
	 * @throws APIException if not linked template is found.
	 */
	protected function checkTriggerDependenciesOfInsTemplates(array $ins_templates): void {
		$result = DBselect(
			'SELECT DISTINCT i.hostid AS ins_templateid,td.triggerid_down,ii.hostid'.
			' FROM items i,functions f,trigger_depends td,functions ff,items ii,hosts h'.
			' WHERE i.itemid=f.itemid'.
				' AND f.triggerid=td.triggerid_down'.
				' AND td.triggerid_up=ff.triggerid'.
				' AND ff.itemid=ii.itemid'.
				' AND ii.hostid=h.hostid'.
				' AND '.dbConditionId('i.hostid', array_keys($ins_templates)).
				' AND '.dbConditionInt('h.status', [HOST_STATUS_TEMPLATE])
		);

		while ($row = DBfetch($result)) {
			foreach ($ins_templates[$row['ins_templateid']] as $hostid => $templateids) {
				if (bccomp($row['hostid'], $hostid) == 0 && $this instanceof CTemplate) {
					$objects = DB::select('hosts', [
						'output' => ['host'],
						'hostids' => [$row['ins_templateid'], $hostid],
						'preservekeys' => true
					]);

					$triggers = DB::select('triggers', [
						'output' => ['description'],
						'triggerids' => $row['triggerid_down']
					]);

					self::exception(ZBX_API_ERROR_PARAMETERS,
						_s('Cannot link template "%1$s" to template "%2$s" due to dependency of trigger "%3$s".',
							$objects[$row['ins_templateid']]['host'], $objects[$hostid]['host'],
							$triggers[0]['description']
						)
					);
				}

				if (!in_array($row['hostid'], $templateids)) {
					$objects = DB::select('hosts', [
						'output' => ['host', 'status', 'flags'],
						'hostids' => [$row['ins_templateid'], $row['hostid'], $hostid],
						'preservekeys' => true
					]);

					$triggers = DB::select('triggers', [
						'output' => ['description'],
						'triggerids' => $row['triggerid_down']
					]);

					if ($objects[$hostid]['status'] == HOST_STATUS_TEMPLATE) {
						$error = _('Cannot link template "%1$s" without template "%2$s" to template "%3$s" due to dependency of trigger "%4$s".');
					}
					elseif ($objects[$hostid]['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
						$error = _('Cannot link template "%1$s" without template "%2$s" to host prototype "%3$s" due to dependency of trigger "%4$s".');
					}
					else {
						$error = _('Cannot link template "%1$s" without template "%2$s" to host "%3$s" due to dependency of trigger "%4$s".');
					}

					self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $objects[$row['ins_templateid']]['host'],
						$objects[$row['hostid']]['host'], $objects[$hostid]['host'], $triggers[0]['description']
					));
				}
			}
		}

		if ($this instanceof CTemplate) {
			$hostids = [];

			foreach ($ins_templates as $hostids_templateids) {
				foreach ($hostids_templateids as $hostid => $templateids) {
					$hostids[$hostid] = true;
				}
			}

			$result = DBselect(
				'SELECT DISTINCT i.hostid AS ins_templateid,td.triggerid_down,ii.hostid'.
				' FROM items i,functions f,trigger_depends td,functions ff,items ii'.
				' WHERE i.itemid=f.itemid'.
					' AND f.triggerid=td.triggerid_up'.
					' AND td.triggerid_down=ff.triggerid'.
					' AND ff.itemid=ii.itemid'.
					' AND '.dbConditionId('i.hostid', array_keys($ins_templates)).
					' AND '.dbConditionId('ii.hostid', array_keys($hostids))
			);

			while ($row = DBfetch($result)) {
				if (array_key_exists($row['hostid'], $ins_templates[$row['ins_templateid']])) {
					$objects = DB::select('hosts', [
						'output' => ['host'],
						'hostids' => [$row['ins_templateid'], $row['hostid']],
						'preservekeys' => true
					]);

					$triggers = DB::select('triggers', [
						'output' => ['description'],
						'triggerids' => $row['triggerid_down']
					]);

					self::exception(ZBX_API_ERROR_PARAMETERS,
						_s('Cannot link template "%1$s" to template "%2$s" due to dependency of trigger "%3$s".',
							$objects[$row['ins_templateid']]['host'], $objects[$row['hostid']]['host'],
							$triggers[0]['description']
						)
					);
				}
			}
		}
	}

	/**
	 * Check whether all templates of triggers of linking templates are linked to target hosts or templates.
	 *
	 * @param array  $ins_templates
	 * @param array  $ins_templates[<templateid>][<hostid>]  Array of template IDs to replace on target object.
	 *
	 * @throws APIException if not linked template is found.
	 */
	protected function checkTriggerExpressionsOfInsTemplates(array $ins_templates): void {
		$result = DBselect(
			'SELECT DISTINCT i.hostid AS ins_templateid,f.triggerid,ii.hostid'.
			' FROM items i,functions f,functions ff,items ii'.
			' WHERE i.itemid=f.itemid'.
				' AND f.triggerid=ff.triggerid'.
				' AND ff.itemid=ii.itemid'.
				' AND '.dbConditionId('i.hostid', array_keys($ins_templates))
		);

		while ($row = DBfetch($result)) {
			foreach ($ins_templates[$row['ins_templateid']] as $hostid => $templateids) {
				if (bccomp($row['hostid'], $hostid) == 0 && $this instanceof CTemplate) {
					$objects = DB::select('hosts', [
						'output' => ['host'],
						'hostids' => [$row['ins_templateid'], $hostid],
						'preservekeys' => true
					]);

					$triggers = DB::select('triggers', [
						'output' => ['description'],
						'triggerids' => $row['triggerid']
					]);

					self::exception(ZBX_API_ERROR_PARAMETERS,
						_s('Cannot link template "%1$s" to template "%2$s" due to expression of trigger "%3$s".',
							$objects[$row['ins_templateid']]['host'], $objects[$hostid]['host'],
							$triggers[0]['description']
						)
					);
				}

				if (!in_array($row['hostid'], $templateids)) {
					$objects = DB::select('hosts', [
						'output' => ['host', 'status', 'flags'],
						'hostids' => [$row['ins_templateid'], $row['hostid'], $hostid],
						'preservekeys' => true
					]);

					$triggers = DB::select('triggers', [
						'output' => ['description'],
						'triggerids' => $row['triggerid']
					]);

					if ($objects[$hostid]['status'] == HOST_STATUS_TEMPLATE) {
						$error = _('Cannot link template "%1$s" without template "%2$s" to template "%3$s" due to expression of trigger "%4$s".');
					}
					elseif ($objects[$hostid]['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
						$error = _('Cannot link template "%1$s" without template "%2$s" to host prototype "%3$s" due to expression of trigger "%4$s".');
					}
					else {
						$error = _('Cannot link template "%1$s" without template "%2$s" to host "%3$s" due to expression of trigger "%4$s".');
					}

					self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $objects[$row['ins_templateid']]['host'],
						$objects[$row['hostid']]['host'], $objects[$hostid]['host'], $triggers[0]['description']
					));
				}
			}
		}
	}

	/**
	 * Update table "hosts_templates".
	 *
	 * @param array      $hosts
	 * @param array|null $db_hosts
	 * @param array|null $upd_hostids
	 */
	protected function updateTemplates(array &$hosts, ?array &$db_hosts = null, ?array &$upd_hostids = null): void {
		$id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid';

		$ins_hosts_templates = [];
		$del_hosttemplateids = [];

		foreach ($hosts as $i => &$host) {
			if (!array_key_exists('templates', $host) && !array_key_exists('templates_clear', $host)) {
				continue;
			}

			$db_templates = ($db_hosts !== null)
				? array_column($db_hosts[$host[$id_field_name]]['templates'], null, 'templateid')
				: [];
			$changed = false;

			if (array_key_exists('templates', $host)) {
				foreach ($host['templates'] as &$template) {
					if (array_key_exists($template['templateid'], $db_templates)) {
						$template['hosttemplateid'] = $db_templates[$template['templateid']]['hosttemplateid'];
						unset($db_templates[$template['templateid']]);
					}
					else {
						$ins_hosts_templates[] = [
							'hostid' => $host[$id_field_name],
							'templateid' => $template['templateid']
						];
						$changed = true;
					}
				}
				unset($template);

				$templates_clear_indexes = [];

				if (array_key_exists('templates_clear', $host)) {
					foreach ($host['templates_clear'] as $index => $template) {
						$templates_clear_indexes[$template['templateid']] = $index;
					}
				}

				foreach ($db_templates as $del_template) {
					$changed = true;
					$del_hosttemplateids[] = $del_template['hosttemplateid'];

					if (array_key_exists($del_template['templateid'], $templates_clear_indexes)) {
						$index = $templates_clear_indexes[$del_template['templateid']];
						$host['templates_clear'][$index]['hosttemplateid'] = $del_template['hosttemplateid'];
					}
				}
			}
			elseif (array_key_exists('templates_clear', $host)) {
				foreach ($host['templates_clear'] as &$template) {
					$template['hosttemplateid'] = $db_templates[$template['templateid']]['hosttemplateid'];
					$del_hosttemplateids[] = $db_templates[$template['templateid']]['hosttemplateid'];
				}
				unset($template);
			}

			if ($db_hosts !== null) {
				if ($changed) {
					$upd_hostids[$i] = $host[$id_field_name];
				}
				else {
					unset($host['templates'], $db_hosts[$host[$id_field_name]]['templates']);
				}
			}
		}
		unset($host);

		if ($del_hosttemplateids) {
			DB::delete('hosts_templates', ['hosttemplateid' => $del_hosttemplateids]);
		}

		if ($ins_hosts_templates) {
			$hosttemplateids = DB::insertBatch('hosts_templates', $ins_hosts_templates);
		}

		foreach ($hosts as &$host) {
			if (!array_key_exists('templates', $host)) {
				continue;
			}

			foreach ($host['templates'] as &$template) {
				if (!array_key_exists('hosttemplateid', $template)) {
					$template['hosttemplateid'] = array_shift($hosttemplateids);
				}
			}
			unset($template);
		}
		unset($host);
	}

	/**
	 * @param array      $hosts
	 * @param array|null $db_hosts
	 * @param array|null $upd_hostids
	 */
	protected function updateTags(array &$hosts, ?array &$db_hosts = null, ?array &$upd_hostids = null): void {
		$id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid';

		$ins_tags = [];
		$del_hosttagids = [];

		foreach ($hosts as $i => &$host) {
			if (!array_key_exists('tags', $host)) {
				continue;
			}

			$changed = false;
			$db_tags = ($db_hosts !== null) ? $db_hosts[$host[$id_field_name]]['tags'] : [];

			$hosttagid_by_tag_value = [];

			foreach ($db_tags as $db_tag) {
				$hosttagid_by_tag_value[$db_tag['tag']][$db_tag['value']] = $db_tag['hosttagid'];
			}

			foreach ($host['tags'] as &$tag) {
				if (array_key_exists($tag['tag'], $hosttagid_by_tag_value)
						&& array_key_exists($tag['value'], $hosttagid_by_tag_value[$tag['tag']])) {
					$tag['hosttagid'] = $hosttagid_by_tag_value[$tag['tag']][$tag['value']];
					unset($db_tags[$tag['hosttagid']]);
				}
				else {
					$ins_tags[] = ['hostid' => $host[$id_field_name]] + $tag;
					$changed  = true;
				}
			}
			unset($tag);

			$db_tags = array_filter($db_tags, static function (array $db_tag): bool {
				return $db_tag['automatic'] == ZBX_TAG_MANUAL;
			});

			if ($db_tags) {
				$del_hosttagids = array_merge($del_hosttagids, array_keys($db_tags));
				$changed = true;
			}

			if ($db_hosts !== null) {
				if ($changed) {
					$upd_hostids[$i] = $host[$id_field_name];
				}
				else {
					unset($host['tags'], $db_hosts[$host[$id_field_name]]['tags']);
				}
			}
		}
		unset($host);

		if ($del_hosttagids) {
			DB::delete('host_tag', ['hosttagid' => $del_hosttagids]);
		}

		if ($ins_tags) {
			$hosttagids = DB::insert('host_tag', $ins_tags);
		}

		foreach ($hosts as &$host) {
			if (!array_key_exists('tags', $host)) {
				continue;
			}

			foreach ($host['tags'] as &$tag) {
				if (!array_key_exists('hosttagid', $tag)) {
					$tag['hosttagid'] = array_shift($hosttagids);
				}
			}
			unset($tag);
		}
		unset($host);
	}

	/**
	 * @param array      $hosts
	 * @param array|null $db_hosts
	 * @param array|null $upd_hostids
	 */
	protected static function updateMacros(array &$hosts, ?array &$db_hosts = null, ?array &$upd_hostids = null): void {
		$id_field_name = self::isTemplate() ? 'templateid' : 'hostid';

		$ins_hostmacros = [];
		$upd_hostmacros = [];
		$del_hostmacroids = [];

		foreach ($hosts as $i => &$host) {
			if (!array_key_exists('macros', $host)) {
				continue;
			}

			$changed = false;
			$db_macros = ($db_hosts !== null) ? $db_hosts[$host[$id_field_name]]['macros'] : [];

			foreach ($host['macros'] as &$macro) {
				if (array_key_exists('hostmacroid', $macro) && $db_macros) {
					$upd_hostmacro = DB::getUpdatedValues('hostmacro', $macro, $db_macros[$macro['hostmacroid']]);

					if ($upd_hostmacro) {
						$upd_hostmacros[] = [
							'values' => $upd_hostmacro,
							'where' => ['hostmacroid' => $macro['hostmacroid']]
						];
						$changed = true;
					}

					unset($db_macros[$macro['hostmacroid']]);
				}
				else {
					$ins_hostmacros[] = ['hostid' => $host[$id_field_name]] + $macro;
					$changed = true;
				}
			}
			unset($macro);

			if ($db_macros) {
				$del_hostmacroids = array_merge($del_hostmacroids, array_keys($db_macros));
				$changed = true;
			}

			if (self::isHostPrototype() && $db_hosts !== null) {
				if ($changed) {
					$upd_hostids[$i] = $host[$id_field_name];
				}
				else {
					unset($host['macros'], $db_hosts[$host[$id_field_name]]['macros']);
				}
			}
		}
		unset($host);

		if ($del_hostmacroids) {
			DB::delete('hostmacro', ['hostmacroid' => $del_hostmacroids]);
		}

		if ($upd_hostmacros) {
			DB::update('hostmacro', $upd_hostmacros);
		}

		if ($ins_hostmacros) {
			$hostmacroids = DB::insert('hostmacro', $ins_hostmacros);
		}

		if (self::isTemplate()) {
			$macros = [];
			$db_macros = [];

			foreach ($hosts as &$host) {
				if (!array_key_exists('macros', $host)) {
					continue;
				}

				foreach ($host['macros'] as &$macro) {
					if (!array_key_exists('hostmacroid', $macro)) {
						$macro['hostmacroid'] = array_shift($hostmacroids);

						if (array_key_exists('config', $macro)) {
							$macros[] = $macro;
						}
					}
					elseif (array_key_exists('config', $macro)) {
						$macros[] = $macro;
						$db_macros[$macro['hostmacroid']] =
							$db_hosts[$host[$id_field_name]]['macros'][$macro['hostmacroid']];
					}
				}
				unset($macro);
			}
			unset($host);

			if ($macros) {
				self::updateMacroConfigs($macros, $db_macros);
			}
		}
		else {
			foreach ($hosts as &$host) {
				if (!array_key_exists('macros', $host)) {
					continue;
				}

				foreach ($host['macros'] as &$macro) {
					if (!array_key_exists('hostmacroid', $macro)) {
						$macro['hostmacroid'] = array_shift($hostmacroids);
					}
				}
				unset($macro);
			}
			unset($host);
		}
	}

	private static function updateMacroConfigs(array $macros, array $db_macros): void {
		$ins_hostmacro_configs = [];
		$upd_hostmacro_configs = [];
		$del_hostmacroids = [];

		foreach ($macros as $macro) {
			if (array_key_exists('options', $macro['config'])) {
				$macro['config']['options'] = $macro['config']['options']
					? json_encode($macro['config']['options'])
					: '';
			}

			if (array_key_exists($macro['hostmacroid'], $db_macros)) {
				$db_macro = $db_macros[$macro['hostmacroid']];

				$db_macro['config']['options'] = $db_macro['config']['options']
					? json_encode($db_macro['config']['options'])
					: '';

				if ($macro['config']['type'] != $db_macro['config']['type']) {
					if ($db_macro['config']['type'] == ZBX_WIZARD_FIELD_NOCONF) {
						$ins_hostmacro_configs[] = ['hostmacroid' => $macro['hostmacroid']] + $macro['config'];
					}
					elseif ($macro['config']['type'] == ZBX_WIZARD_FIELD_NOCONF) {
						$del_hostmacroids[] = $macro['hostmacroid'];
					}
					else {
						self::addMacroConfigFieldDefaultsByType($macro['config'], $db_macro['config']);

						$upd_hostmacro_configs[] = [
							'values' => DB::getUpdatedValues('hostmacro_config', $macro['config'], $db_macro['config']),
							'where' => ['hostmacroid' => $macro['hostmacroid']]
						];
					}
				}
				elseif ($macro['config']['type'] != ZBX_WIZARD_FIELD_NOCONF) {
					$upd_hostmacro_config =
						DB::getUpdatedValues('hostmacro_config', $macro['config'], $db_macro['config']);

					if ($upd_hostmacro_config) {
						$upd_hostmacro_configs[] = [
							'values' => $upd_hostmacro_config,
							'where' => ['hostmacroid' => $macro['hostmacroid']]
						];
					}
				}
			}
			elseif ($macro['config']['type'] != ZBX_WIZARD_FIELD_NOCONF) {
				$ins_hostmacro_configs[] = ['hostmacroid' => $macro['hostmacroid']] + $macro['config'];
			}
		}

		if ($del_hostmacroids) {
			DB::delete('hostmacro_config', ['hostmacroid' => $del_hostmacroids]);
		}

		if ($upd_hostmacro_configs) {
			DB::update('hostmacro_config', $upd_hostmacro_configs);
		}

		if ($ins_hostmacro_configs) {
			DB::insert('hostmacro_config', $ins_hostmacro_configs, false);
		}
	}

	public static function addMacroConfigFieldDefaultsByType(array &$config, array $db_config): void {
		$type_fields = [
			ZBX_WIZARD_FIELD_TEXT => ['priority', 'section_name', 'label', 'description', 'required', 'regex'],
			ZBX_WIZARD_FIELD_LIST => ['priority', 'section_name', 'label', 'description', 'required', 'options'],
			ZBX_WIZARD_FIELD_CHECKBOX => ['priority', 'section_name', 'label', 'description', 'options']
		];

		$config += array_intersect_key(DB::getDefaults('hostmacro_config'),
			array_flip(array_diff($type_fields[$db_config['type']], $type_fields[$config['type']]))
		);
	}

	/**
	 * Checks user macros for host.update, template.update and hostprototype.update methods.
	 *
	 * @param array  $hosts
	 * @param array  $hosts[]['templateid|hostid']
	 * @param array  $hosts[]['macros']             (optional)
	 * @param array  $db_hosts
	 * @param array  $db_hosts[<hostid>]['macros']
	 *
	 * @return array Array of passed hosts/templates with padded macros data, when it's necessary.
	 *
	 * @throws APIException if input of host macros data is invalid.
	 */
	protected function validateHostMacros(array $hosts, array $db_hosts): array {
		$hostmacro_defaults = [
			'type' => DB::getDefault('hostmacro', 'type')
		];

		$id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid';

		foreach ($hosts as $i1 => &$host) {
			if (!array_key_exists('macros', $host)) {
				continue;
			}

			$db_host = $db_hosts[$host[$id_field_name]];
			$path = '/'.($i1 + 1).'/macros';

			$db_macros = array_column($db_host['macros'], 'hostmacroid', 'macro');
			$macros = [];

			foreach ($host['macros'] as $i2 => &$hostmacro) {
				if (!array_key_exists('hostmacroid', $hostmacro)) {
					foreach (['macro', 'value'] as $field_name) {
						if (!array_key_exists($field_name, $hostmacro)) {
							self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
								$path.'/'.($i2 + 1), _s('the parameter "%1$s" is missing', $field_name)
							));
						}
					}

					$hostmacro += $hostmacro_defaults;
				}
				else {
					if (!array_key_exists($hostmacro['hostmacroid'], $db_host['macros'])) {
						self::exception(ZBX_API_ERROR_PERMISSIONS,
							_('No permissions to referred object or it does not exist!')
						);
					}

					$db_hostmacro = $db_host['macros'][$hostmacro['hostmacroid']];

					// Check if this is not an attempt to modify automatic host macro.
					if ($this instanceof CHost) {
						$macro_fields = array_flip(['macro', 'value', 'type', 'description']);
						$hostmacro += array_intersect_key($db_hostmacro, array_flip(['automatic']));

						if ($hostmacro['automatic'] == ZBX_USERMACRO_AUTOMATIC
								&& array_diff_assoc(array_intersect_key($hostmacro, $macro_fields), $db_hostmacro)) {
							self::exception(ZBX_API_ERROR_PERMISSIONS,
								_s('Not allowed to modify automatic user macro "%1$s".', $db_hostmacro['macro'])
							);
						}
					}

					$hostmacro += array_intersect_key($db_hostmacro, array_flip(['macro', 'type']));

					if ($hostmacro['type'] != $db_hostmacro['type']) {
						if ($db_hostmacro['type'] == ZBX_MACRO_TYPE_SECRET) {
							$hostmacro += ['value' => ''];
						}

						if ($hostmacro['type'] == ZBX_MACRO_TYPE_VAULT) {
							$hostmacro += ['value' => $db_hostmacro['value']];
						}
					}

					$macros[$hostmacro['hostmacroid']] = $hostmacro['macro'];
				}

				if (array_key_exists('value', $hostmacro) && $hostmacro['type'] == ZBX_MACRO_TYPE_VAULT) {
					if (!CApiInputValidator::validate([
								'type' => API_VAULT_SECRET,
								'provider' => CSettingsHelper::get(CSettingsHelper::VAULT_PROVIDER)
							], $hostmacro['value'], $path.'/'.($i2 + 1).'/value', $error)) {
						self::exception(ZBX_API_ERROR_PARAMETERS, $error);
					}
				}
			}
			unset($hostmacro);

			// Checking for cross renaming of existing macros.
			foreach ($macros as $hostmacroid => $macro) {
				if (array_key_exists($macro, $db_macros) && bccomp($hostmacroid, $db_macros[$macro]) != 0
						&& array_key_exists($db_macros[$macro], $macros)) {
					$hosts = DB::select('hosts', [
						'output' => ['name'],
						'hostids' => $host[$id_field_name]
					]);

					self::exception(ZBX_API_ERROR_PARAMETERS,
						_s('Macro "%1$s" already exists on "%2$s".', $macro, $hosts[0]['name'])
					);
				}
			}

			$api_input_rules = ['type' => API_OBJECTS, 'uniq' => [['macro']], 'fields' => [
				'macro' =>	['type' => API_USER_MACRO]
			]];

			if (!CApiInputValidator::validateUniqueness($api_input_rules, $host['macros'], $path, $error)) {
				self::exception(ZBX_API_ERROR_PARAMETERS, $error);
			}
		}
		unset($host);

		return $hosts;
	}

	/**
	 * @param array $hosts
	 * @param array $db_hosts
	 */
	protected function addAffectedTemplates(array $hosts, array &$db_hosts): void {
		$id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid';

		$hostids = [];

		foreach ($hosts as $host) {
			if (array_key_exists('templates', $host) || array_key_exists('templates_clear', $host)) {
				$hostids[] = $host[$id_field_name];
				$db_hosts[$host[$id_field_name]]['templates'] = [];
			}
		}

		if (!$hostids) {
			return;
		}

		$permitted_templates = [];

		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
			$permitted_templates = API::Template()->get([
				'output' => [],
				'hostids' => $hostids,
				'preservekeys' => true
			]);
		}

		$options = [
			'output' => ['hosttemplateid', 'hostid', 'templateid', 'link_type'],
			'filter' => ['hostid' => $hostids]
		];
		$db_templates = DBselect(DB::makeSql('hosts_templates', $options));

		while ($db_template = DBfetch($db_templates)) {
			if (self::$userData['type'] == USER_TYPE_SUPER_ADMIN
					|| array_key_exists($db_template['templateid'], $permitted_templates)) {
				$db_hosts[$db_template['hostid']]['templates'][$db_template['hosttemplateid']] =
					array_diff_key($db_template, array_flip(['hostid']));
			}
			else {
				$db_hosts[$db_template['hostid']]['nopermissions_templates'][$db_template['hosttemplateid']] =
					array_diff_key($db_template, array_flip(['hostid']));
			}
		}
	}

	/**
	 * @param array $hosts
	 * @param array $db_hosts
	 */
	protected function addAffectedTags(array $hosts, array &$db_hosts): void {
		$id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid';

		$hostids = [];

		foreach ($hosts as $host) {
			if (array_key_exists('tags', $host)) {
				$hostids[] = $host[$id_field_name];
				$db_hosts[$host[$id_field_name]]['tags'] = [];
			}
		}

		if (!$hostids) {
			return;
		}

		$options = [
			'output' => ['hosttagid', 'hostid', 'tag', 'value', 'automatic'],
			'filter' => ['hostid' => $hostids]
		];
		$db_tags = DBselect(DB::makeSql('host_tag', $options));

		while ($db_tag = DBfetch($db_tags)) {
			$db_hosts[$db_tag['hostid']]['tags'][$db_tag['hosttagid']] =
				array_diff_key($db_tag, array_flip(['hostid']));
		}
	}

	protected static function addAffectedMacros(array $hosts, array &$db_hosts): void {
		$id_field_name = self::isTemplate() ? 'templateid' : 'hostid';

		$hostids = [];

		foreach ($hosts as $host) {
			if (array_key_exists('macros', $host)) {
				$hostids[] = $host[$id_field_name];
				$db_hosts[$host[$id_field_name]]['macros'] = [];
			}
		}

		if (!$hostids) {
			return;
		}

		$options = [
			'output' => ['hostmacroid', 'hostid', 'macro', 'value', 'description', 'type', 'automatic'],
			'filter' => ['hostid' => $hostids]
		];
		$resource = DBselect(DB::makeSql('hostmacro', $options));

		if (self::isTemplate()) {
			$db_macros = [];

			while ($db_macro = DBfetch($resource)) {
				$db_hosts[$db_macro['hostid']]['macros'][$db_macro['hostmacroid']] =
					array_diff_key($db_macro, array_flip(['hostid']));

				$db_macros[$db_macro['hostmacroid']] =
					&$db_hosts[$db_macro['hostid']]['macros'][$db_macro['hostmacroid']];
			}

			if ($db_macros) {
				self::addAffectedMacroConfigs($db_macros);
			}
		}
		else {
			while ($db_macro = DBfetch($resource)) {
				$db_hosts[$db_macro['hostid']]['macros'][$db_macro['hostmacroid']] =
					array_diff_key($db_macro, array_flip(['hostid']));
			}
		}
	}

	private static function addAffectedMacroConfigs(array &$db_macros): void {
		foreach ($db_macros as &$db_macro) {
			$db_macro['config'] = ['options' => []] + DB::getDefaults('hostmacro_config');
		}
		unset($db_macro);

		$options = [
			'output' => ['hostmacroid', 'type', 'priority', 'section_name', 'label', 'description', 'required', 'regex',
				'options'
			],
			'hostmacroids' => array_keys($db_macros)
		];
		$resource = DBselect(DB::makeSql('hostmacro_config', $options));

		while ($db_config = DBfetch($resource)) {
			$db_config['options'] = $db_config['options'] !== '' ? json_decode($db_config['options'], true) : [];

			$db_macros[$db_config['hostmacroid']]['config'] = array_diff_key($db_config, array_flip(['hostmacroid']));
		}
	}

	protected static function addRelatedMacros(array $options, array &$result): void {
		if ($options['selectMacros'] === null) {
			return;
		}

		foreach ($result as &$host) {
			$host['macros'] = [];
		}
		unset($host);

		$internal_fields = ['hostmacroid', 'hostid'];

		$limit_selects_requested = (self::isHost() || self::isTemplate()) && $options['limitSelects'] !== null;

		if ($limit_selects_requested) {
			$internal_fields[] = 'macro';
		}

		$macros = API::UserMacro()->get([
			'output' => array_unique(array_merge($internal_fields, $options['selectMacros'])),
			'hostids' => array_keys($result),
			'nopermissions' => true
		]);

		if ($limit_selects_requested) {
			CArrayHelper::sort($macros, ['macro']);
		}

		$fields_to_unset = array_flip(array_diff($internal_fields, $options['selectMacros']));

		foreach ($macros as $macro) {
			if ($limit_selects_requested && count($result[$macro['hostid']]['macros']) == $options['limitSelects']) {
				continue;
			}

			$result[$macro['hostid']]['macros'][] = array_diff_key($macro, $fields_to_unset);
		}
	}

	protected static function addRelatedDiscoveryRules(array $options, array &$result): void {
		if ($options['selectDiscoveryRule'] === null) {
			return;
		}

		foreach ($result as &$host) {
			$host['discoveryRule'] = [];
		}
		unset($host);

		$resource = self::isHost()
			? DBselect(
				'SELECT hd.hostid,hd2.lldruleid'.
				' FROM host_discovery hd'.
				' JOIN host_discovery hd2 ON hd.parent_hostid=hd2.hostid'.
				' WHERE '.dbConditionId('hd.hostid', array_keys($result))
			)
			: DBselect(
				'SELECT hd.lldruleid,hd.hostid'.
				' FROM host_discovery hd'.
				' JOIN items i ON i.itemid=hd.lldruleid'.
				' WHERE '.dbConditionId('hd.hostid', array_keys($result)).
					' AND '.dbConditionId('i.flags', [ZBX_FLAG_DISCOVERY_RULE, ZBX_FLAG_DISCOVERY_RULE_CREATED])
			);

		$hostids = [];

		while ($row = DBfetch($resource)) {
			$hostids[$row['lldruleid']][] = $row['hostid'];
		}

		$parent_lld_rules = API::DiscoveryRule()->get([
			'output' => $options['selectDiscoveryRule'],
			'itemids' => array_keys($hostids),
			'nopermissions' => true,
			'preservekeys' => true
		]);

		foreach ($parent_lld_rules as $lldruleid => $parent_lld_rule) {
			foreach ($hostids[$lldruleid] as $hostid) {
				$result[$hostid]['discoveryRule'] = $parent_lld_rule;
			}
		}
	}

	protected static function addRelatedDiscoveryData(array $options, array &$result): void {
		if ($options['selectDiscoveryData'] === null) {
			return;
		}

		foreach ($result as &$host) {
			$host['discoveryData'] = [];
		}
		unset($host);

		$_options = [
			'output' => array_merge(['hostid'], $options['selectDiscoveryData']),
			'hostids' => array_keys($result)
		];
		$resource = DBselect(DB::makeSql('host_discovery', $_options));

		while ($discovery_data = DBfetch($resource)) {
			$result[$discovery_data['hostid']]['discoveryData'] =
				array_diff_key($discovery_data, array_flip(['hostid']));
		}
	}
}
