<!-- TRACES: UR-007, UR-029, UR-030, UR-067 | DR-007, DR-032, DR-033, DR-116 -->
<script lang="ts">
  import { onMount, onDestroy } from "svelte";
  import { listen, type UnlistenFn } from "@tauri-apps/api/event";
  import { goto } from "$app/navigation";
  import { navigateUp } from "$lib/utils/navigation";
  import { currentLibrary } from "$lib/stores/library";
  import { auth } from "$lib/stores/auth";
  import { shouldShowAudioMiniPlayer } from "$lib/stores/player";
  import { isAndroid } from "$lib/stores/appState";
  import SearchBar from "$lib/components/common/SearchBar.svelte";
  import SortButtonGroup from "$lib/components/common/SortButtonGroup.svelte";
  import type { SortOption } from "$lib/components/common/SortButtonGroup.svelte";
  import BackButton from "$lib/components/common/BackButton.svelte";
  import ResultsCounter from "$lib/components/common/ResultsCounter.svelte";
  import { useServerReachabilityReload } from "$lib/composables/useServerReachabilityReload";
  import { useOfflineFilterReload } from "$lib/composables/useOfflineFilterReload";
  import type { MediaItem, Library, ItemType, SearchResult } from "$lib/api/types";
  import LibraryGrid from "./LibraryGrid.svelte";
  import TrackList from "./TrackList.svelte";
  import AlphabetScrollBar from "./AlphabetScrollBar.svelte";
  import { excludePodcasts } from "$lib/utils/podcastFilter";
  import { createLogger } from "$lib/utils/logger";

  const log = createLogger("GenericMediaListPage");

  /**
   * Generic media list page supporting Albums, Artists, Playlists, and Tracks
   * Consolidates duplicate music library browsing logic
   *
   * @req: UR-007 - Navigate media in library
   * @req: UR-008 - Search media across libraries
   * @req: DR-007 - Library browsing screens
   */

  export interface MediaListConfig {
    itemType: ItemType; // "MusicAlbum", "MusicArtist", "Playlist", "Audio"
    title: string; // "Albums", "Artists", "Playlists", "Tracks"
    backPath: string; // "/library/music"
    searchPlaceholder?: string;
    sortOptions: Array<{ key: string; label: string }>; // Jellyfin field names
    defaultSort: string; // Jellyfin field name (e.g., "SortName")
    displayComponent: "grid" | "tracklist"; // Which component to use
  }

  interface Props {
    config: MediaListConfig;
    /**
     * Suppress the back button + title. Set when this renders as a *tab* of a
     * library page, which already has its own header — two stacked headers and
     * two back buttons read as two pages. TRACES: UR-063 | DR-105
     */
    showHeader?: boolean;
  }

  let { config, showHeader = true }: Props = $props();

  let items = $state<MediaItem[]>([]);
  let loading = $state(true);
  let gridWrapper = $state<HTMLDivElement | null>(null);
  let searchQuery = $state("");
  let debouncedSearchQuery = $state("");
  let favoritesOnly = $state(false);
  let sortBy = $state<string>("");
  let sortOrder = $state<"Ascending" | "Descending">("Ascending");
  let searchTimeout: ReturnType<typeof setTimeout> | null = null;
  let initialLoadDone = false;

  /**
   * Payload of the backend `search-event` (mirrors Rust `SearchUpdateEvent`).
   * `repo.search()` resolves instantly with cache-only (downloaded) results;
   * the merged cache+server union arrives later via this event.
   */
  interface SearchUpdateEvent {
    requestId: number;
    result: SearchResult;
  }

  // Monotonic id identifying the latest search request. The deferred
  // `search-event` is only applied when its requestId still matches, so
  // out-of-order / superseded server results never clobber fresher ones.
  let searchRequestId = 0;
  let unlistenSearch: UnlistenFn | null = null;

  async function ensureSearchListener() {
    if (unlistenSearch) return;
    unlistenSearch = await listen<SearchUpdateEvent>("search-event", (event) => {
      const { requestId, result } = event.payload;
      if (requestId !== searchRequestId) return;
      items = excludePodcasts(result.items);
    });
  }

  $effect(() => {
    sortBy = config.defaultSort;
  });

  const { markLoaded } = useServerReachabilityReload(async () => {
    await loadItems();
  });

  // Re-query when the offline downloaded-only gate changes — going offline, or
  // toggling "Show all server media". Without this the listing kept whatever it
  // was first loaded with and the toggle only greyed cards. TRACES: UR-052 | DR-143
  useOfflineFilterReload(() => loadItems());

  onMount(async () => {
    await loadItems();
    markLoaded();
    initialLoadDone = true;
  });

  async function loadItems() {
    if (!$currentLibrary) {
      goto(config.backPath);
      return;
    }

    try {
      // Only show skeleton on first load (no data yet)
      if (items.length === 0) loading = true;
      const repo = auth.getRepository();

      // Use backend search if search query is provided, otherwise use getItems with sort
      // HACK: excludePodcasts drops the "Podcasts" folder stored in the music library.
      if (debouncedSearchQuery.trim()) {
        // Phase 1: instant cache-only (downloaded) results. The merged
        // cache+server union arrives later via the `search-event` listener,
        // tagged with this requestId so superseded queries are ignored.
        await ensureSearchListener();
        const requestId = ++searchRequestId;
        const result = await repo.search(
          debouncedSearchQuery,
          {
            includeItemTypes: [config.itemType],
            limit: 10000,
          },
          requestId
        );
        // Only apply if this is still the active query.
        if (requestId === searchRequestId) {
          items = excludePodcasts(result.items);
        }
      } else {
        // Leaving search — invalidate any in-flight server results.
        searchRequestId++;
        const result = await repo.getItems($currentLibrary.id, {
          includeItemTypes: [config.itemType],
          sortBy,
          sortOrder,
          recursive: true,
          limit: 10000,
          // Narrows the listing in place; the backend owns what "favourite"
          // resolves to online vs offline. TRACES: UR-067 | DR-116
          favoritesOnly: favoritesOnly ? true : undefined,
        });
        items = excludePodcasts(result.items);
      }
    } catch (e) {
      log.error(`Failed to load ${config.itemType}:`, e);
    } finally {
      loading = false;
    }
  }

  function handleSearch(query: string) {
    searchQuery = query;
  }

  /// TRACES: UR-067 | DR-116
  function toggleFavoritesOnly() {
    favoritesOnly = !favoritesOnly;
    loadItems();
  }

  // Debounce search input (300ms delay) - skip initial mount to avoid duplicate load
  $effect(() => {
    const _query = searchQuery; // track for reactivity
    if (!initialLoadDone) return;

    if (searchTimeout) clearTimeout(searchTimeout);
    searchTimeout = setTimeout(() => {
      debouncedSearchQuery = searchQuery;
      loadItems();
    }, 300);
  });

  onDestroy(() => {
    if (unlistenSearch) unlistenSearch();
    if (searchTimeout) clearTimeout(searchTimeout);
  });

  function handleSort(newSort: string) {
    sortBy = newSort;
    loadItems();
  }

  function toggleSortOrder() {
    sortOrder = sortOrder === "Ascending" ? "Descending" : "Ascending";
    loadItems();
  }

  function goBack() {
    navigateUp(config.backPath);
  }

  const searchPlaceholder = $derived(config.searchPlaceholder || `Search ${config.title.toLowerCase()}...`);

  function handleItemClick(item: MediaItem | Library) {
    // Navigate to detail page for browseable items
    goto(`/library/${item.id}`);
  }

  function handleTrackClick(track: MediaItem, _index: number) {
    // For track lists, navigate to the track's album if available, otherwise detail page
    if (track.albumId) {
      goto(`/library/${track.albumId}`);
    } else {
      goto(`/library/${track.id}`);
    }
  }

  // ===== A-Z jump bar =====
  // Bucket a name to its index letter: A-Z, or "#" for digits/symbols/empty.
  function letterFor(name: string): string {
    const first = (name ?? "").trim().charAt(0).toUpperCase();
    return first >= "A" && first <= "Z" ? first : "#";
  }

  // Only meaningful when the list is sorted alphabetically and long enough to scroll.
  const isAlphaSorted = $derived(sortBy === "SortName");
  const showAlphaBar = $derived(
    isAlphaSorted &&
      !loading &&
      !debouncedSearchQuery.trim() &&
      items.length > 30
  );

  const availableLetters = $derived.by(() => {
    const set = new Set<string>();
    if (showAlphaBar) {
      for (const item of items) set.add(letterFor(item.name));
    }
    return set;
  });

  // First item index for each letter, honouring current ascending/descending order.
  const firstIndexForLetter = $derived.by(() => {
    const map = new Map<string, number>();
    items.forEach((item, index) => {
      const letter = letterFor(item.name);
      if (!map.has(letter)) map.set(letter, index);
    });
    return map;
  });

  function jumpToLetter(letter: string) {
    const index = firstIndexForLetter.get(letter);
    if (index === undefined || !gridWrapper) return;
    const target = gridWrapper.querySelector(`[data-grid-index="${index}"]`);
    target?.scrollIntoView({ behavior: "smooth", block: "start" });
  }

  // Bottom space the layout's <main> reserves for the nav / mini-player bars.
  // Mirrors src/routes/library/+layout.svelte so the A-Z strip ends just above
  // whichever bars are visible.
  const bottomGap = $derived(
    $shouldShowAudioMiniPlayer ? ($isAndroid ? "11rem" : "7rem") : "5rem"
  );
