<?php

use App\Http\Controllers\Admin\BlockedIpController;
use App\Http\Controllers\Admin\CurrencyController;
use App\Http\Controllers\Admin\LanguageController;
use App\Http\Controllers\Auth\TwoFactorController;
use App\Http\Controllers\IntegrationOAuthController;
use App\Http\Controllers\OnboardingController;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;

// ─── Web Installer ────────────────────────────────────────────────
Route::prefix('install')->middleware(\App\Http\Middleware\UseFileSession::class)->withoutMiddleware([
    \App\Http\Middleware\EnsureInstalled::class,
    \App\Http\Middleware\EnsureIpNotBlocked::class,
    \App\Http\Middleware\HoneypotProtection::class,
    \App\Http\Middleware\DdosProtection::class,
    \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
])->group(function () {
    Route::get('/',              [\App\Http\Controllers\InstallController::class, 'welcome'])->name('install.welcome');
    // Envato purchase code verification — sits between welcome and
    // requirements. Customer enters their purchase code; we verify via
    // api.envato.com against config('license.product_id'). On success
    // a license JSON is written and session('license_verified') flips
    // true so every downstream step (requirements onward) lets them
    // through.
    Route::get('/license',       [\App\Http\Controllers\InstallController::class, 'license'])->name('install.license');
    Route::post('/license',      [\App\Http\Controllers\InstallController::class, 'verifyLicense'])->name('install.license.verify');
    Route::get('/requirements',  [\App\Http\Controllers\InstallController::class, 'requirements'])->name('install.requirements');
    Route::post('/requirements', [\App\Http\Controllers\InstallController::class, 'checkRequirementsPost'])->name('install.requirements.check');
    Route::get('/database',      [\App\Http\Controllers\InstallController::class, 'database'])->name('install.database');
    Route::post('/database/test',[\App\Http\Controllers\InstallController::class, 'testDatabase'])->name('install.database.test');
    Route::post('/database',     [\App\Http\Controllers\InstallController::class, 'saveDatabase'])->name('install.database.save');
    Route::get('/application',   [\App\Http\Controllers\InstallController::class, 'application'])->name('install.application');
    Route::post('/application',  [\App\Http\Controllers\InstallController::class, 'saveApplication'])->name('install.application.save');
    Route::get('/admin',         [\App\Http\Controllers\InstallController::class, 'admin'])->name('install.admin');
    Route::post('/admin',        [\App\Http\Controllers\InstallController::class, 'saveAdmin'])->name('install.admin.save');
    Route::get('/run',           [\App\Http\Controllers\InstallController::class, 'run'])->name('install.run');
    Route::post('/execute',      [\App\Http\Controllers\InstallController::class, 'execute'])->name('install.execute');
    Route::get('/complete',      [\App\Http\Controllers\InstallController::class, 'complete'])->name('install.complete');
});

// Sitemap
Route::get('/sitemap.xml', [\App\Http\Controllers\SitemapController::class, 'index']);

// Admin bypass during maintenance — exempt from CSRF + maintenance middleware
// (see bootstrap/app.php). Validates admin email/password and redirects to
// /{maintenance_secret} so Laravel sets the bypass cookie automatically.
Route::post('/admin-maintenance-bypass', function (\Illuminate\Http\Request $request) {
    $request->validate([
        'email'    => 'required|email',
        'password' => 'required|string',
    ]);

    $user = \App\Models\User::where('email', $request->input('email'))->first();

    if (!$user
        || !$user->is_admin
        || ($user->status ?? 'active') !== 'active'
        || !\Illuminate\Support\Facades\Hash::check($request->input('password'), $user->password)
    ) {
        return response()->json([
            'ok' => false,
            'message' => 'Invalid admin credentials.',
        ], 422);
    }

    $secret = \App\Models\SystemSetting::get('maintenance_secret', '');
    if (!$secret) {
        return response()->json([
            'ok' => false,
            'message' => 'Bypass secret is not configured. Run `php artisan up` from the server.',
        ], 500);
    }

    \Illuminate\Support\Facades\Auth::login($user, true);

    return response()->json([
        'ok' => true,
        'redirect' => '/' . $secret,
    ]);
})->name('admin.maintenance.bypass');

// Language switcher
Route::post('/locale/switch', function (\Illuminate\Http\Request $request) {
    $code = $request->input('locale');
    $language = \App\Models\Language::where('code', $code)->where('is_active', true)->first();
    if ($language) {
        session(['locale' => $language->code]);
        if ($request->user()) {
            $request->user()->update(['language' => $language->code]);
        }
    }
    return back();
})->name('locale.switch');

// Homepage
//
// Section content (hero, features, testimonials, faq, cta) is pulled
// from the `pages` table by SLUG, not by `type` — pages.type is the
// ENUM('landing','static','blog') category, not the semantic name.
// The old `where('type', $type)` lookup never matched because admin
// saves stored the slug, not the section name in `type`. Pages also
// must be marked is_published = true to appear on the public site.
//
// Default content is provided per section so the landing page still
// renders cleanly on a fresh install where no CMS rows exist. Admin
// edits override the defaults the moment a matching page is created
// and published.
Route::middleware('frontend.enabled')->group(function () {

Route::get('/', function () {
    $defaults = [
        'hero' => [
            'badge'           => __('Now in Beta'),
            'title_line1'     => __('Automate your communication'),
            'title_highlight' => __('AI'),
            'title_line2'     => __('in minutes'),
            'subtitle'        => __('AI-powered email automation, CRM, and multi-channel inbox — all in one self-hosted platform.'),
            'cta_text'        => __('Get Started Free'),
            'cta_url'         => '/register',
            'cta2_text'       => __('See Pricing'),
            'cta2_url'        => '#pricing',
        ],
        'cta' => [
            'badge'    => __('Get Started'),
            'title'    => __('Ready to automate your workflow?'),
            'subtitle' => __('Join thousands of businesses using AI-powered automation.'),
            'cta_text' => __('Get Started Free'),
            'cta_url'  => '/register',
        ],
        'features'     => [],
        'testimonials' => [],
        'faq'          => [],
    ];

    $section = function (string $slug) use ($defaults) {
        $aliases = \App\Models\Page::SLUG_ALIASES[$slug] ?? [];
        $content = \App\Models\Page::findContentBySlug($slug, $aliases);
        $default = $defaults[$slug] ?? [];

        // Merge so admin-set values override defaults, but every expected
        // key still exists for the Blade view to read without crashing.
        return array_merge($default, is_array($content) ? $content : []);
    };

    $homepageTheme = \App\Models\SystemSetting::get('frontend_homepage_theme', 'classic');
    $homepageView  = match ($homepageTheme) {
        'modern' => 'frontend.landing.modern',
        'agent'  => 'frontend.landing.agent',
        default  => 'frontend.landing.index',
    };

    if ($homepageTheme === 'agent') {
        // "Ready" = admin has configured an AI provider with a saved key
        // under Admin → AI Providers. If not, the view still renders all
        // landing sections (hero, features, testimonials, pricing, FAQ,
        // CTA) so the page is never empty — only the chat panel is gated.
        $agentReady = app(\App\Services\HomepageAgentService::class)->isReady();

        return view('frontend.landing.agent', [
            'agentName'     => \App\Models\SystemSetting::get('agent_homepage_name', 'Alex'),
            'agentGreeting' => \App\Models\SystemSetting::get('agent_homepage_greeting', 'Hi! I\'m here to help. Ask me anything or tell me what you\'re looking for.'),
            'agentReady'    => $agentReady,
            'hero'          => $section('hero'),
            'features'      => $section('features'),
            'testimonials'  => $section('testimonials'),
            'faq'           => $section('faq'),
            'cta'           => $section('cta'),
        ]);
    }

    return view($homepageView, [
        'hero'         => $section('hero'),
        'features'     => $section('features'),
        'testimonials' => $section('testimonials'),
        'faq'          => $section('faq'),
        'cta'          => $section('cta'),
    ]);
});

}); // end frontend.enabled group — gates ONLY the marketing homepage

// ──────────────────────────────────────────────────────────────────────
// Homepage AI Agent — public endpoints used by the agentic landing page.
// Session-based (no token), CSRF-protected by the web middleware group,
// throttled to keep abuse + LLM costs bounded.
// ──────────────────────────────────────────────────────────────────────
Route::middleware('throttle:30,1')->prefix('agent-chat')->name('agent-chat.')->group(function () {
    Route::post('/start',   [\App\Http\Controllers\HomepageAgentController::class, 'start'])->name('start');
    Route::post('/send',    [\App\Http\Controllers\HomepageAgentController::class, 'send'])->name('send');
    Route::get('/history',  [\App\Http\Controllers\HomepageAgentController::class, 'history'])->name('history');
    Route::post('/reset',   [\App\Http\Controllers\HomepageAgentController::class, 'reset'])->name('reset');
});

