<?php
/*
** Zabbix
** Copyright (C) 2001-2024 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU 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.
**/

require_once 'vendor/autoload.php';

require_once dirname(__FILE__).'/../../../include/defines.inc.php';
require_once dirname(__FILE__).'/../../../include/hosts.inc.php';
require_once dirname(__FILE__).'/../../../include/db.inc.php';

class CDataHelper extends CAPIHelper {

	protected static $data = null;
	protected static $request = [];
	protected static $response = [];

	private static $objectids = [];
	private static $detachedids = [];

	/**
	 * Prepare request for API call and make API call (@see callRaw).
	 *
	 * @param string $method    API method to be called.
	 * @param mixed $params     API call params.
	 *
	 * @return array
	 *
	 * @throws Exception on API error
	 */
	public static function call($method, $params) {
		global $URL;
		if (!$URL) {
			$URL = PHPUNIT_URL.'api_jsonrpc.php';
		}

		static::$request = [];
		static::$response = [];

		if (CAPIHelper::getSessionId() === null) {
			CAPIHelper::createSessionId();
		}

		$response = CAPIHelper::call($method, $params);

		if (array_key_exists('error', $response)) {
			throw new Exception($method.' API call failed: '.
				json_encode($params, JSON_PRETTY_PRINT)."\n".
				json_encode($response['error'], JSON_PRETTY_PRINT)
			);
		}

		if (!array_key_exists('result', $response)) {
			throw new Exception('API call failed: result is not present');
		}

		static::$request = (CTestArrayHelper::isAssociative($params)) ? [$params] : $params;
		static::$response = $response['result'];

		return static::$response;
	}

	/**
	 * Prepare request for API call and make API call (@see callRaw).
	 *
	 * @param string $method    API method to be called.
	 * @param mixed $params     API call params.
	 *
	 * @return array
	 *
	 * @throws Exception on API error
	 */
	public static function getIds($field) {
		$ids = [];
		$result = reset(static::$response);
		if (count(static::$request) !== count($result)) {
			throw new Exception('Failed to get ids: record counts don\'t match');
		}

		foreach (static::$request as $i => $object) {
			if (!array_key_exists($field, $object)) {
				throw new Exception('Failed to get ids: field "'.$field.'" is not present in request.');
			}

			if (!array_key_exists($i, $result)) {
				throw new Exception('Failed to get ids: element ('.$i.') is not present in result.');
			}

			if (array_key_exists($object[$field], $ids)) {
				throw new Exception('Failed to get ids: field "'.$field.'" is not unique.');
			}

			$ids[$object[$field]] = $result[$i];
		}

		return $ids;
	}

	/**
	 * Load the data source data from the file cache.
	 */
	protected static function preload() {
		if (static::$data === null) {
			static::$data = [];

			if (!defined('PHPUNIT_DATA_DIR')) {
				return;
			}

			foreach (new DirectoryIterator(PHPUNIT_DATA_DIR) as $file) {
				if ($file->isDot() || $file->isDir() || strtolower($file->getExtension()) !== 'json') {
					continue;
				}

				$name = $file->getBasename('.'.$file->getExtension());
				static::$data[$name] = json_decode(file_get_contents($file->getPathname()), true);
			}
		}
	}

	/**
	 * Get data from the data sources.
	 *
	 * @param mixed $path       data path to look for
	 * @param mixed $default    default value to be returned if data doesn't exist
	 *
	 * @return mixed
	 */
	public static function get($path, $default = null) {
		return CTestArrayHelper::get(static::$data, $path, $default);
	}

	/**
	 * Load specific data source data.
	 *
	 * @param mixed $source    name of the data source(s)
	 *
	 * @return boolean
	 *
	 * @throws \Exception
	 */
	public static function load($source) {
		if (is_array($source)) {
			$result = true;
			foreach ($source as $name) {
				if (!static::load($name)) {
					$result = false;
				}
			}

			return $result;
		}

		static::preload();

		if (array_key_exists($source, static::$data)) {
			return true;
		}

		try {
			$path = PHPUNIT_DATA_SOURCES_DIR.$source.'.php';
			if (!file_exists($path)) {
				throw new \Exception('File "'.$path.'" doesn\'t exist.');
			}

			require_once $path;
			static::$data[$source] = forward_static_call([$source, 'load']);

			if (defined('PHPUNIT_DATA_DIR')) {
				$data = json_encode(static::get($source));
				file_put_contents(PHPUNIT_DATA_DIR.$source.'.json', $data);
			}
		}
		catch (\Exception $e) {
			echo 'Failed to load data from data source "'.$source.'".'."\n\n".$e->getMessage()."\n".$e->getTraceAsString();

			return false;
		}

		return true;
	}

	/**
	 * Add data to item.
	 *
	 * @param string $itemid		item id
	 * @param array $values			value that should be sent to item
	 * @param mixed $time			time when value was received
	 */
	public static function addItemData($itemid, $values, $time = null) {
		self::convertValueReference($itemid);

		if (!is_array($values)) {
			$values = [$values];
		}

		if ($time === null) {
			if (is_array($values)) {
				$offset = time();
				$time = [];
				for ($i = count($values); $i > 0; $i--) {
					$time[] = $offset - $i;
				}
			}
			else {
				$time = time();
			}
		}
		elseif (is_array($time)) {
			if (count($time) !== count($values)) {
				throw new Exception('Value count should match the time record count.');
			}

			$time = array_values($time);
		}

		// Set correct history table where to insert data.
		$suffix = static::getItemDataTableSuffix($itemid);

		foreach (array_values($values) as $key => $value) {
			$clock = is_array($time) ? $time[$key] : $time;

			// If value is an array, it means that we are dealing with trend data, which is inserted in differently.
			if (is_array($value)) {
				DBexecute('INSERT INTO trends'.$suffix.' (itemid, clock, num, value_min, value_avg,'.
						' value_max) VALUES ('.zbx_dbstr($itemid).', '.zbx_dbstr($clock).', '.zbx_dbstr($value['num']).
						', '.zbx_dbstr($value['min']).', '.zbx_dbstr($value['avg']).', '.zbx_dbstr($value['max']).')'
				);
			}
			else {
				DBexecute('INSERT INTO history'.$suffix.' (itemid, clock, value) VALUES ('.zbx_dbstr($itemid).
						', '.zbx_dbstr($clock).', '.zbx_dbstr($value).')'
				);
			}
		}
	}

	/**
	 * Remove item data from history and trends tables.
	 *
	 * @param string|array $itemids		item id(s)
	 */
	public static function removeItemData($itemids) {
		$groups = [];

		if (!is_array($itemids)) {
			$itemids = [$itemids];
		}

		foreach ($itemids as $itemid) {
			$groups[self::getItemDataTableSuffix($itemid)][] = zbx_dbstr($itemid);
		}

		foreach (array_keys($groups) as $suffix) {
			DBexecute('DELETE FROM history'.$suffix.' WHERE itemid IN ('.implode(',', $groups[$suffix]).')');

			if ($suffix === '_uint' || $suffix === '') {
				DBexecute('DELETE FROM trends'.$suffix.' WHERE itemid IN ('.implode(',', $groups[$suffix]).')');
			}
		}
	}

	/**
	 * Get history data table.
	 *
	 * @param string $itemid		item id
	 */
	public static function getItemDataTableSuffix($itemid) {
		// Check item value type to set correct history table.
		$value_type = CDBHelper::getValue('SELECT value_type FROM items where itemid='.zbx_dbstr($itemid));
		$suffixes = ['', '_str', '_log', '_uint', '_text'];

		if (!array_key_exists($value_type, $suffixes)) {
			throw new Exception('Unsupported item value type: '.$value_type);
		}

		return $suffixes[$value_type];
	}

