<?php

namespace LimeSurvey\Models\Services;

use LSYii_Application;
use Survey;
use Permission;
use Question;
use QuestionGroup;
use QuestionGroupL10n;
use Exception;
use LimeSurvey\Models\Services\{
    Proxy\ProxyExpressionManager,
    Proxy\ProxyQuestionGroup,
    Exception\PersistErrorException,
    Exception\NotFoundException,
    Exception\PermissionDeniedException
};

/**
 * @TODO There is a separate service GroupHelper.php whose function(s)
 *  should be moved here in the future
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
 */
class QuestionGroupService
{
    private Permission $modelPermission;
    private Survey $modelSurvey;
    private Question $modelQuestion;
    private QuestionGroup $modelQuestionGroup;
    private QuestionGroupL10n $modelQuestionGroupL10n;
    private ProxyExpressionManager $proxyExpressionManager;
    private ProxyQuestionGroup $proxyQuestionGroup;
    private LSYii_Application $yiiApp;

    public function __construct(
        Permission $modelPermission,
        Survey $modelSurvey,
        Question $modelQuestion,
        QuestionGroup $modelQuestionGroup,
        QuestionGroupL10n $modelQuestionGroupL10n,
        ProxyExpressionManager $proxyExpressionManager,
        ProxyQuestionGroup $proxyQuestionGroup,
        LSYii_Application $yiiApp
    ) {
        $this->modelPermission = $modelPermission;
        $this->modelSurvey = $modelSurvey;
        $this->modelQuestion = $modelQuestion;
        $this->modelQuestionGroup = $modelQuestionGroup;
        $this->modelQuestionGroupL10n = $modelQuestionGroupL10n;
        $this->proxyExpressionManager = $proxyExpressionManager;
        $this->proxyQuestionGroup = $proxyQuestionGroup;
        $this->yiiApp = $yiiApp;
    }

    /**
     * Updates a question group and all the languages.
     *
     * @param int $surveyId the survey ID
     * @param int $questionGroupId the question group id
     * @param array $input has the data for a question group, including an array for languages
     *      ['questionGroup']
     *          [gid]
     *          [sid]
     *          [group_order]
     *          [randomization_group]
     *          [grelevance]
     *      ['questionGroupL10N']
     *          [en]
     *              [group_name]
     *              [description]
     *          [...]    //more languages
     * @return QuestionGroup
     * @throws NotFoundException
     * @throws PermissionDeniedException
     * @throws PersistErrorException
     */
    public function updateGroup(int $surveyId, int $questionGroupId, array $input)
    {
        $questionGroup = $this->getQuestionGroupForUpdate(
            $surveyId,
            $questionGroupId
        );

        $questionGroup = $this->updateQuestionGroup(
            $questionGroup,
            $input['questionGroup']
        );
        $this->updateQuestionGroupLanguages(
            $questionGroup,
            $input['questionGroupI10N']
        );

        return $questionGroup;
    }

    /**
     * Checks permissions for updating, and returns a specific question group.
     * Throws an exception if no group can be found.
     * @param int $surveyId
     * @param int $questionGroupId
     * @return QuestionGroup
     * @throws NotFoundException
     * @throws PermissionDeniedException
     */
    public function getQuestionGroupForUpdate(int $surveyId, int $questionGroupId)
    {
        $this->checkUpdatePermission($surveyId);
        $questionGroup = $this->modelQuestionGroup->findByPk($questionGroupId);
        if (!$questionGroup) {
            throw new NotFoundException(
                'Group not found'
            );
        }

        return $questionGroup;
    }

    /**
     * Creates a question group and all the languages.
     *
     * @param int $surveyId the survey ID
     * @param array $input has the data for a question group,
     *  including an array for languages
     *      ['questionGroup']
     *          [gid]
     *          [sid]
     *          [group_order]
     *          [randomization_group]
     *          [grelevance]
     *      ['questionGroupL10N']
     *          [en]
     *              [group_name]
     *              [description]
     *          [...]    //more languages
     * @return QuestionGroup
     * @throws NotFoundException
     * @throws PermissionDeniedException
     * @throws PersistErrorException
     */
    public function createGroup(int $surveyId, array $input)
    {
        $this->checkCreatePermission($surveyId);
        $questionGroup = $this->newQuestionGroup(
            $surveyId,
            $input['questionGroup'] ?? []
        );
        $this->updateQuestionGroupLanguages(
            $questionGroup,
            $input['questionGroupI10N'] ?? []
        );

        return $questionGroup;
    }

