<?php

/**
 * @file classes/notification/PKPNotificationOperationManager.php
 *
 * Copyright (c) 2014-2021 Simon Fraser University
 * Copyright (c) 2000-2021 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class PKPNotificationOperationManager
 *
 * @ingroup notification
 *
 * @see NotificationDAO
 * @see Notification
 *
 * @brief Base class for notification manager that implements
 * basic notification operations and default notifications info.
 * Subclasses can implement specific information.
 */

namespace PKP\notification;

use APP\core\Application;
use APP\core\Request;
use APP\notification\Notification;
use APP\template\TemplateManager;
use Firebase\JWT\JWT;
use InvalidArgumentException;
use PKP\config\Config;
use PKP\core\Core;
use PKP\core\PKPApplication;
use PKP\core\PKPRequest;
use PKP\db\DAORegistry;
use PKP\facades\Locale;
use PKP\linkAction\LinkAction;

abstract class PKPNotificationOperationManager implements INotificationInfoProvider
{
    //
    // Implement INotificationInfoProvider with default values.
    //
    /**
     * @copydoc INotificationInfoProvider::getNotificationUrl()
     */
    public function getNotificationUrl($request, $notification)
    {
        return null;
    }

    /**
     * @copydoc INotificationInfoProvider::getNotificationMessage()
     */
    public function getNotificationMessage($request, $notification)
    {
        return null;
    }

    /**
     * Provide the notification message as default content.
     *
     * @copydoc INotificationInfoProvider::getNotificationContents()
     */
    public function getNotificationContents($request, $notification)
    {
        return $this->getNotificationMessage($request, $notification);
    }

    /**
     * @copydoc INotificationInfoProvider::getNotificationTitle()
     */
    public function getNotificationTitle($notification)
    {
        return __('notification.notification');
    }

    /**
     * @copydoc INotificationInfoProvider::getStyleClass()
     */
    public function getStyleClass($notification)
    {
        return '';
    }

    /**
     * @copydoc INotificationInfoProvider::getIconClass()
     */
    public function getIconClass($notification)
    {
        return '';
    }

    /**
     * @copydoc INotificationInfoProvider::isVisibleToAllUsers()
     */
    public function isVisibleToAllUsers($notificationType, $assocType, $assocId)
    {
        return false;
    }


    //
    // Notification manager operations.
    //
    /**
     * Iterate through the localized params for a notification's locale key.
     *  For each parameter, return (in preferred order) a value for the user's current locale,
     *  a param for the journal's default locale, or the first value (in case the value
     *  is not localized)
     *
     * @param array $params
     *
     * @return array
     */
    public function getParamsForCurrentLocale($params)
    {
        $locale = Locale::getLocale();
        $primaryLocale = Locale::getPrimaryLocale();

        $localizedParams = [];
        foreach ($params as $name => $value) {
            if (!is_array($value)) {
                // Non-localized text
                $localizedParams[$name] = $value;
            } elseif (isset($value[$locale])) {
                // Check if the parameter is in the user's current locale
                $localizedParams[$name] = $value[$locale];
            } elseif (isset($value[$primaryLocale])) {
                // Check if the parameter is in the default site locale
                $localizedParams[$name] = $value[$primaryLocale];
            } else {
                $context = Application::get()->getRequest()->getContext();
                // Otherwise, iterate over all supported locales and return the first match
                $locales = $context->getSupportedLocaleNames();
                foreach ($locales as $localeKey) {
                    if (isset($value[$localeKey])) {
                        $localizedParams[$name] = $value[$localeKey];
                    }
                }
            }
        }

        return $localizedParams;
    }

    /**
     * Create a new notification with the specified arguments and insert into DB
     *
     * @param ?PKPRequest $request
     * @param int $userId (optional)
     * @param int $notificationType
     * @param int $contextId
     * @param int $assocType
     * @param int $assocId
     * @param int $level
     * @param array $params
     *
     * @return ?Notification
     */
    public function createNotification($request, $userId = null, $notificationType = null, $contextId = null, $assocType = null, $assocId = null, $level = Notification::NOTIFICATION_LEVEL_NORMAL, $params = null)
    {
        $blockedNotifications = $this->getUserBlockedNotifications($userId, $contextId);

        if (!in_array($notificationType, $blockedNotifications)) {
            $notificationDao = DAORegistry::getDAO('NotificationDAO'); /** @var NotificationDAO $notificationDao */
            $notification = $notificationDao->newDataObject(); /** @var Notification $notification */
            $notification->setUserId((int) $userId);
            $notification->setType((int) $notificationType);
            $notification->setContextId((int) $contextId);
            $notification->setAssocType((int) $assocType);
            $notification->setAssocId((int) $assocId);
            $notification->setLevel((int) $level);

            $notificationId = $notificationDao->insertObject($notification);

            if ($params) {
                $notificationSettingsDao = DAORegistry::getDAO('NotificationSettingsDAO'); /** @var NotificationSettingsDAO $notificationSettingsDao */
                foreach ($params as $name => $value) {
                    $notificationSettingsDao->updateNotificationSetting($notificationId, $name, $value);
                }
            }

            return $notification;
        }
    }

