<?php
/**
 * @copyright Copyright (c) 2016, ownCloud, Inc.
 *
 * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
 * @author Christoph Wurst <christoph@winzerhof-wurst.at>
 * @author Lukas Reschke <lukas@statuscode.ch>
 * @author Robin Appelman <robin@icewind.nl>
 * @author Robin McCorkell <robin@mccorkell.me.uk>
 * @author Roeland Jago Douma <roeland@famdouma.nl>
 *
 * @license AGPL-3.0
 *
 * This code is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * 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, version 3,
 * along with this program. If not, see <http://www.gnu.org/licenses/>
 *
 */
namespace OC\Files\Storage\Wrapper;

use OCP\Files\Storage\IStorage;
use OCP\Files\StorageAuthException;
use OCP\Files\StorageNotAvailableException;
use OCP\IConfig;

/**
 * Availability checker for storages
 *
 * Throws a StorageNotAvailableException for storages with known failures
 */
class Availability extends Wrapper {
	public const RECHECK_TTL_SEC = 600; // 10 minutes

	/** @var IConfig */
	protected $config;

	public function __construct($parameters) {
		$this->config = $parameters['config'] ?? \OC::$server->getConfig();
		parent::__construct($parameters);
	}

	public static function shouldRecheck($availability) {
		if (!$availability['available']) {
			// trigger a recheck if TTL reached
			if ((time() - $availability['last_checked']) > self::RECHECK_TTL_SEC) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Only called if availability === false
	 *
	 * @return bool
	 */
	private function updateAvailability() {
		// reset availability to false so that multiple requests don't recheck concurrently
		$this->setAvailability(false);
		try {
			$result = $this->test();
		} catch (\Exception $e) {
			$result = false;
		}
		$this->setAvailability($result);
		return $result;
	}

	/**
	 * @return bool
	 */
	private function isAvailable() {
		$availability = $this->getAvailability();
		if (self::shouldRecheck($availability)) {
			return $this->updateAvailability();
		}
		return $availability['available'];
	}

	/**
	 * @throws StorageNotAvailableException
	 */
	private function checkAvailability() {
		if (!$this->isAvailable()) {
			throw new StorageNotAvailableException();
		}
	}

	/** {@inheritdoc} */
	public function mkdir($path) {
		$this->checkAvailability();
		try {
			return parent::mkdir($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function rmdir($path) {
		$this->checkAvailability();
		try {
			return parent::rmdir($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function opendir($path) {
		$this->checkAvailability();
		try {
			return parent::opendir($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function is_dir($path) {
		$this->checkAvailability();
		try {
			return parent::is_dir($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function is_file($path) {
		$this->checkAvailability();
		try {
			return parent::is_file($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function stat($path) {
		$this->checkAvailability();
		try {
			return parent::stat($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function filetype($path) {
		$this->checkAvailability();
		try {
			return parent::filetype($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function filesize($path): false|int|float {
		$this->checkAvailability();
		try {
			return parent::filesize($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function isCreatable($path) {
		$this->checkAvailability();
		try {
			return parent::isCreatable($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function isReadable($path) {
		$this->checkAvailability();
		try {
			return parent::isReadable($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function isUpdatable($path) {
		$this->checkAvailability();
		try {
			return parent::isUpdatable($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function isDeletable($path) {
		$this->checkAvailability();
		try {
			return parent::isDeletable($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function isSharable($path) {
		$this->checkAvailability();
		try {
			return parent::isSharable($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function getPermissions($path) {
		$this->checkAvailability();
		try {
			return parent::getPermissions($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function file_exists($path) {
		if ($path === '') {
			return true;
		}
		$this->checkAvailability();
		try {
			return parent::file_exists($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function filemtime($path) {
		$this->checkAvailability();
		try {
			return parent::filemtime($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function file_get_contents($path) {
		$this->checkAvailability();
		try {
			return parent::file_get_contents($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function file_put_contents($path, $data) {
		$this->checkAvailability();
		try {
			return parent::file_put_contents($path, $data);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function unlink($path) {
		$this->checkAvailability();
		try {
			return parent::unlink($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function rename($source, $target) {
		$this->checkAvailability();
		try {
			return parent::rename($source, $target);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function copy($source, $target) {
		$this->checkAvailability();
		try {
			return parent::copy($source, $target);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function fopen($path, $mode) {
		$this->checkAvailability();
		try {
			return parent::fopen($path, $mode);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function getMimeType($path) {
		$this->checkAvailability();
		try {
			return parent::getMimeType($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function hash($type, $path, $raw = false) {
		$this->checkAvailability();
		try {
			return parent::hash($type, $path, $raw);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
			return false;
		}
	}

	/** {@inheritdoc} */
	public function free_space($path) {
		$this->checkAvailability();
		try {
			return parent::free_space($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function search($query) {
		$this->checkAvailability();
		try {
			return parent::search($query);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function touch($path, $mtime = null) {
		$this->checkAvailability();
		try {
			return parent::touch($path, $mtime);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function getLocalFile($path) {
		$this->checkAvailability();
		try {
			return parent::getLocalFile($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function hasUpdated($path, $time) {
		if (!$this->isAvailable()) {
			return false;
		}
		try {
			return parent::hasUpdated($path, $time);
		} catch (StorageNotAvailableException $e) {
			// set unavailable but don't rethrow
			$this->setUnavailable(null);
			return false;
		}
	}

	/** {@inheritdoc} */
	public function getOwner($path) {
		try {
			return parent::getOwner($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function getETag($path) {
		$this->checkAvailability();
		try {
			return parent::getETag($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function getDirectDownload($path) {
		$this->checkAvailability();
		try {
			return parent::getDirectDownload($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
		$this->checkAvailability();
		try {
			return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
		$this->checkAvailability();
		try {
			return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/** {@inheritdoc} */
	public function getMetaData($path) {
		$this->checkAvailability();
		try {
			return parent::getMetaData($path);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}

	/**
	 * @template T of StorageNotAvailableException|null
	 * @param T $e
	 * @psalm-return (T is null ? void : never)
	 * @throws StorageNotAvailableException
	 */
	protected function setUnavailable(?StorageNotAvailableException $e): void {
		$delay = self::RECHECK_TTL_SEC;
		if ($e instanceof StorageAuthException) {
			$delay = max(
				// 30min
				$this->config->getSystemValueInt('external_storage.auth_availability_delay', 1800),
				self::RECHECK_TTL_SEC
			);
		}
		$this->getStorageCache()->setAvailability(false, $delay);
		if ($e !== null) {
			throw $e;
		}
	}



	public function getDirectoryContent($directory): \Traversable {
		$this->checkAvailability();
		try {
			return parent::getDirectoryContent($directory);
		} catch (StorageNotAvailableException $e) {
			$this->setUnavailable($e);
		}
	}
}
