<script lang="ts">
  import '../app.css';
  import { page } from '$app/state';
  import { goto } from '$app/navigation';
  import { QueryClientProvider } from '@tanstack/svelte-query';
  import { queryClient } from '$lib/query/client';
  import { user } from '$lib/auth/store.svelte';
  import { isPublicRoute } from '$lib/auth/publicRoutes';
  import Shell from '$lib/components/Shell.svelte';
  import QueueDrawer from '$lib/components/QueueDrawer.svelte';
  import SelectionBar from '$lib/components/SelectionBar.svelte';
  import ToastHost from '$lib/components/ToastHost.svelte';
  import { selection, clearSelection } from '$lib/selection/store.svelte';
  import {
    registerAudioEl,
    reportTimeUpdate,
    reportDuration,
    reportStateFromAudio,
    player,
    closeQueueDrawer,
    consumePendingRestorePosition,
    deriveFadeScalar
  } from '$lib/player/store.svelte';
  import { useMediaSession } from '$lib/player/mediaSession.svelte';
  import { useEventsDispatcher } from '$lib/player/events.svelte';
  import { useServerEvents } from '$lib/serverEvents.svelte';
  import { useGlobalShortcuts } from '$lib/player/shortcuts.svelte';
  import { applyMetaThemeColor } from '$lib/theme/applyMetaThemeColor.svelte';
  import { audioLoader } from '$lib/player/audioLoader';
  import { attachStallRetry } from '$lib/player/stallRetry';

  let { children } = $props<{ children: import('svelte').Snippet }>();
  let audioEl: HTMLAudioElement | undefined = $state();
  let prefetchEl: HTMLAudioElement | undefined = $state();

  // Reactive guard: runs every time page.url or user.value changes.
  $effect(() => {
    const path = page.url.pathname;
    const isPublic = isPublicRoute(path);
    if (user.value === null && !isPublic) {
      const returnTo = path + page.url.search;
      goto('/login?returnTo=' + encodeURIComponent(returnTo), { replaceState: true });
    } else if (user.value !== null && isPublic) {
      goto('/', { replaceState: true });
    }
  });

  $effect(() => {
    if (audioEl) registerAudioEl(audioEl);
    return () => registerAudioEl(null);
  });

  // Auto-recovery for transient network failures during playback.
  $effect(() => {
    if (!audioEl) return;
    return attachStallRetry(audioEl);
  });

  $effect(() => {
    if (!audioEl) return;
    const cur = player.current;
    const src = cur ? audioLoader.resolve(cur) : null;
    if (src) {
      const absolute = new URL(src, window.location.origin).href;
      if (audioEl.src !== absolute) audioEl.src = src;
    } else {
      audioEl.removeAttribute('src');
      audioEl.load();
    }
  });

  // Phase 2 prefetch: when the current track passes the halfway mark,
  // start loading the next track's bytes into a hidden <audio> element.
  // The browser HTTP cache + audio buffer warm-up means the swap on
  // track-end is gap-free on slow networks. Reset src when no next
  // track exists or the index advances.
  $effect(() => {
    if (!prefetchEl) return;
    const next = player.queue[player.index + 1];
    const ratio = player.duration > 0 ? player.position / player.duration : 0;
    if (next && ratio >= 0.5) {
      const nextSrc = audioLoader.resolve(next);
      if (prefetchEl.src !== new URL(nextSrc, window.location.origin).href) {
        prefetchEl.src = nextSrc;
        audioLoader.prefetch?.(next);
      }
    } else if (!next && prefetchEl.src) {
      prefetchEl.removeAttribute('src');
      prefetchEl.load();
    }
  });

  $effect(() => {
    if (!audioEl) return;
    // Per-position fade scalar: 1 normally, ramping in/out of the
    // crossfade window at track boundaries. Pure function of position
    // + duration + the operator's chosen crossfade duration.
    const scalar = deriveFadeScalar(player.position, player.duration, player.crossfadeSec);
    audioEl.volume = player.volume * scalar;
  });

  $effect(() => {
    if (!audioEl) return;
    // "Intent to play" = loading or playing. During 'loading' the DOM hasn't
    // emitted 'playing' yet, but we still need to call play() to get there.
    const intent = player.state === 'playing' || player.state === 'loading';
    if (intent) {
      audioEl.play().catch(() => { /* interrupted / blocked — store gets the event */ });
    } else {
      audioEl.pause();
    }
  });

  $effect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        // Selection bar takes priority over queue drawer: pressing Esc
        // with both open clears the (less-modal) selection first.
        if (selection.active) clearSelection();
        else if (player.queueDrawerOpen) closeQueueDrawer();
      }
    };
    document.addEventListener('keydown', handler);
    return () => document.removeEventListener('keydown', handler);
  });

  // Route-change reset: selection state is per-page; navigating away
  // (or to a different track-list surface) clears whatever was picked.
  $effect(() => {
    page.url.pathname; // subscribe
    clearSelection();
  });

  useMediaSession();
  useEventsDispatcher();
  useServerEvents();
  useGlobalShortcuts();
  applyMetaThemeColor();
</script>

<audio
  bind:this={audioEl}
  preload="auto"
  ontimeupdate={() => audioEl && reportTimeUpdate(audioEl.currentTime)}
  onloadedmetadata={() => {
    if (!audioEl) return;
    reportDuration(audioEl.duration);
    const restorePos = consumePendingRestorePosition();
    if (restorePos !== null) {
      audioEl.currentTime = restorePos;
    }
  }}
  onplaying={() => reportStateFromAudio('playing')}
  onpause={() => reportStateFromAudio('paused')}
  onwaiting={() => reportStateFromAudio('waiting')}
  onended={() => reportStateFromAudio('ended')}
  onerror={() => reportStateFromAudio('error', audioEl?.error?.message)}
></audio>

<!-- Hidden prefetch element. Loads the next track's bytes ahead of
     time so the swap on track-end is gap-free. Muted + display:none
     so it can't be played or interacted with directly. -->
<audio
  bind:this={prefetchEl}
  preload="auto"
  muted
  aria-hidden="true"
  style="display: none"
></audio>

<QueryClientProvider client={queryClient}>
  <!-- QueueDrawer must be inside the provider: its rows render LikeButton,
       which calls useQueryClient() at init. The drawer's <aside> is always
       mounted, so the moment the queue is populated (on first play) those
       LikeButtons instantiate — outside the provider they throw
       "No QueryClient was found" and abort the play flush. -->
  <QueueDrawer />

  {#if user.value !== null && page.url.pathname !== '/login' && page.url.pathname !== '/now-playing'}
    <Shell>{@render children()}</Shell>
  {:else}
    {@render children()}
  {/if}
  <!-- SelectionBar must be inside the QueryClientProvider so its
       Like-all action can use useQueryClient. -->
  <SelectionBar />
</QueryClientProvider>

<ToastHost />
