<?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\UserApplicationAttribute\Add;

use Sso\WebserviceBundle\Api\AuthorizationManagerInterface;
use Sso\WebserviceBundle\Api\Error\Storage as ErrorStorage;
use Sso\WebserviceBundle\ErrorHandler\ErrorHandlerInterface;
use Sso\WebserviceBundle\Exception\GeneratorExceptionInterface;
use Sso\WebserviceBundle\Exception\InvalidRequestException;
use Sso\WebserviceBundle\Generator\ApplicationAttribute\GeneratorInterface;
use Sso\WebserviceBundle\Services\HandlerInterface;
use Sso\WebserviceBundle\Services\RequestInterface;
use Sso\WebserviceBundle\Services\ResponseBuilderInterface as ServicesResponseBuilderInterface;
use Sso\WebserviceBundle\Database\Webservice\Manager as RepositoryManager;
use Sso\WebserviceBundle\Entity\Webservice\Type\User as ModelUser;
use Sso\WebserviceBundle\Entity\Webservice\Type\Application as ModelApplication;
use Sso\WebserviceBundle\Entity\Webservice\Type\UserApplication as ModelUserApplication;
use Sso\WebserviceBundle\Services\UserApplicationAttribute\Add\RequestData\User;
use Sso\WebserviceBundle\Services\UserApplicationAttribute\Add\RequestData\Attribute;
use Symfony\Component\Validator\Validator\ValidatorInterface;

/**
 * Class Handler
 *
 * @copyright  2018 Lifestyle Webconsulting GmbH
 * @link       http://www.life-style.de
 * @package Sso\WebserviceBundle\Services\UserApplicationAttribute\Add
 */
class Handler implements HandlerInterface
{
    /**
     * @var RequestParserInterface
     */
    private $requestParser;

    /**
     * @var ResponseBuilderInterface|ServicesResponseBuilderInterface
     */
    private $responseBuilder;

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

    /**
     * @var RepositoryManager
     */
    private $repositoryManager;

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

    /**
     * @var ValidatorInterface;
     */
    private $validator;

    /**
     * @var GeneratorInterface[]
     */
    private $attributeGenerators = [];

    /**
     * Handler constructor.
     * @param RequestParserInterface $requestParser
     * @param ServicesResponseBuilderInterface|ResponseBuilderInterface $responseBuilder
     * @param ErrorHandlerInterface $errorHandler
     * @param RepositoryManager $repositoryManager
     * @param AuthorizationManagerInterface $authorization
     * @param ValidatorInterface $validator
     */
    public function __construct(
        RequestParserInterface $requestParser,
        $responseBuilder,
        ErrorHandlerInterface $errorHandler,
        RepositoryManager $repositoryManager,
        AuthorizationManagerInterface $authorization,
        ValidatorInterface $validator
    ) {
        $this->requestParser = $requestParser;
        $this->responseBuilder = $responseBuilder;
        $this->errorHandler = $errorHandler;
        $this->repositoryManager = $repositoryManager;
        $this->authorization = $authorization;
        $this->validator = $validator;
    }

    /**
     * @param GeneratorInterface $generator
     */
    public function addAttributeGenerator(GeneratorInterface $generator): void
    {
        $this->attributeGenerators[] = $generator;
    }

    /**
     * @param RequestInterface $request
     * @return ResponseBuilderInterface
     */
    public function handle(RequestInterface $request)
    {
        try {
            $userType = $this->parseRequest($request);
            $user = $this->getUser($userType);
            $application = $this->getApplication($userType);
            $this->checkPermissions($application);
            $userApplication = $this->getUserApplication($user, $application);
            $userUpdateAttributes = $this->setGeneratedAttributes($userType->getApplication()->getAttributes());

            $attributeHandler = ApplicationAttributeListHandler::create(
                $this->validator,
                $application->getAttributes(),
                $userApplication->getAttributes(),
                $userUpdateAttributes
            );

            $this->addNewAttributes($attributeHandler, $userType, $application);
            $this->deleteUserApplicationAttributes($attributeHandler, $userApplication);
            $this->updateUserApplicationAttributes($attributeHandler, $userApplication, $application);
        } catch (InvalidRequestException $exception) {
            if (!$this->errorHandler->hasErrors()) {
                $this->errorHandler->addError(500, 'CriticalError', 'uaat100', '', '');
            }
        }

        return $this->responseBuilder;
    }

