<?php
/*
** Zabbix
** Copyright (C) 2001-2016 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 IT services.
 *
 * @package API
 */
class CService extends CApiService {

	protected $tableName = 'services';
	protected $tableAlias = 's';
	protected $sortColumns = ['sortorder', 'name'];

	public function __construct() {
		parent::__construct();

		$this->getOptions = array_merge($this->getOptions, [
			'parentids' => null,
			'childids' => null,
			'countOutput' => null,
			'selectParent' => null,
			'selectDependencies' => null,
			'selectParentDependencies' => null,
			'selectTimes' => null,
			'selectAlarms' => null,
			'selectTrigger' => null,
			'sortfield' => '',
			'sortorder' => ''
		]);
	}

	/**
	 * Get services.
	 *
	 * Allowed options:
	 * - parentids                      - fetch the services that are hardlinked to the given parent services;
	 * - childids                       - fetch the services that are hardlinked to the given child services;
	 * - countOutput                    - return the number of the results as an integer;
	 * - selectParent                   - include the parent service in the result;
	 * - selectDependencies             - include service child dependencies in the result;
	 * - selectParentDependencies       - include service parent dependencies in the result;
	 * - selectTimes                    - include service times in the result;
	 * - selectAlarms                   - include alarms generated by the service;
	 * - selectTrigger                  - include the linked trigger;
	 * - sortfield                      - name of columns to sort by;
	 * - sortorder                      - sort order.
	 *
	 * @param array $options
	 *
	 * @return array
	 */
	public function get(array $options) {
		$options = zbx_array_merge($this->getOptions, $options);

		// build and execute query
		$sql = $this->createSelectQuery($this->tableName(), $options);
		$res = DBselect($sql, $options['limit']);

		// fetch results
		$result = [];
		while ($row = DBfetch($res)) {
			// a count query, return a single result
			if ($options['countOutput'] !== null) {
				$result = $row['rowscount'];
			}
			// a normal select query
			else {
				$result[$row[$this->pk()]] = $row;
			}
		}

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

		if ($result) {
			$result = $this->addRelatedObjects($options, $result);
			$result = $this->unsetExtraFields($result, ['triggerid'], $options['output']);
		}

		if ($options['preservekeys'] === null) {
			$result = zbx_cleanHashes($result);
		}

		return $result;
	}

	/**
	 * Validates the input parameters for the create() method.
	 *
	 * @throws APIException if the input is invalid
	 *
	 * @param array $services
	 *
	 * @return void
	 */
	protected function validateCreate(array $services) {
		foreach ($services as $service) {
			$this->checkName($service);
			$this->checkAlgorithm($service);
			$this->checkShowSla($service);
			$this->checkGoodSla($service);
			$this->checkSortOrder($service);
			$this->checkTriggerId($service);
			$this->checkStatus($service);
			$this->checkParentId($service);

			$error = _s('Wrong fields for service "%1$s".', $service['name']);
			$this->checkUnsupportedFields($this->tableName(), $service, $error, [
				'parentid', 'dependencies', 'times'
			]);
		}

		$this->checkTriggerPermissions($services);
	}

	/**
	 * Creates the given services.
	 *
	 * @param array $services
	 *
	 * @return array
	 */
	public function create(array $services) {
		$services = zbx_toArray($services);
		$this->validateCreate($services);

		// save the services
		$serviceIds = DB::insert($this->tableName(), $services);

		$dependencies = [];
		$serviceTimes = [];
		foreach ($services as $key => $service) {
			$serviceId = $serviceIds[$key];

			// save dependencies
			if (!empty($service['dependencies'])) {
				foreach ($service['dependencies'] as $dependency) {
					$dependency['serviceid'] = $serviceId;
					$dependencies[] = $dependency;
				}
			}

			// save parent service
			if (!empty($service['parentid'])) {
				$dependencies[] = [
					'serviceid' => $service['parentid'],
					'dependsOnServiceid' => $serviceId,
					'soft' => 0
				];
			}

			// save service times
			if (isset($service['times'])) {
				foreach ($service['times'] as $serviceTime) {
					$serviceTime['serviceid'] = $serviceId;
					$serviceTimes[] = $serviceTime;
				}
			}
		}

		if ($dependencies) {
			$this->addDependencies($dependencies);
		}

		if ($serviceTimes) {
			$this->addTimes($serviceTimes);
		}

		updateItServices();

		return ['serviceids' => $serviceIds];
	}

	/**
	 * Validates the input parameters for the update() method.
	 *
	 * @throws APIException if the input is invalid
	 *
	 * @param array $services
	 *
	 * @return void
	 */
	public function validateUpdate(array $services) {
		foreach ($services as $service) {
			if (empty($service['serviceid'])) {
				self::exception(ZBX_API_ERROR_PARAMETERS, _('Invalid method parameters.'));
			}
		}

		$this->checkServicePermissions(zbx_objectValues($services, 'serviceid'));

		$services = $this->extendObjects($this->tableName(), $services, ['name']);
		foreach ($services as $service) {
			$this->checkName($service);

			if (isset($service['algorithm'])) {
				$this->checkAlgorithm($service);
			}
			if (isset($service['showsla'])) {
				$this->checkShowSla($service);
			}
			if (isset($service['goodsla'])) {
				$this->checkGoodSla($service);
			}
			if (isset($service['sortorder'])) {
				$this->checkSortOrder($service);
			}
			if (isset($service['triggerid'])) {
				$this->checkTriggerId($service);
			}
			if (isset($service['status'])) {
				$this->checkStatus($service);
			}
			if (isset($service['parentid'])) {
				$this->checkParentId($service);
			}

			$error = _s('Wrong fields for service "%1$s".', $service['name']);
			$this->checkUnsupportedFields($this->tableName(), $service, $error, [
				'parentid', 'dependencies', 'times'
			]);
		}

		$this->checkTriggerPermissions($services);
	}

