<?php

/**
 * @file api/v1/stats/sushi/PKPStatsSushiHandler.php
 *
 * Copyright (c) 2022 Simon Fraser University
 * Copyright (c) 2022 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class PKPStatsSushiHandler
 *
 * @ingroup api_v1_stats
 *
 * @brief Handle API requests for COUNTER R5 SUSHI statistics.
 *
 */

namespace PKP\API\v1\stats\sushi;

use APP\core\Application;
use APP\facades\Repo;
use APP\sushi\PR;
use APP\sushi\PR_P1;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\security\authorization\ContextRequiredPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\sushi\CounterR5Report;
use PKP\sushi\SushiException;
use PKP\validation\ValidatorFactory;
use Slim\Http\Request as SlimHttpRequest;

class PKPStatsSushiHandler extends APIHandler
{
    /** @var bool Whether the API is public */
    public $isPublic = true;

    /**
     * Constructor
     */
    public function __construct()
    {
        $site = Application::get()->getRequest()->getSite();
        $context = Application::get()->getRequest()->getContext();
        if (($site->getData('isSushiApiPublic') !== null && !$site->getData('isSushiApiPublic')) ||
            ($context->getData('isSushiApiPublic') !== null && !$context->getData('isSushiApiPublic'))) {
            $this->isPublic = false;
        }

        $this->_handlerPath = 'stats/sushi';
        $roles = $this->isPublic ? null : [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER];
        $this->_endpoints = [
            'GET' => $this->getGETDefinitions($roles)
        ];
        parent::__construct();
    }

    /**
     * Get this API's endpoints definitions
     */
    protected function getGETDefinitions(array $roles = null): array
    {
        return [
            [
                'pattern' => $this->getEndpointPattern() . '/status',
                'handler' => [$this, 'getStatus'],
                'roles' => $roles
            ],
            [
                'pattern' => $this->getEndpointPattern() . '/members',
                'handler' => [$this, 'getMembers'],
                'roles' => $roles
            ],
            [
                'pattern' => $this->getEndpointPattern() . '/reports',
                'handler' => [$this, 'getReports'],
                'roles' => $roles
            ],
            [
                'pattern' => $this->getEndpointPattern() . '/reports/pr',
                'handler' => [$this, 'getReportsPR'],
                'roles' => $roles
            ],
            [
                'pattern' => $this->getEndpointPattern() . '/reports/pr_p1',
                'handler' => [$this, 'getReportsPR1'],
                'roles' => $roles
            ],
        ];
    }

    /**
     * @copydoc PKPHandler::authorize()
     */
    public function authorize($request, &$args, $roleAssignments)
    {
        $this->addPolicy(new ContextRequiredPolicy($request));
        if (!$this->isPublic) {
            $this->addPolicy(new UserRolesRequiredPolicy($request), true);
            $rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
            foreach ($roleAssignments as $role => $operations) {
                $rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
            }
            $this->addPolicy($rolePolicy);
        }
        return parent::authorize($request, $args, $roleAssignments);
    }

