<script lang="ts">
  import { onMount, onDestroy } from "svelte";
  import { get } from "svelte/store";
  import { page } from "$app/stores";
  import { platform } from "@tauri-apps/plugin-os";
  import "../app.css";
  import { auth, needsReauth, isAuthenticated } from "$lib/stores/auth";
  import { connectivity, isConnected } from "$lib/stores/connectivity";
  import { initPlayerEvents, cleanupPlayerEvents } from "$lib/services/playerEvents";
  import { downloads, initDownloadEvents, cleanupDownloadEvents } from "$lib/stores/downloads";
  import { syncService } from "$lib/services/syncService";
  import { onReconnected as onCatalogReconnected, syncCatalog, refreshSyncStatus, showServerCatalog, lastCatalogSync } from "$lib/services/offlineCatalog";
  import { playbackMode } from "$lib/stores/playbackMode";
  import { sessions } from "$lib/stores/sessions";
  import ReauthModal from "$lib/components/auth/ReauthModal.svelte";
  import Toast from "$lib/components/Toast.svelte";
  import SleepTimerModal from "$lib/components/player/SleepTimerModal.svelte";
  import BottomUi from "$lib/components/BottomUi.svelte";
  import AppHeader from "$lib/components/AppHeader.svelte";
  import { isInitialized, pendingSyncCount, isAndroid, showSleepTimerModal } from "$lib/stores/appState";
  import {
    showBottomNav as computeShowBottomNav,
    showGlobalMiniPlayer as computeShowGlobalMiniPlayer,
    showGlobalHeader as computeShowGlobalHeader,
    routeOwnsLayout as computeRouteOwnsLayout,
  } from "$lib/utils/layoutShell";
  import { registerNavigationTracking } from "$lib/utils/navigation";
  import { startNetworkReporting } from "$lib/services/networkType";

  let { children } = $props();

  /** Teardown for the network-transport reporter (WiFi-only gate). */
  let stopNetworkReporting: (() => void) | null = null;

  // Track in-app navigation depth so the header "back" affordance knows when a
  // real in-app Back exists (vs. a stale WebView stack after a background /
  // restore). Must run during component init — afterNavigate needs a component
  // context, not the async onMount callback below.
  registerNavigationTracking();

  // Layout-shell visibility rules live in one pure, unit-tested module
  // ($lib/utils/layoutShell) so they can't drift per route/platform.
  //
  // The bottom UI (mini player + nav) is rendered IN FLEX FLOW below the
  // scroller — never as a fixed overlay — so the list is physically bounded
  // above it and cannot render behind it. There is nothing to measure or
  // reserve; the old ResizeObserver/`bottomUiHeight`/padding scheme (which
  // started at 0 and kept regressing into "last row hidden behind the nav") is
  // gone. See BottomUi.svelte.
  const pathname = $derived($page.url.pathname);
  const showBottomNav = $derived(
    computeShowBottomNav({ pathname, isAuthenticated: $isAuthenticated })
  );
  const showGlobalMiniPlayer = $derived(computeShowGlobalMiniPlayer({ pathname }));

  // The shared header (with the account menu) is rendered by the root on every
  // authenticated non-immersive route that doesn't own its own layout. Library
  // renders its own AppHeader; settings/player/login get none. (UR-054)
  const showGlobalHeader = $derived(
    computeShowGlobalHeader({ pathname, isAuthenticated: $isAuthenticated })
  );

  // Library/settings/player/login own their own full-height flex column
  // (header + scroller + their own in-flow BottomUi), so the root just clips
  // and lets them manage layout. Every other route renders into the root's
  // scroller, with the root's in-flow BottomUi as a flex sibling below it.
  const routeOwnsLayout = $derived(computeRouteOwnsLayout({ pathname }));

  onMount(async () => {
    // Detect platform first (synchronously, before any await) so the global
    // mini player's Android visibility gate is correct from the first render.
    // Android needs the global mini player because the library layout hides its
    // own; if this ran after `await auth.initialize()` the store stayed false
    // long enough that the mini player never appeared on library routes.
    try {
      const platformName = platform();
      isAndroid.set(platformName === "android");
    } catch (err) {
      console.error("Platform detection failed:", err);
    }

    // Initialize auth state (restore session from secure storage)
    await auth.initialize();
    isInitialized.set(true);

    // Initialize player event listener for push-based updates
    await initPlayerEvents();

    // Initialize download event listener
    await initDownloadEvents();

    // Report the network transport to the backend and keep it current, so the
    // WiFi-only download gate has real data to act on (UR-053). No-op on
    // desktop, where the backend defaults to unmetered.
    stopNetworkReporting = startNetworkReporting();

    // Prime the downloads store from the DB. The store starts empty each launch,
    // and download badges (e.g. AlbumDownloadButton) derive purely from it, so
    // without an initial refresh a previously-downloaded album shows as
    // not-downloaded until the user opens the Downloads page.
    const userId = get(auth).user?.id;
    if (userId) {
      downloads.refresh(userId).catch((err) =>
        console.error("Initial downloads refresh failed:", err)
      );
    }

    // Start sync service for offline mutation queue
    syncService.start();

    // Kick off a best-effort full-catalog pre-sync so the whole server catalog
    // is browsable (greyed out) offline, and load the last-sync hint for the
    // offline banner. Non-blocking — no-ops when not connected.
    void syncCatalog();
    void refreshSyncStatus();

    // Initialize playback mode and session monitoring
    playbackMode.initializeSessionMonitoring();
    await playbackMode.refresh();
  });

  onDestroy(() => {
    stopNetworkReporting?.();
    cleanupPlayerEvents();
    cleanupDownloadEvents();
    connectivity.stopMonitoring();
    syncService.stop();
    auth.cleanupEventListeners();
  });

  // Connectivity monitoring is now started early in auth.initialize()
  // This effect is kept only for when the user logs in during the session
  $effect(() => {
    if ($isAuthenticated) {
      // Check if monitoring is already running by attempting to get status
      // If not running, start it (handles login during current session)
      auth.getCurrentSession().then((session) => {
        if (session?.serverUrl) {
          connectivity.forceCheck().catch((error) => {
            // If check fails, monitoring might not be started yet, so start it
            console.debug("[Layout] Queue status check failed, starting monitoring:", error);
            connectivity.startMonitoring(session.serverUrl, {
              onServerReconnected: () => {
                // Retry session verification when server becomes reachable
                auth.retryVerification();
                // Resume offline-queued downloads and refresh the catalog.
                void onCatalogReconnected();
              },
            }).catch((monitorError) => {
              console.error("[Layout] Failed to start connectivity monitoring:", monitorError);
            });
          });
        }
      });
    }
  });

  // Update pending sync count periodically
  $effect(() => {
    if ($isAuthenticated) {
      const updateCount = async () => {
        const count = await syncService.getPendingCount();
        pendingSyncCount.set(count);
      };
      updateCount();
      // Update every 10 seconds
      const interval = setInterval(updateCount, 10000);
      return () => clearInterval(interval);
    }
  });
