<?php

namespace App\Controller\Api;

use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

#[Route('/api/auth')]
class ApiAuthController extends AbstractController
{
    private const JWT_TTL = 86400; // 24h

    /** @codeCoverageIgnore Autowire injection */
    public function __construct(
        #[Autowire('%kernel.secret%')] private string $appSecret,
    ) {
    }

    /** @codeCoverageIgnore Requires DB + password hasher */
    #[Route('/login', name: 'app_api_auth_login', methods: ['POST'])]
    public function login(
        Request $request,
        EntityManagerInterface $em,
        UserPasswordHasherInterface $passwordHasher,
    ): JsonResponse {
        $data = json_decode($request->getContent(), true);
        $email = $data['email'] ?? '';
        $password = $data['password'] ?? '';

        if ('' === $email || '' === $password) {
            return $this->json(['success' => false, 'data' => null, 'error' => 'Email et mot de passe requis.'], 400);
        }

        $user = $em->getRepository(User::class)->findOneBy(['email' => $email]);

        $hasAccess = $user && (\in_array('ROLE_ORGANIZER', $user->getRoles(), true) || \in_array('ROLE_ROOT', $user->getRoles(), true));

        if (!$user || !$passwordHasher->isPasswordValid($user, $password) || !$hasAccess) {
            return $this->json(['success' => false, 'data' => null, 'error' => !$user || !$passwordHasher->isPasswordValid($user, $password) ? 'Identifiants invalides.' : 'Acces reserve aux organisateurs.'], 401);
        }

        return $this->tokenResponse($user);
    }

    /**
     * @return array{userId: int|null, expired: bool}
     */
    private const INVALID_JWT = ['userId' => null, 'expired' => false];

    /**
     * @return array{userId: int|null, expired: bool}
     */
    public static function verifyJwt(string $token, string $email, string $appSecret): array
    {
        $parts = explode('.', $token);
        if (3 !== \count($parts)) {
            return self::INVALID_JWT;
        }

        [$headerB64, $payloadB64, $signatureB64] = $parts;

        $expectedSignature = self::base64UrlEncode(hash_hmac('sha256', $headerB64.'.'.$payloadB64, $appSecret, true));
        $payload = hash_equals($expectedSignature, $signatureB64) ? json_decode(self::base64UrlDecode($payloadB64), true) : null;

        if (!$payload || ($payload['email'] ?? '') !== $email) {
            return self::INVALID_JWT;
        }

        return [
            'userId' => $payload['userId'] ?? null,
            'expired' => ($payload['exp'] ?? 0) < time(),
        ];
    }

    /** @codeCoverageIgnore Requires DB + JWT */
    #[Route('/refresh', name: 'app_api_auth_refresh', methods: ['POST'])]
    public function refresh(
        Request $request,
        EntityManagerInterface $em,
    ): JsonResponse {
        $email = $request->headers->get('ETicket-Email', '');
        $jwt = $request->headers->get('ETicket-JWT', '');
        $result = ('' !== $email && '' !== $jwt) ? self::verifyJwt($jwt, $email, $this->appSecret) : self::INVALID_JWT;

        if (null === $result['userId'] || !$result['expired']) {
            $error = null === $result['userId'] ? 'Token invalide ou headers manquants.' : 'Token encore valide, pas besoin de refresh.';

            return $this->json(['success' => false, 'data' => null, 'error' => $error], null === $result['userId'] ? 401 : 400);
        }

        $user = $em->getRepository(User::class)->find($result['userId']);
        if (!$user || $user->getEmail() !== $email) {
            return $this->json(['success' => false, 'data' => null, 'error' => 'Utilisateur introuvable.'], 401);
        }

        return $this->tokenResponse($user);
    }

