<?php

declare(strict_types=1);

namespace App\Infrastructure\Http\Middleware;

use App\Domain\Audit\AuditContext;
use App\Domain\Auth\AuthenticatedPrincipal;
use App\Domain\Auth\TokenKind;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
 * Resolves an {@see AuditContext} from the active principal +
 * source IP and attaches it to the request. Runs AFTER
 * TokenAuthenticationMiddleware + ImpersonationMiddleware so that
 * service-token + impersonation has already produced `userId` and we
 * can honour the SPEC §8 invariant: the impersonated user is the
 * audit actor, never the service token.
 *
 * SEC_REVIEW F11: also derives `actor_via` (`oidc` / `local` /
 * `admin-token` / `service` / `reporter` / `consumer` / `system`) for
 * the audit row. ImpersonationMiddleware threads the user's `is_local`
 * flag through to the principal so we can split impersonated rows by
 * sign-in path without re-querying `users`.
 */
final class AuditContextMiddleware implements MiddlewareInterface
{
    public const ATTR_AUDIT_CONTEXT = 'auditContext';

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $principal = $request->getAttribute(TokenAuthenticationMiddleware::ATTR_PRINCIPAL);
        $context = self::contextFor($principal, self::clientIp($request), self::requestId($request));
        $request = $request->withAttribute(self::ATTR_AUDIT_CONTEXT, $context);

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

    private static function contextFor(mixed $principal, ?string $sourceIp, ?string $requestId): AuditContext
    {
        if (!$principal instanceof AuthenticatedPrincipal) {
            return new AuditContext(
                AuditContext::KIND_SYSTEM,
                null,
                null,
                $sourceIp,
                $requestId,
                AuditContext::VIA_SYSTEM,
            );
        }

        // Service token + impersonation: attribute to the impersonated user.
        // `actor_via` distinguishes the upstream sign-in path so an auditor
        // can filter local-admin actions separately from OIDC-user actions.
        if ($principal->tokenKind === TokenKind::Service && $principal->userId !== null) {
            $via = $principal->userIsLocal === true
                ? AuditContext::VIA_LOCAL
                : AuditContext::VIA_OIDC;

            return new AuditContext(
                AuditContext::KIND_USER,
                $principal->userId,
                null,
                $sourceIp,
                $requestId,
                $via,
            );
        }

        return match ($principal->tokenKind) {
            TokenKind::Admin => new AuditContext(
                AuditContext::KIND_ADMIN_TOKEN,
                $principal->userId ?? $principal->tokenId,
                null,
                $sourceIp,
                $requestId,
                AuditContext::VIA_ADMIN_TOKEN,
            ),
            TokenKind::Reporter => new AuditContext(
                AuditContext::KIND_REPORTER,
                $principal->reporterId,
                null,
                $sourceIp,
                $requestId,
                AuditContext::VIA_REPORTER,
            ),
            TokenKind::Consumer => new AuditContext(
                AuditContext::KIND_CONSUMER,
                $principal->consumerId,
                null,
                $sourceIp,
                $requestId,
                AuditContext::VIA_CONSUMER,
            ),
            // Service token without impersonation (auth endpoints only).
            TokenKind::Service => new AuditContext(
                AuditContext::KIND_SYSTEM,
                null,
                null,
                $sourceIp,
                $requestId,
                AuditContext::VIA_SERVICE,
            ),
        };
    }

    private static function clientIp(ServerRequestInterface $request): ?string
    {
        $server = $request->getServerParams();
        $remote = $server['REMOTE_ADDR'] ?? null;

        return is_string($remote) && $remote !== '' ? $remote : null;
    }

    private static function requestId(ServerRequestInterface $request): ?string
    {
        $header = $request->getHeaderLine('X-Request-Id');

        return $header !== '' ? $header : null;
    }
}
