<?php
/**
 * @author Viktar Dubiniuk <dubiniuk@owncloud.com>
 *
 * @copyright Copyright (c) 2018, ownCloud GmbH
 * @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 OCA\FederatedFileSharing;

use OCA\FederatedFileSharing\Ocm\Permissions;
use OCA\Files_Sharing\Activity;
use OCP\Activity\IManager as ActivityManager;
use OCP\Files\NotFoundException;
use OCP\IUserManager;
use OCP\Notification\IManager as NotificationManager;
use OCP\Share\IShare;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;

/**
 * Class FedShareManager holds the share logic
 *
 * @package OCA\FederatedFileSharing
 */
class FedShareManager {
	public const ACTION_URL = 'ocs/v1.php/apps/files_sharing/api/v1/remote_shares/pending/';

	/**
	 * @var FederatedShareProvider
	 */
	private $federatedShareProvider;

	/**
	 * @var Notifications
	 */
	private $notifications;

	/**
	 * @var IUserManager
	 */
	private $userManager;

	/**
	 * @var ActivityManager
	 */
	private $activityManager;

	/**
	 * @var NotificationManager
	 */
	private $notificationManager;

	/**
	 * @var AddressHandler
	 */
	private $addressHandler;

	/**
	 * @var EventDispatcherInterface
	 */
	private $eventDispatcher;

	/**
	 * FedShareManager constructor.
	 *
	 * @param FederatedShareProvider $federatedShareProvider
	 * @param Notifications $notifications
	 * @param IUserManager $userManager
	 * @param ActivityManager $activityManager
	 * @param NotificationManager $notificationManager
	 * @param AddressHandler $addressHandler
	 * @param EventDispatcherInterface $eventDispatcher
	 */
	public function __construct(
		FederatedShareProvider $federatedShareProvider,
		Notifications $notifications,
		IUserManager $userManager,
		ActivityManager $activityManager,
		NotificationManager $notificationManager,
		AddressHandler $addressHandler,
		EventDispatcherInterface $eventDispatcher
	) {
		$this->federatedShareProvider = $federatedShareProvider;
		$this->notifications = $notifications;
		$this->userManager = $userManager;
		$this->activityManager = $activityManager;
		$this->notificationManager = $notificationManager;
		$this->addressHandler = $addressHandler;
		$this->eventDispatcher = $eventDispatcher;
	}

	/**
	 * Create an incoming share
	 *
	 * @param Address $ownerAddress
	 * @param Address $sharedByAddress
	 * @param string $shareWith
	 * @param string $remoteId
	 * @param string $name
	 * @param string $token
	 *
	 * @return void
	 */
	public function createShare(
		Address $ownerAddress,
		Address $sharedByAddress,
		$shareWith,
		$remoteId,
		$name,
		$token
	) {
		$owner = $ownerAddress->getUserId();
		$remote = $ownerAddress->getOrigin();
		$shareId = $this->federatedShareProvider->addShare(
			$remote,
			$token,
			$name,
			$owner,
			$shareWith,
			$remoteId
		);

		$this->eventDispatcher->dispatch(
			new GenericEvent(
				null,
				[
					'name' => $name,
					'targetuser' => $sharedByAddress->getCloudId(),
					'owner' => $owner,
					'sharewith' => $shareWith,
					'sharedby' => $sharedByAddress->getUserId(),
					'remoteid' => $remoteId
				]
			),
			'\OCA\FederatedFileSharing::remote_shareReceived'
		);
		$this->publishActivity(
			$shareWith,
			Activity::SUBJECT_REMOTE_SHARE_RECEIVED,
			[$ownerAddress->getCloudId(), \trim($name, '/'), ['shareId' => $shareId]],
			'files',
			'',
			'',
			''
		);
		$link = $this->getActionLink($shareId);
		$params = [
			$ownerAddress->getCloudId(),
			$sharedByAddress->getCloudId(),
			\trim($name, '/')
		];
		if (!$this->federatedShareProvider->getAccepted($remote, $shareWith)) {
			$notification = $this->createNotification($shareWith);
			$notification->setDateTime(new \DateTime())
				->setObject('remote_share', $shareId)
				->setSubject('remote_share', $params)
				->setMessage('remote_share', $params);
			$declineAction = $notification->createAction();
			$declineAction->setLabel('decline')
				->setLink($link, 'DELETE');
			$notification->addAction($declineAction);
			$acceptAction = $notification->createAction();
			$acceptAction->setLabel('accept')
				->setLink($link, 'POST');
			$notification->addAction($acceptAction);
			$this->notificationManager->notify($notification);
		}
	}

