<script lang="ts">
    import { onMount, onDestroy } from 'svelte';
    import { goto } from '$app/navigation';
    import { page } from '$app/stores';
    import { logout } from '$lib/api/auth';
    import { authConfig } from '$lib/auth-config.svelte';
    import { preferences } from '$lib/preferences.svelte';
    import { session } from '$lib/session.svelte';
    import { theme } from '$lib/theme.svelte';
    import AppBar from '$lib/components/AppBar.svelte';
    import BottomNav, { type BottomNavTab } from '$lib/components/BottomNav.svelte';
    import NavProgress from '$lib/components/NavProgress.svelte';
    import Toaster from '$lib/components/Toaster.svelte';
    import IconButton from '$lib/components/IconButton.svelte';
    import Upload from '@lucide/svelte/icons/upload';
    import UserCircle from '@lucide/svelte/icons/user-circle';
    import Bookmark from '@lucide/svelte/icons/bookmark';
    import FolderOpen from '@lucide/svelte/icons/folder-open';
    import LogOut from '@lucide/svelte/icons/log-out';
    import Shield from '@lucide/svelte/icons/shield';
    import House from '@lucide/svelte/icons/house';
    import BookOpen from '@lucide/svelte/icons/book-open';
    import User from '@lucide/svelte/icons/user';
    import '$lib/styles/tokens.css';

    let { children, data } = $props();
    let loggingOut = $state(false);
    let headerEl: HTMLElement | undefined = $state();
    let appBarEl: HTMLElement | undefined = $state();
    let bottomNavEl: HTMLElement | undefined = $state();

    // Static-route title map. Dynamic pages (manga / author / collection /
    // chapter) override this via their own <svelte:head><title>, since the
    // title depends on data the layout doesn't have. Routes omitted here
    // (notably the dynamic ones) fall through to the bare brand and rely
    // on the page to set the descriptive form.
    const STATIC_TITLES: Record<string, string> = {
        '/': 'Mangalord',
        '/login': 'Mangalord | Login',
        '/register': 'Mangalord | Register',
        '/upload': 'Mangalord | Upload',
        '/bookmarks': 'Mangalord | Bookmarks',
        '/collections': 'Mangalord | Collections',
        '/library': 'Mangalord | Library',
        '/profile': 'Mangalord | Profile',
        '/profile/account': 'Mangalord | Account',
        '/profile/bookmarks': 'Mangalord | Bookmarks',
        '/profile/collections': 'Mangalord | Collections',
        '/profile/history': 'Mangalord | Reading history',
        '/profile/preferences': 'Mangalord | Preferences',
        '/admin': 'Mangalord | Admin',
        '/admin/mangas': 'Mangalord | Admin · Mangas',
        '/admin/users': 'Mangalord | Admin · Users',
        '/admin/system': 'Mangalord | Admin · System'
    };

    const layoutTitle = $derived(STATIC_TITLES[$page.route?.id ?? ''] ?? 'Mangalord');

    // Mobile bottom-nav tabs. Library is its own wrapper at /library
    // (Phase 5) hosting a SegmentedControl over Bookmarks / Collections /
    // History; the older top-level /bookmarks + /collections routes stay
    // active for desktop users and as deep-link targets, so they remain
    // in the `match` set. Upload replaced the placeholder Browse tab
    // (which used to share Home's destination) with a real action; the
    // curated-feed split is deferred until there's curated content to
    // show.
    const MOBILE_TABS: BottomNavTab[] = [
        { label: 'Home', href: '/', icon: House, match: [] },
        { label: 'Upload', href: '/upload', icon: Upload, match: ['/upload'] },
        {
            label: 'Library',
            href: '/library',
            icon: BookOpen,
            match: [
                '/library',
                '/bookmarks',
                '/collections',
                '/profile/bookmarks',
                '/profile/collections',
                '/profile/history'
            ]
        },
        {
            label: 'Account',
            href: '/profile/account',
            icon: User,
            // `/admin` keeps the Account tab highlighted while an admin is in
            // the admin area — its only mobile entry point is the Account hub.
            match: ['/profile', '/profile/preferences', '/admin']
        }
    ];

    // Auth-gate and reader routes hide the mobile chrome entirely — the
    // bottom nav would be visual clutter on a login form and would fight
    // the reader's own chrome for screen real-estate. The desktop header
    // is unaffected; it continues to follow the reader fullscreen slide
    // transform.
    const HIDE_MOBILE_CHROME_PATHS = new Set(['/login', '/register']);
    const HIDE_MOBILE_CHROME_ROUTES = new Set([
        '/manga/[id]',
        '/manga/[id]/chapter/[chapter_id]'
    ]);
    const showMobileChrome = $derived.by(() => {
        if (HIDE_MOBILE_CHROME_PATHS.has($page.url.pathname)) return false;
        if (HIDE_MOBILE_CHROME_ROUTES.has($page.route?.id ?? '')) return false;
        return true;
    });

    // Seed authConfig from the universal layout load. $effect keeps
    // the store in sync if `data` is replaced by a subsequent layout
    // load (client-side nav). The first run also covers initial
    // hydration so the navbar's register link reflects the real
    // server flag without a separate fetch.
    $effect(() => {
        authConfig.seed(data.authConfig);
    });

    onMount(() => {
        theme.init();
        preferences.init();
        if (!session.loaded) session.refresh();
    });

    // Three runtime height publishers — each is an independent $effect
    // keyed by its element ref, so they handle mount, unmount, and
    // SPA navigation (e.g. entering / leaving the reader, which
    // toggles `showMobileChrome`) without leaking observers.
    //
    // Skipping publish when offsetHeight === 0 is what lets the
    // desktop header and the mobile AppBar coexist in the DOM: only
    // the one visible at the current viewport publishes a non-zero
    // height; the other (display: none) stays at its CSS fallback.
    $effect(() => {
        if (!headerEl) return;
        const el = headerEl;
        const publish = () => {
            const h = el.offsetHeight;
            if (h > 0) document.documentElement.style.setProperty('--app-header-h', `${h}px`);
        };
        publish();
        const ro = new ResizeObserver(publish);
        ro.observe(el);
        return () => ro.disconnect();
    });

    $effect(() => {
        if (!appBarEl) return;
        const el = appBarEl;
        const publish = () => {
            const h = el.offsetHeight;
            if (h > 0)
                document.documentElement.style.setProperty('--mobile-app-bar-h', `${h}px`);
        };
        publish();
        const ro = new ResizeObserver(publish);
        ro.observe(el);
        return () => ro.disconnect();
    });

    $effect(() => {
        if (!bottomNavEl) return;
        const el = bottomNavEl;
        const publish = () => {
            const h = el.offsetHeight;
            if (h > 0)
                document.documentElement.style.setProperty('--app-bottom-nav-h', `${h}px`);
        };
        publish();
        const ro = new ResizeObserver(publish);
        ro.observe(el);
        return () => ro.disconnect();
    });

    // Pull fresh server preferences whenever the user changes (login,
    // logout, account switch). The store's seq guard keeps the most recent
    // response authoritative.
    $effect(() => {
        if (session.user) preferences.refresh();
    });

    onDestroy(() => theme.destroy());

    async function handleLogout() {
        loggingOut = true;
        try {
            await logout();
        } finally {
            session.setUser(null);
            // Don't let user A's reader preferences linger for the next
            // person who uses this browser (or as guest state for the
            // same user). Resets state + localStorage.
            preferences.clearForLogout();
            loggingOut = false;
            goto('/login');
        }
    }
