<?php

declare(strict_types=1);

namespace App\Security;

use App\Entity\RefreshToken;
use App\Entity\User;
use App\Repository\RefreshTokenRepository;
use Doctrine\ORM\EntityManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class RefreshTokenManager
{
    public function __construct(
        private readonly EntityManagerInterface $entityManager,
        private readonly RefreshTokenRepository $refreshTokenRepository,
        private readonly JWTTokenManagerInterface $jwtTokenManager,
        #[Autowire('%app.jwt_refresh_ttl%')]
        private readonly int $refreshTokenTtl,
    ) {
    }

    public function createAccessToken(User $user): string
    {
        return $this->jwtTokenManager->create($user);
    }

    public function createRefreshToken(User $user): string
    {
        $now = new \DateTimeImmutable();
        $rawToken = $this->createRefreshTokenEntity($user, $now);
        $this->entityManager->flush();

        return $rawToken;
    }

    public function rotateFromToken(?string $rawToken): ?RefreshTokenRotationResult
    {
        $rawToken = trim((string) $rawToken);
        if ($rawToken === '') {
            return null;
        }

        $now = new \DateTimeImmutable();
        $currentToken = $this->refreshTokenRepository->findValidByTokenHash($this->hashToken($rawToken), $now);
        if (!$currentToken instanceof RefreshToken) {
            return null;
        }

        $user = $currentToken->getUser();
        $currentToken->revoke($now);
        $newRefreshToken = $this->createRefreshTokenEntity($user, $now);
        $this->entityManager->flush();

        return new RefreshTokenRotationResult(
            user: $user,
            accessToken: $this->createAccessToken($user),
            refreshToken: $newRefreshToken,
        );
    }

    public function revokeToken(?string $rawToken): void
    {
        $rawToken = trim((string) $rawToken);
        if ($rawToken === '') {
            return;
        }

        $token = $this->refreshTokenRepository->findOneByTokenHash($this->hashToken($rawToken));
        if (!$token instanceof RefreshToken || $token->isRevoked()) {
            return;
        }

        $token->revoke(new \DateTimeImmutable());
        $this->entityManager->flush();
    }

    private function createRefreshTokenEntity(User $user, \DateTimeImmutable $now): string
    {
        $rawToken = $this->generateRawToken();
        $expiresAt = $now->modify(sprintf('+%d seconds', $this->refreshTokenTtl));

        $token = new RefreshToken(
            user: $user,
            tokenHash: $this->hashToken($rawToken),
            expiresAt: $expiresAt,
        );

        $this->entityManager->persist($token);

        return $rawToken;
    }

    private function hashToken(string $rawToken): string
    {
        return hash('sha256', $rawToken);
    }

    private function generateRawToken(): string
    {
        return rtrim(strtr(base64_encode(random_bytes(64)), '+/', '-_'), '=');
    }
}