    /**
     * Create a new notification with the specified arguments and insert into DB
     * This is a static method
     *
     * @param int $userId
     * @param int $notificationType
     * @param array $params
     *
     * @return Notification object
     */
    public function createTrivialNotification($userId, $notificationType = PKPNotification::NOTIFICATION_TYPE_SUCCESS, $params = null)
    {
        $notificationDao = DAORegistry::getDAO('NotificationDAO'); /** @var NotificationDAO $notificationDao */
        $notification = $notificationDao->newDataObject();
        $notification->setUserId($userId);
        $notification->setContextId(PKPApplication::CONTEXT_ID_NONE);
        $notification->setType($notificationType);
        $notification->setLevel(Notification::NOTIFICATION_LEVEL_TRIVIAL);

        $notificationId = $notificationDao->insertObject($notification);

        if ($params) {
            $notificationSettingsDao = DAORegistry::getDAO('NotificationSettingsDAO'); /** @var NotificationSettingsDAO $notificationSettingsDao */
            foreach ($params as $name => $value) {
                $notificationSettingsDao->updateNotificationSetting($notificationId, $name, $value);
            }
        }

        return $notification;
    }

    /**
     * Deletes trivial notifications from database.
     *
     * @param array $notifications
     */
    public function deleteTrivialNotifications($notifications)
    {
        $notificationDao = DAORegistry::getDAO('NotificationDAO'); /** @var NotificationDAO $notificationDao */
        foreach ($notifications as $notification) {
            // Delete only trivial notifications.
            if ($notification->getLevel() == Notification::NOTIFICATION_LEVEL_TRIVIAL) {
                $notificationDao->deleteById($notification->getId(), $notification->getUserId());
            }
        }
    }

    /**
     * General notification data formatting.
     *
     * @param PKPRequest $request
     * @param array $notifications
     *
     * @return array
     */
    public function formatToGeneralNotification($request, $notifications)
    {
        $formattedNotificationsData = [];
        foreach ($notifications as $notification) { /** @var Notification $notification */
            $formattedNotificationsData[$notification->getLevel()][$notification->getId()] = [
                'title' => $this->getNotificationTitle($notification),
                'text' => $this->getNotificationContents($request, $notification),
                'addclass' => $this->getStyleClass($notification),
                'notice_icon' => $this->getIconClass($notification),
                'styling' => 'jqueryui',
            ];
        }

        return $formattedNotificationsData;
    }

    /**
     * In place notification data formating.
     *
     * @param PKPRequest $request
     * @param array $notifications
     *
     * @return array
     */
    public function formatToInPlaceNotification($request, $notifications)
    {
        $formattedNotificationsData = null;

        if (!empty($notifications)) {
            $templateMgr = TemplateManager::getManager($request);
            foreach ((array)$notifications as $notification) {
                $formattedNotificationsData[$notification->getLevel()][$notification->getId()] = $this->formatNotification($request, $notification, 'controllers/notification/inPlaceNotificationContent.tpl');
            }
        }

        return $formattedNotificationsData;
    }