	/**
	 * Updates the given services.
	 *
	 * @param array $services
	 *
	 * @return array
	 */
	public function update(array $services) {
		$services = zbx_toArray($services);
		$this->validateUpdate($services);

		// save the services
		foreach ($services as $service) {
			DB::updateByPk($this->tableName(), $service['serviceid'], $service);
		}

		// update dependencies
		$dependencies = [];
		$parentDependencies = [];
		$serviceTimes = [];
		$deleteParentsForServiceIds = [];
		$deleteDependenciesForServiceIds = [];
		$deleteTimesForServiceIds = [];
		foreach ($services as $service) {
			if (isset($service['dependencies'])) {
				$deleteDependenciesForServiceIds[] = $service['serviceid'];

				if ($service['dependencies']) {
					foreach ($service['dependencies'] as $dependency) {
						$dependency['serviceid'] = $service['serviceid'];
						$dependencies[] = $dependency;
					}
				}
			}

			// update parent
			if (isset($service['parentid'])) {
				$deleteParentsForServiceIds[] = $service['serviceid'];

				if ($service['parentid']) {
					$parentDependencies[] = [
						'serviceid' => $service['parentid'],
						'dependsOnServiceid' => $service['serviceid'],
						'soft' => 0
					];
				}
			}

			// save service times
			if (isset($service['times'])) {
				$deleteTimesForServiceIds[] = $service['serviceid'];

				foreach ($service['times'] as $serviceTime) {
					$serviceTime['serviceid'] = $service['serviceid'];
					$serviceTimes[] = $serviceTime;
				}
			}
		}

		// replace dependencies
		if ($deleteParentsForServiceIds) {
			$this->deleteParentDependencies(zbx_objectValues($services, 'serviceid'));
		}
		if ($deleteDependenciesForServiceIds) {
			$this->deleteDependencies(array_unique($deleteDependenciesForServiceIds));
		}
		if ($parentDependencies || $dependencies) {
			$this->addDependencies(array_merge($parentDependencies, $dependencies));
		}

		// replace service times
		if ($deleteTimesForServiceIds) {
			$this->deleteTimes($deleteTimesForServiceIds);
		}
		if ($serviceTimes) {
			$this->addTimes($serviceTimes);
		}

		updateItServices();

		return ['serviceids' => zbx_objectValues($services, 'serviceid')];
	}

	/**
	 * Validates the input parameters for the delete() method.
	 *
	 * @throws APIException if the input is invalid
	 *
	 * @param array $serviceIds
	 *
	 * @return void
	 */
	public function validateDelete($serviceIds) {
		if (!$serviceIds) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
		}

