<?php

/*
 * This file is part of the Sylius package.
 *
 * (c) Paweł Jędrzejewski
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace Sylius\Bundle\UserBundle\Controller;

use FOS\RestBundle\View\View;
use Sylius\Bundle\ResourceBundle\Controller\RequestConfiguration;
use Sylius\Bundle\ResourceBundle\Controller\ResourceController;
use Sylius\Bundle\UserBundle\Form\Model\ChangePassword;
use Sylius\Bundle\UserBundle\Form\Model\PasswordReset;
use Sylius\Bundle\UserBundle\Form\Model\PasswordResetRequest;
use Sylius\Bundle\UserBundle\Form\Type\UserChangePasswordType;
use Sylius\Bundle\UserBundle\Form\Type\UserRequestPasswordResetType;
use Sylius\Bundle\UserBundle\Form\Type\UserResetPasswordType;
use Sylius\Bundle\UserBundle\UserEvents;
use Sylius\Component\User\Model\UserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Sylius\Component\User\Security\Generator\GeneratorInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Webmozart\Assert\Assert;

class UserController extends ResourceController
{
    public function changePasswordAction(Request $request): Response
    {
        $configuration = $this->requestConfigurationFactory->create($this->metadata, $request);

        if (!$this->container->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
            throw new AccessDeniedException('You have to be registered user to access this section.');
        }

        $user = $this->container->get('security.token_storage')->getToken()->getUser();

        $changePassword = new ChangePassword();
        $formType = $this->getSyliusAttribute($request, 'form', UserChangePasswordType::class);
        $form = $this->createResourceForm($configuration, $formType, $changePassword);

        if (in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && $form->handleRequest($request)->isValid()) {
            return $this->handleChangePassword($request, $configuration, $user, $changePassword->getNewPassword());
        }

        if (!$configuration->isHtmlRequest()) {
            return $this->viewHandler->handle($configuration, View::create($form, Response::HTTP_BAD_REQUEST));
        }

        return new Response($this->container->get('twig')->render(
            $configuration->getTemplate('changePassword.html'),
            ['form' => $form->createView()]
        ));
    }

    public function requestPasswordResetTokenAction(Request $request): Response
    {
        /** @var GeneratorInterface $generator */
        $generator = $this->container->get(sprintf('sylius.%s.token_generator.password_reset', $this->metadata->getName()));

        return $this->prepareResetPasswordRequest($request, $generator, UserEvents::REQUEST_RESET_PASSWORD_TOKEN);
    }

    public function requestPasswordResetPinAction(Request $request): Response
    {
        /** @var GeneratorInterface $generator */
        $generator = $this->container->get(sprintf('sylius.%s.pin_generator.password_reset', $this->metadata->getName()));

        return $this->prepareResetPasswordRequest($request, $generator, UserEvents::REQUEST_RESET_PASSWORD_PIN);
    }

    public function resetPasswordAction(Request $request, string $token): Response
    {
        $configuration = $this->requestConfigurationFactory->create($this->metadata, $request);
        /** @var UserInterface|null $user */
        $user = $this->repository->findOneBy(['passwordResetToken' => $token]);
        if (null === $user) {
            throw new NotFoundHttpException('Token not found.');
        }

        $resetting = $this->metadata->getParameter('resetting');
        $lifetime = new \DateInterval($resetting['token']['ttl']);
        if (!$user->isPasswordRequestNonExpired($lifetime)) {
            return $this->handleExpiredToken($request, $configuration, $user);
        }

        $passwordReset = new PasswordReset();
        $formType = $this->getSyliusAttribute($request, 'form', UserResetPasswordType::class);
        $form = $this->createResourceForm($configuration, $formType, $passwordReset);

        if (in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && $form->handleRequest($request)->isValid()) {
            return $this->handleResetPassword($request, $configuration, $user, $passwordReset->getPassword());
        }

        if (!$configuration->isHtmlRequest()) {
            return $this->viewHandler->handle($configuration, View::create($form, Response::HTTP_BAD_REQUEST));
        }

        return new Response($this->container->get('twig')->render(
            $configuration->getTemplate('resetPassword.html'),
            [
                'form' => $form->createView(),
                'user' => $user,
            ]
        ));
    }

    public function verifyAction(Request $request, string $token): Response
    {
        $configuration = $this->requestConfigurationFactory->create($this->metadata, $request);
        $redirectRoute = $this->getSyliusAttribute($request, 'redirect', null);

        $response = $this->redirectToRoute($redirectRoute);

        /** @var UserInterface|null $user */
        $user = $this->repository->findOneBy(['emailVerificationToken' => $token]);
        if (null === $user) {
            if (!$configuration->isHtmlRequest()) {
                return $this->viewHandler->handle($configuration, View::create($configuration, Response::HTTP_BAD_REQUEST));
            }

            $this->addTranslatedFlash('error', 'sylius.user.verify_email_by_invalid_token');

            return $this->redirectToRoute($redirectRoute);
        }

        $user->setVerifiedAt(new \DateTime());
        $user->setEmailVerificationToken(null);
        $user->enable();

        $eventDispatcher = $this->container->get('event_dispatcher');
        $eventDispatcher->dispatch(new GenericEvent($user), UserEvents::PRE_EMAIL_VERIFICATION);

        $this->manager->flush();

        $eventDispatcher->dispatch(new GenericEvent($user), UserEvents::POST_EMAIL_VERIFICATION);

        if (!$configuration->isHtmlRequest()) {
            return $this->viewHandler->handle($configuration, View::create($user));
        }

        $flashMessage = $this->getSyliusAttribute($request, 'flash', 'sylius.user.verify_email');
        $this->addTranslatedFlash('success', $flashMessage);

        return $response;
    }

    public function requestVerificationTokenAction(Request $request): Response
    {
        $configuration = $this->requestConfigurationFactory->create($this->metadata, $request);
        $redirectRoute = $this->getSyliusAttribute($request, 'redirect', 'referer');

        $user = $this->getUser();
        if (null === $user) {
            if (!$configuration->isHtmlRequest()) {
                return $this->viewHandler->handle($configuration, View::create($configuration, Response::HTTP_UNAUTHORIZED));
            }

            $this->addTranslatedFlash('notice', 'sylius.user.verify_no_user');

            return $this->redirectHandler->redirectToRoute($configuration, $redirectRoute);
        }

        if (null !== $user->getVerifiedAt()) {
            if (!$configuration->isHtmlRequest()) {
                return $this->viewHandler->handle($configuration, View::create($configuration, Response::HTTP_BAD_REQUEST));
            }

            $this->addTranslatedFlash('notice', 'sylius.user.verify_verified_email');

            return $this->redirectHandler->redirectToRoute($configuration, $redirectRoute);
        }

        /** @var GeneratorInterface $tokenGenerator */
        $tokenGenerator = $this->container->get(sprintf('sylius.%s.token_generator.email_verification', $this->metadata->getName()));
        $user->setEmailVerificationToken($tokenGenerator->generate());

        $this->manager->flush();

        $eventDispatcher = $this->container->get('event_dispatcher');
        $eventDispatcher->dispatch(new GenericEvent($user), UserEvents::REQUEST_VERIFICATION_TOKEN);

        if (!$configuration->isHtmlRequest()) {
            return $this->viewHandler->handle($configuration, View::create(null, Response::HTTP_NO_CONTENT));
        }

        $this->addTranslatedFlash('success', 'sylius.user.verify_email_request');

        return $this->redirectHandler->redirectToRoute($configuration, $redirectRoute);
    }

    protected function prepareResetPasswordRequest(Request $request, GeneratorInterface $generator, string $senderEvent): Response
    {
        $configuration = $this->requestConfigurationFactory->create($this->metadata, $request);

        $passwordReset = new PasswordResetRequest();
        $formType = $this->getSyliusAttribute($request, 'form', UserRequestPasswordResetType::class);
        $form = $this->createResourceForm($configuration, $formType, $passwordReset);
        $template = $this->getSyliusAttribute($request, 'template', null);
        if ($configuration->isHtmlRequest()) {
            Assert::notNull($template, 'Template is not configured.');
        }

        if (in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && $form->handleRequest($request)->isValid()) {
            $userRepository = $this->repository;

            /** @var UserRepositoryInterface $userRepository */
            Assert::isInstanceOf($userRepository, UserRepositoryInterface::class);

            $user = $userRepository->findOneByEmail($passwordReset->getEmail());
            if (null !== $user) {
                $this->handleResetPasswordRequest($generator, $user, $senderEvent);
            }

            if (!$configuration->isHtmlRequest()) {
                return $this->viewHandler->handle($configuration, View::create(null, Response::HTTP_NO_CONTENT));
            }

            $this->addTranslatedFlash('success', 'sylius.user.reset_password_request');
            $redirectRoute = $this->getSyliusAttribute($request, 'redirect', null);
            Assert::notNull($redirectRoute, 'Redirect is not configured.');

            if (is_array($redirectRoute)) {
                return $this->redirectHandler->redirectToRoute(
                    $configuration,
                    $configuration->getParameters()->get('redirect')['route'],
                    $configuration->getParameters()->get('redirect')['parameters']
                );
            }

            return $this->redirectHandler->redirectToRoute($configuration, $redirectRoute);
        }

        if (!$configuration->isHtmlRequest()) {
            return $this->viewHandler->handle($configuration, View::create($form, Response::HTTP_BAD_REQUEST));
        }

        return new Response($this->container->get('twig')->render(
            $template,
            [
                'form' => $form->createView(),
            ]
        ));
    }

    protected function addTranslatedFlash(string $type, string $message): void
    {
        $translator = $this->container->get('translator');
        $this->container->get('session')->getFlashBag()->add($type, $translator->trans($message, [], 'flashes'));
    }

    /**
     * @param object $object
     */
    protected function createResourceForm(
        RequestConfiguration $configuration,
        string $type,
        $object
    ): FormInterface {
        if (!$configuration->isHtmlRequest()) {
            return $this->container->get('form.factory')->createNamed('', $type, $object, ['csrf_protection' => false]);
        }

        return $this->container->get('form.factory')->create($type, $object);
    }

    protected function handleExpiredToken(Request $request, RequestConfiguration $configuration, UserInterface $user): Response
    {
        $user->setPasswordResetToken(null);
        $user->setPasswordRequestedAt(null);

        $this->manager->flush();

        if (!$configuration->isHtmlRequest()) {
            return $this->viewHandler->handle($configuration, View::create($user, Response::HTTP_BAD_REQUEST));
        }

        $this->addTranslatedFlash('error', 'sylius.user.expire_password_reset_token');

        $redirectRouteName = $this->getSyliusAttribute($request, 'redirect', null);
        Assert::notNull($redirectRouteName, 'Redirect is not configured.');

        return new RedirectResponse($this->container->get('router')->generate($redirectRouteName));
    }

    protected function handleResetPasswordRequest(
        GeneratorInterface $generator,
        UserInterface $user,
        string $senderEvent
    ): void {
        $user->setPasswordResetToken($generator->generate());
        $user->setPasswordRequestedAt(new \DateTime());

        // I have to use doctrine manager directly, because domain manager functions add a flash messages. I can't get rid of them.
        $manager = $this->container->get('doctrine.orm.default_entity_manager');
        $manager->persist($user);
        $manager->flush();

        $dispatcher = $this->container->get('event_dispatcher');
        $dispatcher->dispatch(new GenericEvent($user), $senderEvent);
    }

    protected function handleResetPassword(
        Request $request,
        RequestConfiguration $configuration,
        UserInterface $user,
        string $newPassword
    ): Response {
        $user->setPlainPassword($newPassword);
        $user->setPasswordResetToken(null);
        $user->setPasswordRequestedAt(null);

        $dispatcher = $this->container->get('event_dispatcher');
        $dispatcher->dispatch(new GenericEvent($user), UserEvents::PRE_PASSWORD_RESET);

        $this->manager->flush();
        $this->addTranslatedFlash('success', 'sylius.user.reset_password');

        $dispatcher->dispatch(new GenericEvent($user), UserEvents::POST_PASSWORD_RESET);

        if (!$configuration->isHtmlRequest()) {
            return $this->viewHandler->handle($configuration, View::create(null, Response::HTTP_NO_CONTENT));
        }

        $redirectRouteName = $this->getSyliusAttribute($request, 'redirect', null);
        Assert::notNull($redirectRouteName, 'Redirect is not configured.');

        return new RedirectResponse($this->container->get('router')->generate($redirectRouteName));
    }

    protected function handleChangePassword(
        Request $request,
        RequestConfiguration $configuration,
        UserInterface $user,
        string $newPassword
    ): Response {
        $user->setPlainPassword($newPassword);

        $dispatcher = $this->container->get('event_dispatcher');
        $dispatcher->dispatch(new GenericEvent($user), UserEvents::PRE_PASSWORD_CHANGE);

        $this->manager->flush();
        $this->addTranslatedFlash('success', 'sylius.user.change_password');

        $dispatcher->dispatch(new GenericEvent($user), UserEvents::POST_PASSWORD_CHANGE);

        if (!$configuration->isHtmlRequest()) {
            return $this->viewHandler->handle($configuration, View::create(null, Response::HTTP_NO_CONTENT));
        }

        $redirectRouteName = $this->getSyliusAttribute($request, 'redirect', null);
        Assert::notNull($redirectRouteName, 'Redirect is not configured.');

        return new RedirectResponse($this->container->get('router')->generate($redirectRouteName));
    }

    protected function getUser(): ?UserInterface
    {
        $user = parent::getUser();
        $authorizationChecker = $this->container->get('security.authorization_checker');

        if (
            $authorizationChecker->isGranted('IS_AUTHENTICATED_REMEMBERED') &&
            $user instanceof UserInterface
        ) {
            return $user;
        }

        return null;
    }

    private function getSyliusAttribute(Request $request, string $attribute, $default = null)
    {
        $attributes = $request->attributes->get('_sylius');

        return $attributes[$attribute] ?? $default;
    }
}
