<?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/>.
**/


namespace Widgets\NavTree\Actions;

use API,
	CControllerDashboardWidgetView,
	CControllerResponseData,
	CProfile,
	CSeverityHelper;

class WidgetView extends CControllerDashboardWidgetView {

	private array $problems_per_severity_tpl;

	protected function init(): void {
		parent::init();

		$this->addValidationRules([
			'name' => 'string',
			'widgetid' => 'db widget.widgetid',
			'fields' => 'array'
		]);
	}

	protected function doAction(): void {
		// Get list of sysmapids.
		$sysmapids = [];
		$navtree_items = [];

		foreach ($this->fields_values['navtree'] as $id => $navtree_item) {
			$sysmapid = array_key_exists('sysmapid', $navtree_item) ? $navtree_item['sysmapid'] : 0;

			if ($sysmapid != 0) {
				$sysmapids[$sysmapid] = true;
			}

			$navtree_items[$id] = [
				'parent' => $navtree_item['parent'],
				'sysmapid' => $sysmapid,
				'child_sysmapids' => []
			];
		}

		// Propagate item mapids to all its parent items.
		foreach ($navtree_items as $navtree_item) {
			$parent = $navtree_item['parent'];

			while (array_key_exists($parent, $navtree_items)) {
				if ($navtree_item['sysmapid'] != 0) {
					$navtree_items[$parent]['child_sysmapids'][$navtree_item['sysmapid']] = true;
				}
				$parent = $navtree_items[$parent]['parent'];
			}
		}

		// Get severity levels and colors and select list of sysmapids to count problems per maps.
		$this->problems_per_severity_tpl = [];
		$severity_config = [];

		$maps_accessible = $sysmapids
			? API::Map()->get([
				'output' => [],
				'sysmapids' => array_keys($sysmapids),
				'preservekeys' => true
			])
			: [];

		for ($severity = TRIGGER_SEVERITY_NOT_CLASSIFIED; $severity < TRIGGER_SEVERITY_COUNT; $severity++) {
			$this->problems_per_severity_tpl[$severity] = 0;
			$severity_config[$severity] = [
				'name' => CSeverityHelper::getName($severity),
				'style_class' => CSeverityHelper::getStatusStyle($severity)
			];
		}

		$widgetid = $this->getInput('widgetid', 0);
		$navtree_item_selected = 0;
		$navtree_items_opened = [];

		if ($widgetid) {
			$pattern = 'web.dashboard.widget.navtree.item-%.toggle';
			$discard_from_start = strpos($pattern, '%');
			$discard_from_end = strlen($pattern) - $discard_from_start - 1;

			foreach (CProfile::findByIdxPattern($pattern, $widgetid) as $item_opened) {
				$navtree_items_opened[] = substr($item_opened, $discard_from_start, -$discard_from_end);
			}

			$navtree_item_selected = CProfile::get('web.dashboard.widget.navtree.item.selected', 0, $widgetid);
		}

		$this->setResponse(new CControllerResponseData([
			'name' => $this->getInput('name', $this->widget->getDefaultName()),
			'navtree' => $this->fields_values['navtree'],
			'navtree_item_selected' => $navtree_item_selected,
			'navtree_items_opened' => $navtree_items_opened,
			'problems' => $this->getNumberOfProblemsBySysmap($navtree_items),
			'show_unavailable' => $this->fields_values['show_unavailable'],
			'maps_accessible' => array_keys($maps_accessible),
			'severity_config' => $severity_config,
			'initial_load' => $this->getInput('initial_load', 0),
			'user' => [
				'debug_mode' => $this->getDebugMode()
			]
		]));
	}

