<script lang="ts">
/** @type {import('./$types').LayoutData} */

let log_lvl: number = 0;

// *** Import Svelte specific
import { onMount, untrack } from 'svelte';
import { goto, invalidateAll } from '$app/navigation';

import '../app.css';

// *** Import other supporting libraries
import {
    RefreshCw,
    WifiOff,
    ChevronDown,
} from '@lucide/svelte';

// Highlight JS
import hljs from 'highlight.js/lib/core';
import 'highlight.js/styles/github-dark.css';

import { browser } from '$app/environment';
import { online } from 'svelte/reactivity/window';
import xml from 'highlight.js/lib/languages/xml'; // for HTML
import css from 'highlight.js/lib/languages/css';
import javascript from 'highlight.js/lib/languages/javascript';
import typescript from 'highlight.js/lib/languages/typescript';

hljs.registerLanguage('xml', xml); // for HTML
hljs.registerLanguage('css', css);
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('typescript', typescript);

// *** Import Aether specific variables and functions
// import Analytics from '$lib/app_components/e_app_analytics.svelte';
import {
    ae_loc,
    ae_sess,
    ae_api,
    slct,
    slct_trigger,
    ae_auth_error
} from '$lib/stores/ae_stores';
import { LoaderCircle } from '@lucide/svelte';

import E_app_debug_menu from '$lib/app_components/e_app_debug_menu.svelte';
import E_app_sys_bar from '$lib/app_components/e_app_sys_bar.svelte';
import { pwa_install } from '$lib/elements/pwa_install.svelte';

interface Props {
    data: any;
    children?: import('svelte').Snippet;
}

let { data, children }: Props = $props();

// Remove the pre-JS loader from app.html as soon as Svelte mounts.
// The existing is_hydrating overlay takes over from here.
onMount(() => {
    document.getElementById('ae_loader')?.remove();
});

// STABLE DERIVATION: Using prop data directly to avoid store loops
let ae_acct = $derived(data[data.account_id]);

let flag_clear_idb: boolean = $state(false);
let flag_clear_local: boolean = $state(false);
let flag_clear_sess: boolean = $state(false);
let flag_reload: boolean = $state(false);
let flag_hard_reload: boolean = $state(false);

let flag_new_ver: boolean = $state(false);
let flag_expired: boolean = $state(false);
let flag_denied: boolean = $state(false);

// Connection Status
let is_offline = $derived(browser && online.current === false);
let api_unreachable = $derived($ae_loc?.account_id === 'ghost');
let api_error_msg = $derived($ae_loc?.account_name || 'API Server Unreachable');
let show_connection_details = $state(true);

let last_reload_time = 0;
let sw_heal_in_flight = false;

// Shallow equality guard — avoids triggering Svelte store updates when the merged
// object is functionally identical to the current one. Comparing JSON.stringify on
// large objects like $ae_loc (site config, device info, flags) is expensive and
// runs on every navigation. Key-by-key identity check is O(n keys), not O(n chars).
function shallow_equal(
    a: Record<string, any>,
    b: Record<string, any>
): boolean {
    const keys_a = Object.keys(a);
    const keys_b = Object.keys(b);
    if (keys_a.length !== keys_b.length) return false;
    for (const k of keys_a) {
        if (a[k] !== b[k]) return false;
    }
    return true;
}