	/**
	 * Create objects using API.
	 * Usually only the object's "name/key" need to be provided. Some defaults (e.g. item's type, value_type) are mixed
	 * in and the "parent ID" property (e.g. host's groupid, item's hostid, etc.), if not specified, inferred by using
	 * the last known such object's ID. Previously defined objects can be linked by their reference ID, f.e.:
	 * `'master_itemid' => ':item:key.of.master.item'`.
	 *
	 * Call CDataHelper::cleanUp() to delete objects defined here along with child objects created during the test.
	 *
	 * @param array $objects
	 */
	public static function createObjects(array $objects): void {
		$object_types = array_fill_keys(['template_groups', 'host_groups', 'templates', 'proxies', 'hosts', 'triggers',
			'roles', 'user_groups', 'users', 'scripts',  'drules', 'actions', 'graphs', 'dashboards', 'services',
			'maintenances', 'correlations', 'slas', 'reports'
		], []);

		if (array_diff_key($objects, $object_types)) {
			throw new Exception('Unsupported object types for data helper: '.
				implode(', ', array_keys(array_diff_key($objects, $object_types)))
			);
		}

		$objects += $object_types;

		try {
			self::createTemplateGroups($objects['template_groups']);
			self::createHostGroups($objects['host_groups']);
			self::createTemplates($objects['templates']);
			self::createProxies($objects['proxies']);
			self::createHosts($objects['hosts']);
			self::createTriggers($objects['triggers']);
			self::createRoles($objects['roles']);
			self::createUserGroups($objects['user_groups']);
			self::createUsers($objects['users']);
			self::createScripts($objects['scripts']);
			self::createDrules($objects['drules']);
			self::createActions($objects['actions']);
			self::createGraphs($objects['graphs']);
			self::createDashboards($objects['dashboards']);
			self::createServices($objects['services']);
			self::createMaintenances($objects['maintenances']);
			self::createCorrelations($objects['correlations']);
			self::createSlas($objects['slas']);
			self::createReports($objects['reports']);
		}
		catch (Exception $e) {
			self::cleanUp();

			throw new Exception($e->getMessage()."\n".$e->getFile().':'.$e->getLine(), 0, $e);
		}
	}

	/**
	 * @param array $template_groups
	 */
	private static function createTemplateGroups(array $template_groups): void {
		if (!$template_groups) {
			return;
		}

		$result = ZABBIX_VERSION < 6.4
			? self::call('hostgroup.create', $template_groups)
			: self::call('templategroup.create', $template_groups);

		foreach ($template_groups as $template_group) {
			self::$objectids['template_group'][$template_group['name']] = array_shift($result['groupids']);
		}
	}

	/**
	 * @param array $host_groups
	 */
	private static function createHostGroups(array $host_groups): void {
		if (!$host_groups) {
			return;
		}

		$result = self::call('hostgroup.create', $host_groups);

		foreach ($host_groups as $host_group) {
			self::$objectids['host_group'][$host_group['name']] = array_shift($result['groupids']);
		}
	}

	/**
	 * @param array $templates
	 *
	 * @return array
	 */
	public static function createTemplates(array $templates): array {
		if (!$templates) {
			return [];
		}

		$value_maps = [];
		$items = [];
		$lld_rules = [];
		$dashboards = [];
		$httptests = [];

		foreach ($templates as $i => &$template) {
			$template += array_key_exists('template_group', self::$objectids)
				? [
					'groups' => [
						['groupid' => end(self::$objectids['template_group'])]
					]
				]
				: [];

			$parent_hostid = ['hostid' => array_key_exists('templateid', $template)
				? $template['templateid']
				: ':template:'.$template['host']
			];

			if (array_key_exists('value_maps', $template)) {
				foreach ($template['value_maps'] as $value_map) {
					$value_maps[] = $value_map + $parent_hostid;
				}

				unset($template['value_maps']);
			}

			if (array_key_exists('items', $template)) {
				foreach ($template['items'] as $item) {
					$items[] = $item + $parent_hostid;
				}

				unset($template['items']);
			}

			if (array_key_exists('discoveryrules', $template)) {
				$template['lld_rules'] = $template['discoveryrules'];
				unset($template['discoveryrules']);
			}

			if (array_key_exists('lld_rules', $template)) {
				foreach ($template['lld_rules'] as $lld_rule) {
					$lld_rules[] = $lld_rule + $parent_hostid;
				}

				unset($template['lld_rules']);
			}

			if (array_key_exists('httptests', $template)) {
				foreach ($template['httptests'] as $httptest) {
					$httptests[] = $httptest + $parent_hostid;
				}

				unset($template['httptests']);
			}

			if (array_key_exists('dashboards', $template)) {
				foreach ($template['dashboards'] as $dashboard) {
					$dashboards[] = $dashboard + ['templateid' => $parent_hostid['hostid']];
				}

				unset($template['dashboards']);
			}

			if (array_key_exists('templateid', $template)) {
				unset($templates[$i]);
			}
		}
		unset($template);

		if ($templates) {
			self::convertTemplateReferences($templates);

			$result = self::call('template.create', $templates);

			foreach ($templates as &$template) {
				$templateid = array_shift($result['templateids']);

				self::$objectids['template'][$template['host']] = $templateid;
				$template['templateid'] = $templateid;
			}
			unset($template);
		}

		self::createValueMaps($value_maps);
		self::createItems($items);
		self::createLldRules($lld_rules);
		self::createHttptests($httptests);
		self::createTemplateDashboards($dashboards);

		if (!$templates) {
			self::$detachedids += array_fill_keys(['value_map', 'item', 'lld_rule', 'httptest', 'template_dashboard'], []);
			self::$detachedids['value_map']
				= array_merge(self::$detachedids['value_map'], array_column($value_maps, 'valuemapid'));
			self::$detachedids['item']= array_merge(self::$detachedids['item'], array_column($items, 'itemid'));
			self::$detachedids['lld_rule']
				= array_merge(self::$detachedids['lld_rule'], array_column($lld_rules, 'itemid'));
			self::$detachedids['httptest']
				= array_merge(self::$detachedids['httptest'], array_column($httptests, 'httptestid'));
			self::$detachedids['template_dashboard']
				= array_merge(self::$detachedids['template_dashboard'], array_column($dashboards, 'dashboardid'));
			self::$detachedids = array_filter(self::$detachedids);
		}

		return $templates
			? [
				'templateids' => array_column($templates, 'templateid', 'host'),
				'itemids' => self::extractItemMap($items, $templates, 'templateid'),
				'discoveryruleids' => self::extractItemMap($lld_rules, $templates, 'templateid')
			]
			: [];
	}

	public static function convertTemplateReferences(array &$templates): void {
		self::convertPropertyReference($templates, 'templateid');
		self::convertPropertyReference($templates, 'groups.groupid');
		self::convertPropertyReference($templates, 'templates.templateid');
	}

	private static function createProxies(array $proxies): void {
		if (!$proxies) {
			return;
		}

		foreach ($proxies as &$proxy) {
			$proxy = self::prepareProxy($proxy);
		}
		unset($proxy);

		$result = self::call('proxy.create', $proxies);

		foreach ($proxies as $proxy) {
			ZABBIX_VERSION < 7
				? self::$objectids['proxy'][$proxy['host']] = array_shift($result['proxyids'])
				: self::$objectids['proxy'][$proxy['name']] = array_shift($result['proxyids']);
		}
	}

	/**
	 * @param array $proxy
	 *
	 * @return array
	 */
	public static function prepareProxy(array $proxy): array {
		return $proxy + ['status' => HOST_STATUS_PROXY_PASSIVE];
	}

