<?php declare(strict_types=1);

/*
 * This file is part of the Rapsys UserBundle package.
 *
 * (c) Raphaël Gertz <symfony@rapsys.eu>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Rapsys\UserBundle\Handler;

use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Contracts\Translation\TranslatorInterface;

use Rapsys\PackBundle\Util\SluggerUtil;
use Rapsys\UserBundle\RapsysUserBundle;

/**
 * {@inheritdoc}
 */
class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler {
	/**
	 * Config array
	 */
	protected array $config;
	protected array $options;
	protected array $defaultOptions = [
		'failure_path' => null,
		'failure_forward' => false,
		'login_path' => '/login',
		'failure_path_parameter' => '_failure_path',
	];

	/**
	 * Doctrine instance
	 */
	protected ManagerRegistry $doctrine;

	/**
	 * MailerInterface
	 */
	protected MailerInterface $mailer;

	/**
	 * Router instance
	 */
	protected RouterInterface $router;

	/**
	 * Slugger instance
	 */
	protected SluggerUtil $slugger;

	/**
	 * RequestStack instance
	 */
	protected RequestStack $stack;

	/**
	 * Translator instance
	 */
	protected TranslatorInterface $translator;

	/**
	 * @xxx Second argument will be replaced by security.firewalls.main.logout.target
	 * @see vendor/symfony/security-bundle/DependencyInjection/SecurityExtension.php +360
	 *
	 * @param HttpKernelInterface $httpKernel The http kernel
	 * @param HttpUtils $httpUtils The http utils
	 * @param array $options The options
	 * @param LoggerInterface $logger The logger instance
	 * @param ContainerInterface $container The container instance
	 * @param ManagerRegistry $doctrine The doctrine instance
	 * @param MailerInterface $mailer The mailer instance
	 * @param RouterInterface $router The router instance
	 * @param SluggerUtil $slugger The slugger instance
	 * @param RequestStack $stack The stack instance
	 * @param TranslatorInterface $translator The translator instance
	 *
	 * {@inheritdoc}
	 */
	public function __construct(HttpKernelInterface $httpKernel, HttpUtils $httpUtils, array $options, LoggerInterface $logger, ContainerInterface $container, ManagerRegistry $doctrine, MailerInterface $mailer, RouterInterface $router, SluggerUtil $slugger, RequestStack $stack, TranslatorInterface $translator) {
		//Set config
		$this->config = $container->getParameter(self::getAlias());

		//Set doctrine
		$this->doctrine = $doctrine;

		//Set mailer
		$this->mailer = $mailer;

		//Set router
		$this->router = $router;

		//Set slugger
		$this->slugger = $slugger;

		//Set stack
		$this->stack = $stack;

		//Set translator
		$this->translator = $translator;

		//Call parent constructor
		parent::__construct($httpKernel, $httpUtils, $options, $logger);
	}

	/**
	 * Adds a flash message to the current session for type.
	 *
	 * @throws \LogicException
	 */
	protected function addFlash(string $type, mixed $message): void {
		try {
			$session = $this->stack->getSession();
		} catch (SessionNotFoundException $e) {
			throw new \LogicException('You cannot use the addFlash method if sessions are disabled. Enable them in "config/packages/framework.yaml".', 0, $e);
		}

		if (!$session instanceof FlashBagAwareSessionInterface) {
			throw new \LogicException(sprintf('You cannot use the addFlash method because class "%s" doesn\'t implement "%s".', get_debug_type($session), FlashBagAwareSessionInterface::class));
		}

		$session->getFlashBag()->add($type, $message);
	}