// 1. CONSOLIDATED SYNC EFFECT (One single point of entry for store updates)
$effect(() => {
    if (!browser || !ae_acct) return;

    untrack(() => {
        if (log_lvl > 1) console.log('ROOT: Running Sync Effect');

        // A. Sync Global Stores
        const current_api = $ae_api;
        const new_api = { ...current_api, ...(ae_acct.api || {}) };
        // Deep check for JWT specifically to avoid extra triggers
        if (current_api.jwt !== $ae_loc.jwt) new_api.jwt = $ae_loc.jwt;
        if (!shallow_equal(current_api, new_api)) {
            $ae_api = new_api;
        }

        const current_loc = $ae_loc;
        const new_loc = { ...current_loc, ...(ae_acct.loc || {}) };
        // Restore structural integrity if clobbered
        if (!new_loc.sys_menu)
            new_loc.sys_menu = {
                hide: false,
                hide_access_type: false,
                expand_access_type: false,
                hide_edit_mode: false,
                expand_edit_mode: false,
                hide_user: false,
                expand_user: false,
                hide_theme: false,
                expand_theme: false,
                hide_cfg: false,
                expand_cfg: false
            };
        if (!new_loc.debug_menu)
            new_loc.debug_menu = { hide: false, expand: false };

        if (!shallow_equal(current_loc, new_loc)) {
            $ae_loc = new_loc;
        }

        const current_slct = $slct;
        const new_slct = { ...current_slct, ...(ae_acct.slct || {}) };
        if (!shallow_equal(current_slct, new_slct)) {
            $slct = new_slct;
        }

        // B. Version & Sanity Check
        if (new_loc.ver && $ae_sess.ver && new_loc.ver !== $ae_sess.ver) {
            if (!flag_new_ver) {
                console.log(
                    'ROOT: Version mismatch detected; clearing stale service workers and caches before reload'
                );
                flag_new_ver = true;
                flag_hard_reload = true;
                flag_reload = true;
            }
        }

        // C. Access Check (Idempotent)
        if (new_loc.site_access_key || new_loc.site_domain_access_key) {
            const key = new_loc.allow_access;
            const match =
                new_loc.site_access_key === key ||
                new_loc.site_domain_access_key === key;
            if (!match && !new_loc.trusted_access) {
                if (new_loc.allow_access !== false)
                    $ae_loc.allow_access = false;
                if (!flag_denied) flag_denied = true;
            }
        } else if (new_loc.allow_access !== true) {
            $ae_loc.allow_access = true;
        }

        // D. Theme Initialization (Once)
        if (!current_loc.theme_mode) {
            if (window.matchMedia('(prefers-color-scheme: dark)').matches)
                $ae_loc.theme_mode = 'dark';
            else $ae_loc.theme_mode = 'light';
        }
    });
});

// 2. RELOAD THROTTLE EFFECT
$effect(() => {
    if (!browser) return;

    if (flag_reload) {
        untrack(() => {
            const now = Date.now();
            if (now - last_reload_time < 10000) {
                console.warn(
                    'ROOT: Critical loop prevention - reload suppressed'
                );
                flag_reload = false;
                return;
            }
            last_reload_time = now;
            flag_reload = false;

            if (flag_clear_local) clear_local();
            if (flag_clear_sess) clear_sess();

            if (flag_hard_reload) {
                flag_hard_reload = false;
                console.log('ROOT: Executing hard reload after service worker heal');
                void clear_stale_service_worker_state().finally(() => {
                    location.reload();
                });
                return;
            }

            console.log('ROOT: Executing throttled reload');
            invalidateAll();
        });
    }
});

// 3. UI STATE & THEME DOM EFFECT
let is_hydrating = $state(true);
$effect(() => {
    if (!browser) return;

    // Theme name — controls which Skeleton color palette is active
    document.documentElement.setAttribute(
        'data-theme',
        $ae_loc?.theme_name ?? 'nouveau'
    );

    // Dark/light class — controls Tailwind v4 class-based dark mode variant.
    // @custom-variant dark in app.css registers this; without it the class does nothing.
    if ($ae_loc?.theme_mode === 'dark') {
        document.documentElement.classList.add('dark');
        document.documentElement.classList.remove('light');
    } else {
        document.documentElement.classList.add('light');
        document.documentElement.classList.remove('dark');
    }

    // Font size mode — cycles default | larger | smaller.
    // CSS classes are defined in app.css; no class = browser default (16px).
    document.documentElement.classList.remove(
        'font-size-larger',
        'font-size-smaller'
    );
    if ($ae_loc?.font_size_mode === 'larger') {
        document.documentElement.classList.add('font-size-larger');
    } else if ($ae_loc?.font_size_mode === 'smaller') {
        document.documentElement.classList.add('font-size-smaller');
    }

    // Hydration overlay timer
    if ($ae_loc?.account_id) {
        const timer = setTimeout(() => (is_hydrating = false), 500);
        return () => clearTimeout(timer);
    }
});

function clear_idb() {
    indexedDB.deleteDatabase('ae_archives_db');
    indexedDB.deleteDatabase('ae_core_db');
    indexedDB.deleteDatabase('ae_events_db');
    indexedDB.deleteDatabase('ae_journals_db');
    indexedDB.deleteDatabase('ae_posts_db');
    indexedDB.deleteDatabase('ae_sponsorships_db');
}

function clear_local() {
    localStorage.clear();
}

function clear_sess() {
    sessionStorage.clear();
}

