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


/**
 * Common methods for "charts.view" and "charts.view.json" actions.
 */
abstract class CControllerCharts extends CController {
	// Number of subfilter values per row.
	const SUBFILTERS_VALUES_PER_ROW = 100;

	// Number of tag value rows allowed to be included in subfilter.
	const SUBFILTERS_TAG_VALUE_ROWS = 20;

	/**
	 * Fetches all host graphs based on hostid and graph name or item name used in graph.
	 *
	 * @param array  $hostids  Limit returned graphs to these hosts.
	 * @param string $name     Graphs or items in them should contain this string in their name.
	 *
	 * @return array
	 */
	protected function getHostGraphs(array $hostids, string $name): array {
		$graphs = API::Graph()->get([
			'output' => ['graphid', 'name'],
			'hostids' => $hostids,
			'search' => $name !== '' ? ['name' => $name] : null,
			'selectItems' => ['itemid'],
			'preservekeys' => true
		]);

		$graph_items = [];
		foreach ($graphs as $graph) {
			foreach ($graph['items'] as $item) {
				$graph_items[$item['itemid']] = $item;
			}
		}

		$filter_items = [];
		if ($name !== '') {
			$filter_items = API::Item()->get([
				'output' => ['itemid'],
				'hostids' => $hostids,
				'search' => ['name_resolved' => $name],
				'preservekeys' => true
			]);

			if ($filter_items) {
				$graphs += API::Graph()->get([
					'output' => ['graphid', 'name'],
					'hostids' => $hostids,
					'itemids' => array_keys($filter_items),
					'selectItems' => ['itemid'],
					'preservekeys' => true
				]);
			}
		}

		$items = [];
		if ($graph_items || $filter_items) {
			$items = CArrayHelper::renameObjectsKeys(API::Item()->get([
				'output' => ['itemid', 'name_resolved'],
				'hostids' => $hostids,
				'itemids' => array_keys($graph_items + $filter_items),
				'selectTags' => ['tag', 'value'],
				'preservekeys' => true
			]), ['name_resolved' => 'name']);
		}

		return $this->addTagsToGraphs($graphs, $items);
	}

	private function addTagsToGraphs(array $graphs, array $items): array {
		foreach ($graphs as &$graph) {
			$graph['tags'] = [];
			$tags = [];

			foreach ($graph['items'] as $item) {
				if (!array_key_exists($item['itemid'], $items)) {
					continue;
				}

				foreach ($items[$item['itemid']]['tags'] as $tag) {
					$tags[$tag['tag']][$tag['value']] = true;
				}
			}

			foreach ($tags as $tag_name => $tag) {
				foreach (array_keys($tag) as $tag_value) {
					$graph['tags'][] = [
						'tag' => $tag_name,
						'value' => $tag_value
					];
				}
			}
		}
		unset($graph);

		return $graphs;
	}

	/**
	 * Fetches all host graphs based on hostid and graph name or item name used in graph.
	 *
	 * @param array  $hostids  Limit returned graphs to these hosts.
	 * @param string $name     Graphs or items in them should contain this string in their name.
	 *
	 * @return array
	 */
	protected function getSimpleGraphs(array $hostids, string $name): array {
		return CArrayHelper::renameObjectsKeys(API::Item()->get([
			'output' => ['itemid', 'name_resolved'],
			'selectTags' => ['tag', 'value'],
			// TODO VM: filter by tags
			'hostids' => $hostids,
			'search' => $name !== '' ? ['name_resolved' => $name] : null,
			'filter' => ['value_type' => [ITEM_VALUE_TYPE_UINT64, ITEM_VALUE_TYPE_FLOAT]],
			'preservekeys' => true
		]), ['name_resolved' => 'name']);
	}

