<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\MainBundle\Controller;

use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\UserPasswordType;
use Chill\MainBundle\Security\ChillSecurity;
use Chill\MainBundle\Security\PasswordRecover\PasswordRecoverEvent;
use Chill\MainBundle\Security\PasswordRecover\PasswordRecoverVoter;
use Chill\MainBundle\Security\PasswordRecover\RecoverPasswordHelper;
use Chill\MainBundle\Security\PasswordRecover\TokenManager;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
 * Class PasswordController.
 */
final class PasswordController extends AbstractController
{
    /**
     * PasswordController constructor.
     */
    public function __construct(private readonly LoggerInterface $chillLogger, private readonly \Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface $passwordEncoder, private readonly RecoverPasswordHelper $recoverPasswordHelper, private readonly TokenManager $tokenManager, private readonly TranslatorInterface $translator, private readonly EventDispatcherInterface $eventDispatcher, private readonly ChillSecurity $security, private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry) {}

    /**
     * @return Response
     */
    #[Route(path: '/public/{_locale}/password/request-changed', name: 'password_request_recover_changed')]
    public function changeConfirmedAction()
    {
        return $this->render('@ChillMain/Password/recover_password_changed.html.twig');
    }

    #[Route(path: '/public/{_locale}/password/recover', name: 'password_recover')]
    public function recoverAction(Request $request): Response|\Symfony\Component\HttpFoundation\RedirectResponse
    {
        if (false === $this->isGranted(PasswordRecoverVoter::ASK_TOKEN)) {
            return new Response($this->translator->trans('You are not allowed '
                .'to try to recover password, due to mitigating possible '
                .'attack. Try to contact your system administrator'), Response::HTTP_FORBIDDEN);
        }

        $query = $request->query;
        $username = $query->get(TokenManager::USERNAME_CANONICAL);
        $hash = $query->getAlnum(TokenManager::HASH);
        $token = $query->getAlnum(TokenManager::TOKEN);
        $timestamp = $query->getAlnum(TokenManager::TIMESTAMP);
        $user = $this->managerRegistry->getRepository(User::class)
            ->findOneByUsernameCanonical($username);

        if (null === $user) {
            $this->eventDispatcher->dispatch(
                new PasswordRecoverEvent($token, null, $request->getClientIp()),
                PasswordRecoverEvent::INVALID_TOKEN
            );

            throw $this->createNotFoundException(sprintf('User %s not found', $username));
        }

        if (true !== $this->tokenManager->verify($hash, $token, $user, $timestamp)) {
            $this->eventDispatcher->dispatch(
                new PasswordRecoverEvent($token, $user, $request->getClientIp()),
                PasswordRecoverEvent::INVALID_TOKEN
            );

            return new Response('Invalid token', Response::HTTP_FORBIDDEN);
        }

        $form = $this->passwordForm($user);
        $form->remove('actual_password');

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $password = $form->get('new_password')->getData();
            $user->setPassword($this->passwordEncoder->hashPassword($user, $password));
            // logging for prod
            $this
                ->chillLogger
                ->notice(
                    'setting new password for user',
                    [
                        'user' => $user->getUsername(),
                    ]
                );

            $this->managerRegistry->getManager()->flush();

            return $this->redirectToRoute('password_request_recover_changed');
        }