	private function getNumberOfProblemsBySysmap(array $navtree_items = []): array {
		$response = [];
		$sysmapids = [];

		foreach ($navtree_items as $navtree_item) {
			$sysmapids[$navtree_item['sysmapid']] = true;
		}
		unset($sysmapids[0]);

		$sysmaps = $sysmapids
			? API::Map()->get([
				'output' => ['sysmapid', 'severity_min'],
				'selectLinks' => ['linktriggers', 'permission'],
				'selectSelements' => ['elements', 'elementtype', 'permission'],
				'sysmapids' => array_keys($sysmapids),
				'preservekeys' => true
			])
			: [];

		if ($sysmaps) {
			$triggers_per_hosts = [];
			$triggers_per_host_groups = [];
			$problems_per_trigger = [];
			$submaps_relations = [];
			$submaps_found = [];
			$host_groups = [];
			$hosts = [];

			// Gather submaps from all selected maps.
			foreach ($sysmaps as $map) {
				foreach ($map['selements'] as $selement) {
					if ($selement['elementtype'] == SYSMAP_ELEMENT_TYPE_MAP) {
						if (($element = reset($selement['elements'])) !== false) {
							$submaps_relations[$map['sysmapid']][] = $element['sysmapid'];
							$submaps_found[] = $element['sysmapid'];
						}
					}
				}
			}

			// Gather maps added as submaps for each of map in any depth.
			$sysmaps_resolved = array_keys($sysmaps);
			while ($diff = array_diff($submaps_found, $sysmaps_resolved)) {
				$submaps = API::Map()->get([
					'output' => ['sysmapid', 'severity_min'],
					'selectLinks' => ['linktriggers', 'permission'],
					'selectSelements' => ['elements', 'elementtype', 'permission'],
					'sysmapids' => $diff,
					'preservekeys' => true
				]);

				$sysmaps_resolved = array_merge($sysmaps_resolved, $diff);

				foreach ($submaps as $submap) {
					$sysmaps[$submap['sysmapid']] = $submap;

					foreach ($submap['selements'] as $selement) {
						if ($selement['elementtype'] == SYSMAP_ELEMENT_TYPE_MAP) {
							$element = reset($selement['elements']);
							if ($element) {
								$submaps_relations[$submap['sysmapid']][] = $element['sysmapid'];
								$submaps_found[] = $element['sysmapid'];
							}
						}
					}
				}
			}

			// Gather elements from all maps selected.
			foreach ($sysmaps as $map) {
				// Collect triggers from map links.
				foreach ($map['links'] as $link) {
					foreach ($link['linktriggers'] as $linktrigger) {
						$problems_per_trigger[$linktrigger['triggerid']] = $this->problems_per_severity_tpl;
					}
				}

				// Collect map elements.
				foreach ($map['selements'] as $selement) {
					switch ($selement['elementtype']) {
						case SYSMAP_ELEMENT_TYPE_HOST_GROUP:
							if (($element = reset($selement['elements'])) !== false) {
								$host_groups[$element['groupid']] = true;
							}
							break;

						case SYSMAP_ELEMENT_TYPE_TRIGGER:
							foreach (array_column($selement['elements'], 'triggerid') as $triggerid) {
								$problems_per_trigger[$triggerid] = $this->problems_per_severity_tpl;
							}
							break;

						case SYSMAP_ELEMENT_TYPE_HOST:
							if (($element = reset($selement['elements'])) !== false) {
								$hosts[$element['hostid']] = true;
							}
							break;
					}
				}
			}

			// Drop all disabled and inaccessible triggers.
			if ($problems_per_trigger) {
				$triggers = API::Trigger()->get([
					'output' => [],
					'triggerids' => array_keys($problems_per_trigger),
					'monitored' => true,
					'preservekeys' => true
				]);

				$problems_per_trigger = array_intersect_key($problems_per_trigger, $triggers);

				unset($triggers);
			}

			// Select lowest severity to reduce amount of data returned by API.
			$severity_min = min(array_column($sysmaps, 'severity_min'));

			// Get triggers related to host groups.
			if ($host_groups) {
				$triggers = API::Trigger()->get([
					'output' => ['triggerid'],
					'selectHostGroups' => ['groupid'],
					'groupids' => array_keys($host_groups),
					'preservekeys' => true
				]);

				foreach ($triggers as $trigger) {
					foreach ($trigger['hostgroups'] as $host_group) {
						$triggers_per_host_groups[$host_group['groupid']][$trigger['triggerid']] = true;
					}
					$problems_per_trigger[$trigger['triggerid']] = $this->problems_per_severity_tpl;
				}

				unset($host_groups);
			}

			// Get triggers related to hosts.
			if ($hosts) {
				$triggers = API::Trigger()->get([
					'output' => ['triggerid'],
					'selectHosts' => ['hostid'],
					'hostids' => array_keys($hosts),
					'preservekeys' => true,
					'monitored' => true
				]);

				foreach ($triggers as $trigger) {
					if (($host = reset($trigger['hosts'])) !== false) {
						$triggers_per_hosts[$host['hostid']][$trigger['triggerid']] = true;
						$problems_per_trigger[$trigger['triggerid']] = $this->problems_per_severity_tpl;
					}
				}

				unset($hosts);
			}

			// Count problems per trigger.
			if ($problems_per_trigger) {
				$problems = API::Problem()->get([
					'output' => ['objectid', 'severity'],
					'source' => EVENT_SOURCE_TRIGGERS,
					'object' => EVENT_OBJECT_TRIGGER,
					'objectids' => array_keys($problems_per_trigger),
					'severities' => range($severity_min, TRIGGER_SEVERITY_COUNT - 1),
					'symptom' => false,
					'preservekeys' => true
				]);

				if ($problems) {
					foreach ($problems as $problem) {
						$problems_per_trigger[$problem['objectid']][$problem['severity']]++;
					}
				}
			}

			// Count problems in each submap included in navigation tree:
			foreach ($navtree_items as $id => $navtree_item) {
				$maps_need_to_count_in = $navtree_item['child_sysmapids'];
				if ($navtree_item['sysmapid'] != 0) {
					$maps_need_to_count_in[$navtree_item['sysmapid']] = true;
				}

				$response[$id] = $this->problems_per_severity_tpl;
				$problems_counted = [];

				foreach (array_keys($maps_need_to_count_in) as $sysmapid) {
					if (array_key_exists($sysmapid, $sysmaps)) {
						$map = $sysmaps[$sysmapid];

						// Count problems occurred in linked elements.
						foreach ($map['selements'] as $selement) {
							if ($selement['permission'] >= PERM_READ) {
								$problems = $this->getElementProblems($selement, $problems_per_trigger, $sysmaps,
									$submaps_relations, $map['severity_min'], $problems_counted, $triggers_per_hosts,
									$triggers_per_host_groups
								);

								if ($problems !== null) {
									$response[$id] = self::sumArrayValues($response[$id], $problems);
								}
							}
						}

						// Count problems occurred in triggers which are related to the links.
						foreach ($map['links'] as $link) {
							$uncounted_problem_triggers = array_diff_key(
								array_flip(array_column($link['linktriggers'], 'triggerid')),
								$problems_counted
							);

							foreach ($uncounted_problem_triggers as $triggerid => $var) {
								$problems_counted[$triggerid] = true;

								// Skip disabled and inaccessible triggers.
								if (!array_key_exists($triggerid, $problems_per_trigger)) {
									continue;
								}

								$problems_to_add = $problems_per_trigger[$triggerid];

								// Remove problems which are less important than map's min-severity.
								if ($map['severity_min'] > 0) {
									foreach (array_keys($problems_to_add) as $severity) {
										if ($map['severity_min'] > $severity) {
											$problems_to_add[$severity] = 0;
										}
									}
								}

								$response[$id] = self::sumArrayValues($response[$id], $problems_to_add);
							}
							unset($uncounted_problem_triggers);
						}
					}
				}
			}
		}

		foreach ($response as &$row) {
			// Reduce the amount of data transferred over Ajax.
			if ($row === $this->problems_per_severity_tpl) {
				$row = 0;
			}
		}
		unset($row);

		return $response;
	}

