// Home / Entdecken — browse the accepted catalog and audition tracks.
//
// Three sections, top to bottom:
//
//   1. Hero       — sunset-gradient banner featuring the top-streamed
//                   track. Uses a separate one-shot fetch (limit=1,
//                   sort=streams) so the hero doesn't depend on which
//                   page of the library is currently visible.
//   2. Frisch     — 4-tile rail of the most-recently-accepted tracks.
//                   Also a one-shot fetch (limit=4, sort=newest).
//   3. Bibliothek — full sortable + searchable table backed by the
//                   server's paginated catalog endpoint. 50 rows per
//                   page, sort + tag filter + search live on the
//                   server so a 10k-track catalog stays cheap.
//
// All three sections hit the same `/api/submissions/catalog` endpoint
// with different params — server-side filtering means the browser
// never receives the full library at once. The legacy `submissions`
// prop is still accepted for any callers that pre-warm it but is
// otherwise unused now.

const PAGE_SIZE = 50;

// Sort options exposed in the library dropdown. Each maps to a `sort`
// query param the server understands.
const SORT_OPTIONS = [
  { id: 'streams',       label: 'Meist gehört' },
  { id: 'streams-asc',   label: 'Weniger gehört' },
  { id: 'newest',        label: 'Neueste zuerst' },
  { id: 'oldest',        label: 'Älteste zuerst' },
  { id: 'title',         label: 'Titel A–Z' },
  { id: 'title-desc',    label: 'Titel Z–A' },
  { id: 'artist',        label: 'Künstler A–Z' },
  { id: 'artist-desc',   label: 'Künstler Z–A' },
  { id: 'duration-asc',  label: 'Kürzeste zuerst' },
  { id: 'duration-desc', label: 'Längste zuerst' },
];

// Column-header → (asc id, desc id) sort pairs. Clicking a header
// cycles through its pair (or switches column with that pair's ASC
// direction as the default — except streams which defaults to DESC
// because "most popular first" is the natural expectation).
//
// `#` and `Tags` columns are intentionally absent — # is just the
// view position (would loop infinitely if it tried to "sort by
// index"), Tags is a multi-value column with no single sort key.
const COLUMN_SORTS = {
  title:    { asc: 'title',         desc: 'title-desc' },
  artist:   { asc: 'artist',        desc: 'artist-desc' },
  streams:  { asc: 'streams-asc',   desc: 'streams',       defaultDir: 'desc' },
  duration: { asc: 'duration-asc',  desc: 'duration-desc' },
};

// Duration buckets — picked to roughly match how listeners think
// about tracks. "kurz" is single/snippet territory, "mittel" is the
// bulk of regular tracks, "lang" is extended cut / instrumental.
const DURATION_OPTIONS = [
  { id: 'short',  label: 'Kurz',   hint: '< 2 min' },
  { id: 'medium', label: 'Mittel', hint: '2–4 min' },
  { id: 'long',   label: 'Lang',   hint: '> 4 min' },
];

// Timeframe filter — matches the server's SINCE_MS table. "Alle Zeit"
// is the absence of any timeframe filter; the chip just resets to ''.
const TIMEFRAME_OPTIONS = [
  { id: 'day',   label: 'Heute' },
  { id: 'week',  label: '7 Tage' },
  { id: 'month', label: '30 Tage' },
  { id: 'year',  label: '1 Jahr' },
];