        return $this->render('@ChillMain/Password/recover_password_form.html.twig', [
            'form' => $form->createView(),
        ]);
    }

    /**
     * @throws \Doctrine\ORM\NoResultException
     * @throws \Doctrine\ORM\NonUniqueResultException
     */
    #[Route(path: '/public/{_locale}/password/request-recover', name: 'password_request_recover')]
    public function requestRecoverAction(Request $request): Response|\Symfony\Component\HttpFoundation\RedirectResponse
    {
        if (false === $this->isGranted(PasswordRecoverVoter::ASK_TOKEN)) {
            return new Response($this->translator->trans('You are not allowed '
                .'to try to recover password, due to mitigating possible '
                .'attack. Try to contact your system administrator'), Response::HTTP_FORBIDDEN);
        }

        $form = $this->requestRecoverForm();

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            /** @var \Doctrine\ORM\QueryBuilder $qb */
            $qb = $this->managerRegistry->getManager()
                ->createQueryBuilder();
            $qb->select('u')
                ->from(User::class, 'u')
                ->where($qb->expr()->eq('u.usernameCanonical', 'UNACCENT(LOWER(:pattern))'))
                ->orWhere($qb->expr()->eq('u.emailCanonical', 'UNACCENT(LOWER(:pattern))'))
                ->setParameter('pattern', $form->get('username_or_email')->getData());

            $user = $qb->getQuery()->getSingleResult();

            if (empty($user->getEmail())) {
                $this->addFlash('error', $this->translator->trans('This account does not have an email address. '
                    .'Please ask your administrator to renew your password.'));
            } else {
                if (false === $this->isGranted(PasswordRecoverVoter::ASK_TOKEN, $user)) {
                    return new Response($this->translator->trans('You are not allowed '
                        .'to try to recover password, due to mitigating possible '
                        .'attack. Try to contact your system administrator'), Response::HTTP_FORBIDDEN);
                }

                $this->recoverPasswordHelper->sendRecoverEmail(
                    $user,
                    (new \DateTimeImmutable('now'))->add(new \DateInterval('PT30M'))
                );

                // logging for prod
                $this
                    ->chillLogger
                    ->notice(
                        'Sending an email for password recovering',
                        [
                            'user' => $user->getUsername(),
                        ]
                    );

                $this->eventDispatcher->dispatch(
                    new PasswordRecoverEvent(null, $user, $request->getClientIp()),
                    PasswordRecoverEvent::ASK_TOKEN_SUCCESS
                );

                return $this->redirectToRoute('password_request_recover_confirm');
            }
        } elseif ($form->isSubmitted() && false === $form->isValid()) {
            $this->eventDispatcher->dispatch(
                new PasswordRecoverEvent(null, null, $request->getClientIp()),
                PasswordRecoverEvent::ASK_TOKEN_INVALID_FORM
            );
        }

        return $this->render('@ChillMain/Password/request_recover_password.html.twig', [
            'form' => $form->createView(),
        ]);
    }

    /**
     * @return Response
     */
    #[Route(path: '/public/{_locale}/password/request-confirm', name: 'password_request_recover_confirm')]
    public function requestRecoverConfirmAction()
    {
        return $this->render('@ChillMain/Password/request_recover_password_confirm.html.twig');
    }

    /**
     * @return Response
     */
    #[Route(path: '/{_locale}/my/password', name: 'change_my_password')]
    public function UserPasswordAction(Request $request)
    {
        if (!$this->security->isGranted('ROLE_USER')) {
            throw new AccessDeniedHttpException();
        }
        // get authentified user
        $user = $this->security->getUser();

        // create a form for password_encoder
        $form = $this->passwordForm($user);

        // process the form
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $password = $form->get('new_password')->getData();

            // logging for prod
            $this
                ->chillLogger
                ->notice(
                    'update password for an user',
                    [
                        'method' => $request->getMethod(),
                        'user' => $user->getUserIdentifier(),
                    ]
                );

            $user->setPassword($this->passwordEncoder->hashPassword($user, $password));

            $em = $this->managerRegistry->getManager();
            $em->flush();

            $this->addFlash('success', $this->translator->trans('Password successfully updated!'));

            return $this->redirectToRoute('change_my_password');
        }

        // render into a template
        return $this->render('@ChillMain/Password/password.html.twig', [
            'form' => $form->createView(),
        ]);
    }

    /**
     * @return \Symfony\Component\Form\FormInterface
     */
    protected function requestRecoverForm()
    {
        $builder = $this->createFormBuilder();
        $builder
            ->add('username_or_email', TextType::class, [
                'label' => 'Username or email',
                'constraints' => [
                    new Callback([
                        'callback' => function ($pattern, ExecutionContextInterface $context, $payload) {
                            $qb = $this->managerRegistry->getManager()
                                ->createQueryBuilder();
                            $qb->select('COUNT(u)')
                                ->from(User::class, 'u')
                                ->where($qb->expr()->eq('u.usernameCanonical', 'UNACCENT(LOWER(:pattern))'))
                                ->orWhere($qb->expr()->eq('u.emailCanonical', 'UNACCENT(LOWER(:pattern))'))
                                ->setParameter('pattern', $pattern);

                            if (1 !== (int) $qb->getQuery()->getSingleScalarResult()) {
                                $context->addViolation('This username or email does not exists');
                            }
                        },
                    ]),
                ],
            ])
            ->add('submit', SubmitType::class, [
                'label' => 'Request recover',
            ]);

        return $builder->getForm();
    }

    /**
     * @return \Symfony\Component\Form\FormInterface
     */
    private function passwordForm(User $user)
    {
        return $this
            ->createForm(
                UserPasswordType::class,
                [],
                ['user' => $user]
            )
            ->add('submit', SubmitType::class, ['label' => 'Change password']);
    }
}