</script>

<div class="h-screen bg-[var(--color-background)] overflow-hidden flex flex-col">
  {#if isInitialized}
    <!-- Offline indicator banner -->
    {#if $isAuthenticated && !$isConnected}
      <div class="bg-amber-600/90 text-white px-4 py-2 text-sm flex items-center justify-center gap-2 shrink-0">
        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414" />
        </svg>
        <span>You're offline. Some features may be limited.</span>
        {#if $pendingSyncCount > 0}
          <span class="bg-white/20 px-2 py-0.5 rounded-full text-xs">
            {$pendingSyncCount} pending sync{$pendingSyncCount !== 1 ? 's' : ''}
          </span>
        {/if}
        <button
          type="button"
          onclick={() => showServerCatalog.update((v) => !v)}
          class="ml-1 bg-white/20 hover:bg-white/30 px-2 py-0.5 rounded-full text-xs transition-colors"
          aria-pressed={$showServerCatalog}
          title={$lastCatalogSync ? `Catalog last synced ${new Date($lastCatalogSync).toLocaleString()}` : 'Catalog not yet synced'}
        >
          {$showServerCatalog ? 'Hide server media' : 'Show all server media'}
        </button>
      </div>
    {/if}

    <!-- Main content. Routes that own their full-height layout (library,
         settings, player, login) get a plain clipped box and manage their own
         scrolling internally. All other top-level pages render directly here, so
         this wrapper must scroll and reserve the fixed bottom UI's measured
         height so the mini player / bottom nav never overlap the last rows. -->
    {#if routeOwnsLayout}
      <!-- These routes own their own full-height flex column (header + scroller
           + their own in-flow BottomUi), so the root just clips and steps back. -->
      <div class="flex-1 overflow-hidden">
        {@render children()}
      </div>
    {:else}
      <!-- Shared header (account menu, desktop nav) as a flex-shrink-0 sibling
           above the scroller, so it never eats into the scroller's bounds. -->
      {#if showGlobalHeader}
        <AppHeader />
      {/if}
      <!-- Scroller is flex-1/min-h-0; the in-flow BottomUi below is a flex
           sibling, so the list is physically bounded above it and can never
           render behind it. No measurement, no reserved padding. -->
      <div
        class="flex-1 overflow-y-auto min-h-0"
        style="overscroll-behavior: contain"
      >
        {@render children()}
      </div>
    {/if}

    <!-- Re-authentication modal -->
    <ReauthModal isOpen={$needsReauth} />

    <!-- Toast notifications (global) -->
    <Toast />

    <!-- Bottom UI (mini player + nav), in normal flex flow below the scroller.
         Owned routes render their own BottomUi inside their own column instead. -->
    {#if !routeOwnsLayout && (showBottomNav || showGlobalMiniPlayer)}
      <BottomUi showMiniPlayer={showGlobalMiniPlayer} showNav={showBottomNav} />
    {/if}

    <!-- Sleep Timer Modal (global) -->
    <SleepTimerModal
      isOpen={$showSleepTimerModal}
      onClose={() => showSleepTimerModal.set(false)}
    />
  {:else}
    <div class="flex items-center justify-center h-screen">
      <div class="w-8 h-8 border-2 border-[var(--color-jellyfin)] border-t-transparent rounded-full animate-spin"></div>
    </div>
  {/if}
</div>
