<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\Api\Processor;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Administration\Application\Command\RequestPasswordReset\RequestPasswordResetCommand;
use App\Administration\Application\Command\RequestPasswordReset\RequestPasswordResetHandler;
use App\Administration\Infrastructure\Api\Resource\RequestPasswordResetInput;
use App\Administration\Infrastructure\Api\Resource\RequestPasswordResetOutput;
use App\Shared\Domain\Tenant\TenantId;
use App\Shared\Infrastructure\Tenant\TenantNotFoundException;
use App\Shared\Infrastructure\Tenant\TenantResolver;
use Override;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\RateLimiter\RateLimiterFactory;

/**
 * API Platform processor for password reset request.
 *
 * Security:
 * - Always returns success to prevent email enumeration
 * - Rate limited: 3 requests/hour per email, 10 requests/hour per IP
 *
 * @implements ProcessorInterface<RequestPasswordResetInput, RequestPasswordResetOutput>
 */
final readonly class RequestPasswordResetProcessor implements ProcessorInterface
{
    public function __construct(
        private RequestPasswordResetHandler $handler,
        private RequestStack $requestStack,
        private TenantResolver $tenantResolver,
        private RateLimiterFactory $passwordResetByEmailLimiter,
        private RateLimiterFactory $passwordResetByIpLimiter,
    ) {
    }

    /**
     * @param RequestPasswordResetInput $data
     */
    #[Override]
    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): RequestPasswordResetOutput
    {
        $request = $this->requestStack->getCurrentRequest();

        if ($request === null) {
            throw new BadRequestHttpException('Request not available');
        }

        $email = strtolower(trim($data->email));
        $ip = $request->getClientIp() ?? 'unknown';
        $tenantId = $this->resolveCurrentTenant();

        // Check rate limits - returns false if email limit exceeded (skip processing silently)
        // Tenant is included in email key to isolate rate limits per establishment
        if (!$this->checkRateLimits($tenantId, $email, $ip)) {
            // Email rate limit exceeded - return success without processing
            // This prevents email enumeration while stopping token flooding
            return new RequestPasswordResetOutput();
        }

        $command = new RequestPasswordResetCommand(
            email: $email,
            tenantId: $tenantId,
        );

        // Handler always succeeds (no exceptions) - this is by design
        ($this->handler)($command);

        // Always return success message (prevents email enumeration)
        return new RequestPasswordResetOutput();
    }

    /**
     * Check rate limits for email and IP.
     *
     * @throws TooManyRequestsHttpException if IP rate limit exceeded
     *
     * @return bool true if processing should continue, false if email limit exceeded
     */
    private function checkRateLimits(TenantId $tenantId, string $email, string $ip): bool
    {
        // Check IP rate limit first (throws exception - visible to user)
        $ipLimiter = $this->passwordResetByIpLimiter->create($ip);
        $ipLimit = $ipLimiter->consume();

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

        // Check email rate limit (silent - prevents enumeration)
        // Key includes tenant to isolate rate limits per establishment
        $emailLimiter = $this->passwordResetByEmailLimiter->create("$tenantId:$email");
        $emailLimit = $emailLimiter->consume();

        if (!$emailLimit->isAccepted()) {
            // Return false to skip processing silently
            // User sees success but no token is generated (prevents flooding)
            return false;
        }

        return true;
    }

    /**
     * Resolves the current tenant from the request host.
     *
     * For localhost (dev), uses a default tenant.
     *
     * @throws BadRequestHttpException if tenant cannot be resolved
     */
    private function resolveCurrentTenant(): TenantId
    {
        $request = $this->requestStack->getCurrentRequest();

        if ($request === null) {
            throw new BadRequestHttpException('Request not available');
        }

        $host = $request->getHost();

        // Skip validation for localhost (dev environment uses ecole-alpha tenant)
        if ($host === 'localhost' || $host === '127.0.0.1') {
            // In dev mode, use ecole-alpha tenant
            return TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
        }

        try {
            return $this->tenantResolver->resolve($host)->tenantId;
        } catch (TenantNotFoundException) {
            throw new BadRequestHttpException('Établissement non reconnu.');
        }
    }
}