function HomeScreen({ me, refresh: refreshApp }) {
  const audio = useAudioStatus();
  const isAdmin = canForceAccept(me);

  // Hero + curated discovery rails. All landed in one call to
  // /api/submissions/discover (server-cached 5 min). Plus per-user
  // "Dein Mix" via /api/me/mix.
  //
  // Sections rendered (top to bottom):
  //   • Hero          — top track of the last 7 days
  //   • Frisch        — most-recently accepted
  //   • Dein Mix       — personalised by tag overlap, daily-rotated
  //   • Im Trend (5 Tage) — most-played tracks across all listeners
  //   • Meist gehört  — highest stream count ever (RANKED)
  //   • Bibliothek    — full sortable + searchable table (below)
  //
  // "Zuletzt gehört" used to live on this page but was moved to
  // Profile-only — it's already there and the discover stack was
  // getting heavy with too many rails.
  const [hero, setHero] = React.useState(null);
  const [rail, setRail] = React.useState([]);        // newest (back-compat name)
  const [trending5d, setTrending5d] = React.useState([]);
  const [topAllTime, setTopAllTime] = React.useState([]);
  const [myMix, setMyMix] = React.useState([]);

  // Library state — sort/tag/duration/since/search/page all drive
  // the same paginated server query below.
  const [query, setQuery] = React.useState('');
  const [sort, setSort] = React.useState('streams');
  // Initial tag state can be pre-set by the global openHomeWithTag()
  // helper — clicking a tag chip in the Big Player or anywhere else
  // stashes the desired tag, then route-pushes to home. We consume
  // the global on mount so a refresh or re-navigation without the
  // stash gets the empty default.
  const [tag, setTag] = React.useState(() => {
    const stash = window.__pendingHomeTag;
    if (stash) { delete window.__pendingHomeTag; return stash; }
    return '';
  });
  const [duration, setDuration] = React.useState(''); // '' / 'short' / 'medium' / 'long'
  const [since, setSince] = React.useState('');       // '' / 'day' / 'week' / 'month' / 'year'
  const [page, setPage] = React.useState(0);
  const [library, setLibrary] = React.useState({ items: [], total: 0 });
  const [loading, setLoading] = React.useState(true);
  const [availableTags, setAvailableTags] = React.useState([]);

  // Has ANY filter been applied? Controls whether the "Filter zurücksetzen"
  // button is shown — keeps the chrome clean when nothing's filtering.
  const hasActiveFilter = !!(query.trim() || tag || duration || since || sort !== 'streams');

  const resetFilters = () => {
    setQuery('');
    setTag('');
    setDuration('');
    setSince('');
    setSort('streams');
    setPage(0);
  };

  // Debounce the search input — React 18's useDeferredValue avoids
  // hitting the server on every keystroke. Typing "deutschrap" then
  // fires one query, not eleven.
  const deferredQuery = React.useDeferredValue(query);

  // Boot-time load: discover bundle + user mix + tag-filter chips.
  // Fired in parallel; the rails are independent so a slow query on
  // one doesn't block another. The user's recent-plays list is NOT
  // pulled here any more (it lives on the profile screen).
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const [discRes, mixRes, tagsRes] = await Promise.all([
          Api.discover(),
          Api.mix(),
          Api.availableTags(),
        ]);
        if (cancelled) return;
        // Hero is the top of the last 7 days (from discover.hero). If
        // nobody has played anything all week, the server falls back to
        // newest in that response, so we just trust whatever it sends.
        setHero(discRes.hero);
        setRail(discRes.newest || []);
        setTrending5d(discRes.trending5d || []);
        setTopAllTime(discRes.topAllTime || []);
        setMyMix(mixRes.tracks || []);
        setAvailableTags(tagsRes);
      } catch (e) {
        toast.error('Katalog konnte nicht geladen werden: ' + e.message);
      }
    })();
    return () => { cancelled = true; };
  }, []);

  // Library query — re-fires whenever the user changes any filter or
  // navigates the page. All filter dimensions go in the same Api.catalog
  // call so the server returns a fully-narrowed slice.
  const loadLibrary = React.useCallback(async () => {
    setLoading(true);
    try {
      const out = await Api.catalog({
        status: 'accepted',
        sort, tag, duration, since, q: deferredQuery,
        limit: PAGE_SIZE, offset: page * PAGE_SIZE,
      });
      setLibrary(out);
    } catch (e) {
      toast.error('Bibliothek laden fehlgeschlagen: ' + e.message);
    } finally {
      setLoading(false);
    }
  }, [sort, tag, duration, since, deferredQuery, page]);

  React.useEffect(() => { loadLibrary(); }, [loadLibrary]);

  // Reset to page 0 whenever any filter changes — otherwise a new
  // filter strands the user on "page 5 of 1 result". Page change
  // itself is the only state that doesn't trigger a reset.
  React.useEffect(() => { setPage(0); }, [sort, tag, duration, since, deferredQuery]);

  // Re-pull catalog data after an admin action (cover change, edit,
  // delete) so the row reflects the latest state. Wraps the app's
  // refresh and also re-pulls the current page.
  const refreshAll = async () => {
    if (refreshApp) await refreshApp();
    await loadLibrary();
  };

  const play = (track) => audio.play(track);

  // Empty-catalog state — show before the first library load if the
  // hero query came back with nothing.
  if (hero === null && !loading && library.total === 0 && !deferredQuery && !tag) {
    return (
      <div style={{ padding: '60px 32px 140px', maxWidth: 620, margin: '0 auto' }}>
        <EmptyState
          icon="music" iconCircle
          radius={28} orb="var(--lila-500)" size="lg"
          title="Noch keine Tracks freigegeben"
          description="Sobald die ersten Tracks die Prüfung passieren, tauchen sie hier auf."
        />
      </div>
    );
  }

  const totalPages = Math.max(1, Math.ceil(library.total / PAGE_SIZE));

  return (
    <div style={{ padding: '32px 36px 140px', maxWidth: 1240, margin: '0 auto' }}>

      {/* HERO — featured track, sunset gradient */}
      {hero && (
        <Glass radius={28} padding={0} style={{
          marginBottom: 28, background: 'var(--grad-palmbar)',
          border: '1px solid var(--border-warm)', overflow: 'hidden',
        }}>
          <div style={{
            position: 'relative', padding: 36,
            display: 'flex', alignItems: 'center', gap: 28,
          }}>
            <div style={{
              position: 'absolute', top: '-30%', right: '-10%', width: 420, height: 420,
              borderRadius: '50%', background: 'var(--gold)',
              filter: 'blur(80px)', opacity: 0.35, pointerEvents: 'none',
            }}/>
            <div style={{ position: 'relative', flexShrink: 0 }}>
              <Cover colors={hero.cover} src={hero.coverUrl} size={180} radius={24}
                style={{ boxShadow: '0 24px 60px rgba(20,8,40,0.55)' }}/>
              <button
                onClick={() => play(hero)}
                aria-label={audio.currentId === hero.id && audio.isPlaying ? 'Pausieren' : 'Abspielen'}
                style={{
                  position: 'absolute', right: -14, bottom: -14,
                  width: 60, height: 60, borderRadius: '50%',
                  background: 'var(--foam)', border: 'none', cursor: 'pointer',
                  color: 'var(--lila-700)', display: 'flex', alignItems: 'center', justifyContent: 'center',
                  boxShadow: '0 12px 36px rgba(20,8,40,0.55)',
                }}
              >
                <ApIcon name={audio.currentId === hero.id && audio.isPlaying ? 'pause' : 'play'}
                  size={20} style={{ marginLeft: 2 }}/>
              </button>
            </div>

            <div style={{ flex: 1, position: 'relative', color: 'var(--foam)' }}>
              <Eyebrow style={{ color: 'rgba(255,247,236,0.7)', marginBottom: 10 }}>
                {hero.recentPlays > 0
                  ? `Top diese Woche · ${numDE(hero.recentPlays)} Plays`
                  : (hero.streams > 0 ? `Meist gehört · ${numDE(hero.streams)} Streams` : 'Frisch freigegeben')}
              </Eyebrow>
              <h1 style={{
                fontWeight: 800, fontSize: 'clamp(2.2rem, 4.5vw, 3.5rem)', lineHeight: 1, letterSpacing: '-0.02em',
                margin: '0 0 10px', color: 'var(--foam)',
              }}>{hero.title}</h1>
              <div style={{ fontSize: 18, color: 'rgba(255,247,236,0.85)', marginBottom: 18 }}>
                <ArtistLink identifier={hero.identifier} name={hero.artist}
                  size={18} color="rgba(255,247,236,0.85)"/>
                {' · '}{fmt(hero.duration)}
              </div>
              <div style={{ display: 'flex', gap: 10 }}>
                <Button variant="reward" size="lg" icon="play" onClick={() => play(hero)}>
                  Jetzt hören
                </Button>
              </div>
            </div>
          </div>
        </Glass>
      )}

      {/* Section order, top to bottom:
            1. Frisch veröffentlicht  — what's brand new
            2. Dein Mix               — personalised
            3. Im Trend (5 Tage)       — what's catching on (RANKED)
            4. Meist gehört            — all-time classics (RANKED)
            5. Zuletzt gehört         — your own recent history
          Bibliothek table follows below.
          The two "ranked" rails get gold/silver/bronze badges on
          the top 3 tiles (see TrackTile's `rank` prop). */}

      {/* RAIL — Frisch veröffentlicht */}
      {rail.length > 0 && (
        <RailSection
          eyebrow={`${library.total} freigegeben`}
          title="Frisch veröffentlicht"
          tracks={rail.slice(0, 8)}
          play={play}
          audio={audio}/>
      )}

      {/* RAIL — Dein Mix. Personalised by tag overlap with the user's
          last-5-days listening, daily-rotated. Empty for users with
          no recent listening history. Same algorithm as the in-game
          phone produces so the mix is consistent across surfaces. */}
      {myMix.length > 0 && (
        <RailSection
          eyebrow="Für dich"
          title="Dein Mix"
          tracks={myMix.slice(0, 8)}
          play={play}
          audio={audio}/>
      )}

      {/* RAIL — Im Trend (5 Tage). Global, most-played in a rolling
          5-day window. RANKED: top 3 get gold/silver/bronze badges. */}
      {trending5d.length > 0 && (
        <RailSection
          eyebrow="5 Tage"
          title="Im Trend"
          tracks={trending5d.slice(0, 8)}
          play={play}
          audio={audio}
          ranked/>
      )}

      {/* RAIL — Meist gehört (All-Time). Highest stream counts ever.
          RANKED: top 3 get gold/silver/bronze badges. */}
      {topAllTime.length > 0 && (
        <RailSection
          eyebrow="All-Time"
          title="Meist gehört"
          tracks={topAllTime.slice(0, 8)}
          play={play}
          audio={audio}
          ranked/>
      )}

      {/* "Zuletzt gehört" rail removed — the profile screen already
          has a "Zuletzt gehört" section and the home discover stack
          was getting heavy. Lives only on the profile page now. */}

      {/* LIBRARY HEADER — title + sort + search.
          Filter rows live below; this row only handles the primary
          sort selector and free-text search. */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 14, marginBottom: 16, flexWrap: 'wrap' }}>
        <h2 style={{
          fontWeight: 800, fontSize: 28, lineHeight: 1.05, letterSpacing: '-0.02em', margin: 0,
        }}>Vollständige Bibliothek</h2>
        <div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
          <SortDropdown value={sort} onChange={setSort}/>
          <div style={{ width: 240 }}>
            <TextInput value={query} onChange={(e) => setQuery(e.target.value)}
              placeholder="Suchen…" icon="search"/>
          </div>
        </div>
      </div>

      {/* Filter rows. All three (Tags / Dauer / Zeitraum) are
          always visible — easier than collapsing them behind a "more
          filters" toggle, and the three rows fit comfortably above
          the table on any reasonable viewport. */}

      {/* TAG FILTER chips. Tags sorted by global frequency server-side
          so the most common float left. */}
      {availableTags.length > 0 && (
        <FilterRow label="Tags">
          <FilterChip active={!tag} onClick={() => setTag('')}>Alle</FilterChip>
          {availableTags.slice(0, 18).map(({ tag: t, count }) => (
            <FilterChip key={t} active={tag === t} onClick={() => setTag(tag === t ? '' : t)}>
              {t} <span style={{ opacity: 0.55 }}>· {count}</span>
            </FilterChip>
          ))}
        </FilterRow>
      )}

      {/* DURATION + TIMEFRAME — two parallel filter rows. Either both
          chips inactive (no filter applied) OR exactly one active per
          row. Clicking an active chip toggles it off again. */}
      <FilterRow label="Dauer">
        <FilterChip active={!duration} onClick={() => setDuration('')}>Alle</FilterChip>
        {DURATION_OPTIONS.map(o => (
          <FilterChip key={o.id} active={duration === o.id}
            onClick={() => setDuration(duration === o.id ? '' : o.id)}>
            {o.label} <span style={{ opacity: 0.55 }}>· {o.hint}</span>
          </FilterChip>
        ))}
      </FilterRow>

      <FilterRow label="Zeitraum">
        <FilterChip active={!since} onClick={() => setSince('')}>Alle</FilterChip>
        {TIMEFRAME_OPTIONS.map(o => (
          <FilterChip key={o.id} active={since === o.id}
            onClick={() => setSince(since === o.id ? '' : o.id)}>
            {o.label}
          </FilterChip>
        ))}
        {hasActiveFilter && (
          <button onClick={resetFilters} style={{
            marginLeft: 'auto', padding: '6px 12px', borderRadius: 999, cursor: 'pointer',
            background: 'transparent', border: '1px solid var(--border-soft)',
            color: 'var(--fg-3)', fontSize: 11.5, fontWeight: 700, letterSpacing: 0.3,
            display: 'inline-flex', alignItems: 'center', gap: 6,
            fontFamily: 'inherit',
          }}>
            <ApIcon name="x" size={10}/> Filter zurücksetzen
          </button>
        )}
      </FilterRow>

      <Glass radius={20} padding={0}>
        {/* Column headers. Clickable for sortable columns. Different
            shape with vs. without admin because the admin column adds
            the three-dot menu placeholder. */}
        <div style={{
          display: 'grid',
          // Trailing-action column is always present now (Share +
          // 3-dot menu live there for every user), so the header
          // grid mirrors the row grid 1:1.
          gridTemplateColumns: '48px 1fr 180px 220px 90px 90px 80px',
          gap: 12, padding: '12px 22px',
          borderBottom: '1px solid var(--border-soft)',
          fontSize: 11, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'var(--fg-3)',
        }}>
          <div style={{ textAlign: 'center' }}>#</div>
          <SortHeader column="title"    label="Titel"     sort={sort} setSort={setSort}/>
          <SortHeader column="artist"   label="Künstler"  sort={sort} setSort={setSort}/>
          <div>Tags</div>
          <SortHeader column="streams"  label="Streams"   sort={sort} setSort={setSort} align="right"/>
          <SortHeader column="duration" label="Länge"     sort={sort} setSort={setSort} align="right"/>
          <div/>
        </div>

        {loading && library.items.length === 0 ? (
          <div style={{ padding: 28, textAlign: 'center', color: 'var(--fg-3)', fontSize: 13 }}>
            Lädt…
          </div>
        ) : library.items.length === 0 ? (
          <div style={{ padding: 28, textAlign: 'center', color: 'var(--fg-3)', fontSize: 13 }}>
            {deferredQuery ? `Keine Treffer für „${deferredQuery}"` :
             tag ? `Keine Tracks mit dem Tag „${tag}"` :
             'Keine Tracks gefunden.'}
          </div>
        ) : library.items.map((t, i) => (
          <LibraryRow key={t.id} idx={page * PAGE_SIZE + i + 1} track={t} onPlay={play}
            active={audio.currentId === t.id}
            isPlaying={audio.isPlaying && audio.currentId === t.id}
            isAdmin={isAdmin}
            onTagClick={setTag}
            refresh={refreshAll}/>
        ))}

        {totalPages > 1 && (
          <div style={{
            display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 14,
            padding: '14px 22px', borderTop: '1px solid var(--border-hairline)', flexWrap: 'wrap',
          }}>
            <span style={{ fontSize: 12, color: 'var(--fg-3)' }}>
              <span style={{ fontVariantNumeric: 'tabular-nums', color: 'var(--fg-2)' }}>
                {page * PAGE_SIZE + 1}-{Math.min(library.total, (page + 1) * PAGE_SIZE)}
              </span>
              {' '}von{' '}
              <span style={{ fontVariantNumeric: 'tabular-nums', color: 'var(--fg-2)' }}>
                {numDE(library.total)}
              </span>
            </span>
            <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
              <Button variant="ghost" size="sm" icon="chevron-left"
                disabled={page <= 0}
                onClick={() => setPage(p => Math.max(0, p - 1))}>
                Zurück
              </Button>
              <span style={{ fontSize: 12, color: 'var(--fg-3)', fontVariantNumeric: 'tabular-nums', padding: '0 8px' }}>
                Seite {page + 1} / {totalPages}
              </span>
              <Button variant="ghost" size="sm" iconRight="chevron-right"
                disabled={page >= totalPages - 1}
                onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))}>
                Weiter
              </Button>
            </div>
          </div>
        )}
      </Glass>
    </div>
  );
}