    /**
     * @param RequestInterface $request
     * @return User
     * @throws InvalidRequestException
     */
    private function parseRequest(RequestInterface $request): User
    {
        $userType = $this->requestParser->parse($request->getRequestBody());
        if ($this->errorHandler->hasErrors()) {
            throw new InvalidRequestException();
        }
        return $userType;
    }

    /**
     * @param User $userType
     * @return ModelUser
     * @throws InvalidRequestException
     */
    private function getUser(User $userType): ModelUser
    {
        $user = $this->findUser($userType);
        if (!$user instanceof ModelUser) {
            $this->errorHandler->addError(404, 'UserNotFound', 'uaat021', '', '');
            throw new InvalidRequestException();
        }
        return $user;
    }

    /**
     * @param User $userType
     * @return null|ModelUser
     */
    private function findUser(User $userType)
    {
        $user = null;
        if ($userType->hasGuid()) {
            $user = $this->repositoryManager->user()->getUserByGuid($userType->getGuid());
        } elseif ($userType->hasEmail()) {
            $user = $this->repositoryManager->user()->getUserByEmail($userType->getEmail());
        } elseif ($userType->hasUsername()) {
            $user = $this->repositoryManager->user()->getUserByUsername($userType->getUsername());
        } elseif ($userType->hasIdentifier()) {
            try {
                $user = $this->repositoryManager->user()->getUserByIdentifier($userType->getIdentifier());
            } catch (\Exception $exception) {
                $this->errorHandler->addError(500, 'CriticalError', 'uaat027',
                    'Error while fetching user by identifier', $exception->getMessage());
            }
        }
        return $user;
    }

    /**
     * @param User $userType
     * @return ModelApplication
     * @throws InvalidRequestException
     */
    private function getApplication(User $userType): ModelApplication
    {
        $application = $this->repositoryManager->application()->getApplicationByName(
            $userType->getApplication()->getName()
        );
        if (!$application instanceof ModelApplication) {
            $this->errorHandler->addError(400, 'ApplicationNotFound', 'uaat022', '', '');
            throw new InvalidRequestException();
        }
        return $application;
    }

    /**
     * @param ModelApplication $application
     * @throws InvalidRequestException
     */
    private function checkPermissions(ModelApplication $application): void
    {
        if (!$this->authorization->canWriteApplication($application->getName())) {
            $this->errorHandler->addError(403, 'ApplicationWriteAccessDenied', 'uaat023', '', '');
            throw new InvalidRequestException();
        }
    }

    /**
     * @param ModelUser $user
     * @param ModelApplication $application
     * @return ModelUserApplication
     * @throws InvalidRequestException
     */
    private function getUserApplication(ModelUser $user, ModelApplication $application): ModelUserApplication
    {
        $userApplication = $this->repositoryManager->userApplication()->getUserApplication($user, $application);
        if (!$userApplication instanceof ModelUserApplication) {
            $this->errorHandler->addError(400, 'UserApplicationNotFound', 'uaat024', '', '');
            throw new InvalidRequestException();
        }
        return $userApplication;
    }

    /**
     * @param Attribute[] $userApplicationAttributes
     * @return Attribute[]
     * @throws InvalidRequestException
     */
    private function setGeneratedAttributes(array $userApplicationAttributes): array
    {
        $userUpdateAttributes = [];
        foreach ($userApplicationAttributes as $userApplicationAttribute) {
            $userUpdateAttribute = clone $userApplicationAttribute;
            if ('generator' === $userUpdateAttribute->getType()) {
                $generator = $this->getGenerator($userUpdateAttribute->getValue());
                try {
                    $userUpdateAttribute->setValue($generator->generate());
                } catch (GeneratorExceptionInterface $exception) {
                    $this->errorHandler->addError(
                        500,
                        'AttributeGenerateValueError',
                        'uaat029',
                        $exception->getMessage(),
                        ''
                    );
                    throw new InvalidRequestException(
                        'Generate attribute value failed',
                        $exception->getCode(),
                        $exception
                    );
                }
            }
            $userUpdateAttributes[] = $userUpdateAttribute;
        }

        return $userUpdateAttributes;
    }