	/**
	 * @param IShare $share
	 * @param string $remoteId
	 * @param string $shareWith
	 * @param int|null $permissions - null for OCM 1.0-proposal1
	 *
	 * @return IShare
	 *
	 * @throws \OCP\Share\Exceptions\ShareNotFound
	 */
	public function reShare(IShare $share, $remoteId, $shareWith, $permissions = null) {
		if ($permissions !== null) {
			$share->setPermissions($share->getPermissions() & $permissions);
		}
		// the recipient of the initial share is now the initiator for the re-share
		$share->setSharedBy($share->getSharedWith());
		$share->setSharedWith($shareWith);
		$result = $this->federatedShareProvider->create($share);
		$this->federatedShareProvider->storeRemoteId(
			(int)$result->getId(),
			$remoteId
		);
		return $result;
	}

	/**
	 *
	 *
	 * @param IShare $share
	 *
	 * @throws \OCP\Files\InvalidPathException
	 * @throws \OCP\Files\NotFoundException
	 */
	public function acceptShare(IShare $share) {
		$uid = $this->getCorrectUid($share);
		$fileId = $share->getNode()->getId();
		list($file, $link) = $this->getFile($uid, $fileId);
		$this->publishActivity(
			$uid,
			Activity::SUBJECT_REMOTE_SHARE_ACCEPTED,
			[$share->getSharedWith(), \basename($file)],
			'files',
			$fileId,
			$file,
			$link
		);
		$this->notifyRemote($share, [$this->notifications, 'sendAcceptShare']);
	}

	/**
	 * Delete declined share and create a activity
	 *
	 * @param IShare $share
	 *
	 * @throws \OCP\Files\InvalidPathException
	 * @throws \OCP\Files\NotFoundException
	 */
	public function declineShare(IShare $share) {
		$this->notifyRemote($share, [$this->notifications, 'sendDeclineShare']);
		$uid = $this->getCorrectUid($share);
		$fileId = $share->getNode()->getId();
		$this->federatedShareProvider->removeShareFromTable($share);
		list($file, $link) = $this->getFile($uid, $fileId);
		$this->publishActivity(
			$uid,
			Activity::SUBJECT_REMOTE_SHARE_DECLINED,
			[$share->getSharedWith(), \basename($file)],
			'files',
			$fileId,
			$file,
			$link
		);
	}

	/**
	 * Unshare an item from self
	 *
	 * @param int $id
	 * @param string $token
	 *
	 * @return void
	 */
	public function unshare($id, $token) {
		$shareRow = $this->federatedShareProvider->unshare($id, $token);
		if ($shareRow === false) {
			return;
		}
		$ownerAddress = new Address($shareRow['owner'] . '@' . $shareRow['remote']);
		$mountpoint = $shareRow['mountpoint'];
		$user = $shareRow['user'];
		if ($shareRow['accepted']) {
			$path = \trim($mountpoint, '/');
		} else {
			$path = \trim($shareRow['name'], '/');
		}
		$notification = $this->createNotification($user);
		$notification->setObject('remote_share', (int) $shareRow['id']);
		$this->notificationManager->markProcessed($notification);
		$this->publishActivity(
			$user,
			Activity::SUBJECT_REMOTE_SHARE_UNSHARED,
			[$ownerAddress->getCloudId(), $path],
			'files',
			'',
			'',
			''
		);
	}

	/**
	 * @param IShare $share
	 *
	 * @return void
	 */
	public function undoReshare(IShare $share) {
		$this->federatedShareProvider->removeShareFromTable($share);
	}

	/**
	 * Update permissions
	 *
	 * @param IShare $share
	 * @param string[] $ocmPermissions as ['read', 'write', 'share']
	 *
	 * @return void
	 */
	public function updateOcmPermissions(IShare $share, $ocmPermissions) {
		$permissions = Permissions::toOcPermissions($ocmPermissions);
		$this->updatePermissions($share, $permissions);
	}

