<?php

namespace App\Http\Middleware;

use App\Services\MediaStorage;
use App\Services\Settings;
use App\Support\AppMode;
use App\Support\MarketingDefaults;
use App\Support\MarketingMigrator;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    protected $rootView = 'app';

    public function version(Request $request): ?string
    {
        return parent::version($request);
    }

    public function share(Request $request): array
    {
        $user = $request->user();

        return [
            ...parent::share($request),
            'appMode' => AppMode::current()->value,
            'appName' => config('app.name'),
            // Branding is shared so AppNav (and any future surface) renders
            // the configured logo without each page having to fetch it. The
            // Settings::get cache fronts the DB read, so this isn't a hot
            // query even with sparse render churn. Wrapped in try/catch so
            // requests during early bootstrap (no migrations yet — install
            // wizard, ExampleTest without RefreshDatabase) still resolve.
            'branding' => function () {
                $resolveAsset = static function (?string $path): ?string {
                    return is_string($path) && $path !== ''
                        ? MediaStorage::url($path)
                        : null;
                };

                try {
                    // Theme assets are the single source of truth for the
                    // brand logo + favicon. The legacy Settings::site.logo_path
                    // upload was retired; existing rows were migrated into
                    // the active theme's assets.logo_light by the
                    // 2026_05_04 migration.
                    $rowAssets = [];
                    try {
                        $row = \App\Models\ThemeVersion::query()
                            ->active()
                            ->first(['assets']);
                        $rowAssets = is_array($row?->assets) ? $row->assets : [];
                    } catch (\Throwable) {
                        $rowAssets = [];
                    }

                    // Default AppShot AI wordmarks ship as static public assets
                    // (public/user-uploads is empty in the release, so a DB seed
                    // wouldn't survive install). An admin logo upload lands in
                    // the theme's assets.logo_* and overrides these; a buyer who
                    // sets only logo_light gets it in both light + dark modes.
                    $themeLogo = $resolveAsset($rowAssets['logo_light'] ?? null);
                    $themeLogoDark = $resolveAsset($rowAssets['logo_dark'] ?? null);
                    $logoUrl = $themeLogo ?? asset('brand/appshot-ai-wordmark-light.png');
                    $logoDarkUrl = $themeLogoDark ?? $themeLogo ?? asset('brand/appshot-ai-wordmark-dark.png');
                    $faviconUrl = $resolveAsset($rowAssets['favicon'] ?? null);

                    $name = (string) Settings::get('site.name', config('app.name'));
                    // Single Settings::get per call — Settings caches in
                    // memory so 6 lookups are still one DB hit per request
                    // worth of work. Empty string normalized to null so the
                    // client-side "URL present?" check is consistent.
                    $supportEmail = ((string) (Settings::get('site.support_email') ?? '')) ?: null;
                    $socials = [
                        'twitter' => ((string) (Settings::get('social.twitter') ?? '')) ?: null,
                        'instagram' => ((string) (Settings::get('social.instagram') ?? '')) ?: null,
                        'facebook' => ((string) (Settings::get('social.facebook') ?? '')) ?: null,
                        'youtube' => ((string) (Settings::get('social.youtube') ?? '')) ?: null,
                        'linkedin' => ((string) (Settings::get('social.linkedin') ?? '')) ?: null,
                        'github' => ((string) (Settings::get('social.github') ?? '')) ?: null,
                    ];
                } catch (\Throwable) {
                    $logoUrl = null;
                    $logoDarkUrl = null;
                    $faviconUrl = null;
                    $name = (string) config('app.name');
                    $supportEmail = null;
                    $socials = [
                        'twitter' => null, 'instagram' => null, 'facebook' => null,
                        'youtube' => null, 'linkedin' => null, 'github' => null,
                    ];
                }

                return [
                    'site_name' => $name,
                    'logo_url' => $logoUrl,
                    'logo_dark_url' => $logoDarkUrl,
                    'favicon_url' => $faviconUrl,
                    'support_email' => $supportEmail,
                    'socials' => $socials,
                ];
            },
            // Active theme knobs (not derived) ride alongside branding so the
            // admin appearance editor and any client-side live preview can
            // recompute the token map on the fly. Derived tokens stay server-
            // side — Blade injects them into a `<style>` block at the top of
            // <head> so first paint already has correct colors. Try/catch
            // mirrors the branding closure for early-bootstrap safety.
            'theme' => function () {
                try {
                    $active = Settings::activeTheme();

                    return [
                        'knobs' => $active['knobs'],
                        'name' => $active['name'],
                    ];
                } catch (\Throwable) {
                    return null;
                }
            },
            // Registered billing-provider labels, derived from the
            // BillingManager registry. UI copy that lists supported
            // gateways ("Configure Stripe and Razorpay…") reads from
            // here so adding a new provider doesn't require sweeping
            // every blurb. `labels_or` is the same list joined with
            // "or" for disjunctive copy ("Add Stripe or Razorpay").
            'billingProviders' => function () {
                try {
                    $manager = app(\App\Services\Billing\BillingManager::class);

                    return [
                        'labels' => $manager->registeredLabels(),
                        'labels_and' => $manager->humanProviderList('and'),
                        'labels_or' => $manager->humanProviderList('or'),
                    ];
                } catch (\Throwable) {
                    return [
                        'labels' => [],
                        'labels_and' => '',
                        'labels_or' => '',
                    ];
                }
            },
            // Marketing CMS — header nav + footer columns + per-page section
            // configs (welcome, pricing, templates) rendered on every public
            // page. Settings::get returns the deserialized array (JSON cast on
            // the model); admins customize via /admin/marketing, defaults in
            // MarketingDefaults mirror the TS side byte-for-byte.
            //
            // Pricing + Templates are page-scoped CMS configs (mandatory
            // pricing_grid / kit_grid sections + admin-editable sections
            // around them). The page dispatchers (Pages/Pricing.tsx,
            // Pages/Templates.tsx) read directly from this prop — Server
            // delegates to MarketingController::*Payload() which runs the
            // migrator + enforces the mandatory section + resolves URLs in
            // one shot.
            //
            // Scoped to the surfaces that actually read it (public marketing
            // pages + the admin marketing editor) rather than shared globally
            // — the full CMS tree is multi-KB and the editor / app / admin
            // pages never render it. `null` (not a closure) elsewhere so the
            // MarketingController payload methods don't even run. See
            // `wantsMarketingProp()`.
            'marketing' => $this->wantsMarketingProp($request) ? function () {
                try {
                    $nav = Settings::get('marketing.nav');
                    $footer = Settings::get('marketing.footer');
                    $welcome = Settings::get('marketing.welcome');
                    $welcome = is_array($welcome)
                        ? MarketingMigrator::migrateWelcome($welcome)
                        : MarketingDefaults::welcome();

                    $marketingController = app(\App\Http\Controllers\Admin\MarketingController::class);

                    return [
                        'nav' => is_array($nav)
                            ? MarketingMigrator::migrateNav($nav)
                            : MarketingDefaults::header(),
                        'footer' => is_array($footer)
                            ? MarketingMigrator::migrateFooter($footer)
                            : MarketingDefaults::footer(),
                        // resolveWelcomeUrls walks the sections and adds
                        // server-derived `imageUrl` to hero sections that
                        // have an `image` path. Same shape used by the
                        // admin index() so admin preview and public render
                        // are byte-equivalent.
                        'welcome' => \App\Http\Controllers\Admin\MarketingController::resolveWelcomeUrls($welcome),
                        'pricing' => $marketingController->pricingPayload(),
                        'templates' => $marketingController->templatesPayload(),
                        'about' => $marketingController->aboutPayload(),
                        'contact' => $marketingController->contactPayload(),
                        'privacy' => $marketingController->privacyPayload(),
                        'terms' => $marketingController->termsPayload(),
                        'refund' => $marketingController->refundPayload(),
                        'customPages' => $marketingController->customPagesPayload(),
                        'seo' => $marketingController->seoPayload(),
                        'cookieConsent' => $marketingController->cookieConsentPayload(),
                    ];
                } catch (\Throwable) {
                    return [
                        'nav' => MarketingDefaults::header(),
                        'footer' => MarketingDefaults::footer(),
                        'welcome' => MarketingDefaults::welcome(),
                        'pricing' => MarketingDefaults::pricing(),
                        'templates' => MarketingDefaults::templates(),
                        'about' => MarketingDefaults::about(),
                        'contact' => MarketingDefaults::contact(),
                        'privacy' => MarketingDefaults::privacy(),
                        'terms' => MarketingDefaults::terms(),
                        'refund' => MarketingDefaults::refund(),
                        'customPages' => MarketingDefaults::customPages(),
                        'seo' => MarketingDefaults::seo(),
                        'cookieConsent' => MarketingDefaults::cookieConsent(),
                    ];
                }
            } : null,
            'landingMode' => function () {
                try {
                    return [
                        'mode' => \App\Support\MarketingMode::current()->value,
                        // Shipped so client-side chrome (BackToHomeLink etc.)
                        // can link to the buyer's external marketing site
                        // without an extra round-trip. Already-public info —
                        // anon visitors hitting `/` get 302'd to this URL in
                        // redirect mode, so it's not a leak.
                        'redirectUrl' => \App\Support\MarketingMode::redirectUrl(),
                    ];
                } catch (\Throwable) {
                    return ['mode' => 'enabled', 'redirectUrl' => null];
                }
            },
            'auth' => [
                'user' => $user ? [
                    'id' => $user->id,
                    'name' => $user->name,
                    'email' => $user->email,
                    // Δ3 — in SaaS the global role is wrong by construction
                    // (a user can be owner of their personal workspace and
                    // viewer of a team). Re-source from the ACTIVE pivot
                    // membership so nav/CTA gating matches the active
                    // workspace. Self-hosted has no pivot — `users.role` is
                    // the role-of-record there, so read it directly.
                    'role' => $this->activeRoleFor($user),
                    'tenant_id' => $user->tenant_id,
                    'is_admin' => (bool) $user->is_admin,
                    // Plan-derived entitlement flags so client UI can
                    // pre-gate CTAs (e.g. hide / lock premium-kit cards
                    // for Free users) without a round-trip 402. Server is
                    // still authoritative — `ProjectController::store`
                    // re-checks via `canUsePremiumKits()`.
                    'can_use_premium_kits' => $user->canUsePremiumKits(),
                ] : null,
            ],
            // Workspace switcher data (Team Workspaces P3). SaaS app-surfaces
            // only — null in self-hosted (single shared pool, no switcher) and
            // omitted on surfaces that never render the switcher (marketing /
            // auth / admin / error), via `wantsWorkspacesProp`. Two indexed
            // queries: the membership list + the pending-invite count.
            //
            // PROP CONTRACT (the WorkspaceSwitcher component depends on this):
            //   { active: {id,name,role,personal,seat_over_limit,
            //              seat:{used,limit}},
            //     memberships: [{id,name,role,personal}],
            //     pending_invites_count }
            //
            // `active.seat_over_limit` / `active.seat` (P5) drive the
            // owner-downgrade banner on /projects + /workspace/members: when
            // the workspace's plan can no longer cover its edit-capable
            // members (an owner downgraded below the team's editor count),
            // `seat_over_limit` is true and the owner-only banner surfaces.
            // Never auto-kick — banner only.
            'workspaces' => ($user !== null && AppMode::isSaas() && $this->wantsWorkspacesProp($request))
                ? function () use ($user) {
                    try {
                        $memberships = $user->workspaces()
                            ->orderBy('tenants.name')
                            ->get()
                            // Personal workspace first, then alphabetical.
                            ->sortBy(fn ($t) => ($t->personal ? '0' : '1').mb_strtolower((string) $t->name))
                            ->map(fn ($t) => [
                                'id' => $t->id,
                                'name' => $t->name,
                                'role' => $t->getRelationValue('pivot')?->role,
                                'personal' => (bool) $t->personal,
                            ])
                            ->values()
                            ->all();

                        $activeId = $user->tenant_id;
                        $active = collect($memberships)->firstWhere('id', $activeId);

                        // Seat-over-limit signal for the active workspace
                        // (P5 — owner-downgrade banner). Resolve the seat
                        // window off the active Tenant model; null limit
                        // (Scale / no cap) is never over-limit.
                        if ($active !== null) {
                            $activeTenant = $user->tenant;
                            if ($activeTenant !== null) {
                                $limit = $activeTenant->seatLimit();
                                $used = $activeTenant->seatUsage();
                                $active['seat'] = ['used' => $used, 'limit' => $limit];
                                $active['seat_over_limit'] = $limit !== null && $used > $limit;
                            } else {
                                $active['seat'] = ['used' => 0, 'limit' => null];
                                $active['seat_over_limit'] = false;
                            }

                            // Upgrade affordance: true when a higher-tier active
                            // plan exists above the workspace's current plan.
                            // Drives the AppNav "Upgrade" button + gates the
                            // in-dashboard upgrade modal from showing to top-tier
                            // workspaces. Best-effort; false on any hiccup.
                            try {
                                $currentPos = $activeTenant?->activePlan()?->position ?? -1;
                                $active['can_upgrade'] = \App\Models\Plan::query()
                                    ->where('is_active', true)
                                    ->where('position', '>', $currentPos)
                                    ->exists();
                            } catch (\Throwable) {
                                $active['can_upgrade'] = false;
                            }
                        }

                        // Pending invites the user could accept (addressed to
                        // their email, un-accepted, un-expired) — drives the
                        // switcher badge.
                        $pending = (int) \App\Models\WorkspaceInvitation::query()
                            ->where('email', mb_strtolower($user->email))
                            ->whereNull('accepted_at')
                            ->where(function ($q) {
                                $q->whereNull('expires_at')->orWhere('expires_at', '>', now());
                            })
                            ->count();

                        return [
                            'active' => $active,
                            'memberships' => $memberships,
                            'pending_invites_count' => $pending,
                        ];
                    } catch (\Throwable) {
                        return null;
                    }
                }
                : null,
            // In-app notification feed (Team Workspaces P3d / Δ25). Backs the
            // AppNav bell. SaaS app-surfaces only (same gating as `workspaces`)
            // — null in self-hosted (no project-share flow) and omitted on
            // marketing / auth / admin / error surfaces that never mount the
            // bell. The latest ~10 + the full unread count.
            //
            // PROP CONTRACT (NotificationBell depends on this):
            //   { unread_count:number,
            //     items:[{id,title,body,url,read_at,created_at}] }
            'notifications' => ($user !== null && AppMode::isSaas() && $this->wantsWorkspacesProp($request))
                ? function () use ($user) {
                    try {
                        $items = $user->notifications()
                            ->latest()
                            ->limit(10)
                            ->get()
                            ->map(fn ($n) => [
                                'id' => (string) $n->id,
                                'title' => $n->data['title'] ?? '',
                                'body' => $n->data['body'] ?? '',
                                'url' => $n->data['url'] ?? null,
                                'workspace' => $n->data['workspace'] ?? null,
                                'category' => $n->data['category'] ?? null,
                                'read_at' => $n->read_at?->toIso8601String(),
                                'created_at' => $n->created_at?->toIso8601String(),
                            ])
                            ->values()
                            ->all();

                        return [
                            'unread_count' => (int) $user->unreadNotifications()->count(),
                            'items' => $items,
                        ];
                    } catch (\Throwable) {
                        return ['unread_count' => 0, 'items' => []];
                    }
                }
                : null,
            // Collab transport selector (Team Workspaces P6 / Tier 2 — Reverb
            // opt-in, Δ2/Δ21). DEFAULT 'lock' — the pessimistic project lock
            // (Tier 1) is the shared-hosting-safe default in both modes; an
            // SSH/VPS operator flips this to 'reverb' via the admin Realtime
            // Settings card. `reverb` config (key/host/port/scheme) ships ONLY
            // when the Reverb tier is active so the Echo client can connect;
            // null otherwise (so resources/js/echo.ts never initializes a
            // websocket on the default path — honors CLAUDE.md §Don'ts).
            //
            // Scoped to app surfaces that mount the editor (the only reader)
            // via `wantsWorkspacesProp` — same EnsureTenant gating — so the
            // closure (one cheap Settings read) is skipped on marketing /
            // auth / admin / error pages.
            //
            // PROP CONTRACT (resources/js/echo.ts + Editor.tsx depend on this):
            //   { transport:'lock'|'reverb', editing:'locked'|'concurrent',
            //     reverb: {key,host,port,scheme}|null }
            // `editing` only bites under transport='reverb': 'locked' keeps the
            // single-editor lock (presence + sync on top); 'concurrent' is the
            // opt-in free-for-all. Default 'locked'.
            'collab' => ($user !== null && $this->wantsWorkspacesProp($request))
                ? function () {
                    try {
                        $transport = Settings::get('collab.transport', 'lock') === 'reverb'
                            ? 'reverb'
                            : 'lock';
                        $editing = Settings::get('collab.reverb_editing', 'locked') === 'concurrent'
                            ? 'concurrent'
                            : 'locked';

                        $reverb = null;
                        if ($transport === 'reverb') {
                            $host = (string) (Settings::get('collab.reverb_host') ?? '');
                            $key = (string) (Settings::get('collab.reverb_key') ?? '');
                            // Only ship a usable config when the minimum
                            // connect fields are present; otherwise the client
                            // falls back to the LockAdapter (never unprotected).
                            if ($host !== '' && $key !== '') {
                                $scheme = Settings::get('collab.reverb_scheme') === 'http' ? 'http' : 'https';
                                $portSetting = Settings::get('collab.reverb_port');
                                $port = ($portSetting !== null && $portSetting !== '')
                                    ? (int) $portSetting
                                    : ($scheme === 'https' ? 443 : 80);
                                $reverb = [
                                    'key' => $key,
                                    'host' => $host,
                                    'port' => $port,
                                    'scheme' => $scheme,
                                ];
                            }
                        }

                        return ['transport' => $transport, 'editing' => $editing, 'reverb' => $reverb];
                    } catch (\Throwable) {
                        return ['transport' => 'lock', 'editing' => 'locked', 'reverb' => null];
                    }
                }
                : null,
            'canAuthorKits' => fn () => $user !== null && Gate::forUser($user)->allows('author-kits'),
            // Impersonation banner needs both ends of the swap to render the
            // "Impersonating X — back to Y" affordance. `impersonator_id`
            // holds the original admin's id; the currently-authenticated
            // user is the impersonation target.
            'impersonation' => function () use ($request, $user) {
                $impersonatorId = $request->session()->get('impersonator_id');
                if (! $impersonatorId || ! $user) {
                    return [
                        'active' => false,
                        'target_email' => null,
                        'admin_email' => null,
                    ];
                }
                $admin = \App\Models\User::query()->find($impersonatorId);

                return [
                    'active' => true,
                    'target_email' => $user->email,
                    'admin_email' => $admin?->email,
                ];
            },
            'flash' => [
                'success' => fn () => $request->session()->get('success'),
                'error' => fn () => $request->session()->get('error'),
            ],
            'errors' => function () use ($request) {
                $bag = $request->session()->get('errors');

                return $bag ? $bag->getBag('default')->getMessages() : (object) [];
            },
        ];
    }

    /**
     * Whether the current request renders a surface that reads the `marketing`
     * shared prop. The public marketing pages all carry the `landing.mode`
     * middleware (Welcome / Pricing / Templates / About / Contact / Privacy /
     * Terms / Refund / CustomPage); the admin marketing editor reads
     * `marketing.customPages` for its href autocomplete. Everything else (the
     * editor, app, admin, auth, error pages) never touches it, so the multi-KB
     * CMS tree is omitted there.
     */
    private function wantsMarketingProp(Request $request): bool
    {
        $route = $request->route();
        if ($route === null) {
            // No matched route (e.g. error pages rendered from the exception
            // handler). ErrorPage doesn't read `marketing`, so omit it.
            return false;
        }

        foreach ($route->gatherMiddleware() as $middleware) {
            if (is_string($middleware) && str_starts_with($middleware, 'landing.mode')) {
                return true;
            }
        }

        return $request->routeIs('admin.marketing.*');
    }

    /**
     * The user's role in their ACTIVE workspace (Δ3). SaaS: the pivot role of
     * the tenant the `users.tenant_id` pointer currently targets, falling back
     * to the stale column if no pivot row resolves (pre-backfill safety).
     * Self-hosted: `users.role` directly (the role-of-record — no pivot).
     */
    private function activeRoleFor(\App\Models\User $user): ?string
    {
        if (AppMode::isSelfHosted()) {
            return $user->role;
        }

        $tenant = $user->tenant;
        if ($tenant === null) {
            return $user->role;
        }

        try {
            return $user->roleIn($tenant) ?? $user->role;
        } catch (\Throwable) {
            return $user->role;
        }
    }

    /**
     * Whether the current request renders a surface that mounts the workspace
     * switcher (the SaaS app shell — AppNav). Marketing / auth / admin / error
     * surfaces never render it, so the membership queries are skipped there.
     * Mirrors `wantsMarketingProp`'s route-based gating.
     */
    private function wantsWorkspacesProp(Request $request): bool
    {
        $route = $request->route();
        if ($route === null) {
            return false;
        }

        // The app shell lives behind the `EnsureTenant` middleware group
        // (projects gallery + editor + account). That's exactly the set of
        // surfaces that mount AppNav and therefore the switcher.
        foreach ($route->gatherMiddleware() as $middleware) {
            if ($middleware === \App\Http\Middleware\EnsureTenant::class) {
                return true;
            }
        }

        // The members page, workspace-management, and the notifications
        // history page also mount AppNav (switcher + bell), so they read it.
        return $request->routeIs('workspace.*')
            || $request->routeIs('notifications.index');
    }
}
