<?php

namespace App\Service;

use App\Entity\OtpCode;
use App\Exception\TooManyOtpRequestsException;
use App\Repository\OtpCodeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;

/**
 * Génération, envoi et vérification des codes OTP de connexion.
 * Port de la logique de app/api/auth/{send-code,verify}/route.ts.
 */
final class OtpManager
{
    private const TTL_MINUTES = 15;
    private const MAX_PER_HOUR = 10;
    // Nombre d'essais infructueux tolérés sur un même code avant invalidation
    // (défense anti-brute-force par code, en plus du login_throttling par IP).
    private const MAX_ATTEMPTS = 5;

    public function __construct(
        private readonly EntityManagerInterface $em,
        private readonly OtpCodeRepository $otpCodes,
        private readonly MailerInterface $mailer,
        #[Autowire('%env(MAILER_FROM_EMAIL)%')]
        private readonly string $fromEmail,
        #[Autowire('%env(MAILER_FROM_NAME)%')]
        private readonly string $fromName,
    ) {
    }

    /**
     * Génère un code, le stocke et l'envoie par email.
     *
     * @throws TooManyOtpRequestsException si le quota horaire est dépassé
     */
    public function requestCode(string $email): void
    {
        $oneHourAgo = new \DateTimeImmutable('-1 hour');
        if ($this->otpCodes->countRecentForEmail($email, $oneHourAgo) >= self::MAX_PER_HOUR) {
            throw new TooManyOtpRequestsException('Trop de tentatives. Réessayez dans une heure.');
        }

        $code = $this->generateCode();
        $expiresAt = new \DateTimeImmutable(sprintf('+%d minutes', self::TTL_MINUTES));

        // Seul le hash est stocké ; le code en clair ne sert qu'à l'envoi de l'email.
        $this->em->persist(new OtpCode($email, self::hash($code), $expiresAt));
        $this->em->flush();

        $this->sendEmail($email, $code);
    }

    /**
     * Vérifie un code. En cas de succès, le marque utilisé et purge les anciens.
     * En cas d'échec, incrémente le compteur d'essais du code et l'invalide au-delà du seuil.
     */
    public function verify(string $email, string $code): bool
    {
        $now = new \DateTimeImmutable();
        $otp = $this->otpCodes->findLatestValid($email, $now);

        if (null === $otp) {
            return false;
        }

        if (!hash_equals($otp->getCode(), self::hash(trim($code)))) {
            // Essai infructueux : on le comptabilise et on brûle le code au-delà du seuil
            // (empêche le brute-force du code 6 chiffres pendant sa fenêtre de validité).
            $otp->incrementAttempts();
            if ($otp->getAttempts() >= self::MAX_ATTEMPTS) {
                $otp->markUsed();
            }
            $this->em->flush();

            return false;
        }

        $otp->markUsed();
        $this->em->flush();

        $this->otpCodes->deleteOlderThan($email, new \DateTimeImmutable('-1 hour'));

        return true;
    }

    private function generateCode(): string
    {
        // Code à 6 chiffres cryptographiquement aléatoire (100000–999999).
        return (string) random_int(100000, 999999);
    }

    /** Hash déterministe du code (SHA-256). Suffisant pour un secret court-vivant à haute entropie. */
    private static function hash(string $code): string
    {
        return hash('sha256', $code);
    }

    private function sendEmail(string $to, string $code): void
    {
        $email = (new TemplatedEmail())
            ->from(new Address($this->fromEmail, $this->fromName))
            ->to($to)
            ->subject('Votre code de connexion Scan Inventaire')
            ->htmlTemplate('email/otp.html.twig')
            ->text(sprintf(
                "Votre code de connexion Scan Inventaire : %s\n\nCe code est valable %d minutes.\n\nSi vous n'avez pas demandé ce code, ignorez cet email.",
                $code,
                self::TTL_MINUTES
            ))
            ->context(['code' => $code, 'ttlMinutes' => self::TTL_MINUTES]);

        $this->mailer->send($email);
    }
}