    /** @codeCoverageIgnore Requires live Keycloak */
    #[Route('/login/sso', name: 'app_api_auth_sso', methods: ['GET'])]
    public function sso(Request $request, ClientRegistry $clientRegistry): RedirectResponse
    {
        $from = $request->query->get('from', '');
        $session = $request->getSession();
        if ('scanner' === $from) {
            $session->set('sso_from', 'scanner');
        }

        return $clientRegistry->getClient('keycloak')->redirect(
            ['openid', 'email', 'profile'],
            ['redirect_uri' => $this->generateUrl('app_api_auth_sso_validate', [], UrlGeneratorInterface::ABSOLUTE_URL)],
        );
    }

    /**
     * @codeCoverageIgnore Requires live Keycloak
     */
    #[Route('/login/sso/validate', name: 'app_api_auth_sso_validate', methods: ['GET'])]
    public function ssoValidate(
        Request $request,
        ClientRegistry $clientRegistry,
        EntityManagerInterface $em,
    ): JsonResponse|RedirectResponse {
        $session = $request->getSession();
        $fromScanner = 'scanner' === $session->get('sso_from', '');
        $session->remove('sso_from');

        try {
            $client = $clientRegistry->getClient('keycloak_api');
            $accessToken = $client->getAccessToken();
            $keycloakUser = $client->fetchUserFromToken($accessToken);
        } catch (\Throwable) {
            if ($fromScanner) {
                return new RedirectResponse('/scanner/#sso_error=auth_failed');
            }

            return $this->json(['success' => false, 'data' => null, 'error' => 'Authentification SSO echouee.'], 401);
        }

        $data = $keycloakUser->toArray();
        $keycloakId = $keycloakUser->getId();
        $email = $data['email'] ?? '';

        $user = $em->getRepository(User::class)->findOneBy(['keycloakId' => $keycloakId])
            ?? $em->getRepository(User::class)->findOneBy(['email' => $email]);

        $hasAccess = $user && (\in_array('ROLE_ORGANIZER', $user->getRoles(), true) || \in_array('ROLE_ROOT', $user->getRoles(), true));

        if (!$user || !$hasAccess) {
            if ($fromScanner) {
                $error = !$user ? 'no_account' : 'no_access';

                return new RedirectResponse('/scanner/#sso_error='.$error);
            }

            return $this->json(['success' => false, 'data' => null, 'error' => !$user ? 'Aucun compte associe a ce SSO.' : 'Acces reserve aux organisateurs.'], 403);
        }

        if ($fromScanner) {
            $token = $this->generateJwt($user);
            $expiresAt = (new \DateTimeImmutable())->modify('+'.self::JWT_TTL.' seconds')->format(\DateTimeInterface::ATOM);

            return new RedirectResponse('/scanner/#sso_token='.urlencode($token).'&sso_email='.urlencode($user->getEmail()).'&sso_expires='.urlencode($expiresAt));
        }

        return $this->tokenResponse($user, true);
    }

    /** @codeCoverageIgnore Helper */
    private function tokenResponse(User $user, bool $includeEmail = false): JsonResponse
    {
        $token = $this->generateJwt($user);
        $expiresAt = (new \DateTimeImmutable())->modify('+'.self::JWT_TTL.' seconds');

        $data = ['token' => $token, 'expiresAt' => $expiresAt->format(\DateTimeInterface::ATOM)];
        if ($includeEmail) {
            $data['email'] = $user->getEmail();
        }

        return $this->json(['success' => true, 'data' => $data, 'error' => null]);
    }

    /** @codeCoverageIgnore Helper */
    private function generateJwt(User $user): string
    {
        $header = self::base64UrlEncode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));

        $payload = self::base64UrlEncode(json_encode([
            'userId' => $user->getId(),
            'email' => $user->getEmail(),
            'roles' => $user->getRoles(),
            'iat' => time(),
            'exp' => time() + self::JWT_TTL,
        ]));

        $signature = self::base64UrlEncode(
            hash_hmac('sha256', $header.'.'.$payload, $this->appSecret, true)
        );

        return $header.'.'.$payload.'.'.$signature;
    }

    private static function base64UrlEncode(string $data): string
    {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }

    private static function base64UrlDecode(string $data): string
    {
        return base64_decode(strtr($data, '-_', '+/'), true) ?: '';
    }
}