	/**
	 * Update permissions
	 *
	 * @param IShare $share
	 * @param int $permissions
	 *
	 * @return void
	 */
	public function updatePermissions(IShare $share, int $permissions): void {
		# permissions can only be reduced but not upgraded
		if (!Permissions::isNewPermissionHigher($share->getPermissions(), $permissions)) {
			$share->setPermissions($permissions);
			$this->federatedShareProvider->update($share);
		}
	}

	/**
	 * Check if a federated share was re-shared with another federated server.
	 *
	 * @param IShare $share
	 * @return bool
	 * @throws NotFoundException
	 */
	public function isFederatedReShare(IShare $share) {
		// get all federated shares on this file
		$shares = $this->federatedShareProvider->getSharesByPath($share->getNode());

		foreach ($shares as $matchingShare) {
			// if the share initiator (sharedBy) received a share for this file
			// in the past, this is a re-share
			if ($share->getSharedBy() === $matchingShare->getSharedWith()) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @param IShare $share
	 * @param callable $callback
	 *
	 * @throws \OCP\Share\Exceptions\ShareNotFound
	 * @throws \OC\HintException
	 */
	protected function notifyRemote($share, $callback) {
		if ($share->getShareOwner() !== $share->getSharedBy()) {
			try {
				list(, $remote) = $this->addressHandler->splitUserRemote(
					$share->getSharedBy()
				);
				$remoteId = $this->federatedShareProvider->getRemoteId($share);
				$callback($remote, $remoteId, $share->getToken());
			} catch (\Exception $e) {
				// expected fail if sender is a local user
			}
		}
	}

	/**
	 * Publish a new activity
	 *
	 * @param string $affectedUser
	 * @param string $subject
	 * @param array $subjectParams
	 * @param string $objectType
	 * @param int $objectId
	 * @param string $objectName
	 * @param string $link
	 *
	 * @return void
	 */
	protected function publishActivity(
		$affectedUser,
		$subject,
		$subjectParams,
		$objectType,
		$objectId,
		$objectName,
		$link
	) {
		$event = $this->activityManager->generateEvent();
		$event->setApp(Activity::FILES_SHARING_APP)
			->setType(Activity::TYPE_REMOTE_SHARE)
			->setAffectedUser($affectedUser)
			->setSubject($subject, $subjectParams)
			->setObject($objectType, $objectId, $objectName)
			->setLink($link);
		$this->activityManager->publish($event);
	}

	/**
	 * Get a new notification
	 *
	 * @param string $uid
	 *
	 * @return \OCP\Notification\INotification
	 */
	protected function createNotification($uid) {
		$notification = $this->notificationManager->createNotification();
		$notification->setApp('files_sharing');
		$notification->setUser($uid);
		return $notification;
	}

	/**
	 * @param int $shareId
	 * @return string
	 */
	protected function getActionLink($shareId) {
		$urlGenerator = \OC::$server->getURLGenerator();
		return $urlGenerator->linkTo('', self::ACTION_URL . $shareId);
	}

	/**
	 * Get file
	 *
	 * @param string $user
	 * @param int $fileSource
	 *
	 * @return array with internal path of the file and a absolute link to it
	 */
	protected function getFile($user, $fileSource) {
		\OC_Util::setupFS($user);

		try {
			$file = \OC\Files\Filesystem::getPath($fileSource);
		} catch (NotFoundException $e) {
			$file = null;
		}
		// FIXME:  use permalink here, see ViewController for reference
		$args = \OC\Files\Filesystem::is_dir($file)
			? ['dir' => $file]
			: ['dir' => \dirname($file), 'scrollto' => $file];
		$link = \OCP\Util::linkToAbsolute('files', 'index.php', $args);

		return [$file, $link];
	}

	/**
	 * Check if we are the initiator or the owner of a re-share
	 * and return the correct UID
	 *
	 * @param IShare $share
	 *
	 * @return string
	 */
	protected function getCorrectUid(IShare $share) {
		if ($this->userManager->userExists($share->getShareOwner())) {
			return $share->getShareOwner();
		}

		return $share->getSharedBy();
	}
}