// Clickable column header. Reads the current `sort` to figure out
// whether this column is active + which direction; click cycles
// asc ↔ desc on the active column, or switches to this column on
// any other. Renders a chevron indicator when active.
function SortHeader({ column, label, sort, setSort, align = 'left' }) {
  const pair = COLUMN_SORTS[column];
  // Defensive — if a caller passes a column we don't have in the
  // map, render a static label so we don't crash.
  if (!pair) return <div style={{ textAlign: align }}>{label}</div>;

  const isAsc  = sort === pair.asc;
  const isDesc = sort === pair.desc;
  const active = isAsc || isDesc;

  const onClick = () => {
    if (isAsc)  return setSort(pair.desc);
    if (isDesc) return setSort(pair.asc);
    // First click on a fresh column — use the column's natural
    // direction. Streams defaults to DESC ("popular first"),
    // everything else to ASC.
    setSort(pair.defaultDir === 'desc' ? pair.desc : pair.asc);
  };

  return (
    <button onClick={onClick} style={{
      background: 'transparent', border: 'none', padding: 0, cursor: 'pointer',
      display: 'inline-flex', alignItems: 'center', gap: 5,
      // Right-aligned headers need their flex to push to the end,
      // and the chevron lives AFTER the label so the visual order
      // matches the left-aligned headers' flow.
      justifyContent: align === 'right' ? 'flex-end' : 'flex-start',
      width: '100%',
      // Inherit the eyebrow text styling so the headers look
      // identical to non-sortable ones at rest.
      fontSize: 'inherit', fontWeight: 'inherit', letterSpacing: 'inherit',
      textTransform: 'inherit', fontFamily: 'inherit',
      color: active ? 'var(--coral)' : 'var(--fg-3)',
      transition: 'color 150ms var(--ease-out)',
    }}>
      {label}
      <ApIcon
        name={isAsc ? 'chevron-up' : isDesc ? 'chevron-down' : 'sort'}
        size={9}
        style={{ opacity: active ? 1 : 0.4 }}
      />
    </button>
  );
}

