<?php

declare(strict_types=1);

namespace App\Administration\Application\Command\RequestPasswordReset;

use App\Administration\Domain\Event\PasswordResetTokenGenerated;
use App\Administration\Domain\Exception\EmailInvalideException;
use App\Administration\Domain\Model\PasswordResetToken\PasswordResetToken;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Repository\PasswordResetTokenRepository;
use App\Administration\Domain\Repository\UserRepository;
use App\Shared\Domain\Clock;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\MessageBusInterface;

/**
 * Handles password reset requests.
 *
 * Security principles:
 * - Never reveals if email exists (prevents enumeration)
 * - Reuses existing valid token if one exists (prevents token flooding)
 * - Rate limited at API level (3 requests/hour per IP)
 */
#[AsMessageHandler(bus: 'command.bus')]
final readonly class RequestPasswordResetHandler
{
    public function __construct(
        private UserRepository $userRepository,
        private PasswordResetTokenRepository $tokenRepository,
        private Clock $clock,
        private MessageBusInterface $eventBus,
    ) {
    }

    /**
     * Process password reset request.
     *
     * Always succeeds from the caller's perspective.
     * Only generates token and dispatches events if user exists.
     *
     * Security: performs similar work for non-existent emails to prevent timing attacks.
     */
    public function __invoke(RequestPasswordResetCommand $command): void
    {
        // Try to parse email - silently return on invalid email (no enumeration)
        try {
            $email = new Email($command->email);
        } catch (EmailInvalideException) {
            // Perform dummy work to match timing of valid email path
            $this->simulateTokenLookup();

            return;
        }

        // Try to find user by email for this tenant
        $user = $this->userRepository->findByEmail($email, $command->tenantId);

        // If user doesn't exist, silently return (no email enumeration)
        if ($user === null) {
            // Perform dummy work to match timing of valid email path
            // This prevents timing attacks that could reveal email existence
            $this->simulateTokenLookup();

            return;
        }

        // Check if a valid token already exists for this user
        $existingToken = $this->tokenRepository->findValidTokenForUser((string) $user->id);

        if ($existingToken !== null) {
            // Reuse existing token - manually dispatch event to resend email
            // (reconstituted tokens don't have domain events)
            $this->eventBus->dispatch(new PasswordResetTokenGenerated(
                tokenId: $existingToken->id,
                tokenValue: $existingToken->tokenValue,
                userId: $existingToken->userId,
                email: $existingToken->email,
                tenantId: $existingToken->tenantId,
                occurredOn: $this->clock->now(),
            ));

            return;
        }

        // Generate new reset token
        $now = $this->clock->now();
        $token = PasswordResetToken::generate(
            userId: (string) $user->id,
            email: $command->email,
            tenantId: $command->tenantId,
            createdAt: $now,
        );

        // Persist token
        $this->tokenRepository->save($token);

        // Dispatch domain events (triggers email sending)
        foreach ($token->pullDomainEvents() as $event) {
            $this->eventBus->dispatch($event);
        }
    }

    /**
     * Performs dummy work to match timing of valid email path.
     *
     * This prevents timing attacks that could reveal email existence by ensuring
     * similar processing time regardless of whether the email exists or not.
     */
    private function simulateTokenLookup(): void
    {
        // Generate a fake userId to query (will not exist)
        // This matches the timing of findValidTokenForUser() call
        $fakeUserId = bin2hex(random_bytes(16));
        $this->tokenRepository->findValidTokenForUser($fakeUserId);
    }
}