		$this->checkServicePermissions($serviceIds);
		$this->checkThatServicesDontHaveChildren($serviceIds);
	}

	/**
	 * Delete services.
	 *
	 * @param array $serviceIds
	 *
	 * @return array
	 */
	public function delete(array $serviceIds) {
		$this->validateDelete($serviceIds);

		DB::delete($this->tableName(), ['serviceid' => $serviceIds]);

		updateItServices();

		return ['serviceids' => $serviceIds];
	}

	/**
	 * Validates the input parameters for the addDependencies() method.
	 *
	 * @throws APIException if the input is invalid
	 *
	 * @param array $dependencies
	 *
	 * @return void
	 */
	protected function validateAddDependencies(array $dependencies) {
		if (!$dependencies) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
		}

		foreach ($dependencies as $dependency) {
			if (empty($dependency['serviceid']) || empty($dependency['dependsOnServiceid'])) {
				self::exception(ZBX_API_ERROR_PARAMETERS, _('Invalid method parameters.'));
			}
		}

		$serviceIds = array_merge(
			zbx_objectValues($dependencies, 'serviceid'),
			zbx_objectValues($dependencies, 'dependsOnServiceid')
		);
		$serviceIds = array_unique($serviceIds);
		$this->checkServicePermissions($serviceIds);

		foreach ($dependencies as $dependency) {
			$this->checkDependency($dependency);

			$this->checkUnsupportedFields('services_links', $dependency,
				_s('Wrong fields for dependency for service "%1$s".', $dependency['serviceid']),
				['dependsOnServiceid', 'serviceid']
			);
		}

		$this->checkForHardlinkedDependencies($dependencies);
		$this->checkThatParentsDontHaveTriggers($dependencies);
		$this->checkForCircularityInDependencies($dependencies);
	}

	/**
	 * Add the given service dependencies.
	 *
	 * @param array $dependencies   an array of service dependencies, each pair in the form of
	 *                              array('serviceid' => 1, 'dependsOnServiceid' => 2, 'soft' => 0)
	 *
	 * @return array
	 */
	public function addDependencies(array $dependencies) {
		$dependencies = zbx_toArray($dependencies);
		$this->validateAddDependencies($dependencies);

		$data = [];
		foreach ($dependencies as $dependency) {
			$data[] = [
				'serviceupid' => $dependency['serviceid'],
				'servicedownid' => $dependency['dependsOnServiceid'],
				'soft' => $dependency['soft']
			];
		}
		DB::insert('services_links', $data);

		return ['serviceids' => zbx_objectValues($dependencies, 'serviceid')];
	}

	/**
	 * Validates the input for the deleteDependencies() method.
	 *
	 * @throws APIException if the given input is invalid
	 *
	 * @param array $serviceIds
	 *
	 * @return void
	 */
	protected function validateDeleteDependencies(array $serviceIds) {
		if (!$serviceIds) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
		}

		$this->checkServicePermissions($serviceIds);
	}

	/**
	 * Deletes all dependencies for the given services.
	 *
	 * @param array $serviceIds
	 *
	 * @return boolean
	 */
	public function deleteDependencies($serviceIds) {
		$serviceIds = zbx_toArray($serviceIds);
		$this->validateDeleteDependencies($serviceIds);

		DB::delete('services_links', [
			'serviceupid' =>  $serviceIds
		]);

		return ['serviceids' => $serviceIds];
	}

	/**
	 * Validates the input for the addTimes() method.
	 *
	 * @throws APIException if the given input is invalid
	 *
	 * @param array $serviceTimes
	 *
	 * @return void
	 */
	public function validateAddTimes(array $serviceTimes) {
		foreach ($serviceTimes as $serviceTime) {
			$this->checkTime($serviceTime);

			$this->checkUnsupportedFields('services_times', $serviceTime,
				_s('Wrong fields for time for service "%1$s".', $serviceTime['serviceid'])
			);
		}

		$this->checkServicePermissions(array_unique(zbx_objectValues($serviceTimes, 'serviceid')));
	}

	/**
	 * Adds the given service times.
	 *
	 * @param array $serviceTimes an array of service times
	 *
	 * @return array
	 */
	public function addTimes(array $serviceTimes) {
		$serviceTimes = zbx_toArray($serviceTimes);
		$this->validateAddTimes($serviceTimes);

		DB::insert('services_times', $serviceTimes);

		return ['serviceids' => zbx_objectValues($serviceTimes, 'serviceid')];
	}

	/**
	 * Validates the input for the deleteTimes() method.
	 *
	 * @throws APIException if the given input is invalid
	 *
	 * @param array $serviceIds
	 *
	 * @return void
	 */
	protected function validateDeleteTimes(array $serviceIds) {
		if (!$serviceIds) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
		}

		$this->checkServicePermissions($serviceIds);
	}

	/**
	 * Returns availability-related information about the given services during the given time intervals.
	 *
	 * Available options:
	 *  - serviceids    - a single service ID or an array of service IDs;
	 *  - intervals     - a single time interval or an array of time intervals, each containing:
	 *      - from          - the beginning of the interval, timestamp;
	 *      - to            - the end of the interval, timestamp.
	 *
	 * Returns the following availability information for each service:
	 *  - status            - the current status of the service;
	 *  - problems          - an array of triggers that are currently in problem state and belong to the given service
	 *                        or it's descendants;
	 *  - sla               - an array of requested intervals with SLA information:
	 *      - from              - the beginning of the interval;
	 *      - to                - the end of the interval;
	 *      - okTime            - the time the service was in OK state, in seconds;
	 *      - problemTime       - the time the service was in problem state, in seconds;
	 *      - downtimeTime      - the time the service was down, in seconds.
	 *
	 * If the service calculation algorithm is set to SERVICE_ALGORITHM_NONE, the method will return an empty 'problems'
	 * array and null for all of the calculated values.
	 *
	 * @param array $options
	 *
	 * @return array    as array(serviceId2 => data1, serviceId2 => data2, ...)
	 */
	public function getSla(array $options) {
		$serviceIds = (isset($options['serviceids'])) ? zbx_toArray($options['serviceids']) : null;
		$intervals = (isset($options['intervals'])) ? zbx_toArray($options['intervals']) : [];

		// fetch services
		$services = $this->get([
			'output' => ['serviceid', 'name', 'status', 'algorithm'],
			'selectTimes' => API_OUTPUT_EXTEND,
			'selectParentDependencies' => ['serviceupid'],
			'serviceids' => $serviceIds,
			'preservekeys' => true
		]);

		$rs = [];
		if ($services) {
			$usedSeviceIds = [];

			$problemServiceIds = [];
			foreach ($services as &$service) {
				$service['alarms'] = [];

				// don't calculate SLA for services with disabled status calculation
				if ($this->isStatusEnabled($service)) {
					$usedSeviceIds[$service['serviceid']] = $service['serviceid'];

					if ($service['status'] > 0) {
						$problemServiceIds[] = $service['serviceid'];
					}
				}
			}
			unset($service);

			// initial data
			foreach ($services as $service) {
				$rs[$service['serviceid']] = [
					'status' => ($this->isStatusEnabled($service)) ? $service['status'] : null,
					'problems' => [],
					'sla' => []
				];
			}

			if ($usedSeviceIds) {
				// add service alarms
				if ($intervals) {
					$intervalConditions = [];
					foreach ($intervals as $interval) {
						$intervalConditions[] = 'sa.clock BETWEEN '.zbx_dbstr($interval['from']).' AND '.zbx_dbstr($interval['to']);
					}
					$query = DBselect(
						'SELECT *'.
						' FROM service_alarms sa'.
						' WHERE '.dbConditionInt('sa.serviceid', $usedSeviceIds).
							' AND ('.implode(' OR ', $intervalConditions).')'.
						' ORDER BY sa.clock,sa.servicealarmid'
					);
					while ($data = DBfetch($query)) {
						$services[$data['serviceid']]['alarms'][] = $data;
					}
				}

				// add problem triggers
				if ($problemServiceIds) {
					$problemTriggers = $this->fetchProblemTriggers($problemServiceIds);
					$rs = $this->escalateProblems($services, $problemTriggers, $rs);
				}

				$slaCalculator = new CServicesSlaCalculator();

				// calculate SLAs
				foreach ($intervals as $interval) {
					$latestValues = $this->fetchLatestValues($usedSeviceIds, $interval['from']);

					foreach ($services as $service) {
						$serviceId = $service['serviceid'];

						// only calculate the sla for services which require it
						if (isset($usedSeviceIds[$serviceId])) {
							$latestValue = (isset($latestValues[$serviceId])) ? $latestValues[$serviceId] : 0;
							$intervalSla = $slaCalculator->calculateSla($service['alarms'], $service['times'],
								$interval['from'], $interval['to'], $latestValue
							);
						}
						else {
							$intervalSla = [
								'ok' => null,
								'okTime' => null,
								'problemTime' => null,
								'downtimeTime' => null
							];
						}

						$rs[$service['serviceid']]['sla'][] = [
							'from' => $interval['from'],
							'to' => $interval['to'],
							'sla' => $intervalSla['ok'],
							'okTime' => $intervalSla['okTime'],
							'problemTime' => $intervalSla['problemTime'],
							'downtimeTime' => $intervalSla['downtimeTime']
						];
					}
				}
			}
		}

		return $rs;
	}

	/**
	 * Deletes all service times for the given services.
	 *
	 * @param array $serviceIds
	 *
	 * @return boolean
	 */
	public function deleteTimes($serviceIds) {
		$serviceIds = zbx_toArray($serviceIds);
		$this->validateDeleteTimes($serviceIds);

		DB::delete('services_times', [
			'serviceid' =>  $serviceIds
		]);

		return ['serviceids' => $serviceIds];
	}

	/**
	 * Returns true if all of the given objects are available for reading.
	 *
	 * @param $ids
	 *
	 * @return bool
	 */
	public function isReadable(array $ids) {
		if (empty($ids)) {
			return true;
		}
		$ids = array_unique($ids);

		$count = $this->get([
			'serviceids' => $ids,
			'countOutput' => true
		]);
		return count($ids) == $count;
	}

	/**
	 * Returns true if all of the given objects are available for writing.
	 *
	 * @param $ids
	 *
	 * @return bool
	 */
	public function isWritable(array $ids) {
		return $this->isReadable($ids);
	}

	/**
	 * Deletes the dependencies of the parent services on the given services.
	 *
	 * @param $serviceIds
	 *
	 * @return void
	 */
	protected function deleteParentDependencies($serviceIds) {
		DB::delete('services_links', [
			'servicedownid' => $serviceIds,
			'soft' => 0
		]);
	}


	/**
	 * Returns an array of triggers which are in a problem state and are linked to the given services.
	 *
	 * @param array $serviceIds
	 *
	 * @return array    in the form of array(serviceId1 => array(triggerId => trigger), ...)
	 */
	protected function fetchProblemTriggers(array $serviceIds) {
		$sql = 'SELECT s.serviceid,t.triggerid'.
				' FROM services s,triggers t'.
				' WHERE s.status>0'.
					' AND t.triggerid=s.triggerid'.
					' AND '.dbConditionInt('s.serviceid', $serviceIds).
				' ORDER BY s.status DESC,t.description';

		// get service reason
		$triggers = DBfetchArray(DBSelect($sql));

		$rs = [];
		foreach ($triggers as $trigger) {
			$serviceId = $trigger['serviceid'];
			unset($trigger['serviceid']);

			$rs[$serviceId] = [$trigger['triggerid'] => $trigger];
		}

		return $rs;
	}

	/**
	 * Escalates the problem triggers from the child services to their parents and adds them to $slaData.
	 * The escalation will stop if a service has status calculation disabled or is in OK state.
	 *
	 * @param array $services
	 * @param array $serviceProblems    an array of service triggers defines as
	 *                                  array(serviceId1 => array(triggerId => trigger), ...)
	 * @param array $slaData
	 *
	 * @return array
	 */
	protected function escalateProblems(array $services, array $serviceProblems, array $slaData) {
		$parentProblems = [];
		foreach ($serviceProblems as $serviceId => $problemTriggers) {
			$service = $services[$serviceId];

			// add the problem trigger of the current service to the data
			$slaData[$serviceId]['problems'] = zbx_array_merge($slaData[$serviceId]['problems'], $problemTriggers);

			// add the same trigger to the parent services
			foreach ($service['parentDependencies'] as $dependency) {
				$parentServiceId = $dependency['serviceupid'];

				if (isset($services[$parentServiceId])) {
					$parentService = $services[$parentServiceId];

					// escalate only if status calculation is enabled for the parent service and it's in problem state
					if ($this->isStatusEnabled($parentService) && $parentService['status']) {
						if (!isset($parentProblems[$parentServiceId])) {
							$parentProblems[$parentServiceId] = [];
						}
						$parentProblems[$parentServiceId] = zbx_array_merge($parentProblems[$parentServiceId], $problemTriggers);
					}
				}
			}
		}

		// propagate the problems to the parents
		if ($parentProblems) {
			$slaData = $this->escalateProblems($services, $parentProblems, $slaData);
		}

		return $slaData;
	}

	/**
	 * Returns the value of the latest service alarm before the given time.
	 *
	 * @param array $serviceIds
	 * @param int $beforeTime
	 *
	 * @return array
	 */
	protected function fetchLatestValues(array $serviceIds, $beforeTime) {
		// the query will return the alarms with the maximum timestamp for each service
		// since multiple alarms can have the same timestamp, we only need to save the last one
		$query = DBSelect(
			'SELECT sa.serviceid,sa.value'.
			' FROM (SELECT MAX(sa3.servicealarmid) AS servicealarmid'.
					' FROM (SELECT sa2.serviceid,MAX(sa2.clock) AS clock'.
							' FROM service_alarms sa2'.
							' WHERE sa2.clock<'.zbx_dbstr($beforeTime).
								' AND '.dbConditionInt('sa2.serviceid', $serviceIds).
							' GROUP BY sa2.serviceid) ss'.
					' JOIN service_alarms sa3 ON sa3.serviceid = ss.serviceid and sa3.clock = ss.clock'.
					' GROUP BY sa3.serviceid) ss2'.
			' JOIN service_alarms sa ON sa.servicealarmid = ss2.servicealarmid'
		);
		$rs = [];
		while ($alarm = DBfetch($query)) {
			$rs[$alarm['serviceid']] = $alarm['value'];
		}

		return $rs;
	}

	/**
	 * Returns an array of dependencies that are children of the given services. Performs permission checks.
	 *
	 * @param array $parentServiceIds
	 * @param $output
	 *
	 * @return array    an array of service links sorted by "sortorder" in ascending order
	 */
	protected function fetchChildDependencies(array $parentServiceIds, $output) {
		$sqlParts = API::getApiService()->createSelectQueryParts('services_links', 'sl', [
			'output' => $output,
			'filter' => ['serviceupid' => $parentServiceIds]
		]);

		// sort by sortorder
		$sqlParts['from'][] = $this->tableName().' '.$this->tableAlias();
		$sqlParts['where'][] = 'sl.servicedownid='.$this->fieldId('serviceid');
		$sqlParts = $this->addQueryOrder($this->fieldId('sortorder'), $sqlParts);
		$sqlParts = $this->addQueryOrder($this->fieldId('serviceid'), $sqlParts);

		// add permission filter
		if (CWebUser::getType() != USER_TYPE_SUPER_ADMIN) {
			$sqlParts = $this->addPermissionFilter($sqlParts);
		}

		$sql = $this->createSelectQueryFromParts($sqlParts);

		return DBfetchArray(DBselect($sql));
	}

	/**
	 * Returns an array of dependencies from the parent services to the given services.
	 * Performs permission checks.
	 *
	 * @param array $childServiceIds
	 * @param $output
	 * @param boolean $soft             if set to true, will return only soft-linked dependencies
	 *
	 * @return array    an array of service links sorted by "sortorder" in ascending order
	 */
	protected function fetchParentDependencies(array $childServiceIds, $output, $soft = null) {
		$sqlParts = API::getApiService()->createSelectQueryParts('services_links', 'sl', [
			'output' => $output,
			'filter' => ['servicedownid' => $childServiceIds]
		]);

		$sqlParts['from'][] = $this->tableName().' '.$this->tableAlias();
		$sqlParts['where'][] = 'sl.serviceupid='.$this->fieldId('serviceid');
		if ($soft !== null) {
			$sqlParts['where'][] = 'sl.soft='.($soft ? 1 : 0);
		}
		$sqlParts = $this->addQueryOrder($this->fieldId('sortorder'), $sqlParts);
		$sqlParts = $this->addQueryOrder($this->fieldId('serviceid'), $sqlParts);

		// add permission filter
		if (CWebUser::getType() != USER_TYPE_SUPER_ADMIN) {
			$sqlParts = $this->addPermissionFilter($sqlParts);
		}

		$sql = $this->createSelectQueryFromParts($sqlParts);

		return DBfetchArray(DBselect($sql));
	}

	/**
	 * Returns true if status calculation is enabled for the given service.
	 *
	 * @param array $service
	 *
	 * @return bool
	 */
	protected function isStatusEnabled(array $service) {
		return ($service['algorithm'] != SERVICE_ALGORITHM_NONE);
	}

	/**
	 * Validates the "name" field.
	 *
	 * @throws APIException if the name is missing
	 *
	 * @param array $service
	 *
	 * @return void
	 */
	protected function checkName(array $service) {
		if (!isset($service['name']) || zbx_empty($service['name'])) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty name.'));
		}
	}

	/**
	 * Validates the "algorithm" field. Assumes the "name" field is valid.
	 *
	 * @throws APIException if the name is missing or invalid
	 *
	 * @param array $service
	 *
	 * @return void
	 */
	protected function checkAlgorithm(array $service) {
		if (!isset($service['algorithm']) || !serviceAlgorithm($service['algorithm'])) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect algorithm for service "%1$s".', $service['name']));
		}
	}

	/**
	 * Validates the "showsla" field. Assumes the "name" field is valid.
	 *
	 * @throws APIException if the name is missing or is not a boolean value
	 *
	 * @param array $service
	 *
	 * @return void
	 */
	protected function checkShowSla(array $service) {
		$showSlaValues = [
			SERVICE_SHOW_SLA_OFF => true,
			SERVICE_SHOW_SLA_ON => true
		];
		if (!isset($service['showsla']) || !isset($showSlaValues[$service['showsla']])) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect calculate SLA value for service "%1$s".', $service['name']));
		}
	}

	/**
	 * Validates the "showsla" field. Assumes the "name" field is valid.
	 *
	 * @throws APIException if the value is missing, or is out of bounds
	 *
	 * @param array $service
	 *
	 * @return void
	 */
	protected function checkGoodSla(array $service) {
		if ((!empty($service['showsla']) && empty($service['goodsla']))
				|| (isset($service['goodsla'])
					&& (!is_numeric($service['goodsla']) || $service['goodsla'] < 0 || $service['goodsla'] > 100))) {

			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect acceptable SLA for service "%1$s".', $service['name']));
		}
	}

	/**
	 * Validates the "sortorder" field. Assumes the "name" field is valid.
	 *
	 * @throws APIException if the value is missing, or is out of bounds
	 *
	 * @param array $service
	 *
	 * @return void
	 */
	protected function checkSortOrder(array $service) {
		if (!isset($service['sortorder']) || !zbx_is_int($service['sortorder'])
			|| $service['sortorder'] < 0 || $service['sortorder'] > 999) {

			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect sorder order for service "%1$s".', $service['name']));
		}
	}

	/**
	 * Validates the "triggerid" field. Assumes the "name" field is valid.
	 *
	 * @throws APIException if the value is incorrect
	 *
	 * @param array $service
	 *
	 * @return void
	 */
	protected function checkTriggerId(array $service) {
		if (!empty($service['triggerid']) && !zbx_is_int($service['triggerid'])) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect trigger ID for service "%1$s".', $service['name']));
		}
	}

	/**
	 * Validates the "parentid" field. Assumes the "name" field is valid.
	 *
	 * @throws APIException if the value is incorrect
	 *
	 * @param array $service
	 *
	 * @return void
	 */
	protected function checkParentId(array $service) {
		if (!empty($service['parentid']) && !zbx_is_int($service['parentid'])) {
			if (isset($service['name'])) {
				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect parent for service "%1$s".', $service['name']));
			}
			else {
				self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect parent service.'));
			}
		}

		if (isset($service['serviceid']) && idcmp($service['serviceid'], $service['parentid'])) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _('Service cannot be parent and child at the same time.'));
		}
	}

	/**
	 * Validates the "status" field. Assumes the "name" field is valid.
	 *
	 * @throws APIException if the value is incorrect
	 *
	 * @param array $service
	 *
	 * @return void
	 */
	protected function checkStatus(array $service) {
		if (!empty($service['status']) && !zbx_is_int($service['status'])) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect status for service "%1$s".', $service['name']));
		}
	}

	/**
	 * Checks that the user has read access to the given triggers.
	 *
	 * @throws APIException if the user doesn't have permission to access any of the triggers
	 *
	 * @param array $services
	 *
	 * @return void
	 */
	protected function checkTriggerPermissions(array $services) {
		$triggerIds = [];
		foreach ($services as $service) {
			if (!empty($service['triggerid'])) {
				$triggerIds[] = $service['triggerid'];
			}
		}
		if (!API::Trigger()->isReadable($triggerIds)) {
			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
		}
	}

	/**
	 * Checks that all of the given services are readable.
	 *
	 * @throws APIException if at least one of the services doesn't exist
	 *
	 * @param array $serviceIds
	 *
	 * @return void
	 */
	protected function checkServicePermissions(array $serviceIds) {
		if (!$this->isReadable($serviceIds)) {
			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
		}
	}

	/**
	 * Checks that none of the given services have any children.
	 *
	 * @throws APIException if at least one of the services has a child service
	 *
	 * @param array $serviceIds
	 *
	 * @return void
	 */
	protected function checkThatServicesDontHaveChildren(array $serviceIds) {
		$child = API::getApiService()->select('services_links', [
			'output' => ['serviceupid'],
			'filter' => [
				'serviceupid' => $serviceIds,
				'soft' => 0
			],
			'limit' => 1
		]);
		$child = reset($child);
		if ($child) {
			$service = API::getApiService()->select($this->tableName(), [
				'output' => ['name'],
				'serviceids' => $child['serviceupid'],
				'limit' => 1
			]);
			$service = reset($service);
			self::exception(ZBX_API_ERROR_PERMISSIONS,
				_s('Service "%1$s" cannot be deleted, because it is dependent on another service.', $service['name'])
			);
		}
	}

	/**
	 * Checks that the given dependency is valid.
	 *
	 * @throws APIException if the dependency is invalid
	 *
	 * @param array $dependency
	 *
	 * @return void
	 */
	protected function checkDependency(array $dependency) {
		if (idcmp($dependency['serviceid'], $dependency['dependsOnServiceid'])) {
			$service = API::getApiService()->select($this->tableName(), [
				'output' => ['name'],
				'serviceids' => $dependency['serviceid']
			]);
			$service = reset($service);
			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Service "%1$s" cannot be dependent on itself.', $service['name']));
		}

		// check 'soft' field value
		if (!isset($dependency['soft']) || !in_array((int) $dependency['soft'], [0, 1], true)) {
			$service = API::getApiService()->select($this->tableName(), [
				'output' => ['name'],
				'serviceids' => $dependency['serviceid']
			]);
			$service = reset($service);
			self::exception(ZBX_API_ERROR_PARAMETERS,
				_s('Incorrect "soft" field value for dependency for service "%1$s".', $service['name'])
			);
		}
	}

	/**
	 * Checks that that none of the given services are hard linked to a different service.
	 * Assumes the dependencies are valid.
	 *
	 * @throws APIException if at a least one service is hard linked to another service
	 *
	 * @param array $dependencies
	 *
	 * @return void
	 */
	protected function checkForHardlinkedDependencies(array $dependencies) {
		// only check hard dependencies
		$hardDepServiceIds = [];
		foreach ($dependencies as $dependency) {
			if (!$dependency['soft']) {
				$hardDepServiceIds[] = $dependency['dependsOnServiceid'];
			}
		}

		if ($hardDepServiceIds) {
			// look for at least one hardlinked service among the given
			$hardDepServiceIds = array_unique($hardDepServiceIds);
			$dep = API::getApiService()->select('services_links', [
				'output' => ['servicedownid'],
				'filter' => [
					'soft' => 0,
					'servicedownid' => $hardDepServiceIds
				],
				'limit' => 1
			]);
			if ($dep) {
				$dep = reset($dep);
				$service = API::getApiService()->select($this->tableName(), [
					'output' => ['name'],
					'serviceids' => $dep['servicedownid']
				]);
				$service = reset($service);
				self::exception(ZBX_API_ERROR_PARAMETERS,
					_s('Service "%1$s" is already hardlinked to a different service.', $service['name'])
				);
			}
		}
	}

	/**
	 * Checks that none of the parent services are linked to a trigger. Assumes the dependencies are valid.
	 *
	 * @throws APIException if at least one of the parent services is linked to a trigger
	 *
	 * @param array $dependencies
	 *
	 * @return void
	 */
	protected function checkThatParentsDontHaveTriggers(array $dependencies) {
		$parentServiceIds = array_unique(zbx_objectValues($dependencies, 'serviceid'));
		if ($parentServiceIds) {
			$query = DBselect(
				'SELECT s.triggerid,s.name'.
					' FROM services s '.
					' WHERE '.dbConditionInt('s.serviceid', $parentServiceIds).
					' AND s.triggerid IS NOT NULL', 1);
			if ($parentService = DBfetch($query)) {
				self::exception(ZBX_API_ERROR_PARAMETERS,
					_s('Service "%1$s" cannot be linked to a trigger and have children at the same time.', $parentService['name']));
			}
		}
	}

	/**
	 * Checks that dependencies will not create cycles in service dependencies.
	 *
	 * @throws APIException if at least one cycle is possible
	 *
	 * @param array $depsToValid	dependency list to be validated
	 *
	 * @return void
	 */
	protected function checkForCircularityInDependencies($depsToValid) {
		$dbDeps = API::getApiService()->select('services_links', [
			'output' => ['serviceupid', 'servicedownid']
		]);

		// create existing dependency acyclic graph
		$arr = [];
		foreach ($dbDeps as $dbDep) {
			if (!isset($arr[$dbDep['serviceupid']])) {
				$arr[$dbDep['serviceupid']] = [];
			}
			$arr[$dbDep['serviceupid']][$dbDep['servicedownid']] = $dbDep['servicedownid'];
		}

		// check for circularity and add dependencies to the graph
		foreach ($depsToValid as $dep) {
			$this->DFCircularitySearch($dep['serviceid'], $dep['dependsOnServiceid'], $arr);
			$arr[$dep['serviceid']][$dep['dependsOnServiceid']] = $dep['dependsOnServiceid'];
		}

	}

	/**
	 * Depth First Search recursive function to find circularity and rise exception.
	 *
	 * @throws APIException if cycle is possible
	 *
	 * @param int $id	dependency from id
	 * @param int $depId	dependency to id
	 * @param ref $arr	reference to graph structure. Structure is associative array with keys as "from id"
	 *			and values as arrays with keys and values as "to id".
	 *
	 * @return void
	 */
	protected function dfCircularitySearch($id, $depId, &$arr) {
		if ($id == $depId) {
			// cycle found
			self::exception(ZBX_API_ERROR_PARAMETERS, _('Services form a circular dependency.'));
		}
		if (isset($arr[$depId])) {
			foreach ($arr[$depId] as $dep) {
				$this->DFCircularitySearch($id, $dep, $arr);
			}
		}
	}

	/**
	 * Checks that the given service time is valid.
	 *
	 * @throws APIException if the service time is invalid
	 *
	 * @param array $serviceTime
	 *
	 * @return void
	 */
	protected function checkTime(array $serviceTime) {
		if (empty($serviceTime['serviceid'])) {
			self::exception(ZBX_API_ERROR_PARAMETERS, _('Invalid method parameters.'));
		}

		checkServiceTime($serviceTime);
	}

	protected function applyQueryFilterOptions($tableName, $tableAlias, array $options, array $sqlParts) {
		if (CWebUser::getType() != USER_TYPE_SUPER_ADMIN) {
			// if services with specific trigger IDs were requested, return only the ones accessible to the current user.
			if ($options['filter']['triggerid']) {
				$accessibleTriggers = API::Trigger()->get([
					'output' => ['triggerid'],
					'triggerids' => $options['filter']['triggerid']
				]);
				$options['filter']['triggerid'] = zbx_objectValues($accessibleTriggers, 'triggerid');
			}
			// otherwise return services with either no triggers, or any trigger accessible to the current user
			else {
				$sqlParts = $this->addPermissionFilter($sqlParts);
			}
		}

		$sqlParts = parent::applyQueryFilterOptions($tableName, $tableAlias, $options, $sqlParts);

		// parentids
		if ($options['parentids'] !== null) {
			$sqlParts['from'][] = 'services_links slp';
			$sqlParts['where'][] = $this->fieldId('serviceid').'=slp.servicedownid AND slp.soft=0';
			$sqlParts['where'][] = dbConditionInt('slp.serviceupid', (array) $options['parentids']);
		}
		// childids
		if ($options['childids'] !== null) {
			$sqlParts['from'][] = 'services_links slc';
			$sqlParts['where'][] = $this->fieldId('serviceid').'=slc.serviceupid AND slc.soft=0';
			$sqlParts['where'][] = dbConditionInt('slc.servicedownid', (array) $options['childids']);
		}

		return $sqlParts;
	}

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

		$serviceIds = array_keys($result);

		// selectDependencies
		if ($options['selectDependencies'] !== null && $options['selectDependencies'] != API_OUTPUT_COUNT) {
			$dependencies = $this->fetchChildDependencies($serviceIds,
				$this->outputExtend($options['selectDependencies'], ['serviceupid', 'linkid'])
			);
			$dependencies = zbx_toHash($dependencies, 'linkid');
			$relationMap = $this->createRelationMap($dependencies, 'serviceupid', 'linkid');

			$dependencies = $this->unsetExtraFields($dependencies, ['serviceupid', 'linkid'], $options['selectDependencies']);
			$result = $relationMap->mapMany($result, $dependencies, 'dependencies');
		}

		// selectParentDependencies
		if ($options['selectParentDependencies'] !== null && $options['selectParentDependencies'] != API_OUTPUT_COUNT) {
			$dependencies = $this->fetchParentDependencies($serviceIds,
				$this->outputExtend($options['selectParentDependencies'], ['servicedownid', 'linkid'])
			);
			$dependencies = zbx_toHash($dependencies, 'linkid');
			$relationMap = $this->createRelationMap($dependencies, 'servicedownid', 'linkid');

			$dependencies = $this->unsetExtraFields($dependencies, ['servicedownid', 'linkid'],
				$options['selectParentDependencies']
			);
			$result = $relationMap->mapMany($result, $dependencies, 'parentDependencies');
		}

		// selectParent
		if ($options['selectParent'] !== null && $options['selectParent'] != API_OUTPUT_COUNT) {
			$dependencies = $this->fetchParentDependencies($serviceIds, ['servicedownid', 'serviceupid'], false);
			$relationMap = $this->createRelationMap($dependencies, 'servicedownid', 'serviceupid');
			$parents = $this->get([
				'output' => $options['selectParent'],
				'serviceids' => $relationMap->getRelatedIds(),
				'preservekeys' => true
			]);
			$result = $relationMap->mapOne($result, $parents, 'parent');
		}

		// selectTimes
		if ($options['selectTimes'] !== null && $options['selectTimes'] != API_OUTPUT_COUNT) {
			$serviceTimes = API::getApiService()->select('services_times', [
				'output' => $this->outputExtend($options['selectTimes'], ['serviceid', 'timeid']),
				'filter' => ['serviceid' => $serviceIds],
				'preservekeys' => true
			]);
			$relationMap = $this->createRelationMap($serviceTimes, 'serviceid', 'timeid');

			$serviceTimes = $this->unsetExtraFields($serviceTimes, ['serviceid', 'timeid'], $options['selectTimes']);
			$result = $relationMap->mapMany($result, $serviceTimes, 'times');
		}

		// selectAlarms
		if ($options['selectAlarms'] !== null && $options['selectAlarms'] != API_OUTPUT_COUNT) {
			$serviceAlarms = API::getApiService()->select('service_alarms', [
				'output' => $this->outputExtend($options['selectAlarms'], ['serviceid', 'servicealarmid']),
				'filter' => ['serviceid' => $serviceIds],
				'preservekeys' => true
			]);
			$relationMap = $this->createRelationMap($serviceAlarms, 'serviceid', 'servicealarmid');

			$serviceAlarms = $this->unsetExtraFields($serviceAlarms, ['serviceid', 'servicealarmid'],
				$options['selectAlarms']
			);
			$result = $relationMap->mapMany($result, $serviceAlarms, 'alarms');
		}

		// selectTrigger
		if ($options['selectTrigger'] !== null && $options['selectTrigger'] != API_OUTPUT_COUNT) {
			$relationMap = $this->createRelationMap($result, 'serviceid', 'triggerid');
			$triggers = API::getApiService()->select('triggers', [
				'output' => $options['selectTrigger'],
				'triggerids' => $relationMap->getRelatedIds(),
				'preservekeys' => true
			]);
			$result = $relationMap->mapOne($result, $triggers, 'trigger');
		}

		return $result;
	}

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

		if ($options['countOutput'] === null) {
			if ($options['selectTrigger'] !== null) {
				$sqlParts = $this->addQuerySelect($this->fieldId('triggerid'), $sqlParts);
			}
		}

		return $sqlParts;
	}

	/**
	 * Add permission filter SQL query part
	 *
	 * @param array $sqlParts
	 *
	 * @return string
	 */
	protected function addPermissionFilter($sqlParts) {
		$userid = self::$userData['userid'];
		$userGroups = getUserGroupsByUserId($userid);

		$sqlParts['where'][] = '(EXISTS ('.
									'SELECT NULL'.
									' FROM functions f,items i,hosts_groups hgg'.
									' JOIN rights r'.
										' ON r.id=hgg.groupid'.
										' AND '.dbConditionInt('r.groupid', $userGroups).
									' WHERE s.triggerid=f.triggerid'.
										' AND f.itemid=i.itemid'.
										' AND i.hostid=hgg.hostid'.
									' GROUP BY f.triggerid'.
									' HAVING MIN(r.permission)>'.PERM_DENY.
									')'.
								' OR s.triggerid IS NULL)';

		return $sqlParts;
	}
}
