<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response;

// ---------------------------------------------------------------------------
// Self-heal a stale config cache after an update. This runs when index.php /
// artisan `require` this file — BEFORE the framework registers any service
// provider, so it can rescue a boot that would otherwise crash *during*
// provider registration (upstream of the boot-time `optimize:clear`, which can
// therefore never fire). A `bootstrap/cache/config.php` baked under a PRIOR
// version won't carry config sections a newer provider expects — e.g. 1.0.03
// added `laravel/reverb`, whose provider reads `config('reverb…')` at register
// time; against a pre-1.0.03 cache that key is missing → "Undefined array key"
// fatal → every web + artisan request 500s with no client recovery path. If the
// cached config predates the current VERSION file, drop the compiled caches so
// config rebuilds from source. Pure file ops — no DB, container, or providers.
// Self-resetting: a freshly re-cached config is newer than VERSION, so this
// no-ops until the next version bump.
$__cacheDir = __DIR__.'/cache';
$__configCache = $__cacheDir.'/config.php';
$__versionFile = dirname(__DIR__).'/VERSION';
if (is_file($__configCache) && is_file($__versionFile)
    && @filemtime($__configCache) < @filemtime($__versionFile)) {
    @unlink($__configCache);
    foreach (glob($__cacheDir.'/routes*.php') ?: [] as $__routeCache) {
        @unlink($__routeCache);
    }
    @unlink($__cacheDir.'/events.php');
}
unset($__cacheDir, $__configCache, $__versionFile, $__routeCache);

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 {
        // Trust X-Forwarded-* headers from any upstream. Required for
        // local dev behind ngrok / cloudflared / valet-share tunnels +
        // for production behind cloudflare / nginx / load balancers.
        // Without this, Laravel sees the connection as HTTP from the
        // upstream's IP, even though the client is on HTTPS through
        // the tunnel — break secure cookies, CSRF, generated URLs,
        // session cookie domain. Trust * because:
        //  - dev: cloudflared tunnel client is the only thing hitting
        //    127.0.0.1:8002, so trusting all proxies = trusting it.
        //  - prod: behind a single CDN, same reasoning applies (deploy
        //    operator should narrow to specific subnets if running
        //    bare-metal multi-hop).
        $middleware->trustProxies(at: '*');

        // The cookie-consent banner sets `cookie_consent` from JS, so it
        // arrives unencrypted; except it from EncryptCookies or Laravel drops
        // it as un-decryptable and the analytics gate (app.blade.php) never
        // sees the visitor's acceptance. Non-sensitive (accepted/declined).
        $middleware->encryptCookies(except: ['cookie_consent']);

        // RequireInstall must run before any auth / Inertia middleware so
        // a fresh-install buyer hitting `/login` or `/register` is
        // redirected to the wizard instead of hitting routes that
        // expect a populated DB.
        // EnforceLicense runs IMMEDIATELY AFTER RequireInstall: a fresh
        // install must reach the wizard first, then the license gate kicks
        // in. It self-allowlists the license.* routes (and is bypassed on
        // dev hosts + when LICENSE_ENFORCE=false), so it can't redirect-loop.
        $middleware->web(prepend: [
            \App\Http\Middleware\RequireInstall::class,
            \App\Http\Middleware\EnforceLicense::class,
        ]);

        $middleware->web(append: [
            \App\Http\Middleware\HandleInertiaRequests::class,
            // PreviewTheme MUST sit after HandleInertiaRequests so the
            // session driver is bound by the time we read `appearance.draft`,
            // and BEFORE the response is rendered so Blade's <style> block
            // sees the override. AddLinkHeaders + SecurityHeaders run later
            // and don't touch theme state.
            \App\Http\Middleware\PreviewTheme::class,
            \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
            \App\Http\Middleware\SecurityHeaders::class,
            // Live-domain license re-verify — terminable, runs AFTER the
            // response (zero latency), throttled to once/day inside reverify().
            // Reports the real request host so the verify server enforces
            // domain-locking. See VerifyDomainLicense.
            \App\Http\Middleware\VerifyDomainLicense::class,
        ]);

        $middleware->alias([
            'saas-only' => \App\Http\Middleware\RequireSaasMode::class,
            'admin' => \App\Http\Middleware\RequireAdmin::class,
            'landing.mode' => \App\Http\Middleware\EnforceMarketingMode::class,
            // Role write-gate (Team Workspaces P3c, Δ22/Δ24) — viewers 403 on
            // mutating routes. BOTH modes (not saas-only). Applied to project/
            // screen/media/edit-lock mutating routes in routes/web.php.
            'can-edit' => \App\Http\Middleware\EnsureCanEdit::class,
        ]);

        // Provider webhooks don't carry a session cookie or CSRF token —
        // exclude the URI here rather than attaching `withoutMiddleware()`
        // per-route so the rule is centralized + visible alongside the
        // other middleware stack tweaks.
        $middleware->validateCsrfTokens(except: [
            'webhooks/billing',
        ]);
    })
    ->booting(function (Application $app): void {
        // First policy in the app (Team Workspaces P3, Δ22/Δ24). Auto-discovery
        // would look for `TenantPolicy`; we register `WorkspacePolicy` against
        // the Tenant model explicitly so its name tracks the feature. Used via
        // `$user->can('invite'|'transfer'|…, $tenant)` from WorkspaceController.
        // Registered in the booting callback (not the middleware closure) so
        // the Gate facade root is bound by the time we touch it.
        \Illuminate\Support\Facades\Gate::policy(
            \App\Models\Tenant::class,
            \App\Policies\WorkspacePolicy::class,
        );
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        // Render styled Inertia error pages for HTTP errors so visitors get
        // the same chrome (Nav, rails, accent typography) instead of the
        // default Laravel/Symfony stack-trace or whitelabel page.
        //
        // Skip:
        //   - Inertia XHR (`X-Inertia` header) — Inertia handles its own
        //     error responses; replacing them breaks the SPA flow.
        //   - JSON / API requests — those callers expect JSON, not HTML.
        //   - Local + testing envs for 500 — keep Ignition's stack trace
        //     during dev. 404/403/503 still render the styled page locally
        //     so we can preview them while iterating.
        $exceptions->respond(function (Response $response, \Throwable $exception, Request $request) {
            // JSON / API callers want JSON, not an Inertia HTML response.
            if ($request->expectsJson()) {
                return $response;
            }

            $status = $response->getStatusCode();
            $isInertia = $request->hasHeader('X-Inertia');

            // For Inertia XHRs the framework's default 404/403 HTML response
            // gets shown by the client in a modal overlay (the "stranded
            // error iframe"). Rendering the same Errors/{code} component via
            // Inertia returns an Inertia-formatted response that the client
            // navigates to properly — URL updates, page swaps, smart CTAs
            // work. Only bail on Inertia for status codes we don't custom-
            // render (anything outside the allowlist below).
            $custom = in_array($status, [400, 403, 404, 405, 419, 429, 503], true)
                || ($status === 500 && ! app()->environment(['local', 'testing']));
            if ($isInertia && ! $custom) {
                return $response;
            }

            // Shared Inertia props (auth, branding, flash, etc.) are
            // normally injected by HandleInertiaRequests::share(), which
            // runs as part of the regular middleware pipeline. An exception
            // bypasses that pipeline, so without explicit re-registration
            // Inertia::render() ships `props: []` and `usePage()` on the
            // client throws when accessing `page.props.auth.user` — the
            // symptom is a blank screen with the right HTTP status.
            //
            // Two failure modes to defend against:
            //   1. 404s (route never matched) skip the web middleware
            //      group entirely, so StartSession never bound the
            //      session to the request. We invoke StartSession by
            //      hand so $request->user() works for logged-in users
            //      hitting a missing route.
            //   2. Even with the session bound, anything inside share()
            //      can throw (DB down, settings table missing during
            //      install, etc.). A try/catch falls back to a bare
            //      minimum auth shape so the page still renders — the
            //      smart CTAs degrade to the logged-out path but the
            //      user gets a useful page instead of a blank screen.
            $renderError = function (int $code) use ($request) {
                if (! $request->hasSession()) {
                    // Order matters: EncryptCookies must run BEFORE
                    // StartSession. Laravel encrypts the session cookie
                    // by default; without decryption, StartSession sees
                    // gibberish, can't find a matching session ID, and
                    // creates a fresh empty session every request — so
                    // $request->user() returns null even for logged-in
                    // users hitting a 404. This pair is what the web
                    // middleware group's session priority handles
                    // automatically; we replicate the same pair here.
                    try {
                        app(\Illuminate\Cookie\Middleware\EncryptCookies::class)
                            ->handle($request, fn () => null);
                    } catch (\Throwable) {
                        // Continue — StartSession may still succeed if
                        // cookies happen to be unencrypted in this env.
                    }
                    try {
                        app(\Illuminate\Session\Middleware\StartSession::class)
                            ->handle($request, fn () => null);
                    } catch (\Throwable) {
                        // Session bootstrapping failed (DB down,
                        // misconfiguration). Fall through; share() will
                        // hit the catch below and ship anonymous shape.
                    }
                }

                try {
                    $middleware = app(\App\Http\Middleware\HandleInertiaRequests::class);
                    Inertia::share($middleware->share($request));
                } catch (\Throwable) {
                    Inertia::share(['auth' => ['user' => null]]);
                }

                return Inertia::render("Errors/{$code}")
                    ->toResponse($request)
                    ->setStatusCode($code);
            };

            if (in_array($status, [400, 403, 404, 405, 419, 429, 503], true)) {
                return $renderError($status);
            }

            if ($status === 500 && ! app()->environment(['local', 'testing'])) {
                return $renderError(500);
            }

            return $response;
        });
    })->create();