// Legal pages — INTENTIONALLY OUTSIDE the frontend.enabled gate.
// Reason: the login / register screens link to these even when the
// landing-page front-end is disabled (admin-only install). Previously
// `frontend.enabled` swallowed these routes too, so customers running
// MailTrixy with the marketing site off saw their Terms / Privacy /
// Refund links 404.
//
// Per-link admin override: each route checks the matching SystemSetting
// key — already saved by the existing Admin → Settings → Legal tab for
// Terms and Privacy, plus the new `refund_policy_url` row added there.
// When set, the link 302-redirects to the external URL (typically the
// customer's WordPress / static-HTML page); when blank, the internal
// CMS-backed page renders via LegalController.
Route::get('/terms', function () {
    $url = trim((string) \App\Models\SystemSetting::get('terms_of_service_url', ''));
    if ($url !== '') return redirect()->away($url);
    return app(\App\Http\Controllers\LegalController::class)->terms();
})->name('legal.terms');

Route::get('/privacy', function () {
    $url = trim((string) \App\Models\SystemSetting::get('privacy_policy_url', ''));
    if ($url !== '') return redirect()->away($url);
    return app(\App\Http\Controllers\LegalController::class)->privacy();
})->name('legal.privacy');

Route::get('/refund-policy', function () {
    $url = trim((string) \App\Models\SystemSetting::get('refund_policy_url', ''));
    if ($url !== '') return redirect()->away($url);
    return app(\App\Http\Controllers\LegalController::class)->refund();
})->name('legal.refund');

Route::get('/contact', [\App\Http\Controllers\LegalController::class, 'contact'])->name('contact');
Route::get('/about', [\App\Http\Controllers\LegalController::class, 'about'])->name('about');
Route::get('/why-us', [\App\Http\Controllers\LegalController::class, 'whyUs'])->name('why-us');

// API Documentation (public, optionally shows user context when authenticated)
Route::get('/docs/api', [\App\Http\Controllers\ApiDocsController::class, 'index'])->name('api.docs');

// PWA offline fallback
Route::get('/offline', fn () => view('offline'));

// PWA dynamic manifest — reads settings from admin panel
Route::get('/manifest.json', function () {
    $s = fn(string $key, string $default = '') => \App\Models\SystemSetting::get($key, $default);

    $appName   = $s('pwa_app_name', $s('site_name', 'MailTrixy'));
    $shortName = $s('pwa_short_name', $appName);
    $startUrl  = $s('pwa_start_url', '/dashboard');
    $themeColor = $s('pwa_theme_color', '#6366f1');
    $bgColor    = $s('pwa_background_color', '#0f1117');

    // Build icons array from uploaded PWA icon or fallback
    $pwaIcon = $s('pwa_icon');
    $iconUrl = $pwaIcon ? asset('storage/' . $pwaIcon) : asset('images/icon-512.png');

    $icons = [];
    foreach ([72, 96, 128, 144, 152, 192, 384, 512] as $size) {
        $icons[] = [
            'src'     => $iconUrl,
            'sizes'   => "{$size}x{$size}",
            'type'    => 'image/png',
            'purpose' => 'any maskable',
        ];
    }

    $manifest = [
        'name'             => $appName,
        'short_name'       => $shortName,
        'description'      => $s('site_tagline', 'AI-Powered Communication Automation'),
        'start_url'        => $startUrl,
        'display'          => 'standalone',
        'orientation'      => 'any',
        'theme_color'      => $themeColor,
        'background_color' => $bgColor,
        'icons'            => $icons,
    ];

    return response()->json($manifest)
        ->header('Content-Type', 'application/manifest+json')
        ->header('Cache-Control', 'public, max-age=3600');
})->name('pwa.manifest');

// Dynamic robots.txt. Previously a static file in public/ that hardcoded
// `Sitemap: https://mailtrixy.com/sitemap.xml` — a reseller install would
// publicly leak the upstream domain to Googlebot. Now derived from APP_URL.
Route::get('/robots.txt', function () {
    $appUrl = rtrim((string) config('app.url'), '/');
    $body = "User-agent: *\nAllow: /\n"
          . "Disallow: /dashboard\nDisallow: /inbox\nDisallow: /contacts\n"
          . "Disallow: /settings\nDisallow: /admin\nDisallow: /api\n\n"
          . "Sitemap: {$appUrl}/sitemap.xml\n";
    return response($body)
        ->header('Content-Type', 'text/plain; charset=utf-8')
        ->header('Cache-Control', 'public, max-age=3600');
})->name('robots');

// Invite acceptance (public — handles logged-in and logged-out users)
Route::get('/invite/{token}', [\App\Http\Controllers\InviteController::class, 'accept'])->name('invite.accept');

// Knowledge Agent thumbs feedback. Public, signed-URL endpoint hit from
// channel-rendered "Was this helpful?" links (email, widget, IG/FB/WA
// quick replies). The signature is bound to APP_KEY so the link itself
// proves which kb_query_logs row may be updated — no auth needed.
Route::get('/kb/feedback/{token}/{verdict}', [\App\Http\Controllers\KbFeedbackController::class, 'feedback'])
    ->name('kb.feedback')
    ->where('verdict', 'up|down')
    ->middleware('throttle:60,1'); // simple abuse cap; idempotent column update is safe to spam

// Auth routes handled by Fortify (login, register, password reset, email verification, 2fa)

// Two-Factor Authentication — challenge routes (no auth: user is mid-login)
Route::middleware('web')->group(function () {
    Route::get('/two-factor/challenge', [TwoFactorController::class, 'challenge'])->name('two-factor.challenge');
    Route::post('/two-factor/verify', [TwoFactorController::class, 'verify'])->name('two-factor.verify');
});

// Two-Factor Authentication — setup routes (auth required)
Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/two-factor/setup', [TwoFactorController::class, 'setup'])->name('two-factor.setup');
    Route::post('/two-factor/activate', [TwoFactorController::class, 'enable'])->name('two-factor.activate');
    Route::post('/two-factor/deactivate', [TwoFactorController::class, 'disable'])->name('two-factor.deactivate');

    // In-app notifications (bell dropdown)
    Route::get('/notifications', [\App\Http\Controllers\NotificationController::class, 'index'])->name('notifications.index');
    Route::post('/notifications/read', [\App\Http\Controllers\NotificationController::class, 'markAllRead'])->name('notifications.read-all');
    Route::post('/notifications/{id}/read', [\App\Http\Controllers\NotificationController::class, 'markRead'])->name('notifications.read');
    Route::get('/notifications/{id}/open', [\App\Http\Controllers\NotificationController::class, 'open'])->name('notifications.open');
});

// Email Account OAuth (Gmail/Outlook connection — requires auth, NOT verified)
//
// The 'verified' middleware was dropped here on purpose. Onboarding step 2
// redirects newly-registered users to email-oauth.google / .microsoft to
// connect their first mailbox — but a brand-new user hasn't clicked the
// verification email yet, so 'verified' would bounce them to /email/verify
// and the OAuth round-trip never starts. Result: customers reported "site
// goes down when I pick Gmail/Hotmail on step 2", which was actually a
// silent middleware bounce. Auth is still required — only the verification
// check is relaxed here so mailbox-connect works during onboarding.
Route::middleware(['auth'])->group(function () {
    Route::get('/email-oauth/google/redirect', [\App\Http\Controllers\EmailOAuthController::class, 'redirectGoogle'])->name('email-oauth.google');
    Route::get('/email-oauth/google/callback', [\App\Http\Controllers\EmailOAuthController::class, 'callbackGoogle']);
    Route::get('/email-oauth/microsoft/redirect', [\App\Http\Controllers\EmailOAuthController::class, 'redirectMicrosoft'])->name('email-oauth.microsoft');
    Route::get('/email-oauth/microsoft/callback', [\App\Http\Controllers\EmailOAuthController::class, 'callbackMicrosoft']);
});

// Integration OAuth callbacks (Slack, Salesforce, Google Calendar — requires auth)
Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/integrations/slack/redirect', [IntegrationOAuthController::class, 'slackRedirect'])->name('integrations.slack.redirect');
    Route::get('/integrations/slack/callback', [IntegrationOAuthController::class, 'slackCallback'])->name('integrations.slack.callback');
    Route::get('/integrations/salesforce/redirect', [IntegrationOAuthController::class, 'salesforceRedirect'])->name('integrations.salesforce.redirect');
    Route::get('/integrations/salesforce/callback', [IntegrationOAuthController::class, 'salesforceCallback'])->name('integrations.salesforce.callback');
    Route::get('/integrations/google-calendar/redirect', [IntegrationOAuthController::class, 'googleCalendarRedirect'])->name('integrations.google-calendar.redirect');
    Route::get('/integrations/google-calendar/callback', [IntegrationOAuthController::class, 'googleCalendarCallback'])->name('integrations.google-calendar.callback');
});

// OAuth Social Login
// Honors the `social_login_enabled` toggle from Admin → Settings → Auth.
// Callback stays open so anyone mid-flow doesn't 404; only the redirect
// (= initiator) is blocked when the admin disables it.
Route::get('/auth/{provider}/redirect', function (string $provider) {
    if (\App\Models\SystemSetting::get('social_login_enabled', 'true') !== 'true') {
        return redirect('/login')->with('error', __('Social login is currently disabled.'));
    }
    return app(\App\Http\Controllers\Auth\SocialLoginController::class)->redirect($provider);
})->where('provider', 'google|microsoft|github|facebook');

