<?php
declare(strict_types=1);

/**
 * BEdita, API-first content management framework
 * Copyright 2017 ChannelWeb Srl, Chialab Srl
 *
 * This file is part of BEdita: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
 */

namespace BEdita\Core\Model\Action;

use BEdita\Core\Exception\InvalidDataException;
use BEdita\Core\Mailer\UserMailerTrait;
use BEdita\Core\Model\Entity\AsyncJob;
use BEdita\Core\Model\Entity\User;
use BEdita\Core\Model\Validation\Validation;
use Cake\Database\Expression\QueryExpression;
use Cake\Event\EventDispatcherTrait;
use Cake\Event\EventInterface;
use Cake\Event\EventListenerInterface;
use Cake\I18n\FrozenTime;
use Cake\ORM\TableRegistry;
use Cake\Validation\Validator;

/**
 * Command to request an access credentials change
 * Typically a password change
 *
 * Input data MUST contain:
 *  - 'contact' user email, other contact methods in the future
 *  - 'change_url' base URL to use in email sent to user, actual change URL link will be
 *      {change_url}?uuid={uuid}
 *
 * @since 4.0.0
 */
class ChangeCredentialsRequestAction extends BaseAction implements EventListenerInterface
{
    use EventDispatcherTrait;
    use UserMailerTrait;

    /**
     * The UsersTable table
     *
     * @var \BEdita\Core\Model\Table\UsersTable
     */
    protected $Users;

    /**
     * The AsyncJobs table
     *
     * @var \BEdita\Core\Model\Table\AsyncJobsTable
     */
    protected $AsyncJobs;

    /**
     * @inheritDoc
     */
    protected function initialize(array $config)
    {
        $this->Users = TableRegistry::getTableLocator()->get('Users');
        $this->AsyncJobs = TableRegistry::getTableLocator()->get('AsyncJobs');

        $this->getEventManager()->on($this);
    }

    /**
     * @inheritDoc
     */
    public function execute(array $data = [])
    {
        $errors = $this->validate($data);
        if ($errors !== true) {
            throw new InvalidDataException(__d('bedita', 'Invalid data'), $errors);
        }

        // operations are not in transaction: every failure stops following operations
        // and there's nothing to rollback
        $user = $this->getUser($data['contact']);
        $job = $this->createJob($user);
        $changeUrl = $this->getChangeUrl($job->uuid, $data['change_url']);

        $this->dispatchEvent('Auth.credentialsChangeRequest', [$user, $job, $changeUrl], $this->Users);

        return true;
    }

    /**
     * Validate input data.
     *
     * @param array $data Input
     * @return array|true Array of validation errors, or true if input is valid.
     */
    public function validate(array $data)
    {
        $validator = new Validator();
        $validator->setProvider('bedita', Validation::class);

        $validator->email('contact')
            ->notEmptyString('contact')
            ->requirePresence('contact')

            ->notEmptyString('change_url')
            ->requirePresence('change_url')
            ->add('activation_url', 'customUrl', [
                'rule' => 'url',
                'provider' => 'bedita',
            ]);

        $errors = $validator->validate($data);
        if (empty($errors)) {
            return true;
        }

        return $errors;
    }

    /**
     * Get user.
     *
     * @param string $contact Contact method (email).
     * @return \BEdita\Core\Model\Entity\User
     */
    protected function getUser($contact)
    {
        $user = $this->Users->find()
            ->where(function (QueryExpression $exp) use ($contact) {
                return $exp->eq($this->Users->aliasField('email'), $contact);
            })
            ->firstOrFail();

        return $user;
    }

    /**
     * Create the credentials change async job
     *
     * @param \BEdita\Core\Model\Entity\User $user The user requesting change
     * @return \BEdita\Core\Model\Entity\AsyncJob
     */
    protected function createJob(User $user)
    {
        $asyncJobsTable = TableRegistry::getTableLocator()->get('AsyncJobs');
        $action = new SaveEntityAction(['table' => $asyncJobsTable]);

        return $action([
            'entity' => $asyncJobsTable->newEntity([]),
            'data' => [
                'service' => 'credentials_change',
                'payload' => [
                    'user_id' => $user->id,
                ],
                'scheduled_from' => new FrozenTime('1 day'),
                'priority' => 1,
            ],
        ]);
    }

    /**
     * Send change request email to user
     *
     * @param \Cake\Event\EventInterface $event Dispatched event.
     * @param \BEdita\Core\Model\Entity\User $user The user.
     * @param \BEdita\Core\Model\Entity\AsyncJob $asyncJob Async job.
     * @param string $changeUrl Change URL
     * @return void
     */
    public function sendMail(EventInterface $event, User $user, AsyncJob $asyncJob, $changeUrl)
    {
        $options = [
            'params' => compact('user', 'changeUrl'),
        ];
        $this->getUserMailer()->send('changeRequest', [$options]);
    }

    /**
     * Return the credentials change url
     *
     * @param string $uuid Change uuid
     * @param string $changeUrl Base change URL
     * @return string
     */
    protected function getChangeUrl($uuid, $changeUrl)
    {
        $changeUrl .= strpos($changeUrl, '?') === false ? '?' : '&';

        return sprintf('%suuid=%s', $changeUrl, $uuid);
    }

    /**
     * @inheritDoc
     */
    public function implementedEvents(): array
    {
        return [
            'Auth.credentialsChangeRequest' => 'sendMail',
        ];
    }
}
