<?php

/**
 * Lifestyle Webconsulting GmbH
 *
 * LICENSE: This Software is the property of Lifestyle Webconsulting GmbH (Aschaffenburg, Germany)
 * and is private by copyright law - it is NOT Freeware.
 *
 * Any unauthorized use of this software without a valid license
 * is a violation of the license agreement and will be prosecuted by
 * civil and criminal law.
 *
 * @copyright  2018 Lifestyle Webconsulting GmbH
 * @link       http://www.life-style.de
 */

namespace Sso\WebserviceBundle\Services\User\Update;

use Sso\WebserviceBundle\Api\Mapper\UserGroupMapper;
use Sso\WebserviceBundle\ErrorHandler\ErrorHandlerInterface;
use Sso\WebserviceBundle\Security\Authorisation\UserAuthorization;
use Sso\WebserviceBundle\Services\HandlerInterface;
use Sso\WebserviceBundle\Services\RequestInterface as ServiceRequestInterface;
use Sso\WebserviceBundle\Services\ResponseBuilderInterface as ServiceResponseBuilderInterface;
use Sso\WebserviceBundle\Database\Webservice\User as UserRepository;
use Sso\WebserviceBundle\Entity\Webservice\Type\User as ModelUser;
use Sso\WebserviceBundle\Services\User\Update\RequestData\User as RequestDataUser;
use LifeStyle\Tools\MfaBundle\MfaCreator;
use LifeStyle\Tools\MfaBundle\Api\Mfa\Image\MfaException;
use Sso\WebserviceBundle\Api\PasswordPolicy\History\Service as PasswordHistoryService;
use Sso\WebserviceBundle\Database\Webservice\PasswordPolicy as PasswordPolicyDatabase;

/**
 * Class Handler
 *
 * @copyright  2016 Lifestyle Webconsulting GmbH
 * @link       http://www.life-style.de
 * @package Sso\WebserviceBundle\Services\User\Update
 */
final class Handler implements HandlerInterface
{
    /**
     * @var Factory
     */
    private $factory;

    /**
     * @var UserRepository
     */
    private $database;

    /**
     * @var ErrorHandlerInterface
     */
    private $errorHandler;

    /**
     * @var MfaCreator
     */
    private $mfaCreator;

    /**
     * @var PasswordPolicyDatabase
     */
    private $passwordPolicyDatabase;

    /**
     * @var PasswordHistoryService
     */
    private $passwordHistoryService;

    /**
     * @var UserGroupMapper
     */
    private $userGroupMapper;

    /**
     * @var UserAuthorization
     */
    private $authorization;

    /**
     * Handler constructor.
     * @param Factory $factory
     * @param UserRepository $database
     * @param ErrorHandlerInterface $errorHandler
     * @param MfaCreator $mfaCreator
     * @param PasswordPolicyDatabase $passwordPolicyDatabase
     * @param PasswordHistoryService $passwordHistoryService
     * @param UserGroupMapper $userGroupMapper
     * @param UserAuthorization $authorization
     */
    public function __construct(
        Factory $factory,
        UserRepository $database,
        ErrorHandlerInterface $errorHandler,
        MfaCreator $mfaCreator,
        PasswordPolicyDatabase $passwordPolicyDatabase,
        PasswordHistoryService $passwordHistoryService,
        UserGroupMapper $userGroupMapper,
        UserAuthorization $authorization
    ) {
        $this->factory = $factory;
        $this->database = $database;
        $this->errorHandler = $errorHandler;
        $this->mfaCreator = $mfaCreator;
        $this->passwordPolicyDatabase = $passwordPolicyDatabase;
        $this->passwordHistoryService = $passwordHistoryService;
        $this->userGroupMapper = $userGroupMapper;
        $this->authorization = $authorization;
    }

    /**
     * @param ServiceRequestInterface $request
     * @throws MfaException
     * @return ServiceResponseBuilderInterface
     */
    public function handle(ServiceRequestInterface $request)
    {
        $requestDTO = $this->factory->request()->requestParser()->parse($request->getRequestBody());
        $responseBuilder = $this->factory->response()->responseBuilder();
        $userModel = null;

        if (!$this->errorHandler->hasErrors()) {
            //ok get user from db
            $userModel = $this->getUserModel($requestDTO);
            if (null === $userModel) {
                $this->errorHandler->addError(404, 'UserNotFound', 'u2012', '', '');
            }
        }

        // Authorize user update action
        if (!$this->errorHandler->hasErrors() && !$this->authorization->canUpdate($userModel)) {
            $this->errorHandler->addError(401, 'UserNotAuthorized', 'u2014', '', '');
        }

        if (!$this->errorHandler->hasErrors() && !empty($requestDTO->getUpdateData()->getPasswordPolicy())) {
            // check policy in db
            $passwordPolicy = $this->passwordPolicyDatabase->getOne($requestDTO->getUpdateData()->getPasswordPolicy());
            if (null === $passwordPolicy) {
                $this->errorHandler->addError(404, 'PasswordPolicyNotFound', 'u2013', '', '');
            }
        }

        if (!$this->errorHandler->hasErrors()) {
            //update the user from
            $userModel = $this->updateAndSaveUser($userModel, $requestDTO);
            if (!$userModel) {
                $this->errorHandler->addError(400, 'Cant Save User Model', 'u011', '', '');
            }
        }

        if (!$this->errorHandler->hasErrors()) {
            //ok next step set the responseDTO
            $responseData = $this->factory->responseData()->user()->setFromUserModel($userModel);
            $responseBuilder->setResponse($responseData);
        }

        return $responseBuilder;
    }