// Sort dropdown — native <select> styled to look like the rest of
// the Beachy-Lila form controls. Native because a custom popover
// would need keyboard/focus handling we don't have right now.
function SortDropdown({ value, onChange }) {
  return (
    <div style={{ position: 'relative' }}>
      <ApIcon name="bars-staggered" size={13}
        style={{ position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)', color: 'var(--fg-3)', pointerEvents: 'none' }}/>
      <select value={value} onChange={(e) => onChange(e.target.value)}
        style={{
          appearance: 'none', WebkitAppearance: 'none',
          padding: '10px 36px 10px 34px', borderRadius: 14,
          background: 'var(--bg-input)',
          border: '1px solid var(--border-soft)',
          color: 'var(--foam)', fontSize: 13, fontFamily: 'inherit', fontWeight: 600,
          outline: 'none', cursor: 'pointer',
        }}>
        {SORT_OPTIONS.map(o => (
          <option key={o.id} value={o.id} style={{ background: '#1F0E3D', color: 'var(--foam)' }}>
            {o.label}
          </option>
        ))}
      </select>
      <ApIcon name="chevron-down" size={11}
        style={{ position: 'absolute', right: 12, top: '50%', transform: 'translateY(-50%)', color: 'var(--fg-3)', pointerEvents: 'none' }}/>
    </div>
  );
}

// Filter row container — small uppercase label on the left, chip
// cluster on the right. Three rows (Tags / Dauer / Zeitraum) all use
// this so they line up visually and the label tells the user what
// dimension each row controls.
function FilterRow({ label, children }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 12, marginBottom: 10,
      flexWrap: 'wrap',
    }}>
      <div style={{
        fontSize: 10.5, fontWeight: 700, letterSpacing: '0.18em',
        textTransform: 'uppercase', color: 'var(--fg-4)',
        minWidth: 64,
      }}>
        {label}
      </div>
      <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center', flex: 1 }}>
        {children}
      </div>
    </div>
  );
}