Route::get('/auth/{provider}/callback', [\App\Http\Controllers\Auth\SocialLoginController::class, 'callback'])
    ->where('provider', 'google|microsoft|github|facebook');

// Authenticated routes
Route::middleware(['auth', 'verified', 'workspace'])->group(function () {

    // Dashboard
    Route::get('/dashboard', function () {
        $user = auth()->user();
        $workspace = $user?->activeWorkspace;
        $subscription = $workspace?->subscription;

        // Super Admins bypass billing/plan gates — they're operating the platform,
        // not consuming a subscription. Any other user without an active plan
        // gets the upgrade nudge.
        $isSuperAdmin = $user && (
            ($user->is_admin && $user->admin_role === 'super_admin')
            || $user->hasRole('Super Admin')
        );

        if (!$isSuperAdmin && (! $subscription || ! $subscription->isActive())) {
            session()->flash('warning', __('You don\'t have an active plan. Please subscribe to unlock all features.'));
        }

        return view('dashboard.index');
    })->name('dashboard');

    // Checkout (multi-gateway)
    Route::get('/checkout/{plan}', [\App\Http\Controllers\CheckoutController::class, 'show'])->name('checkout.show');
    Route::post('/checkout/{plan}/process', [\App\Http\Controllers\CheckoutController::class, 'process'])->name('checkout.process');
    Route::post('/checkout/validate-coupon', [\App\Http\Controllers\CheckoutController::class, 'validateCoupon'])->name('checkout.validate-coupon');
    Route::get('/checkout-success', [\App\Http\Controllers\CheckoutController::class, 'success'])->name('checkout.success');
    Route::get('/checkout-cancel', [\App\Http\Controllers\CheckoutController::class, 'cancel'])->name('checkout.cancel');

    // Customer Support tickets — customer-facing flow. The admin side
    // lives at /admin/tickets and is owned by Admin\TicketController; this
    // route group is what end users see (list their own tickets, create
    // new ones, reply on the thread). Without these routes the platform
    // shipped with admin-only ticket management and customers had no
    // self-serve way to file a support request.
    Route::get(   '/support',                      [\App\Http\Controllers\SupportController::class, 'index'])->name('support.index');
    Route::get(   '/support/create',               [\App\Http\Controllers\SupportController::class, 'create'])->name('support.create');
    Route::post(  '/support',                      [\App\Http\Controllers\SupportController::class, 'store'])->name('support.store');
    Route::get(   '/support/{ticket}',             [\App\Http\Controllers\SupportController::class, 'show'])->whereNumber('ticket')->name('support.show');
    Route::post(  '/support/{ticket}/reply',       [\App\Http\Controllers\SupportController::class, 'reply'])->whereNumber('ticket')->name('support.reply');

    // Onboarding (5-step wizard)
    Route::prefix('onboarding')->name('onboarding.')->group(function () {
        Route::get('/step-1', [OnboardingController::class, 'step1'])->name('step-1');
        Route::post('/step-1', [OnboardingController::class, 'storeStep1'])->name('store-step-1');
        Route::get('/step-2', [OnboardingController::class, 'step2'])->name('step-2');
        Route::post('/step-2', [OnboardingController::class, 'storeStep2'])->name('store-step-2');
        Route::get('/step-3', [OnboardingController::class, 'step3'])->name('step-3');
        Route::post('/step-3', [OnboardingController::class, 'storeStep3'])->name('store-step-3');
        Route::get('/step-4', [OnboardingController::class, 'step4'])->name('step-4');
        Route::post('/step-4', [OnboardingController::class, 'storeStep4'])->name('store-step-4');
        Route::get('/step-5', [OnboardingController::class, 'step5'])->name('step-5');
        Route::post('/step-5', [OnboardingController::class, 'storeStep5'])->name('store-step-5');
        Route::get('/complete', [OnboardingController::class, 'complete'])->name('complete');
        Route::post('/test-connection', [OnboardingController::class, 'testConnection'])->name('test-connection');
    });

    Route::get('/inbox', fn() => view('inbox.alpine-inbox'))->name('inbox');

    // Inbox API endpoints (JSON)
    Route::get('/inbox/api/conversations', [\App\Http\Controllers\InboxApiController::class, 'conversations']);
    Route::get('/inbox/api/conversations/{id}/messages', [\App\Http\Controllers\InboxApiController::class, 'messages']);
    Route::post('/inbox/api/conversations/{id}/action', [\App\Http\Controllers\InboxApiController::class, 'action']);
    Route::post('/inbox/api/conversations/{id}/summarize', [\App\Http\Controllers\InboxApiController::class, 'summarize']);
    Route::post('/inbox/api/conversations/{id}/ai-chat', [\App\Http\Controllers\InboxApiController::class, 'aiChat']);
    Route::post('/inbox/api/bulk', [\App\Http\Controllers\InboxApiController::class, 'bulk']);
    Route::get('/inbox/api/poll', [\App\Http\Controllers\InboxApiController::class, 'poll']);
    Route::get('/inbox/api/sidebar', [\App\Http\Controllers\InboxApiController::class, 'sidebar']);
    Route::get('/inbox/api/tags', [\App\Http\Controllers\InboxApiController::class, 'tags']);
    Route::get('/inbox/api/team', [\App\Http\Controllers\InboxApiController::class, 'team']);
    Route::get('/inbox/api/accounts', [\App\Http\Controllers\InboxApiController::class, 'accounts']);
    Route::post('/inbox/api/sync', [\App\Http\Controllers\InboxApiController::class, 'syncNow']);
    Route::post('/inbox/api/set-timezone', [\App\Http\Controllers\InboxApiController::class, 'setTimezone']);
    Route::post('/inbox/api/conversations/{id}/fetch-bodies', [\App\Http\Controllers\InboxApiController::class, 'fetchBodies']);
    // Per-message body refetch — works for all providers (Gmail/IMAP/Outlook).
    // Frontend calls this on-click when a message has empty body_html + body_text
    // so users get the content even if the original sync parser failed.
    Route::post('/inbox/api/messages/{id}/fetch-body', [\App\Http\Controllers\InboxApiController::class, 'fetchSingleBody']);
    // Explicit orphan-conversation recovery — wired to the "Retry" button on
    // the empty-thread bubble. Triggers one small account-level syncBatch so
    // the missing messages get re-fetched from the provider. Never runs on
    // page load; user must click Retry.
    Route::post('/inbox/api/conversations/{id}/recover', [\App\Http\Controllers\InboxApiController::class, 'recoverOrphan']);

    Route::get('/inbox/compose', function () {
        return view('inbox.compose');
    })->name('inbox.compose');

    Route::get('/inbox/canned-responses', function () {
        return view('inbox.canned-responses');
    })->name('inbox.canned-responses');

    // Email HTML viewer (serves raw email HTML for iframe rendering)
    // Proxies external images through our server to avoid CORP/CORS blocks.
    Route::get('/messages/{message}/html', function (\App\Models\Message $message) {
        $workspaceId = auth()->user()->active_workspace_id;
        abort_unless($message->workspace_id === $workspaceId, 404);

        $raw = request()->has('raw');
        $html = $message->body_html ?? '';

        // No body — show subject as fallback
        if (empty($html) && empty($message->body_text)) {
            $text = e($message->subject ?? 'Email content unavailable');
            if ($raw) return response('<p>' . nl2br($text) . '</p>', 200)->header('Content-Type', 'text/html; charset=utf-8');
            $fallback = '<!DOCTYPE html><html><head><meta charset="utf-8"></head><body style="margin:0;padding:16px;font-family:system-ui,sans-serif;font-size:14px;color:#999">' . nl2br($text) . '</body></html>';
            return response($fallback, 200)->header('Content-Type', 'text/html; charset=utf-8');
        }

        // Plain text only
        if (empty($html)) {
            $text = e($message->body_text ?? $message->subject ?? '');
            if ($raw) return response('<p>' . nl2br($text) . '</p>', 200)->header('Content-Type', 'text/html; charset=utf-8');
            $html = '<!DOCTYPE html><html><head><meta charset="utf-8"></head><body style="margin:0;padding:16px;font-family:system-ui,sans-serif;font-size:14px;color:#333">' . nl2br($text) . '</body></html>';
            return response($html, 200)->header('Content-Type', 'text/html; charset=utf-8');
        }

        // Wrap incomplete HTML in a proper document structure
        if ($html && !preg_match('/<html[\s>]/i', $html)) {
            // Strip preheader/hidden text
            $html = preg_replace('/<span[^>]*style="[^"]*display\s*:\s*none[^"]*"[^>]*>.*?<\/span>/is', '', $html);
            $html = preg_replace('/<div[^>]*style="[^"]*display\s*:\s*none[^"]*"[^>]*>.*?<\/div>/is', '', $html);
            $html = preg_replace('/<span[^>]*class="[^"]*preheader[^"]*"[^>]*>.*?<\/span>/is', '', $html);
            // Replace cid: images with transparent placeholder
            $html = preg_replace('/src=["\']cid:[^"\']*["\']/is', 'src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"', $html);

            // Extract <style> blocks and <body> content if present
            $styles = '';
            if (preg_match_all('/<style[^>]*>.*?<\/style>/is', $html, $styleMatches)) {
                $styles = implode("\n", $styleMatches[0]);
            }

            // If there's a <body> tag, extract its attributes and content
            if (preg_match('/<body([^>]*)>(.*)/is', $html, $bodyMatch)) {
                $bodyAttrs = $bodyMatch[1];
                $bodyContent = $bodyMatch[2];
                // Remove closing </body> if present
                $bodyContent = preg_replace('/<\/body>\s*$/is', '', $bodyContent);
                // Strip any text/meta before <body>
                $html = '<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">' . $styles . '<style>img{max-width:100%}</style></head><body' . $bodyAttrs . '>' . $bodyContent . '</body></html>';
            } else {
                // No <body> tag — strip bare text before first HTML element and wrap
                $html = preg_replace('/^[^<]+/', '', ltrim($html));
                $html = '<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">' . $styles . '<style>img{max-width:100%}</style></head><body style="margin:0;padding:8px">' . $html . '</body></html>';
            }
        }

        // Rewrite external image URLs to go through our proxy
        $html = preg_replace_callback(
            '/(src\s*=\s*["\'])(https?:\/\/[^"\']+)(["\'])/i',
            function ($matches) use ($message) {
                $url = $matches[2];
                $proxied = route('image.proxy', ['url' => base64_encode($url), 'msg' => $message->id]);
                return $matches[1] . $proxied . $matches[3];
            },
            $html
        );

        // Raw mode: extract just body content + styles for direct injection (no iframe)
        if ($raw) {
            // Strip <script> tags for security
            $html = preg_replace('/<script\b[^>]*>.*?<\/script>/is', '', $html);
            // Extract styles
            $styles = '';
            if (preg_match_all('/<style[^>]*>.*?<\/style>/is', $html, $sm)) {
                $styles = implode("\n", $sm[0]);
            }
            // Extract body content
            if (preg_match('/<body[^>]*>(.*?)<\/body>/is', $html, $bm)) {
                $body = $bm[1];
            } else {
                $body = $html;
            }
            return response($styles . $body, 200)->header('Content-Type', 'text/html; charset=utf-8');
        }

        return response($html, 200)
            ->header('Content-Type', 'text/html; charset=utf-8')
            ->header('Content-Security-Policy', "script-src 'none'; img-src * data: cid: blob:; style-src 'unsafe-inline' *; font-src * data:;");
    })->name('message.html')->withoutMiddleware([\App\Http\Middleware\SecurityHeaders::class]);

    // Image proxy for email content — fetches external images server-side
    Route::get('/image-proxy/{msg}', function (\Illuminate\Http\Request $request, int $msg) {
        $workspaceId = auth()->user()->active_workspace_id;
        $message = \App\Models\Message::where('workspace_id', $workspaceId)->findOrFail($msg);

        $encodedUrl = $request->query('url');
        if (!$encodedUrl) abort(400);

        $url = base64_decode($encodedUrl);
        if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) abort(400);

        // Only allow http/https
        $scheme = parse_url($url, PHP_URL_SCHEME);
        if (!in_array($scheme, ['http', 'https'])) abort(400);

        // SSRF protection: block private/internal IPs
        $host = parse_url($url, PHP_URL_HOST);
        if (!$host) abort(400);

        $hostLower = strtolower($host);

        // Block dangerous domain suffixes
        if (str_ends_with($hostLower, '.local')
            || str_ends_with($hostLower, '.internal')
            || str_ends_with($hostLower, '.localhost')) {
            abort(403, 'Blocked: internal hostname.');
        }

        // Block cloud metadata endpoint
        if ($hostLower === '169.254.169.254' || $hostLower === 'metadata.google.internal') {
            abort(403, 'Blocked: metadata endpoint.');
        }

        // Resolve hostname and check all IPs
        $resolvedIps = gethostbynamel($hostLower);
        if (!$resolvedIps) {
            // If it's a raw IP, check it directly
            if (filter_var($host, FILTER_VALIDATE_IP)) {
                $resolvedIps = [$host];
            } else {
                abort(403, 'Blocked: hostname could not be resolved.');
            }
        }

        foreach ($resolvedIps as $ip) {
            // filter_var returns false if IP is private or reserved
            if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                abort(403, 'Blocked: private or reserved IP address.');
            }
            // Block IPv6 loopback
            if ($ip === '::1') {
                abort(403, 'Blocked: loopback address.');
            }
        }

        try {
            $response = \Illuminate\Support\Facades\Http::timeout(10)
                ->get($url);

            if (!$response->successful()) abort(404);

            $contentType = $response->header('Content-Type') ?? 'image/png';

            // Only proxy image types
            if (!str_starts_with($contentType, 'image/')) abort(403);

            return response($response->body(), 200)
                ->header('Content-Type', $contentType)
                ->header('Cache-Control', 'public, max-age=86400');
        } catch (\Throwable $e) {
            abort(404);
        }
    })->name('image.proxy')->withoutMiddleware([\App\Http\Middleware\SecurityHeaders::class]);

    // Contacts
    Route::get('/contacts', function () {
        return view('contacts.index');
    })->name('contacts');

    Route::get('/contacts/groups', function () {
        return view('contacts.groups');
    })->name('contacts.groups');

    Route::get('/contacts/trash', function () {
        return view('contacts.trash');
    })->name('contacts.trash');

    Route::get('/contacts/merge', function () {
        return view('contacts.merge');
    })->name('contacts.merge');

    Route::get('/contacts/{contact}', function (\App\Models\Contact $contact) {
        // Ensure the contact belongs to the user's active workspace
        abort_unless($contact->workspace_id === auth()->user()->active_workspace_id, 404);
        $contact->load('tags');
        return view('contacts.show', compact('contact'));
    })->name('contacts.detail');

    // Deals
    Route::get('/deals', function () {
        return view('deals.index');
    })->name('deals')->middleware('plan.feature:deal_pipeline');

    // Campaigns
    Route::middleware('plan.feature:campaigns')->group(function () {
        Route::get('/campaigns', function () {
            return view('campaigns.index');
        })->name('campaigns');

        Route::get('/campaigns/templates', function () {
            return view('campaigns.templates');
        })->name('campaigns.templates');

        // Standalone Email Templates library — same EmailTemplate
        // rows the campaigns module uses, but exposed as a top-level
        // CRUD surface so templates can be created/edited outside
        // campaign creation and inserted into inbox compose, AI
        // replies, and any future feature that wants a reusable HTML
        // body.
        Route::get('/templates', function () {
            return view('templates.index');
        })->name('templates');

        // Templates builder — mounts App\Livewire\Campaigns\EmailBuilder,
        // the SAME block builder the campaign editor uses. Sharing one
        // builder means a template authored here renders identically in
        // both the campaigns send path and the inbox TemplatePicker,
        // and any future block-type additions auto-flow into both.
        // The optional {id} edits an existing template; without it a
        // blank canvas is opened for a new template.
        Route::get('/templates/builder', function () {
            return view('templates.builder', ['templateId' => null]);
        })->name('templates.builder.new');
        Route::get('/templates/builder/{id}', function ($id) {
            return view('templates.builder', ['templateId' => (int) $id]);
        })->name('templates.builder.edit')->whereNumber('id');

        Route::get('/campaigns/create', function () {
            return view('campaigns.create');
        })->name('campaigns.create');

        Route::get('/campaigns/{campaign}/edit', function ($campaign) {
            return view('campaigns.edit', ['campaignId' => (int) $campaign]);
        })->name('campaigns.edit');

        Route::get('/campaigns/{campaign}/report', function ($campaign) {
            return view('campaigns.report', ['campaignId' => (int) $campaign]);
        })->name('campaigns.report');
    });

    // Workflows
    Route::middleware('plan.feature:workflows')->group(function () {
        Route::get('/workflows', function () {
            return view('workflows.index');
        })->name('workflows');

        Route::get('/workflows/create', function () {
            return view('workflows.create');
        })->name('workflows.create');

        Route::get('/workflows/{workflow}/edit', function ($workflow) {
            return view('workflows.edit', ['workflowId' => (int) $workflow]);
        })->name('workflows.edit');

        Route::get('/workflows/{workflow}/logs', function ($workflow) {
            return view('workflows.logs', ['workflowId' => (int) $workflow]);
        })->name('workflows.logs');
    });

    // Knowledge Base
    Route::get('/knowledge-base', function () {
        return view('knowledge-base.index');
    })->name('knowledge-base')->middleware('plan.feature:knowledge_base');

    // Knowledge Agent Insights — deflection rate, knowledge gaps, top
    // sources, and a thumbs-tunable recent-answers feed sourced from
    // kb_query_logs. Same plan gate as the KB itself.
    Route::get('/knowledge-base/insights', function () {
        return view('knowledge-base.insights');
    })->name('knowledge-base.insights')->middleware('plan.feature:knowledge_base');

    // Temp Mail
    Route::get('/temp-mail', function () {
        return view('temp-mail.index');
    })->name('temp-mail');

    // Activity
    Route::get('/activity', function () {
        return view('activity.index');
    })->name('activity');

    // Analytics
    Route::get('/analytics', function () {
        return view('analytics.index');
    })->name('analytics')->middleware('plan.feature:analytics');

    // Help Center
    Route::get('/help', function () {
        return view('help.index');
    })->name('help');

    // Settings
    Route::prefix('settings')->name('settings.')->group(function () {
        Route::get('/', fn () => redirect(url('/settings/profile')));
        Route::get('/profile', function () { return view('settings.profile'); })->name('profile');
        Route::get('/security', function () { return view('settings.security'); })->name('security');
        Route::get('/email', function () { return view('settings.email'); })->name('email');
        Route::get('/channels', function () { return view('settings.channels'); })->name('channels');
        Route::get('/ai', function () { return view('settings.ai'); })->name('ai');
        Route::get('/contacts', function () { return view('settings.contacts'); })->name('contacts');
        Route::get('/team', function () { return view('settings.team'); })->name('team');
        Route::get('/billing', function () { return view('settings.billing'); })->name('billing');
        Route::get('/workspace', function () { return view('settings.workspace'); })->name('workspace');
        Route::get('/notifications', function () { return view('settings.notifications'); })->name('notifications');
        Route::get('/auto-reply', function () { return view('settings.auto-reply'); })->name('auto-reply');
        Route::get('/integrations', function () { return view('settings.integrations'); })->name('integrations');
        Route::get('/webhooks', function () { return view('settings.webhooks'); })->name('webhooks');
        Route::get('/data-privacy', function () { return view('settings.data-privacy'); })->name('data-privacy');
        Route::get('/account', function () { return view('settings.account'); })->name('account');
        Route::get('/invoices', [\App\Http\Controllers\InvoiceController::class, 'index'])->name('invoices');
    });

    // Invoice download/preview — outside the settings prefix so the URL is
    // cleaner ("/invoices/{id}/download") and the route can be linked
    // directly from outside the settings nav (e.g. payment receipt emails).
    Route::get('/invoices/{payment}/download', [\App\Http\Controllers\InvoiceController::class, 'download'])->name('invoices.download');
    Route::get('/invoices/{payment}/view', [\App\Http\Controllers\InvoiceController::class, 'view'])->name('invoices.view');
    // HTML preview rendered into the in-app modal iframe (no browser PDF chrome).
    Route::get('/invoices/{payment}/preview', [\App\Http\Controllers\InvoiceController::class, 'preview'])->name('invoices.preview');

    // GDPR data export & deletion (rate limited — export is expensive and generates large payloads)
    Route::get('/settings/export-data', [\App\Http\Controllers\DataExportController::class, 'export'])
        ->middleware('throttle:sensitive')
        ->name('settings.export-data');
    Route::post('/settings/delete-account', [\App\Http\Controllers\DataExportController::class, 'requestDeletion'])
        ->middleware('throttle:sensitive')
        ->name('settings.delete-account');
    Route::post('/settings/deactivate-account', [\App\Http\Controllers\DataExportController::class, 'deactivate'])
        ->middleware('throttle:sensitive')
        ->name('settings.deactivate-account');

    // Search (real Livewire backend)
    Route::get('/search', function () {
        return view('search.index');
    })->name('search');

    // Quick search JSON endpoint for header dropdown
    Route::get('/search/quick', function (\Illuminate\Http\Request $request) {
        $q = trim($request->input('q', ''));
        $wsId = auth()->user()->active_workspace_id;

        if (strlen($q) < 2 || !$wsId) {
            return response()->json(['contacts' => [], 'conversations' => []]);
        }

        $contacts = \App\Models\Contact::where('workspace_id', $wsId)
            ->where(fn ($query) => $query->where('first_name', 'like', "%{$q}%")
                ->orWhere('last_name', 'like', "%{$q}%")
                ->orWhere('email', 'like', "%{$q}%")
                ->orWhere('company', 'like', "%{$q}%"))
            ->limit(5)
            ->get(['id', 'first_name', 'last_name', 'email'])
            ->map(fn ($c) => [
                'id' => $c->id,
                'name' => trim("{$c->first_name} {$c->last_name}"),
                'email' => $c->email ?? '',
                'initials' => strtoupper(substr($c->first_name ?? '?', 0, 1) . substr($c->last_name ?? '', 0, 1)),
                'url' => url("/contacts/{$c->id}"),
            ]);

        $conversations = \App\Models\Conversation::where('workspace_id', $wsId)
            ->where('subject', 'like', "%{$q}%")
            ->with('contact:id,first_name,last_name')
            ->limit(5)
            ->get(['id', 'subject', 'contact_id'])
            ->map(fn ($c) => [
                'id' => $c->id,
                'subject' => $c->subject ?: 'No subject',
                'contact' => $c->contact ? trim("{$c->contact->first_name} {$c->contact->last_name}") : 'Unknown',
                'url' => url("/inbox?cid={$c->id}"),
            ]);

        return response()->json([
            'contacts' => $contacts,
            'conversations' => $conversations,
        ]);
    })->name('search.quick');

});