    /**
     * @param RequestDataUser $requestDTO
     * @throws \Exception
     * @return ModelUser|null
     */
    private function getUserModel(RequestDataUser $requestDTO)
    {
        $userModel = null;
        if ($identifier = $requestDTO->getIdentifier()) {
            $userModel = $this->database->getUserByIdentifier($identifier);
        }
        if (null === $userModel && ($guid = $requestDTO->getGuid())) {
            $userModel = $this->database->getUserByGuid($guid);
        }
        if (null === $userModel && ($username = $requestDTO->getUsername())) {
            $userModel = $this->database->getUserByUsername($username);
        }
        if (null === $userModel && ($email = $requestDTO->getEmail())) {
            $userModel = $this->database->getUserByEmail($email);
        }

        return $userModel;
    }

    /**
     * @param ModelUser $userModel
     * @param RequestDataUser $requestDTO
     * @throws MfaException
     * @return ModelUser|false
     */
    private function updateAndSaveUser(ModelUser $userModel, RequestDataUser $requestDTO)
    {
        $updateData = $requestDTO->getUpdateData();

        $updateData->getEmail() ? $userModel->setEmail($updateData->getEmail()) : null;
        $updateData->getUsername() ? $userModel->setUsername($updateData->getUsername()) : null;
        $updateData->getFirstname() ? $userModel->setFirstname($updateData->getFirstname()) : null;
        $updateData->getLastname() ? $userModel->setLastname($updateData->getLastname()) : null;
        $updateData->isActive() !== null ? $userModel->setActive($updateData->isActive()) : null;
        $updateData->isDeleted() !== null ? $userModel->setDeleted($updateData->isDeleted()) : null;
        $updateData->getAuthId() !== null ? $userModel->setAuthId($updateData->getAuthId()) : null;
        $updateData->getLdapSearchAttributes() !== null ? $userModel->setLdapSearchAttributes($updateData->getLdapSearchAttributes()) : null;
        $updateData->getLdapSearchValue() !== null ? $userModel->setLdapSearchValue($updateData->getLdapSearchValue()) : null;
        $updateData->isMfaEnabled() !== null ? $userModel->setMfaEnabled($updateData->isMfaEnabled()) : null;
        $updateData->isMfaRecreate() !== null ? $userModel->setMfaRecreate($updateData->isMfaRecreate()) : null;
        $updateData->isActivateTokenRecreate() !== null ? $userModel->setActivateTokenRecreate($updateData->isActivateTokenRecreate()) : null;
        $updateData->getPasswordEncryptType() !== null ? $userModel->setPasswordEncryptType($updateData->getPasswordEncryptType()) : null;
        $updateData->getPassword() !== null ? $userModel->setPassword($updateData->getPassword()) : null;
        $updateData->getPasswordPolicy() !== null ? $userModel->setPasswordPolicy($updateData->getPasswordPolicy()) : null;
        $updateData->getLastPasswordChange() !== null ? $userModel->setLastPasswordChange($updateData->getLastPasswordChange()) : null;
        $updateData->isPasswordExpired() !== null ? $userModel->setPasswordExpired($updateData->isPasswordExpired()) : null;
        $userModel = $this->userGroupMapper->syncUserGroupsByNames($userModel, $requestDTO->getUpdateData()->getUserGroups());

        $this->updateMfaSecret($userModel);
        $this->updateActivateToken($userModel);

        if (!$this->database->saveUser($userModel)) {
            $this->errorHandler->addErrors($userModel->errors()->getErrors());
            return false;
        }

        if ($updateData->getPassword() !== null) {
            $this->passwordHistoryService->addPassword(
                $userModel->getGuid(),
                $userModel->getPassword(),
                $userModel->getPasswordEncryptType()
            );
        }

        return $userModel;
    }

    /**
     * Update MFA secret if necessary
     *
     * @param ModelUser $user
     * @throws MfaException
     */
    private function updateMfaSecret(ModelUser $user)
    {
        if ($user->isMfaEnabled() && (0 === strlen($user->getMfaSecret()) || $user->isMfaRecreate())) {
            $user->setMfaSecret($this->mfaCreator->generateSecret());
        }
    }

    /**
     * Update ActivateToken if necessary
     *
     * @param ModelUser $user
     */
    private function updateActivateToken(ModelUser $user)
    {
        if (
            !$user->isActive() &&                    // Only inactive users are allowed to generate a new token
            0 < strlen($user->getActivateToken()) && // Manually disabled users do not have an activate-token. Do not allow them to activate their account!
            $user->hasActivateTokenExpired() &&      // No need to send a token if current token has not expired
            $user->isActivateTokenRecreate()         // Recreate token if flag is set from request
        ) {
            $user->generateActivateRequestToken();
        }
    }
}