// Reusable filter chip (also used in queue.jsx and audit.jsx). The
// "active" variant tints with a soft lila wash; inactive sits
// transparent with a hairline border.
function FilterChip({ active, onClick, color = 'var(--lila-300)', children }) {
  return (
    <button onClick={onClick} style={{
      padding: '6px 12px', borderRadius: 999, cursor: 'pointer',
      background: active ? `${color}26` : 'transparent',
      border: active ? `1px solid ${color}66` : '1px solid var(--border-soft)',
      color: active ? color : 'var(--fg-2)',
      fontSize: 12, fontWeight: 700, letterSpacing: 0.3,
      fontFamily: 'inherit',
      transition: 'all 150ms var(--ease-out)',
    }}>
      {children}
    </button>
  );
}

// Shared section wrapper for the home-page tile rails. Renders an
// eyebrow + h2 + horizontal carousel with up to 4 tiles visible at
// once and arrow buttons that scroll the rest into view.
//
// Layout: `grid-auto-flow: column` + `grid-auto-columns` calculated
// so exactly 4 cards fit in the visible viewport (accounting for the
// 16px gaps between them). Tiles beyond 4 sit off-screen to the
// right; scroll-snap-x keeps each scroll click aligned to the start
// of a card.
//
// `ranked` prop: forwards a 1-based rank to each tile so the first 3
// can render gold/silver/bronze badges (see TrackTile). Used by the
// "Im Trend" and "Meist gehört" rails.
function RailSection({ eyebrow, title, tracks, play, audio, ranked = false }) {
  const scrollRef = React.useRef(null);
  const [overflow, setOverflow] = React.useState({ left: false, right: false });

  // Track which arrow(s) should be enabled. Re-measure after mount +
  // on scroll + on window resize so a layout change doesn't leave a
  // stale arrow lit (or dark) when the user can actually still scroll.
  const measure = React.useCallback(() => {
    const el = scrollRef.current;
    if (!el) return;
    setOverflow({
      left:  el.scrollLeft > 4,
      right: el.scrollLeft + el.clientWidth < el.scrollWidth - 4,
    });
  }, []);
  React.useEffect(() => {
    measure();
    const el = scrollRef.current;
    if (!el) return;
    el.addEventListener('scroll', measure, { passive: true });
    window.addEventListener('resize', measure);
    return () => {
      el.removeEventListener('scroll', measure);
      window.removeEventListener('resize', measure);
    };
  }, [measure, tracks.length]);

  // Step = 2 cards at a time (half the visible row). Less jarring
  // than jumping a full page; matches the Spotify / Apple Music
  // carousel feel.
  const scrollBy = (dir) => {
    const el = scrollRef.current;
    if (!el) return;
    const card = el.querySelector('[data-tile]');
    const step = card
      ? (card.getBoundingClientRect().width + 16) * 2
      : 320;
    el.scrollBy({ left: dir * step, behavior: 'smooth' });
  };

  const hasArrows = tracks.length > 4;

  return (
    <div style={{ marginBottom: 36 }}>
      <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', marginBottom: 16, gap: 14 }}>
        <div>
          <Eyebrow style={{ marginBottom: 4 }}>{eyebrow}</Eyebrow>
          <h2 style={{
            fontWeight: 800, fontSize: 28, lineHeight: 1.05, letterSpacing: '-0.02em', margin: 0,
          }}>{title}</h2>
        </div>
        {hasArrows && (
          <div style={{ display: 'flex', gap: 8 }}>
            <ArrowButton dir="left"  disabled={!overflow.left}  onClick={() => scrollBy(-1)}/>
            <ArrowButton dir="right" disabled={!overflow.right} onClick={() => scrollBy(+1)}/>
          </div>
        )}
      </div>

      <div ref={scrollRef} style={{
        // 4 cards visible at once. `calc((100% - 48px) / 4)` =
        // (full width - 3 gaps × 16px) ÷ 4. Each card width = the
        // computed column width. Cards beyond 4 sit off-screen and
        // become accessible via the arrow buttons / scrollbar.
        display: 'grid',
        gridAutoFlow: 'column',
        gridAutoColumns: 'calc((100% - 48px) / 4)',
        gap: 16,
        overflowX: 'auto',
        scrollSnapType: 'x mandatory',
        scrollbarWidth: 'none',
        // padding-bottom so the box-shadow under hovered tiles
        // doesn't get clipped by the overflow:hidden parent.
        padding: '4px 2px 12px',
        margin: '0 -2px',
      }}>
        {tracks.map((t, idx) => (
          <div key={t.id} data-tile style={{ scrollSnapAlign: 'start' }}>
            <TrackTile track={t} onPlay={play}
              active={audio.currentId === t.id && audio.isPlaying}
              rank={ranked ? idx + 1 : null}/>
          </div>
        ))}
      </div>
    </div>
  );
}