	/**
	 * Prepares graph display details (graph dimensions, URL and sBox-ing flag).
	 *
	 * @param array $graphids  Graph IDs for which details need to be prepared.
	 *
	 * @return array
	 */
	protected function getCharts(array $graphs): array {
		$charts = [];

		foreach ($graphs as $graph) {
			if (array_key_exists('graphid', $graph)) {
				$chart = [
					'chartid' => 'graph_'.$graph['graphid'],
					'graphid' => $graph['graphid'],
					'dimensions' => getGraphDims($graph['graphid'])
				];

				if (in_array($chart['dimensions']['graphtype'], [GRAPH_TYPE_PIE, GRAPH_TYPE_EXPLODED])) {
					$chart['sbox'] = false;
					$chart['src'] = 'chart6.php';
				}
				else {
					$chart['sbox'] = true;
					$chart['src'] = 'chart2.php';
				}
			}
			else {
				$chart = [
					'chartid' => 'item_'.$graph['itemid'],
					'itemid' => $graph['itemid'],
					'dimensions' => getGraphDims(),
					'sbox' => true,
					'src' => 'chart.php'
				];
			}

			$charts[] = $chart;
		}

		return $charts;
	}

	/**
	 * Prepare subfilter fields from filter.
	 *
	 * @param array  $filter
	 * @param array  $filter['subfilter_tagnames']  Selected tagname subfilter parameters.
	 * @param array  $filter['subfilter_tags']      Selected tags subfilter parameters.
	 *
	 * @return array
	 */
	protected static function getSubfilterFields(array $filter): array {
		$subfilters = [];

		$subfilter_keys = ['subfilter_tagnames', 'subfilter_tags'];

		foreach ($subfilter_keys as $key) {
			if (!array_key_exists($key, $filter)) {
				continue;
			}

			if ($key === 'subfilter_tags') {
				$tmp_tags = [];
				foreach ($filter[$key] as $tag => $tag_values) {
					$tmp_tags[urldecode($tag)] = array_flip($tag_values);
				}
				$subfilters[$key] = $tmp_tags;
				unset($tmp_tags);
			}
			else {
				$subfilters[$key] = array_flip($filter[$key]);
			}
		}

		return CArrayHelper::renameKeys($subfilters, [
			'subfilter_tagnames' => 'tagnames',
			'subfilter_tags' => 'tags'
		]);
	}

	/**
	 * Find what subfilters are available based on hosts selected using the main filter.
	 *
	 * @param array  $graphs                           [IN/OUT] Result of host/simple graphs matching primary filter.
	 * @param string $graphs[]['graphid']              [IN] Host graph graphid.
	 * @param array  $graphs[]['tags']                 [IN] Item tags array.
	 * @param string $graphs[]['tags'][]['tag']        [IN] Tag name.
	 * @param string $graphs[]['tags'][]['value']      [IN] Tag value.
	 * @param array  $graphs[]['matching_subfilters']  [OUT] Flag for each of subfilter group showing either graph fits
	 *                                                 its subfilter requirements.
	 * @param bool   $graphs[]['has_data']             [OUT] Flag either item has data.
	 * @param array  $subfilters                       Selected subfilters.
	 *
	 * @return array
	 */
	protected static function getSubfilters(array &$graphs, array $subfilters): array {
		$subfilter_options = self::getSubfilterOptions($graphs, $subfilters);
		$subfilters = self::clearSubfilters($subfilters, $subfilter_options);
		$graphs = self::getGraphMatchings($graphs, $subfilters);

		/*
		 * Calculate how many additional items would match the filtering results after selecting each of provided host
		 * subfilters. So item MUST match all subfilters except the tested one.
		 */
		foreach ($graphs as $graph) {
			// Calculate the counters of tag existence subfilter options.
			$graph_tagnames = [];
			$selected_tagnames =
				array_intersect(array_keys($subfilters['tagnames']), array_column($graph['tags'], 'tag'));

			foreach ($graph['tags'] as $tag) {
				if (array_key_exists($tag['tag'], $graph_tagnames)) {
					continue;
				}

				$graph_tagnames[$tag['tag']] = [];

				if ($selected_tagnames && !in_array($tag['tag'], $selected_tagnames)) {
					continue;
				}

				$graph_matches = true;
				foreach ($graph['matching_subfilters'] as $filter_name => $match) {
					if ($filter_name === 'tagnames') {
						continue;
					}
					if (!$match) {
						$graph_matches = false;
						break;
					}
				}

				if ($graph_matches) {
					$subfilter_options['tagnames'][$tag['tag']]['count']++;
				}
			}

			// Calculate the same for the tag/value pair subfilter options.
			$selected_tagvalues = [];

			if ($subfilters['tags']) {
				foreach ($graph['tags'] as $tag) {
					if (array_key_exists($tag['tag'], $subfilters['tags'])
							&& array_key_exists($tag['value'], $subfilters['tags'][$tag['tag']])) {
						$selected_tagvalues[$tag['tag']][$tag['value']] = [];
					}
				}
			}

			foreach ($graph['tags'] as $tag) {
				if ($selected_tagvalues) {
					if (!(array_key_exists($tag['tag'], $selected_tagvalues)
							&& array_key_exists($tag['value'], $selected_tagvalues[$tag['tag']]))) {
						continue;
					}
				}

				$graph_matches = true;
				foreach ($graph['matching_subfilters'] as $filter_name => $match) {
					if ($filter_name === 'tags') {
						continue;
					}
					if (!$match) {
						$graph_matches = false;
						break;
					}
				}

				if ($graph_matches) {
					$subfilter_options['tags'][$tag['tag']][$tag['value']]['count']++;
				}
			}
		}

		return $subfilter_options;
	}

