<?php

use App\Http\Middleware\EnsureAdmin;
use App\Http\Middleware\EnsureCustomerActive;
use App\Http\Middleware\PublicSiteGate;
use App\Http\Middleware\RequireOperatorTwoFactor;
use App\Http\Middleware\RestrictAdminHost;
use App\Http\Middleware\RestrictConsoleNetwork;
use App\Http\Middleware\TouchLoginSession;
use App\Support\AdminArea;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        channels: __DIR__.'/../routes/channels.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        // The one endpoint that must answer WHILE the application is down.
        //
        // It is the deployment's own status, read by the overlay every three
        // seconds. Behind maintenance mode it answered 503 for the whole run,
        // so the overlay had nothing to show but "still waiting" and could not
        // name the step it was on — and any Livewire request that fired in the
        // same window swapped the console for Laravel's 503 page.
        //
        // Safe to exempt: it reports version, commit and a step name, it is
        // behind the console's host and network guards, and it writes nothing.
        $middleware->preventRequestsDuringMaintenance(except: [
            'update/state',
            '*/update/state',
        ]);

        $middleware->alias([
            'admin' => EnsureAdmin::class,
            'admin.host' => RestrictAdminHost::class,
            'customer.active' => EnsureCustomerActive::class,
            'operator.2fa' => RequireOperatorTwoFactor::class,
        ]);

        // Runs before the whole route stack (incl. `auth`), so /admin on a public
        // hostname 404s instead of redirecting to /login and thereby revealing
        // that an operator console is hosted here. Self-scoped to /admin*.
        $middleware->prependToGroup('web', RestrictAdminHost::class);

        // And WHO may reach it, by network address — the VPN plus whatever the
        // owner has listed in the console. The host guard above cannot answer
        // that question: a Host header is chosen by the caller.
        $middleware->prependToGroup('web', RestrictConsoleNetwork::class);

        // Runs after the host guard: the console must stay reachable even while
        // the public site is switched off.
        $middleware->appendToGroup('web', PublicSiteGate::class);

        // Keeps each login_sessions row pointing at the session it describes.
        // Laravel rotates the session id on sign-in and on every regenerate(),
        // so a row written once stops matching almost immediately — and the
        // list would hide the session its reader is sitting in.
        $middleware->appendToGroup('web', TouchLoginSession::class);

        // TLS is terminated by the reverse proxy (Zoraxy on the private LAN), so
        // without this Laravel sees plain http and builds http:// URLs into an
        // https page — every asset and redirect breaks as mixed content.
        //
        // X_FORWARDED_HOST is deliberately NOT trusted: the console is gated on
        // the request host (ADMIN_HOSTS), and trusting that header would let
        // anyone reach /admin through a public domain by sending
        // "X-Forwarded-Host: admin.…". Zoraxy passes the real Host through, so
        // nothing needs it.
        $middleware->trustProxies(
            at: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '127.0.0.1'],
            headers: Request::HEADER_X_FORWARDED_FOR
                | Request::HEADER_X_FORWARDED_PORT
                | Request::HEADER_X_FORWARDED_PROTO,
        );

        // Stripe posts server-to-server with its own signature (no CSRF token).
        $middleware->validateCsrfTokens(except: ['webhooks/stripe']);

        // ApplicationBuilder::withMiddleware() defaults this to
        // fn () => route('login') BEFORE this callback ever runs — always the
        // PORTAL's Fortify page, regardless of which side of the site the
        // guest was actually turned away from. In non-exclusive (shared)
        // mode that sends a guest bounced off /admin to a sign-in form whose
        // table holds no operators; exclusive mode happened to self-correct
        // only because RestrictAdminHost 404s the wrong host before this
        // ever ran. Fixed for both modes at once, rather than relying on
        // which one a given deployment happens to run: ask AdminArea, which
        // both routes/web.php's own registration and RestrictAdminHost
        // already treat as the one source of truth for "is this the
        // console".
        $middleware->redirectGuestsTo(
            fn (Request $request) => AdminArea::isConsole($request) ? route('admin.login') : route('login'),
        );

        // The mirror image of redirectGuestsTo() above, same reasoning: left
        // at its default, `guest:operator` sends an already signed-in
        // operator revisiting admin.login through Laravel's own fallback —
        // the named 'dashboard' route, the PORTAL, where the web guard is
        // usually unauthenticated, bouncing them to ITS login instead. In
        // exclusive mode it is worse: dashboard is not a console route at
        // all, so RestrictAdminHost 404s it on the console host. Keyed on
        // the same AdminArea::isConsole($request) condition as the guest
        // redirect, so the two read as a pair and neither can drift from it.
        $middleware->redirectUsersTo(
            fn (Request $request) => AdminArea::isConsole($request) ? route('admin.overview') : route('dashboard'),
        );
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        $exceptions->shouldRenderJsonWhen(
            fn (Request $request) => $request->is('api/*'),
        );
    })->create();
