<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\Security;

use App\Administration\Domain\Event\CompteBloqueTemporairement;
use App\Administration\Domain\Event\ConnexionEchouee;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use App\Shared\Infrastructure\Monitoring\MetricsCollector;
use App\Shared\Infrastructure\RateLimit\LoginRateLimiterInterface;
use App\Shared\Infrastructure\RateLimit\LoginRateLimitResult;
use App\Shared\Infrastructure\Tenant\TenantResolver;

use function is_array;
use function is_string;
use function sprintf;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Throwable;

/**
 * Handles login failures: Fibonacci rate limiting, audit, user-friendly messages.
 *
 * Important: Never reveal whether the email exists or not (AC2).
 *
 * Note: /api/login is excluded from TenantMiddleware, so we resolve tenant
 * directly from host header using TenantResolver.
 *
 * @see Story 1.4 - T5: Backend Login Endpoint
 */
final readonly class LoginFailureHandler implements AuthenticationFailureHandlerInterface
{
    public function __construct(
        private LoginRateLimiterInterface $rateLimiter,
        private MessageBusInterface $eventBus,
        private Clock $clock,
        private TenantResolver $tenantResolver,
        private MetricsCollector $metricsCollector,
    ) {
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
    {
        // Suspended account: return a specific message without recording a rate-limit failure.
        // This prevents blocked users from triggering CAPTCHA/delay escalation
        // while informing them clearly why they cannot log in.
        if ($exception instanceof CustomUserMessageAccountStatusException) {
            $this->metricsCollector->recordLoginFailure('account_suspended');

            return $this->createSuspendedResponse($exception);
        }

        $content = json_decode($request->getContent(), true);
        $email = is_array($content) && isset($content['email']) && is_string($content['email'])
            ? $content['email']
            : 'unknown';
        $ipAddress = $request->getClientIp() ?? 'unknown';
        $userAgent = $request->headers->get('User-Agent', 'unknown');

        // Record the failure and get the new state
        $result = $this->rateLimiter->recordFailure($request, $email);

        // Resolve tenant from host header (TenantMiddleware skips /api/login)
        $tenantId = $this->resolveTenantFromHost($request);

        $this->eventBus->dispatch(new ConnexionEchouee(
            email: $email,
            tenantId: $tenantId,
            ipAddress: $ipAddress,
            userAgent: $userAgent,
            reason: 'invalid_credentials',
            occurredOn: $this->clock->now(),
        ));

        // Record metric for Prometheus alerting
        $this->metricsCollector->recordLoginFailure('invalid_credentials');

        // If the IP was just blocked
        if ($result->ipBlocked) {
            $this->eventBus->dispatch(new CompteBloqueTemporairement(
                email: $email,
                tenantId: $tenantId,
                ipAddress: $ipAddress,
                userAgent: $userAgent,
                blockedForSeconds: $result->retryAfter ?? LoginRateLimiterInterface::IP_BLOCK_DURATION,
                failedAttempts: $result->attempts,
                occurredOn: $this->clock->now(),
            ));

            return $this->createBlockedResponse($result);
        }

        // Standard failure response with delay and CAPTCHA info
        return $this->createFailureResponse($result);
    }

    private function createSuspendedResponse(CustomUserMessageAccountStatusException $exception): JsonResponse
    {
        return new JsonResponse([
            'type' => '/errors/account-suspended',
            'title' => 'Compte suspendu',
            'status' => Response::HTTP_FORBIDDEN,
            'detail' => $exception->getMessageKey(),
        ], Response::HTTP_FORBIDDEN);
    }

    private function createBlockedResponse(LoginRateLimitResult $result): JsonResponse
    {
        $response = new JsonResponse([
            'type' => '/errors/ip-blocked',
            'title' => 'Accès temporairement bloqué',
            'status' => Response::HTTP_TOO_MANY_REQUESTS,
            'detail' => sprintf(
                'Trop de tentatives de connexion. Réessayez dans %s.',
                $result->getFormattedDelay(),
            ),
            'retryAfter' => $result->retryAfter,
        ], Response::HTTP_TOO_MANY_REQUESTS);

        foreach ($result->toHeaders() as $name => $value) {
            $response->headers->set($name, $value);
        }

        return $response;
    }

    private function createFailureResponse(LoginRateLimitResult $result): JsonResponse
    {
        $data = [
            'type' => '/errors/authentication-failed',
            'title' => 'Identifiants incorrects',
            'status' => Response::HTTP_UNAUTHORIZED,
            'detail' => 'L\'adresse email ou le mot de passe est incorrect.',
            'attempts' => $result->attempts,
        ];

        // Add delay if applicable
        if ($result->delaySeconds > 0) {
            $data['delay'] = $result->delaySeconds;
            $data['delayFormatted'] = $result->getFormattedDelay();
        }

        // Indicate if CAPTCHA is required for the next attempt
        if ($result->requiresCaptcha) {
            $data['captchaRequired'] = true;
        }

        $response = new JsonResponse($data, Response::HTTP_UNAUTHORIZED);

        foreach ($result->toHeaders() as $name => $value) {
            $response->headers->set($name, $value);
        }

        return $response;
    }

    /**
     * Resolve tenant from request host header.
     *
     * Since /api/login is excluded from TenantMiddleware, we must resolve
     * the tenant ourselves to properly scope audit events.
     *
     * Returns null if tenant cannot be resolved (unknown domain, database issues, etc.)
     * to ensure login failure handling never breaks due to tenant resolution.
     */
    private function resolveTenantFromHost(Request $request): ?TenantId
    {
        try {
            $config = $this->tenantResolver->resolve($request->getHost());

            return $config->tenantId;
        } catch (Throwable) {
            // Login attempt on unknown domain or tenant resolution failed
            // Don't let tenant resolution break the login failure handling
            return null;
        }
    }
}