Route::get('/test-inboc', function () {
    return view('test-inbox');
})->name('test-inboc');

Route::get('/test-compose', function () {
    return view('test-compose');
})->name('test-compose');

// Admin login (separate guard — no auth required)
Route::prefix('admin')->name('admin.')->middleware('web')->group(function () {
    Route::get('/login', [\App\Http\Controllers\Admin\AuthController::class, 'showLogin'])->name('login');
    Route::post('/login', [\App\Http\Controllers\Admin\AuthController::class, 'login']);
    Route::post('/logout', [\App\Http\Controllers\Admin\AuthController::class, 'logout'])->name('admin-logout');
});

// Admin panel (same user auth, admin middleware checks is_admin flag + Spatie permissions)
Route::prefix('admin')->name('admin.')->middleware(['auth', 'verified', 'admin', 'admin.permission', 'log.admin'])->group(function () {
    Route::get('/', fn () => redirect()->route('admin.dashboard'));

    Route::get('/dashboard', [\App\Http\Controllers\Admin\DashboardController::class, 'index'])->name('dashboard');

    Route::get('/test-dashboard', function () {
        return view('admin.test-dashboard');
    })->name('test-dashboard');

    // Export, import, and bulk routes (registered BEFORE resource routes to avoid {id} conflicts)
    Route::get('/users/export', [\App\Http\Controllers\Admin\UserController::class, 'export'])->name('users.export');
    Route::post('/users/import', [\App\Http\Controllers\Admin\UserController::class, 'import'])->name('users.import');
    Route::post('/users/bulk-destroy', [\App\Http\Controllers\Admin\UserController::class, 'bulkDestroy'])->name('users.bulk-destroy');
    Route::get('/plans/export', [\App\Http\Controllers\Admin\PlanController::class, 'export'])->name('plans.export');
    Route::post('/plans/import', [\App\Http\Controllers\Admin\PlanController::class, 'import'])->name('plans.import');
    Route::post('/plans/bulk-destroy', [\App\Http\Controllers\Admin\PlanController::class, 'bulkDestroy'])->name('plans.bulk-destroy');
    Route::get('/payments/export', [\App\Http\Controllers\Admin\PaymentController::class, 'export'])->name('payments.export');
    Route::post('/payments/import', [\App\Http\Controllers\Admin\PaymentController::class, 'import'])->name('payments.import');
    Route::get('/tickets/export', [\App\Http\Controllers\Admin\TicketController::class, 'export'])->name('tickets.export');
    Route::post('/tickets/import', [\App\Http\Controllers\Admin\TicketController::class, 'import'])->name('tickets.import');
    Route::post('/tickets/bulk-destroy', [\App\Http\Controllers\Admin\TicketController::class, 'bulkDestroy'])->name('tickets.bulk-destroy');
    Route::get('/coupons/export', [\App\Http\Controllers\Admin\CouponController::class, 'export'])->name('coupons.export');
    Route::post('/coupons/import', [\App\Http\Controllers\Admin\CouponController::class, 'import'])->name('coupons.import');
    Route::post('/coupons/bulk-destroy', [\App\Http\Controllers\Admin\CouponController::class, 'bulkDestroy'])->name('coupons.bulk-destroy');
    Route::get('/audit-log/export', [\App\Http\Controllers\Admin\AuditLogController::class, 'export'])->name('audit-log.export');
    Route::post('/blocked-ips/bulk-destroy', [BlockedIpController::class, 'bulkDestroy'])->name('blocked-ips.bulk-destroy');

    Route::resource('/users', \App\Http\Controllers\Admin\UserController::class)->only(['index', 'show', 'create', 'store', 'destroy']);
    Route::post('/users/{user}/impersonate', [\App\Http\Controllers\Admin\UserController::class, 'impersonate'])->name('users.impersonate');
    Route::post('/users/{user}/suspend', [\App\Http\Controllers\Admin\UserController::class, 'suspend'])->name('users.suspend');
    Route::post('/users/{user}/unsuspend', [\App\Http\Controllers\Admin\UserController::class, 'unsuspend'])->name('users.unsuspend');

    Route::resource('/plans', \App\Http\Controllers\Admin\PlanController::class)->only(['index', 'create', 'store', 'show', 'edit', 'update', 'destroy']);

    // Addon manager — admin-only ZIP installer for first-party + third-party
    // MailTrixy add-ons. The upload route stays behind the same admin guard
    // as the rest of this group; addons themselves live under /addons/{slug}/.
    Route::get('/addons', [\App\Http\Controllers\Admin\AddonController::class, 'index'])->name('addons.index');
    Route::post('/addons/upload', [\App\Http\Controllers\Admin\AddonController::class, 'upload'])->name('addons.upload');
    // Platform-wide MailTrixy license verification, accessed from the
    // addons page when the customer skipped the installer (e.g.
    // upgraded an existing install). One MailTrixy license unlocks
    // every addon — no per-addon code entry. The Activate button is
    // gated server-side on this license existing.
    Route::post('/addons/license',          [\App\Http\Controllers\Admin\AddonController::class, 'verifyPlatformLicense'])->name('addons.license');
    Route::post('/addons/{addon}/activate', [\App\Http\Controllers\Admin\AddonController::class, 'activate'])->name('addons.activate');
    Route::post('/addons/{addon}/deactivate', [\App\Http\Controllers\Admin\AddonController::class, 'deactivate'])->name('addons.deactivate');
    Route::delete('/addons/{addon}', [\App\Http\Controllers\Admin\AddonController::class, 'uninstall'])->name('addons.uninstall');

    Route::get('/payments', [\App\Http\Controllers\Admin\PaymentController::class, 'index'])->name('payments.index');
    Route::post('/payments/{payment}/refund', [\App\Http\Controllers\Admin\PaymentController::class, 'refund'])->name('payments.refund');
    // Manual approve for Bank Transfer / Offline payments. Restricted
    // to those gateways inside the controller (online gateways are
    // reconciled by their own webhook flow — surface a clear error
    // if an admin tries to manually approve a Stripe row by guessing
    // the URL).
    Route::post('/payments/{payment}/approve', [\App\Http\Controllers\Admin\PaymentController::class, 'approve'])->name('payments.approve');

    Route::get('/tickets', [\App\Http\Controllers\Admin\TicketController::class, 'index'])->name('tickets.index');
    Route::post('/tickets', [\App\Http\Controllers\Admin\TicketController::class, 'store'])->name('tickets.store');
    Route::get('/tickets/{ticket}', [\App\Http\Controllers\Admin\TicketController::class, 'show'])->name('tickets.show');
    Route::post('/tickets/{ticket}/reply', [\App\Http\Controllers\Admin\TicketController::class, 'reply'])->name('tickets.reply');
    Route::patch('/tickets/{ticket}/status', [\App\Http\Controllers\Admin\TicketController::class, 'updateStatus'])->name('tickets.status');
    Route::delete('/tickets/{ticket}', [\App\Http\Controllers\Admin\TicketController::class, 'destroy'])->name('tickets.destroy');

    Route::get('/system', [\App\Http\Controllers\Admin\SystemController::class, 'index'])->name('system');
    Route::get('/system/dedupe', [\App\Http\Controllers\Admin\SystemController::class, 'dedupe'])->name('system.dedupe');
    Route::get('/system-hub', fn () => view('admin.system-hub'))->name('system-hub');
    Route::resource('testimonials', \App\Http\Controllers\Admin\TestimonialController::class)->except(['show']);
    // Homepage Agent leads — captured by the public AI agent on the landing page.
    Route::get('/homepage-leads',                     [\App\Http\Controllers\Admin\HomepageLeadsController::class, 'index'])->name('homepage-leads.index');
    Route::get('/homepage-leads/export',              [\App\Http\Controllers\Admin\HomepageLeadsController::class, 'export'])->name('homepage-leads.export');
    Route::get('/homepage-leads/{lead}',              [\App\Http\Controllers\Admin\HomepageLeadsController::class, 'show'])->name('homepage-leads.show');
    Route::patch('/homepage-leads/{lead}/status',     [\App\Http\Controllers\Admin\HomepageLeadsController::class, 'updateStatus'])->name('homepage-leads.status');
    Route::patch('/homepage-leads/{lead}/notes',      [\App\Http\Controllers\Admin\HomepageLeadsController::class, 'updateNotes'])->name('homepage-leads.notes');
    Route::delete('/homepage-leads/{lead}',           [\App\Http\Controllers\Admin\HomepageLeadsController::class, 'destroy'])->name('homepage-leads.destroy');

    Route::get('/frontend-settings', [\App\Http\Controllers\Admin\FrontendSettingsController::class, 'index'])->name('frontend-settings.index');
    Route::put('/frontend-settings/homepage-theme', [\App\Http\Controllers\Admin\FrontendSettingsController::class, 'updateHomepageTheme'])->name('frontend-settings.homepage-theme');
    Route::post('/frontend-settings/agent-config', [\App\Http\Controllers\Admin\FrontendSettingsController::class, 'updateAgentConfig'])->name('frontend-settings.agent-config');
    Route::get('/frontend-settings/{type}/edit', [\App\Http\Controllers\Admin\FrontendSettingsController::class, 'edit'])->name('frontend-settings.edit');
    Route::put('/frontend-settings/{type}', [\App\Http\Controllers\Admin\FrontendSettingsController::class, 'update'])->name('frontend-settings.update');
    Route::post('/frontend-settings/landing', function (\Illuminate\Http\Request $request) {
        \App\Models\SystemSetting::set('landing_template', $request->input('template', 'default'), 'frontend');
        \App\Models\SystemSetting::set('landing_hero_title', $request->input('hero_title', ''), 'frontend');
        \App\Models\SystemSetting::set('landing_hero_subtitle', $request->input('hero_subtitle', ''), 'frontend');
        \App\Models\SystemSetting::set('landing_hero_cta', $request->input('hero_cta', 'Get Started Free'), 'frontend');
        \App\Models\SystemSetting::set('landing_hero_cta_url', $request->input('hero_cta_url', url('/register')), 'frontend');
        return back()->with('success', 'Landing page settings saved.');
    })->name('frontend-settings.landing');
    Route::post('/frontend-settings/custom-code', function (\Illuminate\Http\Request $request) {
        $request->validate([
            'custom_landing_html' => 'nullable|string|max:50000',
            'head_code' => 'nullable|string|max:10000',
            'footer_code' => 'nullable|string|max:10000',
        ]);
        \App\Models\SystemSetting::set('custom_landing_html', $request->input('custom_landing_html', ''), 'frontend');
        \App\Models\SystemSetting::set('head_code', $request->input('head_code', ''), 'frontend');
        \App\Models\SystemSetting::set('footer_code', $request->input('footer_code', ''), 'frontend');
        return back()->with('success', 'Custom code saved.');
    })->name('frontend-settings.custom-code');

    Route::get('/audit-log', [\App\Http\Controllers\Admin\AuditLogController::class, 'index'])->name('audit-log');

    Route::resource('/coupons', \App\Http\Controllers\Admin\CouponController::class);

    Route::get('/payment-gateways', [\App\Http\Controllers\Admin\PaymentGatewayController::class, 'index'])->name('payment-gateways.index');
    Route::post('/payment-gateways/{gateway}/toggle', [\App\Http\Controllers\Admin\PaymentGatewayController::class, 'toggle'])->name('payment-gateways.toggle');
    Route::post('/payment-gateways/{gateway}/configure', [\App\Http\Controllers\Admin\PaymentGatewayController::class, 'configure'])->name('payment-gateways.configure');

    Route::get('/ai-providers', function () {
        return view('admin.ai-providers');
    })->name('ai-providers');

    Route::get('/settings', [\App\Http\Controllers\Admin\SettingsController::class, 'index'])->name('settings');

    // Theme Studio — runtime color customizer (frontend + admin + user dashboard)
    Route::get('/theme-studio', [\App\Http\Controllers\Admin\ThemeStudioController::class, 'index'])->name('theme-studio.index');
    Route::post('/theme-studio', [\App\Http\Controllers\Admin\ThemeStudioController::class, 'save'])->name('theme-studio.save');
    Route::delete('/theme-studio', [\App\Http\Controllers\Admin\ThemeStudioController::class, 'reset'])->name('theme-studio.reset');

    Route::post('/settings', [\App\Http\Controllers\Admin\SettingsController::class, 'update'])->name('settings.update');
    Route::post('/settings/test-smtp', [\App\Http\Controllers\Admin\SettingsController::class, 'testSmtp'])->name('settings.test-smtp');
    Route::post('/settings/test-storage', [\App\Http\Controllers\Admin\SettingsController::class, 'testStorage'])->name('settings.test-storage');
    Route::post('/settings/maintenance', [\App\Http\Controllers\Admin\SettingsController::class, 'toggleMaintenance'])->name('settings.maintenance');

    Route::post('/cms/bulk-destroy', [\App\Http\Controllers\Admin\CmsPageController::class, 'bulkDestroy'])->name('cms.bulk-destroy');
    Route::get('/cms/export', [\App\Http\Controllers\Admin\CmsPageController::class, 'export'])->name('cms.export');
    Route::resource('/cms', \App\Http\Controllers\Admin\CmsPageController::class);

    Route::get('/ai-usage', [\App\Http\Controllers\Admin\SettingsController::class, 'aiUsage'])->name('ai-usage');

    Route::get('/email-deliverability', [\App\Http\Controllers\Admin\SettingsController::class, 'emailDeliverability'])->name('email-deliverability');

    Route::get('/notifications', function () { return view('admin.notifications'); })->name('notifications');

    // ── Roles (full CRUD with Spatie Permission) ─────────────────
    Route::get('/roles/export', [\App\Http\Controllers\Admin\RoleController::class, 'export'])->name('roles.export');
    Route::post('/roles/import', [\App\Http\Controllers\Admin\RoleController::class, 'import'])->name('roles.import');
    Route::post('/roles/bulk-destroy', [\App\Http\Controllers\Admin\RoleController::class, 'bulkDestroy'])->name('roles.bulk-destroy');
    Route::resource('/roles', \App\Http\Controllers\Admin\RoleController::class);

    // ── Permissions (full CRUD) ───────────────────────────────
    Route::get('/permissions/export', [\App\Http\Controllers\Admin\PermissionController::class, 'export'])->name('permissions.export');
    Route::post('/permissions/import', [\App\Http\Controllers\Admin\PermissionController::class, 'import'])->name('permissions.import');
    Route::post('/permissions/bulk-destroy', [\App\Http\Controllers\Admin\PermissionController::class, 'bulkDestroy'])->name('permissions.bulk-destroy');
    Route::resource('/permissions', \App\Http\Controllers\Admin\PermissionController::class);

    // ── Security Audit Logs ───────────────────────────────────
    Route::get('/security-audit-logs/export', [\App\Http\Controllers\Admin\SecurityAuditLogController::class, 'export'])->name('security-audit-logs.export');
    Route::post('/security-audit-logs/bulk-destroy', [\App\Http\Controllers\Admin\SecurityAuditLogController::class, 'bulkDestroy'])->name('security-audit-logs.bulk-destroy');
    Route::resource('/security-audit-logs', \App\Http\Controllers\Admin\SecurityAuditLogController::class)->only(['index', 'show', 'destroy']);

    // ── Security Settings ─────────────────────────────────────
    Route::get('/security-settings', [\App\Http\Controllers\Admin\SecuritySettingsController::class, 'index'])->name('security-settings.index');
    Route::post('/security-settings', [\App\Http\Controllers\Admin\SecuritySettingsController::class, 'update'])->name('security-settings.update');

    // ── Blocked IPs ───────────────────────────────────────────
    Route::get('/blocked-ips', [BlockedIpController::class, 'index'])->name('blocked-ips.index');
    Route::post('/blocked-ips', [BlockedIpController::class, 'store'])->name('blocked-ips.store');
    Route::delete('/blocked-ips/{id}', [BlockedIpController::class, 'destroy'])->name('blocked-ips.destroy');

    // ── Blocked Locations ─────────────────────────────────────
    Route::get('/blocked-locations/export', [\App\Http\Controllers\Admin\BlockedLocationController::class, 'export'])->name('blocked-locations.export');
    Route::post('/blocked-locations/import', [\App\Http\Controllers\Admin\BlockedLocationController::class, 'import'])->name('blocked-locations.import');
    Route::post('/blocked-locations/bulk-destroy', [\App\Http\Controllers\Admin\BlockedLocationController::class, 'bulkDestroy'])->name('blocked-locations.bulk-destroy');
    Route::resource('/blocked-locations', \App\Http\Controllers\Admin\BlockedLocationController::class)->except(['show']);

    // ── Languages ────────────────────────────────────────────
    Route::get('/languages', [LanguageController::class, 'index'])->name('languages.index');
    Route::post('/languages', [LanguageController::class, 'store'])->name('languages.store');
    Route::put('/languages/{language}', [LanguageController::class, 'update'])->name('languages.update');
    Route::delete('/languages/{language}', [LanguageController::class, 'destroy'])->name('languages.destroy');
    Route::post('/languages/{language}/toggle', [LanguageController::class, 'toggleActive'])->name('languages.toggle');
    Route::post('/languages/{language}/default', [LanguageController::class, 'setDefault'])->name('languages.default');

    // ── Translations editor ──────────────────────────────────
    // In-app UI for editing lang/{code}.json without FTP. See
    // App\Http\Controllers\Admin\TranslationController.
    Route::get('/translations', [\App\Http\Controllers\Admin\TranslationController::class, 'index'])->name('translations.index');
    Route::post('/translations', [\App\Http\Controllers\Admin\TranslationController::class, 'save'])->name('translations.save');
    Route::post('/translations/import', [\App\Http\Controllers\Admin\TranslationController::class, 'import'])->name('translations.import');
    Route::get('/translations/export/{code}', [\App\Http\Controllers\Admin\TranslationController::class, 'export'])->name('translations.export');

    // ── Currencies ───────────────────────────────────────────
    Route::get('/currencies', [CurrencyController::class, 'index'])->name('currencies.index');
    Route::post('/currencies', [CurrencyController::class, 'store'])->name('currencies.store');
    Route::put('/currencies/{currency}', [CurrencyController::class, 'update'])->name('currencies.update');
    Route::delete('/currencies/{currency}', [CurrencyController::class, 'destroy'])->name('currencies.destroy');
    Route::post('/currencies/{currency}/toggle', [CurrencyController::class, 'toggleActive'])->name('currencies.toggle');
    Route::post('/currencies/{currency}/default', [CurrencyController::class, 'setDefault'])->name('currencies.default');

    // ── Temp Mail Admin ──────────────────────────────────
    Route::get('/temp-mail', [\App\Http\Controllers\Admin\TempMailDomainController::class, 'index'])->name('temp-mail.index');
    Route::post('/temp-mail', [\App\Http\Controllers\Admin\TempMailDomainController::class, 'store'])->name('temp-mail.store');
    Route::put('/temp-mail/{domain}', [\App\Http\Controllers\Admin\TempMailDomainController::class, 'update'])->name('temp-mail.update');
    Route::delete('/temp-mail/{domain}', [\App\Http\Controllers\Admin\TempMailDomainController::class, 'destroy'])->name('temp-mail.destroy');
    Route::post('/temp-mail/{domain}/toggle', [\App\Http\Controllers\Admin\TempMailDomainController::class, 'toggleActive'])->name('temp-mail.toggle');
    Route::post('/temp-mail/{domain}/test', [\App\Http\Controllers\Admin\TempMailDomainController::class, 'testConnection'])->name('temp-mail.test');
    Route::post('/temp-mail/{domain}/sync', [\App\Http\Controllers\Admin\TempMailDomainController::class, 'syncNow'])->name('temp-mail.sync');
    Route::post('/temp-mail/settings', [\App\Http\Controllers\Admin\TempMailDomainController::class, 'updateSettings'])->name('temp-mail.settings.update');

    // ── System Update (Super Admin only — guard inside controller via HasMiddleware) ──
    // Uploads a new release ZIP, runs Laravel migrations on top of the
    // existing DB (no data loss), preserves uploaded media + .env. Each
    // step is a JSON endpoint driven by the wizard at /admin/update.
    Route::get('/update', [\App\Http\Controllers\Admin\UpdateController::class, 'index'])->name('update.index');
    // Step 1 of the wizard — verify Envato purchase code. Runs through
    // the same AddonLicenseService as /admin/addons; either page can
    // complete the verification and the other will pick up the file.
    Route::post('/update/license', [\App\Http\Controllers\Admin\UpdateController::class, 'license'])->name('update.license');
    Route::post('/update/backup', [\App\Http\Controllers\Admin\UpdateController::class, 'backup'])->name('update.backup');
    Route::post('/update/upload', [\App\Http\Controllers\Admin\UpdateController::class, 'upload'])->name('update.upload');
    Route::post('/update/apply', [\App\Http\Controllers\Admin\UpdateController::class, 'apply'])->name('update.apply');
    Route::post('/update/migrate', [\App\Http\Controllers\Admin\UpdateController::class, 'migrate'])->name('update.migrate');
    Route::post('/update/finalize', [\App\Http\Controllers\Admin\UpdateController::class, 'finalize'])->name('update.finalize');
    Route::post('/update/rollback', [\App\Http\Controllers\Admin\UpdateController::class, 'rollback'])->name('update.rollback');
});