    /**
     * Get set of notifications types user does not want to be notified of.
     *
     * @param int $userId The notification user
     * @param int $contextId
     *
     * @return array
     */
    protected function getUserBlockedNotifications($userId, $contextId)
    {
        $notificationSubscriptionSettingsDao = DAORegistry::getDAO('NotificationSubscriptionSettingsDAO'); /** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
        return $notificationSubscriptionSettingsDao->getNotificationSubscriptionSettings(NotificationSubscriptionSettingsDAO::BLOCKED_NOTIFICATION_KEY, $userId, (int) $contextId);
    }

    /**
     * Get set of notification types user will also be notified by email.
     *
     * @return array
     */
    protected function getUserBlockedEmailedNotifications($userId, $contextId)
    {
        $notificationSubscriptionSettingsDao = DAORegistry::getDAO('NotificationSubscriptionSettingsDAO'); /** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
        return $notificationSubscriptionSettingsDao->getNotificationSubscriptionSettings(NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY, $userId, (int) $contextId);
    }

    /**
     * Get a notification content with a link action.
     *
     * @param LinkAction $linkAction
     * @param Request $request
     *
     * @return string
     */
    protected function fetchLinkActionNotificationContent($linkAction, $request)
    {
        $templateMgr = TemplateManager::getManager($request);
        $templateMgr->assign('linkAction', $linkAction);
        return $templateMgr->fetch('controllers/notification/linkActionNotificationContent.tpl');
    }

    /**
     * Return a fully formatted notification for display
     *
     * @param PKPRequest $request
     * @param object $notification Notification
     *
     * @return string
     */
    private function formatNotification($request, $notification, $notificationTemplate)
    {
        $templateMgr = TemplateManager::getManager($request);

        // Set the date read if it isn't already set
        if (!$notification->getDateRead()) {
            $notificationDao = DAORegistry::getDAO('NotificationDAO'); /** @var NotificationDAO $notificationDao */
            $dateRead = $notificationDao->setDateRead($notification->getId(), Core::getCurrentDate());
            $notification->setDateRead($dateRead);
        }

        $user = $request->getUser();
        $templateMgr->assign([
            'isUserLoggedIn' => $user,
            'notificationDateCreated' => $notification->getDateCreated(),
            'notificationId' => $notification->getId(),
            'notificationContents' => $this->getNotificationContents($request, $notification),
            'notificationTitle' => $this->getNotificationTitle($notification),
            'notificationStyleClass' => $this->getStyleClass($notification),
            'notificationIconClass' => $this->getIconClass($notification),
            'notificationDateRead' => $notification->getDateRead(),
        ]);

        if ($notification->getLevel() != Notification::NOTIFICATION_LEVEL_TRIVIAL) {
            $templateMgr->assign('notificationUrl', $this->getNotificationUrl($request, $notification));
        }

        return $templateMgr->fetch($notificationTemplate);
    }

    /**
     * Creates and returns a unique string for the given notification, that will be encoded and validated against.
     *
     * @param Notification $notification
     *
     * @return string
     */
    public function createUnsubscribeUniqueKey($notification)
    {
        $uniqueKey = 'unsubscribe' . '-' . $notification->getContextId() . '-' . $notification->getUserId() . '-' . $notification->getId();

        return $uniqueKey;
    }

    /**
     * Creates and returns an encoded token that will be used to validate an unsubscribe url.
     *
     * @param Notification $notification
     *
     * @return string
     */
    public function createUnsubscribeToken($notification)
    {
        $encodeString = $this->createUnsubscribeUniqueKey($notification);

        $secret = Config::getVar('security', 'api_key_secret', '');
        $jwt = '';
        if ($secret !== '') {
            $jwt = JWT::encode(json_encode($encodeString), $secret, 'HS256');
        }

        return $jwt;
    }

    /**
     * The given notification is validated against the requested token.
     *
     * @param string $token
     * @param Notification $notification
     *
     * @return bool
     */
    public function validateUnsubscribeToken($token, $notification)
    {
        $encodeString = $this->createUnsubscribeUniqueKey($notification);

        $secret = Config::getVar('security', 'api_key_secret', '');
        $jwt = '';
        if ($secret !== '') {
            $jwt = json_decode(JWT::decode($token, $secret, ['HS256']));
        }

        if ($jwt == $encodeString) {
            return true;
        }

        return false;
    }

    /**
     * Returns the unsubscribe url for the given notification.
     *
     * @param PKPRequest $request
     * @param Notification $notification
     * @param null|mixed $context
     *
     * @return string
     */
    public function getUnsubscribeNotificationUrl($request, $notification, $context = null)
    {
        $application = Application::get();
        $dispatcher = $application->getDispatcher();
        $contextPath = null;
        if ($context) {
            if ($context->getId() !== $notification->getContextId()) {
                throw new InvalidArgumentException('Trying to build notification unsubscribe URL with the wrong context');
            }
            $contextPath = $context->getData('urlPath');
        }

        return $dispatcher->url(
            $request,
            PKPApplication::ROUTE_PAGE,
            $contextPath,
            'notification',
            'unsubscribe',
            null,
            ['validate' => $this->createUnsubscribeToken($notification), 'id' => $notification->getId()]
        );
    }
}

if (!PKP_STRICT_MODE) {
    class_alias('\PKP\notification\PKPNotificationOperationManager', '\PKPNotificationOperationManager');
}