</script>

<div class="space-y-6">
  <!-- Header -->
  {#if showHeader}
    <div class="flex items-center gap-4">
      <BackButton onClick={goBack} label="Back" />
      <h1 class="text-3xl font-bold text-white">{config.title}</h1>
    </div>
  {/if}

  <!-- Search and Sort Bar -->
  <div class="flex flex-col sm:flex-row gap-4">
    <!-- Search -->
    <div class="flex-1">
      <SearchBar value={searchQuery} placeholder={searchPlaceholder} onInput={handleSearch} />
    </div>

    <!-- Favourites filter. Session-scoped on purpose: a persisted filter that
         hides most of a library reads as data loss on the next launch
         (ux-flows §5C.2). Hidden while searching, which has no favourites
         filter of its own. TRACES: UR-067 | DR-116 -->
    {#if !debouncedSearchQuery.trim()}
      <button
        onclick={toggleFavoritesOnly}
        aria-pressed={favoritesOnly}
        class="px-3 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors
               {favoritesOnly
                 ? 'bg-[var(--color-jellyfin)] text-white'
                 : 'bg-[var(--color-surface)] text-gray-400 hover:text-white'}"
        title={favoritesOnly ? "Showing favourites only" : "Show favourites only"}
      >
        <svg
          class="w-4 h-4"
          fill={favoritesOnly ? "currentColor" : "none"}
          stroke="currentColor"
          viewBox="0 0 24 24"
          stroke-width="2"
        >
          <path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
        </svg>
        Favourites
      </button>
    {/if}

    <!-- Sort (only show if there are sort options) -->
    {#if config.sortOptions.length > 0}
      <SortButtonGroup options={config.sortOptions} selected={sortBy} onSelect={handleSort} />
    {/if}
  </div>

  <!-- Results Count -->
  {#if !loading}
    <ResultsCounter count={items.length} itemType={config.itemType.toLowerCase()} searchQuery={searchQuery} />
  {/if}

  <!-- Items List/Grid -->
  {#if loading}
    {#if config.displayComponent === "grid"}
      <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
        {#each Array(10) as _}
          <div class="animate-pulse">
            <div class="aspect-square bg-[var(--color-surface)] rounded-lg"></div>
            <div class="mt-2 h-4 bg-[var(--color-surface)] rounded w-3/4"></div>
          </div>
        {/each}
      </div>
    {:else}
      <div class="space-y-2">
        {#each Array(5) as _}
          <div class="animate-pulse h-16 bg-[var(--color-surface)] rounded-lg"></div>
        {/each}
      </div>
    {/if}
  {:else if items.length === 0}
    <div class="text-center py-12 text-gray-400">
      <p>No {config.title.toLowerCase()} found</p>
    </div>
  {:else}
    <div class="flex gap-2">
      <div bind:this={gridWrapper} class="flex-1 min-w-0">
        {#if config.displayComponent === "grid"}
          <LibraryGrid items={items} onItemClick={handleItemClick} musicContent={["MusicAlbum", "MusicArtist", "Audio", "Playlist"].includes(config.itemType)} />
        {:else if config.displayComponent === "tracklist"}
          <TrackList tracks={items} onTrackClick={handleTrackClick} />
        {/if}
      </div>
      {#if showAlphaBar}
        <div class="sticky top-2 self-start flex-shrink-0 h-fit">
          <AlphabetScrollBar {availableLetters} onJump={jumpToLetter} {bottomGap} />
        </div>
      {/if}
    </div>
  {/if}
</div>
