<?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 correlations.
 */
class CCorrelation extends CApiService {

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

	protected $tableName = 'correlation';
	protected $tableAlias = 'c';
	protected $sortColumns = ['correlationid', 'name', 'status'];

	/**
	 * Set correlation default options in addition to global options.
	 */
	public function __construct() {
		parent::__construct();

		$this->getOptions = array_merge($this->getOptions, [
			'selectFilter'		=> null,
			'selectOperations'	=> null,
			'correlationids'	=> null,
			'editable'			=> false,
			'sortfield'			=> '',
			'sortorder'			=> ''
		]);
	}

	/**
	 * Get correlation data.
	 *
	 * @param array $options
	 *
	 * @return array|string
	 */
	public function get($options = []) {
		$options = zbx_array_merge($this->getOptions, $options);

		if ($options['editable'] && self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
			return ($options['countOutput'] && !$options['groupCount']) ? '0' : [];
		}

		$res = DBselect($this->createSelectQuery($this->tableName(), $options), $options['limit']);

		$result = [];
		while ($row = DBfetch($res)) {
			if ($options['countOutput']) {
				if ($options['groupCount']) {
					$result[] = $row;
				}
				else {
					$result = $row['rowscount'];
				}
			}
			else {
				$result[$row[$this->pk()]] = $row;
			}
		}

		if ($options['countOutput']) {
			return $result;
		}

		if ($result) {
			$result = $this->addRelatedObjects($options, $result);

			foreach ($result as &$correlation) {
				// Unset the fields that are returned in the filter.
				unset($correlation['formula'], $correlation['evaltype']);

				if ($options['selectFilter'] !== null) {
					$filter = $this->unsetExtraFields(
						[$correlation['filter']],
						['conditions', 'formula', 'evaltype'],
						$options['selectFilter']
					);
					$filter = reset($filter);

					if (array_key_exists('conditions', $filter)) {
						foreach ($filter['conditions'] as &$condition) {
							unset($condition['correlationid'], $condition['corr_conditionid']);
						}
						unset($condition);
					}

					$correlation['filter'] = $filter;
				}
			}
			unset($correlation);
		}

		// removing keys (hash -> array)
		if (!$options['preservekeys']) {
			$result = zbx_cleanHashes($result);
		}

		return $result;
	}

	/**
	 * @param array $correlations
	 *
	 * @return array
	 */
	public function create($correlations) {
		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
			self::exception(ZBX_API_ERROR_PERMISSIONS,
				_s('No permissions to call "%1$s.%2$s".', 'correlation', __FUNCTION__)
			);
		}

		self::validateCreate($correlations);

		$ins_correlations = [];

		foreach ($correlations as $correlation) {
			$ins_correlations[] = $correlation + ['evaltype' => $correlation['filter']['evaltype']];
		}

		// Insert correlations into DB, get back array with new correlation IDs.
		$correlationids = DB::insert('correlation', $ins_correlations);

		foreach ($correlations as $index => &$correlation) {
			$correlation['correlationid'] = $correlationids[$index];
		}
		unset($correlation);

		self::updateConditions($correlations, __FUNCTION__);
		self::updateOperations($correlations, __FUNCTION__);

		self::addAuditLog(CAudit::ACTION_ADD, CAudit::RESOURCE_CORRELATION, $correlations);