	/**
	 * This is called when an interactive authentication attempt fails
	 *
	 * User may retrieve mail + field + hash for each unactivated/locked accounts
	 *
	 * {@inheritdoc}
	 */
	public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response {
		//With bad credential exception
		if ($exception instanceof BadCredentialsException) {
			//With parent exception
			if (($parent = $exception->getPrevious()) instanceof UserNotFoundException) {
				//Retrieve login
				//TODO: check form _token validity ???
				if (
					!empty($login = $request->get('login')) &&
					!empty($mail = $login['mail'])
				) {
					//Set extra parameters
					$extra = ['mail' => $smail = $this->slugger->short($mail), 'field' => $sfield = $this->slugger->serialize([]), 'hash' => $this->slugger->hash($smail.$sfield)];

					//With failure target path option
					if (!empty($failurePath = $this->options['failure_path'])) {
						//With path
						if ($failurePath[0] == '/') {
							//Create login path request instance
							$req = Request::create($failurePath);

							//Get login path pathinfo
							$path = $req->getPathInfo();

							//Remove script name
							$path = str_replace($request->getScriptName(), '', $path);

							//Try with login path path
							try {
								//Save old context
								$oldContext = $this->router->getContext();

								//Force clean context
								//XXX: prevent MethodNotAllowedException on GET only routes because our context method is POST
								//XXX: see vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php +42
								$this->router->setContext(new RequestContext());

								//Retrieve route matching path
								$route = $this->router->match($path);

								//Reset context
								$this->router->setContext($oldContext);

								//Clear old context
								unset($oldContext);

								//With route name
								if ($name = $route['_route']) {
									//Remove route and controller from route defaults
									unset($route['_route'], $route['_controller'], $route['_canonical_route']);

									//Generate url
									$url = $this->router->generate($name, $extra+$route);

									//Return redirect to url response
									return new RedirectResponse($url, 302);
								}
							//No route matched
							} catch (ResourceNotFoundException $e) {
								//Unset default path, name and route
								unset($failurePath, $name, $route);
							}
						//With route name
						} else {
							//Try with login path route
							try {
								//Retrieve route matching path
								$url = $this->router->generate($failurePath, $extra);

								//Return redirect to url response
								return new RedirectResponse($url, 302);
							//Route not found, missing parameter or invalid parameter
							} catch (RouteNotFoundException|MissingMandatoryParametersException|InvalidParameterException $e) {
								//Unset default path and url
								unset($failurePath, $url);
							}
						}
					}

					//Without user
					if (($user = $this->doctrine->getRepository($this->config['class']['user'])->findOneByMail($mail)) === null) {
						//With index route from config
						if (!empty($name = $this->config['route']['register']['name']) && is_array($context = $this->config['route']['register']['context'])) {
							//Try index route
							try {
								//Generate url
								$url = $this->router->generate($name, $extra+$context);

								//Return generated route
								return new RedirectResponse($url, 302);
							//No route matched
							} catch (ResourceNotFoundException $e) {
								//Unset name and context
								unset($name, $context);
							}
						} 

						//With login target path option
						if (!empty($loginPath = $this->options['login_path'])) {
							//With path
							if ($loginPath[0] == '/') {
								//Create login path request instance
								$req = Request::create($loginPath);

								//Get login path pathinfo
								$path = $req->getPathInfo();

								//Remove script name
								$path = str_replace($request->getScriptName(), '', $path);

								//Try with login path path
								try {
									//Save old context
									$oldContext = $this->router->getContext();

									//Force clean context
									//XXX: prevent MethodNotAllowedException on GET only routes because our context method is POST
									//XXX: see vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php +42
									$this->router->setContext(new RequestContext());

									//Retrieve route matching path
									$route = $this->router->match($path);

									//Reset context
									$this->router->setContext($oldContext);

									//Clear old context
									unset($oldContext);

									//With route name
									if ($name = $route['_route']) {
										//Remove route and controller from route defaults
										unset($route['_route'], $route['_controller'], $route['_canonical_route']);

										//Generate url
										$url = $this->router->generate($name, $extra+$route);

										//Return redirect to url response
										return new RedirectResponse($url, 302);
									}
								//No route matched
								} catch (ResourceNotFoundException $e) {
									//Unset default path, name and route
									unset($loginPath, $name, $route);
								}
							//With route name
							} else {
								//Try with login path route
								try {
									//Retrieve route matching path
									$url = $this->router->generate($loginPath, $extra);

									//Return redirect to url response
									return new RedirectResponse($url, 302);
								//Route not found, missing parameter or invalid parameter
								} catch (RouteNotFoundException|MissingMandatoryParametersException|InvalidParameterException $e) {
									//Unset default path and url
									unset($loginPath, $url);
								}
							}
						}
					//With not enabled user
					} elseif (empty($user->isEnabled())) {
						//Add error message account is not enabled
						$this->addFlash('error', $this->translator->trans('Your account is not enable'));

						//Redirect on the same route with sent=1 to cleanup form
						return new RedirectResponse($this->router->generate($request->get('_route'), $request->get('_route_params')), 302);
					//With unactivated user
					} elseif (empty($user->isActivated())) {
						//Set context
						$context = [
							'recipient_mail' => $user->getMail(),
							'recipient_name' => $user->getRecipientName()
						] + array_replace_recursive(
							$this->config['context'],
							$this->config['register']['view']['context'],
							$this->config['register']['mail']['context']
						);

						//Generate each route route
						foreach($this->config['register']['route'] as $route => $tag) {
							//Only process defined routes
							if (!empty($this->config['route'][$route])) {
								//Process for confirm url
								if ($route == 'confirm') {
									//Set the url in context
									$context[$tag] = $this->router->generate(
										$this->config['route'][$route]['name'],
										//Prepend subscribe context with tag
										[
											'mail' => $smail = $this->slugger->short($context['recipient_mail']),
											'hash' => $this->slugger->hash($smail)
										]+$this->config['route'][$route]['context'],
										UrlGeneratorInterface::ABSOLUTE_URL
									);
								}
							}
						}

						//Iterate on keys to translate
						foreach($this->config['translate'] as $translate) {
							//Extract keys
							$keys = explode('.', $translate);

							//Set current
							$current =& $context;

							//Iterate on each subkey
							do {
								//Skip unset translation keys
								if (!isset($current[current($keys)])) {
									continue(2);
								}

								//Set current to subkey
								$current =& $current[current($keys)];
							} while(next($keys));

							//Set translation
							$current = $this->translator->trans($current);

							//Remove reference
							unset($current);
						}

						//Translate subject
						$context['subject'] = $subject = ucfirst(
							$this->translator->trans(
								$this->config['register']['mail']['subject'],
								$this->slugger->flatten($context, null, '.', '%', '%')
							)
						);

						//Create message
						$message = (new TemplatedEmail())
							//Set sender
							->from(new Address($this->config['contact']['address'], $this->config['contact']['name']))
							//Set recipient
							//XXX: remove the debug set in vendor/symfony/mime/Address.php +46
							->to(new Address($context['recipient_mail'], $context['recipient_name']))
							//Set subject
							->subject($context['subject'])

							//Set path to twig templates
							->htmlTemplate($this->config['register']['mail']['html'])
							->textTemplate($this->config['register']['mail']['text'])

							//Set context
							->context($context);

						//Try sending message
						//XXX: mail delivery may silently fail
						try {
							//Send message
							$this->mailer->send($message);
						//Catch obvious transport exception
						} catch(TransportExceptionInterface $e) {
							//Add error message mail unreachable
							$this->addFlash('error', $this->translator->trans('Unable to reach account'));
						}

						//Add notice
						$this->addFlash('notice', $this->translator->trans('Your verification mail has been sent, to activate your account you must follow the confirmation link inside'));

						//Add junk warning
						$this->addFlash('warning', $this->translator->trans('If you did not receive a verification mail, check your Spam or Junk mail folders'));

						//Redirect on the same route with sent=1 to cleanup form
						return new RedirectResponse($this->router->generate($request->get('_route'), $request->get('_route_params')), 302);
					}
				}
			}
		}

		//Call parent function
		return parent::onAuthenticationFailure($request, $exception);
	}

	/**
	 * {@inheritdoc}
	 */
	public function getAlias(): string {
		return RapsysUserBundle::getAlias();
	}
}