<?php namespace RVR\Controller;

use DateTime;
use SokoWeb\Http\Request;
use SokoWeb\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Interfaces\Response\IRedirect;
use SokoWeb\Mailing\Mail;
use SokoWeb\OAuth\GoogleOAuth;
use RVR\PersistentData\Model\UserPasswordResetter;
use SokoWeb\PersistentData\PersistentDataManager;
use RVR\Repository\UserPasswordResetterRepository;
use RVR\Repository\UserRepository;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
use SokoWeb\Response\Redirect;
use SokoWeb\Util\CaptchaValidator;
use SokoWeb\Util\JwtParser;

class LoginController
{
    private IRequest $request;

    private PersistentDataManager $pdm;

    private UserRepository $userRepository;

    private UserPasswordResetterRepository $userPasswordResetterRepository;

    private string $redirectUrl;

    public function __construct(IRequest $request)
    {
        $this->request = $request;
        $this->pdm = new PersistentDataManager();
        $this->userRepository = new UserRepository();
        $this->userPasswordResetterRepository = new UserPasswordResetterRepository();
        $this->redirectUrl = $this->request->session()->has('redirect_after_login') ?
            $this->request->session()->get('redirect_after_login') :
            \Container::$routeCollection->getRoute('index')->generateLink();
    }

    public function getLoginForm()
    {
        if ($this->request->user() !== null) {
            $this->deleteRedirectUrl();
            return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
        }

        return new HtmlContent('login/login', ['redirectUrl' => '/' . $this->redirectUrl]);
    }

    public function getGoogleLoginRedirect(): IRedirect
    {
        $state = bin2hex(random_bytes(16));
        $nonce = bin2hex(random_bytes(16));

        $this->request->session()->set('oauth_state', $state);
        $this->request->session()->set('oauth_nonce', $nonce);

        $oAuth = new GoogleOAuth(new Request());
        $url = $oAuth->getDialogUrl(
            $state,
            $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink(),
            $nonce
        );

        return new Redirect($url, IRedirect::TEMPORARY);
    }

    public function getRequestPasswordResetForm()
    {
        if ($this->request->user() !== null) {
            $this->deleteRedirectUrl();
            return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
        }

        return new HtmlContent('login/password_reset_request', ['email' => $this->request->query('email')]);
    }

    public function getRequestPasswordResetSuccess(): IContent
    {
        return new HtmlContent('login/password_reset_request_success');
    }

    public function getResetPasswordForm()
    {
        if ($this->request->user() !== null) {
            $this->deleteRedirectUrl();
            return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
        }

        $token = $this->request->query('token');
        $resetter = $this->userPasswordResetterRepository->getByToken($token);

        if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
            return new HtmlContent('login/reset_password', ['success' => false]);
        }

        $user = $this->userRepository->getById($resetter->getUserId());