		return ['correlationids' => $correlationids];
	}

	/**
	 * @static
	 *
	 * @param array $db_conditions
	 * @param array $condition
	 *
	 * @return array|null
	 */
	private static function getDbCondition(array $db_conditions, array $condition): ?array {
		foreach ($db_conditions as $db_condition) {
			if ($condition['type'] != $db_condition['type']) {
				continue;
			}

			switch ($condition['type']) {
				case ZBX_CORR_CONDITION_OLD_EVENT_TAG:
				case ZBX_CORR_CONDITION_NEW_EVENT_TAG:
					if ($condition['tag'] === $db_condition['tag']) {
						return $db_condition;
					}
					break;

				case ZBX_CORR_CONDITION_NEW_EVENT_HOSTGROUP:
					if ($condition['operator'] == $db_condition['operator']
							&& bccomp($condition['groupid'], $db_condition['groupid']) == 0) {
						return $db_condition;
					}
					break;

				case ZBX_CORR_CONDITION_EVENT_TAG_PAIR:
					if ($condition['oldtag'] === $db_condition['oldtag']
							&& $condition['newtag'] === $db_condition['newtag']) {
						return $db_condition;
					}
					break;

				case ZBX_CORR_CONDITION_OLD_EVENT_TAG_VALUE:
				case ZBX_CORR_CONDITION_NEW_EVENT_TAG_VALUE:
					if ($condition['operator'] == $db_condition['operator']
							&& $condition['tag'] === $db_condition['tag']
							&& $condition['value'] === $db_condition['value']) {
						return $db_condition;
					}
					break;
			}
		}

		return null;
	}

	/**
	 * @static
	 *
	 * @param array      $correlations
	 * @param string     $method
	 * @param array|null $db_correlations
	 */
	private static function updateConditions(array &$correlations, string $method,
			array $db_correlations = null): void {
		$ins_conditions = [];
		$del_corr_conditionids = [];

		foreach ($correlations as &$correlation) {
			if (!array_key_exists('filter', $correlation)) {
				continue;
			}

			$db_conditions = ($method === 'update')
				? $db_correlations[$correlation['correlationid']]['filter']['conditions']
				: [];

			foreach ($correlation['filter']['conditions'] as &$condition) {
				$db_condition = self::getDbCondition($db_conditions, $condition);

				if ($db_condition !== null) {
					$condition['corr_conditionid'] = $db_condition['corr_conditionid'];
					unset($db_conditions[$db_condition['corr_conditionid']]);
				}
				else {
					$ins_conditions[] = ['correlationid' => $correlation['correlationid']] + $condition;
				}
			}
			unset($condition);

			$del_corr_conditionids = array_merge($del_corr_conditionids, array_keys($db_conditions));
		}
		unset($correlation);

		if ($ins_conditions) {
			$conditionids = DB::insert('corr_condition', $ins_conditions);
		}

		if ($del_corr_conditionids) {
			DB::delete('corr_condition_tag', ['corr_conditionid' => $del_corr_conditionids]);
			DB::delete('corr_condition_group', ['corr_conditionid' => $del_corr_conditionids]);
			DB::delete('corr_condition_tagpair', ['corr_conditionid' => $del_corr_conditionids]);
			DB::delete('corr_condition_tagvalue', ['corr_conditionid' => $del_corr_conditionids]);
			DB::delete('corr_condition', ['corr_conditionid' => $del_corr_conditionids]);
		}

		$ins_condition_tags = [];
		$ins_condition_groups = [];
		$ins_condition_tagpairs = [];
		$ins_condition_tagvalues = [];
		$upd_correlations = [];

		foreach ($correlations as &$correlation) {
			if (!array_key_exists('filter', $correlation)) {
				continue;
			}

			foreach ($correlation['filter']['conditions'] as &$condition) {
				if (!array_key_exists('corr_conditionid', $condition)) {
					$condition['corr_conditionid'] = array_shift($conditionids);

					switch ($condition['type']) {
						case ZBX_CORR_CONDITION_OLD_EVENT_TAG:
						case ZBX_CORR_CONDITION_NEW_EVENT_TAG:
							$ins_condition_tags[] = $condition;
							break;

						case ZBX_CORR_CONDITION_NEW_EVENT_HOSTGROUP:
							$ins_condition_groups[] = $condition;
							break;

						case ZBX_CORR_CONDITION_EVENT_TAG_PAIR:
							$ins_condition_tagpairs[] = $condition;
							break;

						case ZBX_CORR_CONDITION_OLD_EVENT_TAG_VALUE:
						case ZBX_CORR_CONDITION_NEW_EVENT_TAG_VALUE:
							$ins_condition_tagvalues[] = $condition;
							break;
					}
				}
			}
			unset($condition);

			if ($method === 'create' || array_key_exists('filter', $correlation)) {
				if ($correlation['filter']['evaltype'] == CONDITION_EVAL_TYPE_EXPRESSION) {
					$correlation['filter']['formula'] = CConditionHelper::replaceLetterIds(
						$correlation['filter']['formula'],
						array_column($correlation['filter']['conditions'], 'corr_conditionid', 'formulaid')
					);
				}
				else {
					$correlation['filter']['formula'] = '';
				}

				$db_formula = ($method === 'update')
					? $db_correlations[$correlation['correlationid']]['filter']['formula']
					: '';

				if ($correlation['filter']['formula'] !== $db_formula) {
					$upd_correlations[] = [
						'values' => ['formula' => $correlation['filter']['formula']],
						'where' => ['correlationid' => $correlation['correlationid']]
					];
				}
			}
		}
		unset($correlation);

		if ($ins_condition_tags) {
			DB::insert('corr_condition_tag', $ins_condition_tags, false);
		}

		if ($ins_condition_groups) {
			DB::insert('corr_condition_group', $ins_condition_groups, false);
		}

		if ($ins_condition_tagpairs) {
			DB::insert('corr_condition_tagpair', $ins_condition_tagpairs, false);
		}

		if ($ins_condition_tagvalues) {
			DB::insert('corr_condition_tagvalue', $ins_condition_tagvalues, false);
		}

		if ($upd_correlations) {
			DB::update('correlation', $upd_correlations);
		}
	}

	/**
	 * @static
	 *
	 * @param array      $correlations
	 * @param string     $method
	 * @param array|null $db_correlations
	 */
	private static function updateOperations(array &$correlations, string $method,
			array $db_correlations = null): void {
		$ins_operations = [];
		$del_corr_operationids = [];

		foreach ($correlations as &$correlation) {
			if (!array_key_exists('operations', $correlation)) {
				continue;
			}

			$db_operations = ($method === 'update')
				? array_column($db_correlations[$correlation['correlationid']]['operations'], null, 'type')
				: [];

			foreach ($correlation['operations'] as &$operation) {
				if (array_key_exists($operation['type'], $db_operations)) {
					$operation['corr_operationid'] = $db_operations[$operation['type']]['corr_operationid'];
					unset($db_operations[$operation['type']]);
				}
				else {
					$ins_operations[] = ['correlationid' => $correlation['correlationid']] + $operation;
				}
			}
			unset($operation);

			$del_corr_operationids = array_merge($del_corr_operationids,
				array_column($db_operations, 'corr_operationid')
			);
		}
		unset($correlation);

		if ($ins_operations) {
			$corr_operationids = DB::insert('corr_operation', $ins_operations);
		}

		if ($del_corr_operationids) {
			DB::delete('corr_operation', ['corr_operationid' => $del_corr_operationids]);
		}

		foreach ($correlations as &$correlation) {
			if (!array_key_exists('operations', $correlation)) {
				continue;
			}

			foreach ($correlation['operations'] as &$operation) {
				if (!array_key_exists('corr_operationid', $operation)) {
					$operation['corr_operationid'] = array_shift($corr_operationids);
				}
			}
			unset($operation);
		}
		unset($correlation);
	}

	/**
	 * @param array $correlations
	 *
	 * @return array
	 */
	public function update(array $correlations): array {
		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
			self::exception(ZBX_API_ERROR_PERMISSIONS,
				_s('No permissions to call "%1$s.%2$s".', 'correlation', __FUNCTION__)
			);
		}

		self::validateUpdate($correlations, $db_correlations);

		$upd_correlations = [];

		foreach ($correlations as $correlation) {
			$db_correlation = $db_correlations[$correlation['correlationid']];

			if (array_key_exists('filter', $correlation)) {
				$correlation['evaltype'] = $correlation['filter']['evaltype'];
				$db_correlation['evaltype'] = $db_correlation['filter']['evaltype'];
			}

			$upd_correlation = DB::getUpdatedValues('correlation', $correlation, $db_correlation);

			if ($upd_correlation) {
				$upd_correlations[] = [
					'values' => $upd_correlation,
					'where' => ['correlationid' => $correlation['correlationid']]
				];
			}
		}

		if ($upd_correlations) {
			DB::update('correlation', $upd_correlations);
		}

		self::updateConditions($correlations, __FUNCTION__, $db_correlations);
		self::updateOperations($correlations, __FUNCTION__, $db_correlations);

		self::addAuditLog(CAudit::ACTION_UPDATE, CAudit::RESOURCE_CORRELATION, $correlations, $db_correlations);

		return ['correlationids' => array_column($correlations, 'correlationid')];
	}

	/**
	 * @param array $correlationids
	 *
	 * @return array
	 */
	public function delete(array $correlationids): array {
		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
			self::exception(ZBX_API_ERROR_PERMISSIONS,
				_s('No permissions to call "%1$s.%2$s".', 'correlation', __FUNCTION__)
			);
		}

		self::validateDelete($correlationids, $db_correlations);

		DB::delete('correlation', ['correlationid' => $correlationids]);

		self::addAuditLog(CAudit::ACTION_DELETE, CAudit::RESOURCE_CORRELATION, $db_correlations);

		return ['correlationids' => $correlationids];
	}

	/**
	 * Validates the input parameters for the delete() method.
	 *
	 * @static
	 *
	 * @param array      $correlationids
	 * @param array|null $db_correlations
	 *
	 * @throws APIException if the input is invalid.
	 */
	private static function validateDelete(array &$correlationids, array &$db_correlations = null): void {
		$api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];

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

		$db_correlations = DB::select('correlation', [
			'output' => ['correlationid', 'name'],
			'correlationids' => $correlationids,
			'preservekeys' => true
		]);

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

	/**
	 * Check for unique event correlation names.
	 *
	 * @static
	 *
	 * @param array      $correlations
	 * @param array|null $db_correlations
	 *
	 * @throws APIException if event correlation  names are not unique.
	 */
	protected static function checkDuplicates(array $correlations, array $db_correlations = null): void {
		$names = [];

		foreach ($correlations as $correlation) {
			if (!array_key_exists('name', $correlation)) {
				continue;
			}

			if ($db_correlations === null
					|| $correlation['name'] !== $db_correlations[$correlation['correlationid']]['name']) {
				$names[] = $correlation['name'];
			}
		}

		if (!$names) {
			return;
		}

		$duplicates = DB::select('correlation', [
			'output' => ['name'],
			'filter' => ['name' => $names],
			'limit' => 1
		]);

		if ($duplicates) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Correlation "%1$s" already exists.', $duplicates[0]['name']));
		}
	}

	/**
	 * Returns validation rules for the filter object.
	 *
	 * @static
	 *
	 * @return array
	 */
	private static function getFilterValidationRules(): array {
		$condition_fields = [
			'type' =>		['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [ZBX_CORR_CONDITION_OLD_EVENT_TAG, ZBX_CORR_CONDITION_NEW_EVENT_TAG, ZBX_CORR_CONDITION_NEW_EVENT_HOSTGROUP, ZBX_CORR_CONDITION_EVENT_TAG_PAIR, ZBX_CORR_CONDITION_OLD_EVENT_TAG_VALUE, ZBX_CORR_CONDITION_NEW_EVENT_TAG_VALUE])],
			'operator' =>	['type' => API_MULTIPLE, 'rules' => [
								['if' => ['field' => 'type', 'in' => ZBX_CORR_CONDITION_NEW_EVENT_HOSTGROUP], 'type' => API_INT32, 'in' => implode(',', [CONDITION_OPERATOR_EQUAL, CONDITION_OPERATOR_NOT_EQUAL])],
								['if' => ['field' => 'type', 'in' => implode(',', [ZBX_CORR_CONDITION_OLD_EVENT_TAG_VALUE, ZBX_CORR_CONDITION_NEW_EVENT_TAG_VALUE])], 'type' => API_INT32, 'in' => implode(',', [CONDITION_OPERATOR_EQUAL, CONDITION_OPERATOR_NOT_EQUAL, CONDITION_OPERATOR_LIKE, CONDITION_OPERATOR_NOT_LIKE])],
								['else' => true, 'type' => API_INT32, 'in' => CONDITION_OPERATOR_EQUAL]
			]],
			'tag' =>		['type' => API_MULTIPLE, 'rules' => [
								['if' => ['field' => 'type', 'in' => implode(',', [ZBX_CORR_CONDITION_OLD_EVENT_TAG, ZBX_CORR_CONDITION_NEW_EVENT_TAG, ZBX_CORR_CONDITION_OLD_EVENT_TAG_VALUE, ZBX_CORR_CONDITION_NEW_EVENT_TAG_VALUE])], 'type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('corr_condition_tag', 'tag')],
								['else' => true, 'type' => API_UNEXPECTED]
			]],
			'groupid' =>	['type' => API_MULTIPLE, 'rules' => [
								['if' => ['field' => 'type', 'in' => ZBX_CORR_CONDITION_NEW_EVENT_HOSTGROUP], 'type' => API_ID, 'flags' => API_REQUIRED],
								['else' => true, 'type' => API_UNEXPECTED]
			]],
			'oldtag' =>		['type' => API_MULTIPLE, 'rules' => [
								['if' => ['field' => 'type', 'in' => ZBX_CORR_CONDITION_EVENT_TAG_PAIR], 'type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('corr_condition_tagpair', 'oldtag')],
								['else' => true, 'type' => API_UNEXPECTED]
			]],
			'newtag' =>		['type' => API_MULTIPLE, 'rules' => [
								['if' => ['field' => 'type', 'in' => ZBX_CORR_CONDITION_EVENT_TAG_PAIR], 'type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('corr_condition_tagpair', 'newtag')],
								['else' => true, 'type' => API_UNEXPECTED]
			]],
			'value' =>		['type' => API_MULTIPLE, 'rules' => [
								['if' => ['field' => 'type', 'in' => implode(',', [ZBX_CORR_CONDITION_OLD_EVENT_TAG_VALUE, ZBX_CORR_CONDITION_NEW_EVENT_TAG_VALUE])], 'type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('corr_condition_tagvalue', 'value')],
								['else' => true, 'type' => API_UNEXPECTED]
			]]
		];

		return ['type' => API_OBJECT, 'fields' => [
			'evaltype' =>	['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [CONDITION_EVAL_TYPE_AND_OR, CONDITION_EVAL_TYPE_AND, CONDITION_EVAL_TYPE_OR, CONDITION_EVAL_TYPE_EXPRESSION])],
			'formula' =>	['type' => API_MULTIPLE, 'rules' => [
								['if' => ['field' => 'evaltype', 'in' => CONDITION_EVAL_TYPE_EXPRESSION], 'type' => API_COND_FORMULA, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('correlation', 'formula')],
								['else' => true, 'type' => API_UNEXPECTED]
			]],
			'conditions' =>	['type' => API_MULTIPLE, 'flags' => API_REQUIRED, 'rules' => [
								['if' => ['field' => 'evaltype', 'in' => CONDITION_EVAL_TYPE_EXPRESSION], 'type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'uniq' => [['formulaid']], 'fields' => [
									'formulaid' =>	['type' => API_COND_FORMULAID, 'flags' => API_REQUIRED]
								] + $condition_fields],
								['else' => true, 'type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'fields' => $condition_fields]
			]]
		]];
	}

	/**
	 * Validates the input parameters for the create() method.
	 *
	 * @static
	 *
	 * @param array $correlations
	 *
	 * @throws APIException if the input is invalid.
	 */
	private static function validateCreate(array &$correlations): void {
		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['name']], 'fields' => [
			'name' =>			['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('correlation', 'name')],
			'description' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('correlation', 'description')],
			'status' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_CORRELATION_ENABLED, ZBX_CORRELATION_DISABLED])],
			'filter' =>			self::getFilterValidationRules() + ['flags' => API_REQUIRED],
			'operations' =>		['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'uniq' => [['type']], 'fields' => [
				'type' =>			['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [ZBX_CORR_OPERATION_CLOSE_OLD, ZBX_CORR_OPERATION_CLOSE_NEW])]
			]]
		]];

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

		self::checkDuplicates($correlations);
		self::validateFormula($correlations);
		self::checkHostGroups($correlations);
	}

	/**
	 * Check host group permissions.
	 *
	 * @static
	 *
	 * @param array $correlations
	 */
	private static function checkHostGroups(array $correlations): void {
		$groupids = [];

		foreach ($correlations as $correlation) {
			if (!array_key_exists('filter', $correlation) || !array_key_exists('conditions', $correlation['filter'])) {
				continue;
			}

			foreach ($correlation['filter']['conditions'] as $condition) {
				if ($condition['type'] == ZBX_CORR_CONDITION_NEW_EVENT_HOSTGROUP) {
					$groupids[$condition['groupid']] = true;
				}
			}
		}

		if (!$groupids) {
			return;
		}

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

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

	/**
	 * @static
	 *
	 * @param array      $correlations
	 * @param array|null $db_correlations
	 *
	 * @throws APIException if the input is invalid.
	 */
	private static function validateUpdate(array &$correlations, array &$db_correlations = null): void {
		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['correlationid'], ['name']], 'fields' => [
			'correlationid' =>	['type' => API_ID, 'flags' => API_REQUIRED],
			'name' =>			['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('correlation', 'name')],
			'description' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('correlation', 'description')],
			'status' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_CORRELATION_ENABLED, ZBX_CORRELATION_DISABLED])],
			'filter' =>			self::getFilterValidationRules(),
			'operations' =>		['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'uniq' => [['type']], 'fields' => [
				'type' =>			['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [ZBX_CORR_OPERATION_CLOSE_OLD, ZBX_CORR_OPERATION_CLOSE_NEW])]
			]]
		]];

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

		$db_correlations = DB::select('correlation', [
			'output' => ['correlationid', 'name', 'description', 'status'],
			'correlationids' => array_column($correlations, 'correlationid'),
			'preservekeys' => true
		]);

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

		self::checkDuplicates($correlations, $db_correlations);

		self::addAffectedObjects($correlations, $db_correlations);
		self::validateFormula($correlations);
		self::checkHostGroups($correlations);
	}

	/**
	 * Validate correlation condition formula IDs. Check the "formulaid" field and that formula matches the conditions.
	 *
	 * @static
	 *
	 * @param array $correlations
	 *
	 * @throws APIException if the input is invalid.
	 */
	protected static function validateFormula(array $correlations): void {
		$condition_formula_parser = new CConditionFormula();

		foreach ($correlations as $i => $correlation) {
			if (!array_key_exists('filter', $correlation)) {
				continue;
			}

			if ($correlation['filter']['evaltype'] == CONDITION_EVAL_TYPE_EXPRESSION) {
				$condition_formula_parser->parse($correlation['filter']['formula']);

				$constants = array_column($condition_formula_parser->constants, 'value', 'value');
				$path = '/'.($i + 1).'/filter';

				if (count($correlation['filter']['conditions']) != count($constants)) {
					self::exception(ZBX_API_ERROR_PARAMETERS,
						_s('Invalid parameter "%1$s": %2$s.', $path.'/conditions', _('incorrect number of conditions'))
					);
				}

				foreach ($correlation['filter']['conditions'] as $j => $condition) {
					if (!array_key_exists($condition['formulaid'], $constants)) {
						self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
							$path.'/conditions/'.($j + 1).'/formulaid', _('an identifier is not defined in the formula')
						));
					}
				}
			}
		}
	}

	/**
	 * Apply query output options.
	 *
	 * @param string $table_name
	 * @param string $table_alias
	 * @param array $options
	 * @param array $sql_parts
	 *
	 * @return array
	 */
	protected function applyQueryOutputOptions($table_name, $table_alias, array $options, array $sql_parts) {
		$sql_parts = parent::applyQueryOutputOptions($table_name, $table_alias, $options, $sql_parts);

		if (!$options['countOutput']) {
			// Add filter fields.
			if ($this->outputIsRequested('formula', $options['selectFilter'])
					|| $this->outputIsRequested('eval_formula', $options['selectFilter'])
					|| $this->outputIsRequested('conditions', $options['selectFilter'])) {

				$sql_parts = $this->addQuerySelect('c.formula', $sql_parts);
				$sql_parts = $this->addQuerySelect('c.evaltype', $sql_parts);
			}

			if ($this->outputIsRequested('evaltype', $options['selectFilter'])) {
				$sql_parts = $this->addQuerySelect('c.evaltype', $sql_parts);
			}
		}

		return $sql_parts;
	}

	/**
	 * Extend result with requested objects.
	 *
	 * @param array $options
	 * @param array $result
	 *
	 * @return array
	 */
	protected function addRelatedObjects(array $options, array $result): array {
		$result = parent::addRelatedObjects($options, $result);

		$correlationids = array_keys($result);

		// Adding formulas and conditions.
		if ($options['selectFilter'] !== null) {
			$formula_requested = $this->outputIsRequested('formula', $options['selectFilter']);
			$eval_formula_requested = $this->outputIsRequested('eval_formula', $options['selectFilter']);
			$conditions_requested = $this->outputIsRequested('conditions', $options['selectFilter']);

			$filters = [];

			if ($options['selectFilter']) {
				foreach ($result as $correlation) {
					$filters[$correlation['correlationid']] = [
						'evaltype' => $correlation['evaltype'],
						'formula' => array_key_exists('formula', $correlation) ? $correlation['formula'] : '',
						'conditions' => []
					];
				}

				if ($formula_requested || $eval_formula_requested || $conditions_requested) {
					$sql = 'SELECT c.correlationid,c.corr_conditionid,c.type,ct.tag AS ct_tag,'.
								'cg.operator AS cg_operator,cg.groupid,ctp.oldtag,ctp.newtag,ctv.tag AS ctv_tag,'.
								'ctv.operator AS ctv_operator,ctv.value'.
							' FROM corr_condition c'.
							' LEFT JOIN corr_condition_tag ct ON ct.corr_conditionid = c.corr_conditionid'.
							' LEFT JOIN corr_condition_group cg ON cg.corr_conditionid = c.corr_conditionid'.
							' LEFT JOIN corr_condition_tagpair ctp ON ctp.corr_conditionid = c.corr_conditionid'.
							' LEFT JOIN corr_condition_tagvalue ctv ON ctv.corr_conditionid = c.corr_conditionid'.
							' WHERE '.dbConditionInt('c.correlationid', $correlationids);

					$db_corr_conditions = DBselect($sql);

					while ($row = DBfetch($db_corr_conditions)) {
						$fields = [
							'corr_conditionid' => $row['corr_conditionid'],
							'correlationid' => $row['correlationid'],
							'type' => $row['type']
						];

						switch ($row['type']) {
							case ZBX_CORR_CONDITION_OLD_EVENT_TAG:
							case ZBX_CORR_CONDITION_NEW_EVENT_TAG:
								$fields['tag'] = $row['ct_tag'];
								break;

							case ZBX_CORR_CONDITION_NEW_EVENT_HOSTGROUP:
								$fields['operator'] = $row['cg_operator'];
								$fields['groupid'] = $row['groupid'];
								break;

							case ZBX_CORR_CONDITION_EVENT_TAG_PAIR:
								$fields['oldtag'] = $row['oldtag'];
								$fields['newtag'] = $row['newtag'];
								break;

							case ZBX_CORR_CONDITION_OLD_EVENT_TAG_VALUE:
							case ZBX_CORR_CONDITION_NEW_EVENT_TAG_VALUE:
								$fields['tag'] = $row['ctv_tag'];
								$fields['operator'] = $row['ctv_operator'];
								$fields['value'] = $row['value'];
								break;
						}

						$filters[$row['correlationid']]['conditions'][] = $fields;
					}

					foreach ($filters as &$filter) {
						// In case of a custom expression, use the given formula.
						if ($filter['evaltype'] == CONDITION_EVAL_TYPE_EXPRESSION) {
							$formula = $filter['formula'];
						}
						// In other cases generate the formula automatically.
						else {
							$conditions = $filter['conditions'];
							CArrayHelper::sort($conditions, ['type']);
							$conditions_for_formula = [];

							foreach ($conditions as $condition) {
								$conditions_for_formula[$condition['corr_conditionid']] = $condition['type'];
							}

							$formula = CConditionHelper::getFormula($conditions_for_formula, $filter['evaltype']);
						}

						// Generate formulaids from the effective formula.
						$formulaids = CConditionHelper::getFormulaIds($formula);

						foreach ($filter['conditions'] as &$condition) {
							$condition['formulaid'] = $formulaids[$condition['corr_conditionid']];
						}
						unset($condition);

						// Generated a letter based formula only for actions with custom expressions.
						if ($formula_requested && $filter['evaltype'] == CONDITION_EVAL_TYPE_EXPRESSION) {
							$filter['formula'] = CConditionHelper::replaceNumericIds($formula, $formulaids);
						}

						if ($eval_formula_requested) {
							$filter['eval_formula'] = CConditionHelper::replaceNumericIds($formula, $formulaids);
						}
					}
					unset($filter);
				}
			}
			else {
				// In case no fields are actually selected in "filter", return empty array.
				foreach ($result as $correlation) {
					$filters[$correlation['correlationid']] = [];
				}
			}

			// Add filters to the result.
			foreach ($result as &$correlation) {
				$correlation['filter'] = $filters[$correlation['correlationid']];
			}
			unset($correlation);
		}

		// Adding operations.
		if ($options['selectOperations'] !== null && $options['selectOperations'] != API_OUTPUT_COUNT) {
			$operations = API::getApiService()->select('corr_operation', [
				'output' => $this->outputExtend($options['selectOperations'], [
					'correlationid', 'corr_operationid', 'type'
				]),
				'filter' => ['correlationid' => $correlationids],
				'preservekeys' => true
			]);
			$relation_map = $this->createRelationMap($operations, 'correlationid', 'corr_operationid');

			foreach ($operations as &$operation) {
				unset($operation['correlationid'], $operation['corr_operationid']);
			}
			unset($operation);

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

		return $result;
	}

	/**
	 * @static
	 *
	 * @param array $correlations
	 * @param array $db_correlations
	 */
	private static function addAffectedObjects(array $correlations, array &$db_correlations): void {
		$correlationids = ['filter' => [], 'operations' => []];

		foreach ($correlations as $correlation) {
			if (array_key_exists('filter', $correlation)) {
				$correlationids['filter'][] = $correlation['correlationid'];
				$db_correlations[$correlation['correlationid']]['filter']['conditions'] = [];
			}

			if (array_key_exists('operations', $correlation)) {
				$correlationids['operations'][] = $correlation['correlationid'];
				$db_correlations[$correlation['correlationid']]['operations'] = [];
			}
		}

		if ($correlationids['filter']) {
			$options = [
				'output' => ['correlationid', 'evaltype', 'formula'],
				'filter' => ['correlationid' => $correlationids['filter']]
			];
			$db_filters = DBselect(DB::makeSql('correlation', $options));

			while ($db_filter = DBfetch($db_filters)) {
				$db_correlations[$db_filter['correlationid']]['filter'] =
					array_diff_key($db_filter, array_flip(['correlationid']));
			}

			$sql = 'SELECT c.correlationid,c.corr_conditionid,c.type,ct.tag AS ct_tag,'.
					'cg.operator AS cg_operator,cg.groupid,ctp.oldtag,ctp.newtag,ctv.tag AS ctv_tag,'.
					'ctv.operator AS ctv_operator,ctv.value'.
				' FROM corr_condition c'.
				' LEFT JOIN corr_condition_tag ct ON ct.corr_conditionid=c.corr_conditionid'.
				' LEFT JOIN corr_condition_group cg ON cg.corr_conditionid=c.corr_conditionid'.
				' LEFT JOIN corr_condition_tagpair ctp ON ctp.corr_conditionid=c.corr_conditionid'.
				' LEFT JOIN corr_condition_tagvalue ctv ON ctv.corr_conditionid=c.corr_conditionid'.
				' WHERE '.dbConditionId('c.correlationid', $correlationids['filter']);
			$db_conditions = DBselect($sql);

			while ($db_condition = DBfetch($db_conditions)) {
				$condition = [
					'corr_conditionid' => $db_condition['corr_conditionid'],
					'type' => $db_condition['type']
				];

				switch ($db_condition['type']) {
					case ZBX_CORR_CONDITION_OLD_EVENT_TAG:
					case ZBX_CORR_CONDITION_NEW_EVENT_TAG:
						$condition['tag'] = $db_condition['ct_tag'];
						$condition['operator'] = CONDITION_OPERATOR_EQUAL;
						break;

					case ZBX_CORR_CONDITION_NEW_EVENT_HOSTGROUP:
						$condition['groupid'] = $db_condition['groupid'];
						$condition['operator'] = $db_condition['cg_operator'];
						break;

					case ZBX_CORR_CONDITION_EVENT_TAG_PAIR:
						$condition['oldtag'] = $db_condition['oldtag'];
						$condition['newtag'] = $db_condition['newtag'];
						$condition['operator'] = CONDITION_OPERATOR_EQUAL;
						break;

					case ZBX_CORR_CONDITION_OLD_EVENT_TAG_VALUE:
					case ZBX_CORR_CONDITION_NEW_EVENT_TAG_VALUE:
						$condition['tag'] = $db_condition['ctv_tag'];
						$condition['value'] = $db_condition['value'];
						$condition['operator'] = $db_condition['ctv_operator'];
						break;
				}

				$db_correlations[$db_condition['correlationid']]['filter']['conditions']
					[$db_condition['corr_conditionid']] = $condition;
			}

			foreach ($db_correlations as $correlationid => &$db_correlation) {
				if ($db_correlation['filter']['evaltype'] == CONDITION_EVAL_TYPE_EXPRESSION) {
					$formula = $db_correlation['filter']['formula'];

					$formulaids = CConditionHelper::getFormulaIds($formula);

					foreach ($db_correlation['filter']['conditions'] as &$db_condition) {
						$db_condition['formulaid'] = $formulaids[$db_condition['corr_conditionid']];
					}
					unset($db_condition);
				}
			}
			unset($db_correlation);
		}

		if ($correlationids['operations']) {
			$options = [
				'output' => ['corr_operationid', 'correlationid', 'type'],
				'filter' => ['correlationid' => $correlationids['operations']]
			];
			$db_operations = DBselect(DB::makeSql('corr_operation', $options));

			while ($db_operation = DBfetch($db_operations)) {
				$db_correlations[$db_operation['correlationid']]['operations'][$db_operation['corr_operationid']] =
					array_diff_key($db_operation, array_flip(['correlationid']));
			}
		}
	}
}