	private function getElementProblems(array $selement, array $problems_per_trigger, array $sysmaps,
			array $submaps_relations, $severity_min = 0, array &$problems_counted = [], array $triggers_per_hosts = [],
			array $triggers_per_host_groups = []): ?array {
		$problems = null;

		switch ($selement['elementtype']) {
			case SYSMAP_ELEMENT_TYPE_HOST_GROUP:
				$problems = $this->problems_per_severity_tpl;

				if (($element = reset($selement['elements'])) !== false) {
					if (array_key_exists($element['groupid'], $triggers_per_host_groups)) {
						$uncounted_problem_triggers = array_diff_key($triggers_per_host_groups[$element['groupid']],
							$problems_counted
						);
						foreach ($uncounted_problem_triggers as $triggerid => $var) {
							$problems_counted[$triggerid] = true;

							// Skip disabled and inaccessible triggers.
							if (array_key_exists($triggerid, $problems_per_trigger)) {
								$problems = self::sumArrayValues($problems, $problems_per_trigger[$triggerid]);
							}
						}
						unset($uncounted_problem_triggers);
					}
				}
				break;

			case SYSMAP_ELEMENT_TYPE_TRIGGER:
				$problems = $this->problems_per_severity_tpl;
				$uncounted_problem_triggers = array_diff_key(
					array_flip(array_column($selement['elements'], 'triggerid')),
					$problems_counted
				);
				foreach ($uncounted_problem_triggers as $triggerid => $var) {
					$problems_counted[$triggerid] = true;

					// Skip disabled and inaccessible triggers.
					if (array_key_exists($triggerid, $problems_per_trigger)) {
						$problems = self::sumArrayValues($problems, $problems_per_trigger[$triggerid]);
					}
				}
				unset($uncounted_problem_triggers);
				break;

			case SYSMAP_ELEMENT_TYPE_HOST:
				$problems = $this->problems_per_severity_tpl;

				if (($element = reset($selement['elements'])) !== false) {
					if (array_key_exists($element['hostid'], $triggers_per_hosts)) {
						$uncounted_problem_triggers = array_diff_key($triggers_per_hosts[$element['hostid']],
							$problems_counted
						);
						foreach ($uncounted_problem_triggers as $triggerid => $var) {
							$problems_counted[$triggerid] = true;

							// Skip disabled and inaccessible triggers.
							if (array_key_exists($triggerid, $problems_per_trigger)) {
								$problems = self::sumArrayValues($problems, $problems_per_trigger[$triggerid]);
							}
						}
						unset($uncounted_problem_triggers);
					}
				}
				break;

			case SYSMAP_ELEMENT_TYPE_MAP:
				$problems = $this->problems_per_severity_tpl;

				if (($submap_element = reset($selement['elements'])) !== false) {
					// Recursively find all submaps in any depth and put them into an array.
					$maps_to_process[$submap_element['sysmapid']] = false;

					while (array_filter($maps_to_process, static function($item) {return !$item;})) {
						foreach ($maps_to_process as $linked_map => $val) {
							$maps_to_process[$linked_map] = true;

							if (array_key_exists($linked_map, $submaps_relations)) {
								foreach ($submaps_relations[$linked_map] as $submap) {
									if (!array_key_exists($submap, $maps_to_process)) {
										$maps_to_process[$submap] = false;
									}
								}
							}
						}
					}

					// Count problems in each of selected submap.
					foreach ($maps_to_process as $sysmapid => $val) {
						// Count problems in elements assigned to selements.
						if (array_key_exists($sysmapid, $sysmaps)) {
							foreach ($sysmaps[$sysmapid]['selements'] as $submap_selement) {
								if ($submap_selement['permission'] >= PERM_READ) {
									$problems_in_submap = $this->getElementProblems($submap_selement,
										$problems_per_trigger, $sysmaps, $submaps_relations,
										$sysmaps[$sysmapid]['severity_min'], $problems_counted, $triggers_per_hosts,
										$triggers_per_host_groups
									);

									if ($problems_in_submap !== null) {
										$problems = self::sumArrayValues($problems, $problems_in_submap);
									}
								}
							}
						}

						// Count problems in triggers assigned to linked.
						if (array_key_exists($sysmapid, $sysmaps)) {
							foreach ($sysmaps[$sysmapid]['links'] as $link) {
								if ($link['permission'] >= PERM_READ) {
									$uncounted_problem_triggers = array_diff_key(
										array_flip(array_column($link['linktriggers'], 'triggerid')),
										$problems_counted
									);
									foreach ($uncounted_problem_triggers as $triggerid => $var) {
										$problems_counted[$triggerid] = true;

										// Skip disabled and inaccessible triggers.
										if (array_key_exists($triggerid, $problems_per_trigger)) {
											$problems = self::sumArrayValues($problems,
												$problems_per_trigger[$triggerid]
											);
										}
									}
									unset($uncounted_problem_triggers);
								}
							}
						}
					}
				}
				break;
		}

		// Remove problems which are less important than $severity_min.
		if ($problems !== null && $severity_min > 0) {
			foreach (array_keys($problems) as $severity) {
				if ($severity_min > $severity) {
					$problems[$severity] = 0;
				}
			}
		}

		return $problems;
	}

	/**
	 * Function is used to sum problems in 2 arrays.
	 *
	 * Example:
	 * $a1 = [1 => 0, 2 => 5, 3 => 10];
	 * $a2 = [1 => 1, 2 => 2, 3 => 3];
	 * self::sumArrayValues($a1, $a2); // returns [1 => 1, 2 => 7, 3 => 13]
	 *
	 * @param array $a1  Array containing severity as key and number of problems as value.
	 * @param array $a2  Array containing severity as key and number of problems as value.
	 *
	 * @return array  Array containing problems in both arrays summed.
	 */
	private static function sumArrayValues(array $a1, array $a2): array {
		foreach ($a1 as $key => &$value) {
			$value += $a2[$key];
		}
		unset($value);

		return $a1;
	}
}
