<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\Api\Processor;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Administration\Application\Command\ActivateParentInvitation\ActivateParentInvitationCommand;
use App\Administration\Application\Command\ActivateParentInvitation\ActivateParentInvitationHandler;
use App\Administration\Application\Command\LinkParentToStudent\LinkParentToStudentCommand;
use App\Administration\Application\Command\LinkParentToStudent\LinkParentToStudentHandler;
use App\Administration\Domain\Exception\InvitationCodeInvalideException;
use App\Administration\Domain\Exception\InvitationDejaActiveeException;
use App\Administration\Domain\Exception\InvitationExpireeException;
use App\Administration\Domain\Exception\InvitationNonEnvoyeeException;
use App\Administration\Domain\Exception\ParentInvitationNotFoundException;
use App\Administration\Domain\Model\Invitation\ParentInvitationId;
use App\Administration\Domain\Model\StudentGuardian\RelationshipType;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\User;
use App\Administration\Domain\Policy\ConsentementParentalPolicy;
use App\Administration\Domain\Repository\ParentInvitationRepository;
use App\Administration\Domain\Repository\UserRepository;
use App\Administration\Infrastructure\Api\Resource\ActivateParentInvitationOutput;
use App\Administration\Infrastructure\Api\Resource\ParentInvitationResource;
use App\Shared\Domain\Clock;
use App\Shared\Infrastructure\Tenant\TenantId as InfrastructureTenantId;
use App\Shared\Infrastructure\Tenant\TenantRegistry;
use Override;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Throwable;

/**
 * Handles parent invitation activation (public endpoint).
 *
 * Creates a parent account, activates it, and links the parent to the student.
 *
 * @implements ProcessorInterface<ParentInvitationResource, ActivateParentInvitationOutput>
 */
final readonly class ActivateParentInvitationProcessor implements ProcessorInterface
{
    public function __construct(
        private ActivateParentInvitationHandler $handler,
        private UserRepository $userRepository,
        private ParentInvitationRepository $invitationRepository,
        private ConsentementParentalPolicy $consentementPolicy,
        private LinkParentToStudentHandler $linkHandler,
        private TenantRegistry $tenantRegistry,
        private Clock $clock,
        private MessageBusInterface $eventBus,
        private LoggerInterface $logger,
        private RateLimiterFactory $parentActivationByIpLimiter,
        private RequestStack $requestStack,
    ) {
    }

    /**
     * @param ParentInvitationResource $data
     */
    #[Override]
    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): ActivateParentInvitationOutput
    {
        // Rate limiting (H5: prevent DoS via bcrypt hashing)
        $request = $this->requestStack->getCurrentRequest();
        if ($request !== null) {
            $ip = $request->getClientIp() ?? 'unknown';
            $limiter = $this->parentActivationByIpLimiter->create($ip);
            $limit = $limiter->consume();

            if (!$limit->isAccepted()) {
                throw new TooManyRequestsHttpException(
                    $limit->getRetryAfter()->getTimestamp() - time(),
                    'Trop de tentatives. Veuillez réessayer plus tard.',
                );
            }
        }

        $command = new ActivateParentInvitationCommand(
            code: $data->code ?? '',
            firstName: $data->firstName ?? '',
            lastName: $data->lastName ?? '',
            password: $data->password ?? '',
        );

        try {
            $result = ($this->handler)($command);
        } catch (ParentInvitationNotFoundException|InvitationCodeInvalideException) {
            throw new NotFoundHttpException('Code d\'invitation invalide ou introuvable.');
        } catch (InvitationDejaActiveeException) {
            throw new HttpException(Response::HTTP_CONFLICT, 'Cette invitation a déjà été activée.');
        } catch (InvitationNonEnvoyeeException) {
            throw new BadRequestHttpException('Cette invitation n\'a pas encore été envoyée.');
        } catch (InvitationExpireeException) {
            throw new HttpException(Response::HTTP_GONE, 'Cette invitation a expiré. Veuillez contacter votre établissement.');
        }

        $tenantConfig = $this->tenantRegistry->getConfig(
            InfrastructureTenantId::fromString((string) $result->tenantId),
        );
        $now = $this->clock->now();

        // Check for duplicate email (H3: prevents duplicate accounts, H4: mitigates race condition)
        $existingUser = $this->userRepository->findByEmail(
            new Email($result->parentEmail),
            $result->tenantId,
        );

        if ($existingUser !== null) {
            throw new BadRequestHttpException('Un compte existe déjà avec cette adresse email.');
        }

        // Create parent user account
        $parentUser = User::inviter(
            email: new Email($result->parentEmail),
            role: Role::PARENT,
            tenantId: $result->tenantId,
            schoolName: $tenantConfig->subdomain,
            firstName: $result->firstName,
            lastName: $result->lastName,
            invitedAt: $now,
        );

        // Clear the UtilisateurInvite event (we don't want to trigger the regular invitation email)
        $parentUser->pullDomainEvents();

        // Activate the account immediately with the provided password
        $parentUser->activer(
            hashedPassword: $result->hashedPassword,
            at: $now,
            consentementPolicy: $this->consentementPolicy,
        );

        $this->userRepository->save($parentUser);

        // Dispatch activation events from User
        foreach ($parentUser->pullDomainEvents() as $event) {
            $this->eventBus->dispatch($event);
        }

        // Mark invitation as activated
        $invitation = $this->invitationRepository->get(
            ParentInvitationId::fromString($result->invitationId),
            $result->tenantId,
        );
        $invitation->activer($parentUser->id, $now);
        $this->invitationRepository->save($invitation);

        foreach ($invitation->pullDomainEvents() as $event) {
            $this->eventBus->dispatch($event);
        }

        // Auto-link parent to student (non-fatal failure)
        try {
            $link = ($this->linkHandler)(new LinkParentToStudentCommand(
                studentId: $result->studentId,
                guardianId: (string) $parentUser->id,
                relationshipType: RelationshipType::OTHER->value,
                tenantId: (string) $result->tenantId,
            ));

            foreach ($link->pullDomainEvents() as $linkEvent) {
                $this->eventBus->dispatch($linkEvent);
            }
        } catch (Throwable $e) {
            $this->logger->warning('Auto-link parent-élève échoué lors de l\'activation invitation : {message}', [
                'message' => $e->getMessage(),
                'userId' => (string) $parentUser->id,
                'studentId' => $result->studentId,
            ]);
        }

        return new ActivateParentInvitationOutput(
            userId: (string) $parentUser->id,
            email: $result->parentEmail,
        );
    }
}
