<?php

/**
 * @file api/v1/submissions/PKPSubmissionHandler.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 PKPSubmissionHandler
 *
 * @ingroup api_v1_submission
 *
 * @brief Handle API requests for submission operations.
 *
 */

namespace PKP\API\v1\submissions;

use APP\core\Application;
use APP\core\Request;
use APP\core\Services;
use APP\facades\Repo;
use APP\mail\variables\ContextEmailVariable;
use APP\notification\Notification;
use APP\notification\NotificationManager;
use APP\section\Section;
use APP\submission\Collector;
use APP\submission\Submission;
use Illuminate\Support\Enumerable;
use Illuminate\Support\Facades\Mail;
use PKP\core\APIResponse;
use PKP\core\Core;
use PKP\core\PKPApplication;
use PKP\db\DAORegistry;
use PKP\decision\DecisionType;
use PKP\handler\APIHandler;
use PKP\log\event\PKPSubmissionEventLogEntry;
use PKP\mail\mailables\PublicationVersionNotify;
use PKP\mail\mailables\SubmissionSavedForLater;
use PKP\notification\NotificationSubscriptionSettingsDAO;
use PKP\notification\PKPNotification;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\DecisionWritePolicy;
use PKP\security\authorization\PublicationWritePolicy;
use PKP\security\authorization\StageRolePolicy;
use PKP\security\authorization\SubmissionAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\authorization\internal\SubmissionCompletePolicy;
use PKP\security\Role;
use PKP\security\Validation;
use PKP\services\PKPSchemaService;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\submission\GenreDAO;
use PKP\submission\PKPSubmission;
use PKP\submission\reviewAssignment\ReviewAssignment;
use PKP\userGroup\UserGroup;
use Slim\Http\Request as SlimRequest;

class PKPSubmissionHandler extends APIHandler
{
    /** @var int The default number of items to return in one request */
    public const DEFAULT_COUNT = 30;

    /** @var int Max items that can be requested */
    public const MAX_COUNT = 100;

    /** @var array Handlers that must be authorized to access a submission */
    public $requiresSubmissionAccess = [
        'get',
        'edit',
        'saveForLater',
        'submit',
        'delete',
        'getGalleys',
        'getDecisions',
        'getParticipants',
        'getPublications',
        'getPublication',
        'addPublication',
        'versionPublication',
        'editPublication',
        'publishPublication',
        'unpublishPublication',
        'deletePublication',
        'getContributors',
        'getContributor',
        'addContributor',
        'deleteContributor',
        'editContributor',
        'saveContributorsOrder',
        'addDecision',
    ];

    /** @var array Handlers that must be authorized to write to a publication */
    public $requiresPublicationWriteAccess = [
        'editPublication',
        'addContributor',
        'deleteContributor',
        'editContributor',
        'saveContributorsOrder',
    ];

    /** @var array Handlers that must be authorized to access a submission's production stage */
    public $requiresProductionStageAccess = [
        'addPublication',
        'versionPublication',
        'publishPublication',
        'unpublishPublication',
        'deletePublication',
    ];

