<?php declare(strict_types=1);
/**
 * This file is part of the SplendidBear Websites' projects.
 *
 * Copyright (c) 2026 @ www.splendidbear.org
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace App\Controller;

use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Exception\ValidationException;
use Endroid\QrCode\Writer\PngWriter;
use RuntimeException;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;

/**
 * Class TwoFactorController
 *
 * Handles TOTP 2FA setup/teardown and the login challenge form.
 *
 * @package  App\Controller
 * @author   Lang <https://www.splendidbear.org>
 * @category Class
 * @license  https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
 * @link     www.splendidbear.org
 * @since    2026. 04. 12.
 */
#[AsController]
class TwoFactorController extends AbstractController
{
    public function __construct(
        private readonly TotpAuthenticatorInterface $totpAuthenticator,
        private readonly EntityManagerInterface     $em,
    ) {
    }

    /** 2FA challenge form shown during login when TOTP is enabled. */
    #[Route('/2fa', name: '2fa_login', methods: ['GET'])]
    public function loginForm(): Response
    {
        return $this->render('Security/2fa.html.twig');
    }

    /**
     * Dummy action — the actual POST is intercepted by the scheb/2fa firewall listener
     * before it ever reaches a controller. The route must exist so path('2fa_login_check')
     * resolves in Twig.
     */
    #[Route('/2fa_check', name: '2fa_login_check', methods: ['POST'])]
    public function loginCheck(): never
    {
        throw new \LogicException('This action is intercepted by the 2FA firewall listener.');
    }

    /** Step 1 — generate a pending secret, store it in the session. */
    #[Route('/profile/security/2fa/prepare', name: 'MineSeekerBundle_2fa_prepare', methods: ['POST'])]
    public function prepare(Request $request): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');

        if (!$this->isCsrfTokenValid('2fa_prepare', $request->request->get('_token'))) {
            throw $this->createAccessDeniedException('Invalid CSRF token.');
        }

        $secret = $this->totpAuthenticator->generateSecret();
        $request->getSession()->set('totp_pending_secret', $secret);

        return $this->redirectToRoute('MineSeekerBundle_2fa_setup');
    }

    /** Step 2 — show the setup page with QR code and verification form. */
    #[Route('/profile/security/2fa/setup', name: 'MineSeekerBundle_2fa_setup', methods: ['GET'])]
    public function setup(Request $request): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');

        $pendingSecret = $request->getSession()->get('totp_pending_secret');
        if (!$pendingSecret) {
            return $this->redirectToRoute('MineSeekerBundle_profile_security');
        }

        return $this->render('Security/2fa_setup.html.twig', [
            'pending_secret' => $pendingSecret,
        ]);
    }

    /** Serve a PNG QR code for the pending TOTP secret. */
    #[Route('/profile/security/2fa/qr-code', name: 'MineSeekerBundle_2fa_qr_code', methods: ['GET'])]
    public function qrCode(Request $request): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');

        /** @var User $user */
        $user = $this->getUser();
        $pendingSecret = $request->getSession()->get('totp_pending_secret');

        if (!$pendingSecret) {
            return new Response('', Response::HTTP_NOT_FOUND);
        }

        $provisioningUri = sprintf(
            'otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=SHA1&digits=6&period=30',
            rawurlencode('Mine Seeker'),
            rawurlencode($user->getUserIdentifier()),
            $pendingSecret,
            rawurlencode('Mine Seeker'),
        );

        try {
            $result = new Builder(
                writer: new PngWriter(),
                data: $provisioningUri,
                size: 220,
                margin: 10,
            )->build();
        } catch (ValidationException $e) {
            throw new RuntimeException("Failed to generate QR code: $e->getMessage()", 0, $e);
        }

        return new Response(
            $result->getString(),
            Response::HTTP_OK,
            ['Content-Type' => 'image/png', 'Cache-Control' => 'no-cache, no-store, must-revalidate'],
        );
    }

    /** Step 3 — verify the first code and save the secret. */
    #[Route('/profile/security/2fa/enable', name: 'MineSeekerBundle_2fa_enable', methods: ['POST'])]
    public function enable(Request $request): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');

        if (!$this->isCsrfTokenValid('2fa_enable', $request->request->get('_token'))) {
            throw $this->createAccessDeniedException('Invalid CSRF token.');
        }

        /** @var User $user */
        $user = $this->getUser();
        $pendingSecret = $request->getSession()->get('totp_pending_secret');

        if (!$pendingSecret) {
            $this->addFlash('error', 'No pending 2FA setup found. Please start again.');
            return $this->redirectToRoute('MineSeekerBundle_profile_security');
        }

        $code = $request->request->getString('_auth_code');

        // Temporarily set the pending secret to verify the code
        $user->totpSecret = $pendingSecret;

        if (!$this->totpAuthenticator->checkCode($user, $code)) {
            $user->totpSecret = null;
            $this->addFlash('error', 'Invalid verification code. Please try again.');
            return $this->redirectToRoute('MineSeekerBundle_2fa_setup');
        }

        $backupCodes = $this->generateBackupCodes();
        $user->backupCodes = $backupCodes;
        $this->em->flush();

        $request->getSession()->remove('totp_pending_secret');
        $this->addFlash('2fa_backup_codes', $backupCodes);
        $this->addFlash('success', 'Two-factor authentication has been enabled.');

        return $this->redirectToRoute('MineSeekerBundle_profile_security');
    }

    /** Disable TOTP for the current user. */
    #[Route('/profile/security/2fa/disable', name: 'MineSeekerBundle_2fa_disable', methods: ['POST'])]
    public function disable(Request $request): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');

        if (!$this->isCsrfTokenValid('2fa_disable', $request->request->get('_token'))) {
            throw $this->createAccessDeniedException('Invalid CSRF token.');
        }

        /** @var User $user */
        $user = $this->getUser();
        $user->totpSecret = null;
        $user->backupCodes = [];
        $this->em->flush();

        $this->addFlash('success', 'Two-factor authentication has been disabled.');
        return $this->redirectToRoute('MineSeekerBundle_profile_security');
    }

    /** Regenerate backup codes for the current user. */
    #[Route(
        '/profile/security/2fa/backup-codes/regenerate',
        name: 'MineSeekerBundle_2fa_backup_regenerate',
        methods: ['POST'],
    )]
    public function regenerateBackupCodes(Request $request): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');

        if (!$this->isCsrfTokenValid('2fa_backup_regen', $request->request->get('_token'))) {
            throw $this->createAccessDeniedException('Invalid CSRF token.');
        }

        /** @var User $user */
        $user = $this->getUser();

        if (!$user->isTotpAuthenticationEnabled()) {
            return $this->redirectToRoute('MineSeekerBundle_profile_security');
        }

        $backupCodes = $this->generateBackupCodes();
        $user->backupCodes = $backupCodes;
        $this->em->flush();

        $this->addFlash('2fa_backup_codes', $backupCodes);
        $this->addFlash('success', 'Backup codes have been regenerated. Save them somewhere safe.');

        return $this->redirectToRoute('MineSeekerBundle_profile_security');
    }

    /** @return string[] Eight 8-character uppercase alphanumeric codes */
    private function generateBackupCodes(int $count = 8): array
    {
        $codes = [];

        for ($i = 0; $i < $count; $i++) {
            $codes[] = strtoupper(bin2hex(random_bytes(4)));
        }

        return $codes;
    }
}