	/**
	 * Create hosts with nested objects.
	 *
	 * @param array $hosts  API call parameters.
	 *                      In addition to the default host.create params, f.e., "items" and "lld_rules" can be set
	 *                      for any of the host in order to create the related objects.
	 *
	 * @return array
	 */
	public static function createHosts(array $hosts): array {
		if (!$hosts) {
			return [];
		}

		$value_maps = [];
		$items = [];
		$lld_rules = [];
		$httptests = [];

		$hostids = [];

		foreach ($hosts as $i => &$host) {
			$host += array_key_exists('host_group', self::$objectids)
				? [
					'groups' => [
						['groupid' => end(self::$objectids['host_group'])]
					]
				]
				: [];

			$parent_hostid = ['hostid' => array_key_exists('hostid', $host) ? $host['hostid'] : ':host:'.$host['host']];
			$hostids[] = $parent_hostid['hostid'];

			if (array_key_exists('value_maps', $host)) {
				foreach ($host['value_maps'] as $value_map) {
					$value_maps[] = $value_map + $parent_hostid;
				}

				unset($host['value_maps']);
			}

			if (array_key_exists('items', $host)) {
				foreach ($host['items'] as $item) {
					$items[] = $item + $parent_hostid;
				}

				unset($host['items']);
			}

			if (array_key_exists('discoveryrules', $host)) {
				$host['lld_rules'] = $host['discoveryrules'];
				unset($host['discoveryrules']);
			}

			if (array_key_exists('lld_rules', $host)) {
				foreach ($host['lld_rules'] as $lld_rule) {
					$lld_rules[] = $lld_rule + $parent_hostid;
				}

				unset($host['lld_rules']);
			}

			if (array_key_exists('httptests', $host)) {
				foreach ($host['httptests'] as $httptest) {
					$httptests[] = $httptest + $parent_hostid;
				}

				unset($host['httptests']);
			}

			if (array_key_exists('hostid', $host)) {
				unset($hosts[$i]);
			}
		}
		unset($host);

		if ($hosts) {
			self::convertHostReferences($hosts);

			$result = self::call('host.create', $hosts);

			foreach ($hosts as &$host) {
				$hostid = array_shift($result['hostids']);

				self::$objectids['host'][$host['host']] = $hostid;
				$host['hostid'] = $hostid;
			}
			unset($host);
		}

		if ($items || $lld_rules) {
			self::convertValueReferences($hostids);

			$interfaces = static::getInterfaces(array_unique($hostids));

			self::assignInterfaces($items, $interfaces);
			self::assignInterfaces($lld_rules, $interfaces);
		}

		self::createValueMaps($value_maps);
		self::createItems($items);
		self::createLldRules($lld_rules);
		self::createHttptests($httptests);

		if (!$hosts) {
			self::$detachedids += array_fill_keys(['value_map', 'item', 'lld_rule', 'httptest'], []);
			self::$detachedids['value_map']
				= array_merge(self::$detachedids['value_map'], array_column($value_maps, 'valuemapid'));
			self::$detachedids['item']= array_merge(self::$detachedids['item'], array_column($items, 'itemid'));
			self::$detachedids['lld_rule']
				= array_merge(self::$detachedids['lld_rule'], array_column($lld_rules, 'itemid'));
			self::$detachedids['httptest']
				= array_merge(self::$detachedids['httptest'], array_column($httptests, 'httptestid'));
			self::$detachedids = array_filter(self::$detachedids);
		}

		return $hosts
			? [
				'hostids' => array_column($hosts, 'hostid', 'host'),
				'itemids' => self::extractItemMap($items, $hosts, 'hostid'),
				'discoveryruleids' => self::extractItemMap($lld_rules, $hosts, 'hostid')
			]
			: [];
	}

	public static function convertHostReferences(array &$hosts): void {
		self::convertPropertyReference($hosts, 'hostid');
		self::convertPropertyReference($hosts, 'groups.groupid');
		self::convertPropertyReference($hosts, 'templates.templateid');
		ZABBIX_VERSION < 7
			? self::convertPropertyReference($hosts, 'proxy_hostid')
			: self::convertPropertyReference($hosts, 'proxyid');
	}

	/**
	 * Get host interfaces.
	 *
	 * @param array $hostids
	 *
	 * @return array
	 */
	public static function getInterfaces(array $hostids): array {
		$result = [
			'default_interfaces' => [],
			'ids' => []
		];
		$hosts = static::call('host.get', [
			'output' => ['hostid'],
			'hostids' => array_values($hostids),
			'selectInterfaces' => ['interfaceid', 'useip', 'ip', 'type', 'dns', 'port', 'main']
		]);

		foreach ($hosts as $host) {
			foreach ($host['interfaces'] as $interface) {
				if ($interface['main'] == INTERFACE_PRIMARY) {
					$result['default_interfaces'][$host['hostid']][$interface['type']] = $interface['interfaceid'];
				}

				$address = $interface['useip'] == INTERFACE_USE_IP ? $interface['ip'] : $interface['dns'];
				$result['ids'][$host['hostid']][$address.':'.$interface['port']] = $interface['interfaceid'];
			}
		}

		return $result;
	}

	private static function assignInterfaces(array &$items, array $interfaces): void {
		$item_type_interfaces = [
			ITEM_TYPE_SNMP => INTERFACE_TYPE_SNMP,
			ITEM_TYPE_SNMPTRAP => INTERFACE_TYPE_SNMP,
			ITEM_TYPE_IPMI => INTERFACE_TYPE_IPMI,
			ITEM_TYPE_ZABBIX => INTERFACE_TYPE_AGENT,
			ITEM_TYPE_SIMPLE => INTERFACE_TYPE_ANY,
			ITEM_TYPE_EXTERNAL => INTERFACE_TYPE_ANY,
			ITEM_TYPE_SSH => INTERFACE_TYPE_ANY,
			ITEM_TYPE_TELNET => INTERFACE_TYPE_ANY,
			ITEM_TYPE_JMX => INTERFACE_TYPE_JMX,
			ITEM_TYPE_HTTPAGENT => INTERFACE_TYPE_OPT
		];

		foreach ($items as &$item) {
			if (array_key_exists('interfaceid', $item) || !array_key_exists($item['type'], $item_type_interfaces)) {
				continue;
			}

			$interface_type = $item_type_interfaces[$item['type']];
			$hostid = self::getConvertedValueReference($item['hostid']);

			if (array_key_exists($hostid, $interfaces['default_interfaces'])) {
				if ($interface_type !== null
						&& array_key_exists($interface_type, $interfaces['default_interfaces'][$hostid])) {
					$item['interfaceid'] = $interfaces['default_interfaces'][$hostid][$interface_type];
				}
			}

			if (array_key_exists($hostid, $interfaces['ids'])) {
				$interface = CTestArrayHelper::get($item, 'interface');
				unset($item['interface']);

				if ($interface !== null && array_key_exists($interfaces['ids'][$hostid], $interface)) {
					$item['interfaceid'] = $interfaces['ids'][$hostid][$interface];
				}
			}
		}
		unset($item);
	}

	private static function extractItemMap(array $items, array $hosts, string $id_field_name): array {
		$result = [];
		$hosts = array_column($hosts, null, $id_field_name);

		foreach ($items as $item) {
			$result[$hosts[$item['hostid']]['host'].':'.$item['key_']] = $item['itemid'];
		}

		return $result;
	}

	/**
	 * @param array $value_maps
	 */
	private static function createValueMaps(array &$value_maps): void {
		if (!$value_maps) {
			return;
		}

		self::convertValueMapReferences($value_maps);

		$result = self::call('valuemap.create', $value_maps);

		foreach ($value_maps as &$value_map) {
			$value_map['valuemapid'] = array_shift($result['valuemapids']);

			self::$objectids['value_map'][$value_map['name']] = $value_map['valuemapid'];
		}
		unset($value_map);
	}

	public static function convertValueMapReferences(array &$value_maps): void {
		self::convertPropertyReference($value_maps, 'valuemapid');
		self::convertPropertyReference($value_maps, 'hostid');
	}

	/**
	 * @param array $items
	 *
	 * @return array
	 */
	private static function createItems(array &$items): void {
		if (!$items) {
			return;
		}

		$host_refs = [];
		$item_indexes = [];

		foreach ($items as $i => &$item) {
			$host_refs[$i] = $item['hostid'];

			$item = self::prepareItem($item);

			$item_indexes[$item['hostid']][':item:'.$item['key_']] = $i;
		}
		unset($item);

		$dep_items = [];

		foreach ($items as $i => $item) {
			if ($item['type'] == ITEM_TYPE_DEPENDENT) {
				if (!array_key_exists($item['hostid'], $item_indexes)
						|| !array_key_exists($item['master_itemid'], $item_indexes[$item['hostid']])) {
					throw new Exception(sprintf('Wrong master item ID for item with key "%1$s" on "%2$s".',
						$item['key_'], $host_refs[$i]
					));
				}

				$dep_items[$item_indexes[$item['hostid']][$item['master_itemid']]][$i] = $item;

				unset($items[$i]);
			}
		}

		$result_items = [];

		do {
			self::convertItemReferences($items);

			$result = self::call('item.create', array_values($items));

			$_items = [];

			foreach ($items as $i => &$item) {
				$item['itemid'] = array_shift($result['itemids']);

				self::$objectids['item'][$item['key_']][$host_refs[$i]] = $item['itemid'];

				if (array_key_exists($i, $dep_items)) {
					$_items += $dep_items[$i];
				}
			}
			unset($item);

			$result_items += $items;
		} while ($items = $_items);

		$items = $result_items;
	}

