<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\Api\Processor;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Administration\Application\Service\RefreshTokenManager;
use App\Administration\Domain\Event\TokenReplayDetecte;
use App\Administration\Domain\Exception\TokenAlreadyRotatedException;
use App\Administration\Domain\Exception\TokenReplayDetectedException;
use App\Administration\Domain\Model\RefreshToken\DeviceFingerprint;
use App\Administration\Domain\Repository\SessionRepository;
use App\Administration\Domain\Repository\UserRepository;
use App\Administration\Infrastructure\Api\Resource\RefreshTokenInput;
use App\Administration\Infrastructure\Api\Resource\RefreshTokenOutput;
use App\Administration\Infrastructure\Security\SecurityUserFactory;
use App\Shared\Domain\Clock;
use App\Shared\Infrastructure\Tenant\TenantNotFoundException;
use App\Shared\Infrastructure\Tenant\TenantResolver;
use DateTimeImmutable;
use InvalidArgumentException;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\Messenger\MessageBusInterface;

/**
 * Processor for token refresh.
 *
 * Flow:
 * 1. Read the refresh token from the HttpOnly cookie
 * 2. Validate the token and device fingerprint
 * 3. Detect replay attacks
 * 4. Generate a new JWT and rotate the refresh token
 * 5. Update the cookie
 *
 * @implements ProcessorInterface<RefreshTokenInput, RefreshTokenOutput>
 *
 * @see Story 1.4 - T6: Refresh Token Endpoint
 */
final readonly class RefreshTokenProcessor implements ProcessorInterface
{
    public function __construct(
        private RefreshTokenManager $refreshTokenManager,
        private JWTTokenManagerInterface $jwtManager,
        private UserRepository $userRepository,
        private SessionRepository $sessionRepository,
        private RequestStack $requestStack,
        private SecurityUserFactory $securityUserFactory,
        private TenantResolver $tenantResolver,
        private MessageBusInterface $eventBus,
        private Clock $clock,
    ) {
    }

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

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

        // Read the refresh token from the cookie
        $refreshTokenString = $request->cookies->get('refresh_token');

        if ($refreshTokenString === null) {
            throw new UnauthorizedHttpException('Bearer', 'Refresh token not found');
        }

        // Create the device fingerprint for validation
        $ipAddress = $request->getClientIp() ?? 'unknown';
        $userAgent = $request->headers->get('User-Agent', 'unknown');
        $fingerprint = DeviceFingerprint::fromRequest($userAgent, $ipAddress);

        try {
            // Validate and rotate the refresh token
            $newRefreshToken = $this->refreshTokenManager->refresh($refreshTokenString, $fingerprint);

            // Security: verify that the refresh token's tenant matches the request tenant
            // Prevents using a token from one tenant to access another
            $currentTenantId = $this->resolveCurrentTenant($request->getHost());
            if ($currentTenantId !== null && (string) $newRefreshToken->tenantId !== (string) $currentTenantId) {
                $this->clearRefreshTokenCookie();

                throw new AccessDeniedHttpException('Invalid token for this tenant');
            }

            // Load the user to generate the JWT
            $user = $this->userRepository->get($newRefreshToken->userId);

            // Verify the user can still log in (not suspended/archived)
            if (!$user->peutSeConnecter()) {
                // Invalidate the entire family and delete the cookie
                $this->refreshTokenManager->invalidateFamily($newRefreshToken->familyId);
                $this->clearRefreshTokenCookie();

                throw new AccessDeniedHttpException('Account is no longer active');
            }

            $securityUser = $this->securityUserFactory->fromDomainUser($user);

            // Update session last activity with TTL aligned to refresh token expiry
            $now = $this->clock->now();
            $remainingTtlSeconds = $newRefreshToken->expiresAt->getTimestamp() - $now->getTimestamp();
            $this->sessionRepository->updateActivity($newRefreshToken->familyId, $now, $remainingTtlSeconds);

            // Generate the new JWT
            $jwt = $this->jwtManager->create($securityUser);

            // Store the cookie in request attributes for the listener
            // The RefreshTokenCookieListener will add it to the response
            // Secure flag only on HTTPS (prod), not HTTP (dev)
            $isSecure = $request->isSecure();
            $cookie = Cookie::create('refresh_token')
                ->withValue($newRefreshToken->toTokenString())
                ->withExpires($newRefreshToken->expiresAt)
                ->withPath('/api')
                ->withSecure($isSecure)
                ->withHttpOnly(true)
                ->withSameSite($isSecure ? 'strict' : 'lax');

            $request->attributes->set('_refresh_token_cookie', $cookie);

            return new RefreshTokenOutput(token: $jwt);
        } catch (TokenReplayDetectedException $e) {
            // Replay attack detected - the family has been invalidated
            // Dispatch the security event for alerts/audit
            $this->eventBus->dispatch(new TokenReplayDetecte(
                familyId: $e->familyId,
                ipAddress: $ipAddress,
                userAgent: $userAgent,
                occurredOn: $this->clock->now(),
            ));

            // Delete the cookie on client side
            $this->clearRefreshTokenCookie();

            throw new AccessDeniedHttpException(
                'Session compromise detected. All sessions have been invalidated. Please log in again.',
            );
        } catch (TokenAlreadyRotatedException) {
            // Token already rotated but in grace period - legitimate race condition
            // DO NOT delete the cookie! The client probably already has the new token
            // from a concurrent request. Return 409 Conflict so the client retries.
            throw new ConflictHttpException('Token already rotated, retry with current cookie');
        } catch (InvalidArgumentException $e) {
            // Invalid or expired token
            $this->clearRefreshTokenCookie();

            throw new UnauthorizedHttpException('Bearer', $e->getMessage());
        }
    }

    private function clearRefreshTokenCookie(): void
    {
        $request = $this->requestStack->getCurrentRequest();

        if ($request !== null) {
            $isSecure = $request->isSecure();
            $cookie = Cookie::create('refresh_token')
                ->withValue('')
                ->withExpires(new DateTimeImmutable('-1 day'))
                ->withPath('/api')
                ->withSecure($isSecure)
                ->withHttpOnly(true)
                ->withSameSite($isSecure ? 'strict' : 'lax');

            $request->attributes->set('_refresh_token_cookie', $cookie);
        }
    }

    /**
     * Resolves the current tenant from the request host.
     *
     * Returns null only for localhost (dev environment).
     * Throws AccessDeniedHttpException for unknown hosts in production to prevent
     * cross-tenant token exchange via direct IP or base domain access.
     */
    private function resolveCurrentTenant(string $host): ?\App\Shared\Domain\Tenant\TenantId
    {
        // Skip validation for localhost (dev environment)
        if ($host === 'localhost' || $host === '127.0.0.1') {
            return null;
        }

        try {
            return $this->tenantResolver->resolve($host)->tenantId;
        } catch (TenantNotFoundException) {
            // Security: reject requests from unknown hosts to prevent cross-tenant attacks
            // An attacker with a valid refresh token from tenant A could try to use it
            // via direct IP access or base domain to bypass tenant isolation
            throw new AccessDeniedHttpException('Invalid host for token refresh');
        }
    }
}
