<?php
/*
** Zabbix
** Copyright (C) 2001-2023 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/


/**
 * Class containing methods for operations with hosts.
 */
abstract class CHostGeneral extends CHostBase {

	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],
		'massadd' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
		'massupdate' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
		'massremove' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN]
	];

	/**
	 * Check for valid host groups.
	 *
	 * @param array      $hosts
	 * @param array|null $db_hosts
	 *
	 * @throws APIException if groups are not valid.
	 */
	protected function checkGroups(array $hosts, array $db_hosts = null): void {
		$id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid';

		$edit_groupids = [];

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

			$groupids = array_column($host['groups'], 'groupid');

			if ($db_hosts === null) {
				$edit_groupids += array_flip($groupids);
			}
			else {
				$db_groupids = array_column($db_hosts[$host[$id_field_name]]['groups'], 'groupid');

				$ins_groupids = array_flip(array_diff($groupids, $db_groupids));
				$del_groupids = array_flip(array_diff($db_groupids, $groupids));

				$edit_groupids += $ins_groupids + $del_groupids;
			}
		}

		if (!$edit_groupids) {
			return;
		}

		$count = API::HostGroup()->get([
			'countOutput' => true,
			'groupids' => array_keys($edit_groupids),
			'editable' => true
		]);

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

	/**
	 * Check for unique host names.
	 *
	 * @param array      $hosts
	 * @param array|null $db_hosts
	 *
	 * @throws APIException if host names are not unique.
	 */
	protected function checkDuplicates(array $hosts, array $db_hosts = null): void {
		$id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid';

		$h_names = [];
		$v_names = [];

		foreach ($hosts as $host) {
			if (array_key_exists('host', $host)) {
				if ($db_hosts === null || $host['host'] !== $db_hosts[$host[$id_field_name]]['host']) {
					$h_names[] = $host['host'];
				}
			}

			if (array_key_exists('name', $host)) {
				if ($db_hosts === null || $host['name'] !== $db_hosts[$host[$id_field_name]]['name']) {
					$v_names[] = $host['name'];
				}
			}
		}

		if ($h_names) {
			$duplicates = DB::select('hosts', [
				'output' => ['host', 'status'],
				'filter' => [
					'flags' => [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED],
					'status' => [HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED, HOST_STATUS_TEMPLATE],
					'host' => $h_names
				],
				'limit' => 1
			]);

			if ($duplicates) {
				$error = ($duplicates[0]['status'] == HOST_STATUS_TEMPLATE)
					? _s('Template with host name "%1$s" already exists.', $duplicates[0]['host'])
					: _s('Host with host name "%1$s" already exists.', $duplicates[0]['host']);

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

		if ($v_names) {
			$duplicates = DB::select('hosts', [
				'output' => ['name', 'status'],
				'filter' => [
					'flags' => [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED],
					'status' => [HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED, HOST_STATUS_TEMPLATE],
					'name' => $v_names
				],
				'limit' => 1
			]);

			if ($duplicates) {
				$error = ($duplicates[0]['status'] == HOST_STATUS_TEMPLATE)
					? _s('Template with visible name "%1$s" already exists.', $duplicates[0]['name'])
					: _s('Host with visible name "%1$s" already exists.', $duplicates[0]['name']);

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

	/**
	 * Check templates links for given data of mass API methods.
	 *
	 * @param string $method
	 * @param array  $templateids
	 * @param array  $db_hosts
	 */
	protected function massCheckTemplatesLinks(string $method, array $templateids, array $db_hosts,
			array $templateids_clear = []): void {
		$ins_templates = [];
		$del_links = [];
		$check_double_linkage = false;
		$del_templates = [];
		$del_links_clear  = [];

		foreach ($db_hosts as $hostid => $db_host) {
			$db_templateids = array_column($db_host['templates'], 'templateid');

			if ($method === 'massadd') {
				$_templateids = array_diff($templateids, $db_templateids);
			}
			elseif ($method === 'massremove') {
				$_templateids = array_diff($db_templateids, $templateids);
			}
			else {
				$_templateids = $templateids;
			}

			$permitted_templateids = $_templateids;
			$templates_count = count($permitted_templateids);
			$upd_templateids = [];

			if (array_key_exists('nopermissions_templates', $db_host)) {
				foreach ($db_host['nopermissions_templates'] as $db_template) {
					$_templateids[] = $db_template['templateid'];
					$templates_count++;
					$upd_templateids[] = $db_template['templateid'];
				}
			}

			foreach ($permitted_templateids as $templateid) {
				$index = array_search($templateid, $db_templateids);

				if ($index !== false) {
					$upd_templateids[] = $templateid;
					unset($db_templateids[$index]);
				}
				else {
					$ins_templates[$templateid][$hostid] = $_templateids;

					if ($this instanceof CTemplate || $templates_count > 1) {
						$check_double_linkage = true;
					}
				}
			}

			foreach ($db_templateids as $db_templateid) {
				$del_links[$db_templateid][$hostid] = true;

				if ($upd_templateids) {
					$del_templates[$db_templateid][$hostid] = $upd_templateids;
				}

				if (array_key_exists($db_templateid, $templateids_clear)) {
					$del_links_clear[$db_templateid][$hostid] = true;
				}
			}
		}

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

		if ($del_links_clear) {
			$this->checkTriggerDependenciesOfHostTriggers($del_links_clear);
		}

		if ($ins_templates) {
			if ($this instanceof CTemplate) {
				self::checkCircularLinkageNew($ins_templates, $del_links);
			}

			if ($check_double_linkage) {
				$this->checkDoubleLinkageNew($ins_templates, $del_links, null);
			}

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

	/**
	 * Update table "hosts_groups" and populate hosts.groups by "hostgroupid" property.
	 *
	 * @param array      $hosts
	 * @param array|null $db_hosts
	 */
	protected function updateGroups(array &$hosts, array $db_hosts = null): void {
		$id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid';

		$ins_hosts_groups = [];
		$del_hostgroupids = [];

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

			$db_groups = ($db_hosts !== null)
				? array_column($db_hosts[$host[$id_field_name]]['groups'], null, 'groupid')
				: [];

			foreach ($host['groups'] as &$group) {
				if (array_key_exists($group['groupid'], $db_groups)) {
					$group['hostgroupid'] = $db_groups[$group['groupid']]['hostgroupid'];
					unset($db_groups[$group['groupid']]);
				}
				else {
					$ins_hosts_groups[] = [
						'hostid' => $host[$id_field_name],
						'groupid' => $group['groupid']
					];
				}
			}
			unset($group);

			$del_hostgroupids = array_merge($del_hostgroupids, array_column($db_groups, 'hostgroupid'));
		}
		unset($host);

		if ($del_hostgroupids) {
			DB::delete('hosts_groups', ['hostgroupid' => $del_hostgroupids]);
		}

		if ($ins_hosts_groups) {
			$hostgroupids = DB::insertBatch('hosts_groups', $ins_hosts_groups);
		}

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

			foreach ($host['groups'] as &$group) {
				if (!array_key_exists('hostgroupid', $group)) {
					$group['hostgroupid'] = array_shift($hostgroupids);
				}
			}
			unset($group);
		}
		unset($host);
	}

	/**
	 * Update table "hosts_templates" and change objects of linked or unliked templates on target hosts or 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';

		parent::updateTemplates($hosts, $db_hosts);

		$ins_links = [];
		$del_links = [];
		$del_links_clear = [];

		foreach ($hosts as $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')
				: [];

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

				$templates_clear = array_key_exists('templates_clear', $host)
					? array_column($host['templates_clear'], null, 'templateid')
					: [];

				foreach ($db_templates as $del_template) {
					if (array_key_exists($del_template['templateid'], $templates_clear)) {
						$del_links_clear[$del_template['templateid']][] = $host[$id_field_name];
					}
					else {
						$del_links[$del_template['templateid']][] = $host[$id_field_name];
					}
				}
			}
			elseif (array_key_exists('templates_clear', $host)) {
				foreach ($host['templates_clear'] as $template) {
					$del_links_clear[$template['templateid']][] = $host[$id_field_name];
				}
			}
		}

		while ($del_links_clear) {
			$templateid = key($del_links_clear);
			$hostids = reset($del_links_clear);
			$templateids = [$templateid];
			unset($del_links_clear[$templateid]);

			foreach ($del_links_clear as $templateid => $_hostids) {
				if ($_hostids === $hostids) {
					$templateids[] = $templateid;
					unset($del_links_clear[$templateid]);
				}
			}

			self::unlinkTemplatesObjects($templateids, $hostids, true);
		}

		while ($del_links) {
			$templateid = key($del_links);
			$hostids = reset($del_links);
			$templateids = [$templateid];
			unset($del_links[$templateid]);

			foreach ($del_links as $templateid => $_hostids) {
				if ($_hostids === $hostids) {
					$templateids[] = $templateid;
					unset($del_links[$templateid]);
				}
			}

			self::unlinkTemplatesObjects($templateids, $hostids);
		}

		while ($ins_links) {
			$templateid = key($ins_links);
			$hostids = reset($ins_links);
			$templateids = [$templateid];
			unset($ins_links[$templateid]);

			foreach ($ins_links as $templateid => $_hostids) {
				if ($_hostids === $hostids) {
					$templateids[] = $templateid;
					unset($ins_links[$templateid]);
				}
			}

			self::linkTemplatesObjects($templateids, $hostids);
		}
	}

	/**
	 * Unlink or clear objects of given templates from given hosts or templates.
	 *
	 * @param array      $templateids
	 * @param array|null $hostids
	 * @param bool       $clear
	 */
	protected static function unlinkTemplatesObjects(array $templateids, array $hostids = null, bool $clear = false): void {
		$flags = ($clear)
			? [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_RULE]
			: [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_RULE, ZBX_FLAG_DISCOVERY_PROTOTYPE];

		// triggers
		$db_triggers = DBselect(
			'SELECT DISTINCT f.triggerid'.
			' FROM functions f,items i'.
			' WHERE f.itemid=i.itemid'.
				' AND '.dbConditionInt('i.hostid', $templateids)
		);

		$tpl_triggerids = DBfetchColumn($db_triggers, 'triggerid');
		$upd_triggers = [
			ZBX_FLAG_DISCOVERY_NORMAL => [],
			ZBX_FLAG_DISCOVERY_PROTOTYPE => []
		];

		if ($tpl_triggerids) {
			$sql_distinct = ($hostids !== null) ? ' DISTINCT' : '';
			$sql_from = ($hostids !== null) ? ',functions f,items i' : '';
			$sql_where = ($hostids !== null)
				? ' AND t.triggerid=f.triggerid'.
					' AND f.itemid=i.itemid'.
					' AND '.dbConditionInt('i.hostid', $hostids)
				: '';

			$db_triggers = DBSelect(
				'SELECT'.$sql_distinct.' t.triggerid,t.flags'.
				' FROM triggers t'.$sql_from.
				' WHERE '.dbConditionInt('t.templateid', $tpl_triggerids).
					' AND '.dbConditionInt('t.flags', $flags).
					$sql_where
			);

			while ($db_trigger = DBfetch($db_triggers)) {
				if ($clear) {
					$upd_triggers[$db_trigger['flags']][$db_trigger['triggerid']] = true;
				}
				else {
					$upd_triggers[$db_trigger['flags']][$db_trigger['triggerid']] = [
						'values' => ['templateid' => 0],
						'where' => ['triggerid' => $db_trigger['triggerid']]
					];
				}
			}

			if (!$clear && ($upd_triggers[ZBX_FLAG_DISCOVERY_NORMAL] || $upd_triggers[ZBX_FLAG_DISCOVERY_PROTOTYPE])) {
				$db_triggers = DBselect(
					'SELECT DISTINCT t.triggerid,t.flags'.
					' 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 h.status='.HOST_STATUS_TEMPLATE.
						' AND '.dbConditionInt('t.triggerid', array_keys(
							$upd_triggers[ZBX_FLAG_DISCOVERY_NORMAL] + $upd_triggers[ZBX_FLAG_DISCOVERY_PROTOTYPE]
						))
				);

				while ($db_trigger = DBfetch($db_triggers)) {
					$upd_triggers[$db_trigger['flags']][$db_trigger['triggerid']]['values']['uuid'] = generateUuidV4();
				}
			}
		}

		if ($upd_triggers[ZBX_FLAG_DISCOVERY_NORMAL]) {
			if ($clear) {
				CTriggerManager::delete(array_keys($upd_triggers[ZBX_FLAG_DISCOVERY_NORMAL]));
			}
			else {
				DB::update('triggers', $upd_triggers[ZBX_FLAG_DISCOVERY_NORMAL]);
			}
		}

		if ($upd_triggers[ZBX_FLAG_DISCOVERY_PROTOTYPE]) {
			if ($clear) {
				CTriggerPrototypeManager::delete(array_keys($upd_triggers[ZBX_FLAG_DISCOVERY_PROTOTYPE]));
			}
			else {
				DB::update('triggers', $upd_triggers[ZBX_FLAG_DISCOVERY_PROTOTYPE]);
			}
		}

		// graphs
		$db_tpl_graphs = DBselect(
			'SELECT DISTINCT g.graphid'.
			' FROM graphs g,graphs_items gi,items i'.
			' WHERE g.graphid=gi.graphid'.
				' AND gi.itemid=i.itemid'.
				' AND '.dbConditionInt('i.hostid', $templateids).
				' AND '.dbConditionInt('g.flags', $flags)
		);

		$tpl_graphids = [];

		while ($db_tpl_graph = DBfetch($db_tpl_graphs)) {
			$tpl_graphids[] = $db_tpl_graph['graphid'];
		}

		if ($tpl_graphids) {
			$upd_graphs = [
				ZBX_FLAG_DISCOVERY_NORMAL => [],
				ZBX_FLAG_DISCOVERY_PROTOTYPE => []
			];

			$sql = ($hostids !== null)
				? 'SELECT DISTINCT g.graphid,g.flags'.
					' FROM graphs g,graphs_items gi,items i'.
					' WHERE g.graphid=gi.graphid'.
						' AND gi.itemid=i.itemid'.
						' AND '.dbConditionInt('g.templateid', $tpl_graphids).
						' AND '.dbConditionInt('i.hostid', $hostids)
				: 'SELECT g.graphid,g.flags'.
					' FROM graphs g'.
					' WHERE '.dbConditionInt('g.templateid', $tpl_graphids);

			$db_graphs = DBSelect($sql);

			while ($db_graph = DBfetch($db_graphs)) {
				if ($clear) {
					$upd_graphs[$db_graph['flags']][$db_graph['graphid']] = true;
				}
				else {
					$upd_graphs[$db_graph['flags']][$db_graph['graphid']] = [
						'values' => ['templateid' => 0],
						'where' => ['graphid' => $db_graph['graphid']]
					];
				}
			}

			if (!$clear && ($upd_graphs[ZBX_FLAG_DISCOVERY_NORMAL] || $upd_graphs[ZBX_FLAG_DISCOVERY_PROTOTYPE])) {
				$db_graphs = DBselect(
					'SELECT DISTINCT g.graphid,g.flags'.
					' FROM graphs g,graphs_items gi,items i,hosts h'.
					' WHERE g.graphid=gi.graphid'.
						' AND gi.itemid=i.itemid'.
						' AND i.hostid=h.hostid'.
						' AND h.status='.HOST_STATUS_TEMPLATE.
						' AND '.dbConditionInt('g.graphid', array_keys(
							$upd_graphs[ZBX_FLAG_DISCOVERY_NORMAL] + $upd_graphs[ZBX_FLAG_DISCOVERY_PROTOTYPE]
						))
				);

				while ($db_graph = DBfetch($db_graphs)) {
					$upd_graphs[$db_graph['flags']][$db_graph['graphid']]['values']['uuid'] = generateUuidV4();
				}
			}

			if ($upd_graphs[ZBX_FLAG_DISCOVERY_PROTOTYPE]) {
				if ($clear) {
					CGraphPrototypeManager::delete(array_keys($upd_graphs[ZBX_FLAG_DISCOVERY_PROTOTYPE]));
				}
				else {
					DB::update('graphs', $upd_graphs[ZBX_FLAG_DISCOVERY_PROTOTYPE]);
				}
			}

			if ($upd_graphs[ZBX_FLAG_DISCOVERY_NORMAL]) {
				if ($clear) {
					CGraphManager::delete(array_keys($upd_graphs[ZBX_FLAG_DISCOVERY_NORMAL]));
				}
				else {
					DB::update('graphs', $upd_graphs[ZBX_FLAG_DISCOVERY_NORMAL]);
				}
			}
		}

		// items, discovery rules
		$upd_items = [
			ZBX_FLAG_DISCOVERY_NORMAL => [],
			ZBX_FLAG_DISCOVERY_RULE => [],
			ZBX_FLAG_DISCOVERY_PROTOTYPE => []
		];
		$parent_itemids = [];
		$item_prototypeids = [];

		$sqlFrom = ' items i1,items i2,hosts h';
		$sqlWhere = ' i2.itemid=i1.templateid'.
			' AND '.dbConditionInt('i2.hostid', $templateids).
			' AND '.dbConditionInt('i1.flags', $flags).
			' AND h.hostid=i1.hostid';

		if (!is_null($hostids)) {
			$sqlWhere .= ' AND '.dbConditionInt('i1.hostid', $hostids);
		}
		$sql = 'SELECT DISTINCT i1.itemid,i1.flags,h.status as host_status,i1.type,i1.valuemapid'.
			' FROM '.$sqlFrom.
			' WHERE '.$sqlWhere;

		$dbItems = DBSelect($sql);

		while ($item = DBfetch($dbItems)) {
			if ($clear) {
				$upd_items[$item['flags']][$item['itemid']] = true;
			}
			else {
				$upd_item = ['templateid' => 0];
				if ($item['host_status'] == HOST_STATUS_TEMPLATE && $item['type'] != ITEM_TYPE_HTTPTEST) {
					$upd_item['uuid'] = generateUuidV4();
				}

				if ($item['valuemapid'] !== '0') {
					$upd_item['valuemapid'] = '0';

					if ($item['host_status'] == HOST_STATUS_TEMPLATE) {
						$parent_itemids[] = $item['itemid'];
					}
					elseif ($item['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
						$item_prototypeids[] = $item['itemid'];
					}
				}

				$upd_items[$item['flags']][$item['itemid']] = [
					'values' => $upd_item,
					'where' => ['itemid' => $item['itemid']]
				];
			}
		}

		if ($upd_items[ZBX_FLAG_DISCOVERY_RULE]) {
			if ($clear) {
				CDiscoveryRuleManager::delete(array_keys($upd_items[ZBX_FLAG_DISCOVERY_RULE]));
			}
			else {
				DB::update('items', $upd_items[ZBX_FLAG_DISCOVERY_RULE]);
			}
		}

		if ($upd_items[ZBX_FLAG_DISCOVERY_NORMAL]) {
			if ($clear) {
				CItemManager::delete(array_keys($upd_items[ZBX_FLAG_DISCOVERY_NORMAL]));
			}
			else {
				DB::update('items', $upd_items[ZBX_FLAG_DISCOVERY_NORMAL]);
			}
		}

		if ($upd_items[ZBX_FLAG_DISCOVERY_PROTOTYPE]) {
			if ($clear) {
				CItemPrototypeManager::delete(array_keys($upd_items[ZBX_FLAG_DISCOVERY_PROTOTYPE]));
			}
			else {
				DB::update('items', $upd_items[ZBX_FLAG_DISCOVERY_PROTOTYPE]);
			}
		}

		if ($parent_itemids) {
			$child_upd_items = [];

			while ($parent_itemids) {
				$result = DBselect(
					'SELECT i.itemid,i.flags,h.status AS host_status'.
					' FROM items i,hosts h'.
					' WHERE i.hostid=h.hostid'.
						' AND '.dbConditionId('i.templateid', $parent_itemids)
				);

				$parent_itemids = [];

				while ($row = DBfetch($result)) {
					$parent_itemids[] = $row['itemid'];

					$child_upd_items[] = [
						'values' => ['valuemapid' => '0'],
						'where' => ['itemid' => $row['itemid']]
					];

					if (in_array($row['host_status'], [HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED])
							&& $row['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
						$item_prototypeids[] = $row['itemid'];
					}
				}
			}

			if ($child_upd_items) {
				DB::update('items', $child_upd_items);
			}
		}

		if ($item_prototypeids) {
			$options = [
				'output' => ['itemid'],
				'filter' => ['parent_itemid' => $item_prototypeids],
				'sortfield' => ['itemid'],
				'sortorder' => [ZBX_SORT_DOWN]
			];
			$result = DBselect(DB::makeSql('item_discovery', $options));

			$upd_discovered_items = [];

			while ($row = DBfetch($result)) {
				$upd_discovered_items[] = [
					'values' => ['valuemapid' => '0'],
					'where' => ['itemid' => $row['itemid']]
				];
			}

			if ($upd_discovered_items) {
				DB::update('items', $upd_discovered_items);
			}
		}

		// host prototypes
		if (!$clear && $upd_items[ZBX_FLAG_DISCOVERY_RULE]) {
			API::HostPrototype()->unlinkTemplateObjects(array_keys($upd_items[ZBX_FLAG_DISCOVERY_RULE]));
		}

		// http tests
		$upd_httptests = [];

		$sqlWhere = '';
		if ($hostids !== null) {
			$sqlWhere = ' AND '.dbConditionInt('ht1.hostid', $hostids);
		}
		$sql = 'SELECT DISTINCT ht1.httptestid,h.status as host_status'.
				' FROM httptest ht1,httptest ht2,hosts h'.
				' WHERE ht1.templateid=ht2.httptestid'.
					' AND ht1.hostid=h.hostid'.
					' AND '.dbConditionInt('ht2.hostid', $templateids).
				$sqlWhere;

		$httptests = DBSelect($sql);

		while ($httptest = DBfetch($httptests)) {
			if ($clear) {
				$upd_httptests[$httptest['httptestid']] = true;
			}
			else {
				$upd_httptest = ['templateid' => 0];
				if ($httptest['host_status'] == HOST_STATUS_TEMPLATE) {
					$upd_httptest['uuid'] = generateUuidV4();
				}

				$upd_httptests[$httptest['httptestid']] = [
					'values' => $upd_httptest,
					'where' => ['httptestid' => $httptest['httptestid']]
				];
			}
		}

		if ($upd_httptests) {
			if ($clear) {
				$result = API::HttpTest()->delete(array_keys($upd_httptests), true);
				if (!$result) {
					self::exception(ZBX_API_ERROR_INTERNAL, _('Cannot unlink and clear Web scenarios.'));
				}
			}
			else {
				DB::update('httptest', $upd_httptests);
			}
		}
	}

	/**
	 * Add objects of given templates to given hosts or templates.
	 *
	 * @param array $templateids
	 * @param array $hostids
	 */
	private static function linkTemplatesObjects(array $templateids, array $hostids): void {
		// TODO: Modify parameters of syncTemplates methods when complete audit log will be implementing for hosts.
		$link_request = [
			'templateids' => $templateids,
			'hostids' => $hostids
		];

		foreach ($templateids as $templateid) {
			// Fist link web items, so that later regular items can use web item as their master item.
			Manager::HttpTest()->link($templateid, $hostids);
		}

		API::Item()->syncTemplates($link_request);
		$ruleids = API::DiscoveryRule()->syncTemplates($templateids, $hostids);

		if ($ruleids) {
			API::ItemPrototype()->syncTemplates($link_request);
			API::HostPrototype()->linkTemplateObjects($ruleids, $hostids);
		}

		API::Trigger()->syncTemplates($link_request);

		if ($ruleids) {
			API::TriggerPrototype()->syncTemplates($link_request);
			API::GraphPrototype()->syncTemplates($link_request);
		}

		API::Graph()->syncTemplates($link_request);

		CTriggerGeneral::syncTemplateDependencies($link_request['templateids'], $link_request['hostids']);
	}

	/**
	 * Checks if the current user has access to the given hosts and templates. Assumes the "hostid" field is valid.
	 *
	 * @param array $hostids    an array of host or template IDs
	 *
	 * @throws APIException if the user doesn't have write permissions for the given hosts.
	 */
	protected function checkHostPermissions(array $hostids) {
		if ($hostids) {
			$hostids = array_unique($hostids);

			$count = API::Host()->get([
				'countOutput' => true,
				'hostids' => $hostids,
				'editable' => true
			]);

			if ($count == count($hostids)) {
				return;
			}

			$count += API::Template()->get([
				'countOutput' => true,
				'templateids' => $hostids,
				'editable' => true
			]);

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

	/**
	 * Allows to:
	 * - add hosts to groups;
	 * - link templates to hosts;
	 * - add new macros to hosts.
	 *
	 * Supported $data parameters are:
	 * - hosts          - an array of hosts to be updated
	 * - templates      - an array of templates to be updated
	 * - groups         - an array of host groups to add the host to
	 * - templates_link - an array of templates to link to the hosts
	 * - macros         - an array of macros to create on the host
	 *
	 * @param array $data
	 *
	 * @return array
	 */
	public function massAdd(array $data) {
		$hostIds = zbx_objectValues($data['hosts'], 'hostid');
		$templateIds = zbx_objectValues($data['templates'], 'templateid');

		$allHostIds = array_merge($hostIds, $templateIds);

		// add groups
		if (array_key_exists('groups', $data) && $data['groups']) {
			$options = ['groups' => $data['groups']];

			if ($data['hosts']) {
				$options += [
					'hosts' => array_map(
						static function($host) {
							return array_intersect_key($host, array_flip(['hostid']));
						},
						$data['hosts']
					)
				];
			}

			if ($data['templates']) {
				$options += [
					'templates' => array_map(
						static function($template) {
							return array_intersect_key($template, array_flip(['templateid']));
						},
						$data['templates']
					)
				];
			}

			API::HostGroup()->massAdd($options);
		}

		// link templates
		if (!empty($data['templates_link'])) {
			$this->checkHostPermissions($allHostIds);

			$this->link(zbx_objectValues(zbx_toArray($data['templates_link']), 'templateid'), $allHostIds);
		}

		// create macros
		if (!empty($data['macros'])) {
			$data['macros'] = zbx_toArray($data['macros']);

			$hostMacrosToAdd = [];
			foreach ($data['macros'] as $hostMacro) {
				foreach ($allHostIds as $hostid) {
					$hostMacro['hostid'] = $hostid;
					$hostMacrosToAdd[] = $hostMacro;
				}
			}

			API::UserMacro()->create($hostMacrosToAdd);
		}

		$ids = ['hostids' => $hostIds, 'templateids' => $templateIds];

		return [$this->pkOption() => $ids[$this->pkOption()]];
	}

	/**
	 * Allows to:
	 * - remove hosts from groups;
	 * - unlink and clear templates from hosts;
	 * - remove macros from hosts.
	 *
	 * Supported $data parameters are:
	 * - hostids            - an array of host IDs to be updated
	 * - templateids        - an array of template IDs to be updated
	 * - groupids           - an array of host group IDs the hosts should be removed from
	 * - templateids_link   - an array of template IDs to unlink from the hosts
	 * - templateids_clear  - an array of template IDs to unlink and clear from the hosts
	 * - macros             - an array of macros to delete from the hosts
	 *
	 * @param array $data
	 *
	 * @return array
	 */
	public function massRemove(array $data) {
		$allHostIds = array_merge($data['hostids'], $data['templateids']);

		$this->checkHostPermissions($allHostIds);

		if (!empty($data['templateids_link'])) {
			$this->unlink(zbx_toArray($data['templateids_link']), $allHostIds);
		}

		if (isset($data['templateids_clear'])) {
			$this->unlink(zbx_toArray($data['templateids_clear']), $allHostIds, true);
		}

		if (array_key_exists('macros', $data)) {
			if (!$data['macros']) {
				self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
			}

			$hostMacros = API::UserMacro()->get([
				'output' => ['hostmacroid'],
				'hostids' => $allHostIds,
				'filter' => [
					'macro' => $data['macros']
				]
			]);
			$hostMacroIds = zbx_objectValues($hostMacros, 'hostmacroid');
			if ($hostMacroIds) {
				API::UserMacro()->delete($hostMacroIds);
			}
		}

		if (isset($data['groupids'])) {
			$options = ['groupids' => $data['groupids']];

			if ($data['hostids']) {
				$options['hostids'] = $data['hostids'];
			}

			if ($data['templateids']) {
				$options['templateids'] = $data['templateids'];
			}

			API::HostGroup()->massRemove($options);
		}

		return [$this->pkOption() => $data[$this->pkOption()]];
	}

	protected function link(array $templateIds, array $targetIds) {
		$hosts_linkage_inserts = parent::link($templateIds, $targetIds);
		$templates_hostids = [];
		$link_requests = [];

		foreach ($hosts_linkage_inserts as $host_tpl_ids) {
			$templates_hostids[$host_tpl_ids['templateid']][] = $host_tpl_ids['hostid'];
		}

		foreach ($templates_hostids as $templateid => $hostids) {
			// Fist link web items, so that later regular items can use web item as their master item.
			Manager::HttpTest()->link($templateid, $hostids);
		}

		while ($templates_hostids) {
			$templateid = key($templates_hostids);
			$link_request = [
				'hostids' => reset($templates_hostids),
				'templateids' => [$templateid]
			];
			unset($templates_hostids[$templateid]);

			foreach ($templates_hostids as $templateid => $hostids) {
				if ($link_request['hostids'] === $hostids) {
					$link_request['templateids'][] = $templateid;
					unset($templates_hostids[$templateid]);
				}
			}

			$link_requests[] = $link_request;
		}

		foreach ($link_requests as $link_request) {
			API::Item()->syncTemplates($link_request);
			$ruleids = API::DiscoveryRule()->syncTemplates($link_request['templateids'], $link_request['hostids']);

			if ($ruleids) {
				API::ItemPrototype()->syncTemplates($link_request);
				API::HostPrototype()->linkTemplateObjects($ruleids, $link_request['hostids']);
			}
		}

		// we do linkage in two separate loops because for triggers you need all items already created on host
		foreach ($link_requests as $link_request){
			API::Trigger()->syncTemplates($link_request);
			API::TriggerPrototype()->syncTemplates($link_request);
			API::GraphPrototype()->syncTemplates($link_request);
			API::Graph()->syncTemplates($link_request);
		}

		foreach ($link_requests as $link_request){
			CTriggerGeneral::syncTemplateDependencies($link_request['templateids'], $link_request['hostids']);
		}

		return $hosts_linkage_inserts;
	}

	/**
	 * Unlinks the templates from the given hosts. If $targetids is set to null, the templates will be unlinked from
	 * all hosts.
	 *
	 * @param array      $templateids
	 * @param null|array $targetids		the IDs of the hosts to unlink the templates from
	 * @param bool       $clear			delete all of the inherited objects from the hosts
	 */
	protected function unlink($templateids, $targetids = null, $clear = false) {
		$flags = ($clear)
			? [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_RULE]
			: [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_RULE, ZBX_FLAG_DISCOVERY_PROTOTYPE];

		// check that all triggers on templates that we unlink, don't have items from another templates
		$sql = 'SELECT DISTINCT t.description'.
			' FROM triggers t,functions f,items i'.
			' WHERE t.triggerid=f.triggerid'.
			' AND f.itemid=i.itemid'.
			' AND '.dbConditionInt('i.hostid', $templateids).
			' AND EXISTS ('.
			'SELECT ff.triggerid'.
			' FROM functions ff,items ii'.
			' WHERE ff.itemid=ii.itemid'.
			' AND ff.triggerid=t.triggerid'.
			' AND '.dbConditionInt('ii.hostid', $templateids, true).
			')'.
			' AND t.flags='.ZBX_FLAG_DISCOVERY_NORMAL;
		if ($dbTrigger = DBfetch(DBSelect($sql, 1))) {
			self::exception(ZBX_API_ERROR_PARAMETERS,
				_s('Cannot unlink trigger "%1$s", it has items from template that is left linked to host.',
					$dbTrigger['description']
				)
			);
		}

		$templ_triggerids = [];

		$db_triggers = DBselect(
			'SELECT DISTINCT f.triggerid'.
			' FROM functions f,items i'.
			' WHERE f.itemid=i.itemid'.
				' AND '.dbConditionInt('i.hostid', $templateids)
		);

		while ($db_trigger = DBfetch($db_triggers)) {
			$templ_triggerids[] = $db_trigger['triggerid'];
		}

		$upd_triggers = [
			ZBX_FLAG_DISCOVERY_NORMAL => [],
			ZBX_FLAG_DISCOVERY_PROTOTYPE => []
		];

		if ($templ_triggerids) {
			$sql_distinct = ($targetids !== null) ? ' DISTINCT' : '';
			$sql_from = ($targetids !== null) ? ',functions f,items i' : '';
			$sql_where = ($targetids !== null)
				? ' AND t.triggerid=f.triggerid'.
					' AND f.itemid=i.itemid'.
					' AND '.dbConditionInt('i.hostid', $targetids)
				: '';

			$db_triggers = DBSelect(
				'SELECT'.$sql_distinct.' t.triggerid,t.flags'.
				' FROM triggers t'.$sql_from.
				' WHERE '.dbConditionInt('t.templateid', $templ_triggerids).
					' AND '.dbConditionInt('t.flags', $flags).
					$sql_where
			);

			while ($db_trigger = DBfetch($db_triggers)) {
				if ($clear) {
					$upd_triggers[$db_trigger['flags']][$db_trigger['triggerid']] = true;
				}
				else {
					$upd_triggers[$db_trigger['flags']][$db_trigger['triggerid']] = [
						'values' => ['templateid' => 0],
						'where' => ['triggerid' => $db_trigger['triggerid']]
					];
				}
			}

			if (!$clear && ($upd_triggers[ZBX_FLAG_DISCOVERY_NORMAL] || $upd_triggers[ZBX_FLAG_DISCOVERY_PROTOTYPE])) {
				$db_triggers = DBselect(
					'SELECT DISTINCT t.triggerid,t.flags'.
					' 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 h.status='.HOST_STATUS_TEMPLATE.
						' AND '.dbConditionInt('t.triggerid', array_keys(
							$upd_triggers[ZBX_FLAG_DISCOVERY_NORMAL] + $upd_triggers[ZBX_FLAG_DISCOVERY_PROTOTYPE]
						))
				);

				while ($db_trigger = DBfetch($db_triggers)) {
					$upd_triggers[$db_trigger['flags']][$db_trigger['triggerid']]['values']['uuid'] = generateUuidV4();
				}
			}
		}

		if ($upd_triggers[ZBX_FLAG_DISCOVERY_NORMAL]) {
			if ($clear) {
				CTriggerManager::delete(array_keys($upd_triggers[ZBX_FLAG_DISCOVERY_NORMAL]));
			}
			else {
				DB::update('triggers', $upd_triggers[ZBX_FLAG_DISCOVERY_NORMAL]);
			}
		}

		if ($upd_triggers[ZBX_FLAG_DISCOVERY_PROTOTYPE]) {
			if ($clear) {
				CTriggerPrototypeManager::delete(array_keys($upd_triggers[ZBX_FLAG_DISCOVERY_PROTOTYPE]));
			}
			else {
				DB::update('triggers', $upd_triggers[ZBX_FLAG_DISCOVERY_PROTOTYPE]);
			}
		}

		/* GRAPHS {{{ */
		$db_tpl_graphs = DBselect(
			'SELECT DISTINCT g.graphid'.
			' FROM graphs g,graphs_items gi,items i'.
			' WHERE g.graphid=gi.graphid'.
				' AND gi.itemid=i.itemid'.
				' AND '.dbConditionInt('i.hostid', $templateids).
				' AND '.dbConditionInt('g.flags', $flags)
		);

		$tpl_graphids = [];

		while ($db_tpl_graph = DBfetch($db_tpl_graphs)) {
			$tpl_graphids[] = $db_tpl_graph['graphid'];
		}

		if ($tpl_graphids) {
			$upd_graphs = [
				ZBX_FLAG_DISCOVERY_NORMAL => [],
				ZBX_FLAG_DISCOVERY_PROTOTYPE => []
			];

			$sql = ($targetids !== null)
				? 'SELECT DISTINCT g.graphid,g.flags'.
					' FROM graphs g,graphs_items gi,items i'.
					' WHERE g.graphid=gi.graphid'.
						' AND gi.itemid=i.itemid'.
						' AND '.dbConditionInt('g.templateid', $tpl_graphids).
						' AND '.dbConditionInt('i.hostid', $targetids)
				: 'SELECT g.graphid,g.flags'.
					' FROM graphs g'.
					' WHERE '.dbConditionInt('g.templateid', $tpl_graphids);

			$db_graphs = DBSelect($sql);

			while ($db_graph = DBfetch($db_graphs)) {
				if ($clear) {
					$upd_graphs[$db_graph['flags']][$db_graph['graphid']] = true;
				}
				else {
					$upd_graphs[$db_graph['flags']][$db_graph['graphid']] = [
						'values' => ['templateid' => 0],
						'where' => ['graphid' => $db_graph['graphid']]
					];
				}
			}

			if (!$clear && ($upd_graphs[ZBX_FLAG_DISCOVERY_NORMAL] || $upd_graphs[ZBX_FLAG_DISCOVERY_PROTOTYPE])) {
				$db_graphs = DBselect(
					'SELECT DISTINCT g.graphid,g.flags'.
					' FROM graphs g,graphs_items gi,items i,hosts h'.
					' WHERE g.graphid=gi.graphid'.
						' AND gi.itemid=i.itemid'.
						' AND i.hostid=h.hostid'.
						' AND h.status='.HOST_STATUS_TEMPLATE.
						' AND '.dbConditionInt('g.graphid', array_keys(
							$upd_graphs[ZBX_FLAG_DISCOVERY_NORMAL] + $upd_graphs[ZBX_FLAG_DISCOVERY_PROTOTYPE]
						))
				);

				while ($db_graph = DBfetch($db_graphs)) {
					$upd_graphs[$db_graph['flags']][$db_graph['graphid']]['values']['uuid'] = generateUuidV4();
				}
			}

			if ($upd_graphs[ZBX_FLAG_DISCOVERY_PROTOTYPE]) {
				if ($clear) {
					CGraphPrototypeManager::delete(array_keys($upd_graphs[ZBX_FLAG_DISCOVERY_PROTOTYPE]));
				}
				else {
					DB::update('graphs', $upd_graphs[ZBX_FLAG_DISCOVERY_PROTOTYPE]);
				}
			}

			if ($upd_graphs[ZBX_FLAG_DISCOVERY_NORMAL]) {
				if ($clear) {
					CGraphManager::delete(array_keys($upd_graphs[ZBX_FLAG_DISCOVERY_NORMAL]));
				}
				else {
					DB::update('graphs', $upd_graphs[ZBX_FLAG_DISCOVERY_NORMAL]);
				}
			}
		}
		/* }}} GRAPHS */

		/* ITEMS, DISCOVERY RULES {{{ */
		$upd_items = [
			ZBX_FLAG_DISCOVERY_NORMAL => [],
			ZBX_FLAG_DISCOVERY_RULE => [],
			ZBX_FLAG_DISCOVERY_PROTOTYPE => []
		];
		$item_prototypeids = [];

		$sqlFrom = ' items i1,items i2,hosts h';
		$sqlWhere = ' i2.itemid=i1.templateid'.
			' AND '.dbConditionInt('i2.hostid', $templateids).
			' AND '.dbConditionInt('i1.flags', $flags).
			' AND h.hostid=i1.hostid';

		if (!is_null($targetids)) {
			$sqlWhere .= ' AND '.dbConditionInt('i1.hostid', $targetids);
		}
		$sql = 'SELECT DISTINCT i1.itemid,i1.flags,h.status as host_status,i1.type,i1.valuemapid'.
			' FROM '.$sqlFrom.
			' WHERE '.$sqlWhere;

		$dbItems = DBSelect($sql);

		while ($item = DBfetch($dbItems)) {
			if ($clear) {
				$upd_items[$item['flags']][$item['itemid']] = true;
			}
			else {
				$upd_item = ['templateid' => 0];
				if ($item['host_status'] == HOST_STATUS_TEMPLATE && $item['type'] != ITEM_TYPE_HTTPTEST) {
					$upd_item['uuid'] = generateUuidV4();
				}

				if ($item['valuemapid'] !== '0') {
					$upd_item['valuemapid'] = '0';

					if (in_array($item['host_status'], [HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED])
							&& $item['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
						$item_prototypeids[] = $item['itemid'];
					}
				}

				$upd_items[$item['flags']][$item['itemid']] = [
					'values' => $upd_item,
					'where' => ['itemid' => $item['itemid']]
				];
			}
		}

		if ($upd_items[ZBX_FLAG_DISCOVERY_RULE]) {
			if ($clear) {
				CDiscoveryRuleManager::delete(array_keys($upd_items[ZBX_FLAG_DISCOVERY_RULE]));
			}
			else {
				DB::update('items', $upd_items[ZBX_FLAG_DISCOVERY_RULE]);
			}
		}

		if ($upd_items[ZBX_FLAG_DISCOVERY_NORMAL]) {
			if ($clear) {
				CItemManager::delete(array_keys($upd_items[ZBX_FLAG_DISCOVERY_NORMAL]));
			}
			else {
				DB::update('items', $upd_items[ZBX_FLAG_DISCOVERY_NORMAL]);
			}
		}

		if ($upd_items[ZBX_FLAG_DISCOVERY_PROTOTYPE]) {
			if ($clear) {
				CItemPrototypeManager::delete(array_keys($upd_items[ZBX_FLAG_DISCOVERY_PROTOTYPE]));
			}
			else {
				DB::update('items', $upd_items[ZBX_FLAG_DISCOVERY_PROTOTYPE]);

				if ($item_prototypeids) {
					$options = [
						'output' => ['itemid'],
						'filter' => ['parent_itemid' => $item_prototypeids],
						'sortfield' => ['itemid'],
						'sortorder' => [ZBX_SORT_DOWN]
					];
					$result = DBselect(DB::makeSql('item_discovery', $options));

					$upd_discovered_items = [];

					while ($row = DBfetch($result)) {
						$upd_discovered_items[] = [
							'values' => ['valuemapid' => '0'],
							'where' => ['itemid' => $row['itemid']]
						];
					}

					if ($upd_discovered_items) {
						DB::update('items', $upd_discovered_items);
					}
				}
			}
		}
		/* }}} ITEMS, DISCOVERY RULES */

		// host prototypes
		// we need only to unlink host prototypes. in case of unlink and clear they will be deleted together with LLD rules.
		if (!$clear && $upd_items[ZBX_FLAG_DISCOVERY_RULE]) {
			if (!$clear && $upd_items[ZBX_FLAG_DISCOVERY_RULE]) {
				API::HostPrototype()->unlinkTemplateObjects(array_keys($upd_items[ZBX_FLAG_DISCOVERY_RULE]));
			}
		}

		// http tests
		$upd_httptests = [];

		$sqlWhere = '';
		if (!is_null($targetids)) {
			$sqlWhere = ' AND '.dbConditionInt('ht1.hostid', $targetids);
		}
		$sql = 'SELECT DISTINCT ht1.httptestid,h.status as host_status'.
				' FROM httptest ht1,httptest ht2,hosts h'.
				' WHERE ht1.templateid=ht2.httptestid'.
					' AND ht1.hostid=h.hostid'.
					' AND '.dbConditionInt('ht2.hostid', $templateids).
				$sqlWhere;

		$httptests = DBSelect($sql);

		while ($httptest = DBfetch($httptests)) {
			if ($clear) {
				$upd_httptests[$httptest['httptestid']] = true;
			}
			else {
				$upd_httptest = ['templateid' => 0];
				if ($httptest['host_status'] == HOST_STATUS_TEMPLATE) {
					$upd_httptest['uuid'] = generateUuidV4();
				}

				$upd_httptests[$httptest['httptestid']] = [
					'values' => $upd_httptest,
					'where' => ['httptestid' => $httptest['httptestid']]
				];
			}
		}

		if ($upd_httptests) {
			if ($clear) {
				$result = API::HttpTest()->delete(array_keys($upd_httptests), true);
				if (!$result) {
					self::exception(ZBX_API_ERROR_INTERNAL, _('Cannot unlink and clear Web scenarios.'));
				}
			}
			else {
				DB::update('httptest', $upd_httptests);
			}
		}

		parent::unlink($templateids, $targetids);
	}

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

		$hostids = array_keys($result);

		// adding groups
		if ($options['selectGroups'] !== null && $options['selectGroups'] != API_OUTPUT_COUNT) {
			$relationMap = $this->createRelationMap($result, 'hostid', 'groupid', 'hosts_groups');
			$groups = API::HostGroup()->get([
				'output' => $options['selectGroups'],
				'groupids' => $relationMap->getRelatedIds(),
				'preservekeys' => true
			]);
			$result = $relationMap->mapMany($result, $groups, 'groups');
		}

		// adding templates
		if ($options['selectParentTemplates'] !== null) {
			if ($options['selectParentTemplates'] != API_OUTPUT_COUNT) {
				$templates = [];
				$relationMap = $this->createRelationMap($result, 'hostid', 'templateid', 'hosts_templates');
				$related_ids = $relationMap->getRelatedIds();

				if ($related_ids) {
					$templates = API::Template()->get([
						'output' => $options['selectParentTemplates'],
						'templateids' => $related_ids,
						'nopermissions' => $options['nopermissions'],
						'preservekeys' => true
					]);
					if (!is_null($options['limitSelects'])) {
						order_result($templates, 'host');
					}
				}

				$result = $relationMap->mapMany($result, $templates, 'parentTemplates', $options['limitSelects']);
			}
			else {
				$templates = API::Template()->get([
					'hostids' => $hostids,
					'countOutput' => true,
					'groupCount' => true
				]);
				$templates = zbx_toHash($templates, 'hostid');
				foreach ($result as $hostid => $host) {
					$result[$hostid]['parentTemplates'] = array_key_exists($hostid, $templates)
						? $templates[$hostid]['rowscount']
						: '0';
				}
			}
		}

		// adding items
		if ($options['selectItems'] !== null) {
			if ($options['selectItems'] != API_OUTPUT_COUNT) {
				$items = API::Item()->get([
					'output' => $this->outputExtend($options['selectItems'], ['hostid', 'itemid']),
					'hostids' => $hostids,
					'nopermissions' => true,
					'preservekeys' => true
				]);

				if (!is_null($options['limitSelects'])) {
					order_result($items, 'name');
				}

				$relationMap = $this->createRelationMap($items, 'hostid', 'itemid');

				$items = $this->unsetExtraFields($items, ['hostid', 'itemid'], $options['selectItems']);
				$result = $relationMap->mapMany($result, $items, 'items', $options['limitSelects']);
			}
			else {
				$items = API::Item()->get([
					'hostids' => $hostids,
					'nopermissions' => true,
					'countOutput' => true,
					'groupCount' => true
				]);
				$items = zbx_toHash($items, 'hostid');
				foreach ($result as $hostid => $host) {
					$result[$hostid]['items'] = array_key_exists($hostid, $items) ? $items[$hostid]['rowscount'] : '0';
				}
			}
		}

		// adding discoveries
		if ($options['selectDiscoveries'] !== null) {
			if ($options['selectDiscoveries'] != API_OUTPUT_COUNT) {
				$items = API::DiscoveryRule()->get([
					'output' => $this->outputExtend($options['selectDiscoveries'], ['hostid', 'itemid']),
					'hostids' => $hostids,
					'nopermissions' => true,
					'preservekeys' => true
				]);

				if (!is_null($options['limitSelects'])) {
					order_result($items, 'name');
				}

				$relationMap = $this->createRelationMap($items, 'hostid', 'itemid');

				$items = $this->unsetExtraFields($items, ['hostid', 'itemid'], $options['selectDiscoveries']);
				$result = $relationMap->mapMany($result, $items, 'discoveries', $options['limitSelects']);
			}
			else {
				$items = API::DiscoveryRule()->get([
					'hostids' => $hostids,
					'nopermissions' => true,
					'countOutput' => true,
					'groupCount' => true
				]);
				$items = zbx_toHash($items, 'hostid');
				foreach ($result as $hostid => $host) {
					$result[$hostid]['discoveries'] = array_key_exists($hostid, $items)
						? $items[$hostid]['rowscount']
						: '0';
				}
			}
		}

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

				$related_ids = $relationMap->getRelatedIds();

				if ($related_ids) {
					$triggers = API::Trigger()->get([
						'output' => $options['selectTriggers'],
						'triggerids' => $related_ids,
						'preservekeys' => true
					]);
					if (!is_null($options['limitSelects'])) {
						order_result($triggers, 'description');
					}
				}

				$result = $relationMap->mapMany($result, $triggers, 'triggers', $options['limitSelects']);
			}
			else {
				$triggers = API::Trigger()->get([
					'hostids' => $hostids,
					'countOutput' => true,
					'groupCount' => true
				]);
				$triggers = zbx_toHash($triggers, 'hostid');

				foreach ($result as $hostid => $host) {
					$result[$hostid]['triggers'] = array_key_exists($hostid, $triggers)
						? $triggers[$hostid]['rowscount']
						: '0';
				}
			}
		}

		// adding graphs
		if ($options['selectGraphs'] !== null) {
			if ($options['selectGraphs'] != API_OUTPUT_COUNT) {
				$graphs = [];
				$relationMap = new CRelationMap();
				// discovered items
				$res = DBselect(
					'SELECT i.hostid,gi.graphid'.
						' FROM items i,graphs_items gi'.
						' WHERE '.dbConditionInt('i.hostid', $hostids).
						' AND i.itemid=gi.itemid'
				);
				while ($relation = DBfetch($res)) {
					$relationMap->addRelation($relation['hostid'], $relation['graphid']);
				}

				$related_ids = $relationMap->getRelatedIds();

				if ($related_ids) {
					$graphs = API::Graph()->get([
						'output' => $options['selectGraphs'],
						'graphids' => $related_ids,
						'preservekeys' => true
					]);
					if (!is_null($options['limitSelects'])) {
						order_result($graphs, 'name');
					}
				}

				$result = $relationMap->mapMany($result, $graphs, 'graphs', $options['limitSelects']);
			}
			else {
				$graphs = API::Graph()->get([
					'hostids' => $hostids,
					'countOutput' => true,
					'groupCount' => true
				]);
				$graphs = zbx_toHash($graphs, 'hostid');
				foreach ($result as $hostid => $host) {
					$result[$hostid]['graphs'] = array_key_exists($hostid, $graphs)
						? $graphs[$hostid]['rowscount']
						: '0';
				}
			}
		}

		// adding http tests
		if ($options['selectHttpTests'] !== null) {
			if ($options['selectHttpTests'] != API_OUTPUT_COUNT) {
				$httpTests = API::HttpTest()->get([
					'output' => $this->outputExtend($options['selectHttpTests'], ['hostid', 'httptestid']),
					'hostids' => $hostids,
					'nopermissions' => true,
					'preservekeys' => true
				]);

				if (!is_null($options['limitSelects'])) {
					order_result($httpTests, 'name');
				}

				$relationMap = $this->createRelationMap($httpTests, 'hostid', 'httptestid');

				$httpTests = $this->unsetExtraFields($httpTests, ['hostid', 'httptestid'], $options['selectHttpTests']);
				$result = $relationMap->mapMany($result, $httpTests, 'httpTests', $options['limitSelects']);
			}
			else {
				$httpTests = API::HttpTest()->get([
					'hostids' => $hostids,
					'nopermissions' => true,
					'countOutput' => true,
					'groupCount' => true
				]);
				$httpTests = zbx_toHash($httpTests, 'hostid');
				foreach ($result as $hostid => $host) {
					$result[$hostid]['httpTests'] = array_key_exists($hostid, $httpTests)
						? $httpTests[$hostid]['rowscount']
						: '0';
				}
			}
		}

		// adding tags
		if ($options['selectTags'] !== null && $options['selectTags'] != API_OUTPUT_COUNT) {
			if ($options['selectTags'] === API_OUTPUT_EXTEND) {
				$options['selectTags'] = ['tag', 'value'];
			}

			$tags_options = [
				'output' => $this->outputExtend($options['selectTags'], ['hostid']),
				'filter' => ['hostid' => $hostids]
			];

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

			$tags = DBselect(DB::makeSql('host_tag', $tags_options));

			while ($tag = DBfetch($tags)) {
				$hostid = $tag['hostid'];
				unset($tag['hosttagid'], $tag['hostid']);
				$result[$hostid]['tags'][] = $tag;
			}
		}

		// Add value mapping.
		if ($options['selectValueMaps'] !== null) {
			if ($options['selectValueMaps'] === API_OUTPUT_EXTEND) {
				$options['selectValueMaps'] = ['valuemapid', 'name', 'mappings'];
			}

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

			$valuemaps = DB::select('valuemap', [
				'output' => array_diff($this->outputExtend($options['selectValueMaps'], ['valuemapid', 'hostid']),
					['mappings']
				),
				'filter' => ['hostid' => $hostids],
				'preservekeys' => true
			]);

			if ($this->outputIsRequested('mappings', $options['selectValueMaps']) && $valuemaps) {
				$params = [
					'output' => ['valuemapid', 'type', 'value', 'newvalue'],
					'filter' => ['valuemapid' => array_keys($valuemaps)],
					'sortfield' => ['sortorder']
				];
				$query = DBselect(DB::makeSql('valuemap_mapping', $params));

				while ($mapping = DBfetch($query)) {
					$valuemaps[$mapping['valuemapid']]['mappings'][] = [
						'type' => $mapping['type'],
						'value' => $mapping['value'],
						'newvalue' => $mapping['newvalue']
					];
				}
			}

			foreach ($valuemaps as $valuemap) {
				$result[$valuemap['hostid']]['valuemaps'][] = array_intersect_key($valuemap,
					array_flip($options['selectValueMaps'])
				);
			}
		}

		return $result;
	}

	/**
	 * Validates tags.
	 *
	 * @param array  $host
	 * @param int    $host['evaltype']
	 * @param array  $host['tags']
	 * @param string $host['tags'][]['tag']
	 * @param string $host['tags'][]['value']
	 *
	 * @throws APIException if the input is invalid.
	 */
	protected function validateTags(array $host) {
		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
			'evaltype'	=> ['type' => API_INT32, 'in' => implode(',', [TAG_EVAL_TYPE_AND_OR, TAG_EVAL_TYPE_OR])],
			'tags'		=> ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['tag', 'value']], 'fields' => [
				'tag'		=> ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('host_tag', 'tag')],
				'value'		=> ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('host_tag', 'value'), 'default' => DB::getDefault('host_tag', 'value')]
			]]
		]];

		// Keep values only for fields with defined validation rules.
		$host = array_intersect_key($host, $api_input_rules['fields']);

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

	/**
	 * Add the existing host groups, templates, tags, macros.
	 *
	 * @param array $hosts
	 * @param array $db_hosts
	 */
	protected function addAffectedObjects(array $hosts, array &$db_hosts): void {
		$this->addAffectedGroups($hosts, $db_hosts);
		parent::addAffectedObjects($hosts, $db_hosts);
	}

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

		$hostids = [];

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

		if (!$hostids) {
			return;
		}

		$filter = ['hostid' => $hostids];

		if (self::$userData['type'] == USER_TYPE_ZABBIX_ADMIN) {
			$db_groups = API::HostGroup()->get([
				'output' => [],
				$id_field_name.'s' => $hostids,
				'preservekeys' => true
			]);

			$filter += ['groupid' => array_keys($db_groups)];
		}

		$options = [
			'output' => ['hostgroupid', 'hostid', 'groupid'],
			'filter' => $filter
		];
		$db_groups = DBselect(DB::makeSql('hosts_groups', $options));

		while ($db_group = DBfetch($db_groups)) {
			$db_hosts[$db_group['hostid']]['groups'][$db_group['hostgroupid']] =
				array_diff_key($db_group, array_flip(['hostid']));
		}
	}

	/**
	 * Add the existing groups, macros or templates whether these are affected by the mass methods.
	 *
	 * @param string     $objects
	 * @param array      $objectids
	 * @param array      $db_hosts
	 */
	protected function massAddAffectedObjects(string $objects, array $objectids, array &$db_hosts): void {
		$id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid';

		foreach ($db_hosts as &$db_host) {
			$db_host[$objects] = [];
		}
		unset($db_host);

		if ($objects === 'groups') {
			$filter = ['hostid' => array_keys($db_hosts)];

			if ($objectids) {
				$filter += ['groupid' => $objectids];
			}
			elseif (self::$userData['type'] == USER_TYPE_ZABBIX_ADMIN) {
				$db_groups = API::HostGroup()->get([
					'output' => [],
					$id_field_name.'s' => array_keys($db_hosts),
					'preservekeys' => true
				]);

				$filter += ['groupid' => array_keys($db_groups)];
			}

			$options = [
				'output' => ['hostgroupid', 'hostid', 'groupid'],
				'filter' => $filter
			];
			$db_hosts_groups = DBselect(DB::makeSql('hosts_groups', $options));

			while ($link = DBfetch($db_hosts_groups)) {
				$db_hosts[$link['hostid']]['groups'][$link['hostgroupid']] =
					array_diff_key($link, array_flip(['hostid']));
			}
		}

		if ($objects === 'macros') {
			$options = [
				'output' => ['hostmacroid', 'hostid', 'macro', 'value', 'description', 'type'],
				'filter' => ['hostid' => array_keys($db_hosts)]
			];

			if ($objectids) {
				$macro_patterns = [];
				$trimmed_macros = [];

				foreach ($objectids as $macro) {
					$trimmed_macro = CApiInputValidator::trimMacro($macro);
					$context_pos = strpos($trimmed_macro, ':');

					$macro_patterns[] = ($context_pos === false)
						? '{$'.$trimmed_macro
						: '{$'.substr($trimmed_macro, 0, $context_pos);
					$trimmed_macros[] = $trimmed_macro;
				}

				$options += [
					'search' => ['macro' => $macro_patterns],
					'startSearch' => true,
					'searchByAny' => true
				];
			}

			$db_macros = DBselect(DB::makeSql('hostmacro', $options));

			while ($db_macro = DBfetch($db_macros)) {
				if (!$objectids || in_array(CApiInputValidator::trimMacro($db_macro['macro']), $trimmed_macros)) {
					$db_hosts[$db_macro['hostid']]['macros'][$db_macro['hostmacroid']] =
						array_diff_key($db_macro, array_flip(['hostid']));
				}
			}
		}

		if ($objects === 'templates') {
			$permitted_templates = [];

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

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

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

	/**
	 * Get templates or hosts input array based on requested data and database data.
	 *
	 * @param array $data
	 * @param array $db_objects
	 *
	 * @return array
	 */
	protected function getObjectsByData(array $data, array $db_objects): array {
		$id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid';

		$objects = [];

		foreach ($db_objects as $db_object) {
			$object = [$id_field_name => $db_object[$id_field_name]];

			if (array_key_exists('groups', $db_object)) {
				$object['groups'] = [];

				if (array_key_exists('groups', $data)) {
					foreach ($data['groups'] as $group) {
						$object['groups'][] = ['groupid' => $group['groupid']];
					}
				}
			}

			if (array_key_exists('macros', $db_object)) {
				$object['macros'] = [];

				if (array_key_exists('macros', $data) && is_array(reset($data['macros']))) {
					$db_macros = [];

					foreach ($db_object['macros'] as $db_macro) {
						$db_macros[CApiInputValidator::trimMacro($db_macro['macro'])] = $db_macro;
					}

					foreach ($data['macros'] as $macro) {
						$trimmed_macro = CApiInputValidator::trimMacro($macro['macro']);

						if (array_key_exists($trimmed_macro, $db_macros)) {
							$object['macros'][] = ['hostmacroid' => $db_macros[$trimmed_macro]['hostmacroid']] + $macro
								+ ['description' => DB::getDefault('hostmacro', 'description')];
						}
						else {
							$object['macros'][] = $macro;
						}
					}
				}
			}

			if (array_key_exists('templates', $db_object)) {
				$templates = $this instanceof CTemplate ? 'templates_link' : 'templates';
				$templateids = $this instanceof CTemplate ? 'templateids_link' : 'templateids';

				if (array_key_exists($templates, $data) || array_key_exists($templateids, $data)) {
					$object['templates'] = [];

					if (array_key_exists($templates, $data)) {
						foreach ($data[$templates] as $template) {
							$object['templates'][] = ['templateid' => $template['templateid']];
						}
					}
				}

				if (array_key_exists('templates_clear', $data) || array_key_exists('templateids_clear', $data)) {
					$object['templates_clear'] = [];
					$db_templateids = array_column($db_object['templates'], 'templateid');

					if (array_key_exists('templates_clear', $data)) {
						foreach ($data['templates_clear'] as $template) {
							if (in_array($template['templateid'], $db_templateids)) {
								$object['templates_clear'][] = ['templateid' => $template['templateid']];
							}
						}
					}
					else {
						foreach ($data['templateids_clear'] as $templateid) {
							if (in_array($templateid, $db_templateids)) {
								$object['templates_clear'][] = ['templateid' => $templateid];
							}
						}
					}
				}
			}

			$objects[] = $object;
		}

		return $objects;
	}
}
