<?php

/**
 * @file classes/context/SubEditorsDAO.php
 *
 * Copyright (c) 2014-2021 Simon Fraser University
 * Copyright (c) 2003-2021 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class SubEditorsDAO
 *
 * @ingroup context
 *
 * @brief Base class associating sections, series and categories to sub editors.
 */

namespace PKP\context;

use APP\core\Application;
use APP\facades\Repo;
use APP\notification\Notification;
use APP\notification\NotificationManager;
use APP\submission\Submission;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use PKP\db\DAORegistry;
use PKP\log\SubmissionEmailLogDAO;
use PKP\log\SubmissionEmailLogEntry;
use PKP\mail\mailables\EditorAssigned;
use PKP\notification\NotificationSubscriptionSettingsDAO;
use PKP\security\Role;
use PKP\stageAssignment\StageAssignment;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\userGroup\UserGroup;

class SubEditorsDAO extends \PKP\db\DAO
{
    /**
     * Insert a new sub editor.
     *
     * @param int $contextId
     * @param int $assocId
     * @param int $userId
     */
    public function insertEditor($contextId, $assocId, $userId, $assocType, int $userGroupId)
    {
        return $this->update(
            'INSERT INTO subeditor_submission_group
				(context_id, assoc_id, user_id, assoc_type, user_group_id)
				VALUES
				(?, ?, ?, ?, ?)',
            [
                (int) $contextId,
                (int) $assocId,
                (int) $userId,
                (int) $assocType,
                $userGroupId,
            ]
        );
    }

    /**
     * Delete a sub editor.
     *
     * @param int $contextId
     * @param int $assocId
     * @param int $userId
     * @param int $assocType Application::ASSOC_TYPE_SECTION or Application::ASSOC_TYPE_CATEGORY
     */
    public function deleteEditor($contextId, $assocId, $userId, $assocType)
    {
        $this->update(
            'DELETE FROM subeditor_submission_group WHERE context_id = ? AND section_id = ? AND user_id = ? AND assoc_type = ?',
            [
                (int) $contextId,
                (int) $assocId,
                (int) $userId,
                (int) $assocType,
            ]
        );
    }

    /**
     * Retrieve a list of all sub editors assigned to the specified submission group.
     *
     * @param int[] $assocIds Section or category ids
     * @param int $assocType Application::ASSOC_TYPE_SECTION or Application::ASSOC_TYPE_CATEGORY
     *
     * @return Collection result rows with userId and userGroupId columns
     */
    public function getBySubmissionGroupIds(array $assocIds, int $assocType, int $contextId): Collection
    {
        return DB::table('subeditor_submission_group')
            ->where('assoc_type', '=', $assocType)
            ->where('context_id', '=', $contextId)
            ->whereIn('assoc_id', $assocIds)
            ->get(['user_id as userId', 'user_group_id as userGroupId']);
    }

    /**
     * Delete all sub editors for a specified submission group in a context.
     *
     * @param int $assocId
     * @param int $assocType Application::ASSOC_TYPE_SECTION or Application::ASSOC_TYPE_CATEGORY
     * @param int $contextId
     */
    public function deleteBySubmissionGroupId($assocId, $assocType, $contextId = null)
    {
        $params = [(int) $assocId, (int) $assocType];
        if ($contextId) {
            $params[] = (int) $contextId;
        }
        $this->update(
            'DELETE FROM subeditor_submission_group WHERE assoc_id = ? AND assoc_type = ?' .
            ($contextId ? ' AND context_id = ?' : ''),
            $params
        );
    }

    /**
     * Delete all submission group assignments for the specified user.
     */
    public function deleteByUserId(int $userId)
    {
        /**
         * This warning was added in 3.4 due to a change in the function signature.
         * It can be removed with the next LTS release.
         *
         * @deprecated 3.4
         */
        if (func_num_args() !== 1) {
            throw new Exception('Invalid number of arguments passed to ' . self::class . '::' . __FUNCTION__);
        }

        DB::table('subeditor_submission_group')
            ->where('user_id', '=', $userId)
            ->delete();
    }

    /**
     * Delete all submission group assignments for a user group
     */
    public function deleteByUserGroupId(int $userGroupId)
    {
        DB::table('subeditor_submission_group')
            ->where('user_group_id', '=', $userGroupId)
            ->delete();
    }

    /**
     * Check if a user is assigned to a specified submission group.
     *
     * @param int $contextId
     * @param int $assocId
     * @param int $userId
     * @param int $assocType optional Application::ASSOC_TYPE_SECTION or Application::ASSOC_TYPE_CATEGORY
     *
     * @return bool
     */
    public function editorExists($contextId, $assocId, $userId, $assocType)
    {
        $result = $this->retrieve(
            'SELECT COUNT(*) AS row_count FROM subeditor_submission_group WHERE context_id = ? AND section_id = ? AND user_id = ? AND assoc_id = ?',
            [(int) $contextId, (int) $assocId, (int) $userId, (int) $assocType]
        );
        $row = $result->current();
        return $row ? (bool) $row->row_count : false;
    }