// CRITICAL — all three storage layers must be cleared together or the fix is incomplete.
//
// The service worker (SW) maintains its own Cache Storage that is entirely separate from
// IndexedDB, localStorage, and sessionStorage. The SW uses Cache Storage to serve the
// app's JS/CSS bundles and HTML shell directly from disk — bypassing the network entirely
// for those assets. This means:
//
//   Clearing IDB + localStorage ONLY:
//     → SW is still registered and still serving old JS bundles from its cache.
//     → Next reload: SW intercepts, returns OLD cached JS → user is still stuck.
//
//   Clearing SW registrations ONLY:
//     → Cache Storage still contains old entries. A newly registered SW may serve them
//       before it rebuilds the cache from the network, causing a transient stale state.
//
//   Clearing BOTH SW registrations AND Cache Storage, THEN IDB/localStorage:
//     → Next reload: no SW intercepts → fresh HTML from server → fresh JS bundle
//       filenames → fresh JS loaded → user is on current code. ✅
//
// This function is called by the version-mismatch detection path (flag_hard_reload).
// All manual cache-clearing buttons in the app must follow the same order.
async function clear_stale_service_worker_state() {
    if (!browser || sw_heal_in_flight) return;
    sw_heal_in_flight = true;

    try {
        if ('serviceWorker' in navigator) {
            const registrations = await navigator.serviceWorker.getRegistrations();
            for (const registration of registrations) {
                await registration.unregister();
            }
        }
    } catch (err) {
        console.warn('ROOT: Service worker unregister failed during heal:', err);
    }

    try {
        if ('caches' in window) {
            const cache_keys = await caches.keys();
            for (const cache_key of cache_keys) {
                await caches.delete(cache_key);
            }
        }
    } catch (err) {
        console.warn('ROOT: Cache cleanup failed during heal:', err);
    } finally {
        sw_heal_in_flight = false;
    }
}