// Chat widget JS — served via PHP so we can set short cache headers
// (5 minutes instead of the default static-asset day+ that browsers
// happily hold). This lets a deploy of v1.js propagate to every
// embedding site within ~5 min, instead of forcing the seller to
// bump a `?v=N` query string on every customer's embed snippet.
//
// CRITICAL — TWO URLS, ONE FILE, DIFFERENT CACHING BEHAVIOR:
//
//   /widget/v1.js   → STATIC. Apache serves public/widget/v1.js directly,
//                     bypassing Laravel entirely. No Cache-Control header,
//                     so browsers cache it for hours/days at their own
//                     discretion. Kept for backwards compatibility with
//                     embed snippets already deployed in the wild. Updates
//                     to this URL require manual `?v=N` cache busting.
//
//   /widget.js      → DYNAMIC. No matching static file → Apache routes
//                     through index.php → this route fires → response
//                     includes Cache-Control: max-age=300 and ETag.
//                     This is the RECOMMENDED URL for new embeds.
//                     Future deploys propagate to visitors within 5 min
//                     automatically; no `?v=N` bumping needed.
//
// We previously registered a route ALSO at /widget/v1.js hoping to apply
// cache headers to it — but Laravel never received those requests
// because the static file at public/widget/v1.js intercepts them at the
// Apache layer (the .htaccess rule that routes to index.php only fires
// when REQUEST_FILENAME doesn't exist as a file). Hence the new URL.
//
// Excluded from CSRF (read-only, public asset).
Route::get('/widget.js', function () {
    $path = public_path('widget/v1.js');
    if (!file_exists($path)) {
        abort(404, 'Widget script not deployed.');
    }
    $contents = file_get_contents($path);

    // ETag based on file content hash — lets browsers send
    // If-None-Match on subsequent requests. If the file hasn't changed
    // since their last fetch, we return 304 Not Modified (no body),
    // which is the cheapest possible "still valid" response.
    $etag = '"' . md5($contents) . '"';

    if (request()->header('If-None-Match') === $etag) {
        return response('', 304)->withHeaders(['ETag' => $etag]);
    }

    return response($contents, 200)->withHeaders([
        'Content-Type'           => 'application/javascript; charset=utf-8',
        // 5 min browser + CDN cache. Long enough to absorb the request
        // load from a popular embedder, short enough that a deploy
        // propagates to visitors within minutes without manual cache
        // busting on every customer's embed snippet.
        'Cache-Control'          => 'public, max-age=300, s-maxage=300, must-revalidate',
        'ETag'                   => $etag,
        // Lets old browsers / CDNs validate freshness with If-Modified-Since
        'Last-Modified'          => gmdate('D, d M Y H:i:s', filemtime($path)) . ' GMT',
        // Defense-in-depth: prevent browsers from sniffing the MIME type
        // and executing the response as anything other than JavaScript.
        'X-Content-Type-Options' => 'nosniff',
        // Tag the response so a customer inspecting Network tab can see
        // they're hitting the dynamic route and not the static file.
        // Strips on production via SecurityHeaders middleware in prod
        // builds, so this is dev-friendly only.
        'X-Widget-Source'        => 'dynamic',
    ]);
})->name('widget.js');

