<?php

declare(strict_types=1);

namespace apexl\Io\modules\user\Middleware;

use apexl\Config\ConfigurationInterface;
use apexl\Io\modules\user\enums\TwoFactorGateDecision;
use apexl\Io\modules\user\enums\TwoFactorMode;
use apexl\Io\modules\user\Actions\TwoFactorGateAction;
use apexl\Io\modules\user\services\CurrentUserFactory;
use apexl\Io\modules\user\interfaces\SessionManagerInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Psr7\Factory\StreamFactory;
use Slim\Psr7\Response as SlimResponse;
use Slim\Routing\RouteContext;
use Throwable;

/**
 * Per-request 2FA enforcement (Plan 007 §2). Registered via
 * ProvidesMiddlewareInterface AFTER SessionMiddleware (needs the loaded
 * session + `authenticated` attribute) and BEFORE PermissionMiddleware.
 *
 * The session "passed" flag reuses V3's existing
 * SessionManagerInterface::addDataToCurrentSession(['twoFaPassed' => true]) /
 * sessions.session_data — no bespoke session storage.
 *
 * COMPATIBILITY GUARANTEE: when mode is off this short-circuits on the very
 * first line, before touching the session, user, or route — byte-for-byte the
 * same request handling as before the feature existed.
 */
final readonly class TwoFactorMiddleware implements MiddlewareInterface
{
    public const string SESSION_PASSED_KEY = 'twoFaPassed';

    /** Route names that must stay reachable while a user is gated. */
    private const array IGNORED_ROUTE_NAMES = [
        'user.login',
        'user.login.nfc',
        'user.logout',
        'user.refresh',
        'user.forgot-password',
        'user.forgot-password.hash',
        'user.resetPassword',
        'user.masquerade.end',
    ];

    /** URI-path fragments that must stay reachable (frontend bootstrap). */
    private const array IGNORED_PATH_FRAGMENTS = [
        'globalData',
        '/action/two-factor-auth/',
    ];

    public function __construct(
        private ConfigurationInterface $config,
        private CurrentUserFactory $currentUserFactory,
        private TwoFactorGateAction $gateAction,
        private SessionManagerInterface $sessionManager,
        private StreamFactory $streamFactory,
    ) {}

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): Response
    {
        // Compatibility short-circuit — mode off means zero behavioural change.
        if ($this->mode()->isOff()) {
            return $handler->handle($request);
        }

        // Never gate preflight, and never gate the routes needed to pass the gate
        // itself or to bootstrap the frontend that renders the challenge.
        if ($request->getMethod() === 'OPTIONS' || $this->isIgnored($request)) {
            return $handler->handle($request);
        }

        // 2FA only gates authenticated sessions. Anonymous access is governed by
        // SessionMiddleware / auth.requires_auth, not here.
        if ($request->getAttribute('authenticated') !== true) {
            return $handler->handle($request);
        }

        $user = $this->currentUserFactory->get();
        if ($user === null || !$user->id) {
            return $handler->handle($request);
        }

        // Already satisfied the second factor for this session? Let it through.
        if ($this->sessionHasPassedFlag()) {
            return $handler->handle($request);
        }

        $decision = $this->gateAction->evaluate($user);
        if (!$decision->requiresGate()) {
            return $handler->handle($request);
        }

        return $this->blockedResponse($decision);
    }

    private function mode(): TwoFactorMode
    {
        return TwoFactorMode::fromConfigValue($this->config->get('auth.two_factor.mode'));
    }

    private function isIgnored(ServerRequestInterface $request): bool
    {
        $path = $request->getUri()->getPath();
        foreach (self::IGNORED_PATH_FRAGMENTS as $fragment) {
            if (str_contains($path, $fragment)) {
                return true;
            }
        }

        $name = $this->routeName($request);
        if ($name === null) {
            // Unmatched route — let the normal 404 path run rather than gating it.
            return true;
        }

        return in_array($name, self::IGNORED_ROUTE_NAMES, true)
            || str_starts_with($name, 'user.twoFactor.');
    }

    private function routeName(ServerRequestInterface $request): ?string
    {
        try {
            $route = RouteContext::fromRequest($request)->getRoute();
        } catch (Throwable) {
            return null;
        }

        return $route?->getName();
    }

    private function sessionHasPassedFlag(): bool
    {
        try {
            $session = $this->sessionManager->current();
        } catch (Throwable) {
            return false;
        }

        return $session !== null && (($session->data[self::SESSION_PASSED_KEY] ?? false) === true);
    }

    private function blockedResponse(TwoFactorGateDecision $decision): Response
    {
        $message = $decision === TwoFactorGateDecision::SETUP_REQUIRED
            ? 'Two-factor authentication setup is required before continuing.'
            : 'Two-factor authentication is required to continue.';

        $body = [
            'success' => false,
            'two_factor_required' => true,
            'two_factor_status' => $decision->value, // 'challenge' | 'setup_required'
            'message' => $message,
        ];

        return new SlimResponse(403)
            ->withHeader('Content-Type', 'application/json')
            ->withHeader('X-Two-Factor-Required', $decision->value)
            ->withBody($this->streamFactory->createStream(json_encode($body, JSON_THROW_ON_ERROR)));
    }
}
