<?php

declare(strict_types=1);

namespace App\Infrastructure\Http\Middleware;

use App\Domain\Auth\AuthenticatedPrincipal;
use App\Infrastructure\Http\RateLimiter;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
 * Rate-limit gate, used in two positions per route group (SEC_REVIEW F27):
 *
 *  1. Outermost — runs before TokenAuthenticationMiddleware. The principal
 *     is absent so the bucket key falls back to `ip:<REMOTE_ADDR>`. This
 *     throttles invalid-bearer-token floods (every miss costs an
 *     `tokens.findByHash()` query and, on rare hash collisions, a
 *     `last_used_at` write) and 401-only paths that previously bypassed
 *     the limiter entirely.
 *
 *  2. After auth — runs once a principal exists, bucketed on
 *     `token:<tokenId>` so a single token cannot saturate the API even
 *     from a fleet of egress IPs.
 *
 * On exhaustion: 429 with `Retry-After: 1` per SPEC §6.
 */
final class RateLimitMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly RateLimiter $limiter,
        private readonly ResponseFactoryInterface $responseFactory,
    ) {
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $key = $this->bucketKey($request);

        if (!$this->limiter->tryConsume($key)) {
            $response = $this->responseFactory->createResponse(429)
                ->withHeader('Content-Type', 'application/json')
                ->withHeader('Retry-After', '1');
            $response->getBody()->write((string) json_encode(['error' => 'rate_limited']));

            return $response;
        }

        return $handler->handle($request);
    }

    private function bucketKey(ServerRequestInterface $request): string
    {
        $principal = $request->getAttribute(TokenAuthenticationMiddleware::ATTR_PRINCIPAL);
        if ($principal instanceof AuthenticatedPrincipal) {
            return 'token:' . $principal->tokenId;
        }

        $params = $request->getServerParams();
        $remote = $params['REMOTE_ADDR'] ?? null;

        return 'ip:' . (is_string($remote) && $remote !== '' ? $remote : 'unknown');
    }
}