// Payment gateway callbacks and webhooks (no auth — verified by gateway signatures)
Route::any('/payment/callback/{gateway}', [\App\Http\Controllers\PaymentCallbackController::class, 'handle'])
    ->name('payment.callback')
    ->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);

Route::post('/payment/webhook/{gateway}', [\App\Http\Controllers\PaymentCallbackController::class, 'webhook'])
    ->name('payment.webhook')
    ->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);

// Stripe webhook (no auth — verified by Stripe signature; excluded from CSRF)
Route::post('/stripe/webhook', [\App\Http\Controllers\Webhooks\StripeWebhookController::class, 'handle'])
    ->name('stripe.webhook')
    ->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);

// PayPal webhook (no auth — verified by PayPal signature; excluded from CSRF)
// URL the admin pastes into PayPal Developer Dashboard → My Apps → Webhooks:
//     https://{APP_URL}/paypal/webhook
Route::post('/paypal/webhook', [\App\Http\Controllers\Webhooks\PayPalWebhookController::class, 'handle'])
    ->name('paypal.webhook')
    ->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);

// Webhook endpoints (no auth — verified by provider signatures; CSRF exempted)
Route::prefix('api/webhooks')->name('webhooks.')
    ->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class])
    ->middleware('throttle:600,1')
    ->group(function () {
    Route::match(['get', 'post'], '/whatsapp', [\App\Http\Controllers\Webhooks\WhatsAppWebhookController::class, 'handle'])->name('whatsapp');
    Route::post('/twilio/incoming', [\App\Http\Controllers\Webhooks\TwilioWebhookController::class, 'incoming'])->name('twilio.incoming');
    Route::post('/twilio/status', [\App\Http\Controllers\Webhooks\TwilioWebhookController::class, 'status'])->name('twilio.status');
    Route::post('/telegram', [\App\Http\Controllers\Webhooks\TelegramWebhookController::class, 'handle'])->name('telegram');
    Route::post('/slack/events', [\App\Http\Controllers\Webhooks\SlackWebhookController::class, 'events'])->name('slack.events');
    Route::post('/slack/commands', [\App\Http\Controllers\Webhooks\SlackWebhookController::class, 'commands'])->name('slack.commands');
    Route::post('/slack/interactions', [\App\Http\Controllers\Webhooks\SlackWebhookController::class, 'interactions'])->name('slack.interactions');
    Route::post('/zapier/{webhookToken}', [\App\Http\Controllers\Webhooks\ZapierWebhookController::class, 'handle'])->name('zapier');
});