// Carousel arrow button — disabled state fades out so the user can
// see when they've hit the end. Hover bump matches the Glass-card
// idiom used elsewhere.
function ArrowButton({ dir, onClick, disabled }) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      aria-label={dir === 'left' ? 'Zurück' : 'Weiter'}
      style={{
        width: 36, height: 36, borderRadius: '50%',
        background: 'rgba(196,154,255,0.10)',
        border: '1px solid var(--border-soft)',
        color: disabled ? 'var(--fg-4)' : 'var(--foam)',
        cursor: disabled ? 'default' : 'pointer',
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
        opacity: disabled ? 0.4 : 1,
        transition: 'opacity 200ms var(--ease-out), background 200ms var(--ease-out)',
      }}
    >
      <ApIcon name={dir === 'left' ? 'chevron-left' : 'chevron-right'} size={14}/>
    </button>
  );
}

// Rank-badge styling for the top 3 tiles in "ranked" rails (Im
// Trend / Meist gehört). Gold / silver / bronze gradient with a
// matching coloured glow under the tile itself for #1 specifically
// — second and third get the badge but not the tile-level glow,
// so the leaderboard hierarchy reads at a glance.
const RANK_STYLES = {
  1: {
    badgeBg: 'linear-gradient(135deg, #fde68a, #f59e0b)',
    badgeGlow: '0 6px 22px rgba(251,191,36,0.55), inset 0 1px 0 rgba(255,255,255,0.6)',
    tileGlow: '0 14px 40px rgba(251,191,36,0.22)',
    textShadow: '0 1px 2px rgba(120,60,0,0.45)',
    color: '#3d2200',
    label: '#1',
  },
  2: {
    badgeBg: 'linear-gradient(135deg, #f3f4f6, #94a3b8)',
    badgeGlow: '0 5px 18px rgba(203,213,225,0.45), inset 0 1px 0 rgba(255,255,255,0.6)',
    tileGlow: null,
    textShadow: '0 1px 2px rgba(40,40,60,0.35)',
    color: '#1e293b',
    label: '#2',
  },
  3: {
    badgeBg: 'linear-gradient(135deg, #fdba74, #b45309)',
    badgeGlow: '0 5px 18px rgba(217,119,87,0.45), inset 0 1px 0 rgba(255,255,255,0.5)',
    tileGlow: null,
    textShadow: '0 1px 2px rgba(80,30,0,0.4)',
    color: '#3a1a00',
    label: '#3',
  },
};

