<script lang="ts">
  import { page } from '$app/state';
  import { pageTitle } from '$lib/branding';
  import { ChevronLeft } from 'lucide-svelte';
  import {
    createGenresQuery,
    listAlbumsByGenre,
    BROWSE_PAGE_SIZE,
    type GenreCount
  } from '$lib/api/browse';
  import AlbumCard from '$lib/components/AlbumCard.svelte';
  import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte';
  import EmptyState from '$lib/components/EmptyState.svelte';
  import QuickFilter from '$lib/components/QuickFilter.svelte';
  import type { AlbumRef } from '$lib/api/types';

  const indexStore = createGenresQuery();
  const index = $derived($indexStore);
  const genres = $derived(index.data ?? []);

  // Selection rides a query parameter rather than a route segment: "Rock/Pop"
  // is a real ID3 tag and a slash cannot survive a path — neither the server's
  // router nor an SPA-fallback reload would reconstruct it.
  const selected = $derived(page.url.searchParams.get('g') ?? '');

  let filter = $state('');
  const filteredGenres = $derived.by(() => {
    const q = filter.trim().toLowerCase();
    if (!q) return genres;
    return genres.filter((g: GenreCount) => g.genre.toLowerCase().includes(q));
  });

  // Count-first by default because that's what the server returns and it's the
  // right default: the head of this list is genuinely where you're going. But a
  // real library runs to several hundred genres with a long tail of one-offs
  // (391 / ~3.7 per track on the operator's), and at that size "I know roughly
  // what it's called" needs A-Z as much as filtering does.
  //
  // View state only, not a query parameter — same as `filter` above. Neither is
  // worth making shareable, and putting one in the URL and not the other would
  // be the inconsistent choice.
  let sortMode = $state<'count' | 'name'>('count');

  const visibleGenres = $derived.by(() => {
    // Copy before sorting. With no filter applied `filteredGenres` IS the array
    // held by the query cache, and Array.sort mutates in place — sorting it
    // directly would reorder cached data for every other consumer.
    const list = [...filteredGenres];
    if (sortMode === 'name') {
      // sensitivity 'base' so case and accents don't split neighbours apart.
      return list.sort((a: GenreCount, b: GenreCount) =>
        a.genre.localeCompare(b.genre, undefined, { sensitivity: 'base' })
      );
    }
    return list; // server order: count DESC, then name
  });

  let albums = $state<AlbumRef[]>([]);
  let total = $state(0);
  let loading = $state(false);
  let failed = $state(false);

  // Plain `let`, deliberately not $state: it's read inside the fetch path, and
  // as reactive state that read would make this effect depend on its own
  // writes. Its only job is to let a late response for a previous genre be
  // discarded rather than painted over the current one.
  let requestToken = 0;

  $effect(() => {
    const g = selected; // the only tracked read — reload when selection moves
    void reload(g);
  });

  async function reload(genre: string) {
    requestToken += 1;
    albums = [];
    total = 0;
    failed = false;
    if (!genre) return;
    await fetchPage(genre, 0, requestToken);
  }

  async function fetchPage(genre: string, offset: number, token: number) {
    loading = true;
    try {
      const p = await listAlbumsByGenre(genre, BROWSE_PAGE_SIZE, offset);
      if (token !== requestToken) return; // selection moved on; drop it
      albums = offset === 0 ? p.items : [...albums, ...p.items];
      total = p.total;
    } catch {
      if (token === requestToken) failed = true;
    } finally {
      if (token === requestToken) loading = false;
    }
  }

  function loadMore() {
    void fetchPage(selected, albums.length, requestToken);
  }

  function genreHref(genre: string): string {
    return `/library/genres?g=${encodeURIComponent(genre)}`;
  }
</script>

<svelte:head>
  <title>{pageTitle(selected ? `Library · ${selected}` : 'Library · Genres')}</title>
</svelte:head>