	/**
	 * @param array $item
	 *
	 * @return array
	 */
	public static function prepareItem(array $item): array {
		return $item + [
			'name' => $item['key_'],
			'type' => array_key_exists('master_itemid', $item) ? ITEM_TYPE_DEPENDENT : ITEM_TYPE_TRAPPER,
			'value_type' => ITEM_VALUE_TYPE_STR
		];
	}

	public static function convertItemReferences(array &$items): void {
		self::convertPropertyReference($items, 'itemid');
		self::convertPropertyReference($items, 'hostid');
		self::convertPropertyReference($items, 'valuemapid');
		self::convertPropertyReference($items, 'interfaceid');
		self::convertPropertyReference($items, 'master_itemid');
	}

	/**
	 * @param array $item
	 * @param int   $from
	 * @param int   $to
	 *
	 * @return array
	 *
	 * @throws Exception
	 */
	public static function prepareItemSet(array $item, int $from, int $to): array {
		if ($from > $to) {
			throw new Exception('Incorrect range parameters.');
		}

		$bracket_pos = strpos($item['key_'], '[');
		$items = [];

		for ($i = $from; $i <= $to; $i++) {
			$key_ = $bracket_pos === false
				? $item['key_'].'.'.$i
				: substr_replace($item['key_'], '.'.$i, $bracket_pos, 0);

			$items[] = self::prepareItem(['key_' => $key_] + $item);
		}

		return $items;
	}

	private static function createLldRules(array &$lld_rules): void {
		if (!$lld_rules) {
			return;
		}

		$host_refs = [];
		$item_prototypes = [];
		$host_prototypes = [];
		$trigger_prototypes = [];
		$graph_prototypes = [];

		foreach ($lld_rules as $i => &$lld_rule) {
			$host_refs[$i] = $lld_rule['hostid'];

			$parent_ruleid = array_key_exists('itemid', $lld_rule)
				? ['ruleid' => $lld_rule['itemid']]
				: ['ruleid' => ':lld_rule:'.$lld_rule['key_'].($lld_rule['hostid'][0] === ':' ? $host_refs[$i] : '')];

			$lld_rule = self::prepareLldRule($lld_rule);

			if (array_key_exists('item_prototypes', $lld_rule)) {
				foreach ($lld_rule['item_prototypes'] as $item_prototype) {
					if (array_key_exists('item_prototype', self::$objectids)
							&& array_key_exists($item_prototype['key_'], self::$objectids['item_prototype'])) {
						throw new Exception('Non-unique graph prototype alias: '.$item_prototype['key_']);
					}

					$item_prototypes[] = $item_prototype + ['hostid' => $host_refs[$i]] + $parent_ruleid;
				}

				unset($lld_rule['item_prototypes']);
			}

			if (array_key_exists('host_prototypes', $lld_rule)) {
				foreach ($lld_rule['host_prototypes'] as $host_prototype) {
					$host_prototypes[] = $host_prototype + $parent_ruleid;
				}

				unset($lld_rule['host_prototypes']);
			}

			if (array_key_exists('trigger_prototypes', $lld_rule)) {
				foreach ($lld_rule['trigger_prototypes'] as $alias => $trigger_prototype) {
					if (is_numeric($alias)) {
						throw new Exception('Trigger prototypes must use aliases as indexes: '.
							json_encode($trigger_prototype)
						);
					}
					elseif (array_key_exists('trigger_prototype', self::$objectids)
							&& array_key_exists($alias, self::$objectids['trigger_prototype'])) {
						throw new Exception('Non-unique trigger prototype alias: '.$alias);
					}
				}

				$trigger_prototypes += $lld_rule['trigger_prototypes'];
				unset($lld_rule['trigger_prototypes']);
			}

			if (array_key_exists('graph_prototypes', $lld_rule)) {
				foreach ($lld_rule['graph_prototypes'] as $alias => $graph_prototype) {
					if (is_numeric($alias)) {
						throw new Exception('Graph prototypes must use aliases as indexes: '.
							json_encode($graph_prototype)
						);
					}
					elseif (array_key_exists('graph_prototype', self::$objectids)
							&& array_key_exists($alias, self::$objectids['graph_prototype'])) {
						throw new Exception('Non-unique graph prototype alias: '.$alias);
					}

					$graph_prototypes += $lld_rule['graph_prototypes'];
				}

				unset($lld_rule['graph_prototypes']);
			}

			if (array_key_exists('itemid', $lld_rule)) {
				unset($lld_rules[$i]);
			}
		}
		unset($lld_rule);

		if ($lld_rules) {
			self::convertLldRuleReferences($lld_rules);

			$result = self::call('discoveryrule.create', $lld_rules);

			foreach ($lld_rules as $i => &$lld_rule) {
				$lld_rule['itemid'] = array_shift($result['itemids']);

				self::$objectids['lld_rule'][$lld_rule['key_']][$host_refs[$i]] = $lld_rule['itemid'];
			}
			unset($lld_rule);
		}

		self::createItemPrototypes($item_prototypes);
		self::createHostPrototypes($host_prototypes);
		self::createTriggerPrototypes($trigger_prototypes);
		self::createGraphPrototypes($graph_prototypes);

		if (!$lld_rules) {
			self::$detachedids += array_fill_keys(['item_prototype', 'host_prototype', 'trigger_prototype',
				'graph_prototype'], []
			);
			self::$detachedids['item_prototype']
				= array_merge(self::$detachedids['item_prototype'], array_column($item_prototypes, 'itemid'));
			self::$detachedids['host_prototype']
				= array_merge(self::$detachedids['host_prototype'], array_column($host_prototypes, 'hostid'));
			self::$detachedids['trigger_prototype']
				= array_merge(self::$detachedids['trigger_prototype'], array_column($trigger_prototypes, 'triggerid'));
			self::$detachedids['graph_prototype']
				= array_merge(self::$detachedids['graph_prototype'], array_column($graph_prototypes, 'graphid'));
			self::$detachedids = array_filter(self::$detachedids);
		}
	}

	/**
	 * @param array $lld_rule
	 *
	 * @return array
	 */
	public static function prepareLldRule(array $lld_rule): array {
		return $lld_rule + (array_key_exists('key_', $lld_rule) ? ['name' => $lld_rule['key_']] : [])
			+ ['type' => array_key_exists('master_itemid', $lld_rule) ? ITEM_TYPE_DEPENDENT : ITEM_TYPE_TRAPPER];
	}

	public static function convertLldRuleReferences(array &$lld_rules): void {
		self::convertPropertyReference($lld_rules, 'itemid');
		self::convertPropertyReference($lld_rules, 'hostid');
		self::convertPropertyReference($lld_rules, 'interfaceid');
		self::convertPropertyReference($lld_rules, 'master_itemid');
	}

	/**
	 * @param array $lld_rule
	 * @param int   $from
	 * @param int   $to
	 *
	 * @return array
	 */
	public static function prepareLldRuleSet(array $lld_rule, int $from, int $to): array {
		if ($from > $to) {
			throw new Exception('Incorrect range parameters.');
		}

		$bracket_pos = strpos($lld_rule['key_'], '[');
		$lld_rules = [];

		for ($i = $from; $i <= $to; $i++) {
			$key_ = $bracket_pos === false
				? $lld_rule['key_'].'.'.$i
				: substr_replace($lld_rule['key_'], '.'.$i, $bracket_pos, 0);

			$lld_rules[] = self::prepareLldRule(['key_' => $key_] + $lld_rule);
		}

		return $lld_rules;
	}

	private static function createTemplateDashboards(array &$dashboards): void {
		if (!$dashboards) {
			return;
		}

		$host_refs = [];

		foreach ($dashboards as $i => &$dashboard) {
			$host_refs[$i] = $dashboard['templateid'];
		}

		self::convertTemplateDashboardReferences($dashboards);

		$result = self::call('templatedashboard.create', $dashboards);

		foreach ($dashboards as $i => &$dashboard) {
			$dashboard['dashboardid'] = array_shift($result['dashboardids']);

			self::$objectids['template_dashboard'][$dashboard['name']][$host_refs[$i]] = $dashboard['dashboardid'];
		}
		unset($dashboard);
	}

