<?php 

namespace NPS\EventSubscriber;

use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent;
use NPS\Model\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTInvalidEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTExpiredEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTNotFoundEvent;
use Psr\Log\LoggerInterface as Logger;

/**
 * Subscriber to JWT authentication
 * @author Lukas Korinek
 */
class AuthenticationSubscriber implements EventSubscriberInterface
{
    const TEST_IP = '192.168.255.255';
    const TEST_AGENT = 'chrome';

    /** @var Request $request */
    private $request;

    /** @var TokenStorageInterface $storage */
    private $storage;

    /** @var RoleHierarchyInterface $hierarchy */
    private $hierarchy;

    /** @var EntityManagerInterface $em */
    private $em;

    /** @var Logger $logger */
    private $logger;

    public function __construct(RequestStack $stack, TokenStorageInterface $storage, RoleHierarchyInterface $hierarchy, EntityManagerInterface $em, Logger $securityLogger)
    {
        $this->request = $stack->getCurrentRequest();
        $this->hierarchy = $hierarchy;
        $this->storage = $storage;
        $this->logger = $securityLogger;
        $this->em = $em;
    }

    /**
     * @inheritdoc
     */
    public static function getSubscribedEvents()
    {
        return [
            Events::AUTHENTICATION_SUCCESS => 'onAuthenticationSuccess',
            Events::JWT_AUTHENTICATED => 'onJWTAuthenticated',
            Events::JWT_CREATED => 'onJWTCreated',
            Events::JWT_DECODED => 'onJWTdecoded',
            Events::JWT_INVALID => 'onJWTInvalid',
            Events::JWT_NOT_FOUND => 'onJWTNotFound',
            Events::JWT_EXPIRED => 'onJWTExpired',
        ];
    }

    /**
     * @param AuthenticationSuccessEvent $event
     */
    public function onAuthenticationSuccess(AuthenticationSuccessEvent $event)
    {
        $user = $event->getUser();
        if(!$user instanceof User)
        {
            return;
        }

        // Update user information
        $user->acknowledgeLogin();
        $this->em->flush();

        // Set the authentication cookie
        $data = json_decode($event->getResponse()->getContent(), TRUE);
        $this->setCookie($data['token']);
        
        $event->setData(
            array_merge($event->getData(), [
                'user' => [
                    'email' => $user->getEmail(),
                    'username' => $user->getUsername(),
                    'roles' => array_map(function($role){ return $role->getRole(); }, $this->hierarchy->getReachableRoles($user->getRoles()))
                ],
                'debug' => isset($GLOBALS['debug']) ? $GLOBALS['debug'] : []
            ])
        );
    }

    /**
     * @param JWTCreatedEvent $event
     */
    public function onJWTCreated(JWTCreatedEvent $event)
    {
        // Test compatibility
        $remote = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : self::TEST_IP;
        $agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : self::TEST_AGENT;

        $event->setData(array_merge(
            $event->getData(), ['IP' => $remote, 'agent' => $agent, 'timestamp' => time()]
        ));
    }

    /**
     * @param JWTDecodedEvent $event
     * @todo Solve IP address checking as its problematic
     */
    public function onJWTDecoded(JWTDecodedEvent $event)
    {
        $payload = $event->getPayload();

        // if(!isset($payload['IP']) || !$payload['IP'])
        // {
        //     $this->logger->warning('Auth: token does not contain IP information');
        //     $event->markAsInvalid();
        //     return;
        // }

        if(!isset($payload['agent']) || !$payload['agent'])
        {
            $this->logger->warning('Auth: token does not contain user agent information');
            $event->markAsInvalid();
            return;
        }
        
        // $remote = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : self::TEST_IP; // Test compatibility, remote address never empty on server env
        // if ($payload['IP'] !== $remote) 
        // {
        //     $this->logger->warning(sprintf('token IP %s does not match request IP %s', $payload['IP'], $remote));
        //     $event->markAsInvalid();
        //     return;
        // }
        
        $agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : self::TEST_AGENT; // Test compatibility
        if ($payload['agent'] !== $agent) 
        {
            $this->logger->warning(sprintf('token user agent "%s" does not match request user agent "%s"', $payload['agent'], $agent));
            $event->markAsInvalid();
            return;
        }

        // Obsolete token invalidation
        $token = $this->storage->getToken();
        if($token)
        {
            if($user = $token->getUser())
            {
                if(!isset($payload['timestamp']) || $payload['timestamp'] < $user->getCredentialsUpdateDate()->getTimestamp())
                {
                    $this->logger->warning('Auth: user credentials updated after token creation (user ' . $user->getUsername());
                    $event->markAsInvalid();
                }
            }
        }
    }

    /**
     * @param JWTAuthenticatedEvent $event
     */
    public function onJWTAuthenticated(JWTAuthenticatedEvent $event)
    {
        $token = $event->getToken();
        $this->setCookie($token);
    }

    /**
     * @param JWTInvalidEvent $event
     */
    public function onJWTInvalid(JWTInvalidEvent $event)
    {
        $this->unsetCookie();

        $response = new JsonResponse([
            'message' => 'Invalid JWT Token',
            'debug' => isset($GLOBALS['debug']) ? $GLOBALS['debug'] : []
        ], 401);
        
        $event->setResponse($response);
    }

    /**
     * @param JWTNotFoundEvent $event
     */
    public function onJWTNotFound(JWTNotFoundEvent $event)
    {
        $this->unsetCookie();

        $response = new JsonResponse([
            'message' => 'JWT Token not found',
            'debug' => isset($GLOBALS['debug']) ? $GLOBALS['debug'] : []
        ], 401);
        
        $event->setResponse($response);
    }

    /**
     * @param JWTExpiredEvent $event
     */
    public function onJWTExpired(JWTExpiredEvent $event)
    {
        $this->unsetCookie();

        $response = new JsonResponse([
            'message' => 'Expired JWT Token',
            'debug' => isset($GLOBALS['debug']) ? $GLOBALS['debug'] : []
        ], 401);
        
        $event->setResponse($response);
    }

    /**
     * Creates and sets the bearer cookie for cookie based authentication
     * @param string $token 
     */
    private function setCookie($token)
    {
        if(headers_sent())
        {
            // Headers already sent, do nothing
            return;
        }

        if(!empty($_SERVER['HTTPS']) || $_SERVER['HTTP_HOST'] == 'localhost')
        {
            setcookie('BEARER', $token, time() + 3600 * 60, '/', $_SERVER['HTTP_HOST'], TRUE, TRUE);
        }
    }

    /**
     * Unsets the bearer cookie in case of failure
     */
    private function unsetCookie()
    {
        if(!headers_sent())
        {
            $domain = ($_SERVER['HTTP_HOST'] == 'localhost') ? FALSE : $_SERVER['HTTP_HOST'];
            setcookie('BEARER', '', time() - 1, '/', $domain, TRUE, TRUE);
        }
    }
}



