<?php

declare(strict_types=1);

namespace NoahVet\Reef\Security;

use NoahVet\Reef\Exception\NotJWTException;
use NoahVet\Reef\Security\Authentication\Passport\Badge\TokenExpirationBadge;
use NoahVet\Reef\Security\Authentication\Token\ReefOAuthToken;
use NoahVet\Reef\Security\IAM\TokenExchange\TokenExchangerInterface;
use NoahVet\Reef\Security\Token\JWTDecoderInterface;
use NoahVet\Reef\Security\User\Provider\ReefOAuthUserProviderInterface;
use NoahVet\Reef\Security\User\ReefOAuthUser;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;

class ReefOAuthAuthenticator extends AbstractAuthenticator implements ReefOAuthAuthenticatorInterface
{
    public function __construct(
        private readonly JWTDecoderInterface $JWTDecoder,
        private readonly string $reefClientId,
        private readonly TokenExchangerInterface $tokenExchanger,
        private readonly ReefOAuthUserProviderInterface $userProvider,
    ) {
    }

    public function supports(Request $request): ?bool
    {
        return $request->headers->has('Authorization')
            && \str_starts_with($request->headers->get('Authorization') ?? '', 'Bearer ');
    }

    public function authenticate(Request $request): Passport
    {
        $oauthToken = \trim(\substr($request->headers->get('Authorization') ?? '', 7));

        try {
            $parsedToken = $this->JWTDecoder->decodeJWT($oauthToken);

            if (null === $parsedToken) {
                throw new AuthenticationException("Can't parse token");
            }
        } catch (NotJWTException $e) {
            throw new AuthenticationException('Error parsing JWT', previous: $e);
        }

        if ($parsedToken->aud !== $this->reefClientId) {
            $oauthToken = $this->tokenExchanger->exchangeTokenForCurrentClient($oauthToken);
        }

        return new SelfValidatingPassport(
            new UserBadge($oauthToken, [$this, 'loadUser']),
            [new TokenExpirationBadge($oauthToken, [$this, 'getTokenExpiresAt'])],
        );
    }

    public function createToken(Passport $passport, string $firewallName): TokenInterface
    {
        /** @var UserBadge $userBadge */
        $userBadge = $passport->getBadge(UserBadge::class);
        /** @var ReefOAuthUser $user */
        $user = $userBadge->getUser();

        /** @var TokenExpirationBadge $tokenExpirationBadge */
        $tokenExpirationBadge = $passport->getBadge(TokenExpirationBadge::class);

        return new ReefOAuthToken($firewallName, $userBadge->getUserIdentifier(), $user, $tokenExpirationBadge->getExpirationDate());
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        if (\str_starts_with($request->headers->get('Authorization') ?? '', 'Bearer ')) {
            return new JsonResponse(['error' => 'Invalid bearer token provided'], Response::HTTP_FORBIDDEN);
        } else {
            return new JsonResponse(['error' => 'No bearer token provided'], Response::HTTP_UNAUTHORIZED);
        }
    }

    public function getTokenExpiresAt(string $bearerToken): ?\DateTimeImmutable
    {
        return $this->userProvider->getTokenExpiresAt($bearerToken);
    }

    public function loadUser(string $bearerToken): ?ReefOAuthUser
    {
        return $this->userProvider->loadUser($bearerToken);
    }

    /**
     * @deprecated
     */
    public function setCache(?CacheItemPoolInterface $cache): void
    {
    }

    /**
     * @deprecated
     */
    public function getCache(): ?CacheItemPoolInterface
    {
        return null;
    }
}
