<?php

declare(strict_types=1);

namespace Sentry\SentryBundle\EventListener;

use Sentry\State\HubInterface;
use Sentry\State\Scope;
use Sentry\UserDataBag;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * This listener ensures that a new {@see \Sentry\State\Scope} is created for
 * each request and that it is filled with useful information, e.g. the IP
 * address of the client.
 */
final class RequestListener
{
    use KernelEventForwardCompatibilityTrait;

    /**
     * @var HubInterface The current hub
     */
    private $hub;

    /**
     * @var TokenStorageInterface|null The token storage
     */
    private $tokenStorage;

    /**
     * Constructor.
     *
     * @param HubInterface               $hub          The current hub
     * @param TokenStorageInterface|null $tokenStorage The token storage
     */
    public function __construct(HubInterface $hub, ?TokenStorageInterface $tokenStorage)
    {
        $this->hub = $hub;
        $this->tokenStorage = $tokenStorage;
    }

    /**
     * This method is called for each request handled by the framework and
     * fills the Sentry scope with information about the current user.
     *
     * @param RequestEvent $event The event
     */
    public function handleKernelRequestEvent(RequestEvent $event): void
    {
        if (!$this->isMainRequest($event)) {
            return;
        }

        $client = $this->hub->getClient();

        if (null === $client || !$client->getOptions()->shouldSendDefaultPii()) {
            return;
        }

        $userData = new UserDataBag();
        $userData->setIpAddress($event->getRequest()->getClientIp());

        if (null !== $this->tokenStorage) {
            $this->setUserData($userData, $this->tokenStorage->getToken());
        }

        $this->hub->configureScope(static function (Scope $scope) use ($userData): void {
            $scope->setUser($userData);
        });
    }

    /**
     * This method is called for each request handled by the framework and
     * sets the route on the current Sentry scope.
     *
     * @param ControllerEvent $event The event
     */
    public function handleKernelControllerEvent(ControllerEvent $event): void
    {
        if (!$this->isMainRequest($event)) {
            return;
        }

        $route = $event->getRequest()->attributes->get('_route');

        if (!\is_string($route)) {
            return;
        }

        $this->hub->configureScope(static function (Scope $scope) use ($route): void {
            $scope->setTag('route', $route);
        });
    }

    /**
     * @param UserInterface|object|string|null $user
     */
    private function getUsername($user): ?string
    {
        if ($user instanceof UserInterface) {
            if (method_exists($user, 'getUserIdentifier')) {
                return $user->getUserIdentifier();
            }

            if (method_exists($user, 'getUsername')) {
                return $user->getUsername();
            }
        }

        if (\is_string($user)) {
            return $user;
        }

        if (\is_object($user) && method_exists($user, '__toString')) {
            return (string) $user;
        }

        return null;
    }

    private function getImpersonatorUser(TokenInterface $token): ?string
    {
        if (!$token instanceof SwitchUserToken) {
            return null;
        }

        return $this->getUsername($token->getOriginalToken()->getUser());
    }

    private function setUserData(UserDataBag $userData, ?TokenInterface $token): void
    {
        if (null === $token || !$this->isTokenAuthenticated($token)) {
            return;
        }

        $userData->setUsername($this->getUsername($token->getUser()));

        $impersonatorUser = $this->getImpersonatorUser($token);

        if (null !== $impersonatorUser) {
            $userData->setMetadata('impersonator_username', $impersonatorUser);
        }
    }

    private function isTokenAuthenticated(TokenInterface $token): bool
    {
        if (method_exists($token, 'isAuthenticated') && !$token->isAuthenticated(false)) {
            return false;
        }

        return null !== $token->getUser();
    }
}