	public static function convertTemplateDashboardReferences(array &$dashboards): void {
		self::convertPropertyReference($dashboards, 'dashboardid');
		self::convertPropertyReference($dashboards, 'templateid');
	}

	private static function createHttptests(array &$httptests): void {
		if (!$httptests) {
			return;
		}

		$host_refs = [];

		foreach ($httptests as $i => $httptest) {
			$host_refs[$i] = $httptest['hostid'];
		}

		self::convertHttptestReferences($httptests);

		$result = self::call('httptest.create', $httptests);

		foreach ($httptests as $i => &$httptest) {
			$httptest['httptestid'] = array_shift($result['httptestids']);

			self::$objectids['httptest'][$httptest['name']][$host_refs[$i]] = $httptest['httptestid'];
		}
		unset($httptest);
	}

	/**
	 * @param array $httptests
	 */
	public static function convertHttptestReferences(array &$httptests): void {
		self::convertPropertyReference($httptests, 'httptestid');
		self::convertPropertyReference($httptests, 'hostid');
	}

	/**
	 * @param array $items
	 */
	private static function createItemPrototypes(array &$items): void {
		if (!$items) {
			return;
		}

		$host_refs = [];
		$discovered_items = [];
		$item_indexes = [];

		foreach ($items as $i => &$item) {
			$host_refs[$i] = $item['hostid'];

			$item = self::prepareItemPrototype($item);

			if (array_key_exists('discovered_items', $item)) {
				foreach ($item['discovered_items'] as $discovered_item) {
					$discovered_items[] = $discovered_item + [
						'hostid' => $host_refs[$i],
						'item_prototypeid' => ':item_prototype:'.$item['key_']
					];
				}

				unset($item['discovered_items']);
			}

			$item_indexes[$item['ruleid']][':item_prototype:'.$item['key_']] = $i;
		}
		unset($item);

		$dep_items = [];

		foreach ($items as $i => $item) {
			if ($item['type'] == ITEM_TYPE_DEPENDENT
					&& strpos($item['master_itemid'], ':item_prototype:') === 0) {
				if (!array_key_exists($item['ruleid'], $item_indexes)
						|| !array_key_exists($item['master_itemid'], $item_indexes[$item['ruleid']])) {
					throw new Exception(sprintf('Wrong master item ID for item prototype with key "%1$s" on "%2$s".',
						$item['key_'], $host_refs[$i]
					));
				}

				$dep_items[$item_indexes[$item['ruleid']][$item['master_itemid']]][$i] = $item;

				unset($items[$i]);
			}
		}

		$result_items = [];

		do {
			self::convertItemPrototypeReferences($items);

			$result = self::call('itemprototype.create', $items);

			$_items = [];

			foreach ($items as $i => &$item) {
				$item['itemid'] = array_shift($result['itemids']);

				self::$objectids['item_prototype'][$item['key_']][$host_refs[$i]] = $item['itemid'];

				if (array_key_exists($i, $dep_items)) {
					$_items += $dep_items[$i];
				}
			}
			unset($item);

			$result_items += $items;
		} while ($items = $_items);

		self::createDiscoveredItems($discovered_items);

		$items = $result_items;
	}

	/**
	 * @param array $item
	 *
	 * @return array
	 */
	public static function prepareItemPrototype(array $item): array {
		return $item + [
			'name' => $item['key_'],
			'type' => array_key_exists('master_itemid', $item) ? ITEM_TYPE_DEPENDENT : ITEM_TYPE_TRAPPER,
			'value_type' => ITEM_VALUE_TYPE_STR
		];
	}

	public static function convertItemPrototypeReferences(array &$items): void {
		self::convertPropertyReference($items, 'itemid');
		self::convertPropertyReference($items, 'hostid');
		self::convertPropertyReference($items, 'ruleid');
		self::convertPropertyReference($items, 'valuemapid');
		self::convertPropertyReference($items, 'interfaceid');
		self::convertPropertyReference($items, 'master_itemid');
	}

	/**
	 * @param array $item
	 * @param int   $from
	 * @param int   $to
	 *
	 * @return array
	 *
	 * @throws Exception
	 */
	public static function prepareItemPrototypeSet(array $item, int $from, int $to): array {
		if ($from > $to) {
			throw new Exception('Incorrect range parameters.');
		}

		$bracket_pos = strpos($item['key_'], '[');
		$items = [];

		for ($i = $from; $i <= $to; $i++) {
			$key_ = $bracket_pos === false
				? $item['key_'].'.'.$i
				: substr_replace($item['key_'], '.'.$i, $bracket_pos, 0);

			$items[] = self::prepareItemPrototype(['key_' => $key_] + $item);
		}

		return $items;
	}

	/**
	 * @param array $host_prototypes
	 */
	private static function createHostPrototypes(array &$host_prototypes): void {
		if (!$host_prototypes) {
			return;
		}

		self::convertHostPrototypeReferences($host_prototypes);

		$result = self::call('hostprototype.create', array_values($host_prototypes));

		foreach ($host_prototypes as &$host_prototype) {
			$host_prototype['hostid'] = array_shift($result['hostids']);

			self::$objectids['host_prototype'][$host_prototype['host']] = $host_prototype['hostid'];
		}
		unset($host_prototype);
	}

	public static function convertHostPrototypeReferences(array &$host_prototypes): void {
		self::convertPropertyReference($host_prototypes, 'hostid');
		self::convertPropertyReference($host_prototypes, 'ruleid');
		self::convertPropertyReference($host_prototypes, 'groupLinks.groupid');
		self::convertPropertyReference($host_prototypes, 'templates.templateid');
	}

	/**
	 * @param array $trigger_prototypes
	 */
	private static function createTriggerPrototypes(array &$trigger_prototypes): void {
		if (!$trigger_prototypes) {
			return;
		}

		self::convertTriggerPrototypeReferences($trigger_prototypes);

		$result = self::call('triggerprototype.create', array_values($trigger_prototypes));

		foreach ($trigger_prototypes as $alias => &$trigger_prototype) {
			$trigger_prototype['triggerid'] = array_shift($result['triggerids']);

			self::$objectids['trigger_prototype'][$alias] = $trigger_prototype['triggerid'];
		}
		unset($trigger_prototype);
	}

	public static function convertTriggerPrototypeReferences(array &$trigger_prototypes): void {
		self::convertPropertyReference($trigger_prototypes, 'triggerid');
		self::convertPropertyReference($trigger_prototypes, 'dependencies.triggerid');
	}

	/**
	 * @param array $graph_prototypes
	 */
	private static function createGraphPrototypes(array &$graph_prototypes): void {
		if (!$graph_prototypes) {
			return;
		}

		self::convertGraphPrototypeReferences($graph_prototypes);

		$result = self::call('graphprototype.create', array_values($graph_prototypes));

		foreach ($graph_prototypes as $alias => &$graph_prototype) {
			$graph_prototype['graphid'] = array_shift($result['graphids']);

			self::$objectids['graph_prototype'][$alias] = $graph_prototype['graphid'];
		}
		unset($graph_prototype);
	}

	public static function convertGraphPrototypeReferences(array &$graph_prototypes): void {
		self::convertPropertyReference($graph_prototypes, 'graphid');
		self::convertPropertyReference($graph_prototypes, 'gitems.itemid');
	}

