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


class CUserDirectory extends CApiService {

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

	protected $tableName = 'userdirectory';
	protected $tableAlias = 'ud';
	protected $sortColumns = ['name'];

	public const COMMON_OUTPUT_FIELDS = ['userdirectoryid', 'name', 'idp_type', 'provision_status', 'description'];
	public const LDAP_OUTPUT_FIELDS = [
		'host', 'port', 'base_dn', 'search_attribute', 'bind_dn', 'start_tls', 'search_filter', 'group_basedn',
		'group_name', 'group_member', 'group_filter', 'group_membership', 'user_username', 'user_lastname',
		'user_ref_attr'
	];
	public const SAML_OUTPUT_FIELDS = [
		'idp_entityid', 'sso_url', 'slo_url', 'username_attribute', 'sp_entityid', 'nameid_format', 'sign_messages',
		'sign_assertions', 'sign_authn_requests', 'sign_logout_requests', 'sign_logout_responses', 'encrypt_nameid',
		'encrypt_assertions', 'group_name', 'user_username', 'user_lastname', 'scim_status'
	];

	public const OUTPUT_FIELDS = [
		// Common output fields.
		'userdirectoryid', 'name', 'idp_type', 'provision_status', 'description',

		// LDAP and SAML main fields.
		'group_name', 'user_username', 'user_lastname',

		// LDAP output fields.
		'host', 'port', 'base_dn', 'search_attribute', 'bind_dn', 'start_tls', 'search_filter', 'group_basedn',
		'group_member', 'group_filter', 'group_membership', 'user_ref_attr',

		// SAML output fields.
		'idp_entityid', 'sso_url', 'slo_url', 'username_attribute', 'sp_entityid', 'nameid_format', 'sign_messages',
		'sign_assertions', 'sign_authn_requests', 'sign_logout_requests', 'sign_logout_responses', 'encrypt_nameid',
		'encrypt_assertions', 'scim_status'
	];

	public const MEDIA_OUTPUT_FIELDS = [
		'userdirectory_mediaid', 'mediatypeid', 'name', 'attribute', 'active', 'severity', 'period'
	];

	/**
	 * @param array $options
	 *
	 * @throws APIException
	 *
	 * @return array|string
	 */
	public function get(array $options) {
		$this->validateGet($options);

		if (!$options['countOutput']) {
			if ($options['output'] === API_OUTPUT_EXTEND) {
				$options['output'] = self::OUTPUT_FIELDS;
			}

			$request_output = $options['output'];
			$db_userdirectories_by_type = [IDP_TYPE_LDAP => [], IDP_TYPE_SAML => []];
			$db_userdirectories = [];

			$options['output'] =
				array_merge(['idp_type'], array_intersect($request_output, self::COMMON_OUTPUT_FIELDS));

			$ldap_output = array_intersect($request_output, self::LDAP_OUTPUT_FIELDS);
			$saml_output = array_intersect($request_output, self::SAML_OUTPUT_FIELDS);
		}


		$result = DBselect($this->createSelectQuery($this->tableName, $options), $options['limit']);
		while ($row = DBfetch($result)) {
			if ($options['countOutput']) {
				return $row['rowscount'];
			}

			$db_userdirectories[$row['userdirectoryid']] = $row;
			$db_userdirectories_by_type[$row['idp_type']][] = $row['userdirectoryid'];
		}

		if ($db_userdirectories_by_type[IDP_TYPE_LDAP] && $ldap_output) {
			$sql_parts = [
				'select' => array_merge(['userdirectoryid'], $ldap_output),
				'from' => ['userdirectory_ldap'],
				'where' => [dbConditionInt('userdirectoryid', $db_userdirectories_by_type[IDP_TYPE_LDAP])]
			];

			$result = DBselect($this->createSelectQueryFromParts($sql_parts));
			while ($row = DBfetch($result)) {
				$db_userdirectories[$row['userdirectoryid']] += $row;
			}
		}

		if ($db_userdirectories_by_type[IDP_TYPE_SAML] && $saml_output) {
			$sql_parts = [
				'select' => array_merge(['userdirectoryid'], $saml_output),
				'from' => ['userdirectory_saml'],
				'where' => [dbConditionInt('userdirectoryid', $db_userdirectories_by_type[IDP_TYPE_SAML])]
			];

			$result = DBselect($this->createSelectQueryFromParts($sql_parts));
			while ($row = DBfetch($result)) {
				$db_userdirectories[$row['userdirectoryid']] += $row;
			}
		}

		if ($db_userdirectories) {
			$db_userdirectories = $this->addRelatedObjects($options, $db_userdirectories);
			$db_userdirectories = $this->unsetExtraFields($db_userdirectories, ['userdirectoryid', 'idp_type'],
				$request_output
			);

			if (!$options['preservekeys']) {
				$db_userdirectories = array_values($db_userdirectories);
			}
		}

		return $db_userdirectories;
	}

	protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sql_parts) {
		$sql_parts = parent::applyQueryOutputOptions($tableName, $tableAlias, $options, $sql_parts);

		$selected_ldap_fields = [];
		foreach (self::LDAP_OUTPUT_FIELDS as $field) {
			if ($this->outputIsRequested($field, $options['output'])) {
				$selected_ldap_fields[] = 'ldap.'.$field;
			}
		}
		if ($selected_ldap_fields) {
			$sql_parts['left_join'][] = [
				'alias' => 'ldap',
				'table' => 'userdirectory_ldap',
				'using' => 'userdirectoryid'
			];
			$sql_parts['left_table'] = ['alias' => $tableAlias, 'table' => $tableName];

			if (!$options['countOutput']) {
				$sql_parts['select'] = array_merge($sql_parts['select'], $selected_ldap_fields);
			}
		}

		$selected_saml_fields = [];
		foreach (self::SAML_OUTPUT_FIELDS as $field) {
			if ($this->outputIsRequested($field, $options['output'])) {
				$selected_saml_fields[] = 'saml.'.$field;
			}
		}
		if ($selected_saml_fields) {
			$sql_parts['left_join'][] = [
				'alias' => 'saml',
				'table' => 'userdirectory_saml',
				'using' => 'userdirectoryid'
			];
			$sql_parts['left_table'] = ['alias' => $tableAlias, 'table' => $tableName];

			if (!$options['countOutput']) {
				$sql_parts['select'] = array_merge($sql_parts['select'], $selected_saml_fields);
			}
		}

		return $sql_parts;
	}

	private function validateGet(array &$options): void {
		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
			// filter
			'userdirectoryids' =>			['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
			'filter' =>						['type' => API_FILTER, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => ['userdirectoryid', 'provision_status', 'idp_type']],
			'search' =>						['type' => API_FILTER, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => ['name', 'description']],
			'searchByAny' =>				['type' => API_BOOLEAN, 'default' => false],
			'startSearch' =>				['type' => API_FLAG, 'default' => false],
			'excludeSearch' =>				['type' => API_FLAG, 'default' => false],
			'searchWildcardsEnabled' =>		['type' => API_BOOLEAN, 'default' => false],
			// output
			'output' =>						['type' => API_OUTPUT, 'in' => implode(',', self::OUTPUT_FIELDS), 'default' => API_OUTPUT_EXTEND],
			'countOutput' =>				['type' => API_FLAG, 'default' => false],
			'selectUsrgrps' =>				['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL | API_ALLOW_COUNT, 'in' => implode(',', CUserGroup::OUTPUT_FIELDS), 'default' => null],
			'selectProvisionMedia' =>		['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', self::MEDIA_OUTPUT_FIELDS), 'default' => null],
			'selectProvisionGroups' =>		['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', ['name', 'roleid', 'user_groups']), 'default' => null],
			// sort and limit
			'sortfield' =>					['type' => API_STRINGS_UTF8, 'flags' => API_NORMALIZE, 'in' => implode(',', ['name']), 'uniq' => true, 'default' => []],
			'sortorder' =>					['type' => API_SORTORDER, 'default' => []],
			'limit' =>						['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'in' => '1:'.ZBX_MAX_INT32, 'default' => null],
			// flags
			'preservekeys' =>				['type' => API_BOOLEAN, 'default' => false]
		]];

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

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

		self::addRelatedUserGroups($options, $result);
		self::addRelatedProvisionMedia($options, $result);
		self::addRelatedProvisionGroups($options, $result);

		return $result;
	}

	/**
	 * @param array $options
	 * @param array $result
	 */
	private static function addRelatedUserGroups(array $options, array &$result): void {
		if ($options['selectUsrgrps'] === null) {
			return;
		}

		foreach ($result as &$row) {
			$row['usrgrps'] = [];
		}
		unset($row);

		if ($options['selectUsrgrps'] === API_OUTPUT_COUNT) {
			$output = ['userdirectoryid'];
		}
		elseif ($options['selectUsrgrps'] === API_OUTPUT_EXTEND) {
			$output = ['usrgrpid', 'name', 'gui_access', 'users_status', 'debug_mode', 'userdirectoryid', 'mfa_status',
				'mfaid'
			];
		}
		else {
			$output = array_unique(array_merge(['userdirectoryid'], $options['selectUsrgrps']));
		}

		$db_usergroups = API::UserGroup()->get([
			'output' => $output,
			'filter' => ['userdirectoryid' => array_keys($result)]
		]);

		foreach ($db_usergroups as $db_usergroup) {
			$result[$db_usergroup['userdirectoryid']]['usrgrps'][] =
				array_diff_key($db_usergroup, array_flip(['userdirectoryid']));
		}

		if ($options['selectUsrgrps'] === API_OUTPUT_COUNT) {
			foreach ($result as &$row) {
				$row['usrgrps'] = (string) count($row['usrgrps']);
			}
			unset($row);
		}
	}

	/**
	 * Add provision media objects.
	 *
	 * @param array $options
	 * @param array $result
	 */
	private static function addRelatedProvisionMedia(array $options, array &$result): void {
		if ($options['selectProvisionMedia'] === null) {
			return;
		}

		foreach ($result as &$row) {
			$row['provision_media'] = [];
		}
		unset($row);

		if ($options['selectProvisionMedia'] === API_OUTPUT_EXTEND) {
			$options['selectProvisionMedia'] = self::MEDIA_OUTPUT_FIELDS;
		}

		$db_provisioning_media = DB::select('userdirectory_media', [
			'output' => array_merge($options['selectProvisionMedia'], ['userdirectoryid']),
			'filter' => [
				'userdirectoryid' => array_keys($result)
			]
		]);
		$requested_output = array_flip($options['selectProvisionMedia']);

		foreach ($db_provisioning_media as $db_provisioning_media) {
			$result[$db_provisioning_media['userdirectoryid']]['provision_media'][]
				= array_intersect_key($db_provisioning_media, $requested_output);
		}
	}

	/**
	 * Add provision user group objects.
	 *
	 * @param array $options
	 * @param array $result
	 */
	private static function addRelatedProvisionGroups(array $options, array &$result): void {
		if ($options['selectProvisionGroups'] === null) {
			return;
		}

		foreach ($result as &$row) {
			$row['provision_groups'] = [];
		}
		unset($row);

		if ($options['selectProvisionGroups'] === API_OUTPUT_EXTEND) {
			$options['selectProvisionGroups'] = ['name', 'roleid', 'user_groups'];
		}

		$user_groups_index = array_search('user_groups', $options['selectProvisionGroups']);
		if ($user_groups_index !== false) {
			unset($options['selectProvisionGroups'][$user_groups_index]);
		}

		$db_provision_idpgroups = DB::select('userdirectory_idpgroup', [
			'output' => array_merge($options['selectProvisionGroups'],
				['userdirectoryid', 'userdirectory_idpgroupid']
			),
			'filter' => [
				'userdirectoryid' => array_keys($result)
			],
			'preservekeys' => true
		]);
		$provision_usergroups = [];

		if ($user_groups_index !== false && $db_provision_idpgroups) {
			$db_provision_usergroups = DB::select('userdirectory_usrgrp', [
				'output' => ['usrgrpid', 'userdirectory_idpgroupid'],
				'filter' => [
					'userdirectory_idpgroupid' => array_keys($db_provision_idpgroups)
				]
			]);

			foreach ($db_provision_usergroups as $usrgrp) {
				$provision_usergroups[$usrgrp['userdirectory_idpgroupid']][] = [
					'usrgrpid' => $usrgrp['usrgrpid']
				];
			}
		}

		foreach ($db_provision_idpgroups as $provision_idpgroupid => $db_provision_idpgroup) {
			$idpgroup = array_intersect_key($db_provision_idpgroup, array_flip($options['selectProvisionGroups']));

			if ($user_groups_index !== false && array_key_exists($provision_idpgroupid, $provision_usergroups)) {
				$idpgroup['user_groups'] = $provision_usergroups[$provision_idpgroupid];
			}

			$result[$db_provision_idpgroup['userdirectoryid']]['provision_groups'][] = $idpgroup;
		}
	}

	/**
	 * @param array $userdirectories
	 *
	 * @return array
	 */
	public function create(array $userdirectories): array {
		self::validateCreate($userdirectories);

		$userdirectoryids = DB::insert('userdirectory', $userdirectories);

		$ins_userdirectories_ldap = [];
		$ins_userdirectories_saml = [];
		$ldap_userdirectoryids = [];

		foreach ($userdirectories as $i => &$userdirectory) {
			$userdirectory['userdirectoryid'] = $userdirectoryids[$i];

			if ($userdirectory['idp_type'] == IDP_TYPE_LDAP) {
				$ins_userdirectories_ldap[] = array_intersect_key($userdirectory,
					array_flip(self::LDAP_OUTPUT_FIELDS) + array_flip(['userdirectoryid', 'bind_password'])
				);

				$ldap_userdirectoryids[] = $userdirectory['userdirectoryid'];
			}

			if ($userdirectory['idp_type'] == IDP_TYPE_SAML) {
				$ins_userdirectories_saml[] = array_intersect_key($userdirectory,
					array_flip(self::SAML_OUTPUT_FIELDS) + array_flip(['userdirectoryid'])
				);
			}
		}
		unset($userdirectory);

		if ($ins_userdirectories_ldap) {
			DB::insert('userdirectory_ldap', $ins_userdirectories_ldap, false);
		}

		if ($ins_userdirectories_saml) {
			DB::insert('userdirectory_saml', $ins_userdirectories_saml, false);
		}

		self::updateProvisionGroups($userdirectories);
		self::updateProvisionMedia($userdirectories);

		self::addAuditLog(CAudit::ACTION_ADD, CAudit::RESOURCE_USERDIRECTORY, $userdirectories);

		if ($ldap_userdirectoryids) {
			self::setDefaultUserdirectory($ldap_userdirectoryids);
		}

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

	/**
	 * @param array $userdirectories
	 *
	 * @throws APIException
	 */
	private static function validateCreate(array &$userdirectories): void {
		$api_input_rules = self::getValidationRules();

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

		self::checkDuplicates($userdirectories);
		self::checkProvisionGroups($userdirectories);
		self::checkMediaTypes($userdirectories);
		self::checkSamlExists($userdirectories);
	}

	/**
	 * Validate if only one user directory of type IDP_TYPE_SAML exists.
	 *
	 * @throws APIException
	 */
	private static function checkSamlExists(array $userdirectories): void {
		$idps = array_column($userdirectories, 'idp_type');
		$idps_count = count(array_keys($idps, IDP_TYPE_SAML));

		if ($idps_count == 0) {
			return;
		}

		if ($idps_count == 1) {
			$idps_count += DB::select('userdirectory', [
				'countOutput' => true,
				'filter' => [
					'idp_type' => IDP_TYPE_SAML
				]
			]);
		}

		if ($idps_count > 1) {
			self::exception(ZBX_API_ERROR_PARAMETERS,
				_s('Only one user directory of type "%1$s" can exist.', IDP_TYPE_SAML)
			);
		}
	}

	private static function setDefaultUserdirectory(array $ldap_userdirectoryids): void {
		if (!self::checkOtherLdapUserdirectoryExists($ldap_userdirectoryids)) {
			API::Authentication()->update(['ldap_userdirectoryid' => reset($ldap_userdirectoryids)]);
		}
	}

	private static function checkOtherLdapUserdirectoryExists(array $userdirectoryids): bool {
		return (bool) DBfetch(DBselect(
			'SELECT u.userdirectoryid'.
			' FROM userdirectory u'.
			' WHERE '.dbConditionId('u.userdirectoryid', $userdirectoryids, true).
				' AND '.dbConditionInt('u.idp_type', [IDP_TYPE_LDAP]),
			1
		));
	}

	/**
	 * @param array $userdirectories
	 *
	 * @return array
	 */
	public function update(array $userdirectories): array {
		$this->validateUpdate($userdirectories, $db_userdirectories);

		self::addFieldDefaultsByType($userdirectories, $db_userdirectories);

		$upd_userdirectories = [];
		$upd_userdirectories_ldap = [];
		$upd_userdirectories_saml = [];

		foreach ($userdirectories as $userdirectory) {
			$db_userdirectory = $db_userdirectories[$userdirectory['userdirectoryid']];

			$upd_userdirectory = DB::getUpdatedValues('userdirectory',
				array_intersect_key($userdirectory, array_flip(self::COMMON_OUTPUT_FIELDS)), $db_userdirectory
			);

			if ($upd_userdirectory) {
				$upd_userdirectories[] = [
					'values' => $upd_userdirectory,
					'where' => ['userdirectoryid' => $userdirectory['userdirectoryid']]
				];
			}

			if ($userdirectory['idp_type'] == IDP_TYPE_LDAP) {
				$upd_userdirectory_ldap = DB::getUpdatedValues('userdirectory_ldap',
					array_intersect_key($userdirectory, array_flip(self::LDAP_OUTPUT_FIELDS) + ['bind_password' => '']),
					$db_userdirectory
				);

				if ($upd_userdirectory_ldap) {
					$upd_userdirectories_ldap[] = [
						'values' => $upd_userdirectory_ldap,
						'where' => ['userdirectoryid' => $userdirectory['userdirectoryid']]
					];
				}
			}

			if ($userdirectory['idp_type'] == IDP_TYPE_SAML) {
				$upd_userdirectory_saml = DB::getUpdatedValues('userdirectory_saml',
					array_intersect_key($userdirectory, array_flip(self::SAML_OUTPUT_FIELDS)), $db_userdirectory
				);

				if ($upd_userdirectory_saml) {
					$upd_userdirectories_saml[] = [
						'values' => $upd_userdirectory_saml,
						'where' => ['userdirectoryid' => $userdirectory['userdirectoryid']]
					];
				}
			}
		}

		if ($upd_userdirectories) {
			DB::update('userdirectory', $upd_userdirectories);
		}

		if ($upd_userdirectories_ldap) {
			DB::update('userdirectory_ldap', $upd_userdirectories_ldap);
		}

		if ($upd_userdirectories_saml) {
			DB::update('userdirectory_saml', $upd_userdirectories_saml);
		}

		self::updateProvisionMedia($userdirectories, $db_userdirectories);
		self::updateProvisionGroups($userdirectories, $db_userdirectories);

		self::addAuditLog(CAudit::ACTION_UPDATE, CAudit::RESOURCE_USERDIRECTORY, $userdirectories,
			$db_userdirectories
		);

		return ['userdirectoryids' => array_column($userdirectories, 'userdirectoryid')];
	}

	/**
	 * @param array      $userdirectories
	 * @param array|null $db_userdirectories
	 *
	 * @throws APIException
	 */
	private function validateUpdate(array &$userdirectories, ?array &$db_userdirectories): void {
		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE | API_ALLOW_UNEXPECTED, 'uniq' => [['userdirectoryid']], 'fields' => [
			'userdirectoryid' =>	['type' => API_ID, 'flags' => API_REQUIRED],
			'idp_type' =>			['type' => API_INT32, 'in' => implode(',', [IDP_TYPE_LDAP, IDP_TYPE_SAML])],
			'provision_status' =>	['type' => API_INT32, 'in' => implode(',', [JIT_PROVISIONING_DISABLED, JIT_PROVISIONING_ENABLED])]

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

		$db_userdirectories = $this->get([
			'output' => self::OUTPUT_FIELDS,
			'userdirectoryids' => array_column($userdirectories, 'userdirectoryid'),
			'preservekeys' => true
		]);

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

		foreach ($userdirectories as $i => &$userdirectory) {
			$db_userdirectory = $db_userdirectories[$userdirectory['userdirectoryid']];
			$userdirectory += [
				'idp_type' => $db_userdirectory['idp_type'],
				'provision_status' => $db_userdirectory['provision_status']
			];

			if ($userdirectory['idp_type'] != $db_userdirectory['idp_type']) {
				self::exception(ZBX_API_ERROR_PARAMETERS,
					_s('Invalid parameter "%1$s": %2$s.', '/'.($i + 1).'/idp_type', _('cannot be changed'))
				);
			}
		}
		unset($userdirectory);

		self::addRequiredFieldsByType($userdirectories, $db_userdirectories);

		$api_input_rules = self::getValidationRules(true);

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

		self::addAffectedObjects($userdirectories, $db_userdirectories);

		self::validateProvisionMedias($userdirectories, $db_userdirectories);

		self::checkDuplicates($userdirectories, $db_userdirectories);
		self::checkProvisionGroups($userdirectories, $db_userdirectories);
		self::checkMediaTypes($userdirectories, $db_userdirectories);
	}

	private static function addRequiredFieldsByType(array &$userdirectories, array $db_userdirectories): void {
		foreach ($userdirectories as &$userdirectory) {
			$db_userdirectory = $db_userdirectories[$userdirectory['userdirectoryid']];

			if ($userdirectory['provision_status'] != $db_userdirectory['provision_status']) {
				if ($userdirectory['provision_status'] == JIT_PROVISIONING_ENABLED) {
					$userdirectory += ['provision_groups' => []];
				}
			}
		}
		unset($userdirectory);
	}

	private static function addAffectedObjects(array $userdirectories, array &$db_userdirectories): void {
		self::addAffectedProvisionGroups($userdirectories, $db_userdirectories);
		self::addAffectedProvisionMedia($userdirectories, $db_userdirectories);
	}

	private static function addAffectedProvisionGroups(array $userdirectories, array &$db_userdirectories): void {
		$userdirectoryids = [];

		foreach ($userdirectories as $userdirectory) {
			$db_userdirectory = $db_userdirectories[$userdirectory['userdirectoryid']];

			if (array_key_exists('provision_groups', $userdirectory)
					|| ($userdirectory['provision_status'] != $db_userdirectory['provision_status']
						&& $db_userdirectory['provision_status'] == JIT_PROVISIONING_ENABLED)) {
				$userdirectoryids[] = $userdirectory['userdirectoryid'];
				$db_userdirectories[$userdirectory['userdirectoryid']]['provision_groups'] = [];
			}
		}

		if (!$userdirectoryids) {
			return;
		}

		$options = [
			'output' => ['userdirectory_idpgroupid', 'userdirectoryid', 'roleid', 'name'],
			'filter' => ['userdirectoryid' => $userdirectoryids]
		];
		$result = DBselect(DB::makeSql('userdirectory_idpgroup', $options));

		$db_provision_groups = [];

		while ($row = DBfetch($result)) {
			$db_userdirectories[$row['userdirectoryid']]['provision_groups'][$row['userdirectory_idpgroupid']] =
				array_diff_key($row, array_flip(['userdirectoryid']));

			$db_provision_groups[$row['userdirectory_idpgroupid']] =
				&$db_userdirectories[$row['userdirectoryid']]['provision_groups'][$row['userdirectory_idpgroupid']];
		}

		if (!$db_provision_groups) {
			return;
		}

		$options = [
			'output' => ['userdirectory_usrgrpid', 'userdirectory_idpgroupid', 'usrgrpid'],
			'filter' => ['userdirectory_idpgroupid' => array_keys($db_provision_groups)]
		];
		$result = DBselect(DB::makeSql('userdirectory_usrgrp', $options));

		while ($row = DBfetch($result)) {
			$db_provision_groups[$row['userdirectory_idpgroupid']]['user_groups'][$row['userdirectory_usrgrpid']] =
				array_diff_key($row, array_flip(['userdirectory_idpgroupid']));
		}
	}

	private static function addAffectedProvisionMedia(array $userdirectories, array &$db_userdirectories): void {
		$userdirectoryids = [];

		foreach ($userdirectories as $userdirectory) {
			$db_userdirectory = $db_userdirectories[$userdirectory['userdirectoryid']];

			if (array_key_exists('provision_media', $userdirectory)
					|| ($userdirectory['provision_status'] != $db_userdirectory['provision_status']
						&& $db_userdirectory['provision_status'] == JIT_PROVISIONING_ENABLED)) {
				$userdirectoryids[] = $userdirectory['userdirectoryid'];
				$db_userdirectories[$userdirectory['userdirectoryid']]['provision_media'] = [];
			}
		}

		if (!$userdirectoryids) {
			return;
		}

		$options = [
			'output' => array_merge(self::MEDIA_OUTPUT_FIELDS, ['userdirectoryid']),
			'filter' => ['userdirectoryid' => $userdirectoryids]
		];
		$result = DBselect(DB::makeSql('userdirectory_media', $options));

		while ($row = DBfetch($result)) {
			$db_userdirectories[$row['userdirectoryid']]['provision_media'][$row['userdirectory_mediaid']] =
				array_diff_key($row, array_flip(['userdirectoryid']));
		}
	}

	/**
	 * @return array
	 */
	private static function getProvisionMediaValidationFields(bool $is_update = false): array {
		$api_required = $is_update ? 0 : API_REQUIRED;

		$specific_rules = $is_update
			? [
				'userdirectory_mediaid' =>	['type' => API_ANY]
			]
			: [];

		return $specific_rules + [
			'name' =>			['type' => API_STRING_UTF8, 'flags' => $api_required | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory_media', 'name')],
			'mediatypeid' =>	['type' => API_ID, 'flags' => $api_required],
			'attribute' =>		['type' => API_STRING_UTF8, 'flags' => $api_required | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory_media', 'attribute')],
			'active' =>			['type' => API_INT32, 'in' => implode(',', [MEDIA_STATUS_ACTIVE, MEDIA_STATUS_DISABLED])],
			'severity' =>		['type' => API_INT32, 'in' => '0:63'],
			'period' =>			['type' => API_TIME_PERIOD, 'flags' => API_ALLOW_USER_MACRO, 'length' => DB::getFieldLength('userdirectory_media', 'period')]
		];
	}

	private static function validateProvisionMedias(array &$userdirectories, array $db_userdirectories): void {
		foreach ($userdirectories as $i1 => &$userdirectory) {
			if (!array_key_exists('provision_media', $userdirectory)) {
				return;
			}

			$path = '/'.($i1 + 1).'/provision_media';

			$db_provision_medias =  $db_userdirectories[$userdirectory['userdirectoryid']]['provision_media'];

			foreach ($userdirectory['provision_media'] as $i2 => &$provision_media) {
				$is_update = array_key_exists('userdirectory_mediaid', $provision_media);

				if ($is_update) {
					if (!array_key_exists($provision_media['userdirectory_mediaid'], $db_provision_medias)) {
						self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
							$path.'/'.($i2 + 1).'/userdirectory_mediaid',
							_('object does not exist or belongs to another object')
						));
					}

					$db_provision_media = $db_provision_medias[$provision_media['userdirectory_mediaid']];
					$provision_media += array_intersect_key($db_provision_media, array_flip(['mediatypeid', 'attribute']));
				}

				$api_input_rules = ['type' => API_OBJECT, 'fields' => self::getProvisionMediaValidationFields($is_update)];

				if (!CApiInputValidator::validate($api_input_rules, $provision_media, $path.'/'.($i2 + 1), $error)) {
					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
				}
			}
			unset($provision_media);

			$api_input_rules = ['type' => API_OBJECTS, 'uniq' => [['mediatypeid', 'attribute']], 'fields' => [
				'mediatypeid' =>	['type' => API_ANY],
				'attribute' =>		['type' => API_ANY]
			]];
			$data = $userdirectory['provision_media'];

			if (!CApiInputValidator::validateUniqueness($api_input_rules, $data, $path, $error)) {
				self::exception(ZBX_API_ERROR_PARAMETERS, $error);
			}
		}
		unset($userdirectory);
	}

	/**
	 * Check for unique names.
	 *
	 * @param array      $userdirectories
	 * @param array|null $db_userdirectories
	 *
	 * @throws APIException if userdirectory name is not unique.
	 */
	private static function checkDuplicates(array $userdirectories, array $db_userdirectories = null): void {
		$names = [];

		foreach ($userdirectories as $userdirectory) {
			if (!array_key_exists('name', $userdirectory)) {
				continue;
			}

			if ($db_userdirectories === null
					|| $userdirectory['name'] !== $db_userdirectories[$userdirectory['userdirectoryid']]['name']) {
				$names[] = $userdirectory['name'];
			}
		}

		if (!$names) {
			return;
		}

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

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

	private static function checkProvisionGroups(array $userdirectories, array $db_userdirectories = null): void {
		$role_indexes = [];
		$user_group_indexes = [];

		foreach ($userdirectories as $i1 => $userdirectory) {
			if ($userdirectory['provision_status'] != JIT_PROVISIONING_ENABLED
					|| !array_key_exists('provision_groups', $userdirectory)) {
				continue;
			}

			$db_provision_groups = $db_userdirectories !== null
				? array_column($db_userdirectories[$userdirectory['userdirectoryid']]['provision_groups'], null, 'name')
				: [];

			foreach ($userdirectory['provision_groups'] as $i2 => $provision_group) {
				$db_provision_group = array_key_exists($provision_group['name'], $db_provision_groups)
					? $db_provision_groups[$provision_group['name']]
					: null;

				if ($db_provision_group === null
						|| bccomp($provision_group['roleid'], $db_provision_group['roleid']) != 0) {
					$role_indexes[$provision_group['roleid']][$i1][] = $i2;
				}

				$db_usrgrpids = $db_provision_group !== null
					? array_column($db_provision_group['user_groups'], 'usrgrpid')
					: [];

				foreach ($provision_group['user_groups'] as $i3 => $user_group) {
					if (!in_array($user_group['usrgrpid'], $db_usrgrpids)) {
						$user_group_indexes[$user_group['usrgrpid']][$i1][$i2] = $i3;
					}
				}
			}
		}

		if ($role_indexes) {
			$db_roles = API::Role()->get([
				'output' => [],
				'roleids' => array_keys($role_indexes),
				'preservekeys' => true
			]);

			foreach ($role_indexes as $roleid => $indexes) {
				if (!array_key_exists($roleid, $db_roles)) {
					$i1 = key($indexes);
					$i2 = reset($indexes[$i1]);

					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
						'/'.($i1 + 1).'/provision_groups/'.($i2 + 1).'/roleid', _('object does not exist')
					));
				}
			}
		}

		if ($user_group_indexes) {
			$db_user_groups = API::UserGroup()->get([
				'output' => [],
				'usrgrpids' => array_keys($user_group_indexes),
				'preservekeys' => true
			]);

			foreach ($user_group_indexes as $usrgrpid => $indexes) {
				if (!array_key_exists($usrgrpid, $db_user_groups)) {
					$i1 = key($indexes);
					$i2 = key($indexes[$i1]);
					$i3 = $indexes[$i1][$i2];

					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
						'/'.($i1 + 1).'/provision_groups/'.($i2 + 1).'/user_groups/'.($i3 + 1),
						_('object does not exist')
					));
				}
			}
		}
	}

	private static function checkMediaTypes(array $userdirectories, array $db_userdirectories = null): void {
		$media_indexes = [];

		foreach ($userdirectories as $i1 => $userdirectory) {
			if ($userdirectory['provision_status'] != JIT_PROVISIONING_ENABLED
					|| !array_key_exists('provision_media', $userdirectory) || !$userdirectory['provision_media']) {
				continue;
			}

			$db_mediatypeids = $db_userdirectories !== null
				? array_column($db_userdirectories[$userdirectory['userdirectoryid']], 'mediatypeid', 'mediatypeid')
				: [];

			foreach ($userdirectory['provision_media'] as $i2 => $media) {
				if (!in_array($media['mediatypeid'], $db_mediatypeids)) {
					$media_indexes[$media['mediatypeid']][$i1][] = $i2;
				}
			}
		}

		if (!$media_indexes) {
			return;
		}

		$db_mediatypes = API::MediaType()->get([
			'output' => [],
			'mediatypeids' => array_keys($media_indexes),
			'preservekeys' => true
		]);

		foreach ($media_indexes as $mediatypeid => $indexes) {
			if (!array_key_exists($mediatypeid, $db_mediatypes)) {
				$i1 = key($indexes);
				$i2 = reset($indexes[$i1]);

				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
					'/'.($i1 + 1).'/provision_media/'.($i2 + 1).'/mediatypeid', _('object does not exist')
				));
			}
		}
	}

	private static function addFieldDefaultsByType(array &$userdirectories, array $db_userdirectories): void {
		foreach ($userdirectories as &$userdirectory) {
			$db_userdirectory = $db_userdirectories[$userdirectory['userdirectoryid']];

			if ($userdirectory['provision_status'] != $db_userdirectory['provision_status']
					&& $db_userdirectory['provision_status'] == JIT_PROVISIONING_ENABLED) {
				if ($userdirectory['idp_type'] == IDP_TYPE_LDAP) {
					$userdirectory += [
						'group_basedn' => DB::getDefault('userdirectory_ldap', 'group_basedn'),
						'user_ref_attr' => DB::getDefault('userdirectory_ldap', 'user_ref_attr'),
						'group_name' => DB::getDefault('userdirectory_ldap', 'group_name'),
						'user_username' => DB::getDefault('userdirectory_ldap', 'user_username'),
						'user_lastname' => DB::getDefault('userdirectory_ldap', 'user_lastname'),
						'group_member' => DB::getDefault('userdirectory_ldap', 'group_member'),
						'group_membership' => DB::getDefault('userdirectory_ldap', 'group_membership'),
						'provision_groups' => [],
						'provision_media' => []
					];
				}

				if ($userdirectory['idp_type'] == IDP_TYPE_SAML) {
					$userdirectory += [
						'group_name' => DB::getDefault('userdirectory_saml', 'group_name'),
						'user_username' => DB::getDefault('userdirectory_saml', 'user_username'),
						'user_lastname' => DB::getDefault('userdirectory_saml', 'user_lastname'),
						'provision_groups' => [],
						'provision_media' => []
					];
				}
			}
		}
		unset($userdirectory);
	}

	/**
	 * @param array $userdirectoryids
	 *
	 * @throws APIException
	 *
	 * @return array
	 */
	public function delete(array $userdirectoryids): array {
		self::validateDelete($userdirectoryids, $db_userdirectories);

		DB::update('users', [[
			'values' => ['userdirectoryid' => 0],
			'where' => ['userdirectoryid' => $userdirectoryids]
		]]);

		self::deleteAffectedProvisionGroups($userdirectoryids);

		DB::delete('userdirectory_media', ['userdirectoryid' => $userdirectoryids]);
		DB::delete('userdirectory_ldap', ['userdirectoryid' => $userdirectoryids]);
		DB::delete('userdirectory_saml', ['userdirectoryid' => $userdirectoryids]);
		DB::delete('userdirectory', ['userdirectoryid' => $userdirectoryids]);

		self::addAuditLog(CAudit::ACTION_DELETE, CAudit::RESOURCE_USERDIRECTORY, $db_userdirectories);

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

	/**
	 * @param array      $userdirectoryids
	 * @param array|null $db_userdirectories
	 *
	 * @throws APIException
	 */
	private static function validateDelete(array $userdirectoryids, ?array &$db_userdirectories): void {
		$api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];

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

		$db_userdirectories = API::UserDirectory()->get([
			'output' => ['userdirectoryid', 'idp_type', 'name'],
			'userdirectoryids' => $userdirectoryids,
			'preservekeys' => true
		]);

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

		$userdirectoryid_idptype = array_column($db_userdirectories, 'idp_type', 'userdirectoryid');
		$auth = API::Authentication()->get([
			'output' => ['ldap_userdirectoryid', 'authentication_type', 'ldap_auth_enabled', 'saml_auth_enabled']
		]);

		if ($auth['saml_auth_enabled'] == ZBX_AUTH_SAML_ENABLED && in_array(IDP_TYPE_SAML, $userdirectoryid_idptype)) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot delete default user directory.'));
		}

		$ldap_userdirectories_delete = array_keys($userdirectoryid_idptype, IDP_TYPE_LDAP);
		$ldap_userdirectories_left = API::UserDirectory()->get([
			'countOutput' => true,
			'filter' => ['idp_type' => IDP_TYPE_LDAP]
		]);
		$ldap_userdirectories_left -= count($ldap_userdirectories_delete);

		// Default LDAP server cannot be removed if there are remaining LDAP servers.
		if (in_array($auth['ldap_userdirectoryid'], $userdirectoryids)
				&& ($auth['ldap_auth_enabled'] == ZBX_AUTH_LDAP_ENABLED || $ldap_userdirectories_left > 0)) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot delete default user directory.'));
		}

		// Cannot remove the last remaining LDAP server if LDAP authentication is on.
		if ($auth['ldap_auth_enabled'] == ZBX_AUTH_LDAP_ENABLED && $ldap_userdirectories_left == 0) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot delete default user directory.'));
		}

		$db_groups = API::UserGroup()->get([
			'output' => ['userdirectoryid'],
			'filter' => [
				'gui_access' => [GROUP_GUI_ACCESS_LDAP, GROUP_GUI_ACCESS_SYSTEM],
				'userdirectoryid' => $userdirectoryids
			],
			'limit' => 1
		]);

		if ($db_groups) {
			$db_group = reset($db_groups);

			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot delete user directory "%1$s".',
				$db_userdirectories[$db_group['userdirectoryid']]['name'])
			);
		}

		if (in_array($auth['ldap_userdirectoryid'], $userdirectoryids)) {
			// If last (default) is removed, reset default userdirectoryid to prevent from foreign key constraint.
			API::Authentication()->update(['ldap_userdirectoryid' => 0]);
		}
	}

	private static function deleteAffectedProvisionGroups(array $userdirectoryids): void {
		$del_provision_groupids = array_keys(DB::select('userdirectory_idpgroup', [
			'filter' => ['userdirectoryid' => $userdirectoryids],
			'preservekeys' => true
		]));

		if ($del_provision_groupids) {
			self::deleteProvisionGroups($del_provision_groupids);
		}
	}

	/**
	 * Test user against specific userdirectory connection.
	 * Return user data in LDAP
	 *
	 * @param array $userdirectory
	 *
	 * @throws APIException
	 *
	 * @return array
	 */
	public function test(array $userdirectory): array {
		$this->validateTest($userdirectory);

		$user = [
			'username' => $userdirectory['test_username'],
			'password' => $userdirectory['test_password']
		];
		$ldap = new CLdap($userdirectory);
		$ldap_validator = new CLdapAuthValidator(['ldap' => $ldap]);

		if (!$ldap_validator->validate($user)) {
			self::exception(
				$ldap_validator->isConnectionError() ? ZBX_API_ERROR_PARAMETERS : ZBX_API_ERROR_PERMISSIONS,
				$ldap_validator->getError()
			);
		}

		if ($userdirectory['provision_status'] == JIT_PROVISIONING_ENABLED) {
			$mapping_roles = [];

			if ($userdirectory['provision_groups']) {
				$mapping_roles = DB::select('role', [
					'output' => ['roleid', 'name', 'type'],
					'roleids' => array_column($userdirectory['provision_groups'], 'roleid', 'roleid'),
					'preservekeys' => true
				]);
			}

			$provisioning = new CProvisioning($userdirectory, $mapping_roles);
			$user = array_merge(
				$user,
				$ldap->getProvisionedData($provisioning, $user['username'])
			);

			if (array_key_exists('userdirectoryid', $userdirectory)) {
				$user['userdirectoryid'] = $userdirectory['userdirectoryid'];
			}
		}

		unset($user['password']);

		return $user;
	}

	/**
	 * Validate user directory and test user credentials to be used for testing.
	 *
	 * @param array $userdirectory
	 *
	 * @throws APIException
	 */
	protected function validateTest(array &$userdirectory): void {
		$api_input_rules = ['type' => API_OBJECT, 'flags' => API_ALLOW_UNEXPECTED, 'fields' => [
			'userdirectoryid' =>	['type' => API_ID, 'default' => 0]
		]];

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

		if ($userdirectory['userdirectoryid'] != 0) {
			$db_userdirectory = $this->get([
				'output' => ['host', 'port', 'base_dn', 'bind_dn', 'search_attribute', 'start_tls', 'search_filter',
					'provision_status', 'idp_type'
				],
				'userdirectoryids' => [$userdirectory['userdirectoryid']],
				'filter' => ['idp_type' => IDP_TYPE_LDAP]
			]);

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

			$userdirectory += reset($db_userdirectory);
			$userdirectory += DB::select('userdirectory_ldap', [
				'output' => ['bind_password'],
				'userdirectoryids' => [$userdirectory['userdirectoryid']]
			])[0];

			if ($userdirectory['provision_status'] == JIT_PROVISIONING_ENABLED) {
				$userdirectory += $this->get([
					'output' => ['group_basedn', 'group_name', 'group_member', 'group_filter', 'group_membership',
						'user_ref_attr', 'user_username', 'user_lastname'
					],
					'userdirectoryids' => $userdirectory['userdirectoryid'],
					'selectProvisionMedia' => self::MEDIA_OUTPUT_FIELDS,
					'selectProvisionGroups' => ['name', 'roleid', 'user_groups']
				])[0];
			}
		}

		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
			'userdirectoryid' =>	['type' => API_ID, 'default' => 0],
			'host' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory_ldap', 'host')],
			'port' =>				['type' => API_PORT, 'flags' => API_REQUIRED | API_NOT_EMPTY],
			'base_dn' =>			['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory_ldap', 'base_dn')],
			'bind_dn' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_ldap', 'bind_dn'), 'default' => ''],
			'bind_password' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_ldap', 'bind_password')],
			'search_attribute' =>	['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory_ldap', 'search_attribute')],
			'start_tls' =>			['type' => API_INT32, 'in' => ZBX_AUTH_START_TLS_OFF.','.ZBX_AUTH_START_TLS_ON, 'default' => ZBX_AUTH_START_TLS_OFF],
			'search_filter' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_ldap', 'search_filter'), 'default' => ''],
			'provision_status' =>	['type' => API_INT32, 'in' => implode(',', [JIT_PROVISIONING_DISABLED, JIT_PROVISIONING_ENABLED]), 'default' => JIT_PROVISIONING_DISABLED],
			'group_basedn' =>		['type' => API_STRING_UTF8],
			'group_name' =>			['type' => API_STRING_UTF8],
			'group_member' =>		['type' => API_STRING_UTF8],
			'user_ref_attr' =>		['type' => API_STRING_UTF8],
			'group_filter' =>		['type' => API_STRING_UTF8],
			'group_membership' =>	['type' => API_STRING_UTF8],
			'user_username' =>		['type' => API_STRING_UTF8],
			'user_lastname' =>		['type' => API_STRING_UTF8],
			'idp_type' =>			['type' => API_INT32, 'in' => implode(',', [IDP_TYPE_LDAP]), 'default' => IDP_TYPE_LDAP],
			'provision_media' =>	['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'provision_status', 'in' => implode(',', [JIT_PROVISIONING_ENABLED])], 'type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['mediatypeid', 'attribute']], 'fields' => [
											'userdirectory_mediaid' =>	['type' => API_ID, 'default' => 0],
											'name' =>					['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory_media', 'name')],
											'mediatypeid' =>			['type' => API_ID, 'flags' => API_REQUIRED],
											'attribute' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory_media', 'attribute')],
											'active' =>					['type' => API_INT32, 'in' => implode(',', [MEDIA_STATUS_ACTIVE, MEDIA_STATUS_DISABLED]), 'default' => DB::getDefault('userdirectory_media', 'active')],
											'severity' =>				['type' => API_INT32, 'in' => '0:63', 'default' => DB::getDefault('userdirectory_media', 'severity')],
											'period' =>					['type' => API_TIME_PERIOD, 'flags' => API_ALLOW_USER_MACRO, 'length' => DB::getFieldLength('userdirectory_media', 'period'), 'default' => DB::getDefault('userdirectory_media', 'period')]
										]],
										['else' => true, 'type' => API_OBJECTS, 'length' => 0]
			]],
			'provision_groups' =>	['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'provision_status', 'in' => JIT_PROVISIONING_ENABLED],
											'type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'uniq' => [['name']], 'fields' => [
												'name' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY],
												'roleid' =>				['type' => API_ID, 'flags' => API_REQUIRED],
												'user_groups' =>		['type' => API_OBJECTS, 'flags' => API_REQUIRED, 'fields' => [
													'usrgrpid' =>			['type' => API_ID, 'flags' => API_REQUIRED]
												]]
											]
										],
										['else' => true, 'type' => API_OBJECTS, 'length' => 0]
			]],
			'test_username' =>		['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY],
			'test_password' =>		['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY]
		]];

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

	private static function updateProvisionMedia(array &$userdirectories, array $db_userdirectories = null): void {
		$ins_provision_medias = [];
		$upd_provision_medias = [];
		$del_provision_mediaids = [];

		foreach ($userdirectories as $userdirectory) {
			if (!array_key_exists('provision_media', $userdirectory)) {
				continue;
			}

			$db_provision_medias = $db_userdirectories !== null
				? $db_userdirectories[$userdirectory['userdirectoryid']]['provision_media']
				: [];

			foreach ($userdirectory['provision_media'] as $provision_media) {
				if (array_key_exists('userdirectory_mediaid', $provision_media)) {
					$upd_provision_media = DB::getUpdatedValues('userdirectory_media', $provision_media,
						$db_provision_medias[$provision_media['userdirectory_mediaid']]
					);

					if ($upd_provision_media) {
						$upd_provision_medias[] = [
							'values' => $upd_provision_media,
							'where' => ['userdirectory_mediaid' => $provision_media['userdirectory_mediaid']]
						];
					}

					unset($db_provision_medias[$provision_media['userdirectory_mediaid']]);
				}
				else {
					$ins_provision_medias[] =
						['userdirectoryid' => $userdirectory['userdirectoryid']] + $provision_media;
				}
			}

			$del_provision_mediaids = array_merge($del_provision_mediaids, array_keys($db_provision_medias));
		}

		if ($del_provision_mediaids) {
			DB::delete('userdirectory_media', ['userdirectory_mediaid' => $del_provision_mediaids]);
		}

		if ($upd_provision_medias) {
			DB::update('userdirectory_media', $upd_provision_medias);
		}

		if ($ins_provision_medias) {
			$userdirectory_mediaids = DB::insert('userdirectory_media', $ins_provision_medias);
		}

		foreach ($userdirectories as &$userdirectory) {
			if (!array_key_exists('provision_media', $userdirectory)) {
				continue;
			}

			foreach ($userdirectory['provision_media'] as &$provision_media) {
				if (!array_key_exists('userdirectory_mediaid', $provision_media)) {
					$provision_media['userdirectory_mediaid'] = array_shift($userdirectory_mediaids);
				}
			}
			unset($provision_media);
		}
		unset($userdirectory);
	}

	private static function updateProvisionGroups(array &$userdirectories, array $db_userdirectories = null): void {
		$ins_provision_groups = [];
		$upd_provision_groups = [];
		$del_provision_groupids = [];

		foreach ($userdirectories as &$userdirectory) {
			if (!array_key_exists('provision_groups', $userdirectory)) {
				continue;
			}

			$db_provision_groups = $db_userdirectories !== null
				? array_column($db_userdirectories[$userdirectory['userdirectoryid']]['provision_groups'], null, 'name')
				: [];

			foreach ($userdirectory['provision_groups'] as &$provision_group) {
				if (array_key_exists($provision_group['name'], $db_provision_groups)) {
					$db_provision_group = $db_provision_groups[$provision_group['name']];
					$provision_group['userdirectory_idpgroupid'] = $db_provision_group['userdirectory_idpgroupid'];

					$upd_provision_group =
						DB::getUpdatedValues('userdirectory_idpgroup', $provision_group, $db_provision_group);

					if ($upd_provision_group) {
						$upd_provision_groups[] = [
							'values' => $upd_provision_group,
							'where' => ['userdirectory_idpgroupid' => $db_provision_group['userdirectory_idpgroupid']]
						];
					}

					unset($db_provision_groups[$provision_group['name']]);
				}
				else {
					$ins_provision_groups[] =
						['userdirectoryid' => $userdirectory['userdirectoryid']] + $provision_group;
				}
			}
			unset($provision_group);

			$del_provision_groupids =
				array_merge($del_provision_groupids, array_column($db_provision_groups, 'userdirectory_idpgroupid'));
		}
		unset($userdirectory);

		if ($del_provision_groupids) {
			self::deleteProvisionGroups($del_provision_groupids);
		}

		if ($upd_provision_groups) {
			DB::update('userdirectory_idpgroup', $upd_provision_groups);
		}

		if ($ins_provision_groups) {
			$userdirectory_idpgroupids = DB::insert('userdirectory_idpgroup', $ins_provision_groups);
		}

		$provision_groups = [];
		$db_provision_groups = $db_userdirectories !== null ? [] : null;

		foreach ($userdirectories as &$userdirectory) {
			if (!array_key_exists('provision_groups', $userdirectory)) {
				continue;
			}

			foreach ($userdirectory['provision_groups'] as &$provision_group) {
				if (!array_key_exists('userdirectory_idpgroupid', $provision_group)) {
					$provision_group['userdirectory_idpgroupid'] = array_shift($userdirectory_idpgroupids);

					if ($db_userdirectories !== null) {
						$db_provision_groups[$provision_group['userdirectory_idpgroupid']] = [
							'userdirectory_idpgroupid' => $provision_group['userdirectory_idpgroupid'],
							'user_groups' => []
						];
					}
				}
				else {
					$db_userdirectory = $db_userdirectories[$userdirectory['userdirectoryid']];

					$db_provision_groups[$provision_group['userdirectory_idpgroupid']] =
						$db_userdirectory['provision_groups'][$provision_group['userdirectory_idpgroupid']];
				}

				$provision_groups[] = &$provision_group;
			}
			unset($provision_group);
		}
		unset($userdirectory);

		if ($provision_groups) {
			self::updateProvisionGroupUserGroups($provision_groups, $db_provision_groups);
		}
	}

	private static function deleteProvisionGroups(array $del_provision_groupids): void {
		DB::delete('userdirectory_usrgrp', ['userdirectory_idpgroupid' => $del_provision_groupids]);
		DB::delete('userdirectory_idpgroup', ['userdirectory_idpgroupid' => $del_provision_groupids]);
	}

	private static function updateProvisionGroupUserGroups(array &$provision_groups,
			?array $db_provision_groups): void {
		$ins_user_groups = [];
		$del_user_groupids = [];

		foreach ($provision_groups as &$provision_group) {
			$idpgroupid = $provision_group['userdirectory_idpgroupid'];
			$db_user_groups = $db_provision_groups !== null && array_key_exists($idpgroupid, $db_provision_groups)
				? array_column($db_provision_groups[$idpgroupid]['user_groups'], null, 'usrgrpid')
				: [];

			foreach ($provision_group['user_groups'] as &$user_group) {
				if (array_key_exists($user_group['usrgrpid'], $db_user_groups)) {
					$user_group['userdirectory_usrgrpid'] =
						$db_user_groups[$user_group['usrgrpid']]['userdirectory_usrgrpid'];

					unset($db_user_groups[$user_group['usrgrpid']]);
				}
				else {
					$ins_user_groups[] =
						['userdirectory_idpgroupid' => $provision_group['userdirectory_idpgroupid']] + $user_group;
				}
			}
			unset($user_group);

			$del_user_groupids =
				array_merge($del_user_groupids, array_column($db_user_groups, 'userdirectory_usrgrpid'));
		}
		unset($provision_group);

		if ($del_user_groupids) {
			DB::delete('userdirectory_usrgrp', ['userdirectory_usrgrpid' => $del_user_groupids]);
		}

		if ($ins_user_groups) {
			$userdirectory_usrgrpids = DB::insert('userdirectory_usrgrp', $ins_user_groups);
		}

		foreach ($provision_groups as &$provision_group) {
			foreach ($provision_group['user_groups'] as &$user_group) {
				if (!array_key_exists('userdirectory_usrgrpid', $user_group)) {
					$user_group['userdirectory_usrgrpid'] = array_shift($userdirectory_usrgrpids);
				}
			}
			unset($user_group);
		}
		unset($provision_group);
	}

	private static function getValidationRules(bool $is_update = false): array {
		$api_required = $is_update ? 0 : API_REQUIRED;

		$specific_fields = $is_update
			? [
				'userdirectoryid' =>	['type' => API_ANY],
				'idp_type' =>			['type' => API_ANY],
				'provision_status' =>	['type' => API_ANY]
			]
			: [];

		$provision_media_rule = $is_update
			? ['type' => API_OBJECTS, 'flags' => API_NORMALIZE | API_ALLOW_UNEXPECTED, 'uniq' => [['userdirectory_mediaid']], 'fields' => [
				'userdirectory_mediaid' =>	['type' => API_ANY]
			]]
			: ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['mediatypeid', 'attribute']], 'fields' => self::getProvisionMediaValidationFields()];

		return ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['name']], 'fields' => $specific_fields + [
			'idp_type' =>			['type' => API_INT32, 'flags' => $api_required, 'in' => implode(',', [IDP_TYPE_LDAP, IDP_TYPE_SAML])],
			'name' =>				['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_STRING_UTF8, 'flags' => $api_required | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'name')],
										['else' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'name')] + ($is_update ? [] : ['default' => DB::getDefault('userdirectory', 'name')])
			]],
			'provision_status' =>	['type' => API_INT32, 'in' => implode(',', [JIT_PROVISIONING_DISABLED, JIT_PROVISIONING_ENABLED]), 'default' => DB::getDefault('userdirectory', 'provision_status')],
			'description' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'description')],
			'host' =>				['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_STRING_UTF8, 'flags' => $api_required | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory_ldap', 'host')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'port' =>				['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_PORT, 'flags' => $api_required | API_NOT_EMPTY],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'base_dn' =>			['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_STRING_UTF8, 'flags' => $api_required | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory_ldap', 'base_dn')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'bind_dn' =>			['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_ldap', 'bind_dn')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'bind_password' =>		['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_ldap', 'bind_password')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'search_attribute' =>	['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_STRING_UTF8, 'flags' => $api_required | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory_ldap', 'search_attribute')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'start_tls' =>			['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_INT32, 'in' => implode(',', [ZBX_AUTH_START_TLS_OFF, ZBX_AUTH_START_TLS_ON])],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'search_filter' =>		['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_ldap', 'search_filter')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'group_basedn' =>		['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_ldap', 'group_basedn')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'user_ref_attr' =>		['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_ldap', 'user_ref_attr')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'group_name' =>		['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_ldap', 'group_name')],
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_saml', 'group_name')]
			]],
			'user_username' =>		['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_ldap', 'user_username')],
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_saml', 'user_username')]
			]],
			'user_lastname' =>		['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_ldap', 'user_lastname')],
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_saml', 'user_lastname')]
			]],
			'group_member' =>		['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_ldap', 'group_member')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'group_filter' =>		['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_ldap', 'group_filter')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'group_membership' =>	['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_LDAP])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_ldap', 'group_membership')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'idp_entityid' =>		['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_saml', 'idp_entityid')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'sso_url' =>			['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_saml', 'sso_url')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'slo_url' =>			['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_saml', 'slo_url')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'sp_entityid' =>		['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_saml', 'sp_entityid')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'nameid_format' =>		['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_saml', 'nameid_format')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'username_attribute' =>	['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory_saml', 'username_attribute')],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'sign_messages' =>		['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_INT32, 'in' => implode(',', ['0', '1'])],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'sign_assertions' =>	['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_INT32, 'in' => implode(',', ['0', '1'])],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'sign_authn_requests' => ['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_INT32, 'in' => implode(',', ['0', '1'])],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'sign_logout_requests' => ['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_INT32, 'in' => implode(',', ['0', '1'])],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'sign_logout_responses' => ['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_INT32, 'in' => implode(',', ['0', '1'])],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'encrypt_nameid' =>		['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_INT32, 'in' => implode(',', ['0', '1'])],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'encrypt_assertions' => ['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_INT32, 'in' => implode(',', ['0', '1'])],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'scim_status' =>		['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'idp_type', 'in' => implode(',', [IDP_TYPE_SAML])], 'type' => API_INT32, 'in' => implode(',', ['0', '1'])],
										['else' => true, 'type' => API_UNEXPECTED]
			]],
			'provision_groups' =>	['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'provision_status', 'in' => implode(',', [JIT_PROVISIONING_ENABLED])], 'type' => API_OBJECTS, 'flags' => $api_required | API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['name']], 'fields' => [
											'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory_idpgroup', 'name')],
											'roleid' =>				['type' => API_ID, 'flags' => API_REQUIRED],
											'user_groups' =>		['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'uniq' => [['usrgrpid']], 'fields' => [
												'usrgrpid' =>		['type' => API_ID, 'flags' => API_REQUIRED]
											]]
										]],
										['else' => true, 'type' => API_OBJECTS, 'length' => 0]
			]],
			'provision_media' =>	['type' => API_MULTIPLE, 'rules' => [
										['if' => ['field' => 'provision_status', 'in' => implode(',', [JIT_PROVISIONING_ENABLED])]] + $provision_media_rule,
										['else' => true, 'type' => API_OBJECTS, 'length' => 0]
			]]
		]];
	}
}
