<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 { listen, type UnlistenFn } from "@tauri-apps/api/event";
  import "../app.css";
  import { commands } from "$lib/api/bindings";
  import { auth, needsReauth, isAuthenticated } from "$lib/stores/auth";
  import { connectivity, isConnected } from "$lib/stores/connectivity";
  import { initPlayerEvents, cleanupPlayerEvents } from "$lib/services/playerEvents";
  import { initWebviewAudio, cleanupWebviewAudio } from "$lib/services/webviewAudio";
  import { downloads, initDownloadEvents, cleanupDownloadEvents } from "$lib/stores/downloads";
  import { syncService } from "$lib/services/syncService";
  import { clearFavorite } from "$lib/stores/favorites";
  import { onReconnected as onCatalogReconnected, 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 PendingSyncModal from "$lib/components/sync/PendingSyncModal.svelte";
  import { isInitialized, pendingSyncCount, isAndroid, showSleepTimerModal } from "$lib/stores/appState";
  import {
    showBottomNav as computeShowBottomNav,
    showGlobalMiniPlayer as computeShowGlobalMiniPlayer,
    showGlobalHeader as computeShowGlobalHeader,
    routeOwnsLayout as computeRouteOwnsLayout,
    shellReservesBottomInset,
  } from "$lib/utils/layoutShell";
  import { registerNavigationTracking } from "$lib/utils/navigation";
  import { useScrollRestore } from "$lib/utils/scrollContainer";
  import { startNetworkReporting } from "$lib/services/networkType";
  import { initSafeArea } from "$lib/utils/safeArea";

  let { children } = $props();

  /** Offline banner badge → the list of what is actually queued (DR-132). */
  let showPendingSync = $state(false);

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

  /** Teardown for the native window-inset subscription (safe areas). */
  let stopSafeArea: (() => 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();

  // The shell's scroller outlives every route rendered into it, so without this
  // a new page inherits the previous page's offset. Must be registered here at
  // init, alongside the tracker above, for the same reason. (DR-156)
  let shellScroller = $state<HTMLElement>();
  useScrollRestore(() => shellScroller, "shell");

  // 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 }));

  // Bottom safe-area inset (Android navigation/gesture bar): BottomUi reserves
  // it wherever one renders, so the shell only takes it on routes that have no
  // bottom UI at all (login, the full-screen player). Exactly one owner, or the
  // bar is either ignored or double-padded. (UR-066)
  const shellPadsBottom = $derived(
    shellReservesBottomInset({ pathname, isAuthenticated: $isAuthenticated })
  );

  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);
    }

    // Prime the safe-area custom properties from the native WindowInsets bridge
    // BEFORE the first await, so the very first paint already clears the status
    // bar and the navigation/gesture bar. Native also pushes updates directly,
    // but a page load wipes the inline style it set, so this pull is required.
    // No-op without the Android bridge. (UR-066)
    stopSafeArea = initSafeArea();

    // 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 the webview audio controller (plays audio-only media in an
    // <audio> element on platforms with no native audio backend, e.g. Windows;
    // self-gates and is a no-op on Linux/Android).
    await initWebviewAudio();

    // Initialize download event listener
    await initDownloadEvents();

    // Favourite state can change behind the UI: another Jellyfin client marks
    // something, or the Rust drain pushes toggles queued while offline. Drop
    // the session overrides for those ids so the next render reads the freshly
    // cached server value rather than a stale local guess.
    // TRACES: UR-069 | DR-120
    stopFavoritesListener = await listen<{ itemIds: string[] }>(
      "favorites-changed",
      (event) => {
        for (const id of event.payload?.itemIds ?? []) clearFavorite(id);
      }
    );

    // 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 the queue once at startup. The Rust drain otherwise only runs on an
    // offline→online transition, so a queue built up in a previous session sits
    // untouched for a whole run of the app if the server was reachable the
    // whole time. Safe when it isn't: an unreachable server leaves rows queued
    // without spending their retry budget (DR-131).
    if (get(auth).user?.id) {
      commands.syncProcessPending().catch((err) =>
        console.debug("[Layout] Startup sync drain skipped:", err)
      );
    }

    // Load the last-sync hint for the offline banner. The catalog *index* is no
    // longer kicked off from here: the Rust background indexer (DR-109) owns
    // when to re-index, so a long session no longer searches a stale catalog and
    // a restart no longer forces a full crawl regardless of freshness.
    void refreshSyncStatus();

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

  onDestroy(() => {
    stopNetworkReporting?.();
    stopFavoritesListener?.();
    stopFavoritesListener = null;
    stopSafeArea?.();
    cleanupPlayerEvents();
    cleanupWebviewAudio();
    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, and immediately whenever the Rust
  // drain (DR-131) reports it pushed or gave up on rows — otherwise the badge
  // lags a reconnect by up to 10s and reads as if nothing happened.
  $effect(() => {
    if ($isAuthenticated) {
      const updateCount = async () => {
        const count = await syncService.getPendingCount();
        pendingSyncCount.set(count);
      };
      updateCount();
      // Update every 10 seconds
      const interval = setInterval(updateCount, 10000);
      let unlistenDrain: UnlistenFn | null = null;
      listen("sync-queue-changed", () => {
        void updateCount();
      })
        .then((unlisten) => {
          unlistenDrain = unlisten;
        })
        .catch((err) => console.debug("[Layout] sync-queue-changed listen failed:", err));

      return () => {
        clearInterval(interval);
        unlistenDrain?.();
      };
    }
  });
</script>

<!--
  The app shell reserves the top/side safe-area insets (status bar, display
  cutout) so no route has to. The bottom inset belongs to BottomUi wherever one
  renders — see shellReservesBottomInset. `h-screen` is border-box, so the
  padding is taken out of the 100vh rather than added to it.

  TRACES: UR-066 | DR-112
-->
<!--
  data-app-shell marks the layer app.css clears for native video. This div
  paints --color-background across the entire viewport, *under* a VideoPlayer
  that makes itself transparent on the native path — so while it stays painted,
  the ExoPlayer SurfaceView behind the WebView cannot be seen no matter what
  else is cleared. The rule in app.css was written for this attribute; the
  attribute was never added. (DR-185)
-->
<div
  data-app-shell
  class="h-screen bg-[var(--color-background)] overflow-hidden flex flex-col
         pt-[var(--safe-top)] pl-[var(--safe-left)] pr-[var(--safe-right)]"
  style:padding-bottom={shellPadsBottom ? "var(--safe-bottom)" : undefined}
>
  {#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>
        <!-- The badge is answerable: it opens the queue it counts. Read as
             "pending transfers" it used to send people to the Downloads page,
             which lists a different table entirely and can never show these.
             TRACES: UR-025 | DR-132 -->
        {#if $pendingSyncCount > 0}
          <button
            type="button"
            onclick={() => (showPendingSync = true)}
            class="bg-white/20 hover:bg-white/30 px-2 py-0.5 rounded-full text-xs transition-colors"
            title="Changes waiting to reach the server"
          >
            {$pendingSyncCount} waiting to sync
          </button>
        {/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
        bind:this={shellScroller}
        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)}
    />

    <!-- What the offline banner's badge counts (DR-132) -->
    <PendingSyncModal
      isOpen={showPendingSync}
      onClose={() => (showPendingSync = false)}
    />
  {:else}
    <div class="flex items-center justify-center h-full">
      <div class="w-8 h-8 border-2 border-[var(--color-jellyfin)] border-t-transparent rounded-full animate-spin"></div>
    </div>
  {/if}
</div>