	/**
	 * @param array $discovered_items
	 */
	private static function createDiscoveredItems(array &$discovered_items): void {
		if (!$discovered_items) {
			return;
		}

		$host_refs = [];
		$item_indexes = [];
		$item_prototypeids = [];

		foreach ($discovered_items as $i => &$item) {
			$host_refs[$i] = $item['hostid'];

			$item = self::prepareItem($item);

			$item_indexes[$item['item_prototypeid']][':discovered_item:'.$item['key_']] = $i;

			$item_prototypeids[$i] = $item['item_prototypeid'];
			unset($item['item_prototypeid']);
		}
		unset($item);

		$dep_items = [];

		foreach ($discovered_items as $i => &$item) {
			if ($item['type'] == ITEM_TYPE_DEPENDENT && strpos($item['master_itemid'], ':discovered_item:') === 0) {
				if (!array_key_exists($item_prototypeids[$i], $item_indexes)
						|| !array_key_exists($item['master_itemid'], $item_indexes[$item_prototypeids[$i]])) {
					throw new Exception(sprintf('Wrong master item ID for discovered item with key "%1$s" on "%2$s".',
						$item['key_'], $host_refs[$i]
					));
				}

				$dep_items[$item_indexes[$item_prototypeids[$i]][$item['master_itemid']]][$i] = $item;

				unset($discovered_items[$i]);
			}
		}
		unset($item);

		do {
			self::convertDiscoveredItemReferences($discovered_items);

			$result = self::call('item.create', $discovered_items);

			$item_discoveries = [];
			$_discovered_items = [];

			foreach ($discovered_items as $i => &$item) {
				$item['itemid'] = $result['itemids'][$i];

				self::$objectids['discovered_item'][$item['key_']][$host_refs[$i]] = $item['itemid'];

				$item_discoveries[] = [
					'itemid' => $item['itemid'],
					'parent_itemid' => $item_prototypeids[$i],
					'key_' => $item['key_']
				];

				if (array_key_exists($i, $dep_items)) {
					$_discovered_items += $dep_items[$i];
				}
			}
			unset($item);

			self::convertPropertyReference($item_discoveries, 'parent_itemid');

			DB::insert('item_discovery', $item_discoveries);

			DB::update('items', [
				'values' => ['flags' => ZBX_FLAG_DISCOVERY_CREATED],
				'where' => ['itemid' => $result['itemids']]
			]);
		} while ($discovered_items = $_discovered_items);
	}

	public static function convertDiscoveredItemReferences(array &$discovered_items): void {
		self::convertPropertyReference($discovered_items, 'itemid');
		self::convertPropertyReference($discovered_items, 'hostid');
		self::convertPropertyReference($discovered_items, 'item_prototypeid');
		self::convertPropertyReference($discovered_items, 'valuemapid');
		self::convertPropertyReference($discovered_items, 'interfaceid');
		self::convertPropertyReference($discovered_items, 'master_itemid');
	}

	private static function createTriggers(array $triggers): void {
		if (!$triggers) {
			return;
		}

		foreach ($triggers as $alias => $trigger) {
			if (is_numeric($alias)) {
				throw new Exception('Triggers must use aliases as indexes: '.json_encode($trigger));
			}
			elseif (array_key_exists('trigger', self::$objectids)
					&& array_key_exists($alias, self::$objectids['trigger'])) {
				throw new Exception('Non-unique trigger alias: '.$alias);
			}
		}

		self::convertTriggerReferences($triggers);

		$result = self::call('trigger.create', array_values($triggers));

		foreach ($triggers as $alias => $trigger) {
			self::$objectids['trigger'][$alias] = array_shift($result['triggerids']);
		}
	}

	public static function convertTriggerReferences(array &$triggers): void {
		self::convertPropertyReference($triggers, 'triggerid');
		self::convertPropertyReference($triggers, 'dependencies.triggerid');
	}

	private static function createRoles(array $roles): void {
		if (!$roles) {
			return;
		}

		$result = self::call('role.create', $roles);

		foreach ($roles as $role) {
			self::$objectids['role'][$role['name']] = array_shift($result['roleids']);
		}
	}

	private static function createUserGroups(array $user_groups): void {
		if (!$user_groups) {
			return;
		}

		self::convertUserGroupReferences($user_groups);

		$result = self::call('usergroup.create', $user_groups);

		foreach ($user_groups as $user_group) {
			self::$objectids['user_group'][$user_group['name']] = array_shift($result['usrgrpids']);
		}
	}

	public static function convertUserGroupReferences(array &$user_groups): void {
		self::convertPropertyReference($user_groups, 'usrgrpid');
		self::convertPropertyReference($user_groups, 'templategroup_rights.id');
		self::convertPropertyReference($user_groups, 'hostgroup_rights.id');
		self::convertPropertyReference($user_groups, 'users');
	}

	private static function createUsers(array $users): void {
		if (!$users) {
			return;
		}

		foreach ($users as &$user) {
			$user += array_key_exists('role', self::$objectids) ? ['roleid' => end(self::$objectids['role'])] : [];
			$user += array_key_exists('user_group', self::$objectids)
				? ['usrgrps' => [
					['usrgrpid' => end(self::$objectids['user_group'])]
				]]
				: [];
		}
		unset($user);

		self::convertUserReferences($users);

		$result = self::call('user.create', $users);

		foreach ($users as $user) {
			self::$objectids['user'][$user['username']] = array_shift($result['userids']);
		}
	}

	public static function convertUserReferences(array &$users): void {
		self::convertPropertyReference($users, 'userid');
		self::convertPropertyReference($users, 'roleid');
		self::convertPropertyReference($users, 'usrgrps.usrgrpid');
	}

	private static function createScripts(array $scripts): void {
		if (!$scripts) {
			return;
		}

		self::convertScriptReferences($scripts);

		$result = self::call('script.create', $scripts);

		foreach ($scripts as $script) {
			self::$objectids['script'][$script['name']] = array_shift($result['scriptids']);
		}
	}

	public static function convertScriptReferences(array &$scripts): void {
		self::convertPropertyReference($scripts, 'scriptid');
		self::convertPropertyReference($scripts, 'groupid');
		self::convertPropertyReference($scripts, 'usrgrpid');
	}

	private static function createDrules(array $drules): void {
		if (!$drules) {
			return;
		}

		foreach ($drules as &$drule) {
			$drule = self::prepareDrule($drule);
		}
		unset($drule);

		self::convertDruleReferences($drules);

		$result = self::call('drule.create', $drules);

		foreach ($drules as $drule) {
			self::$objectids['drule'][$drule['name']] = array_shift($result['druleids']);
		}
	}

	/**
	 * @param array $drule
	 *
	 * @return array
	 */
	public static function prepareDrule(array $drule): array {
		return $drule + [
			'iprange' => '192.168.1.1-255',
			'dchecks' => [
				[
					'type' => SVC_HTTP,
					'ports' => '80',
					'name' => 'HTTP'
				]
			]
		];
	}

	public static function convertDruleReferences(array &$drules): void {
		self::convertPropertyReference($drules, 'druleid');
		self::convertPropertyReference($drules, 'proxyid');
		self::convertPropertyReference($drules, 'proxy_hostid');
	}

	private static function createActions(array $actions): void {
		if (!$actions) {
			return;
		}

		foreach ($actions as &$action) {
			if (array_key_exists('filter', $action) && array_key_exists('conditions', $action['filter'])) {
				$referenced_condition_types = [CONDITION_TYPE_HOST_GROUP, CONDITION_TYPE_HOST,
					CONDITION_TYPE_TRIGGER, CONDITION_TYPE_TEMPLATE, CONDITION_TYPE_DRULE,
					CONDITION_TYPE_PROXY
				];

				foreach ($action['filter']['conditions'] as &$condition) {
					if (in_array($condition['conditiontype'], $referenced_condition_types)) {
						if (array_key_exists('value', $condition)) {
							self::convertValueReference($condition['value']);
						}
					}
				}
				unset($condition);
			}
		}
		unset($action);

		self::convertActionReferences($actions);

		$result = self::call('action.create', $actions);

		foreach ($actions as $action) {
			self::$objectids['action'][$action['name']] = array_shift($result['actionids']);
		}
	}

	public static function convertActionReferences(array &$actions): void {
		self::convertPropertyReference($actions, 'actionid');
		self::convertPropertyReference($actions, 'operations.opmessage_grp.usrgrpid');
		self::convertPropertyReference($actions, 'operations.opmessage_usr.userid');
		self::convertPropertyReference($actions, 'operations.opcommand.scriptid');
		self::convertPropertyReference($actions, 'operations.opcommand_grp.groupid');
		self::convertPropertyReference($actions, 'operations.opcommand_hst.hostid');
		self::convertPropertyReference($actions, 'operations.opgroup.groupid');
		self::convertPropertyReference($actions, 'operations.optemplate.templateid');
	}

	private static function createGraphs(array $graphs): void {
		if (!$graphs) {
			return;
		}

		foreach ($graphs as $alias => $graph) {
			if (is_numeric($alias)) {
				throw new Exception('Graphs must use aliases as indexes: '.json_encode($graph));
			}
			elseif (array_key_exists('graph', self::$objectids)
					&& array_key_exists($alias, self::$objectids['graph'])) {
				throw new Exception('Non-unique graph alias: '.$alias);
			}
		}

		self::convertGraphReferences($graphs);

		$result = self::call('graph.create', array_values($graphs));

		foreach ($graphs as $alias => $graph) {
			self::$objectids['graph'][$alias] = array_shift($result['graphids']);
		}
	}

