<?php

/**
 * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */
namespace OC\Files\Cache;

use OC\Files\Cache\Wrapper\CacheJail;
use OC\Files\Search\QueryOptimizer\QueryOptimizer;
use OC\Files\Search\SearchBinaryOperator;
use OC\SystemConfig;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\Cache\ICache;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\IMimeTypeLoader;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\Search\ISearchBinaryOperator;
use OCP\Files\Search\ISearchQuery;
use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\FilesMetadata\IMetadataQuery;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUser;
use Psr\Log\LoggerInterface;

class QuerySearchHelper {
	public function __construct(
		private IMimeTypeLoader $mimetypeLoader,
		private IDBConnection $connection,
		private SystemConfig $systemConfig,
		private LoggerInterface $logger,
		private SearchBuilder $searchBuilder,
		private QueryOptimizer $queryOptimizer,
		private IGroupManager $groupManager,
		private IFilesMetadataManager $filesMetadataManager,
	) {
	}

	protected function getQueryBuilder() {
		return new CacheQueryBuilder(
			$this->connection->getQueryBuilder(),
			$this->filesMetadataManager,
		);
	}

	/**
	 * @param CacheQueryBuilder $query
	 * @param ISearchQuery $searchQuery
	 * @param array $caches
	 * @param IMetadataQuery|null $metadataQuery
	 */
	protected function applySearchConstraints(
		CacheQueryBuilder $query,
		ISearchQuery $searchQuery,
		array $caches,
		?IMetadataQuery $metadataQuery = null,
	): void {
		$storageFilters = array_values(array_map(function (ICache $cache) {
			return $cache->getQueryFilterForStorage();
		}, $caches));
		$storageFilter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $storageFilters);
		$filter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$searchQuery->getSearchOperation(), $storageFilter]);
		$this->queryOptimizer->processOperator($filter);

		$searchExpr = $this->searchBuilder->searchOperatorToDBExpr($query, $filter, $metadataQuery);
		if ($searchExpr) {
			$query->andWhere($searchExpr);
		}

		$this->searchBuilder->addSearchOrdersToQuery($query, $searchQuery->getOrder(), $metadataQuery);

		if ($searchQuery->getLimit()) {
			$query->setMaxResults($searchQuery->getLimit());
		}
		if ($searchQuery->getOffset()) {
			$query->setFirstResult($searchQuery->getOffset());
		}
	}


	/**
	 * @return array<array-key, array{id: int, name: string, visibility: int, editable: int, ref_file_id: int, number_files: int}>
	 */
	public function findUsedTagsInCaches(ISearchQuery $searchQuery, array $caches): array {
		$query = $this->getQueryBuilder();
		$query->selectTagUsage();

		$this->applySearchConstraints($query, $searchQuery, $caches);

		$result = $query->executeQuery();
		$tags = $result->fetchAll();
		$result->closeCursor();
		return $tags;
	}

	protected function equipQueryForSystemTags(CacheQueryBuilder $query, IUser $user): void {
		$query->leftJoin('file', 'systemtag_object_mapping', 'systemtagmap', $query->expr()->andX(
			$query->expr()->eq('file.fileid', $query->expr()->castColumn('systemtagmap.objectid', IQueryBuilder::PARAM_INT)),
			$query->expr()->eq('systemtagmap.objecttype', $query->createNamedParameter('files'))
		));
		$on = $query->expr()->andX($query->expr()->eq('systemtag.id', 'systemtagmap.systemtagid'));
		if (!$this->groupManager->isAdmin($user->getUID())) {
			$on->add($query->expr()->eq('systemtag.visibility', $query->createNamedParameter(true)));
		}
		$query->leftJoin('systemtagmap', 'systemtag', 'systemtag', $on);
	}

	protected function equipQueryForDavTags(CacheQueryBuilder $query, IUser $user): void {
		$query
			->leftJoin('file', 'vcategory_to_object', 'tagmap', $query->expr()->eq('file.fileid', 'tagmap.objid'))
			->leftJoin('tagmap', 'vcategory', 'tag', $query->expr()->andX(
				$query->expr()->eq('tagmap.categoryid', 'tag.id'),
				$query->expr()->eq('tag.type', $query->createNamedParameter('files')),
				$query->expr()->eq('tag.uid', $query->createNamedParameter($user->getUID()))
			));
	}


	protected function equipQueryForShares(CacheQueryBuilder $query): void {
		$query->join('file', 'share', 's', $query->expr()->eq('file.fileid', 's.file_source'));
	}

	/**
	 * Perform a file system search in multiple caches
	 *
	 * the results will be grouped by the same array keys as the $caches argument to allow
	 * post-processing based on which cache the result came from
	 *
	 * @template T of array-key
	 * @param ISearchQuery $searchQuery
	 * @param array<T, ICache> $caches
	 * @return array<T, ICacheEntry[]>
	 */
	public function searchInCaches(ISearchQuery $searchQuery, array $caches): array {
		// search in multiple caches at once by creating one query in the following format
		// SELECT ... FROM oc_filecache WHERE
		//     <filter expressions from the search query>
		// AND (
		//     <filter expression for storage1> OR
		//     <filter expression for storage2> OR
		//     ...
		// );
		//
		// This gives us all the files matching the search query from all caches
		//
		// while the resulting rows don't have a way to tell what storage they came from (multiple storages/caches can share storage_id)
		// we can just ask every cache if the row belongs to them and give them the cache to do any post processing on the result.

		$builder = $this->getQueryBuilder();

		$query = $builder->selectFileCache('file', false);

		$requestedFields = $this->searchBuilder->extractRequestedFields($searchQuery->getSearchOperation());

		if (in_array('systemtag', $requestedFields)) {
			$this->equipQueryForSystemTags($query, $this->requireUser($searchQuery));
		}
		if (in_array('tagname', $requestedFields) || in_array('favorite', $requestedFields)) {
			$this->equipQueryForDavTags($query, $this->requireUser($searchQuery));
		}
		if (in_array('owner', $requestedFields) || in_array('share_with', $requestedFields) || in_array('share_type', $requestedFields)) {
			$this->equipQueryForShares($query);
		}

		$metadataQuery = $query->selectMetadata();

		$this->applySearchConstraints($query, $searchQuery, $caches, $metadataQuery);

		$result = $query->executeQuery();
		$files = $result->fetchAll();

		$rawEntries = array_map(function (array $data) use ($metadataQuery) {
			$data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
			return Cache::cacheEntryFromData($data, $this->mimetypeLoader);
		}, $files);

		$result->closeCursor();

		// loop through all caches for each result to see if the result matches that storage
		// results are grouped by the same array keys as the caches argument to allow the caller to distinguish the source of the results
		$results = array_fill_keys(array_keys($caches), []);
		foreach ($rawEntries as $rawEntry) {
			foreach ($caches as $cacheKey => $cache) {
				$entry = $cache->getCacheEntryFromSearchResult($rawEntry);
				if ($entry) {
					$results[$cacheKey][] = $entry;
				}
			}
		}
		return $results;
	}

	protected function requireUser(ISearchQuery $searchQuery): IUser {
		$user = $searchQuery->getUser();
		if ($user === null) {
			throw new \InvalidArgumentException('This search operation requires the user to be set in the query');
		}
		return $user;
	}

	/**
	 * @return list{0?: array<array-key, ICache>, 1?: array<array-key, IMountPoint>}
	 */
	public function getCachesAndMountPointsForSearch(IRootFolder $root, string $path, bool $limitToHome = false): array {
		$rootLength = strlen($path);
		$mount = $root->getMount($path);
		$storage = $mount->getStorage();
		if ($storage === null) {
			return [];
		}
		$internalPath = $mount->getInternalPath($path);

		if ($internalPath !== '') {
			// a temporary CacheJail is used to handle filtering down the results to within this folder
			/** @var ICache[] $caches */
			$caches = ['' => new CacheJail($storage->getCache(''), $internalPath)];
		} else {
			/** @var ICache[] $caches */
			$caches = ['' => $storage->getCache('')];
		}
		/** @var IMountPoint[] $mountByMountPoint */
		$mountByMountPoint = ['' => $mount];

		if (!$limitToHome) {
			$mounts = $root->getMountsIn($path);
			foreach ($mounts as $mount) {
				$storage = $mount->getStorage();
				if ($storage) {
					$relativeMountPoint = ltrim(substr($mount->getMountPoint(), $rootLength), '/');
					$caches[$relativeMountPoint] = $storage->getCache('');
					$mountByMountPoint[$relativeMountPoint] = $mount;
				}
			}
		}

		return [$caches, $mountByMountPoint];
	}
}
