<?php

namespace Mautic\UserBundle\Security\Authenticator;

use Mautic\PluginBundle\Helper\IntegrationHelper;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Event\AuthenticationEvent;
use Mautic\UserBundle\Exception\WeakPasswordException;
use Mautic\UserBundle\Model\PasswordStrengthEstimatorModel;
use Mautic\UserBundle\Security\Authentication\Token\PluginToken;
use Mautic\UserBundle\UserEvents;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;

class FormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
    use TargetPathTrait;

    public const LOGIN_ROUTE       = 'login';

    public const LOGIN_CHECK_ROUTE = 'mautic_user_logincheck';

    /**
     * @var string|null After upgrade to Symfony 5.2 we should use Passport system to store the authenticatingService
     */
    private ?string $authenticatingService = null;

    private ?Response $authEventResponse = null;

    public function __construct(
        private IntegrationHelper $integrationHelper,
        private UserPasswordHasher $hasher,
        private EventDispatcherInterface $dispatcher,
        private ?RequestStack $requestStack,
        private CsrfTokenManagerInterface $csrfTokenManager,
        private UrlGeneratorInterface $urlGenerator,
        private PasswordStrengthEstimatorModel $passwordStrengthEstimatorModel
    ) {
    }

    public function supports(Request $request): bool
    {
        return self::LOGIN_CHECK_ROUTE === $request->attributes->get('_route')
            && $request->isMethod(Request::METHOD_POST);
    }

    /**
     * @return array<string, mixed|null>
     */
    public function getCredentials(Request $request): array
    {
        $credentials = [
            'username'    => $request->request->get('_username'),
            'password'    => $request->request->get('_password'),
            'csrf_token'  => $request->request->get('_csrf_token'),
            'integration' => $request->get('integration'),
        ];

        $request->getSession()->set(Security::LAST_USERNAME, $credentials['username']);

        return $credentials;
    }

    public function getUser($credentials, UserProviderInterface $userProvider): ?User
    {
        $csrfToken = new CsrfToken('authenticate', $credentials['csrf_token']);
        if (!$this->csrfTokenManager->isTokenValid($csrfToken)) {
            throw new InvalidCsrfTokenException();
        }

        try {
            /** @var User $user */
            $user = $userProvider->loadUserByIdentifier($credentials['username']);
        } catch (UserNotFoundException) {
            /** @var string $user */
            $user = $credentials['username'];
        }

        $this->authenticatingService = $credentials['integration'] ?? null;

        // Try authenticating with a plugin first
        $integrations = $this->integrationHelper->getIntegrationObjects($this->authenticatingService, ['sso_form'], false, null, true);
        $token        = new PluginToken(
            null, // In 4.4 there was a provider key. If the issue will be severe we need to override whole guard. Otherwise, wait for Symfony 5.2 and Passport.
            $this->authenticatingService,
            $user,
            ($user instanceof User) ? $this->getPassword($credentials) : '',
            ($user instanceof User) ? $user->getRoles() : [],
            $this->authEventResponse // though this will be null ?
        );
        $authEvent = new AuthenticationEvent(
            $user,
            $token,
            $userProvider,
            $this->requestStack->getCurrentRequest(),
            false,
            $this->authenticatingService,
            $integrations
        );

        if ($this->dispatcher->hasListeners(UserEvents::USER_FORM_AUTHENTICATION)) {
            $this->dispatcher->dispatch($authEvent, UserEvents::USER_FORM_AUTHENTICATION);
        }

        if ($authEvent->isAuthenticated()) {
            $user                        = $authEvent->getUser();
            $this->authenticatingService = $authEvent->getAuthenticatingService();
        } elseif ($authEvent->isFailed()) {
            throw new AuthenticationException($authEvent->getFailedAuthenticationMessage());
        }

        $this->authEventResponse = $authEvent->getResponse();

        if (!$user instanceof User) {
            // Still 'check' the credentials to prevent user enumeration via response timing comparison.
            // We check it against a pre-calculated hash so the verify functions take roughly
            // the same amount of time, and we pass the actual entered password so the response
            // timing varies with the given password the same way it does for existing users.
            try {
                $fakeUser = new User();
                $fakeUser->setUsername($credentials['username']);
                $fakeUser->setPassword('$2y$13$aAwXNyqA87lcXQQuk8Cp6eo2amRywLct29oG2uWZ8lYBeamFZ8UhK');
                $this->checkCredentials($credentials, $fakeUser);
            } catch (WeakPasswordException) {
                // Ignore.
            }
            throw new BadCredentialsException();
        }

        if ($this->dispatcher->hasListeners(UserEvents::USER_FORM_POST_LOCAL_PASSWORD_AUTHENTICATION)) {
            $authEvent = new AuthenticationEvent($user, $token, $userProvider, $this->requestStack->getCurrentRequest());
            $this->dispatcher->dispatch($authEvent, UserEvents::USER_FORM_POST_LOCAL_PASSWORD_AUTHENTICATION);
        }

        return $user;
    }

    public function checkCredentials($credentials, UserInterface $user): bool
    {
        // Temp solution to remap a UserInterface object to a PasswordAuthenticatedUserInterface object
        $newUser = new User();
        $newUser->setUsername($user->getUserIdentifier());
        $newUser->setPassword($user->getPassword());

        $valid =  $this->hasher->isPasswordValid($newUser, $this->getPassword($credentials));

        if ($valid) {
            if (!$this->passwordStrengthEstimatorModel->validate($credentials['password'])) {
                throw new WeakPasswordException();
            }
        }

        return $valid;
    }

    public function getPassword($credentials): ?string
    {
        return $credentials['password'] ?? null;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?RedirectResponse
    {
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
            return new RedirectResponse($targetPath);
        }

        // If integrations fail due to redirect to dashboard look into
        // how to detect if that's a proper form auth and return null if request must continue w/o redirect
        return new RedirectResponse($this->urlGenerator->generate('mautic_dashboard_index'));
    }

    protected function getLoginUrl(): string
    {
        return $this->urlGenerator->generate(self::LOGIN_ROUTE);
    }
}
