// 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 + rail are loaded once when the page mounts; they only
  // change when the underlying catalog changes (re-mount on refresh).
  const [hero, setHero] = React.useState(null);
  const [rail, setRail] = 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: hero + rail + available-tag list. Three small
  // queries in parallel because they don't depend on each other.
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const [topRes, recentRes, tagsRes] = await Promise.all([
          Api.catalog({ status: 'accepted', sort: 'streams', limit: 1 }),
          Api.catalog({ status: 'accepted', sort: 'newest', limit: 4 }),
          Api.availableTags(),
        ]);
        if (cancelled) return;
        // If nothing has streams yet, fall back to the most-recent
        // track for the hero so the page doesn't feel empty.
        const featured = topRes.items[0] && topRes.items[0].streams > 0
          ? topRes.items[0]
          : recentRes.items[0] || null;
        setHero(featured);
        setRail(recentRes.items);
        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.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>
      )}

      {/* RAIL — Frisch veröffentlicht */}
      {rail.length > 0 && (
        <>
          <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 16 }}>
            <div>
              <Eyebrow style={{ marginBottom: 4 }}>{library.total} freigegeben</Eyebrow>
              <h2 style={{
                fontWeight: 800, fontSize: 32, lineHeight: 1.05, letterSpacing: '-0.02em', margin: 0,
              }}>Frisch veröffentlicht</h2>
            </div>
          </div>

          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, marginBottom: 36 }}>
            {rail.map(t => (
              <TrackTile key={t.id} track={t} onPlay={play}
                active={audio.currentId === t.id && audio.isPlaying}/>
            ))}
          </div>
        </>
      )}

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

// Tile (single track in the "Frisch veröffentlicht" rail).
function TrackTile({ track, onPlay, active }) {
  const [hover, setHover] = React.useState(false);
  return (
    <div
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      onClick={() => onPlay(track)}
      style={{ cursor: 'pointer' }}
    >
      <Glass radius={18} padding={14} hover>
        <div style={{ position: 'relative' }}>
          <Cover colors={track.cover} src={track.coverUrl}
            size="100%" radius={14}
            style={{ width: '100%', aspectRatio: '1', height: 'auto' }}/>
          <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={{
            fontSize: 14, fontWeight: 600, color: active ? 'var(--coral)' : 'var(--foam)',
            whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
          }}>{track.title}</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;
