<?php

declare(strict_types=1);

/**
 * @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
 *
 * @author Julius Härtl <jus@bitgrid.net>
 *
 * @license GNU AGPL version 3 or any later version
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  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
 *  along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 */

namespace OCA\Deck\Notification;

use DateTime;
use Exception;
use OCA\Deck\AppInfo\Application;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\User;
use OCA\Deck\Service\ConfigService;
use OCA\Deck\Service\PermissionService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\Comments\IComment;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\Notification\IManager;
use OCP\Notification\INotification;

class NotificationHelper {

	/** @var CardMapper */
	protected $cardMapper;
	/** @var BoardMapper */
	protected $boardMapper;
	/** @var AssignmentMapper */
	protected $assignmentMapper;
	/** @var PermissionService */
	protected $permissionService;
	/** @var IConfig */
	protected $config;
	/** @var IManager */
	protected $notificationManager;
	/** @var IGroupManager */
	protected $groupManager;
	/** @var string */
	protected $currentUser;
	/** @var array */
	private $boards = [];

	public function __construct(
		CardMapper $cardMapper,
		BoardMapper $boardMapper,
		AssignmentMapper $assignmentMapper,
		PermissionService $permissionService,
		IConfig $config,
		IManager $notificationManager,
		IGroupManager $groupManager,
		$userId
	) {
		$this->cardMapper = $cardMapper;
		$this->boardMapper = $boardMapper;
		$this->assignmentMapper = $assignmentMapper;
		$this->permissionService = $permissionService;
		$this->config = $config;
		$this->notificationManager = $notificationManager;
		$this->groupManager = $groupManager;
		$this->currentUser = $userId;
	}

	/**
	 * @throws DoesNotExistException
	 * @throws Exception thrown on invalid due date
	 */
	public function sendCardDuedate(Card $card): void {
		// check if notification has already been sent
		// ideally notifications should not be deleted once seen by the user so we can
		// also deliver due date notifications for users who have been added later to a board
		// this should maybe be addressed in nextcloud/server
		if ($card->getNotified()) {
			return;
		}

		$boardId = $this->cardMapper->findBoardId($card->getId());
		$board = $this->getBoard($boardId, false, true);
		/** @var User $user */
		foreach ($this->permissionService->findUsers($boardId) as $user) {
			$notificationSetting = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'board:' . $boardId . ':notify-due', ConfigService::SETTING_BOARD_NOTIFICATION_DUE_DEFAULT);

			if ($notificationSetting === ConfigService::SETTING_BOARD_NOTIFICATION_DUE_OFF) {
				continue;
			}

			$shouldNotify = $notificationSetting === ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ALL;

			if ($user->getUID() === $board->getOwner() && count($board->getAcl()) === 0) {
				// Notify if all or assigned is configured for unshared boards
				$shouldNotify = true;
			} elseif ($notificationSetting === ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED && $this->assignmentMapper->isUserAssigned($card->getId(), $user->getUID())) {
				// Notify if the user is assigned and has the assigned setting selected
				$shouldNotify = true;
			}

			if ($shouldNotify) {
				$notification = $this->notificationManager->createNotification();
				$notification
					->setApp('deck')
					->setUser((string)$user->getUID())
					->setObject('card', (string)$card->getId())
					->setSubject('card-overdue', [
						$card->getTitle(), $board->getTitle()
					])
					->setDateTime($card->getDuedate());
				$this->notificationManager->notify($notification);
			}
		}
		$this->cardMapper->markNotified($card);
	}

	public function markDuedateAsRead(Card $card): void {
		$notification = $this->notificationManager->createNotification();
		$notification
			->setApp('deck')
			->setObject('card', (string)$card->getId())
			->setSubject('card-overdue', []);
		$this->notificationManager->markProcessed($notification);
	}

	public function sendCardAssigned(Card $card, string $userId): void {
		$boardId = $this->cardMapper->findBoardId($card->getId());
		try {
			$board = $this->getBoard($boardId);
		} catch (Exception $e) {
			return;
		}

		$notification = $this->notificationManager->createNotification();
		$notification
			->setApp('deck')
			->setUser($userId)
			->setDateTime(new DateTime())
			->setObject('card', (string)$card->getId())
				->setSubject('card-assigned', [
					$card->getTitle(),
					$board->getTitle(),
					$this->currentUser
				]);
		$this->notificationManager->notify($notification);
	}

	public function markCardAssignedAsRead(Card $card, string $userId): void {
		$notification = $this->notificationManager->createNotification();
		$notification
			->setApp('deck')
			->setUser($userId)
			->setObject('card', (string)$card->getId())
			->setSubject('card-assigned', []);
		$this->notificationManager->markProcessed($notification);
	}

	/**
	 * Send notifications that a board was shared with a user/group
	 */
	public function sendBoardShared(int $boardId, Acl $acl, bool $markAsRead = false): void {
		try {
			$board = $this->getBoard($boardId);
		} catch (Exception $e) {
			return;
		}

		if ($acl->getType() === Acl::PERMISSION_TYPE_USER) {
			$notification = $this->generateBoardShared($board, $acl->getParticipant());
			if ($markAsRead) {
				$this->notificationManager->markProcessed($notification);
			} else {
				$notification->setDateTime(new DateTime());
				$this->notificationManager->notify($notification);
			}
		}
		if ($acl->getType() === Acl::PERMISSION_TYPE_GROUP) {
			$group = $this->groupManager->get($acl->getParticipant());
			if ($group === null) {
				return;
			}
			foreach ($group->getUsers() as $user) {
				if ($user->getUID() === $this->currentUser) {
					continue;
				}
				$notification = $this->generateBoardShared($board, $user->getUID());
				if ($markAsRead) {
					$this->notificationManager->markProcessed($notification);
				} else {
					$notification->setDateTime(new DateTime());
					$this->notificationManager->notify($notification);
				}
			}
		}
	}

	public function sendMention(IComment $comment): void {
		foreach ($comment->getMentions() as $mention) {
			$card = $this->cardMapper->find($comment->getObjectId());
			$boardId = $this->cardMapper->findBoardId($card->getId());
			$notification = $this->notificationManager->createNotification();
			$notification
				->setApp('deck')
				->setUser((string) $mention['id'])
				->setDateTime(new DateTime())
				->setObject('card', (string) $card->getId())
				->setSubject('card-comment-mentioned', [$card->getTitle(), $boardId, $this->currentUser])
				->setMessage('{message}', ['message' => $comment->getMessage()]);
			$this->notificationManager->notify($notification);
		}
	}

	/**
	 * @throws DoesNotExistException
	 * @throws MultipleObjectsReturnedException
	 */
	private function getBoard(int $boardId, bool $withLabels = false, bool $withAcl = false): Board {
		if (!array_key_exists($boardId, $this->boards)) {
			$this->boards[$boardId] = $this->boardMapper->find($boardId, $withLabels, $withAcl);
		}
		return $this->boards[$boardId];
	}

	private function generateBoardShared(Board $board, string $userId): INotification {
		$notification = $this->notificationManager->createNotification();
		$notification
			->setApp('deck')
			->setUser($userId)
			->setObject('board', (string)$board->getId())
			->setSubject('board-shared', [$board->getTitle(), $this->currentUser]);
		return $notification;
	}
}