// Tile (single track in any of the home-page rails). The optional
// `rank` prop turns on the leaderboard badge in the top-left of
// the cover — see RANK_STYLES for the gold/silver/bronze palette.
// Ranks beyond 3 don't get a badge (tiles 4-8 stay clean).
function TrackTile({ track, onPlay, active, rank }) {
  const [hover, setHover] = React.useState(false);
  const rankStyle = rank && rank <= 3 ? RANK_STYLES[rank] : null;

  return (
    <div
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      onClick={() => onPlay(track)}
      style={{ cursor: 'pointer' }}
    >
      <Glass radius={18} padding={14} hover
        style={rankStyle?.tileGlow ? {
          boxShadow: `${rankStyle.tileGlow}, var(--shadow-card)`,
        } : undefined}>
        <div style={{ position: 'relative' }}>
          <Cover colors={track.cover} src={track.coverUrl}
            size="100%" radius={14}
            style={{ width: '100%', aspectRatio: '1', height: 'auto' }}/>

          {/* Rank badge — only top 3, only in "ranked" rails.
              Gold/silver/bronze gradient circle with the rank
              number. The big "#1" badge on the trending leader
              is the visual hook the user asked for. */}
          {rankStyle && (
            <div style={{
              position: 'absolute', top: 10, left: 10,
              minWidth: 44, height: 44, borderRadius: 14,
              padding: '0 10px',
              background: rankStyle.badgeBg,
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
              fontWeight: 900, fontSize: 18,
              color: rankStyle.color,
              textShadow: rankStyle.textShadow,
              boxShadow: rankStyle.badgeGlow,
              letterSpacing: '-0.02em',
              fontVariantNumeric: 'tabular-nums',
            }}>{rankStyle.label}</div>
          )}

          <button
            onClick={(e) => { e.stopPropagation(); onPlay(track); }}
            aria-label="Abspielen"
            style={{
              position: 'absolute', bottom: 10, right: 10,
              width: 44, height: 44, borderRadius: '50%',
              background: 'var(--grad-sunset)', border: 'none', cursor: 'pointer',
              color: '#3d0011', display: 'flex', alignItems: 'center', justifyContent: 'center',
              boxShadow: 'var(--glow-coral)',
              opacity: hover || active ? 1 : 0,
              transform: hover || active ? 'translateY(0)' : 'translateY(6px)',
              transition: 'opacity 200ms var(--ease-out), transform 200ms var(--ease-out)',
            }}
          >
            <ApIcon name={active ? 'pause' : 'play'} size={14} style={{ marginLeft: active ? 0 : 2 }}/>
          </button>
        </div>
        <div style={{
          fontSize: 14, fontWeight: 700, marginTop: 12, color: 'var(--foam)',
          whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
        }}>{track.title}</div>
        <div style={{
          fontSize: 12, color: 'var(--fg-3)', marginTop: 2,
          whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
        }}>{track.artist}</div>
        {track.streams > 0 && (
          <div style={{
            display: 'flex', gap: 10, marginTop: 10, fontSize: 11,
            color: 'var(--fg-3)', fontVariantNumeric: 'tabular-nums',
          }}>
            <span><ApIcon name="headphones" size={10}/> {numDE(track.streams)}</span>
          </div>
        )}
      </Glass>
    </div>
  );
}

