<?php

declare(strict_types=1);

namespace App\Infrastructure\Http\Middleware;

use App\Domain\Auth\AuthenticatedPrincipal;
use App\Domain\Auth\TokenKind;
use App\Infrastructure\Auth\UserRepository;
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;

/**
 * For service tokens, requires `X-Acting-User-Id` and rewrites the
 * principal to carry that user's id and role. Per SPEC §8 this header is
 * trusted *only* alongside a service token; for any other kind it is
 * silently ignored (no 400 — that would let an attacker probe header
 * handling on non-service tokens).
 *
 * Failure modes:
 *  - service token + missing header                → 400
 *  - service token + malformed header              → 400
 *  - service token + header points to unknown user → 403
 *  - service token + header points to disabled user → 403 (SEC_REVIEW F11)
 */
final class ImpersonationMiddleware implements MiddlewareInterface
{
    public const HEADER = 'X-Acting-User-Id';

    public function __construct(
        private readonly UserRepository $users,
        private readonly ResponseFactoryInterface $responseFactory,
    ) {
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $principal = $request->getAttribute(TokenAuthenticationMiddleware::ATTR_PRINCIPAL);
        if (!$principal instanceof AuthenticatedPrincipal) {
            // Auth middleware should have populated this. Defensive 401.
            return $this->jsonError(401, 'unauthorized');
        }

        if ($principal->tokenKind !== TokenKind::Service) {
            return $handler->handle($request);
        }

        $headerValue = trim($request->getHeaderLine(self::HEADER));
        if ($headerValue === '') {
            return $this->jsonError(400, 'missing X-Acting-User-Id');
        }

        if (preg_match('/^[1-9][0-9]*$/', $headerValue) !== 1) {
            return $this->jsonError(400, 'invalid X-Acting-User-Id');
        }

        $userId = (int) $headerValue;
        $user = $this->users->findById($userId);
        if ($user === null) {
            return $this->jsonError(403, 'unknown impersonated user');
        }

        // SEC_REVIEW F11: a disabled user is unimpersonatable until the
        // operator re-enables them. The 403 mirrors "unknown user" so an
        // attacker probing user ids cannot use the response to distinguish
        // "doesn't exist" from "disabled".
        if ($user->isDisabled()) {
            return $this->jsonError(403, 'user_disabled');
        }

        $rewritten = new AuthenticatedPrincipal(
            tokenKind: $principal->tokenKind,
            userId: $user->id,
            role: $user->role,
            reporterId: null,
            consumerId: null,
            tokenId: $principal->tokenId,
            userIsLocal: $user->isLocal,
        );

        return $handler->handle(
            $request->withAttribute(TokenAuthenticationMiddleware::ATTR_PRINCIPAL, $rewritten)
        );
    }

    private function jsonError(int $status, string $message): ResponseInterface
    {
        $response = $this->responseFactory->createResponse($status)
            ->withHeader('Content-Type', 'application/json');
        $response->getBody()->write((string) json_encode(['error' => $message]));

        return $response;
    }
}