    /** @var array Roles that can access a submission's production stage */
    public $productionStageAccessRoles = [
        Role::ROLE_ID_MANAGER,
        Role::ROLE_ID_SUB_EDITOR,
        Role::ROLE_ID_ASSISTANT
    ];

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->_handlerPath = 'submissions';
        $this->_endpoints = [
            'GET' => [
                [
                    'pattern' => $this->getEndpointPattern(),
                    'handler' => [$this, 'getMany'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_REVIEWER, Role::ROLE_ID_AUTHOR],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}',
                    'handler' => [$this, 'get'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_REVIEWER, Role::ROLE_ID_AUTHOR],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/decisions',
                    'handler' => [$this, 'getDecisions'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/participants',
                    'handler' => [$this, 'getParticipants'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/participants/{stageId:\d+}',
                    'handler' => [$this, 'getParticipants'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/publications',
                    'handler' => [$this, 'getPublications'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_REVIEWER, Role::ROLE_ID_AUTHOR],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/publications/{publicationId:\d+}',
                    'handler' => [$this, 'getPublication'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_REVIEWER, Role::ROLE_ID_AUTHOR],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/publications/{publicationId:\d+}/contributors',
                    'handler' => [$this, 'getContributors'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_REVIEWER, Role::ROLE_ID_AUTHOR],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/publications/{publicationId:\d+}/contributors/{contributorId:\d+}',
                    'handler' => [$this, 'getContributor'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_REVIEWER, Role::ROLE_ID_AUTHOR],
                ],
            ],
            'POST' => [
                [
                    'pattern' => $this->getEndpointPattern(),
                    'handler' => [$this, 'add'],
                    'roles' => Role::getAllRoles(),
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/publications',
                    'handler' => [$this, 'addPublication'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/publications/{publicationId:\d+}/version',
                    'handler' => [$this, 'versionPublication'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/publications/{publicationId:\d+}/contributors',
                    'handler' => [$this, 'addContributor'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/decisions',
                    'handler' => [$this, 'addDecision'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
                ],
            ],
            'PUT' => [
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}',
                    'handler' => [$this, 'edit'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_AUTHOR],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/saveForLater',
                    'handler' => [$this, 'saveForLater'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_AUTHOR],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/submit',
                    'handler' => [$this, 'submit'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_AUTHOR],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/publications/{publicationId:\d+}',
                    'handler' => [$this, 'editPublication'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/publications/{publicationId:\d+}/publish',
                    'handler' => [$this, 'publishPublication'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/publications/{publicationId:\d+}/unpublish',
                    'handler' => [$this, 'unpublishPublication'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/publications/{publicationId:\d+}/contributors/{contributorId:\d+}',
                    'handler' => [$this, 'editContributor'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/publications/{publicationId:\d+}/contributors/saveOrder',
                    'handler' => [$this, 'saveContributorsOrder'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
                ],
            ],
            'DELETE' => [
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}',
                    'handler' => [$this, 'delete'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/publications/{publicationId:\d+}',
                    'handler' => [$this, 'deletePublication'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT],
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/publications/{publicationId:\d+}/contributors/{contributorId:\d+}',
                    'handler' => [$this, 'deleteContributor'],
                    'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
                ],
            ],
        ];
        parent::__construct();
    }

    //
    // Implement methods from PKPHandler
    //
    public function authorize($request, &$args, $roleAssignments)
    {
        $routeName = $this->getSlimRequest()->getAttribute('route')->getName();

        $this->addPolicy(new UserRolesRequiredPolicy($request), true);

        $this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));

        if (in_array($routeName, $this->requiresSubmissionAccess)) {
            $this->addPolicy(new SubmissionAccessPolicy($request, $args, $roleAssignments));
        }

        if (in_array($routeName, $this->requiresPublicationWriteAccess)) {
            $this->addPolicy(new PublicationWritePolicy($request, $args, $roleAssignments));
        }

        if (in_array($routeName, $this->requiresProductionStageAccess)) {
            $this->addPolicy(new StageRolePolicy($this->productionStageAccessRoles, WORKFLOW_STAGE_ID_PRODUCTION, false));
        }

        if ($routeName === 'addDecision') {
            $this->addPolicy(new SubmissionCompletePolicy($request, $args));
            $this->addPolicy(new DecisionWritePolicy($request, $args, (int) $request->getUserVar('decision'), $request->getUser()));
        }

        return parent::authorize($request, $args, $roleAssignments);
    }

    /**
     * Get a collection of submissions
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function getMany($slimRequest, $response, $args)
    {
        $request = Application::get()->getRequest();
        $currentUser = $request->getUser();
        $context = $request->getContext();

        $collector = $this->getSubmissionCollector($slimRequest->getQueryParams());

        Hook::call('API::submissions::params', [$collector, $slimRequest]);

        // Prevent users from viewing submissions they're not assigned to,
        // except for journal managers and admins.
        $userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
        $canAccessUnassignedSubmission = !empty(array_intersect([Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER], $userRoles));
        if (!$canAccessUnassignedSubmission) {
            if (!is_array($collector->assignedTo)) {
                $collector->assignedTo([$currentUser->getId()]);
            } elseif ($collector->assignedTo != [$currentUser->getId()]) {
                return $response->withStatus(403)->withJsonError('api.submissions.403.requestedOthersUnpublishedSubmissions');
            }
        }

        $submissions = $collector->getMany();

        $userGroups = Repo::userGroup()->getCollector()
            ->filterByContextIds([$context->getId()])
            ->getMany();

        /** @var \PKP\submission\GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        $genres = $genreDao->getByContextId($context->getId())->toArray();

        return $response->withJson([
            'itemsMax' => $collector->getCount(),
            'items' => Repo::submission()->getSchemaMap()->summarizeMany($submissions, $userGroups, $genres)->values(),
        ], 200);
    }

    /**
     * Configure a submission Collector based on the query params
     */
    protected function getSubmissionCollector(array $queryParams): Collector
    {
        $request = Application::get()->getRequest();
        /** @var \PKP\context\Context $context */
        $context = $request->getContext();

        $collector = Repo::submission()->getCollector()
            ->filterByContextIds([$context->getId()])
            ->limit(self::DEFAULT_COUNT)
            ->offset(0);

        foreach ($queryParams as $param => $val) {
            switch ($param) {
                case 'orderBy':
                    if (in_array($val, [
                        $collector::ORDERBY_DATE_PUBLISHED,
                        $collector::ORDERBY_DATE_SUBMITTED,
                        $collector::ORDERBY_LAST_ACTIVITY,
                        $collector::ORDERBY_LAST_MODIFIED,
                        $collector::ORDERBY_SEQUENCE,
                        $collector::ORDERBY_TITLE,
                    ])) {
                        $direction = isset($queryParams['orderDirection']) && $queryParams['orderDirection'] === $collector::ORDER_DIR_ASC
                            ? $collector::ORDER_DIR_ASC
                            : $collector::ORDER_DIR_DESC;
                        $collector->orderBy($val, $direction);
                    }
                    break;

                case 'categoryIds':
                    $collector->filterByCategoryIds(array_map('intval', $this->paramToArray($val)));
                    break;

                case 'status':
                    $collector->filterByStatus(array_map('intval', $this->paramToArray($val)));
                    break;

                case 'stageIds':
                    $collector->filterByStageIds(array_map('intval', $this->paramToArray($val)));
                    break;

                case 'assignedTo':
                    $val = array_map('intval', $this->paramToArray($val));
                    if ($val == [\PKP\submission\Collector::UNASSIGNED]) {
                        $val = array_shift($val);
                    }
                    $collector->assignedTo($val);
                    break;

                case 'daysInactive':
                    $collector->filterByDaysInactive((int) $val);
                    break;

                case 'offset':
                    $collector->offset((int) $val);
                    break;

                case 'searchPhrase':
                    $collector->searchPhrase($val);
                    break;

                case 'count':
                    $collector->limit(min(self::MAX_COUNT, (int) $val));
                    break;

                case 'isIncomplete':
                    $collector->filterByIncomplete(true);
                    break;

                case 'isOverdue':
                    $collector->filterByOverdue(true);
                    break;
                case 'doiStatus':
                    $collector->filterByDoiStatuses(array_map('intval', $this->paramToArray($val)));
                    break;
                case 'hasDois':
                    $collector->filterByHasDois((bool) $val, $context->getEnabledDoiTypes());
                    break;
            }
        }

        return $collector;
    }

    /**
     * Get a single submission
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function get($slimRequest, $response, $args)
    {
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);

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

        /** @var GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();

        return $response->withJson(Repo::submission()->getSchemaMap()->map($submission, $userGroups, $genres), 200);
    }

    /**
     * Add a new submission
     *
     */
    public function add(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
    {
        $request = $this->getRequest();
        $context = $request->getContext();
        $user = $request->getUser();

        if ($context->getData('disableSubmissions')) {
            return $response->withStatus(403)->withJsonError('author.submit.notAccepting');
        }

        $params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_SUBMISSION, $slimRequest->getParsedBody());

        $readOnlyErrors = $this->getWriteDisabledErrors(PKPSchemaService::SCHEMA_SUBMISSION, $params);
        if (!empty($readOnlyErrors)) {
            return $response->withStatus(400)->withJson($readOnlyErrors);
        }

        $params['contextId'] = $context->getId();

        $errors = Repo::submission()->validate(null, $params, $context);

        $sectionIdPropName = Application::getSectionIdPropName();
        if (isset($params[$sectionIdPropName])) {
            $sectionId = $params[$sectionIdPropName];
            $section = Repo::section()->get($sectionId, $context->getId());
            if ($section->getIsInactive()) {
                $errors[$sectionIdPropName] = [__('api.submission.400.inactiveSection')];
            } else {
                if ($section->getEditorRestricted() && !$this->isEditor()) {
                    $errors[$sectionIdPropName] = [__('submission.sectionRestrictedToEditors')];
                }
            }
        }

        if (!empty($errors)) {
            return $response->withStatus(400)->withJson($errors);
        }

        $submitterUserGroups = Repo::userGroup()
            ->getCollector()
            ->filterByContextIds([$context->getId()])
            ->filterByUserIds([$user->getId()])
            ->filterByRoleIds([Role::ROLE_ID_MANAGER, Role::ROLE_ID_AUTHOR])
            ->getMany();

        if (isset($params['userGroupId'])) {
            $submitAsUserGroup = $submitterUserGroups
                ->first(function (UserGroup $userGroup) use ($params) {
                    return $userGroup->getId() === $params['userGroupId'];
                });
            if (!$submitAsUserGroup) {
                return $response->withStatus(400)->withJson([
                    'userGroupId' => [__('api.submissions.400.invalidSubmitAs')]
                ]);
            }
        } elseif ($submitterUserGroups->count()) {
            $submitAsUserGroup = $submitterUserGroups
                ->sort(function (UserGroup $a, UserGroup $b) {
                    return $a->getRoleId() === Role::ROLE_ID_AUTHOR ? 1 : -1;
                })
                ->first();
        } else {
            $submitAsUserGroup = Repo::userGroup()->getFirstSubmitAsAuthorUserGroup($context->getId());
            if (!$submitAsUserGroup) {
                return $response->withStatus(400)->withJson([
                    'userGroupId' => [__('submission.wizard.notAllowed.description')]
                ]);
            }
            Repo::userGroup()->assignUserToGroup(
                $user->getId(),
                $submitAsUserGroup->getId()
            );
        }

        $publicationProps = [];
        if (isset($params[$sectionIdPropName])) {
            $publicationProps[$sectionIdPropName] = $params[$sectionIdPropName];
            unset($params[$sectionIdPropName]);
        }

        $submission = Repo::submission()->newDataObject($params);
        $publication = Repo::publication()->newDataObject($publicationProps);
        $submissionId = Repo::submission()->add($submission, $publication, $request->getContext());

        $submission = Repo::submission()->get($submissionId);

        // Assign submitter to submission
        /** @var StageAssignmentDAO $stageAssignmentDao */
        $stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO');
        $stageAssignmentDao->build(
            $submission->getId(),
            $submitAsUserGroup->getId(),
            $request->getUser()->getId(),
            $submitAsUserGroup->getRecommendOnly(),
            // Authors can always edit metadata before submitting
            $submission->getData('submissionProgress')
                ? true
                : $submitAsUserGroup->getPermitMetadataEdit()
        );

        // Create an author record from the submitter's user account
        if ($submitAsUserGroup->getRoleId() === Role::ROLE_ID_AUTHOR) {
            $author = Repo::author()->newAuthorFromUser($request->getUser());
            $author->setData('publicationId', $publication->getId());
            $author->setUserGroupId($submitAsUserGroup->getId());
            $authorId = Repo::author()->add($author);
            Repo::publication()->edit($publication, ['primaryContactId' => $authorId]);
        }

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

        /** @var GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();

        return $response->withJson(Repo::submission()->getSchemaMap()->map($submission, $userGroups, $genres), 200);
    }

    /**
     * Edit a submission
     */
    public function edit(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
    {
        $request = $this->getRequest();
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);

        $parsedBody = $slimRequest->getParsedBody();

        if (isset($parsedBody)){
            $params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_SUBMISSION, $parsedBody);
    
            $readOnlyErrors = $this->getWriteDisabledErrors(PKPSchemaService::SCHEMA_SUBMISSION, $params);
            if (!empty($readOnlyErrors)) {
                return $response->withStatus(400)->withJson($readOnlyErrors);
            }
    
            $params['id'] = $submission->getId();
            $params['contextId'] = $request->getContext()->getId();
    
            $errors = Repo::submission()->validate($submission, $params, $request->getContext());
    
            if (!empty($errors)) {
                return $response->withStatus(400)->withJson($errors);
            }
    
            Repo::submission()->edit($submission, $params);
        }


        $submission = Repo::submission()->get($submission->getId());

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

        /** @var GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();

        return $response->withJson(Repo::submission()->getSchemaMap()->map($submission, $userGroups, $genres), 200);
    }

    /**
     * Save a submission for later
     *
     * Saves the current step and sends the submitter an
     * email with a link to resume their submission.
     */
    public function saveForLater(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
    {
        $request = $this->getRequest();
        $context = $request->getContext();
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);

        $params = $slimRequest->getParsedBody();
        if (!empty($params['step'])) {
            if (!ctype_alnum(str_replace(['-', '_'], '', $params['step']))) {
                return $response->withStatus(400)->withJson([
                    'step' => [__('validator.alpha_dash')]
                ]);
            }

            Repo::submission()->edit($submission, ['submissionProgress' => $params['step']]);
        }

        $emailTemplate = Repo::emailTemplate()->getByKey($context->getId(), SubmissionSavedForLater::getEmailTemplateKey());
        $mailable = new SubmissionSavedForLater($context, $submission);
        $mailable
            ->from($context->getData('contactEmail'), $context->getData('contactName'))
            ->recipients([$request->getUser()])
            // The template may not exist, see pkp/pkp-lib#9217
            ->subject($emailTemplate?->getLocalizedData('subject') ?? __('emails.submissionSavedForLater.subject'))
            ->body($emailTemplate?->getLocalizedData('body') ?? __('emails.submissionSavedForLater.body'));

        if (!$emailTemplate) {
            $templateVariables = $mailable->getData();
            $mailable->addData([
                'contextName' => $templateVariables[ContextEmailVariable::CONTEXT_NAME],
                'contextUrl' => $templateVariables[ContextEmailVariable::CONTEXT_URL],
            ]);
        }

        Mail::send($mailable);

        $submission = Repo::submission()->get($submission->getId());

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

        /** @var GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();

        return $response->withJson(Repo::submission()->getSchemaMap()->map($submission, $userGroups, $genres), 200);
    }

    /**
     * Submit a submission
     *
     * Submits a submission by changing its `submissionProgress` property.
     *
     * Pass the `_validateOnly` property to validate the submission without submitting it.
     */
    public function submit(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
    {
        $request = $this->getRequest();
        $context = $request->getContext();
        /** @var Submission $submission*/
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
        $publication = $submission->getCurrentPublication();

        $errors = Repo::submission()->validateSubmit($submission, $context);

        /** @var int $sectionId */
        $sectionId = $publication->getData(Application::getSectionIdPropName());

        if ($sectionId) {
            $section = Repo::section()->get($sectionId, $context->getId());
        }

        if (isset($section) &&
            (
                $section->getIsInactive() ||
                ($section->getEditorRestricted() && !$this->isEditor())
            )
        ) {
            $errors[Application::getSectionIdPropName()] = __('submission.wizard.sectionClosed.message', [
                'contextName' => $context->getLocalizedData('name'),
                'section' => $section->getLocalizedTitle(),
                'email' => $context->getData('contactEmail'),
                'name' => $context->getData('contactName'),
            ]);
        }

        if (!empty($errors)) {
            return $response->withStatus(400)->withJson($errors);
        }

        if ($slimRequest->getParsedBodyParam('_validateOnly')) {
            return $response->withStatus(200);
        }

        Repo::submission()->submit($submission, $context);

        $submission = Repo::submission()->get($submission->getId());

        if ($slimRequest->getParsedBodyParam('confirmCopyright')) {
            $user = $request->getUser();
            $eventLog = Repo::eventLog()->newDataObject([
                'assocType' => PKPApplication::ASSOC_TYPE_SUBMISSION,
                'assocId' => $submission->getId(),
                'eventType' => PKPSubmissionEventLogEntry::SUBMISSION_LOG_COPYRIGHT_AGREED,
                'userId' => Validation::loggedInAs() ?? $user->getId(),
                'message' => 'submission.event.copyrightAgreed',
                'isTranslated' => false,
                'dateLogged' => Core::getCurrentDate(),
                'username' => $user->getUsername(),
                'userFullName' => $user->getFullName(),
                'copyrightNotice' => $context->getData('copyrightNotice'),
            ]);

            Repo::eventLog()->add($eventLog);
        }


        $notificationManager = new NotificationManager();
        $notificationManager->updateNotification(
            Application::get()->getRequest(),
            [PKPNotification::NOTIFICATION_TYPE_APPROVE_SUBMISSION],
            null,
            PKPApplication::ASSOC_TYPE_SUBMISSION,
            $submission->getId()
        );

        $userGroups = Repo::userGroup()
            ->getCollector()
            ->filterByContextIds([$context->getId()])
            ->getMany();

        /** @var GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();

        return $response->withJson(Repo::submission()->getSchemaMap()->map($submission, $userGroups, $genres), 200);
    }

    /**
     * Delete a submission
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function delete($slimRequest, $response, $args)
    {
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);

        if (!$submission) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

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

        /** @var GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();

        $submissionProps = Repo::submission()->getSchemaMap()->map($submission, $userGroups, $genres);

        Repo::submission()->delete($submission);

        return $response->withJson($submissionProps, 200);
    }

    /**
     * Get the decisions recorded on a submission
     */
    public function getDecisions(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
    {
        $request = Application::get()->getRequest();
        $context = $request->getContext();
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);

        if (!$submission || $submission->getData('contextId') !== $context->getId()) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        $decisionIterator = Repo::decision()->getCollector()
            ->filterBySubmissionIds([$submission->getId()])
            ->getMany();

        $data = Repo::decision()
            ->getSchemaMap()
            ->mapMany($decisionIterator->values());

        return $response->withJson($data, 200);
    }

    /**
     * Get the participants assigned to a submission
     *
     * This does not return reviewers.
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function getParticipants($slimRequest, $response, $args)
    {
        $request = Application::get()->getRequest();
        $context = $request->getContext();
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
        $stageId = $args['stageId'] ?? null;

        if (!$submission || $submission->getData('contextId') !== $context->getId()) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        $data = [];

        $usersIterator = Repo::user()->getCollector()
            ->filterByContextIds([$context->getId()])
            ->assignedTo($submission->getId(), $stageId)
            ->getMany();

        $map = Repo::user()->getSchemaMap();
        foreach ($usersIterator as $user) {
            $data[] = $map->summarizeReviewer($user);
        }

        return $response->withJson($data, 200);
    }


    /**
     * Get all of this submissions's publications
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function getPublications($slimRequest, $response, $args)
    {
        $request = $this->getRequest();

        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);

        if (!$submission) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        $collector = Repo::publication()->getCollector()
            ->filterBySubmissionIds([$submission->getId()]);

        $publications = $collector->getMany();

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

        $reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var \PKP\submission\reviewAssignment\ReviewAssignmentDAO $reviewAssignmentDao */
        $currentUserReviewAssignment = $reviewAssignmentDao->getLastReviewRoundReviewAssignmentByReviewer(
            $submission->getId(),
            $request->getUser()->getId()
        );
        $anonymize = $currentUserReviewAssignment && $currentUserReviewAssignment->getReviewMethod() === ReviewAssignment::SUBMISSION_REVIEW_METHOD_DOUBLEANONYMOUS;

        /** @var GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();

        return $response->withJson([
            'itemsMax' => $collector->getCount(),
            'items' => Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->summarizeMany($publications, $anonymize)->values(),
        ], 200);
    }

    /**
     * Get one of this submission's publications
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function getPublication($slimRequest, $response, $args)
    {
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);

        $publication = Repo::publication()->get((int) $args['publicationId']);

        if (!$publication) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        if ($submission->getId() !== $publication->getData('submissionId')) {
            return $response->withStatus(403)->withJsonError('api.publications.403.submissionsDidNotMatch');
        }

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

        /** @var GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();

        return $response->withJson(
            Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication),
            200
        );
    }

    /**
     * Add a new publication to this submission
     *
     * This will create a new publication from scratch. If you want to create a new
     * version of a publication, see self::versionPublication().
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function addPublication($slimRequest, $response, $args)
    {
        $request = $this->getRequest();
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);

        $params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_PUBLICATION, $slimRequest->getParsedBody());
        $params['submissionId'] = $submission->getId();

        $submissionContext = $request->getContext();
        if (!$submissionContext || $submissionContext->getId() !== $submission->getData('contextId')) {
            $submissionContext = Services::get('context')->get($submission->getData('contextId'));
        }

        $errors = Repo::publication()->validate(null, $params, $submission, $submissionContext);

        if (!empty($errors)) {
            return $response->withStatus(400)->withJson($errors);
        }

        $publication = Repo::publication()->newDataObject($params);
        $newId = Repo::publication()->add($publication);
        $publication = Repo::publication()->get($newId);

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

        /** @var GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();

        return $response->withJson(
            Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication),
            200
        );
    }

    /**
     * Create a new version of a publication
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function versionPublication($slimRequest, $response, $args)
    {
        $request = $this->getRequest();
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
        $publication = Repo::publication()->get((int) $args['publicationId']);
        $context = $request->getContext();

        if (!$publication) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        if ($submission->getId() !== $publication->getData('submissionId')) {
            return $response->withStatus(403)->withJsonError('api.publications.403.submissionsDidNotMatch');
        }

        $newId = Repo::publication()->version($publication);
        $publication = Repo::publication()->get($newId);

        $notificationManager = new NotificationManager();
        $usersIterator = Repo::user()->getCollector()
            ->filterByContextIds([$submission->getContextId()])
            ->assignedTo($submission->getId())
            ->getMany();

        /** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
        $notificationSubscriptionSettingsDao = DAORegistry::getDAO('NotificationSubscriptionSettingsDAO');
        foreach ($usersIterator as $user) {
            $notification = $notificationManager->createNotification(
                $request,
                $user->getId(),
                PKPNotification::NOTIFICATION_TYPE_SUBMISSION_NEW_VERSION,
                $submission->getContextId(),
                Application::ASSOC_TYPE_SUBMISSION,
                $submission->getId(),
                Notification::NOTIFICATION_LEVEL_TASK,
            );

            // Check if user is subscribed to this type of notification emails
            if (!$notification || in_array(
                PKPNotification::NOTIFICATION_TYPE_SUBMISSION_NEW_VERSION,
                $notificationSubscriptionSettingsDao->getNotificationSubscriptionSettings(
                    NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY,
                    $user->getId(),
                    (int) $context->getId()
                )
            )
            ) {
                continue;
            }

            $mailable = new PublicationVersionNotify($context, $submission);
            $template = Repo::emailTemplate()->getByKey($context->getId(), PublicationVersionNotify::getEmailTemplateKey());
            $mailable
                ->from($context->getData('contactEmail'), $context->getData('contactName'))
                ->recipients([$user])
                ->body($template->getLocalizedData('body'))
                ->subject($template->getLocalizedData('subject'))
                ->allowUnsubscribe($notification);

            Mail::send($mailable);
        }

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

        /** @var GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();

        return $response->withJson(
            Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication),
            200
        );
    }

    /**
     * Edit one of this submission's publications
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function editPublication($slimRequest, $response, $args)
    {
        $request = $this->getRequest();
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
        $currentUser = $request->getUser();
        $publication = Repo::publication()->get((int) $args['publicationId']);

        if (!$publication) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        if ($submission->getId() !== $publication->getData('submissionId')) {
            return $response->withStatus(403)->withJsonError('api.publications.403.submissionsDidNotMatch');
        }

        // Publications can not be edited when they are published
        if ($publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) {
            return $response->withStatus(403)->withJsonError('api.publication.403.cantEditPublished');
        }

        // Prevent users from editing publications if they do not have permission. Except for admins.
        $userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
        if (!in_array(Role::ROLE_ID_SITE_ADMIN, $userRoles) && !Repo::submission()->canEditPublication($submission->getId(), $currentUser->getId())) {
            return $response->withStatus(403)->withJsonError('api.submissions.403.userCantEdit');
        }

        $params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_PUBLICATION, $slimRequest->getParsedBody());
        $params['id'] = $publication->getId();

        // Don't allow the status to be modified through the API. The `/publish` and /unpublish endpoints
        // should be used instead.
        if (array_key_exists('status', $params)) {
            return $response->withStatus(403)->withJsonError('api.publication.403.cantEditStatus');
        }

        $submissionContext = $request->getContext();
        if (!$submissionContext || $submissionContext->getId() !== $submission->getData('contextId')) {
            $submissionContext = Services::get('context')->get($submission->getData('contextId'));
        }

        $errors = Repo::publication()->validate($publication, $params, $submission, $submissionContext);

        if (!empty($errors)) {
            return $response->withStatus(400)->withJson($errors);
        }

        Repo::publication()->edit($publication, $params);

        $publication = Repo::publication()->get($publication->getId());

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

        /** @var GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();

        return $response->withJson(
            Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication),
            200
        );
    }

    /**
     * Publish one of this submission's publications
     *
     * If this is a GET request, it will run the pre-publish validation
     * checks and return errors but it will not perform the final
     * publication step.
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function publishPublication($slimRequest, $response, $args)
    {
        $request = $this->getRequest();
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
        $publication = Repo::publication()->get((int) $args['publicationId']);

        if (!$publication) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        if ($submission->getId() !== $publication->getData('submissionId')) {
            return $response->withStatus(403)->withJsonError('api.publications.403.submissionsDidNotMatch');
        }

        if ($publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) {
            return $response->withStatus(403)->withJsonError('api.publication.403.alreadyPublished');
        }

        $submissionContext = $request->getContext();
        if (!$submissionContext || $submissionContext->getId() !== $submission->getData('contextId')) {
            $submissionContext = Services::get('context')->get($submission->getData('contextId'));
        }
        $primaryLocale = $submission->getData('locale');
        $allowedLocales = $submissionContext->getData('supportedSubmissionLocales');

        $errors = Repo::publication()->validatePublish($publication, $submission, $allowedLocales, $primaryLocale);

        if (!empty($errors)) {
            return $response->withStatus(400)->withJson($errors);
        }

        Repo::publication()->publish($publication);

        $publication = Repo::publication()->get($publication->getId());

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

        /** @var GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();

        return $response->withJson(
            Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication),
            200
        );
    }

    /**
     * Unpublish one of this submission's publications
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function unpublishPublication($slimRequest, $response, $args)
    {
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
        $publication = Repo::publication()->get((int) $args['publicationId']);

        if (!$publication) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        if ($submission->getId() !== $publication->getData('submissionId')) {
            return $response->withStatus(403)->withJsonError('api.publications.403.submissionsDidNotMatch');
        }

        if (!in_array($publication->getData('status'), [PKPSubmission::STATUS_PUBLISHED, PKPSubmission::STATUS_SCHEDULED])) {
            return $response->withStatus(403)->withJsonError('api.publication.403.alreadyUnpublished');
        }

        Repo::publication()->unpublish($publication);

        $publication = Repo::publication()->get($publication->getId());

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

        /** @var GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();

        return $response->withJson(
            Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication),
            200
        );
    }

    /**
     * Delete one of this submission's publications
     *
     * Published publications can not be deleted. First you must unpublish them.
     * See self::unpublishPublication().
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function deletePublication($slimRequest, $response, $args)
    {
        $request = $this->getRequest();
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
        $publication = Repo::publication()->get((int) $args['publicationId']);

        if (!$publication) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        if ($submission->getId() !== $publication->getData('submissionId')) {
            return $response->withStatus(403)->withJsonError('api.publications.403.submissionsDidNotMatch');
        }

        if ($publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) {
            return $response->withStatus(403)->withJsonError('api.publication.403.cantDeletePublished');
        }

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

        /** @var GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();

        $output = Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication);

        Repo::publication()->delete($publication);

        return $response->withJson($output, 200);
    }

    /**
     * Get one of a publication's contributors
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function getContributor($slimRequest, $response, $args)
    {
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);

        $publication = Repo::publication()->get((int) $args['publicationId']);
        $author = Repo::author()->get((int) $args['contributorId']);

        if (!$publication) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        if (!$author) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        if ($submission->getId() !== $publication->getData('submissionId')) {
            return $response->withStatus(403)->withJsonError('api.publications.403.submissionsDidNotMatch');
        }

        if ($publication->getId() !== $author->getData('publicationId')) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        return $response->withJson(
            Repo::author()->getSchemaMap()->map($author),
            200
        );
    }

    /**
     * Get all publication's contributors
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function getContributors($slimRequest, $response, $args)
    {
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
        $publication = Repo::publication()->get((int) $args['publicationId']);

        if (!$publication) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        if ($submission->getId() !== $publication->getData('submissionId')) {
            return $response->withStatus(403)->withJsonError('api.publications.403.submissionsDidNotMatch');
        }

        $collector = Repo::author()->getCollector()
            ->filterByPublicationIds([$publication->getId()]);
        $authors = $collector->getMany();

        return $response->withJson([
            'itemsMax' => $collector->getCount(),
            'items' => Repo::author()->getSchemaMap()->summarizeMany($authors)->values(),
        ], 200);
    }

    /**
     * Add a new contributor to publication
     *
     * This will create a new contributor from scratch.
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function addContributor($slimRequest, $response, $args)
    {
        $request = $this->getRequest();
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
        $currentUser = $request->getUser();

        $publication = Repo::publication()->get((int) $args['publicationId']);

        if (!$publication) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        if ($submission->getId() !== $publication->getData('submissionId')) {
            return $response->withStatus(403)->withJsonError('api.publications.403.submissionsDidNotMatch');
        }

        // Publications can not be edited when they are published
        if ($publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) {
            return $response->withStatus(403)->withJsonError('api.publication.403.cantEditPublished');
        }

        $params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_AUTHOR, $slimRequest->getParsedBody());
        $params['publicationId'] = $publication->getId();

        $submissionContext = $request->getContext();
        if (!$submissionContext || $submissionContext->getId() !== $submission->getData('contextId')) {
            $submissionContext = Services::get('context')->get($submission->getData('contextId'));
        }

        $errors = Repo::author()->validate(null, $params, $submission, $submissionContext);

        if (!empty($errors)) {
            return $response->withStatus(400)->withJson($errors);
        }

        $author = Repo::author()->newDataObject($params);
        $newId = Repo::author()->add($author);
        $author = Repo::author()->get($newId);

        return $response->withJson(
            Repo::author()->getSchemaMap()->map($author),
            200
        );
    }

    /**
     * Delete one of this publication's contributors
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function deleteContributor($slimRequest, $response, $args)
    {
        $request = $this->getRequest();
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
        $currentUser = $request->getUser();

        $publication = Repo::publication()->get((int) $args['publicationId']);
        $author = Repo::author()->get((int) $args['contributorId']);

        if (!$publication) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        // Publications can not be edited when they are published
        if ($publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) {
            return $response->withStatus(403)->withJsonError('api.publication.403.cantEditPublished');
        }

        if ($submission->getId() !== $publication->getData('submissionId')) {
            return $response->withStatus(403)->withJsonError('api.publications.403.submissionsDidNotMatch');
        }

        if (!$author) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        if ($publication->getId() !== $author->getData('publicationId')) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        $output = Repo::author()->getSchemaMap()->map($author);

        Repo::author()->delete($author);

        return $response->withJson($output, 200);
    }

    /**
     * Edit one of this publication's contributors
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function editContributor($slimRequest, $response, $args)
    {
        $request = $this->getRequest();
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
        $currentUser = $request->getUser();

        $publication = Repo::publication()->get((int) $args['publicationId']);
        $author = Repo::author()->get((int) $args['contributorId']);

        if (!$publication) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        if (!$author) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        if ($submission->getId() !== $publication->getData('submissionId')) {
            return $response->withStatus(403)->withJsonError('api.publications.403.submissionsDidNotMatch');
        }

        // Publications can not be edited when they are published
        if ($publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) {
            return $response->withStatus(403)->withJsonError('api.publication.403.cantEditPublished');
        }

        $params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_AUTHOR, $slimRequest->getParsedBody());
        $params['id'] = $author->getId();

        $submissionContext = $request->getContext();
        if (!$submissionContext || $submissionContext->getId() !== $submission->getData('contextId')) {
            $submissionContext = Services::get('context')->get($submission->getData('contextId'));
        }

        if ($publication->getId() !== $author->getData('publicationId')) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        // Prevent users from editing publications if they do not have permission. Except for admins.
        $userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
        if (!in_array(Role::ROLE_ID_SITE_ADMIN, $userRoles) && !Repo::submission()->canEditPublication($submission->getId(), $currentUser->getId())) {
            return $response->withStatus(403)->withJsonError('api.submissions.403.userCantEdit');
        }

        $errors = Repo::author()->validate($author, $params, $submission, $submissionContext);

        if (!empty($errors)) {
            return $response->withStatus(400)->withJson($errors);
        }

        Repo::author()->edit($author, $params);
        $author = Repo::author()->get($author->getId());

        return $response->withJson(
            Repo::author()->getSchemaMap()->map($author),
            200
        );
    }

    /**
     * Save new order of contributors array
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function saveContributorsOrder($slimRequest, $response, $args)
    {
        $params = $slimRequest->getParsedBody();

        $request = $this->getRequest();
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
        $currentUser = $request->getUser();

        $publication = Repo::publication()->get((int) $args['publicationId']);

        if (!$publication) {
            return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
        }

        if ($submission->getId() !== $publication->getData('submissionId')) {
            return $response->withStatus(403)->withJsonError('api.publications.403.submissionsDidNotMatch');
        }

        // Publications can not be edited when they are published
        if ($publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) {
            return $response->withStatus(403)->withJsonError('api.publication.403.cantEditPublished');
        }

        if (!empty($params['sortedAuthors'])) {
            $authors = [];
            foreach ($params['sortedAuthors'] as $author) {
                $newAuthor = Repo::author()->get((int) $author['id']);

                array_push($authors, $newAuthor);
            }

            Repo::author()->setAuthorsOrder($publication->getId(), $authors);
        }

        $authors = Repo::author()
            ->getCollector()
            ->filterByPublicationIds([$publication->getId()])
            ->getMany();

        return $response->withJson(Repo::author()->getSchemaMap()->summarizeMany($authors));
    }

    /**
     * Record an editorial decision for a submission, such as
     * a decision to accept or reject the submission, request
     * revisions, or send it to another stage.
     *
     * @param SlimRequest $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function addDecision($slimRequest, $response, $args)
    {
        $request = $this->getRequest(); /** @var Request $request */
        $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION); /** @var Submission $submission */
        $decisionType = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_DECISION_TYPE); /** @var DecisionType $decisionType */

        if ($submission->getData('status') === Submission::STATUS_PUBLISHED) {
            return $response->withStatus(403)->withJsonError('api.decisions.403.alreadyPublished');
        }

        $params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_DECISION, $slimRequest->getParsedBody());
        $params['submissionId'] = $submission->getId();
        $params['dateDecided'] = Core::getCurrentDate();
        $params['editorId'] = $request->getUser()->getId();
        $params['stageId'] = $decisionType->getStageId();

        $errors = Repo::decision()->validate($params, $decisionType, $submission, $request->getContext());

        if (!empty($errors)) {
            return $response->withStatus(400)->withJson($errors);
        }

        $decision = Repo::decision()->newDataObject($params);
        $decisionId = Repo::decision()->add($decision);

        // In some cases, recording a decision may delete the decision. This
        // happens for example with the Cancel Review Round decision. When
        // the decision is added, the review round is deleted and all decisions
        // related to that round are deleted. In such cases, we return the
        // original Decision object rather than fetching it from the data store.
        $decision = Repo::decision()->get($decisionId) ?? $decision;

        return $response->withJson(Repo::decision()->getSchemaMap()->map($decision), 200);
    }

    protected function getFirstUserGroupInRole(Enumerable $userGroups, int $role): ?UserGroup
    {
        return $userGroups->first(fn (UserGroup $userGroup) => $userGroup->getRoleId() === $role);
    }

    /**
     * Is the current user an editor
     */
    protected function isEditor(): bool
    {
        return !empty(
            array_intersect(
                Section::getEditorRestrictedRoles(),
                $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES)
            )
        );
    }

    /**
     * This method returns errors for any params that match
     * properties in the schema with writeDisabledInApi set to true.
     *
     * This is used for properties that can not be edited through
     * the API, but which otherwise can be edited by the entity's
     * repository.
     */
    protected function getWriteDisabledErrors(string $schemaName, array $params): array
    {
        $schema = Services::get('schema')->get($schemaName);

        $writeDisabledProps = [];
        foreach ($schema->properties as $propName => $propSchema) {
            if (!empty($propSchema->writeDisabledInApi)) {
                $writeDisabledProps[] = $propName;
            }
        }

        $errors = [];

        $notAllowedProps = array_intersect(
            $writeDisabledProps,
            array_keys($params)
        );

        if (!empty($notAllowedProps)) {
            foreach ($notAllowedProps as $propName) {
                $errors[$propName] = [__('api.400.propReadOnly', ['prop' => $propName])];
            }
        }

        return $errors;
    }
}
