<?php

use App\Http\Middleware\BlockBannedIp;
use App\Http\Middleware\DetectHoneytoken;
use App\Http\Middleware\PanelScheme;
use App\Http\Middleware\SecurityHeaders;
use App\Http\Middleware\SetLocale;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
use Illuminate\Session\Middleware\AuthenticateSession;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    // Registers the `/broadcasting/auth` endpoint (under the `web` group, so PanelScheme
    // guards it) and the private-channel authorization callbacks in routes/channels.php.
    // Without this, private/presence channels can never be subscribed to. See R-note in
    // routes/channels.php: every broadcast channel is PRIVATE (authenticated) — metrics are
    // fleet infrastructure data and must not be reachable on a stale Caddy host via Reverb.
    ->withBroadcasting(__DIR__.'/../routes/channels.php')
    ->withMiddleware(function (Middleware $middleware): void {
        // Trust Caddy's X-Forwarded-* ONLY in production, where the app has no host port
        // (docker-compose.prod.yml exposes app:80 on the internal net only) so Caddy is
        // the sole peer. In dev the app port is published on the host, so trusting forged
        // proxy headers would let a LAN peer fake HTTPS (forcing a Secure cookie over
        // plain HTTP and breaking the bare-IP recovery login).
        //
        // Trust only PRIVATE ranges, not '*': Caddy reaches app:80 from the docker bridge (a private
        // IP), so it stays trusted, but anything arriving from a public source (a stray published
        // port, an off-network relay) can no longer forge X-Forwarded-For to spoof request()->ip()
        // and defeat the IP-keyed login/2FA/forgot throttles + the brute-force ban.
        if (($_SERVER['APP_ENV'] ?? getenv('APP_ENV') ?: 'local') === 'production') {
            $middleware->trustProxies(at: ['127.0.0.1/32', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']);
        }

        // PanelScheme first (scheme/host enforcement before auth), so an HTTP request to
        // the active domain is sent straight to HTTPS and a wrong host is refused before
        // any auth redirect. AuthenticateSession ties each request to the password hash that
        // was current when the session started, so a password change (or a remember-token
        // rotation in the session-management actions) invalidates every OTHER session — the
        // database session driver makes those sessions enumerable/revocable in Settings.
        $middleware->web(
            prepend: [PanelScheme::class],
            append: [SetLocale::class, SecurityHeaders::class, AuthenticateSession::class, BlockBannedIp::class, DetectHoneytoken::class],
        );

        // The terminal sidecar POSTs to the internal resolve endpoint from the private network with
        // no browser session/CSRF token — it is guarded by the shared-secret header instead. The
        // honeypot decoys are also exempt: a probe POSTing the fake login must be trapped + logged,
        // not bounced with a 419 that unmasks the framework (these paths reach only HoneypotController).
        $middleware->validateCsrfTokens(except: [
            '_internal/terminal/*',
            '.env', '.git/config',
            'wp-login.php', 'wp-admin', 'wp-admin/*', 'xmlrpc.php',
            'phpmyadmin', 'phpmyadmin/*', 'administrator', 'admin.php', 'adminer.php',
            'config.json', 'vendor/*', 'cgi-bin/*', 'solr/*', 'actuator/*',
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        $exceptions->shouldRenderJsonWhen(
            fn (Request $request) => $request->is('api/*'),
        );

        // Errors thrown BEFORE the web-group SetLocale middleware (CSRF 419, an
        // unmatched-route 404, maintenance 503) would render the custom error page in
        // the default locale. Resolve the user's language first so those pages honour
        // it too (R16); returning null falls through to the normal renderer (the
        // resources/views/errors/* views).
        $exceptions->render(function (Throwable $e, Request $request) {
            // Best-effort: locale resolution touches the session/user, which can be the
            // very thing that failed (e.g. a DB outage). Swallow any error so this hook
            // can never throw a second exception and break the custom error page — the
            // page just renders in the default locale instead.
            try {
                SetLocale::apply($request);
            } catch (Throwable) {
                // keep the default locale
            }

            return null;
        });
    })->create();