// 4. EXTERNAL INTERFACES EFFECT
$effect(() => {
    if (!browser) return;

    // SW update → page reload bridge. DO NOT REMOVE.
    //
    // How SW updates propagate (all three steps are required):
    //   1. service-worker.js calls skipWaiting() on install → new SW activates immediately
    //      instead of waiting for all existing tabs to close.
    //   2. service-worker.js calls clients.claim() on activate → new SW takes control of
    //      all currently open tabs without requiring a navigation.
    //   3. THIS LISTENER: when the new SW takes control, `controllerchange` fires on every
    //      open tab. We reload so the tab runs the new JS bundle rather than keeping the
    //      old JS that was already loaded in memory.
    //
    // Without step 3, users can be stuck on old code indefinitely after a deployment —
    // even though the SW correctly updated under them. The tab shows no error; it just
    // silently runs whatever JS it had at the time the SW changed. This caused IDAA and
    // other users to run stale code for weeks at a time across multiple deployments.
    // Added 2026-06-22 to close this gap.
    // Capture before registering — if null, this is first activation (fresh page load)
    // and the reload is unnecessary since assets were already fetched without the SW.
    // Guard must be part of the expression: navigator.serviceWorker doesn't exist on
    // browsers without SW support, so accessing .controller before checking would throw.
    const had_controller = 'serviceWorker' in navigator && !!navigator.serviceWorker.controller;
    const on_controller_change = () => {
        if (had_controller) window.location.reload();
    };
    if ('serviceWorker' in navigator) {
        navigator.serviceWorker.addEventListener('controllerchange', on_controller_change);
    }

    // SW activate → reload bridge (parallel path to controllerchange above).
    // WHY: controllerchange only fires in JS that has the listener registered.
    // Tabs stuck on very old JS (pre-June-2026 builds) never set up that listener,
    // so the new SW's postMessage from its activate event is the only way to reach
    // them. The same had_controller guard prevents a spurious reload on fresh page
    // loads where no SW was previously controlling the tab.
    const on_sw_message = (event: MessageEvent) => {
        if (event.data?.type === 'SW_ACTIVATE_RELOAD' && had_controller) {
            window.location.reload();
        }
    };
    if ('serviceWorker' in navigator) {
        navigator.serviceWorker.addEventListener('message', on_sw_message);
    }

    // Initialise PWA install prompt listener (captures beforeinstallprompt early).
    pwa_install.init();

    // Save DS to local
    let ae_ds = ae_acct?.ds;
    if (ae_ds) {
        for (let [key, value] of Object.entries(ae_ds)) {
            localStorage.setItem(`ae_ds__${key}`, JSON.stringify(value));
        }
    }

    // Message Bridge
    const handler = (event: MessageEvent) => {
        if (event.data.type === 'api_download_blob') {
            $ae_sess.api_download_kv[event.data.task_id] = { ...event.data };
        } else if (event.data.type === 'api_post_json_form') {
            $ae_sess.api_upload_kv[event.data.task_id] = { ...event.data };
        }
    };
    window.addEventListener('message', handler);

    // Iframe Detection
    let iframe = data.url.searchParams.get('iframe');
    // let iframe = $derived(data.url.searchParams.get('iframe'));

    if (iframe === 'true') {
        $ae_loc.iframe = true;
        // Hide the AE system bar by default in iframe embeds — it's nav chrome
        // the host page doesn't need. Trusted admins can override with show_menu=true
        // to access the menu while testing an embedded page (e.g. video_conferences).
        $ae_loc.sys_menu.hide = true;
    } else if (iframe === 'false') {
        $ae_loc.iframe = false;
    }

    // show_menu=true — override to show the AE system bar even inside an iframe.
    // Intended for trusted/admin users who need menu access while testing an embed.
    // hide_menu=true — explicitly hide the bar outside of iframe contexts.
    // const menu_override = data.url.searchParams.get('show_menu');
    // if (menu_override === 'true') $ae_loc.sys_menu.hide = false;
    // else if (data.url.searchParams.get('hide_menu') === 'true')
    //     $ae_loc.sys_menu.hide = true;

    const menu_override = data.url.searchParams.get('show_menu');
    if (menu_override === 'true') {
        $ae_loc.sys_menu.hide = false;
    } else if (data.url.searchParams.get('hide_menu') === 'true') {
        $ae_loc.sys_menu.hide = true;
    } else if (data.url.searchParams.get('iframe') === 'true') {
        $ae_loc.sys_menu.hide = true;
    } else if (data.url.searchParams.get('iframe') === 'false') {
        $ae_loc.sys_menu.hide = false;
    }
    // console.log('iframe:', $ae_loc.iframe, 'sys_menu.hide:', $ae_loc.sys_menu.hide);

    // Theme URL params — ?theme=AE_Firefly_SteelBlue&theme_mode=dark
    // Applied once on load, then silently removed from the URL (no history entry).
    const url_theme = data.url.searchParams.get('theme');
    const url_theme_mode = data.url.searchParams.get('theme_mode');
    if (url_theme || url_theme_mode) {
        if (url_theme) {
            // Mark that the user (or URL) explicitly chose a theme so site defaults
            // won't overwrite it later on subsequent loads.
            ae_loc.update((l: any) => ({ ...l, theme_name: url_theme, user_theme_selected: true }));
        }
        if (url_theme_mode === 'light' || url_theme_mode === 'dark') {
            ae_loc.update((l: any) => ({ ...l, theme_mode: url_theme_mode }));
        }
        const clean_url = new URL(data.url.href);
        clean_url.searchParams.delete('theme');
        clean_url.searchParams.delete('theme_mode');
        goto(clean_url.pathname + clean_url.search + clean_url.hash, {
            replaceState: true,
            noScroll: true,
            keepFocus: true
        });
    }

    // Electron Detection
    if ((window as any).native_app || (window as any).aetherNative) {
        if (!$ae_loc.is_native) $ae_loc.is_native = true;
    }

    return () => {
        window.removeEventListener('message', handler);
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.removeEventListener('controllerchange', on_controller_change);
            navigator.serviceWorker.removeEventListener('message', on_sw_message);
        }
    };
});

// 5. SESSION EXPIRED EFFECT — watches ae_auth_error and raises the banner when the API signals 401/403.
// Guards on $ae_loc.jwt so that unauthenticated first-loads (no stored JWT) never trigger it —
// 401s are expected on first visit and should not look like a session expiry.
$effect(() => {
    if ($ae_auth_error?.type === 'expired') {
        untrack(() => {
            if ($ae_loc?.jwt) flag_expired = true;
        });
    }
});
</script>

<svelte:head>
    <link rel="stylesheet" href={ae_acct?.loc?.site_style_href} />
</svelte:head>

