<?php

declare(strict_types=1);

namespace App\App;

use App\Auth\LocalLoginController;
use App\Auth\LogoutController;
use App\Auth\OidcController;
use App\Controllers\AllowlistController;
use App\Controllers\AuditController;
use App\Controllers\CategoriesController;
use App\Controllers\ConsumersController;
use App\Controllers\DashboardController;
use App\Controllers\HealthzController;
use App\Controllers\HomeController;
use App\Controllers\IpsController;
use App\Controllers\ManualBlocksController;
use App\Controllers\MeController;
use App\Controllers\NoAccessController;
use App\Controllers\PoliciesController;
use App\Controllers\ReportersController;
use App\Controllers\SearchController;
use App\Controllers\SettingsController;
use App\Controllers\TokensController;
use App\Controllers\UsersController;
use App\Http\AuthRequiredMiddleware;
use App\Http\CspMiddleware;
use App\Http\CsrfMiddleware;
use App\Http\JsonExceptionHandler;
use App\Http\SessionMiddleware;
use App\Http\TwigGlobalsMiddleware;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Slim\App;
use Slim\Factory\AppFactory as SlimAppFactory;
use Slim\Routing\RouteCollectorProxy;

/**
 * Builds the configured Slim app for the UI.
 *
 * Middleware order (Slim is LIFO; bottom = outermost):
 *   1. Session — always start the PHP session first.
 *   2. Csrf — needs a session to read/write the token.
 *   3. Csp — mints the per-request nonce; needs to run before
 *      TwigGlobals so the template global is populated.
 *   4. TwigGlobals — needs all above.
 *   5. Routing + body parsing.
 *
 * `/app/*` routes get an additional AuthRequiredMiddleware on the
 * route group so anonymous users bounce to /login.
 */