// Email tracking (no auth — UUID-based lookups, rate limited to prevent inflation)
Route::middleware('throttle:600,1')->group(function () {
    Route::get('/api/track/open/{messageId}', [\App\Http\Controllers\TrackingController::class, 'open']);
    Route::get('/api/track/click/{messageId}', [\App\Http\Controllers\TrackingController::class, 'click']);
    Route::get('/api/track/campaign/{recipientId}/open', [\App\Http\Controllers\TrackingController::class, 'campaignOpen']);
    Route::get('/api/track/campaign/{recipientId}/click', [\App\Http\Controllers\TrackingController::class, 'campaignClick']);
});
Route::get('/api/unsubscribe/{contactId}', [\App\Http\Controllers\TrackingController::class, 'unsubscribe'])
    ->name('unsubscribe')
    ->where('contactId', '[0-9a-f\-]+'); // Accept both integer IDs and UUIDs

// Public form-submission endpoint — drives workflow `form_submitted` trigger.
// No auth: the workflow id itself is the "token" (public-by-design since the
// URL is embedded on marketing pages). Abuse protection comes from the
// per-IP rate limit in FormSubmissionController + honeypot middleware.
Route::get('/form/{workflow}', [\App\Http\Controllers\FormSubmissionController::class, 'show'])
    ->where('workflow', '[0-9]+')->name('form.show');