{#if browser && (is_offline || api_unreachable)}
    {#if show_connection_details}
        <!-- Expanded banner -->
        <div
            class="preset-filled-error-200-800 fixed top-0 right-0 left-0 z-100 flex items-center justify-center gap-4 p-4 text-center shadow-2xl print:hidden">
            <WifiOff size="1.2em" class="shrink-0 opacity-70" />
            <div class="flex flex-col items-center gap-0.5">
                <span class="text-xl font-bold leading-tight">
                    {is_offline ? 'Device Offline' : api_error_msg}
                </span>
                <span class="text-xs opacity-75">
                    {#if is_offline}
                        Cached event data is still available for search and print · Edits require network
                    {:else}
                        The Aether server could not be reached — check your connection or contact support
                    {/if}
                </span>
            </div>
            <button
                class="btn btn-sm preset-tonal-surface"
                onclick={() => window.location.reload()}>
                <RefreshCw size="1em" class="opacity-60" /> Retry
            </button>
            <button
                class="btn btn-sm preset-tonal-surface"
                title="Collapse"
                onclick={() => { show_connection_details = false; }}>
                <ChevronDown size="1em" />
            </button>
        </div>
    {:else}
        <!-- Collapsed indicator — small chip, top-right corner -->
        <button
            class="preset-filled-error-200-800 fixed top-2 right-2 z-100 flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-bold shadow-lg print:hidden"
            title={is_offline ? 'Device Offline — click to expand' : `${api_error_msg} — click to expand`}
            onclick={() => { show_connection_details = true; }}>
            <WifiOff size="0.9em" class="shrink-0" />
            {is_offline ? 'Offline' : 'API Error'}
        </button>
    {/if}
{/if}

{#if browser && flag_expired}
    <div
        class="preset-filled-warning-500 fixed top-0 right-0 left-0 z-50 flex items-center justify-between gap-4 px-4 py-2 shadow-xl print:hidden">
        <p class="text-sm font-semibold">
            Your session has expired. Please reload or sign in again.
        </p>
        <div class="flex shrink-0 items-center gap-2">
            <button
                class="btn btn-sm preset-tonal-surface"
                onclick={() => window.location.reload()}>
                    <RefreshCw size="1em" class="opacity-60" /> Reload
                </button>
            <button
                class="btn btn-sm preset-outlined-surface"
                onclick={() => {
                    flag_expired = false;
                    ae_auth_error.set({ type: null, ts: null });
                }}>✕</button>
        </div>
    </div>
{/if}

{#if browser && $ae_loc?.allow_access}
    {@render children?.()}

    {#if is_hydrating}
        <div
            class="bg-surface-50/80 dark:bg-surface-900/80 fixed inset-0 z-99 flex flex-col items-center justify-center backdrop-blur-sm transition-opacity duration-500 print:hidden">
            <div
                class="preset-filled-surface-100-900 flex flex-col items-center gap-4 rounded-2xl p-8 shadow-2xl">
                <LoaderCircle
                    size="3rem"
                    class="text-primary-500 animate-spin" />
                <div class="text-center text-xl font-bold">
                    <!-- Hydrating Aether... -->
                    Loading Aether data...
                </div>
            </div>
        </div>
    {/if}
{:else if browser && flag_denied}
    <div class="flex h-screen flex-col items-center justify-center gap-6 p-8">
        <h1 class="text-error-500 text-4xl font-black">Access Denied</h1>
        <button
            class="btn preset-filled-primary"
            onclick={() => window.location.reload()}>Reload</button>
    </div>
{:else if browser}
    <div class="flex h-screen items-center justify-center">
        <LoaderCircle size="2.5rem" class="animate-spin opacity-20" />
    </div>
{/if}

{#if browser && (!$ae_loc?.iframe || !$ae_loc?.sys_menu?.hide || $ae_loc?.administrator_access || ($ae_loc?.trusted_access && $ae_loc.edit_mode))}
    <!-- print:hidden wrapper: sys/debug menus are fixed overlays — must not appear on printed pages -->
    <div class="print:hidden">
        <E_app_sys_bar
            {data}
            bind:hide={$ae_loc.sys_menu.hide}
            bind:expand={$ae_sess.sys_menu.expand} />

        <!-- You must be in Edit Mode to initially see the Debug expand button. Once expanded, you can toggle the Edit Mode while still seeing the expanded Debug content. -->
        {#if $ae_loc.edit_mode || $ae_loc.debug_menu.expand}
            <E_app_debug_menu
                bind:hide={$ae_loc.debug_menu.hide}
                bind:expand={$ae_loc.debug_menu.expand} />
        {/if}
    </div>
{/if}
