<?php

declare(strict_types=1);

namespace App\Administration\Application\Command\ResetPassword;

use App\Administration\Application\Port\PasswordHasher;
use App\Administration\Domain\Exception\PasswordResetTokenAlreadyUsedException;
use App\Administration\Domain\Exception\PasswordResetTokenExpiredException;
use App\Administration\Domain\Exception\PasswordResetTokenNotFoundException;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\PasswordResetTokenRepository;
use App\Administration\Domain\Repository\RefreshTokenRepository;
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 using a valid token.
 *
 * Security:
 * - Validates token is not expired
 * - Validates token is not already used
 * - Marks token as used after successful reset
 * - Invalidates all user sessions (force re-authentication)
 * - Dispatches events for audit and confirmation email
 */
#[AsMessageHandler(bus: 'command.bus')]
final readonly class ResetPasswordHandler
{
    public function __construct(
        private PasswordResetTokenRepository $tokenRepository,
        private UserRepository $userRepository,
        private RefreshTokenRepository $refreshTokenRepository,
        private PasswordHasher $passwordHasher,
        private Clock $clock,
        private MessageBusInterface $eventBus,
    ) {
    }

    /**
     * Process password reset.
     *
     * @throws PasswordResetTokenNotFoundException    if token doesn't exist
     * @throws PasswordResetTokenExpiredException     if token has expired
     * @throws PasswordResetTokenAlreadyUsedException if token was already used
     */
    public function __invoke(ResetPasswordCommand $command): void
    {
        $now = $this->clock->now();

        // Atomically consume token (validates + marks as used in one operation)
        // This prevents race conditions where two concurrent requests could both
        // pass validation before either marks the token as used
        $token = $this->tokenRepository->consumeIfValid($command->token, $now);

        // Find user
        $userId = UserId::fromString($token->userId);
        $user = $this->userRepository->get($userId);

        // Hash and update password
        $hashedPassword = $this->passwordHasher->hash($command->newPassword);
        $user->changerMotDePasse($hashedPassword, $now);

        // Save user changes
        $this->userRepository->save($user);

        // Invalidate all user sessions (force re-authentication on all devices)
        $this->refreshTokenRepository->invalidateAllForUser($userId);

        // Note: We intentionally keep the used token in storage (until TTL expiry)
        // This allows distinguishing "already used" (410) from "invalid" (400)
        // when the same link is submitted again

        // Dispatch domain events from user (MotDePasseChange)
        foreach ($user->pullDomainEvents() as $event) {
            $this->eventBus->dispatch($event);
        }

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