<?php

namespace KupShop\UserOauthBundle\Security;

use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthAwareUserProviderInterface;
use KupShop\GraphQLBundle\EventListener\JsShopRefreshListener;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\UserBundle\Event\UserRegisteredEvent;
use KupShop\UserOauthBundle\ResourceOwner\ICustomResourceOwner;
use Query\Operator;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class OauthUserProvider implements UserProviderInterface, OAuthAwareUserProviderInterface
{
    use \DatabaseCommunication;

    /** @var EventDispatcherInterface */
    protected $eventDispatcher;

    public function __construct(
        protected TokenStorageInterface $tokenStorage,
        protected SessionInterface $session,
        protected RequestStack $requestStack,
    ) {
    }

    public function loadUserByUsername($username)
    {
        return $this->loadUserByIdentifier($username);
    }

    public function loadUserByIdentifier(string $identifier): UserInterface
    {
        throw new UserNotFoundException();
    }

    /**
     * Refreshes the user for the account interface.
     *
     * It is up to the implementation to decide if the user data should be
     * totally reloaded (e.g. from the database), or if the UserInterface
     * object can just be merged into some internal array of users / identity
     * map.
     *
     * @throws UnsupportedUserException if the account is not supported
     */
    public function refreshUser(UserInterface $user): UserInterface
    {
        if ($user instanceof OauthUser) {
            $userObject = $this->getUserByClientID($user->getProvider(), $user->getClientID());
            if ($userObject) {
                $userObject->activateUser();

                return new OauthUser(
                    $userObject->id,
                    $userObject->email,
                    $user->getProvider(),
                    $user->getClientID(),
                    ['ROLE_USER'],
                    $userObject
                );
            } else {
                // this usually happens when user unbinds his fb/google account and is currently logged in with it
                $this->tokenStorage->setToken(null);
                // hack to preserve userMessages
                $userMessages = $this->session->get('userMessages');
                $this->session->invalidate();
                $this->session->set('userMessages', $userMessages);
                if (findModule(\Modules::JS_SHOP)) {
                    // set `jsShopReload: true` session to notify js-cart
                    $this->session->set(JsShopRefreshListener::SESSION_NAME, true);
                }
                addUserMessage(translate('oauth', 'login')['invalid_login']);
                redirection(path('kupshop_user_login_login'));
            }
        }

        throw new UnsupportedUserException();
    }

    /**
     * Whether this provider supports the given user class.
     *
     * @param string $class
     */
    public function supportsClass($class): bool
    {
        return $class === OauthUser::class;
    }

    private function getUserByClientID(string $provider, string $clientID)
    {
        $providerRow = $this->selectSQL('users_provider_ids', [
            'provider' => $provider,
            'client_id' => $clientID,
        ])->fetch();
        if ($providerRow) {
            $user = \User::createFromId($providerRow['id_user']);
        }

        return $user ?? false;
    }

    /**
     * Loads the user by a given UserResponseInterface object.
     *
     * @return UserInterface
     *
     * @throws UserNotFoundException if the user is not found
     */
    public function loadUserByOAuthUserResponse(UserResponseInterface $response)
    {
        $email = $response->getEmail();
        $resourceOwner = $response->getResourceOwner();
        $provider = $resourceOwner->getName();
        $clientID = $response->getUsername();
        $user = $this->getUserByClientID($provider, $clientID ?? '');
        $token = $this->tokenStorage->getToken();

        if ($user && !$user->isActive()) {
            throw new UserNotFoundException();
        }

        if ($user && $token?->getUser()) {
            if ($this->session->get('bind_external_account') === $provider) {
                // external account is already linked so refuse bind
                $this->session->remove('bind_external_account');
                throw new OauthBindRefusedException('Account already binded.');
            } else {
                // unintended bind (eg: when user is already logged in, but goes to /connect/google without proper session)
                throw new ShouldLogOutException('Unintended bind.');
            }
        }

        // bind account to current user
        // to restrict binding for non logged users add condition: $token && $token->isAuthenticated()
        if (!$user) {
            $tokenUsername = $token?->getUser() ? $token->getUserIdentifier() : null;
            // unintended bind check
            if ($tokenUsername && $this->session->get('bind_external_account') !== $provider) {
                throw new OauthBindRefusedException('Unintended bind.');
            }
            $this->session->remove('bind_external_account');

            $user = \User::createFromLogin($tokenUsername ?? $email);
            if ($user) {
                try {
                    $user = sqlGetConnection()->transactional(function () use ($user, $provider, $clientID, $email) {
                        $this->insertSQL(
                            'users_provider_ids',
                            [
                                'id_user' => $user->id,
                                'provider' => $provider,
                                'client_id' => $clientID,
                                'email' => $email,
                            ]
                        );

                        return $this->getUserByClientID($provider, $clientID);
                    });

                    if ($token?->getUser()) {
                        if ($user) {
                            addUserMessage(
                                sprintf(
                                    translate('oauth', 'login')['bind_success'],
                                    ucfirst($provider),
                                    $email
                                ),
                                'success'
                            );
                            if ($resourceOwner instanceof ICustomResourceOwner) {
                                $resourceOwner->updateUserData($user, $response);
                            }
                            throw new OauthBindSuccessException();
                        } else {
                            throw new OauthBindFailedException('User not found.');
                        }
                    }
                } catch (UniqueConstraintViolationException $e) {
                    throw new OauthBindFailedException('OAuth already binded.');
                }
            }
        }

        // add new user
        if (!$user && !empty($email) && !empty($response->getFirstName()) && !empty($response->getLastName())
            && !empty($clientID)
        ) {
            try {
                $user = sqlGetConnection()->transactional(function () use ($email, $response, $provider, $clientID) {
                    $insert = [
                        'email' => $email,
                        'name' => $response->getFirstName(),
                        'surname' => $response->getLastName(),
                        'date_reg' => date('Y-m-d H:i:s'),
                    ];

                    if (findModule(\Modules::CURRENCIES)) {
                        $currencyContext = Contexts::get(CurrencyContext::class);
                        $insert['currency'] = $currencyContext->getActiveId();
                    }

                    if (findModule(\Modules::TRANSLATIONS)) {
                        $languageContext = Contexts::get(LanguageContext::class);
                        $insert['id_language'] = $languageContext->getActiveId();
                    }

                    try {
                        $this->insertSQL('users', $insert);
                        $userId = sqlInsertId();
                    } catch (UniqueConstraintViolationException) {
                        if (!($userId = $this->checkNewsletterUser($insert['email'], $insert))) {
                            // pokud uzivatel existuje a je neaktivni z jineho duvodu, nez newsletter, tak ho nedovolim sparovat/prihlasit
                            throw new UserNotFoundException();
                        }
                    }

                    $this->insertSQL(
                        'users_provider_ids',
                        [
                            'id_user' => $userId,
                            'provider' => $provider,
                            'client_id' => $clientID,
                            'email' => $email,
                        ]
                    );

                    $this->eventDispatcher->dispatch(
                        new UserRegisteredEvent(\User::createFromId($userId), $response->getResourceOwner()->getName())
                    );

                    return $this->getUserByClientID($provider, $clientID);
                });
                // redirect to user account
                addUserMessage(translate('oauth', 'login')['registration_success'], 'success');
                $this->requestStack->getMainRequest()->attributes->set('gtm_registration', [
                    'email' => $email ?? 'no-email',
                    'firstname' => $response->getFirstName() ?? 'no-firstname',
                ]);
                // force redirection to user settings
                $this->session->set('_security.main.target_path', path('settings'));
            } catch (UniqueConstraintViolationException $e) {
            }
        }

        // user was not found by clientID, but we already have his email, so we can't register him
        // LoginView catches this error and displays translatable message:
        // translate('oauth', 'login')['unknown_clientid_with_known_email']
        if (!$user) {
            throw new ClientIDNotFoundException($email ?? '');
        }

        if ($resourceOwner instanceof ICustomResourceOwner) {
            $resourceOwner->updateUserData($user, $response);
        }

        if ($this->session->get('oauth_register_redirect')) {
            $this->session->set('_security.main.target_path', $this->session->get('oauth_register_redirect'));
            $this->session->remove('oauth_register_redirect');
        }

        // user found or successfully created
        return new OauthUser($user->id, $user->email, $provider, $clientID, ['ROLE_USER'], $user);
    }

    private function checkNewsletterUser(string $email, array $update = []): ?int
    {
        $user = sqlQueryBuilder()
            ->select('id, figure, passw, get_news, date_subscribe, date_unsubscribe')
            ->from('users')
            ->where(Operator::equals(['email' => $email]))
            ->execute()->fetchAssociative();

        if (!$user) {
            return null;
        }

        // pokud je to neaktivni uzivatel, kterej nejspis vznikl pres newsletter, tak ho rovnou zaktivuju at to prihlaseni pres socky vubec funguje
        if ($user['figure'] === 'N' && empty($user['passw']) && ($user['get_news'] === 'Y' || !empty($user['date_subscribe']) || !empty($user['date_unsubscribe']))) {
            sqlQueryBuilder()
                ->update('users')
                ->directValues(array_merge($update, ['figure' => 'Y']))
                ->where(Operator::equals(['id' => $user['id']]))
                ->execute();
        } else {
            // pokud to neni newsletter uzivatel, tak vracim null
            return null;
        }

        return $user['id'];
    }

    /**
     * @required
     */
    public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
    {
        $this->eventDispatcher = $eventDispatcher;
    }
}
