<?php

declare(strict_types=1);

namespace CpCreation\AltchaBundle\Controller;

use CpCreation\AltchaBundle\Contract\AltchaServiceClientInterface;
use CpCreation\AltchaBundle\Exception\AltchaServiceRejectedException;
use CpCreation\AltchaBundle\Scope\ScopeRegistry;
use CpCreation\AltchaBundle\Security\ClientIdentity;
use CpCreation\AltchaBundle\Security\LocalRateLimiter;
use CpCreation\AltchaBundle\Security\ProtectionEnvelopeManager;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;

final readonly class ChallengeController
{
    public function __construct(
        private AltchaServiceClientInterface $client,
        private ScopeRegistry $scopeRegistry,
        private ProtectionEnvelopeManager $envelopeManager,
        private LocalRateLimiter $localRateLimiter,
        private ClientIdentity $clientIdentity = new ClientIdentity(),
    ) {
    }

    #[Route(
        '/_altcha/challenge/{scope}',
        name: 'cp_creation_altcha_challenge',
        requirements: ['scope' => '[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*'],
        methods: ['GET'],
    )]
    public function __invoke(Request $request, string $scope): JsonResponse
    {
        if (!$this->scopeRegistry->contains($scope)) {
            return $this->error('unknown_scope', 404);
        }

        $formToken = $request->query->get('formToken');
        if (!is_string($formToken) || '' === $formToken || 4_096 < strlen($formToken)) {
            return $this->error('invalid_form_token', 400);
        }

        $clientIp = $this->clientIdentity->normalizeIp($request->getClientIp());
        $localLimit = $this->localRateLimiter->consumeChallenge($scope, $clientIp);
        if (!$localLimit->accepted) {
            return $this->error('rate_limited', 429, $localLimit->retryAfter);
        }

        try {
            $policy = $this->client->resolvePolicy($scope, $request->getHost());
        } catch (AltchaServiceRejectedException $exception) {
            return $this->error($exception->reason->value, $exception->statusCode, $exception->retryAfter);
        } catch (\Throwable) {
            return $this->error('unavailable', 503);
        }

        if (!$policy->enabled || !$policy->altchaEnabled) {
            return $this->error('captcha_disabled', 404);
        }
        if (null === $this->envelopeManager->validateForChallenge(
            $formToken,
            $scope,
            $request->getHost(),
            $policy->maximumEnvelopeAgeSeconds,
        )) {
            return $this->error('invalid_form_token', 400);
        }

        try {
            $challenge = $this->client->createChallenge(
                $scope,
                strtolower($request->getHost()),
                $clientIp,
                $formToken,
                $policy->version,
            );
        } catch (AltchaServiceRejectedException $exception) {
            return $this->error($exception->reason->value, $exception->statusCode, $exception->retryAfter);
        } catch (\Throwable) {
            return $this->error('unavailable', 503);
        }

        return new JsonResponse($challenge->toArray(), 200, $this->headers());
    }

    private function error(string $code, int $status, ?int $retryAfter = null): JsonResponse
    {
        $headers = $this->headers();
        if (null !== $retryAfter) {
            $headers['Retry-After'] = (string) $retryAfter;
        }

        return new JsonResponse(['error' => $code], $status, $headers);
    }

    /** @return array<string, string> */
    private function headers(): array
    {
        return [
            'Cache-Control' => 'no-store, private, max-age=0',
            'Pragma' => 'no-cache',
            'X-Content-Type-Options' => 'nosniff',
        ];
    }
}