    /**
     * Deletes a question group and all its dependencies.
     *
     * @param int $questionGroupId the question group id
     * @param int $surveyId the survey ID
     * @return int|null number of deleted rows
     * @throws PermissionDeniedException
     */
    public function deleteGroup(int $questionGroupId, int $surveyId)
    {
        $this->checkDeletePermission($surveyId);
        $this->proxyExpressionManager->revertUpgradeConditionsToRelevance($surveyId);

        $deletedGroups = $this->proxyQuestionGroup->deleteQuestionGroupWithDependency(
            $questionGroupId,
            $surveyId
        );
        if ($deletedGroups > 0) {
            $this->modelQuestionGroup->updateGroupOrder($surveyId);
        }
        $this->proxyExpressionManager->upgradeConditionsToRelevance($surveyId);

        return $deletedGroups;
    }

    /**
     * Returns a QuestionGroup (existing one or new created one)
     *
     * @param int $surveyId
     * @param int | null $questionGroupId ID of group
     * @return QuestionGroup
     * @throws NotFoundException
     */
    public function getQuestionGroupObject(int $surveyId, ?int $questionGroupId = null)
    {
        $oQuestionGroup = $this->modelQuestionGroup->findByPk($questionGroupId);
        if (is_int($questionGroupId) && $oQuestionGroup === null) {
            throw new NotFoundException(gT('Invalid ID'));
        } elseif ($oQuestionGroup == null) {
            $oQuestionGroup = $this->modelQuestionGroup;
            $oQuestionGroup->sid = $surveyId;
        }

        return $oQuestionGroup;
    }

    /**
     * Returns question group data for dataprovider of gridview in
     * "Overview question and groups". Search input parameter is taken
     * into account.
     *
     * @param Survey $survey
     * @param array $questionGroupArray
     * @return QuestionGroup
     */
    public function getGroupData(Survey $survey, array $questionGroupArray)
    {
        $questionGroup = $this->modelQuestionGroup;
        $questionGroup->setScenario('search');
        if (array_key_exists('group_name', $questionGroupArray)) {
            $questionGroup->group_name = $questionGroupArray['group_name'];
        }
        $questionGroup->sid = $survey->primaryKey;
        $questionGroup->language = $survey->language;

        return $questionGroup;
    }

    /**
     * Imports an uploaded question group. Returns array of import results.
     *
     * @param int $surveyId
     * @param string $tmpDir
     * @param string $transLinksFields
     * @return array
     */
    public function importQuestionGroup(
        int $surveyId,
        string $tmpDir,
        string $transLinksFields
    ) {
        $importResults = [];
        $sFullFilepath = $tmpDir . DIRECTORY_SEPARATOR . randomChars(20);
        $aPathInfo = pathinfo((string)$_FILES['the_file']['name']);
        $sExtension = $aPathInfo['extension'];

        if (strtolower($sExtension) !== 'lsg') {
            $fatalerror = gT("Unknown file extension");
        } elseif (
            $_FILES['the_file']['error'] == 1
            || $_FILES['the_file']['error'] == 2
        ) {
            $fatalerror = sprintf(
                gT(
                    'Sorry, this file is too large. '
                    . 'Only files up to %01.2f MB are allowed.'
                ),
                getMaximumFileUploadSize() / 1024 / 1024
            );
        } elseif (
            !@move_uploaded_file(
                $_FILES['the_file']['tmp_name'],
                $sFullFilepath
            )
        ) {
            $fatalerror = gT(
                'An error occurred uploading your file. '
                . 'This may be caused by incorrect permissions '
                . 'for the application /tmp folder.'
            );
        }

        if (isset($fatalerror)) {
            $importResults['fatalerror'] = $fatalerror;
        } else {
            try {
                $this->yiiApp->loadHelper('admin.import');
                $importResults = XMLImportGroup(
                    $sFullFilepath,
                    $surveyId,
                    $transLinksFields == '1'
                );
            } catch (Exception $e) {
                $importResults['fatalerror'] = print_r($e->getMessage(), true);
            }
            // so refreshes syntax highlighting
            $this->proxyExpressionManager->setDirtyFlag();
            fixLanguageConsistency($surveyId);
        }
        $importResults['extension'] = $sExtension;
        unlink($sFullFilepath);

        return $importResults;
    }

