<?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 for generating DB objects or API data for tests.
 */
class CTestDataHelper {

	private static $objectids = [];

	/**
	 * Create objects using API.
	 *
	 * @param array $objects
	 */
	public static function createObjects(array $objects): void {
		$objects += array_fill_keys(['template_groups', 'host_groups', 'templates', 'hosts'], []);

		try {
			self::createTemplateGroups($objects['template_groups']);
			self::createHostGroups($objects['host_groups']);
			self::createTemplates($objects['templates']);
			self::createHosts($objects['hosts']);
		}
		catch (Exception $e) {
			self::cleanUp();

			throw $e;
		}
	}

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

		$result = CDataHelper::call('templategroup.create', $template_groups);

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

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

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

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

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

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

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

			foreach ($template['groups'] as &$template_group) {
				self::processReference($template_group, 'groupid');
			}
			unset($template_group);

			foreach ($template['templates'] as &$_template) {
				self::processReference($_template, 'templateid');
			}
			unset($_template);

			if (array_key_exists('value_maps', $template)) {
				foreach ($template['value_maps'] as $value_map) {
					$value_maps[] = $value_map + ['hostid' => ':template:'.$template['host']];
				}

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

			if (array_key_exists('items', $template)) {
				foreach ($template['items'] as $item) {
					$items[] = $item + ['hostid' => ':template:'.$template['host']];
				}

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

			if (array_key_exists('lld_rules', $template)) {
				foreach ($template['lld_rules'] as $lld_rule) {
					$lld_rules[] = $lld_rule + ['hostid' => ':template:'.$template['host']];
				}

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

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

		foreach ($templates as $template) {
			self::$objectids['templates'][$template['host']] = array_shift($result['templateids']);
		}

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

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

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

		foreach ($hosts as &$host) {
			$host += [
				'groups' => [
					['groupid' => end(self::$objectids['host_groups'])]
				],
				'templates' => []
			];

			foreach ($host['groups'] as &$host_group) {
				self::processReference($host_group, 'groupid');
			}
			unset($host_group);

			foreach ($host['templates'] as &$template) {
				self::processReference($template, 'templateid');
			}
			unset($template);

			self::processReference($host, 'proxy_hostid');

			if (array_key_exists('value_maps', $host)) {
				foreach ($host['value_maps'] as $value_map) {
					$value_maps[] = $value_map + ['hostid' => ':host:'.$host['host']];
				}

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

			if (array_key_exists('items', $host)) {
				foreach ($host['items'] as $item) {
					$items[] = $item + ['hostid' => ':host:'.$host['host']];
				}

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

			if (array_key_exists('lld_rules', $host)) {
				foreach ($host['lld_rules'] as $lld_rule) {
					$lld_rules[] = $lld_rule + ['hostid' => ':host:'.$host['host']];
				}

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

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

		foreach ($hosts as $host) {
			self::$objectids['hosts'][$host['host']] = array_shift($result['hostids']);
		}

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

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

		foreach ($value_maps as &$value_map) {
			self::processReference($value_map, 'hostid');
		}
		unset($value_map);

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

		foreach ($value_maps as $value_map) {
			self::$objectids['value_maps'][$value_map['name']] = array_shift($result['valuemapids']);
		}
	}

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

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

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

			self::processReference($item, 'hostid');
			self::processReference($item, 'valuemapid');
			self::processReference($item, 'interfaceid');

			$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]);
			}
		}

		do {
			foreach ($items as &$item) {
				self::processReference($item, 'master_itemid');
			}
			unset($item);

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

			$_items = [];

			foreach ($items as $i => $item) {

				self::$objectids['items'][$item['key_']][$host_refs[$i]] = array_shift($result['itemids']);

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

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

		return $item;
	}

	/**
	 * @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;
	}

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

		$host_refs = [];
		$item_prototypes = [];

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

			self::processReference($lld_rule, 'hostid');
			self::processReference($lld_rule, 'interfaceid');
			self::processReference($lld_rule, 'master_itemid');

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

			if (array_key_exists('item_prototypes', $lld_rule)) {
				foreach ($lld_rule['item_prototypes'] as $item_prototype) {
					$item_prototypes[] = $item_prototype
						+ ['hostid' => $host_refs[$i], 'ruleid' => ':lld_rule:'.$lld_rule['key_']];
				}

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

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

		foreach ($lld_rules as $i => $lld_rule) {
			self::$objectids['lld_rules'][$lld_rule['key_']][$host_refs[$i]] = array_shift($result['itemids']);
		}

		self::createItemPrototypes($item_prototypes);
	}

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

		return $lld_rule;
	}

	/**
	 * @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;
	}

	/**
	 * @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'];

			self::processReference($item, 'hostid');
			self::processReference($item, 'ruleid');
			self::processReference($item, 'valuemapid');
			self::processReference($item, 'interfaceid');

			$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]);
			}
		}

		do {
			foreach ($items as &$item) {
				self::processReference($item, 'master_itemid');
			}
			unset($item);

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

			$_items = [];

			foreach ($items as $i => $item) {
				self::$objectids['item_prototypes'][$item['key_']][$host_refs[$i]] = array_shift($result['itemids']);

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

		self::createDiscoveredItems($discovered_items);
	}

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

		return $item;
	}

	/**
	 * @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 $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'];

			self::processReference($item, 'hostid');
			self::processReference($item, 'item_prototypeid');
			self::processReference($item, 'valuemapid');
			self::processReference($item, 'interfaceid');

			$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 {
			foreach ($discovered_items as &$item) {
				self::processReference($item, 'master_itemid');
			}
			unset($item);

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

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

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

				self::$objectids['discovered_items'][$item['key_']][$host_refs[$i]] = $itemid;
				$item_discoveries[] = [
					'itemid' => $itemid,
					'parent_itemid' => $item_prototypeids[$i],
					'key_' => $item['key_']
				];

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

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

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

	/**
	 * Check for, and replace a reference ID with the corresponding object's record ID.
	 *
	 * @param array  $object    Array 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 processReference(array &$object, string $property): void {
		if (strpos($property, '.') !== false) {
			[$property, $sub_property] = explode('.', $property, 2);

			if (is_string(key($object))) {
				if (!array_key_exists($property, $object)) {
					return;
				}

				self::processReference($object[$property], $sub_property);
			}
			else {
				foreach ($object as &$_object) {
					if (!array_key_exists($property, $_object)) {
						continue;
					}

					self::processReference($_object[$property], $sub_property);
				}
				unset($_object);
			}

			return;
		}

		if (!array_key_exists($property, $object) || !is_string($object[$property]) || $object[$property][0] !== ':') {
			return;
		}

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

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

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

		$object_type = substr($object[$property], $colon_positions[0] + 1, $colon_positions[1] - 1).'s';
		$name = substr($object[$property], $colon_positions[1] + 1,
			array_key_exists(2, $colon_positions)
				? $colon_positions[2] - $colon_positions[1] - 1
				: strlen($object[$property]) - $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) {
			$value = self::$objectids[$object_type][$name];

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

			$object[$property] = $value;
			return;
		}

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

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

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

			$ref = $colon_positions
				? substr($object[$property], $colon_start, reset($colon_positions) - $colon_start - 1)
				: substr($object[$property], $colon_start);

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

			$value = $value[$ref];
		}

		$object[$property] = $value;
	}

	/**
	 * Replace all references with record IDs in an array recursively.
	 *
	 * @param string $method
	 * @param array $params
	 */
	public static function processReferences(string $method, array &$params): void {
		$object_type = substr($method, 0, strpos($method, '.'));

		$ref_fields = [];

		switch ($object_type) {
			case 'host':
				$ref_fields = ['hostid', 'groups.groupid', 'templates.templateid'];
				break;

			case 'item':
				$ref_fields = ['itemid', 'hostid', 'valuemapid', 'interfaceid', 'master_itemid'];
				break;

			case 'discoveryrule':
				$ref_fields = ['itemid', 'hostid', 'interfaceid', 'master_itemid'];
				break;

			case 'itemprototype':
				$ref_fields = ['itemid', 'hostid', 'ruleid', 'valuemapid', 'interfaceid', 'master_itemid'];
				break;
		}

		if (is_string(key($params))) {
			foreach ($ref_fields as $ref_field) {
				self::processReference($params, $ref_field);
			}
		}
		else {
			foreach ($params as &$object) {
				foreach ($ref_fields as $ref_field) {
					self::processReference($object, $ref_field);
				}
			}
			unset($object);
		}
	}

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

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

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

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

		self::$objectids = [];
	}
}