final class AppFactory
{
    /**
     * @return App<ContainerInterface|null>
     */
    public static function build(ContainerInterface $container): App
    {
        SlimAppFactory::setContainer($container);
        $app = SlimAppFactory::create();
        $app->addRoutingMiddleware();

        /** @var LoggerInterface $logger */
        $logger = $container->get(LoggerInterface::class);
        /** @var bool $isDev */
        $isDev = $container->get('settings.is_dev');

        $errorMiddleware = $app->addErrorMiddleware($isDev, true, true, $logger);
        /** @var JsonExceptionHandler $handler */
        $handler = $container->get(JsonExceptionHandler::class);
        $errorMiddleware->setDefaultErrorHandler($handler);

        // Slim middleware is LIFO — the last `add()` call runs first.
        // Order on the incoming request:
        //   1. Session (needs to start before anything reads $_SESSION)
        //   2. BodyParsing (so CSRF can read form fields)
        //   3. CSRF (reads from session + parsed body)
        //   4. CSP (mints the per-request nonce on the request attr)
        //   5. TwigGlobals (after CSRF token + CSP nonce attrs are set)
        //   6. AuthRequired (per /app/* group)
        /** @var TwigGlobalsMiddleware $globals */
        $globals = $container->get(TwigGlobalsMiddleware::class);
        /** @var CspMiddleware $csp */
        $csp = $container->get(CspMiddleware::class);
        /** @var CsrfMiddleware $csrf */
        $csrf = $container->get(CsrfMiddleware::class);
        /** @var SessionMiddleware $session */
        $session = $container->get(SessionMiddleware::class);
        $app->add($globals);
        $app->add($csp);
        $app->add($csrf);
        $app->addBodyParsingMiddleware();
        $app->add($session);

        /** @var HomeController $home */
        $home = $container->get(HomeController::class);
        $app->get('/', $home);

        /** @var HealthzController $healthz */
        $healthz = $container->get(HealthzController::class);
        $app->get('/healthz', $healthz);

        /** @var LocalLoginController $local */
        $local = $container->get(LocalLoginController::class);
        $app->get('/login', [$local, 'showLogin']);
        $app->post('/login/local', [$local, 'postLocal']);

        /** @var OidcController $oidc */
        $oidc = $container->get(OidcController::class);
        $app->get('/login/oidc', [$oidc, 'initiate']);
        $app->get('/oidc/callback', [$oidc, 'callback']);

        /** @var LogoutController $logout */
        $logout = $container->get(LogoutController::class);
        $app->post('/logout', $logout);

        /** @var NoAccessController $noAccess */
        $noAccess = $container->get(NoAccessController::class);
        $app->get('/no-access', $noAccess);

        /** @var AuthRequiredMiddleware $authRequired */
        $authRequired = $container->get(AuthRequiredMiddleware::class);

        $app->group('/app', function (RouteCollectorProxy $group) use ($container): void {
            /** @var MeController $me */
            $me = $container->get(MeController::class);
            $group->get('/me', $me);

            /** @var DashboardController $dashboard */
            $dashboard = $container->get(DashboardController::class);
            $group->get('/dashboard', $dashboard);

            /** @var IpsController $ips */
            $ips = $container->get(IpsController::class);
            $group->get('/ips', [$ips, 'index']);
            // SEC_REVIEW F43 + F72: pin to an IP-shaped charset AND
            // cap the length. 0-9, a-f/A-F (IPv6 hex), `.` (IPv4),
            // `:` (IPv6), and `%` (urlencoded colons survive).
            // Length cap 80 covers IPv4 (≤15), canonical IPv6 (≤39),
            // and IPv6-with-every-colon-urlencoded (≤53) with
            // headroom; anything past that is malformed-or-
            // malicious. Anything else — `/`, `..`, `?`, spaces,
            // dashes, multi-megabyte strings — 404s at the route
            // layer before anything reads `$args['ip']`.
            $group->get('/ips/{ip:[0-9a-fA-F.:%]{1,80}}', [$ips, 'show']);

            /** @var ManualBlocksController $manualBlocks */
            $manualBlocks = $container->get(ManualBlocksController::class);
            $group->get('/manual-blocks', [$manualBlocks, 'index']);
            $group->post('/manual-blocks', [$manualBlocks, 'create']);
            $group->post('/manual-blocks/{id}/delete', [$manualBlocks, 'delete']);
            // /app/subnets is an alias filtered to kind=subnet so the
            // sidebar's "Subnets" link lands on a focused list.
            $group->get('/subnets', [$manualBlocks, 'subnetsIndex']);

            /** @var AllowlistController $allowlist */
            $allowlist = $container->get(AllowlistController::class);
            $group->get('/allowlist', [$allowlist, 'index']);
            $group->post('/allowlist', [$allowlist, 'create']);
            $group->post('/allowlist/{id}/delete', [$allowlist, 'delete']);

            /** @var PoliciesController $policies */
            $policies = $container->get(PoliciesController::class);
            $group->get('/policies', [$policies, 'index']);
            $group->post('/policies', [$policies, 'create']);
            $group->get('/policies/{id}', [$policies, 'edit']);
            $group->post('/policies/{id}', [$policies, 'update']);
            $group->post('/policies/{id}/delete', [$policies, 'delete']);
            // GET-only XHR proxy used by the edit page's preview pane.
            $group->get('/policies/{id}/preview-proxy', [$policies, 'previewProxy']);
            $group->get('/policies/{id}/score-distribution-proxy', [$policies, 'scoreDistributionProxy']);

            /** @var ReportersController $reporters */
            $reporters = $container->get(ReportersController::class);
            $group->get('/reporters', [$reporters, 'index']);
            $group->post('/reporters', [$reporters, 'create']);
            $group->get('/reporters/{id}', [$reporters, 'edit']);
            $group->post('/reporters/{id}', [$reporters, 'update']);
            $group->post('/reporters/{id}/delete', [$reporters, 'delete']);

            /** @var ConsumersController $consumers */
            $consumers = $container->get(ConsumersController::class);
            $group->get('/consumers', [$consumers, 'index']);
            $group->post('/consumers', [$consumers, 'create']);
            $group->get('/consumers/{id}', [$consumers, 'edit']);
            $group->post('/consumers/{id}', [$consumers, 'update']);
            $group->post('/consumers/{id}/delete', [$consumers, 'delete']);

            /** @var TokensController $tokens */
            $tokens = $container->get(TokensController::class);
            $group->get('/tokens', [$tokens, 'index']);
            $group->post('/tokens', [$tokens, 'create']);
            $group->post('/tokens/{id}/delete', [$tokens, 'delete']);
            $group->post('/tokens/{id}/purge', [$tokens, 'purge']);

            /** @var CategoriesController $categories */
            $categories = $container->get(CategoriesController::class);
            $group->get('/categories', [$categories, 'index']);
            $group->post('/categories', [$categories, 'create']);
            $group->get('/categories/{id}', [$categories, 'edit']);
            $group->post('/categories/{id}', [$categories, 'update']);
            $group->post('/categories/{id}/delete', [$categories, 'delete']);

            /** @var AuditController $audit */
            $audit = $container->get(AuditController::class);
            $group->get('/audit', [$audit, 'index']);

            /** @var UsersController $users */
            $users = $container->get(UsersController::class);
            $group->get('/users', [$users, 'index']);
            $group->post('/users/{id}/disable', [$users, 'disable']);
            $group->post('/users/{id}/enable', [$users, 'enable']);

            /** @var SearchController $search */
            $search = $container->get(SearchController::class);
            $group->get('/search', [$search, 'index']);

            /** @var SettingsController $settings */
            $settings = $container->get(SettingsController::class);
            $group->get('/settings', [$settings, 'index']);
            $group->post('/settings/jobs/trigger/{name}', [$settings, 'trigger']);
            $group->post('/settings/maintenance/purge', [$settings, 'purge']);
            $group->post('/settings/maintenance/seed-demo', [$settings, 'seedDemo']);
            $group->post('/settings/audit-toggles', [$settings, 'updateAuditToggles']);
        })->add($authRequired);

        $app->map(
            ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
            '/{routes:.+}',
            function ($request, $response) {
                return $response->withStatus(404)->withHeader('Content-Type', 'text/plain');
            }
        );

        return $app;
    }
}