</script>

<svelte:head>
    <title>{layoutTitle}</title>
</svelte:head>

<NavProgress />

{#if showMobileChrome}
    <div class="mobile-app-bar-wrap" bind:this={appBarEl}>
        <AppBar title="Mangalord" testid="mobile-app-bar" />
    </div>
{/if}

<header bind:this={headerEl} class="desktop-header">
    <nav aria-label="primary">
        <a class="brand" href="/">Mangalord</a>
        <a class="nav-link" href="/upload">
            <Upload size={18} aria-hidden="true" />
            <span>Upload</span>
        </a>
        <a class="nav-link" href="/profile" data-testid="nav-profile">
            <UserCircle size={18} aria-hidden="true" />
            <span>Profile</span>
        </a>
        <a class="nav-link" href="/bookmarks">
            <Bookmark size={18} aria-hidden="true" />
            <span>Bookmarks</span>
        </a>
        <a class="nav-link" href="/collections">
            <FolderOpen size={18} aria-hidden="true" />
            <span>Collections</span>
        </a>
        {#if session.user?.is_admin}
            <a class="nav-link" href="/admin" data-testid="nav-admin">
                <Shield size={18} aria-hidden="true" />
                <span>Admin</span>
            </a>
        {/if}
    </nav>
    <div class="session" data-testid="session-area">
        {#if !session.loaded}
            <span data-testid="session-loading" aria-busy="true">…</span>
        {:else if session.user}
            <span class="username" data-testid="session-user">{session.user.username}</span>
            <IconButton
                size={36}
                radius="md"
                onclick={handleLogout}
                disabled={loggingOut}
                aria-label="Logout"
                title="Logout"
            >
                {#if loggingOut}
                    <span class="logging-out">…</span>
                {:else}
                    <LogOut size={18} aria-hidden="true" />
                {/if}
            </IconButton>
        {:else}
            <a class="text-link" href="/login" data-testid="nav-login">Login</a>
            {#if authConfig.self_register_enabled}
                <a class="text-link" href="/register" data-testid="nav-register">Register</a>
            {/if}
        {/if}
    </div>
</header>

<main>
    {@render children()}
</main>

{#if showMobileChrome}
    <div class="mobile-bottom-nav-wrap" bind:this={bottomNavEl}>
        <BottomNav tabs={MOBILE_TABS} currentPath={$page.url.pathname} />
    </div>
{/if}

<Toaster />

<style>
    header {
        padding: var(--space-3) var(--space-4);
        border-bottom: 1px solid var(--border);
        background: var(--surface);
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: var(--space-4);
        flex-wrap: wrap;
    }

    nav {
        display: flex;
        align-items: center;
        gap: var(--space-1);
    }

    .brand {
        font-weight: var(--weight-semibold);
        font-size: var(--font-lg);
        color: var(--text);
        padding: var(--space-2) var(--space-3);
        margin-right: var(--space-2);
    }

    .brand:hover {
        text-decoration: none;
        color: var(--primary);
    }

    .nav-link {
        display: inline-flex;
        align-items: center;
        gap: var(--space-2);
        color: var(--text);
        padding: var(--space-2) var(--space-3);
        border-radius: var(--radius-md);
        font-size: var(--font-sm);
        transition: background var(--transition);
    }

    .nav-link:hover {
        background: var(--surface-elevated);
        text-decoration: none;
    }

    .text-link {
        color: var(--primary);
        padding: var(--space-2) var(--space-3);
        font-size: var(--font-sm);
    }

    /* App frame: header is fixed at the viewport top with a slide
       transition so reader fullscreen (set via `data-reader-fullscreen`
       on `<html>`) can hide it without jolting the layout. `main` pays
       the gap with a matching padding-top that animates in lockstep. */
    header {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        z-index: var(--z-sticky);
        transform: translateY(0);
        transition: transform 220ms ease-out;
    }

    :global(html[data-reader-fullscreen='true']) header {
        transform: translateY(-100%);
    }

    .session {
        display: flex;
        align-items: center;
        gap: var(--space-1);
    }

    .username {
        color: var(--text-muted);
        font-size: var(--font-sm);
        padding: 0 var(--space-2);
    }

    .logging-out {
        font-size: var(--font-base);
        line-height: 1;
    }

    main {
        padding: var(--space-4);
        /* Reserve room for the fixed header so its presence doesn't
           overlap content. The header height comes from a runtime
           ResizeObserver (see onMount above) so this always tracks
           the rendered size. */
        padding-top: calc(var(--app-header-h) + var(--space-4));
        max-width: 64rem;
        margin: 0 auto;
        transition: padding-top 220ms ease-out;
    }

    :global(html[data-reader-fullscreen='true']) main {
        /* No top reservation in focus mode — the chapter image runs
           edge-to-edge once the header has slid off. */
        padding-top: 0;
    }

    @media (max-width: 640px) {
        :global(html[data-mobile-full-bleed='true']) main {
            /* Mobile detail page surrenders the layout's top reservation
               so its hero can sit at the viewport top under a transparent
               page-specific app bar. Horizontal and bottom padding are
               preserved so the body content (description, chapter list)
               keeps its gutters; the hero negates the horizontal padding
               itself via negative margins. */
            padding-top: 0;
        }
    }

    /* Mobile chrome (AppBar + BottomNav) is rendered always when the
       route opts in, but CSS-hides above the breakpoint so the desktop
       header is the only visible top-chrome. The ResizeObserver in the
       script ignores zero offsetHeight so the hidden element doesn't
       clobber the visible one's published var. */
    .mobile-app-bar-wrap {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        z-index: var(--z-sticky);
        transform: translateY(0);
        transition: transform 220ms ease-out;
    }

    :global(html[data-reader-fullscreen='true']) .mobile-app-bar-wrap {
        transform: translateY(-100%);
    }

    .mobile-app-bar-wrap,
    .mobile-bottom-nav-wrap {
        display: none;
    }

    @media (max-width: 640px) {
        .desktop-header {
            display: none;
        }

        .mobile-app-bar-wrap,
        .mobile-bottom-nav-wrap {
            display: block;
        }

        main {
            /* On mobile, swap the desktop-header reservation for the
               AppBar's. BottomNav reservation goes on the bottom so
               the last bit of content isn't trapped under the bar.
               16px horizontal gutter matches iOS/Material standard
               and gives chips, description text, and chapter rows
               enough breathing room from the screen edge. */
            padding-top: calc(var(--mobile-app-bar-h) + var(--space-3));
            padding-bottom: calc(var(--app-bottom-nav-h) + var(--space-4));
            padding-left: var(--space-4);
            padding-right: var(--space-4);
        }
    }
</style>