	/**
	 * Collect available options of subfilter from existing items and hosts selected by primary filter.
	 * All currently selected options will be included as well, regardless their presence in the retrieved data.
	 *
	 * @param array $graphs                       Host/Simple graphs selected by primary filter.
	 * @param array $graphs[]['tags']             Item tags.
	 * @param array $graphs[]['tags'][]['tag']    Item tag name.
	 * @param array $graphs[]['tags'][]['value']  Item tag value.
	 * @param array $subfilter
	 * @param array $subfilter['tagnames']        Selected subfilter names.
	 * @param array $subfilter['tags']            Selected subfilter tags.
	 *
	 * @return array
	 */
	protected static function getSubfilterOptions(array $graphs, array $subfilter): array {
		$subfilter_options = [
			'tagnames' => [],
			'tags' => []
		];

		// First, add currently selected options, regardless their presence in the retrieved data.

		foreach (array_keys($subfilter['tagnames']) as $tagname) {
			$subfilter_options['tagnames'][$tagname] = [
				'name' => $tagname,
				'selected' => true,
				'count' => 0
			];
		}

		foreach ($subfilter['tags'] as $tag => $values) {
			foreach (array_keys($values) as $value) {
				$subfilter_options['tags'][$tag][$value] = [
					'name' => $value,
					'selected' => true,
					'count' => 0
				];
			}
		}

		// Second, add options represented by the selected data.

		foreach ($graphs as $graph) {
			foreach ($graph['tags'] as $tag) {
				if (!array_key_exists($tag['tag'], $subfilter_options['tagnames'])) {
					$subfilter_options['tagnames'][$tag['tag']] = [
						'name' => $tag['tag'],
						'selected' => array_key_exists($tag['tag'], $subfilter['tagnames']),
						'count' => 0
					];
				}

				$subfilter_options['tags'][$tag['tag']][$tag['value']] = [
					'name' => $tag['value'],
					'selected' => (array_key_exists($tag['tag'], $subfilter['tags'])
						&& array_key_exists($tag['value'], $subfilter['tags'][$tag['tag']])
					),
					'count' => 0
				];
			}
		}

		// Sort subfilters by values.
		CArrayHelper::sort($subfilter_options['tagnames'], ['name']);
		uksort($subfilter_options['tags'], 'strnatcmp');
		array_walk($subfilter_options['tags'], function (&$tag_values) {
			CArrayHelper::sort($tag_values, ['name']);
		});

		return $subfilter_options;
	}

	protected static function clearSubfilters(array $subfilter, array $subfilter_options): array {
		foreach (array_keys($subfilter['tagnames']) as $tagname) {
			if (!array_key_exists($tagname, $subfilter_options['tagnames'])) {
				unset($subfilter['tagnames'][$tagname]);
			}
		}

		foreach ($subfilter['tags'] as $tag => $values) {
			if (!array_key_exists($tag, $subfilter_options['tags'])) {
				unset($subfilter['tags'][$tag]);
				continue;
			}

			foreach (array_keys($values) as $value) {
				if (!array_key_exists($value, $subfilter_options['tags'][$tag])) {
					unset($subfilter['tags'][$tag][$value]);
				}
			}
		}

		return $subfilter;
	}