	public static function convertGraphReferences(array &$graphs): void {
		self::convertPropertyReference($graphs, 'graphid');
		self::convertPropertyReference($graphs, 'gitems.itemid');
	}

	private static function createDashboards(array &$dashboards): void {
		if (!$dashboards) {
			return;
		}

		self::convertDashboardReferences($dashboards);

		$result = self::call('dashboard.create', $dashboards);

		foreach ($dashboards as &$dashboard) {
			$dashboard['dashboardid'] = array_shift($result['dashboardids']);

			self::$objectids['dashboard'][$dashboard['name']] = $dashboard['dashboardid'];
		}
		unset($dashboard);
	}

	public static function convertDashboardReferences(array &$dashboards): void {
		self::convertPropertyReference($dashboards, 'dashboardid');
		self::convertPropertyReference($dashboards, 'userid');
		self::convertPropertyReference($dashboards, 'pages.widgets.fields.value');
		self::convertPropertyReference($dashboards, 'users.userid');
		self::convertPropertyReference($dashboards, 'userGroups.usrgrpid');
	}

	private static function createServices(array &$services): void {
		if (!$services) {
			return;
		}

		foreach ($services as &$service) {
			$service = self::prepareService($service);
		}
		unset($service);

		self::convertServiceReferences($services);

		$result = self::call('service.create', $services);

		foreach ($services as $service) {
			self::$objectids['service'][$service['name']] = array_shift($result['serviceids']);
		}
	}

	/**
	 * @param array $service
	 *
	 * @return array
	 */
	public static function prepareService(array $service): array {
		return $service + [
			'algorithm' => ZBX_SERVICE_STATUS_CALC_SET_OK,
			'sortorder' => '0'
		];
	}

	public static function convertServiceReferences(array &$services): void {
		self::convertPropertyReference($services, 'serviceid');
		self::convertPropertyReference($services, 'parents.serviceid');
		self::convertPropertyReference($services, 'children.serviceid');
	}

	private static function createMaintenances(array &$maintenances): void {
		if (!$maintenances) {
			return;
		}

		foreach ($maintenances as &$maintenance) {
			$maintenance = self::prepareMaintenance($maintenance);
		}
		unset($maintenance);

		self::convertMaintenanceReferences($maintenances);

		$result = self::call('maintenance.create', $maintenances);

		foreach ($maintenances as $maintenance) {
			self::$objectids['maintenance'][$maintenance['name']] = array_shift($result['maintenanceids']);
		}
	}

	/**
	 * @param array $maintenance
	 *
	 * @return array
	 */
	public static function prepareMaintenance(array $maintenance): array {
		return $maintenance + [
			'active_since' => time(),
			'active_till' => strtotime('now + 1 year')
		];
	}

	public static function convertMaintenanceReferences(array &$maintenances): void {
		self::convertPropertyReference($maintenances, 'maintenanceid');
		self::convertPropertyReference($maintenances, 'groups.groupid');
		self::convertPropertyReference($maintenances, 'hosts.hostid');
	}

	private static function createCorrelations(array &$correlations): void {
		if (!$correlations) {
			return;
		}

		self::convertCorrelationReferences($correlations);

		$result = self::call('correlation.create', $correlations);

		foreach ($correlations as $correlation) {
			self::$objectids['correlation'][$correlation['name']] = array_shift($result['correlationids']);
		}
	}

	public static function convertCorrelationReferences(array &$correlations): void {
		self::convertPropertyReference($correlations, 'correlationid');
		self::convertPropertyReference($correlations, 'filter.conditions.groupid');
	}

	private static function createSlas(array &$slas): void {
		if (!$slas) {
			return;
		}

		foreach ($slas as &$sla) {
			$sla = self::prepareSla($sla);
		}
		unset($sla);

		self::converSlaReferences($slas);

		$result = self::call('sla.create', $slas);

		foreach ($slas as $sla) {
			self::$objectids['sla'][$sla['name']] = array_shift($result['slaids']);
		}
	}

	/**
	 * @param array $sla
	 *
	 * @return array
	 */
	public static function prepareSla(array $sla): array {
		return $sla + [
			'period' => ZBX_SLA_PERIOD_DAILY,
			'slo' => 99.9999,
			'timezone' => 'Europe/Riga'
		];
	}

	public static function converSlaReferences(array &$slas): void {
		self::convertPropertyReference($slas, 'slaid');
		self::convertPropertyReference($slas, 'filter.conditions.groupid');
	}

	private static function createReports(array &$reports): void {
		if (!$reports) {
			return;
		}

		self::converReportReferences($reports);

		$result = self::call('report.create', $reports);

		foreach ($reports as $report) {
			self::$objectids['report'][$report['name']] = array_shift($result['reportids']);
		}
	}

	public static function converReportReferences(array &$reports): void {
		self::convertPropertyReference($reports, 'reportid');
		self::convertPropertyReference($reports, 'userid');
		self::convertPropertyReference($reports, 'dashboardid');
		self::convertPropertyReference($reports, 'users.userid');
		self::convertPropertyReference($reports, 'users.access_userid');
		self::convertPropertyReference($reports, 'user_groups.usrgrpid');
		self::convertPropertyReference($reports, 'user_groups.access_userid');
	}

	/**
	 * Check for, and replace a reference ID in the given objects' property with the corresponding object's record ID.
	 *
	 * @param array  $objects   Array of objects containing the referenced property.
	 * @param string $property  The reference key. A "." symbol is used as a separator for nested property references,
	 *                          f.e., `templates.templateid`. In case of matching object names (e.g. item inherited from
	 *                          template to host), the contained reference should include further specific parent object
	 *                          references, e.g.: `:item:item.key:host:my.name` vs `:items:item.key:template:my.name`.
	 */
	private static function convertPropertyReference(array &$objects, string $property): void {
		is_numeric(key($objects))
			? self::convertPropertyReferenceForObjects($objects, $property)
			: self::convertPropertyReferenceForObject($objects, $property);
	}

	private static function convertPropertyReferenceForObjects(array &$objects, string $property): void {
		$nested = strpos($property, '.') !== false;

		if ($nested) {
			[$property, $sub_property] = explode('.', $property, 2);
		}

		foreach ($objects as &$object) {
			if (!array_key_exists($property, $object)) {
				continue;
			}
			elseif (!$nested) {
				self::convertValueReference($object[$property]);
				continue;
			}

			if (strpos($sub_property, '.') !== false) {
				self::convertPropertyReference($object[$property], $sub_property);
				continue;
			}

			if (is_numeric(key($object[$property]))) {
				foreach ($object[$property] as &$_object) {
					if (array_key_exists($sub_property, $_object)) {
						self::convertValueReference($_object[$sub_property]);
					}
				}
				unset($_object);
			}
			else {
				self::convertValueReference($object[$property][$sub_property]);
			}
		}
		unset($object);
	}

	private static function convertPropertyReferenceForObject(array &$object, string $property): void {
		$nested = strpos($property, '.') !== false;

		if ($nested) {
			[$property, $sub_property] = explode('.', $property, 2);
		}

		if (array_key_exists($property, $object)) {
			if ($nested) {
				is_numeric(key($object[$property]))
					? self::convertPropertyReferenceForObjects($object[$property], $sub_property)
					: self::convertPropertyReferenceForObject($object[$property], $sub_property);
			}
			else {
				self::convertValueReference($object[$property]);
			}
		}
	}

	public static function unsetDeletedObjectIds(array $objectids): void {
		foreach ($objectids as $objectid) {
			self::convertValueReference($objectid, true);
		}
	}

	public static function getConvertedValueReferences(array $values): array {
		self::convertValueReferences($values);

		return $values;
	}

	public static function convertValueReferences(array &$values): void {
		foreach ($values as &$value) {
			self::convertValueReference($value);
		}
		unset($value);
	}

	public static function getConvertedValueReference(string $value): string {
		self::convertValueReference($value);

		return $value;
	}

