<?php

/**
 * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
 * SPDX-License-Identifier: AGPL-3.0-only
 */
namespace OC\Files\Cache;

use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\Storage\IStorage;
use OCP\IDBConnection;
use Psr\Log\LoggerInterface;

/**
 * Handle the mapping between the string and numeric storage ids
 *
 * Each storage has 2 different ids
 * 	a string id which is generated by the storage backend and reflects the configuration of the storage (e.g. 'smb://user@host/share')
 * 	and a numeric storage id which is referenced in the file cache
 *
 * A mapping between the two storage ids is stored in the database and accessible through this class
 *
 * @package OC\Files\Cache
 */
class Storage {
	/** @var StorageGlobal|null */
	private static $globalCache = null;
	private $storageId;
	private $numericId;

	/**
	 * @return StorageGlobal
	 */
	public static function getGlobalCache() {
		if (is_null(self::$globalCache)) {
			self::$globalCache = new StorageGlobal(\OC::$server->getDatabaseConnection());
		}
		return self::$globalCache;
	}

	/**
	 * @param \OC\Files\Storage\Storage|string $storage
	 * @param bool $isAvailable
	 * @throws \RuntimeException
	 */
	public function __construct($storage, $isAvailable, IDBConnection $connection) {
		if ($storage instanceof IStorage) {
			$this->storageId = $storage->getId();
		} else {
			$this->storageId = $storage;
		}
		$this->storageId = self::adjustStorageId($this->storageId);

		if ($row = self::getStorageById($this->storageId)) {
			$this->numericId = (int)$row['numeric_id'];
		} else {
			$available = $isAvailable ? 1 : 0;
			if ($connection->insertIfNotExist('*PREFIX*storages', ['id' => $this->storageId, 'available' => $available])) {
				$this->numericId = $connection->lastInsertId('*PREFIX*storages');
			} else {
				if ($row = self::getStorageById($this->storageId)) {
					$this->numericId = (int)$row['numeric_id'];
				} else {
					throw new \RuntimeException('Storage could neither be inserted nor be selected from the database: ' . $this->storageId);
				}
			}
		}
	}

	/**
	 * @param string $storageId
	 * @return array
	 */
	public static function getStorageById($storageId) {
		return self::getGlobalCache()->getStorageInfo($storageId);
	}

	/**
	 * Adjusts the storage id to use md5 if too long
	 * @param string $storageId storage id
	 * @return string unchanged $storageId if its length is less than 64 characters,
	 *                else returns the md5 of $storageId
	 */
	public static function adjustStorageId($storageId) {
		if (strlen($storageId) > 64) {
			return md5($storageId);
		}
		return $storageId;
	}

	/**
	 * Get the numeric id for the storage
	 *
	 * @return int
	 */
	public function getNumericId() {
		return $this->numericId;
	}

	/**
	 * Get the string id for the storage
	 *
	 * @param int $numericId
	 * @return string|null either the storage id string or null if the numeric id is not known
	 */
	public static function getStorageId(int $numericId): ?string {
		$storage = self::getGlobalCache()->getStorageInfoByNumericId($numericId);
		return $storage['id'] ?? null;
	}

	/**
	 * Get the numeric of the storage with the provided string id
	 *
	 * @param $storageId
	 * @return int|null either the numeric storage id or null if the storage id is not known
	 */
	public static function getNumericStorageId($storageId) {
		$storageId = self::adjustStorageId($storageId);

		if ($row = self::getStorageById($storageId)) {
			return (int)$row['numeric_id'];
		} else {
			return null;
		}
	}

	/**
	 * @return array [ available, last_checked ]
	 */
	public function getAvailability() {
		if ($row = self::getStorageById($this->storageId)) {
			return [
				'available' => (int)$row['available'] === 1,
				'last_checked' => $row['last_checked']
			];
		} else {
			return [
				'available' => true,
				'last_checked' => time(),
			];
		}
	}

	/**
	 * @param bool $isAvailable
	 * @param int $delay amount of seconds to delay reconsidering that storage further
	 */
	public function setAvailability($isAvailable, int $delay = 0) {
		$available = $isAvailable ? 1 : 0;
		if (!$isAvailable) {
			\OCP\Server::get(LoggerInterface::class)->info('Storage with ' . $this->storageId . ' marked as unavailable', ['app' => 'lib']);
		}

		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
		$query->update('storages')
			->set('available', $query->createNamedParameter($available))
			->set('last_checked', $query->createNamedParameter(time() + $delay))
			->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
		$query->executeStatement();
	}

	/**
	 * Check if a string storage id is known
	 *
	 * @param string $storageId
	 * @return bool
	 */
	public static function exists($storageId) {
		return !is_null(self::getNumericStorageId($storageId));
	}

	/**
	 * remove the entry for the storage
	 *
	 * @param string $storageId
	 */
	public static function remove($storageId) {
		$storageId = self::adjustStorageId($storageId);
		$numericId = self::getNumericStorageId($storageId);

		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
		$query->delete('storages')
			->where($query->expr()->eq('id', $query->createNamedParameter($storageId)));
		$query->executeStatement();

		if (!is_null($numericId)) {
			$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
			$query->delete('filecache')
				->where($query->expr()->eq('storage', $query->createNamedParameter($numericId)));
			$query->executeStatement();
		}
	}

	/**
	 * remove the entry for the storage by the mount id
	 *
	 * @param int $mountId
	 */
	public static function cleanByMountId(int $mountId) {
		$db = \OC::$server->getDatabaseConnection();

		try {
			$db->beginTransaction();

			$query = $db->getQueryBuilder();
			$query->select('storage_id')
				->from('mounts')
				->where($query->expr()->eq('mount_id', $query->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
			$storageIds = $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
			$storageIds = array_unique($storageIds);

			$query = $db->getQueryBuilder();
			$query->delete('filecache')
				->where($query->expr()->in('storage', $query->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY)));
			$query->runAcrossAllShards();
			$query->executeStatement();

			$query = $db->getQueryBuilder();
			$query->delete('storages')
				->where($query->expr()->in('numeric_id', $query->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY)));
			$query->executeStatement();

			$query = $db->getQueryBuilder();
			$query->delete('mounts')
				->where($query->expr()->eq('mount_id', $query->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
			$query->executeStatement();

			$db->commit();
		} catch (\Exception $e) {
			$db->rollBack();
			throw $e;
		}
	}
}