    /**
     * Get the current status of the reporting service
     */
    public function getStatus(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
    {
        $request = $this->getRequest();
        $context = $request->getContext();
        // use only the name in the context primary locale to be consistent
        $contextName = $context->getName($context->getPrimaryLocale());
        return $response->withJson([
            'Description' => __('sushi.status.description', ['contextName' => $contextName]),
            'Service_Active' => true,
        ], 200);
    }

    /**
     * Get the list of consortium members related to a Customer_ID
     */
    public function getMembers(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
    {
        $request = $this->getRequest();
        $context = $request->getContext();
        $site = $request->getSite();
        $params = $slimRequest->getQueryParams();
        if (!isset($params['customer_id'])) {
            // error: missing required customer_id
            return $response->withJson([
                'Code' => 1030,
                'Severity' => 'Fatal',
                'Message' => 'Insufficient Information to Process Request',
                'Data' => __('sushi.exception.1030.missing', ['params' => 'customer_id'])
            ], 400);
        }
        $platformId = $context->getPath();
        if ($site->getData('isSiteSushiPlatform')) {
            $platformId = $site->getData('sushiPlatformID');
        }
        $institutionName = $institutionId = null;
        $customerId = $params['customer_id'];
        if (is_numeric($customerId)) {
            $customerId = (int) $customerId;
            if ($customerId == 0) {
                $institutionName = 'The World';
            } else {
                $institution = Repo::institution()->get($customerId);
                if (isset($institution) && $institution->getContextId() == $context->getId()) {
                    $institutionId = [];
                    $institutionName = $institution->getLocalizedName();
                    if (!empty($institution->getROR())) {
                        $institutionId[] = ['Type' => 'ROR', 'Value' => $institution->getROR()];
                    }
                    $institutionId[] = ['Type' => 'Proprietary', 'Value' => $platformId . ':' . $customerId];
                }
            }
        }
        if (!isset($institutionName)) {
            // error: invalid customer_id
            return $response->withJson([
                'Code' => 1030,
                'Severity' => 'Fatal',
                'Message' => 'Insufficient Information to Process Request',
                'Data' => __('sushi.exception.1030.invalid', ['params' => 'customer_id'])
            ], 400);
        }
        $item = [
            'Customer_ID' => $customerId,
            'Name' => $institutionName,
        ];
        if (isset($institutionId)) {
            $item['Institution_ID'] = $institutionId;
        }
        return $response->withJson([$item], 200);
    }

    /**
     * Get list of reports supported by the API
     */
    public function getReports(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
    {
        $items = $this->getReportList();
        return $response->withJson($items, 200);
    }

    /**
     * Get the application specific list of reports supported by the API
     */
    protected function getReportList(): array
    {
        return [
            [
                'Report_Name' => 'Platform Master Report',
                'Report_ID' => 'PR',
                'Release' => '5',
                'Report_Description' => __('sushi.reports.pr.description'),
                'Path' => 'reports/pr'
            ],
            [
                'Report_Name' => 'Platform Usage',
                'Report_ID' => 'PR_P1',
                'Release' => '5',
                'Report_Description' => __('sushi.reports.pr_p1.description'),
                'Path' => 'reports/pr_p1'
            ],
        ];
    }

    /**
     * COUNTER 'Platform Usage' [PR_P1].
     * A customizable report summarizing activity across the Platform (journal, press, or server).
     */
    public function getReportsPR(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
    {
        return $this->getReportResponse(new PR(), $slimRequest, $response, $args);
    }

    /**
     * COUNTER 'Platform Master Report' [PR].
     * This is a Standard View of the Platform Master Report that presents usage for the overall Platform broken down by Metric_Type
     */
    public function getReportsPR1(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
    {
        return $this->getReportResponse(new PR_P1(), $slimRequest, $response, $args);
    }

    /** Validate user input for TSV reports */
    protected function _validateUserInput(CounterR5Report $report, array $params): array
    {
        $request = $this->getRequest();
        $context = $request->getContext();
        $earliestDate = CounterR5Report::getEarliestDate();
        $lastDate = CounterR5Report::getLastDate();
        $submissionIds = Repo::submission()->getCollector()->filterByContextIds([$context->getId()])->getIds()->implode(',');

        $rules = [
            'begin_date' => [
                'regex:/^\d{4}-\d{2}(-\d{2})?$/',
                'after_or_equal:' . $earliestDate,
                'before_or_equal:end_date',
            ],
            'end_date' => [
                'regex:/^\d{4}-\d{2}(-\d{2})?$/',
                'before_or_equal:' . $lastDate,
                'after_or_equal:begin_date',
            ],
            'item_id' => [
                // TO-ASK: shell this rather be just validation for positive integer?
                'in:' . $submissionIds,
            ],
            'yop' => [
                'regex:/^\d{4}((\||-)\d{4})*$/',
            ],
        ];
        $reportId = $report->getID();
        if (in_array($reportId, ['PR', 'TR', 'IR'])) {
            $rules['metric_type'] = ['required'];
        }

        $errors = [];
        $validator = ValidatorFactory::make(
            $params,
            $rules,
            [
                'begin_date.regex' => __(
                    'manager.statistics.counterR5Report.settings.wrongDateFormat'
                ),
                'end_date.regex' => __(
                    'manager.statistics.counterR5Report.settings.wrongDateFormat'
                ),
                'begin_date.after_or_equal' => __(
                    'stats.dateRange.invalidStartDateMin'
                ),
                'end_date.before_or_equal' => __(
                    'stats.dateRange.invalidEndDateMax'
                ),
                'begin_date.before_or_equal' => __(
                    'stats.dateRange.invalidDateRange'
                ),
                'end_date.after_or_equal' => __(
                    'stats.dateRange.invalidDateRange'
                ),
                'item_id.*' => __(
                    'manager.statistics.counterR5Report.settings.wrongItemId'
                ),
                'yop.regex' => __(
                    'manager.statistics.counterR5Report.settings.wrongYOPFormat'
                ),
            ]
        );

        if ($validator->fails()) {
            $errors = $validator->errors()->getMessages();
        }

        return $errors;
    }

    /**
     * Get the requested report
     */
    protected function getReportResponse(CounterR5Report $report, SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
    {
        $responseTSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_TSV) ? true : false;

        $params = $slimRequest->getQueryParams();

        if ($responseTSV) {
            $errors = $this->_validateUserInput($report, $params);
            if (!empty($errors)) {
                return $response->withJson($errors, 400);
            }
        }

        try {
            $report->processReportParams($this->getRequest(), $params);
        } catch (SushiException $e) {
            return $response->withJson($e->getResponseData(), $e->getHttpStatusCode());
        }

        if ($responseTSV) {
            $reportHeader = $report->getTSVReportHeader();
            $reportColumnNames = $report->getTSVColumnNames();
            $reportItems = $report->getTSVReportItems();
            // consider 3030 error (no usage available)
            $key = array_search('3030', array_column($report->warnings, 'Code'));
            if ($key !== false) {
                $error = $report->warnings[$key]['Code'] . ':' . $report->warnings[$key]['Message'] . '(' . $report->warnings[$key]['Data'] . ')';
                foreach ($reportHeader as &$headerRow) {
                    if (in_array('Exceptions', $headerRow)) {
                        $headerRow[1] =
                            $headerRow[1] == '' ?
                            $error :
                            $headerRow[1] . ';' . $error;
                    }
                }
            }
            $report = array_merge($reportHeader, [['']], $reportColumnNames, $reportItems);
            return $response->withCSV($report, [], count($reportItems), APIResponse::RESPONSE_TSV);
        }

        $reportHeader = $report->getReportHeader();
        $reportItems = $report->getReportItems();
        return $response->withJson([
            'Report_Header' => $reportHeader,
            'Report_Items' => $reportItems,
        ], 200);
    }
}