	/**
	 * Calculate which items retrieved using the primary filter matches selected subfilter options. Results are added to
	 * the array stored with 'matching_subfilters' key for each retrieved item. Additionally 'has_data' flag is added to
	 * each of retrieved item to indicate either particular item has data.
	 *
	 * @param array  $graphs
	 * @param string $graphs[]['graphid']                          Item hostid.
	 * @param string $graphs[]['itemid']                           Item itemid.
	 * @param array  $graphs[]['tags']                             Items tags.
	 * @param array  $graphs[]['tags'][]['tag']                    Items tag name.
	 * @param array  $graphs[]['tags'][]['value']                  Items tag value.
	 * @param array  $subfilter
	 * @param array  $subfilter['tagnames']                       Selected subfilter tagnames.
	 * @param array  $subfilter['tags']                           Selected subfilter tags.
	 *
	 * @return array
	 */
	protected static function getGraphMatchings(array $graphs, array $subfilter): array {
		foreach ($graphs as &$graph) {
			$match_tagnames = (!$subfilter['tagnames']
				|| (bool) array_intersect_key($subfilter['tagnames'], array_flip(array_column($graph['tags'], 'tag')))
			);

			if ($subfilter['tags']) {
				$match_tags = false;
				foreach ($graph['tags'] as $tag) {
					if (array_key_exists($tag['tag'], $subfilter['tags'])
							&& array_key_exists($tag['value'], $subfilter['tags'][$tag['tag']])) {
						$match_tags = true;
						break;
					}
				}
			}
			else {
				$match_tags = true;
			}

			$graph['matching_subfilters'] = [
				'tagnames' => $match_tagnames,
				'tags' => $match_tags
			];
		}
		unset($graph);

		return $graphs;
	}

	/**
	 * Unset items not matching selected subfilters.
	 *
	 * @param array $graphs
	 * @param array $graphs['matching_subfilters']    Contains flags either items matches all selected subfilters.
	 *
	 * @return array
	 */
	protected static function applySubfilters(array $graphs): array {
		return array_filter($graphs, function ($graph) {
			return array_sum($graph['matching_subfilters']) == count($graph['matching_subfilters']);
		});
	}

	/**
	 * Make subset of most severe subfilters to reduce the space used by subfilter.
	 *
	 * @param array  $subfilters
	 * @param string $subfilters[<subfilter option>]['name']      Option name.
	 * @param bool   $subfilters[<subfilter option>]['selected']  Flag indicating if option is selected.
	 *
	 * @return array
	 */
	public static function getMostSevereSubfilters(array $subfilters): array {
		if (self::SUBFILTERS_VALUES_PER_ROW >= count($subfilters)) {
			return $subfilters;
		}

		// All selected subfilters always must be included.
		$most_severe = array_filter($subfilters, function ($elmnt) {
			return $elmnt['selected'];
		});

		// Add first non-selected subfilter values in case if limit is not exceeded.
		$remaining = self::SUBFILTERS_VALUES_PER_ROW - count($most_severe);
		if ($remaining > 0) {
			$subfilters = array_diff_key($subfilters, $most_severe);
			$most_severe += array_slice($subfilters, 0, $remaining, true);
		}

		CArrayHelper::sort($most_severe, ['name']);

		return $most_severe;
	}

	/**
	 * Make subset of most severe tag value subfilters to reduce the space used by subfilter.
	 *
	 * @param array $tags
	 * @param bool  $tags[<tagname>][<tagvalue>]['selected']  Flag indicating if tag value is selected.
	 *
	 * @return array
	 */
	public static function getMostSevereTagValueSubfilters(array $tags): array {
		if (self::SUBFILTERS_TAG_VALUE_ROWS >= count($tags)) {
			return $tags;
		}

		// All selected subfilters always must be included.
		$most_severe = array_filter($tags, function ($tag) {
			return (bool) array_sum(array_column($tag, 'selected'));
		});

		// Add first non-selected subfilter values in case if limit is not exceeded.
		$remaining = self::SUBFILTERS_TAG_VALUE_ROWS - count($most_severe);
		if ($remaining > 0) {
			$tags = array_diff_key($tags, $most_severe);
			$most_severe += array_slice($tags, 0, $remaining, true);
		}

		uksort($most_severe, 'strnatcmp');

		return $most_severe;
	}
}