    /**
     * Assign editors to a submission
     *
     * Creates a stage assignment for each editorial user
     * configured in the section and category settings.
     *
     * @return Collection The user ids for editors that were assigned
     */
    public function assignEditors(Submission $submission, Context $context): Collection
    {
        $publication = $submission->getCurrentPublication();
        $sectionIdPropName = Application::getSectionIdPropName();

        $assignments = $this->getBySubmissionGroupIds(
            [$publication->getData($sectionIdPropName)],
            Application::ASSOC_TYPE_SECTION,
            $submission->getData('contextId')
        );

        if (!empty($publication->getData('categoryIds'))) {
            $assignedToCategory = $this->getBySubmissionGroupIds(
                $publication->getData('categoryIds'),
                Application::ASSOC_TYPE_CATEGORY,
                $submission->getData('contextId')
            );
            $assignments = $assignments->merge($assignedToCategory);
        }

        // Remove duplicate assignments for the same user in the
        // same user group by structuring the array with a key
        // that will cause duplicates to be overwritten
        $assignments = collect($assignments)->mapWithKeys(fn ($assignment, $key) => [$assignment->userId . '-' . $assignment->userGroupId => $assignment]);

        $userGroups = Repo::userGroup()
            ->getCollector()
            ->filterByContextIds([$submission->getData('contextId')])
            ->getMany();

        $userGroupIds = $userGroups->keys();

        $assignments = $assignments->filter(function ($assignment) use ($userGroupIds) {
            return Repo::userGroup()->userInGroup($assignment->userId, $assignment->userGroupId)
                && $userGroupIds->contains($assignment->userGroupId);
        });

        /** @var StageAssignmentDAO $stageAssignmentDao */
        $stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO');
        foreach ($assignments as $assignment) {
            $userGroup = $userGroups->first(fn (UserGroup $userGroup) => $userGroup->getId() == $assignment->userGroupId);
            $stageAssignmentDao->build($submission->getId(), $assignment->userGroupId, $assignment->userId, $userGroup->getRecommendOnly());
        }

        // Update assignment notifications
        $notificationManager = new NotificationManager();
        $notificationManager->updateNotification(
            Application::get()->getRequest(),
            $notificationManager->getDecisionStageNotifications(),
            null,
            Application::ASSOC_TYPE_SUBMISSION,
            $submission->getId()
        );

        // Send a notification to assigned users
        foreach ($assignments as $assignment) {
            $notificationManager->createNotification(
                Application::get()->getRequest(),
                $assignment->userId,
                Notification::NOTIFICATION_TYPE_SUBMISSION_SUBMITTED,
                $submission->getContextId(),
                Application::ASSOC_TYPE_SUBMISSION,
                $submission->getId()
            );
        }

        // Send an email to assigned editors
        $editorAssignments = $stageAssignmentDao->getBySubmissionAndRoleIds(
            $submission->getId(),
            [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
            WORKFLOW_STAGE_ID_SUBMISSION
        )->toArray();

        $emailTemplate = Repo::emailTemplate()->getByKey($context->getId(), EditorAssigned::getEmailTemplateKey());
        if (count($editorAssignments) && $emailTemplate) {
            // Never notify the same user twice, even if they are assigned in multiple roles
            $notifiedEditors = [];

            /** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
            $notificationSubscriptionSettingsDao = DAORegistry::getDAO('NotificationSubscriptionSettingsDAO');
            $mailable = new EditorAssigned($context, $submission);
            $mailable
                ->from($context->getData('contactEmail'), $context->getData('contactName'))
                ->subject($emailTemplate->getLocalizedData('subject') ?? '')
                ->body($emailTemplate->getLocalizedData('body') ?? '');

            /** @var StageAssignment $editorAssignment */
            foreach ($editorAssignments as $editorAssignment) {
                $unsubscribed = in_array(
                    Notification::NOTIFICATION_TYPE_SUBMISSION_SUBMITTED,
                    $notificationSubscriptionSettingsDao->getNotificationSubscriptionSettings(
                        NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY,
                        $editorAssignment->getUserId(),
                        $context->getId()
                    )
                );

                if ($unsubscribed || in_array($editorAssignment->getUserId(), $notifiedEditors)) {
                    continue;
                }

                $notifiedEditors[] = $editorAssignment->getUserId();

                $recipient = Repo::user()->get($editorAssignment->getUserId());
                $mailable->recipients([$recipient]);

                Mail::send($mailable);

                /** @var SubmissionEmailLogDAO $logDao */
                $logDao = DAORegistry::getDAO('SubmissionEmailLogDAO');
                $logDao->logMailable(
                    SubmissionEmailLogEntry::SUBMISSION_EMAIL_EDITOR_ASSIGN,
                    $mailable,
                    $submission
                );
            }
        }

        return $assignments->map(fn ($assignment) => $assignment->userId);
    }

    /**
     * Get the section assigned sub editor's associated user groups ids for given section
     *
     * @return  Collection Collection A list of user group IDs for each user, keyed by user ID.
     */
    public function getAssignedUserGroupIds(int $contextId, int $assocType, int $assocId, int|array $userIds): Collection
    {
        return DB::table('subeditor_submission_group')
            ->select(['user_id', 'user_group_id'])
            ->where('assoc_type', $assocType)
            ->where('context_id', $contextId)
            ->where('assoc_id', $assocId)
            ->whereIn('user_id', Arr::wrap($userIds))
            ->get()
            ->groupBy('user_id')
            ->map(fn ($userGroups) => $userGroups->pluck('user_group_id'));
    }
}

if (!PKP_STRICT_MODE) {
    class_alias('\PKP\context\SubEditorsDAO', '\SubEditorsDAO');
}
