<?php
declare(strict_types = 1);

/**
 * Outils de maintenance.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
class Maintenance
{
	/**
	 * Rapport détaillé des corrections de statistiques
	 * effectuées en base de données.
	 *
	 * @var array
	 */
	public static $updateStatsReport =
	[
		// Détails des statistiques corrigées des catégories.
		'categories' => [],

		// Détails des catégories ou albums désactivés car ils
		// se trouvaient dans des catégories désactivées.
		'categories_status' => [],

		// Détails des statistiques corrigées des fichiers.
		'items' => [],

		// Nombre de fichiers désactivés car ils
		// se trouvaient dans des catégories désactivées.
		'items_status' => 0
	];



	/**
	 * Vérifie et corrige la colonne 'cat_parents' des categories.
	 *
	 * @return int
	 *   Retourne le nombre de catégories mises à jour,
	 *   ou -1 en cas d'erreur.
	 */
	public static function catParents(): int
	{
		// Récupération des informations utiles de toutes les catégories.
		if (!DB::execute('SELECT cat_id, cat_parents, cat_path FROM {categories}'))
		{
			return -1;
		}
		$categories = DB::fetchAll('cat_path');

		// Vérification de la colonne 'cat_parents'.
		$params = [];
		foreach ($categories as &$i)
		{
			$parents_id = $i['cat_parents'] . $i['cat_id'];
			$parents_id = explode(Parents::SEPARATOR, $parents_id);
			$parents_path_id = [1];
			$p = '';
			foreach (explode('/', $i['cat_path']) as &$path)
			{
				if (isset($categories[$p = $p ? $p . '/' . $path : $path]))
				{
					$parents_path_id[] = $categories[$p]['cat_id'];
				}
			}
			if ($parents_id != $parents_path_id)
			{
				if (count($parents_path_id) > 1)
				{
					array_pop($parents_path_id);
				}
				$parents_path_id[] = '';
				$params[] =
				[
					implode(Parents::SEPARATOR, $parents_path_id),
					$i['cat_id']
				];
			}
		}
		if (!$params)
		{
			return 0;
		}

		// Mise à jour de la base de données.
		if (!DB::execute('UPDATE {categories} SET cat_parents = ? WHERE cat_id = ?', $params))
		{
			return -1;
		}

		return DB::rowCount();
	}

	/**
	 * Vérifie et corrige les statistiques des fichiers et catégories.
	 *
	 * @return int
	 *   Retourne le nombre de catégories
	 *   et de fichiers qui ont été mis à jour,
	 *   ou -1 en cas d'erreur.
	 */
	public static function dbStats(): int
	{
		if (function_exists('set_time_limit'))
		{
			set_time_limit(600);
		}

		self::$updateStatsReport =
		[
			'categories' => [],
			'categories_status' => [],
			'items' => [],
			'items_status' => 0
		];

		// Début de la transaction.
		if (!($in_transaction = DB::inTransaction()) && !DB::beginTransaction())
		{
			return -1;
		}

		// Vérifications.
		if (!self::_dbStatsItems() || !self::_dbStatsCategories())
		{
			return -1;
		}

		// Validation de la transaction.
		if (!$in_transaction && !DB::commitTransaction())
		{
			return -1;
		}

		$count = ((int) self::$updateStatsReport['items_status'])
			+ count(self::$updateStatsReport['categories_status'])
			+ count(self::$updateStatsReport['items'])
			+ count(self::$updateStatsReport['categories']);

		if (CONF_DEV_MODE && $count)
		{
			l(self::$updateStatsReport);
		}

		return $count;
	}



	/**
	 * Vérification des informations des catégories.
	 *
	 * @return bool
	 */
	private static function _dbStatsCategories(): bool
	{
		$items_stats = [];
		$image_types = DB::inInt(Item::IMAGE_TYPES);
		$video_types = DB::inInt(Item::VIDEO_TYPES);

		// Récupération des informations utiles de toutes les catégories.
		$sql = 'SELECT cat_id, cat_path, cat_a_size, cat_a_subalbs,
					   cat_a_subcats, cat_a_albums, cat_a_images, cat_a_videos,
					   cat_a_hits, cat_a_comments, cat_a_votes, cat_a_rating,
					   cat_a_favorites, cat_a_size, cat_d_subalbs, cat_d_subcats,
					   cat_d_albums, cat_d_images, cat_d_videos, cat_d_hits,
					   cat_d_comments, cat_d_votes, cat_d_rating, cat_d_favorites,
					   cat_d_size, cat_lastpubdt, cat_filemtime, cat_status,
					   CASE WHEN cat_filemtime IS NULL
							THEN "category" ELSE "album"
							 END AS type
				  FROM {categories}
			  ORDER BY cat_path ASC';
		if (!DB::execute($sql))
		{
			return FALSE;
		}
		$categories = DB::fetchAll();

		// On recherche des albums ou catégories activés qui se trouvent
		// dans des catégories désactivées, puis on les désactivent.
		$deactivated = [];
		$params = [];
		foreach ($categories as &$i)
		{
			if (!$i['cat_status'])
			{
				foreach ($categories as &$cat_infos)
				{
					$regexp = '`^' . preg_quote($i['cat_path']) . '/.+`i';
					if (preg_match($regexp, $cat_infos['cat_path']) && $cat_infos['cat_status'])
					{
						$params[] = [$cat_infos['cat_id']];
						self::$updateStatsReport['categories_status'][] =
						[
							'id' => $cat_infos['cat_id'],
							'path' => $cat_infos['cat_path'],
							'type' => $cat_infos['type'],
							'report' => ['cat_status' => ['before' => 1, 'after' => 0]]
						];
					}
				}
				if ($i['cat_id'] > 1)
				{
					$deactivated[$i['cat_id']] = $i['cat_path'];
				}
			}
		}
		if ($params)
		{
			$sql = 'UPDATE {categories} SET cat_status = "0" WHERE cat_id = ?';
			if (!DB::execute($sql, $params))
			{
				return FALSE;
			}
		}

		// On désactive tous les fichiers qui se trouvent
		// dans des catégories désactivées.
		foreach ($deactivated as $id => &$path)
		{
			foreach ($deactivated as $id2 => &$path2)
			{
				if (preg_match('`^' . preg_quote($path) . '/.+`i', $path2))
				{
					unset($deactivated[$id2]);
				}
			}
		}
		foreach ($deactivated as &$path)
		{
			$path = [DB::likeEscape($path) . '/%'];
		}
		if ($deactivated)
		{
			$sql = 'UPDATE {items}
					   SET item_status = "0"
					 WHERE item_path LIKE ?
					   AND item_status = "1"';
			if (!DB::execute($sql, array_values($deactivated)))
			{
				return FALSE;
			}
			$row_count = DB::rowCount();
			if ($row_count > 0)
			{
				self::$updateStatsReport['items_status'] = $row_count;
			}
		}

		// Paramètres des requêtes préparées.
		$params = [];
		foreach ($categories as &$i)
		{
			$params[] = [$i['cat_id'] > 1 ? DB::likeEscape($i['cat_path']) . '/%' : '%'];
		}

		// Récupération du nombre de fichiers.
		foreach (['image', 'video'] as $filetype)
		{
			foreach (['a', 'd'] as $s)
			{
				$sql_item_status = $s == 'a' ? "item_status = '1'" : "item_status != '1'";
				$sql = "SELECT COUNT(*) AS count
						  FROM {items}
						 WHERE item_path LIKE ?
						   AND item_type IN (" . ${$filetype . '_types'} . ")
						   AND $sql_item_status";
				$count = ['fetchAll'];
				if (!DB::execute($sql, $params, [], $count))
				{
					return FALSE;
				}
				foreach ($categories as $k => &$i)
				{
					$items_stats[$i['cat_id']][$s . '_' . $filetype . 's']
						= $count[$k][0]['count'];
				}
			}
		}

		// Récupération des sommes des différentes statistiques des fichiers.
		foreach (['a', 'd'] as $s)
		{
			$sql_item_status = $s == 'a' ? "item_status = '1'" : "item_status != '1'";
			$sql = "SELECT SUM(item_filesize) AS size,
						   SUM(item_hits) AS hits,
						   SUM(item_comments) AS comments,
						   SUM(item_votes) AS votes,
						   CASE WHEN SUM(item_votes) > 0
								THEN SUM(item_rating*item_votes) / SUM(item_votes)
								ELSE 0 END AS rating,
						   SUM(item_favorites) AS favorites,
						   MAX(item_pubdt) AS lastpubdt
					  FROM {items}
					 WHERE item_path LIKE ?
					   AND $sql_item_status";
			$count = ['fetchAll'];
			if (!DB::execute($sql, $params, [], $count))
			{
				return FALSE;
			}
			foreach ($categories as $k => &$i)
			{
				foreach (['comments', 'favorites', 'hits',
				'lastpubdt', 'rating', 'size', 'votes'] as $col)
				{
					$items_stats[$i['cat_id']][$s . '_' . $col] = $count[$k][0][$col] ?? 0;
				}
			}
		}

		// Informations ne concernant pas les albums.
		$params = [];
		foreach ($categories as &$i)
		{
			if ($i['type'] == 'category')
			{
				$params[] =
				[
					'id' => [$i['cat_id']],
					'path' => [$i['cat_id'] > 1 ? DB::likeEscape($i['cat_path']) . '/%' : '%']
				];
			}
		}

		// Récupération du nombre d'albums.
		foreach (['a' => 1, 'd' => 0] as $s => $status)
		{
			$sql = "SELECT COUNT(*) AS count
					  FROM {categories}
					 WHERE cat_path LIKE ?
					   AND cat_filemtime IS NOT NULL
					   AND cat_status = '$status'
					   AND cat_id > 1";
			$count = ['fetchAll'];
			if (!DB::execute($sql, array_column($params, 'path'), [], $count))
			{
				return FALSE;
			}
			$n = 0;
			foreach ($categories as $k => &$i)
			{
				$items_stats[$i['cat_id']][$s . '_albums'] = $i['type'] == 'category'
					? $count[$n++][0]['count']
					: 0;
			}
		}

		// Récupération du nombre de sous-catégories et de sous-albums.
		foreach (['albs' => 'NOT NULL', 'cats' => 'NULL'] as $cat_type => $null)
		{
			foreach (['a' => 1, 'd' => 0] as $s => $status)
			{
				$sql = "SELECT COUNT(*) AS count
						  FROM {categories}
						 WHERE parent_id = ?
						   AND cat_filemtime IS $null
						   AND cat_status = '$status'
						   AND cat_id > 1";
				$count = ['fetchAll'];
				if (!DB::execute($sql, array_column($params, 'id'), [], $count))
				{
					return FALSE;
				}
				$n = 0;
				foreach ($categories as $k => &$i)
				{
					$items_stats[$i['cat_id']][$s . '_sub' . $cat_type]
						= $i['type'] == 'category' ? $count[$n++][0]['count'] : 0;
				}
			}
		}

		// Vérifications des données.
		$stats_integer = ['albums', 'comments', 'favorites', 'hits',
			'images', 'size', 'subalbs', 'subcats', 'videos', 'votes'];
		foreach ($categories as $k => &$i)
		{
			$columns = [];
			$params = [];
			$report = [];
			$id = $i['cat_id'];

			// Comparaisons.
			foreach (['a_', 'd_'] as $s)
			{
				// Statistiques de type INTEGER.
				foreach ($stats_integer as &$col)
				{
					$cat_stat = Utility::getIntVal($i['cat_' . $s . $col]);
					$item_stat = Utility::getIntVal($items_stats[$id][$s . $col]);

					if ($cat_stat != $item_stat)
					{
						$columns[] = 'cat_' . $s . $col . ' = ?';
						$params[] = $item_stat;
						$report['cat_' . $s . $col] = 
						[
							'before' => $cat_stat,
							'after' => $item_stat
						];
					}
				}

				// Note moyenne.
				if (round((float) $i['cat_' . $s . 'rating'], 8)
				 != round((float) $items_stats[$id][$s . 'rating'], 8))
				{
					$after = ($items_stats[$id][$s . 'rating'] == '')
						? 0
						: $items_stats[$id][$s . 'rating'];
					$columns[] = 'cat_' . $s. 'rating = ?';
					$params[] = $after;
					$report['cat_' . $s . 'rating'] =
					[
						'before' => $i['cat_' . $s . 'rating'],
						'after' => $after
					];
				}
			}

			// Date de publication du fichier le plus récent.
			$get_lastpubdt_value = function($value)
			{
				return is_string($value)
					&& preg_match('`^\d{4}\-\d{2}\-\d{2}\s\d{2}:\d{2}:\d{2}$`', $value)
					? $value
					: NULL;
			};
			$lastpubdt = $get_lastpubdt_value(
				($items_stats[$id]['a_images'] + $items_stats[$id]['a_videos']) > 0
				? $items_stats[$id]['a_lastpubdt']
				: NULL
			);
			$i['cat_lastpubdt'] = $get_lastpubdt_value($i['cat_lastpubdt']);
			if ($lastpubdt != $i['cat_lastpubdt'])
			{
				$columns[] = "cat_lastpubdt = ?";
				$params[] = $lastpubdt;
				$report['cat_lastpubdt'] =
				[
					'before' => $i['cat_lastpubdt'] ?? 'NULL',
					'after' => $lastpubdt ?? 'NULL'
				];
			}

			if (!$columns)
			{
				continue;
			}

			// Mise à jour de la catégorie.
			$sql = 'UPDATE {categories} SET ' . implode(', ', $columns) . ' WHERE cat_id = ?';
			$params[] = $i['cat_id'];
			if (!DB::execute($sql, $params))
			{
				return FALSE;
			}

			self::$updateStatsReport['categories'][] =
			[
				'id' => $i['cat_id'],
				'path' => $i['cat_path'],
				'type' => $i['type'],
				'report' => $report
			];
		}

		return TRUE;
	}

	/**
	 * Vérification des statistiques des images et vidéos.
	 *
	 * @return bool
	 */
	private static function _dbStatsItems(): bool
	{
		// Récupération du nombre de commentaires activés
		// dans la table des commentaires, groupés par fichier.
		$sql = 'SELECT com.item_id,
					   i.item_path,
					   COUNT(*) AS item_comments
				  FROM {comments} AS com
			 LEFT JOIN {items} AS i USING (item_id)
				 WHERE com_status = "1"
			  GROUP BY com.item_id, i.item_path
			  ORDER BY com.item_id ASC';
		if (!DB::execute($sql))
		{
			return FALSE;
		}
		$comments_table = DB::fetchAll('item_id');

		// Récupération du nombre de votes et de la note moyenne
		// dans la table des votes, groupés par fichier.
		$sql = 'SELECT v.item_id,
					   i.item_path,
					   COUNT(*) AS item_votes,
					   SUM(vote_rating)*1.0/COUNT(*) AS item_rating
				  FROM {votes} AS v
			 LEFT JOIN {items} AS i USING (item_id)
			  GROUP BY v.item_id, i.item_path, item_votes
			  ORDER BY v.item_id ASC';
		if (!DB::execute($sql))
		{
			return FALSE;
		}
		$votes_table = DB::fetchAll('item_id');

		// Récupération du nombre de favoris d'utilisateurs activés
		// dans la table des favoris, groupés par fichier.
		$sql = 'SELECT f.item_id,
					   i.item_path,
					   COUNT(*) AS item_favorites
				  FROM {favorites} AS f
			 LEFT JOIN {items} AS i
					ON i.item_id = f.item_id
			 LEFT JOIN {users} AS u
					ON u.user_id = f.user_id
			     WHERE user_status = "1"
			  GROUP BY f.item_id, i.item_path
			  ORDER BY f.item_id ASC';
		if (!DB::execute($sql))
		{
			return FALSE;
		}
		$favorites_table = DB::fetchAll('item_id');

		// Récupération des statistiques dans la table des fichiers.
		$sql = 'SELECT item_id,
					   item_path,
					   item_comments,
					   item_votes,
					   item_rating,
					   item_favorites
				  FROM {items}
				 WHERE item_votes > 0
					OR item_comments > 0
					OR item_favorites > 0
			  GROUP BY item_id
			  ORDER BY item_id ASC';
		if (!DB::execute($sql))
		{
			return FALSE;
		}
		$items_table = DB::fetchAll('item_id');
		foreach ($items_table as &$i)
		{
			ksort($i);
		}

		// On regroupe les informations des tables
		// où l'on a récupérer les statistiques à comparer.
		$stats_tables = $comments_table;
		foreach ($votes_table as $id => &$i)
		{
			$stats_tables[$id]['item_id'] = $i['item_id'];
			$stats_tables[$id]['item_path'] = $i['item_path'];
			$stats_tables[$id]['item_votes'] = $i['item_votes'];
			$stats_tables[$id]['item_rating'] = $i['item_rating'];
		}
		foreach ($favorites_table as $id => &$i)
		{
			$stats_tables[$id]['item_id'] = $i['item_id'];
			$stats_tables[$id]['item_path'] = $i['item_path'];
			$stats_tables[$id]['item_favorites'] = $i['item_favorites'];
		}
		foreach ($stats_tables as &$i)
		{
			foreach (['item_comments', 'item_favorites', 'item_votes', 'item_rating'] as $col)
			{
				if (!isset($i[$col]))
				{
					$i[$col] = 0;
				}
			}
			ksort($i);
		}
		ksort($stats_tables);

		// On détermine les différences entre
		// la table des images et les autres tables.
		$params = [];
		foreach ($stats_tables as $id => &$i)
		{
			if (!isset($items_table[$id])
			 || $items_table[$id]['item_comments'] != $i['item_comments']
			 || $items_table[$id]['item_favorites'] != $i['item_favorites']
			 || $items_table[$id]['item_votes'] != $i['item_votes']
			 || round((float) $items_table[$id]['item_rating'], 8) !=
				round((float) $i['item_rating'], 8))
			{
				$params[$id] = $i;
			}
		}
		foreach ($items_table as $id => &$i)
		{
			if (!isset($stats_tables[$id]))
			{
				$params[$id] = $i;
				$params[$id]['item_comments'] = 0;
				$params[$id]['item_favorites'] = 0;
				$params[$id]['item_rating'] = 0;
				$params[$id]['item_votes'] = 0;
			}
		}
		if (!$params)
		{
			return TRUE;
		}

		// On ajoute les différences au rapport.
		foreach ($params as $id => &$i)
		{
			$report = [];
			foreach ($i as $col => &$val)
			{
				switch ($col)
				{
					case 'item_comments' :
						$before = (isset($items_table[$id]))
							? $items_table[$id]['item_comments']
							: 0;
						if ($before != $val)
						{
							$report['item_comments'] =
							[
								'before' => $before,
								'after' => $i['item_comments']
							];
						}
						break;

					case 'item_favorites' :
						$before = (isset($items_table[$id]))
							? $items_table[$id]['item_favorites']
							: 0;
						if ($before != $val)
						{
							$report['item_favorites'] =
							[
								'before' => $before,
								'after' => $i['item_favorites']
							];
						}
						break;

					case 'item_rating' :
						$before = (isset($items_table[$id]))
							? round((float) $items_table[$id]['item_rating'], 8)
							: 0;
						if ($before != round((float) $val, 8))
						{
							$report['item_rating'] =
							[
								'before' => (isset($items_table[$id]))
									? $items_table[$id]['item_rating']
									: 0,
								'after' => $i['item_rating']
							];
						}
						break;

					case 'item_votes' :
						$before = (isset($items_table[$id]))
							? $items_table[$id]['item_votes']
							: 0;
						if ($before != $val)
						{
							$report['item_votes'] =
							[
								'before' => $before,
								'after' => $i['item_votes']
							];
						}
						break;
				}
			}

			self::$updateStatsReport['items'][] =
			[
				'id' => $i['item_id'],
				'path' => $i['item_path'],
				'type' => 'item',
				'report' => $report
			];

			unset($i['item_path']);
		}

		// On met à jour la base de données.
		sort($params);
		$sql = 'UPDATE {items}
				   SET item_comments = :item_comments,
					   item_votes = :item_votes,
				       item_rating = :item_rating,
					   item_favorites = :item_favorites
				 WHERE item_id = :item_id';
		if (!DB::execute($sql, $params))
		{
			return FALSE;
		}

		return TRUE;
	}
}
?>