    /**
     * Stores questiongroup languages.
     *
     * @param QuestionGroup $oQuestionGroup
     * @param array $dataSet array with languages
     * @return bool true if ALL languages could be saved, false otherwise
     */
    public function updateQuestionGroupLanguages(
        QuestionGroup $oQuestionGroup,
        array $dataSet
    ) {
        $storeValid = true;

        foreach ($dataSet as $sLanguage => $aL10NBlock) {
            $l10N = $this->modelQuestionGroupL10n->findByAttributes(
                ['gid' => $oQuestionGroup->gid, 'language' => $sLanguage]
            );
            if ($l10N) {
                if (isset($aL10NBlock['group_name'])) {
                    $l10N->setAttributes([
                        'group_name'  => $aL10NBlock['group_name']
                    ], false);
                }
                if (isset($aL10NBlock['description'])) {
                    $l10N->setAttributes([
                        'description' => $aL10NBlock['description']
                    ], false);
                }
                $storeValid = $storeValid && $l10N->save();
            }
        }

        return $storeValid;
    }

    /**
     * Reorders question groups based on the group array.
     * Returns array containing success (boolean), message (string).
     * @param int $surveyId
     * @param array $groupArray
     * @return array
     * @throws NotFoundException
     */
    public function reorderQuestionGroups(int $surveyId, array $groupArray)
    {
        $success = true;
        $message = '';
        if (!empty($groupArray)) {
            foreach ($groupArray as $aQuestionGroup) {
                //first set up the ordering for questiongroups
                $oQuestionGroups = $this->modelQuestionGroup->findAll(
                    "gid=:gid AND sid=:sid",
                    [':gid' => $aQuestionGroup['gid'], ':sid' => $surveyId]
                );
                array_map(
                    function ($oQuestionGroup) use (
                        $aQuestionGroup,
                        &$success
                    ) {
                        $oQuestionGroup->group_order = $aQuestionGroup['group_order'];
                        $success = $success && $oQuestionGroup->save();
                    },
                    $oQuestionGroups
                );

                $aQuestionGroup['questions'] = $aQuestionGroup['questions'] ?? [];

                foreach ($aQuestionGroup['questions'] as $aQuestion) {
                    $success = $this->updateQuestionsForReorder(
                        $aQuestion,
                        $surveyId,
                        $success
                    );
                }
            }
        } else {
            $message = gT("Nothing to reorder.");
        }
        $this->modelQuestionGroup->cleanOrder($surveyId);
        return [
            'success' => $success,
            'message' => $message
        ];
    }

    /**
     * Gets array with question related parameters (qid, question_order, gid)
     * and updates them in those questions and their subquestions
     * @param array $aQuestion
     * @param int $surveyId
     * @param bool $success
     * @return bool
     */
    private function updateQuestionsForReorder(
        array $aQuestion,
        int $surveyId,
        bool $success
    ) {
        $aQuestions = $this->modelQuestion->findAll(
            "qid=:qid AND sid=:sid",
            [':qid' => $aQuestion['qid'], ':sid' => $surveyId]
        );
        array_walk(
            $aQuestions,
            function ($oQuestion) use ($aQuestion, &$success) {
                $oQuestion->question_order = $aQuestion['question_order'];
                $oQuestion->gid = $aQuestion['gid'];
                if (safecount($oQuestion->subquestions) > 0) {
                    $aSubquestions = $oQuestion->subquestions;
                    array_walk(
                        $aSubquestions,
                        function ($oSubQuestion) use ($aQuestion, &$success) {
                            $oSubQuestion->gid = $aQuestion['gid'];
                            $success = $success && $oSubQuestion->save(true);
                        }
                    );
                }
                $success = $success && $oQuestion->save(true);
            }
        );
        return $success;
    }