Route::post('/form/{workflow}', [\App\Http\Controllers\FormSubmissionController::class, 'submit'])
    ->where('workflow', '[0-9]+')->name('form.submit');

// Public inbound webhook — drives workflow `webhook_received` trigger.
// Path contains a random per-workflow token so only the workflow owner who
// copies the URL can fire it. Accepts any JSON body; fields become
// triggerData for the workflow.
Route::post('/workflows/hook/{token}', [\App\Http\Controllers\WorkflowWebhookController::class, 'receive'])
    ->where('token', '[A-Za-z0-9]+')->name('workflow.webhook');

// Health check endpoint (no auth — for monitoring)
Route::get('/api/health', function () {
    try {
        \DB::connection()->getPdo();
        $dbOk = true;
    } catch (\Exception $e) {
        $dbOk = false;
    }

    $status = $dbOk ? 'healthy' : 'degraded';
    $code = $dbOk ? 200 : 503;

    return response()->json([
        'status' => $status,
        'database' => $dbOk ? 'connected' : 'disconnected',
        'cache' => cache()->has('__health_check__') || cache()->put('__health_check__', true, 10) ? 'ok' : 'error',
        'timestamp' => now()->toIso8601String(),
    ], $code);
});

// Hidden cache clear route (no auth — use this when locked out by rate limiter)
Route::get('/clear-cache-x7k9m', function () {
    // Delete cached config file directly (in case artisan can't)
    @unlink(base_path('bootstrap/cache/config.php'));
    @unlink(base_path('bootstrap/cache/routes-v7.php'));

    \Artisan::call('cache:clear');
    \Artisan::call('config:clear');
    \Artisan::call('route:clear');
    \Artisan::call('view:clear');

    // Also create storage link if missing
    try { \Artisan::call('storage:link'); } catch (\Exception $e) {}

    return response()->json([
        'status' => 'success',
        'message' => 'All caches cleared, storage linked.',
        'app_url' => config('app.url'),
        'storage_url' => config('filesystems.disks.public.url'),
        'timestamp' => now()->toIso8601String(),
    ]);
});

// Live chat widget API (authenticated by workspace token, rate limited to prevent abuse)
// WidgetCors (global middleware) allows any origin for these routes.
// CSRF excluded — widget authenticates via Bearer token, not cookies.
Route::prefix('api/widget')
    ->middleware(['throttle:600,1'])
    ->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class])
    ->group(function () {
    Route::post('/start-chat', [\App\Http\Controllers\Widget\ChatWidgetController::class, 'startChat']);
    Route::post('/send-message', [\App\Http\Controllers\Widget\ChatWidgetController::class, 'sendMessage']);
    Route::get('/history', [\App\Http\Controllers\Widget\ChatWidgetController::class, 'history']);
    Route::post('/rate', [\App\Http\Controllers\Widget\ChatWidgetController::class, 'rate']);
    Route::post('/close', [\App\Http\Controllers\Widget\ChatWidgetController::class, 'close']);
    Route::get('/status', [\App\Http\Controllers\Widget\ChatWidgetController::class, 'status']);
});
