<?php

declare(strict_types=1);

namespace App\Http;

use App\ApiClient\AdminClient;
use App\ApiClient\ApiAuthException;
use App\ApiClient\ApiException;
use App\ApiClient\ApiNotFoundException;
use App\Auth\SessionManager;
use App\Auth\UserContext;
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;
use Psr\Log\LoggerInterface;

/**
 * Gates `/app/*` routes. If no user is in the session, stash the
 * requested URL as `next` and 302 to `/login`. After a successful
 * login the controller pops `next` and redirects there.
 *
 * SEC_REVIEW F36: the cached UserContext (role, identity) is
 * periodically re-checked against `GET /api/v1/admin/me`. If the api
 * reports the user as disabled, deleted, or with a changed role, the
 * UI session is brought back in line — disabled / unknown users are
 * logged out immediately and a role downgrade in Entra propagates to
 * the next protected request rather than waiting for the 8h idle
 * timeout. The api itself is also the authoritative gate (every data
 * call goes through `X-Acting-User-Id` and the api re-resolves the
 * principal each time), so this middleware is mostly about avoiding
 * the UI rendering nav items / forms the user can no longer use.
 */
final class AuthRequiredMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly SessionManager $sessions,
        private readonly ResponseFactoryInterface $responseFactory,
        private readonly AdminClient $admin,
        private readonly LoggerInterface $logger,
        private readonly int $revalidateAfterSeconds = 300,
    ) {
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $user = $this->sessions->getUser();
        if ($user === null) {
            $uri = $request->getUri();
            $path = $uri->getPath();
            $query = $uri->getQuery();
            $next = $query !== '' ? $path . '?' . $query : $path;
            $this->sessions->setNext($next);

            return $this->responseFactory
                ->createResponse(302)
                ->withHeader('Location', '/login');
        }

        if ($this->shouldRevalidate()) {
            $revoked = $this->revalidate($user);
            if ($revoked) {
                $this->sessions->clear();
                $this->sessions->flash('error', 'Your access was revoked. Please sign in again.');

                return $this->responseFactory
                    ->createResponse(302)
                    ->withHeader('Location', '/login');
            }
        }

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

    private function shouldRevalidate(): bool
    {
        $lastRev = $this->sessions->lastRevalidatedAt();
        if ($lastRev === null) {
            // Legacy/pre-F36 session — seed the clock and let the next
            // request past the threshold be the first real revalidation.
            $this->sessions->markRevalidated();

            return false;
        }

        return (time() - $lastRev) >= $this->revalidateAfterSeconds;
    }

    /**
     * @return bool `true` if the session must be cleared (user revoked).
     */
    private function revalidate(UserContext $user): bool
    {
        try {
            $live = $this->admin->getMe($user->userId);
        } catch (ApiAuthException $e) {
            // 401 = ui_service_token deployment problem; 403 = this user
            // is disabled / unknown. Only the 403 case revokes; a 401
            // would lock every session out on a deployment slip-up.
            if ($e->statusCode === 403) {
                $this->logger->warning('session revalidation: user revoked by api', [
                    'user_id' => $user->userId,
                    'api_error' => $e->apiError,
                ]);

                return true;
            }
            $this->logger->error('session revalidation: api auth error, keeping session', [
                'user_id' => $user->userId,
                'status' => $e->statusCode,
                'api_error' => $e->apiError,
            ]);
            $this->sessions->markRevalidated();

            return false;
        } catch (ApiNotFoundException $e) {
            // Defensive: the api currently returns 403 (not 404) when the
            // user record is missing. Treat 404 the same way for safety.
            $this->logger->warning('session revalidation: user record missing', [
                'user_id' => $user->userId,
                'api_error' => $e->apiError,
            ]);

            return true;
        } catch (ApiException $e) {
            // Api unreachable / 5xx / unexpected. Don't lock everyone out
            // on a backend blip; mark revalidated so we don't grind on it
            // every request, and try again after the next interval.
            $this->logger->warning('session revalidation: api unreachable, keeping session', [
                'user_id' => $user->userId,
                'error' => $e->getMessage(),
            ]);
            $this->sessions->markRevalidated();

            return false;
        }

        $live_email = $live->email;
        $changed = $live->role !== $user->role
            || ($live->displayName !== '' && $live->displayName !== $user->displayName)
            || $live_email !== $user->email;

        if ($changed) {
            $this->sessions->updateUser(new UserContext(
                userId: $user->userId,
                displayName: $live->displayName !== '' ? $live->displayName : $user->displayName,
                role: $live->role,
                email: $live_email,
                source: $user->source,
            ));
        }
        $this->sessions->markRevalidated();

        return false;
    }
}