{#if selected}
  <div class="space-y-4">
    <header class="space-y-2">
      <a
        href="/library/genres"
        class="inline-flex items-center gap-1 text-sm text-accent hover:underline"
      >
        <ChevronLeft size={14} aria-hidden="true" />
        All genres
      </a>
      <div>
        <h1 class="font-display text-2xl font-medium text-text-primary">{selected}</h1>
        {#if !loading || albums.length > 0}
          <p class="text-sm text-text-secondary">
            {total} {total === 1 ? 'album' : 'albums'}
          </p>
        {/if}
      </div>
    </header>

    {#if failed}
      <p class="text-sm text-action-destructive">
        Couldn't load albums for this genre.
        <button
          type="button"
          class="underline hover:no-underline"
          onclick={() => reload(selected)}>Try again</button
        >
      </p>
    {:else if loading && albums.length === 0}
      <p class="text-text-secondary">Loading…</p>
    {:else if albums.length === 0}
      <!-- Reachable when a genre exists in the index but its albums have since
           been rescanned away. Not the multi-genre bug that made this whole
           surface worth care — the server splits on both sides now. -->
      <EmptyState
        title="No albums for this genre"
        hint="The library may have been rescanned since this list was built."
      />
    {:else}
      <div
        class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6"
      >
        {#each albums as album (album.id)}
          <AlbumCard {album} />
        {/each}
      </div>
      {#if albums.length < total}
        <div class="flex justify-center py-2">
          <button
            type="button"
            class="rounded-md border border-border px-4 py-2 text-sm hover:bg-surface-hover
                   focus-visible:ring-2 focus-visible:ring-accent disabled:opacity-50"
            disabled={loading}
            onclick={loadMore}
          >
            {loading ? 'Loading…' : `Load more (${total - albums.length} left)`}
          </button>
        </div>
      {:else}
        <p class="py-2 text-center text-sm text-text-secondary">End of genre</p>
      {/if}
    {/if}
  </div>
{:else}
  <div class="space-y-4">
    <header class="flex flex-wrap items-end justify-between gap-3">
      <div>
        <h1 class="font-display text-2xl font-medium text-text-primary">Genres</h1>
        {#if !index.isPending && !index.isError}
          <p class="text-sm text-text-secondary">
            {#if filter.trim()}
              {visibleGenres.length} of {genres.length} genres
            {:else}
              {genres.length} {genres.length === 1 ? 'genre' : 'genres'}, straight from your file tags
            {/if}
          </p>
        {/if}
      </div>
      {#if genres.length > 0}
        <div class="flex flex-wrap items-center gap-2">
          <QuickFilter bind:value={filter} placeholder="Filter genres" />
          <label class="flex items-center gap-1.5 text-xs text-text-secondary">
            Sort
            <select
              bind:value={sortMode}
              aria-label="Sort genres"
              class="rounded border border-border bg-surface px-2 py-1 text-sm text-text-primary
                     focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
            >
              <option value="count">Most tracks</option>
              <option value="name">A–Z</option>
            </select>
          </label>
        </div>
      {/if}
    </header>

    {#if index.isError}
      <ApiErrorBanner error={index.error} onRetry={index.refetch} />
    {:else if index.isPending}
      <p class="text-text-secondary">Loading…</p>
    {:else if genres.length === 0}
      <EmptyState
        title="No genres found"
        hint="Genres come from the genre tag on your audio files. If your library is tagged but this is empty, try a rescan."
      >
        {#snippet actions()}
          <a
            href="/admin"
            class="inline-flex items-center rounded-md bg-action-secondary px-4 py-2 text-sm text-action-fg hover:opacity-90"
          >
            Open admin
          </a>
        {/snippet}
      </EmptyState>
    {:else if filter.trim() && visibleGenres.length === 0}
      <p class="text-text-secondary">
        No genres match <span class="font-medium">'{filter.trim()}'</span>.
      </p>
    {:else}
      <!-- Defaults to track count rather than alphabetical: raw tags carry a
           long tail of one-offs, so A-Z would bury the handful of genres you
           actually have a library's worth of. The Sort control lets you ask for
           A-Z when you already know roughly what you're looking for. -->
      <ul class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
        {#each visibleGenres as g (g.genre)}
          <li>
            <a
              href={genreHref(g.genre)}
              class="flex items-center justify-between gap-3 rounded-md border border-border
                     bg-surface px-3 py-2 hover:bg-surface-hover
                     focus-visible:ring-2 focus-visible:ring-accent"
            >
              <span class="truncate text-text-primary">{g.genre}</span>
              <span class="flex-shrink-0 text-sm text-text-secondary">
                {g.track_count}
              </span>
            </a>
          </li>
        {/each}
      </ul>
    {/if}
  </div>
{/if}