        return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail(), 'redirectUrl' => '/' . $this->redirectUrl]);
    }

    public function login(): IContent
    {
        if ($this->request->user() !== null) {
            $this->deleteRedirectUrl();
            return new JsonContent(['success' => true]);
        }

        $user = $this->userRepository->getByEmail($this->request->post('email'));
        if ($user === null || !$user->checkPassword($this->request->post('password'))) {
            return new JsonContent([
                'error' => [
                    'errorText' => 'No user found with the given email address or the given password is wrong. You can <a href="/password/requestReset?email=' .
                        urlencode($this->request->post('email')) . '" title="Request password reset">request password reset</a>!'
                ]
            ]);
        }

        $this->request->setUser($user);

        $this->deleteRedirectUrl();
        return new JsonContent(['success' => true]);
    }

    public function loginWithGoogle()
    {
        $defaultError = 'Authentication with Google failed. Please <a href="/login/google" title="Login with Google">try again</a>!';

        if ($this->request->user() !== null) {
            $this->deleteRedirectUrl();
            return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
        }

        if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
            return new HtmlContent('login/google_login_error', ['error' => $defaultError]);
        }

        $oAuth = new GoogleOAuth(new Request());
        $tokenData = $oAuth->getToken(
            $this->request->query('code'),
            $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink()
        );

        if (!isset($tokenData['id_token'])) {
            return new HtmlContent('login/google_login_error', ['error' => $defaultError]);
        }

        $jwtParser = new JwtParser($tokenData['id_token']);
        $idToken = $jwtParser->getPayload();

        if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
            return new HtmlContent('login/google_login_error', ['error' => $defaultError]);
        }

        if (!$idToken['email_verified']) {
            return new HtmlContent('login/google_login_error', ['error' => $defaultError]);
        }

        $user = $this->userRepository->getByGoogleSub($idToken['sub']);
        if ($user === null) {
            return new HtmlContent('login/google_login_error', ['error' => 'No user found for this Google account.']);
        }

        $this->request->setUser($user);

        $this->deleteRedirectUrl();
        return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
    }

    public function logout(): IRedirect
    {
        $this->request->setUser(null);

        return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
    }

    public function requestPasswordReset(): IContent
    {
        if ($this->request->user() !== null) {
            $this->deleteRedirectUrl();
            return new JsonContent([
                'redirect' => [
                    'target' => '/' . $this->redirectUrl
                ]
            ]);
        }

        if (!empty($_ENV['RECAPTCHA_SITEKEY'])) {
            if (!$this->request->post('g-recaptcha-response')) {
                return new JsonContent(['error' => ['errorText' => 'Please check "I\'m not a robot" in the reCAPTCHA box!']]);
            }

            $captchaValidator = new CaptchaValidator();
            $captchaResponse = $captchaValidator->validate($this->request->post('g-recaptcha-response'));
            if (!$captchaResponse['success']) {
                return new JsonContent(['error' => ['errorText' => 'reCAPTCHA challenge failed. Please try again!']]);
            }
        }

        $user = $this->userRepository->getByEmail($this->request->post('email'));
        if ($user === null) {
            return new JsonContent([
                'error' => [
                    'errorText' => 'No user found with the given email address.'
                ]
            ]);
        }

        $existingResetter = $this->userPasswordResetterRepository->getByUser($user);
        if ($existingResetter !== null && $existingResetter->getExpiresDate() > new DateTime()) {
            return new JsonContent([
                'error' => [
                    'errorText' => 'Password reset was recently requested for this account. Please check your email, or try again later!'
                ]
            ]);
        }

        $token = bin2hex(random_bytes(16));
        $expires = new DateTime('+1 hour');

        $passwordResetter = new UserPasswordResetter();
        $passwordResetter->setUser($user);
        $passwordResetter->setToken($token);
        $passwordResetter->setExpiresDate($expires);

        \Container::$dbConnection->startTransaction();

        if ($existingResetter !== null) {
            $this->pdm->deleteFromDb($existingResetter);
        }

        $this->pdm->saveToDb($passwordResetter);

        \Container::$dbConnection->commit();

        $this->sendPasswordResetEmail($user->getEmail(), $token, $expires);

        return new JsonContent(['success' => true]);
    }

    public function resetPassword(): IContent
    {
        if ($this->request->user() !== null) {
            $this->deleteRedirectUrl();
            return new JsonContent([
                'redirect' => [
                    'target' => '/' . $this->redirectUrl
                ]
            ]);
        }

        $token = $this->request->query('token');
        $resetter = $this->userPasswordResetterRepository->getByToken($token);

        if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
            return new JsonContent([
                'redirect' => [
                    'target' => '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token])
                ]
            ]);
        }

        if (strlen($this->request->post('password')) < 6) {
            return new JsonContent([
                'error' => [
                    'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
                ]
            ]);
        }

        if ($this->request->post('password') !== $this->request->post('password_confirm')) {
            return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
        }

        \Container::$dbConnection->startTransaction();

        $this->pdm->deleteFromDb($resetter);

        $user = $this->userRepository->getById($resetter->getUserId());
        $user->setPlainPassword($this->request->post('password'));

        $this->pdm->saveToDb($user);

        \Container::$dbConnection->commit();

        $this->request->setUser($user);

        $this->deleteRedirectUrl();
        return new JsonContent(['success' => true]);
    }

    private function sendPasswordResetEmail(string $email, string $token, DateTime $expires): void
    {
        $mail = new Mail();
        $mail->addRecipient($email);
        $mail->setSubject($_ENV['APP_NAME'] . ' - Password reset');
        $mail->setBodyFromTemplate('password-reset', [
            'EMAIL' => $email,
            'RESET_LINK' => $this->request->getBase() . '/' .
                \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]),
            'EXPIRES' => $expires->format('Y-m-d H:i T')
        ]);
        $mail->send();
    }

    private function deleteRedirectUrl(): void
    {
        $this->request->session()->delete('redirect_after_login');
    }
}