<?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 Rapsys\PackBundle\Util\SluggerUtil;

use Rapsys\UserBundle\Exception\UnactivatedException;
use Rapsys\UserBundle\RapsysUserBundle;

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\DisabledException;
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;

/**
 * {@inheritdoc}
 */
class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler {
	/**
	 * Alias string
	 */
	protected string $alias;

	/**
	 * Config array
	 */
	protected array $config;
	protected array $defaultOptions = [
		'failure_path' => null,
		'failure_forward' => false,
		'login_path' => '/login',
		'failure_path_parameter' => '_failure_path',
	];

	/**
	 * {@inheritdoc}
	 *
	 * @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
	 */
	public function __construct(protected HttpKernelInterface $httpKernel, protected HttpUtils $httpUtils, protected array $options, protected ?LoggerInterface $logger, protected ContainerInterface $container, protected ManagerRegistry $doctrine, protected MailerInterface $mailer, protected RouterInterface $router, protected SluggerUtil $slugger, protected RequestStack $stack, protected TranslatorInterface $translator) {
		//Call parent constructor
		parent::__construct($httpKernel, $httpUtils, $options, $logger);

		//Set config
		$this->config = $container->getParameter($this->alias = RapsysUserBundle::getAlias());
	}

	/**
	 * 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);
	}

	/**
	 * {@inheritdoc}
	 *
	 * This is called when an interactive authentication attempt fails
	 */
	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) {
				/** Disabled to prevent user mail + hash retrieval for each unactivated/locked accounts

				//Get user identifier
				$mail = $parent->getUserIdentifier();

				//Set extra parameters
				$extra = ['mail' => $smail = $this->slugger->short($mail), 'hash' => $this->slugger->hash($smail)];*/

				//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);
						}
					}
				}
			//With not enabled user
			} elseif ($parent instanceof DisabledException) {
				//Add error message account is not enabled
				$this->addFlash('error', $this->translator->trans('Account not enabled', [], $this->alias));

				//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 not activated user
			} elseif ($parent instanceof UnactivatedException) {
				//Set user
				$user = $parent->getUser();

				//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 confirm 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, [], $this->alias);

					//Remove reference
					unset($current);
				}

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

				//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', [], $this->alias));
				}

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

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

				//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);
	}
}
