<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CmsBundle\Security;

use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Repository\UserRepository;
use Ferienpass\CoreBundle\Repository\WebauthnUserEntityRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Uid\Uuid;
use Webauthn\Bundle\Exception\MissingUserEntityException;
use Webauthn\Bundle\Security\Guesser\UserEntityGuesser;
use Webauthn\PublicKeyCredentialUserEntity;

/**
 * Resolves the WebAuthn user entity for the account that was just created
 * through the registration form, so a passkey can be attached before the
 * account is activated — without logging in.
 *
 * SECURITY: this guesser drives a *public* creation endpoint
 * (`/register/passkey/*`), so it must positively prove that the caller is in
 * the middle of *their own* fresh sign-up — never trust an attacker-supplied
 * e-mail. We therefore require all of:
 *   1. the passkey *intent* flag the registration form set (`registration.passkey`),
 *   2. the UUID of the account created in *this* session (`registration.passkey_uuid`,
 *      only ever set on the account-creation branch of {@see Registration::submit()}),
 *   3. that the resolved account is still *disabled* (pending e-mail confirmation).
 * Without (2)+(3) an attacker could submit the form with a victim's existing
 * e-mail and bind their own authenticator to that account (account takeover).
 *
 * @see Ferienpass\CmsBundle\Components\SignIn\Registration
 */
final readonly class PendingRegistrationUserEntityGuesser implements UserEntityGuesser
{
    public function __construct(
        private RequestStack $requestStack,
        private WebauthnUserEntityRepository $userEntities,
        private UserRepository $users,
    ) {
    }

    public function findUserEntity(Request $request): PublicKeyCredentialUserEntity
    {
        $session = $this->requestStack->getSession();

        if (true !== $session->get('registration.passkey')) {
            throw MissingUserEntityException::create('No pending passkey registration in the session.');
        }

        $uuid = (string) $session->get('registration.passkey_uuid', '');
        if ('' === $uuid || !Uuid::isValid($uuid)) {
            throw MissingUserEntityException::create('No pending registration found in the session.');
        }

        $user = $this->users->findOneBy(['uuid' => Uuid::fromString($uuid)]);

        // The endpoint may only attach a passkey to the freshly registered,
        // not-yet-activated account from this session. An already-enabled
        // account manages its passkeys through the authenticated
        // `/passkey/*` endpoints (CurrentUserEntityGuesser) instead.
        if (!$user instanceof User || !$user->isDisabled()) {
            throw MissingUserEntityException::create('No pending registration found in the session.');
        }

        $userEntity = $this->userEntities->findOneByUserHandle($uuid);

        $userEntity instanceof PublicKeyCredentialUserEntity || throw MissingUserEntityException::create('No pending registration found in the session.');

        return $userEntity;
    }
}
