<?php

namespace Lexik\Bundle\JWTAuthenticationBundle\Services;

use Lexik\Bundle\JWTAuthenticationBundle\Encoder\HeaderAwareJWTEncoderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTEncodedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichment\NullEnrichment;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
 * Provides convenient methods to manage JWT creation/verification.
 *
 * @author Nicolas Cabot <n.cabot@lexik.fr>
 * @author Robin Chalas <robin.chalas@gmail.com>
 */
class JWTManager implements JWTManagerInterface, JWTTokenManagerInterface
{
    /**
     * @var JWTEncoderInterface
     */
    protected $jwtEncoder;

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

    /**
     * @var string
     *
     * @deprecated since v2.15
     */
    protected $userIdentityField;

    /**
     * @var string
     */
    protected $userIdClaim;

    /**
     * @var PayloadEnrichmentInterface
    */
    private $payloadEnrichment;

    /**
     * @param string|null $userIdClaim
     */
    public function __construct(JWTEncoderInterface $encoder, EventDispatcherInterface $dispatcher, $userIdClaim = null, PayloadEnrichmentInterface $payloadEnrichment = null)
    {
        $this->jwtEncoder = $encoder;
        $this->dispatcher = $dispatcher;
        $this->userIdentityField = 'username';
        $this->userIdClaim = $userIdClaim;
        $this->payloadEnrichment = $payloadEnrichment ?? new NullEnrichment();
    }

    /**
     * @return string The JWT token
     */
    public function create(UserInterface $user): string
    {
        $payload = ['roles' => $user->getRoles()];
        $this->addUserIdentityToPayload($user, $payload);

        $this->payloadEnrichment->enrich($user, $payload);

        return $this->generateJwtStringAndDispatchEvents($user, $payload);
    }

    /**
     * @return string The JWT token
     */
    public function createFromPayload(UserInterface $user, array $payload): string
    {
        $payload = array_merge(['roles' => $user->getRoles()], $payload);
        $this->addUserIdentityToPayload($user, $payload);

        $this->payloadEnrichment->enrich($user, $payload);

        return $this->generateJwtStringAndDispatchEvents($user, $payload);
    }

    /**
     * @return string The JWT token
     */
    private function generateJwtStringAndDispatchEvents(UserInterface $user, array $payload): string
    {
        $jwtCreatedEvent = new JWTCreatedEvent($payload, $user);
        $this->dispatcher->dispatch($jwtCreatedEvent, Events::JWT_CREATED);

        if ($this->jwtEncoder instanceof HeaderAwareJWTEncoderInterface) {
            $jwtString = $this->jwtEncoder->encode($jwtCreatedEvent->getData(), $jwtCreatedEvent->getHeader());
        } else {
            $jwtString = $this->jwtEncoder->encode($jwtCreatedEvent->getData());
        }

        $jwtEncodedEvent = new JWTEncodedEvent($jwtString);

        $this->dispatcher->dispatch($jwtEncodedEvent, Events::JWT_ENCODED);

        return $jwtString;
    }

    /**
     * {@inheritdoc}
     * @throws JWTDecodeFailureException
     */
    public function decode(TokenInterface $token)
    {
        if (!($payload = $this->jwtEncoder->decode($token->getCredentials()))) {
            return false;
        }

        $event = new JWTDecodedEvent($payload);
        $this->dispatcher->dispatch($event, Events::JWT_DECODED);

        if (!$event->isValid()) {
            return false;
        }

        return $event->getPayload();
    }

    /**
     * {@inheritdoc}
     */
    public function parse(string $jwtToken): array
    {
        $payload = $this->jwtEncoder->decode($jwtToken);

        $event = new JWTDecodedEvent($payload);
        $this->dispatcher->dispatch($event, Events::JWT_DECODED);

        if (!$event->isValid()) {
            throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'The token was marked as invalid by an event listener after successful decoding.');
        }

        return $event->getPayload();
    }

    /**
     * Add user identity to payload, username by default.
     * Override this if you need to identify it by another property.
     *
     * @param array &$payload
     */
    protected function addUserIdentityToPayload(UserInterface $user, array &$payload)
    {
        $accessor = PropertyAccess::createPropertyAccessor();
        $identityField = $this->userIdClaim ?: $this->userIdentityField;

        if ($user instanceof InMemoryUser && 'username' === $identityField) {
            $payload[$identityField] = $accessor->getValue($user, 'userIdentifier');

            return;
        }

        $payload[$identityField] = $accessor->getValue($user, $accessor->isReadable($user, $identityField) ? $identityField : 'user_identifier');
    }

    /**
     * {@inheritdoc}
     */
    public function getUserIdentityField(): string
    {
        if (0 === func_num_args() || func_get_arg(0)) {
            trigger_deprecation('lexik/jwt-authentication-bundle', '2.15', 'The "%s()" method is deprecated.', __METHOD__);
        }

        return $this->userIdentityField;
    }

    /**
     * {@inheritdoc}
     */
    public function setUserIdentityField($field)
    {
        if (1 >= func_num_args() || func_get_arg(1)) {
            trigger_deprecation('lexik/jwt-authentication-bundle', '2.15', 'The "%s()" method is deprecated.', __METHOD__);
        }

        $this->userIdentityField = $field;
    }

    /**
     * @return string
     */
    public function getUserIdClaim(): ?string
    {
        return $this->userIdClaim;
    }
}
