<?php

/**
 * @file api/v1/issues/IssueHandler.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 IssueHandler
 *
 * @ingroup api_v1_issues
 *
 * @brief Handle API requests for issues operations.
 *
 */

namespace APP\API\v1\issues;

use APP\core\Application;
use APP\facades\Repo;
use APP\issue\Collector;
use APP\security\authorization\OjsIssueRequiredPolicy;
use APP\security\authorization\OjsJournalMustPublishPolicy;
use Illuminate\Support\LazyCollection;
use PKP\core\APIResponse;
use PKP\db\DAORegistry;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\ContextRequiredPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\submission\GenreDAO;
use Slim\Http\Request;

class IssueHandler extends APIHandler
{
    /** @var int The default number of issues to return in one request */
    public const DEFAULT_COUNT = 20;

    /** @var int The maximum number of issues to return in one request */
    public const MAX_COUNT = 100;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->_handlerPath = 'issues';
        $roles = [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_REVIEWER, Role::ROLE_ID_AUTHOR];
        $this->_endpoints = [
            'GET' => [
                [
                    'pattern' => $this->getEndpointPattern(),
                    'handler' => [$this, 'getMany'],
                    'roles' => $roles
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/current',
                    'handler' => [$this, 'getCurrent'],
                    'roles' => $roles
                ],
                [
                    'pattern' => $this->getEndpointPattern() . '/{issueId:\d+}',
                    'handler' => [$this, 'get'],
                    'roles' => $roles
                ],
            ]
        ];
        parent::__construct();
    }

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

        if (!is_null($slimRequest) && ($route = $slimRequest->getAttribute('route'))) {
            $routeName = $route->getName();
        }

        $this->addPolicy(new UserRolesRequiredPolicy($request), true);
        $this->addPolicy(new ContextRequiredPolicy($request));
        $this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
        $this->addPolicy(new OjsJournalMustPublishPolicy($request));

        if ($routeName === 'get') {
            $this->addPolicy(new OjsIssueRequiredPolicy($request, $args));
        }

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

    //
    // Public handler methods
    //
    /**
     * Get a collection of issues
     *
     * @param Request $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function getMany($slimRequest, $response, $args)
    {
        $collector = Repo::issue()->getCollector()
            ->limit(self::DEFAULT_COUNT)
            ->offset(0);

        $request = $this->getRequest();
        $currentUser = $request->getUser();
        $context = $request->getContext();

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

        // Process query params to format incoming data as needed
        foreach ($slimRequest->getQueryParams() as $param => $val) {
            switch ($param) {
                case 'orderBy':
                    if (in_array($val, [Collector::ORDERBY_DATE_PUBLISHED, Collector::ORDERBY_LAST_MODIFIED, Collector::ORDERBY_SEQUENCE])) {
                        $collector->orderBy($val);
                    }
                    break;

                    // Enforce a maximum count to prevent the API from crippling the
                    // server
                case 'count':
                    $collector->limit(min((int) $val, self::MAX_COUNT));
                    break;

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

                    // Always convert volume, number and year values to array
                case 'volumes':
                case 'volume':
                case 'numbers':
                case 'number':
                case 'years':
                case 'year':

                    // Support deprecated `year`, `number` and `volume` params
                    if (substr($param, -1) !== 's') {
                        $param .= 's';
                    }

                    if (is_string($val)) {
                        $val = explode(',', $val);
                    } elseif (!is_array($val)) {
                        $val = [$val];
                    }
                    $values = array_map('intval', $val);
                    switch ($param) {
                        case 'volumes':
                            $collector->filterByVolumes($values);
                            break;
                        case 'numbers':
                            $collector->filterByNumbers($values);
                            break;
                        case 'years':
                            $collector->filterByYears($values);
                            break;
                    }

                    break;

                case 'isPublished':
                    $collector->filterByPublished((bool) $val);
                    break;

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

        $collector->filterByContextIds([$context->getId()]);

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

        // You must be a manager or site admin to access unpublished Issues
        $isAdmin = $currentUser->hasRole([Role::ROLE_ID_MANAGER], $context->getId()) || $currentUser->hasRole([Role::ROLE_ID_SITE_ADMIN], \PKP\core\PKPApplication::CONTEXT_SITE);
        if (isset($collector->isPublished) && !$collector->isPublished && !$isAdmin) {
            return $response->withStatus(403)->withJsonError('api.submissions.403.unpublishedIssues');
        } elseif (!$isAdmin) {
            $collector->filterByPublished(true);
        }

        $issues = $collector->getMany();

        return $response->withJson([
            'items' => Repo::issue()->getSchemaMap()->summarizeMany($issues, $context)->values(),
            'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
        ], 200);
    }

    /**
     * Get the current issue
     *
     * @param Request $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function getCurrent($slimRequest, $response, $args)
    {
        $context = $this->getRequest()->getContext();

        $issue = Repo::issue()->getCurrent($context->getId());

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

        $data = Repo::issue()->getSchemaMap()->map(
            $issue,
            $context,
            $this->getUserGroups($context->getId()),
            $this->getGenres($context->getId())
        );

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

    /**
     * Get a single issue
     *
     * @param Request $slimRequest Slim request object
     * @param APIResponse $response object
     * @param array $args arguments
     *
     * @return APIResponse
     */
    public function get($slimRequest, $response, $args)
    {
        $context = $this->getRequest()->getContext();

        $issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);

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

        $data = Repo::issue()->getSchemaMap()->map(
            $issue,
            $context,
            $this->getUserGroups($context->getId()),
            $this->getGenres($context->getId())
        );

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

    protected function getUserGroups(int $contextId): LazyCollection
    {
        return Repo::userGroup()->getCollector()
            ->filterByContextIds([$contextId])
            ->getMany();
    }

    protected function getGenres(int $contextId): array
    {
        /** @var GenreDAO $genreDao */
        $genreDao = DAORegistry::getDAO('GenreDAO');
        return $genreDao->getByContextId($contextId)->toArray();
    }
}