// Single row in the library table. New since the migration:
//  • Tags column — pills showing the MAEST/CLAP genre+vibe tags
//  • Admin three-dot column — AdminMenu on the right for admins
function LibraryRow({ idx, track, onPlay, active, isPlaying, isAdmin, onTagClick, refresh }) {
  // Defensive default — pre-fix, this component was accidentally
  // shadowing the global `TrackRow` and getting called from screens
  // that don't pass an `onPlay`. Even though that path is gone, leave
  // a safe fallback in place so a stray caller can't crash the row.
  const handlePlay = onPlay || (() => AudioService.play(track));
  return (
    <div style={{
      display: 'grid',
      // Trailing actions column hosts ShareButton + the 3-dot menu
      // (the menu is open to all users — share + add-to-playlist for
      // everyone, admin power-tools added on for admins). Width is
      // fixed at 80px so both buttons fit side by side in either
      // user tier without the column reflowing.
      gridTemplateColumns: '48px 1fr 180px 220px 90px 90px 80px',
      gap: 12, padding: '12px 22px', width: '100%',
      background: active ? 'rgba(169,114,244,0.08)' : 'transparent',
      cursor: 'default', color: 'inherit', textAlign: 'left',
      alignItems: 'center', borderTop: '1px solid var(--border-hairline)',
      transition: 'background 150ms var(--ease-out)',
    }}>
      {/* Index / equaliser */}
      <div style={{
        fontSize: 13, color: active ? 'var(--coral)' : 'var(--fg-3)',
        fontVariantNumeric: 'tabular-nums', textAlign: 'center', fontWeight: active ? 700 : 500,
      }}>
        {isPlaying
          ? <ApIcon name="volume-high" size={13} color="var(--coral)"/>
          : idx
        }
      </div>

      {/* Title + cover. The whole title cell is a click-to-play
          surface so the row itself reads as the primary affordance
          while the admin menu on the right is the secondary one. */}
      <button onClick={() => handlePlay(track)}
        style={{
          display: 'flex', alignItems: 'center', gap: 12, minWidth: 0,
          background: 'transparent', border: 'none', cursor: 'pointer',
          color: 'inherit', textAlign: 'left', padding: 0, fontFamily: 'inherit',
        }}>
        <Cover colors={track.cover} src={track.coverUrl} size={40} radius={8}/>
        <div style={{ minWidth: 0 }}>
          <div style={{
            display: 'flex', alignItems: 'center', gap: 6, minWidth: 0,
          }}>
            <div style={{
              fontSize: 14, fontWeight: 600, color: active ? 'var(--coral)' : 'var(--foam)',
              whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
            }}>{track.title}</div>
            {/* Explicit + instrumental flags as inline badges next
                to the title — matches the visual idiom Spotify /
                Apple Music use. Both are mutually exclusive in
                practice (instrumental tracks have no lyrics to be
                explicit about) so they never stack. */}
            {track.isExplicit && (
              <span title="Explicit lyrics" style={{
                fontSize: 9, fontWeight: 800, padding: '1px 4px',
                borderRadius: 3, background: 'rgba(255,110,138,0.18)',
                color: 'var(--danger)', letterSpacing: 0.5,
                flexShrink: 0,
              }}>E</span>
            )}
            {track.isInstrumental && (
              <span title="Instrumental" style={{
                fontSize: 9, fontWeight: 800, padding: '1px 4px',
                borderRadius: 3, background: 'rgba(196,154,255,0.18)',
                color: 'var(--lila-200)', letterSpacing: 0.5,
                flexShrink: 0,
              }}>INSTR</span>
            )}
            {/* BPM chip — shown when known. Helpful for DJ-style
                browsing: "show me 120 BPM tracks". Muted styling
                so it doesn't compete with the title — it's reference
                info, not a primary signal. */}
            {track.bpm && (
              <span title={`${Math.round(track.bpm)} BPM`} style={{
                display: 'inline-flex', alignItems: 'baseline', gap: 2,
                fontSize: 10, fontWeight: 600, padding: '0 2px',
                color: 'var(--fg-3)', letterSpacing: 0,
                fontVariantNumeric: 'tabular-nums', flexShrink: 0,
                opacity: 0.7,
              }}>
                {Math.round(track.bpm)}
                <span style={{ fontSize: 8, fontWeight: 700, letterSpacing: 0.5, opacity: 0.75 }}>BPM</span>
              </span>
            )}
          </div>
        </div>
      </button>

      {/* Artist */}
      <div style={{
        fontSize: 13, color: 'var(--fg-2)',
        whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
      }}>{track.artist}</div>

      {/* Tags — show up to 3 chips, "+N" overflow indicator if more.
          Each chip is a button — clicking it sets the row's tag
          filter to that tag, so one click jumps from "show me what
          this track is tagged" to "show me everything else tagged
          the same". stopPropagation prevents the outer click-to-play
          from firing too. */}
      <div style={{ display: 'flex', gap: 4, flexWrap: 'nowrap', overflow: 'hidden' }}>
        {(track.tags || []).slice(0, 3).map(t => (
          <button key={t}
            onClick={(e) => { e.stopPropagation(); onTagClick?.(t); }}
            title={`Bibliothek auf „${t}" filtern`}
            style={{
              display: 'inline-flex', alignItems: 'center', padding: '3px 8px',
              borderRadius: 999, background: 'rgba(196,154,255,0.10)',
              color: 'var(--lila-200)', border: '1px solid var(--border-soft)',
              fontSize: 10.5, fontWeight: 600, letterSpacing: 0.2,
              whiteSpace: 'nowrap', cursor: 'pointer', fontFamily: 'inherit',
            }}>{t}</button>
        ))}
        {(track.tags || []).length > 3 && (
          <span style={{ fontSize: 10.5, color: 'var(--fg-3)', alignSelf: 'center' }}>
            +{track.tags.length - 3}
          </span>
        )}
      </div>

      {/* Streams + duration. Right-aligned with tabular numerals so
          the columns visually line up. */}
      <div style={{ fontSize: 13, color: 'var(--fg-2)', textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>
        {numDE(track.streams)}
      </div>
      <div style={{ fontSize: 13, color: 'var(--fg-3)', textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>
        {fmt(track.duration)}
      </div>

      {/* Admin menu — only renders for admins. Same component used
          in the review queue + decided list, no special-case here. */}
      {/* Track menu — open to all users. Non-admins get the
          short menu (Künstler öffnen + Zu Playlist hinzufügen);
          admins get the full power-tools list. */}
      <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
        <ShareButton track={track}/>
        <AdminMenu sub={track} refresh={refresh}/>
      </div>
    </div>
  );
}

window.HomeScreen = HomeScreen;