    /**
     * Method to store and filter questionGroupData for editing a questionGroup
     *
     * @param QuestionGroup $oQuestionGroup
     * @param array $aQuestionGroupData
     *
     * @return QuestionGroup
     * @throws PersistErrorException
     */
    public function updateQuestionGroup(
        QuestionGroup $oQuestionGroup,
        array $aQuestionGroupData
    ) {
        $oQuestionGroup->setAttributes($aQuestionGroupData, false);
        if ($oQuestionGroup == null) {
            throw new PersistErrorException(
                "Object update failed, input array malformed or invalid"
            );
        }

        $saved = $oQuestionGroup->save();
        if (!$saved) {
            throw new PersistErrorException(
                "Saved question group failed, object may be inconsistent"
            );
        }
        return $oQuestionGroup;
    }

    /**
     * Creates a new question group, and also adds plain entries for the required
     * QuestionGroupL10n data
     *
     * @param int $surveyId
     * @param array|null $aQuestionGroupData
     *
     * @return QuestionGroup
     * @throws NotFoundException
     * @throws PersistErrorException
     */
    public function newQuestionGroup(int $surveyId, array $aQuestionGroupData = null)
    {
        $survey = $this->getSurvey($surveyId);
        $this->refreshModels();
        $aQuestionGroupData = array_merge([
            'sid' => $survey->sid,
        ], $aQuestionGroupData);
        unset($aQuestionGroupData['gid']);

        $oQuestionGroup = $this->modelQuestionGroup;
        $oQuestionGroup->setAttributes($aQuestionGroupData, false);

        if ($oQuestionGroup == null) {
            throw new PersistErrorException(
                "Object creation failed, input array malformed or invalid"
            );
        }
        // Always add at the end
        $oQuestionGroup->group_order = safecount($survey->groups) + 1;
        $saved = $oQuestionGroup->save();
        if (!$saved) {
            throw new PersistErrorException(
                "Object creation failed, couldn't save.\n ERRORS:"
                . print_r($oQuestionGroup->getErrors(), true)
            );
        }

        $l10N = [];
        foreach ($survey->allLanguages as $sLanguage) {
            $l10N[$sLanguage] = clone $this->modelQuestionGroupL10n;
            $l10N[$sLanguage]->setAttributes([
                'gid'         => $oQuestionGroup->gid,
                'language'    => $sLanguage,
                'group_name'  => '',
                'description' => '',
            ], false);
            $l10N[$sLanguage]->save();
        }

        return $oQuestionGroup;
    }

    /**
     * @param int $surveyId
     * @return Survey
     * @throws NotFoundException
     */
    private function getSurvey(int $surveyId)
    {
        $survey = $this->modelSurvey->findByPk($surveyId);
        if (!$survey) {
            throw new NotFoundException(
                'Survey does not exist',
            );
        }
        return $survey;
    }

    /**
     * Resets questionGroup model for cases
     * when multiple groups are created simultaneously
     * @return void
     */
    private function refreshModels()
    {
        $this->modelQuestionGroup->unsetAttributes();
        $this->modelQuestionGroup->setIsNewRecord(true);
    }

    /**
     * @param int $surveyId
     * @return void
     * @throws PermissionDeniedException
     */
    public function checkUpdatePermission(int $surveyId)
    {
        if (
            !$this->modelPermission->hasSurveyPermission(
                $surveyId,
                'surveycontent',
                'update'
            )
        ) {
            throw new PermissionDeniedException(
                'Permission denied'
            );
        }
    }

    /**
     * @param int $surveyId
     * @return void
     * @throws PermissionDeniedException
     */
    public function checkCreatePermission(int $surveyId)
    {
        if (
            !$this->modelPermission->hasSurveyPermission(
                $surveyId,
                'surveycontent',
                'create'
            )
        ) {
            throw new PermissionDeniedException(
                'Permission denied'
            );
        }
    }

    /**
     * @param int $surveyId
     * @return void
     * @throws PermissionDeniedException
     */
    public function checkDeletePermission(int $surveyId)
    {
        $survey = $this->modelSurvey->findByPk($surveyId);
        if (
            $survey->isActive ||
            !$this->modelPermission->hasSurveyPermission(
                $surveyId,
                'surveycontent',
                'delete'
            )
        ) {
            throw new PermissionDeniedException(
                gT('Access denied')
            );
        }
    }
}
