<?php

/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2019 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
 */

namespace oat\taoLti\controller;

use common_exception_BadRequest as BadRequestException;
use core_kernel_classes_Class as KernelClass;
use core_kernel_classes_Resource as KernelResource;
use oat\tao\model\controller\SignedFormInstance;
use oat\tao\model\oauth\DataStore;
use oat\taoLti\models\classes\ConsumerService;
use oat\taoLti\models\classes\Security\Business\Contract\SecretKeyServiceInterface;
use oat\taoLti\models\classes\Security\Business\Domain\Exception\SecretKeyGenerationException;
use tao_actions_form_CreateInstance as CreateInstanceContainer;
use tao_actions_SaSModule;
use tao_helpers_form_Form as Form;
use tao_helpers_form_FormFactory as FormFactory;
use tao_helpers_Uri as UriHelper;
use tao_models_classes_dataBinding_GenerisFormDataBinder as FormDataBinder;
use tao_models_classes_dataBinding_GenerisFormDataBindingException as FormDataBindingException;

/**
 * This controller allows the additon and deletion
 * of LTI Oauth Consumers
 *
 * @package taoLti
 * @license GPLv2 http://www.opensource.org/licenses/gpl-2.0.php
 */
class ConsumerAdmin extends tao_actions_SaSModule
{
    private const EXCLUDED_FIELDS = [
        'id',
        DataStore::PROPERTY_OAUTH_SECRET,
        DataStore::PROPERTY_OAUTH_CALLBACK,
    ];

    private const SECRET_REGENERATION_KEY = 'regenerate_secret';

    /** @var KernelClass|null */
    private $currentClass;

    /** @var KernelResource|null */
    private $currentInstance;

    /**
     * @inheritDoc
     *
     * @throws SecretKeyGenerationException
     */
    public function addInstanceForm(): void
    {
        if (!$this->isXmlHttpRequest()) {
            throw new BadRequestException('wrong request mode');
        }

        $form = $this->createNewInstanceForm();

        $this->handleNewInstanceSubmission($form);

        $this->setData('formTitle', __('Create instance of ') . $this->getCurrentClass()->getLabel());
        $this->setData('myForm', $form->render());

        $this->setView('form.tpl', 'tao');
    }

    /**
     * @inheritDoc
     *
     * @throws FormDataBindingException
     */
    public function editInstance(): void
    {
        $form = $this->createExistingInstanceForm();

        $this->handleExistingInstanceSubmission($form);

        $this->setData('formTitle', __('Edit Instance'));
        $this->setData('myForm', $form->render());
        $this->setView('form.tpl', 'tao');
    }

    /**
     * @inheritDoc
     */
    protected function getClassService()
    {
        return $this->getServiceLocator()->get(ConsumerService::class);
    }

    /**
     * @inheritDoc
     */
    protected function getCurrentClass(): KernelClass
    {
        if (null === $this->currentClass) {
            $this->currentClass = parent::getCurrentClass();
        }

        return $this->currentClass;
    }

    /**
     * @noinspection PhpDocMissingThrowsInspection
     *
     * @param string $parameterName
     *
     * @return KernelResource
     */
    protected function getCurrentInstance($parameterName = 'uri'): KernelResource
    {
        if (null === $this->currentInstance) {
            /** @noinspection PhpUnhandledExceptionInspection */
            $this->currentInstance = parent::getCurrentInstance($parameterName);
        }

        return $this->currentInstance;
    }

    private function createNewInstanceForm(): Form
    {
        $formContainer = new CreateInstanceContainer(
            [$this->getCurrentClass()],
            [
                CreateInstanceContainer::CSRF_PROTECTION_OPTION => true,
                'excludedProperties'                            => self::EXCLUDED_FIELDS,
            ]
        );

        return $formContainer->getForm();
    }

    private function createExistingInstanceForm(): Form
    {
        $myFormContainer = new SignedFormInstance(
            $this->getCurrentClass(),
            $this->getCurrentInstance(),
            [SignedFormInstance::CSRF_PROTECTION_OPTION => true]
        );

        $form = $myFormContainer->getForm();

        $this->enrichWithSecretField($form);
        $this->enrichWithRegenerateSecretAction($form);
        $this->enrichWithRegenerateSecretDisclaimer($form);

        return $form;
    }

    private function enrichWithSecretField(Form $form): void
    {
        $secret = $form->getElement(UriHelper::encode(DataStore::PROPERTY_OAUTH_SECRET));
        $secret->addAttribute('readonly', 'readonly');
        $secret->addClass('copy-to-clipboard');
        $secret->addAttribute(
            'data-copy-success-feedback',
            __('Oauth consumer secret has been copied to the clipboard')
        );

        foreach (self::EXCLUDED_FIELDS as $excludedField) {
            $form->removeElement(UriHelper::encode($excludedField));
        }

        $form->addElement($secret);
    }

    private function enrichWithRegenerateSecretAction(Form $form): void
    {
        /** @noinspection PhpUnhandledExceptionInspection */
        $regenerateButton = FormFactory::getElement(self::SECRET_REGENERATION_KEY, 'Button');
        $regenerateButton->addClass('small form-submitter');
        $regenerateButton->addAttribute('style', 'width: 100%;');
        $regenerateButton->setValue(__('Generate a new secret key'));

        $form->addElement($regenerateButton);
    }

    private function enrichWithRegenerateSecretDisclaimer(Form $form): void
    {
        /** @noinspection PhpUnhandledExceptionInspection */
        $regenerateDisclaimer = FormFactory::getElement('regenerate_disclaimer', 'Free');
        $regenerateDisclaimer->setValue(
            __('Please consider that generating a new key will replace the previously generated one.')
        );

        $form->addElement($regenerateDisclaimer);
    }

    /**
     * @param Form $form
     *
     * @throws SecretKeyGenerationException
     */
    private function handleNewInstanceSubmission(Form $form): void
    {
        if ($form->isSubmited() && $form->isValid()) {
            $values                                   = $this->extractValues($form);
            $values[DataStore::PROPERTY_OAUTH_SECRET] = $this->getSecretKeyService()->generate();

            $instance = $this->createInstance([$this->getCurrentClass()], $values);

            $this->setData('message', __('%s created', $instance->getLabel()));
            $this->setData('reload', true);
        }
    }

    /**
     * @param Form $form
     *
     * @throws FormDataBindingException
     */
    private function handleExistingInstanceSubmission(Form $form): void
    {
        if ($form->isSubmited() && $form->isValid()) {
            $values = $this->extractValues($form);

            $requestBody = $this->getPsrRequest()->getParsedBody();

            if (!empty($requestBody[self::SECRET_REGENERATION_KEY])) {
                $values[DataStore::PROPERTY_OAUTH_SECRET] = $this->getSecretKeyService()->generate();
            }

            $this->updateCurrentInstance($values);

            $this->setData('message', __('Instance saved'));
            $this->setData('reload', true);
        }
    }

    private function extractValues(Form $form): array
    {
        $values = $form->getValues();
        $values = array_diff_key($values, array_flip(self::EXCLUDED_FIELDS));

        return $values;
    }

    /**
     * @param array $values
     *
     * @throws FormDataBindingException
     */
    private function updateCurrentInstance(array $values): void
    {
        (new FormDataBinder($this->getCurrentInstance()))->bind($values);
    }

    private function getSecretKeyService(): SecretKeyServiceInterface
    {
        /** @noinspection PhpIncompatibleReturnTypeInspection */
        return $this->getServiceLocator()->get(SecretKeyServiceInterface::class);
    }
}