    /**
     * @param string $generatorName
     * @return GeneratorInterface
     * @throws InvalidRequestException
     */
    private function getGenerator(string $generatorName): GeneratorInterface
    {
        foreach ($this->attributeGenerators as $generator) {
            if ($generator->getName() === $generatorName) {
                return $generator;
            }
        }
        $this->errorHandler->addError(400, 'AttributeGeneratorNotFound', 'uaat026', '', '');
        throw new InvalidRequestException('Generator ' . $generatorName . ' does not exist.');
    }

    /**
     * @param ApplicationAttributeListHandler $attributeHandler
     * @param User $userType
     * @param ModelApplication $application
     * @return void
     * @throws InvalidRequestException
     */
    private function addNewAttributes(
        ApplicationAttributeListHandler $attributeHandler,
        User $userType,
        ModelApplication $application
    ): void {
        $newAttributes = $attributeHandler->getNewAttributes();
        if (0 === count($newAttributes)) {
            return;
        }

        // As no changes have been made to the database, we are able to throw an error here
        // if it is not allowed to add new attributes to the application
        if (!$userType->getApplication()->isForceAttributeAdd()) {
            $this->errorHandler->addError(400, 'AttributeAddNotForced', 'uaat027', '', '');
            throw new InvalidRequestException();
        }

        // The type for new attributes must be 'one' or 'many'. Generated values are not
        // allowed in the list of new application attributes
        $hasInvalidAttributes = false;
        foreach ($newAttributes as $applicationAttribute) {
            if (!in_array($applicationAttribute->getType(), ['one', 'many'])) {
                $this->errorHandler->addError(400, 'AttributeGeneratorTypeError', 'uaat028', '', '');
            }
        }
        if ($hasInvalidAttributes) {
            throw new InvalidRequestException();
        }

        foreach ($newAttributes as $applicationAttribute) {
            $applicationAttribute->setApplication($application);
            $application->addAttribute($applicationAttribute);
            if ($this->repositoryManager->attribute()->saveAttribute($applicationAttribute)) {
                if (!$this->repositoryManager->application()->saveApplication($application)) {
                    $this->addErrors($application->errors());
                }
            } else {
                $this->addErrors($applicationAttribute->errors());
            }
        }

        $this->repositoryManager->application()->saveApplication($application);
    }

    /**
     * @param ErrorStorage $errorStorage
     */
    private function addErrors(ErrorStorage $errorStorage): void
    {
        foreach ($errorStorage->getErrors() as $error) {
            $this->errorHandler->addError(
                500,
                $error->code,
                $error->ref,
                $error->shortMessage,
                $error->longMessage
            );
        }
    }

    /**
     * @param ApplicationAttributeListHandler $attributeHandler
     * @param ModelUserApplication $userApplication
     * @return void
     * @throws InvalidRequestException
     */
    private function deleteUserApplicationAttributes(
        ApplicationAttributeListHandler $attributeHandler,
        ModelUserApplication $userApplication
    ): void {
        $deletedAttributes = $attributeHandler->getDeletedUserApplicationAttributes();
        foreach ($deletedAttributes as $userApplicationAttribute) {
            $userApplication->removeAttribute($userApplicationAttribute);
        }
        if (!$this->repositoryManager->userApplicationAttribute()->deleteUserApplicationAttributes($deletedAttributes)) {
            foreach ($deletedAttributes as $attribute) {
                $this->addErrors($attribute->errors());
            }
            throw new InvalidRequestException();
        }
    }

    /**
     * @param ApplicationAttributeListHandler $attributeHandler
     * @param ModelUserApplication $userApplication
     * @param ModelApplication $application
     * @return void
     * @throws InvalidRequestException
     */
    private function updateUserApplicationAttributes(
        ApplicationAttributeListHandler $attributeHandler,
        ModelUserApplication $userApplication,
        ModelApplication $application
    ): void {
        $updatedAttributes = $attributeHandler->getUpdatedUserApplicationAttributes();
        foreach ($updatedAttributes as $userApplicationAttribute) {
            $userApplicationAttribute->setAttribute($application->getAttribute($userApplicationAttribute->getName()));
            $userApplicationAttribute->setUserApplication($userApplication);
            $userApplication->addAttribute($userApplicationAttribute);
        }
        if (!$this->repositoryManager->userApplicationAttribute()->saveUserApplicationAttributes($updatedAttributes)) {
            foreach ($updatedAttributes as $attribute) {
                $this->addErrors($attribute->errors());
            }
            throw new InvalidRequestException();
        }
    }
}