	/**
	 * Check for, and replace a reference ID in the given value with the corresponding object's record ID.
	 *
	 * @param string $value  The value possibly containing the reference. In case of matching object names (e.g. item
	 *                       inherited from template to host), the contained reference should include further specific
	 *                       parent object references, e.g.: `:item:item.key:host:my.name` vs
	 *                       `:items:item.key:template:my.name`.
	 * @param bool  $unset   Whether to unset the value from the $objectids array, if it is convertible.
	 */
	private static function convertValueReference(string &$value, bool $unset = false): void {
		if (!is_string($value) || $value === '' || $value[0] !== ':') {
			return;
		}

		$colon_positions = [0];
		$p = 0;

		while ($p = strpos($value, ':', $p + 1)) {
			if ($value[$p - 1] !== '\\') {
				$colon_positions[] = $p;
			}
		}

		if (count($colon_positions) % 2 != 0 || !isset($value[end($colon_positions) + 1])) {
			return;
		}

		$object_type = substr($value, $colon_positions[0] + 1, $colon_positions[1] - 1);
		$name = substr($value, $colon_positions[1] + 1,
			array_key_exists(2, $colon_positions)
				? $colon_positions[2] - $colon_positions[1] - 1
				: strlen($value) - $colon_positions[1] - 1
		);

		unset($colon_positions[0], $colon_positions[1]);

		if (!array_key_exists($object_type, self::$objectids)
				|| !array_key_exists($name, self::$objectids[$object_type])
				|| ($colon_positions && !is_array(self::$objectids[$object_type][$name]))) {
			return;
		}

		if (!$colon_positions) {
			$objectid = self::$objectids[$object_type][$name];

			if ($unset) {
				unset(self::$objectids[$object_type][$name]);

				if (!self::$objectids[$object_type]) {
					unset(self::$objectids[$object_type]);
				}
			}

			while (is_array($objectid)) {
				$objectid = end($objectid);
			}

			$value = $objectid;

			return;
		}

		$objectid = self::$objectids[$object_type][$name];

		while ($colon_positions) {
			if (!is_array($objectid)) {
				return;
			}

			$colon_start = array_shift($colon_positions);
			array_shift($colon_positions);

			$ref = $colon_positions
				? substr($value, $colon_start, reset($colon_positions) - $colon_start - 1)
				: substr($value, $colon_start);

			if (!array_key_exists($ref, $objectid)) {
				return;
			}

			$objectid = $objectid[$ref];
		}

		$value = $objectid;
	}

	/**
	 * Delete inserted objects from the database and reset internal data.
	 */
	public static function cleanUp(): void {
		if (array_key_exists('report', self::$objectids)) {
			self::call('report.delete', array_values(self::$objectids['report']));
		}

		if (array_key_exists('sla', self::$objectids)) {
			self::call('sla.delete', array_values(self::$objectids['sla']));
		}

		if (array_key_exists('correlation', self::$objectids)) {
			self::call('correlation.delete', array_values(self::$objectids['correlation']));
		}

		if (array_key_exists('maintenance', self::$objectids)) {
			self::call('maintenance.delete', array_values(self::$objectids['maintenance']));
		}

		if (array_key_exists('service', self::$objectids)) {
			self::call('service.delete', array_values(self::$objectids['service']));
		}

		if (array_key_exists('dashboard', self::$objectids)) {
			self::call('dashboard.delete', array_values(self::$objectids['dashboard']));
		}

		if (array_key_exists('graph', self::$objectids)) {
			self::call('graph.delete', array_values(self::$objectids['graph']));
		}

		if (array_key_exists('action', self::$objectids)) {
			self::call('action.delete', array_values(self::$objectids['action']));
		}

		if (array_key_exists('drule', self::$objectids)) {
			self::call('drule.delete', array_values(self::$objectids['drule']));
		}

		if (array_key_exists('script', self::$objectids)) {
			self::call('script.delete', array_values(self::$objectids['script']));
		}

		if (array_key_exists('user', self::$objectids)) {
			self::call('user.delete', array_values(self::$objectids['user']));
		}

		if (array_key_exists('user_group', self::$objectids)) {
			self::call('usergroup.delete', array_values(self::$objectids['user_group']));
		}

		if (array_key_exists('role', self::$objectids)) {
			self::call('role.delete', array_values(self::$objectids['role']));
		}

		if (array_key_exists('template', self::$objectids)) {
			self::call('template.delete', array_values(self::$objectids['template']));
		}

		if (array_key_exists('host', self::$objectids)) {
			self::call('host.delete', array_values(self::$objectids['host']));
		}

		if (array_key_exists('proxy', self::$objectids)) {
			self::call('proxy.delete', array_values(self::$objectids['proxy']));
		}

		if (array_key_exists('template_group', self::$objectids)) {
			ZABBIX_VERSION < 6.4
				? self::call('hostgroup.delete', array_values(self::$objectids['template_group']))
				: self::call('templategroup.delete', array_values(self::$objectids['template_group']));
		}

		if (array_key_exists('host_group', self::$objectids)) {
			self::call('hostgroup.delete', array_values(self::$objectids['host_group']));
		}

		if (array_key_exists('value_map', self::$detachedids)) {
			self::call('valuemap.delete', array_values(self::$detachedids['value_map']));
		}

		if (array_key_exists('httptest', self::$detachedids)) {
			self::call('httptest.delete', array_values(self::$detachedids['httptest']));
		}

		if (array_key_exists('item', self::$detachedids)) {
			self::call('item.delete', array_values(self::$detachedids['item']));
		}

		if (array_key_exists('graph_prototype', self::$detachedids)) {
			self::call('graphprototype.delete', array_values(self::$detachedids['graph_prototype']));
		}

		if (array_key_exists('trigger_prototype', self::$detachedids)) {
			self::call('triggerprototype.delete', array_values(self::$detachedids['trigger_prototype']));
		}

		if (array_key_exists('host_prototype', self::$detachedids)) {
			self::call('hostprototype.delete', array_values(self::$detachedids['host_prototype']));
		}

		if (array_key_exists('item_prototype', self::$detachedids)) {
			self::call('itemprototype.delete', array_values(self::$detachedids['item_prototype']));
		}

		if (array_key_exists('lld_rule', self::$detachedids)) {
			self::call('discoveryrule.delete', array_values(self::$detachedids['lld_rule']));
		}

		if (array_key_exists('template_dashboard', self::$detachedids)) {
			self::call('templatedashboard.delete', array_values(self::$detachedids['template_dashboard']));
		}

		self::$objectids = [];
		self::$detachedids = [];
	}

	private static function prepareEnabledGuestUser(): array {
		$guest = self::call('user.get', [
			'output' => ['userid'],
			'filter' => ['username' => 'guest'],
			'selectUsrgrps' => ['usrgrpid', 'name']
		])[0];

		if (!in_array('Disabled', array_column($guest['usrgrps'], 'name'))) {
			return [];
		}

		foreach ($guest['usrgrps'] as $i => &$group) {
			if ($group['name'] === 'Disabled') {
				unset($guest['usrgrps'][$i]);
				continue;
			}

			$group = ['usrgrpid' => $group['usrgrpid']];
		}
		unset($group);

		return $guest;
	}

	private static function prepareDisabledGuestUser(): array {
		$guest = self::call('user.get', [
			'output' => ['userid'],
			'filter' => ['username' => 'guest'],
			'selectUsrgrps' => ['usrgrpid', 'name']
		])[0];

		if (in_array('Disabled', array_column($guest['usrgrps'], 'name'))) {
			return [];
		}

		$groups = self::call('usergroup.get', [
			'output' => ['usrgrpid', 'name'],
			'filter' => ['name' => 'Disabled']
		]);

		$guest['usrgrps'] = array_merge($guest['usrgrps'] , $groups);

		foreach ($guest['usrgrps'] as &$group) {
			$group = ['usrgrpid' => $group['usrgrpid']];
		}
		unset($group);

		return $guest;
	}

	/**
	 * Removes the 'Disabled' user group from guest user, keeping the others.
	 */
	public static function enableGuestUser(): void {
		$guest= self::prepareEnabledGuestUser();

		if ($guest) {
			self::call('user.update', $guest);
		}
	}

	/**
	 * Assigns the 'Disabled' user group to guest user, keeping the others.
	 */
	public static function disableGuestUser(): void {
		$guest = self::prepareDisabledGuestUser();

		if ($guest) {
			self::call('user.update', $guest);
		}
	}
}