import { star, unstar } from '../api/subsonicStarRating';
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { getArtist, getArtistInfo, getTopSongs } from '../api/subsonicArtists';
import { getSong, getAlbum } from '../api/subsonicLibrary';
import type { SubsonicSong, SubsonicArtistInfo, SubsonicAlbum } from '../api/subsonicTypes';
import React, { useState, useRef, useEffect, useCallback, useMemo, memo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Music, ExternalLink, Cast, Users, Radio, Clock, SkipForward, Info, Calendar, Disc3, Play, EyeOff, LayoutGrid, RotateCcw, Eye } from 'lucide-react';
import { open as shellOpen } from '@tauri-apps/plugin-shell';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { useLyricsStore } from '../store/lyricsStore';
import { songToTrack } from '../utils/songToTrack';
import {
  lastfmIsConfigured,
  lastfmGetTrackInfo, lastfmGetArtistStats,
  lastfmLoveTrack, lastfmUnloveTrack,
  type LastfmTrackInfo, type LastfmArtistStats,
} from '../api/lastfm';
import { fetchBandsintownEvents, type BandsintownEvent } from '../api/bandsintown';
import { useCachedUrl } from '../components/CachedImage';
import CachedImage from '../components/CachedImage';
import { useRadioMetadata } from '../hooks/useRadioMetadata';
import { useDragSource, useDragDrop } from '../contexts/DragDropContext';
import OverlayScrollArea from '../components/OverlayScrollArea';
import {
  useNpLayoutStore, NP_CARD_IDS,
  type NpCardId, type NpColumn,
} from '../store/nowPlayingLayoutStore';

// ─── Helpers ──────────────────────────────────────────────────────────────────

import {
  formatTime, formatCompact, isoToParts,
  buildContributorRows,
  type ContributorRow,
} from '../utils/nowPlayingHelpers';
import { makeCache } from '../utils/nowPlayingCache';
import NpCardWrap from '../components/nowPlaying/NpCardWrap';
import NpColumnEl from '../components/nowPlaying/NpColumnEl';
import RadioView from '../components/nowPlaying/RadioView';
import Hero from '../components/nowPlaying/Hero';
import ArtistCard from '../components/nowPlaying/ArtistCard';
import AlbumCard from '../components/nowPlaying/AlbumCard';
import TopSongsCard from '../components/nowPlaying/TopSongsCard';
import CreditsCard from '../components/nowPlaying/CreditsCard';

// ─── Module-level TTL caches (shared across mounts) ───────────────────────────

const songMetaCache    = makeCache<SubsonicSong | null>();
const artistInfoCache  = makeCache<SubsonicArtistInfo | null>();
const albumCache       = makeCache<{ album: SubsonicAlbum; songs: SubsonicSong[] } | null>();
const topSongsCache    = makeCache<SubsonicSong[]>();
const tourCache        = makeCache<BandsintownEvent[]>();
const discographyCache = makeCache<SubsonicAlbum[]>();
const lfmTrackCache    = makeCache<LastfmTrackInfo | null>();
const lfmArtistCache   = makeCache<LastfmArtistStats | null>();

// ─── Subcomponents (all memoized) ─────────────────────────────────────────────


interface TourCardProps {
  artistName: string;
  enabled: boolean;
  loading: boolean;
  events: BandsintownEvent[];
  onEnable: () => void;
}

const TourCard = memo(function TourCard({ artistName, enabled, loading, events, onEnable }: TourCardProps) {
  const { t } = useTranslation();
  const [showAll, setShowAll] = useState(false);
  useEffect(() => { setShowAll(false); }, [artistName]);
  const TOUR_LIMIT = 5;
  const visible = showAll ? events : events.slice(0, TOUR_LIMIT);
  const hidden = Math.max(0, events.length - visible.length);

  return (
    <div className="np-info-card np-dash-card">
      <div className="np-card-header">
        <h3 className="np-card-title">
          <Calendar size={13} style={{ marginRight: 5, verticalAlign: '-2px' }} />
          {t('nowPlayingInfo.onTour', 'On tour')}
        </h3>
      </div>

      {!enabled ? (
        <div className="np-info-bandsintown-prompt">
          <div className="np-info-bandsintown-prompt-title">
            <span>{t('nowPlayingInfo.enableBandsintownPrompt', 'See upcoming tour dates?')}</span>
            <span className="np-info-bandsintown-prompt-info"
              data-tooltip={t('nowPlayingInfo.enableBandsintownPrivacy', 'When enabled, the current artist\'s name is sent to the Bandsintown API to fetch tour dates. No personal account information leaves your device.')}
              data-tooltip-pos="bottom"
              data-tooltip-wrap="true"
              tabIndex={0}>
              <Info size={13} />
            </span>
          </div>
          <div className="np-info-bandsintown-prompt-desc">
            {t('nowPlayingInfo.enableBandsintownPromptDesc', 'Optional. Loads concerts for the current artist via Bandsintown.')}
          </div>
          <button className="np-info-bandsintown-prompt-btn" onClick={onEnable}>
            {t('nowPlayingInfo.enableBandsintownAction', 'Enable')}
          </button>
        </div>
      ) : (
        <>
          {loading && events.length === 0 && (
            <div className="np-info-tour-empty">{t('nowPlayingInfo.tourLoading', 'Loading…')}</div>
          )}
          {!loading && events.length === 0 && (
            <div className="np-info-tour-empty">{t('nowPlayingInfo.noTourEvents', 'No upcoming shows')}</div>
          )}
          {visible.length > 0 && (
            <ul className="np-info-tour">
              {visible.map((ev, idx) => {
                const parts = isoToParts(ev.datetime);
                const place = [ev.venueCity, ev.venueRegion, ev.venueCountry].filter(Boolean).join(', ');
                return (
                  <li key={`${ev.datetime}-${ev.venueName}-${idx}`}
                    className="np-info-tour-item"
                    onClick={() => ev.url && shellOpen(ev.url).catch(() => {})}
                    role={ev.url ? 'button' : undefined}
                    tabIndex={ev.url ? 0 : undefined}>
                    {parts && (
                      <div className="np-info-tour-date">
                        <div className="np-info-tour-date-month">{parts.month}</div>
                        <div className="np-info-tour-date-day">{parts.day}</div>
                      </div>
                    )}
                    <div className="np-info-tour-meta">
                      <div className="np-info-tour-venue">{ev.venueName || place}</div>
                      <div className="np-info-tour-place">
                        {parts && <span className="np-info-tour-when">{parts.weekday}, {parts.time}</span>}
                        {parts && place && <span className="np-info-tour-sep"> • </span>}
                        <span>{place}</span>
                      </div>
                    </div>
                  </li>
                );
              })}
            </ul>
          )}
          {(hidden > 0 || (showAll && events.length > TOUR_LIMIT)) && (
            <button className="np-info-tour-more" onClick={() => setShowAll(v => !v)}>
              {showAll
                ? t('nowPlayingInfo.showLessTours', 'Show less')
                : t('nowPlayingInfo.showMoreTours', { defaultValue: 'Show {{count}} more', count: hidden })}
            </button>
          )}
          <div className="np-info-tour-credit">{t('nowPlayingInfo.poweredByBandsintown', 'Tour data via Bandsintown')}</div>
        </>
      )}
    </div>
  );
});

// ─── Radio view (unchanged from previous implementation) ──────────────────────

// ─── Discography card ────────────────────────────────────────────────────────

interface DiscographyCardProps {
  artistId?: string;
  albums: SubsonicAlbum[];
  currentAlbumId?: string;
  onNavigate: (path: string) => void;
}

const DISC_GRID_COLS = 10;
const DISC_INITIAL_ROWS = 2;
const DISC_INITIAL = DISC_GRID_COLS * DISC_INITIAL_ROWS;

const DiscographyCard = memo(function DiscographyCard({ artistId, albums, currentAlbumId, onNavigate }: DiscographyCardProps) {
  const { t } = useTranslation();
  const [showAll, setShowAll] = useState(false);
  useEffect(() => { setShowAll(false); }, [artistId]);

  if (albums.length === 0) return null;

  // Chronological sort, newest first. Always clamp to initial rows; expansion is explicit.
  const ordered = [...albums].sort((a, b) => (b.year ?? 0) - (a.year ?? 0));
  const visible = showAll ? ordered : ordered.slice(0, DISC_INITIAL);
  const hiddenCount = Math.max(0, ordered.length - visible.length);

  return (
    <div className="np-info-card np-dash-card">
      <div className="np-card-header">
        <h3 className="np-card-title">
          <Disc3 size={13} style={{ marginRight: 5, verticalAlign: '-2px' }} />
          {t('nowPlaying.discography', 'Discography')}
        </h3>
        {artistId && (
          <button className="np-card-link" onClick={() => onNavigate(`/artist/${artistId}`)}>
            {t('nowPlaying.goToArtist')} <ExternalLink size={12} />
          </button>
        )}
      </div>
      <div className="np-dash-disc-grid">
        {visible.map(a => {
          const isActive = a.id === currentAlbumId;
          const fetchUrl = a.coverArt ? buildCoverArtUrl(a.coverArt, 200) : '';
          const key      = a.coverArt ? coverArtCacheKey(a.coverArt, 200) : '';
          return (
            <div key={a.id}
              className={`np-dash-disc-tile${isActive ? ' active' : ''}`}
              onClick={() => onNavigate(`/album/${a.id}`)}
              data-tooltip={`${a.name}${a.year ? ` · ${a.year}` : ''}`}>
              <div className="np-dash-disc-cover">
                {fetchUrl && key
                  ? <CachedImage src={fetchUrl} cacheKey={key} alt={a.name} className="np-dash-disc-img" />
                  : <div className="np-dash-disc-fallback"><Music size={18} /></div>}
              </div>
            </div>
          );
        })}
      </div>
      {ordered.length > DISC_INITIAL && (
        <button className="np-dash-tracklist-more" onClick={() => setShowAll(v => !v)}>
          {showAll
            ? t('nowPlaying.showLessTracks', 'Show less')
            : t('nowPlaying.showMoreTracks', { defaultValue: 'Show {{count}} more', count: hiddenCount })}
        </button>
      )}
    </div>
  );
});

// ─── Main Page ────────────────────────────────────────────────────────────────

export default function NowPlaying() {
  const { t } = useTranslation();
  const navigate = useNavigate();
  const stableNavigate = useCallback((path: string) => navigate(path), [navigate]);

  const currentTrack            = usePlayerStore(s => s.currentTrack);
  const currentRadio            = usePlayerStore(s => s.currentRadio);
  const userRatingOverrides     = usePlayerStore(s => s.userRatingOverrides);
  const showLyrics              = useLyricsStore(s => s.showLyrics);
  const activeTab               = useLyricsStore(s => s.activeTab);
  const isQueueVisible          = usePlayerStore(s => s.isQueueVisible);
  const toggleQueue             = usePlayerStore(s => s.toggleQueue);
  const audiomuseNavidromeEnabled = useAuthStore(
    s => !!(s.activeServerId && s.audiomuseNavidromeByServer[s.activeServerId]),
  );
  const enableBandsintown    = useAuthStore(s => s.enableBandsintown);
  const setEnableBandsintown = useAuthStore(s => s.setEnableBandsintown);
  const lastfmUsername       = useAuthStore(s => s.lastfmUsername);
  const lastfmSessionKey     = useAuthStore(s => s.lastfmSessionKey);
  const playTrackFn          = usePlayerStore(s => s.playTrack);

  const radioMeta = useRadioMetadata(currentRadio ?? null);

  const songId    = currentTrack?.id;
  const artistId  = currentTrack?.artistId;
  const albumId   = currentTrack?.albumId;
  const artistName = currentTrack?.artist ?? '';

  // Entity state, seeded from TTL cache so same-artist song switches are instant
  const [songMeta,   setSongMeta]   = useState<SubsonicSong | null>(() => songId ? songMetaCache.get(songId) ?? null : null);
  const [artistInfo, setArtistInfo] = useState<SubsonicArtistInfo | null>(() => artistId ? artistInfoCache.get(artistId) ?? null : null);
  const [albumData,  setAlbumData]  = useState<{ album: SubsonicAlbum; songs: SubsonicSong[] } | null>(() => albumId ? albumCache.get(albumId) ?? null : null);
  const [topSongs,   setTopSongs]   = useState<SubsonicSong[]>(() => artistName ? topSongsCache.get(artistName) ?? [] : []);
  const [tourEvents, setTourEvents] = useState<BandsintownEvent[]>(() => artistName ? tourCache.get(artistName) ?? [] : []);
  const [tourLoading, setTourLoading] = useState(false);
  const [discography, setDiscography] = useState<SubsonicAlbum[]>(() => artistId ? discographyCache.get(artistId) ?? [] : []);
  const [lfmTrack,   setLfmTrack]   = useState<LastfmTrackInfo | null>(null);
  const [lfmArtist,  setLfmArtist]  = useState<LastfmArtistStats | null>(null);

  // Fetch batch per entity change (not per song switch — same-artist songs share artist/top/tour fetches)
  useEffect(() => {
    if (!songId) { setSongMeta(null); return; }
    const cached = songMetaCache.get(songId);
    if (cached !== undefined) { setSongMeta(cached); return; }
    let cancelled = false;
    getSong(songId)
      .then(v => { if (!cancelled) { songMetaCache.set(songId, v ?? null); setSongMeta(v ?? null); } })
      .catch(() => { if (!cancelled) { songMetaCache.set(songId, null); setSongMeta(null); } });
    return () => { cancelled = true; };
  }, [songId]);

  useEffect(() => {
    if (!artistId) { setArtistInfo(null); return; }
    const cached = artistInfoCache.get(artistId);
    if (cached !== undefined) { setArtistInfo(cached); return; }
    let cancelled = false;
    getArtistInfo(artistId, { similarArtistCount: audiomuseNavidromeEnabled ? 24 : undefined })
      .then(v => { if (!cancelled) { artistInfoCache.set(artistId, v ?? null); setArtistInfo(v ?? null); } })
      .catch(() => { if (!cancelled) { artistInfoCache.set(artistId, null); setArtistInfo(null); } });
    return () => { cancelled = true; };
  }, [artistId, audiomuseNavidromeEnabled]);

  useEffect(() => {
    if (!albumId) { setAlbumData(null); return; }
    const cached = albumCache.get(albumId);
    if (cached !== undefined) { setAlbumData(cached); return; }
    let cancelled = false;
    getAlbum(albumId)
      .then(v => { if (!cancelled) { albumCache.set(albumId, v); setAlbumData(v); } })
      .catch(() => { if (!cancelled) { albumCache.set(albumId, null); setAlbumData(null); } });
    return () => { cancelled = true; };
  }, [albumId]);

  useEffect(() => {
    if (!artistName) { setTopSongs([]); return; }
    const cached = topSongsCache.get(artistName);
    if (cached !== undefined) { setTopSongs(cached); return; }
    let cancelled = false;
    getTopSongs(artistName)
      .then(v => { if (!cancelled) { topSongsCache.set(artistName, v); setTopSongs(v); } })
      .catch(() => { if (!cancelled) { topSongsCache.set(artistName, []); setTopSongs([]); } });
    return () => { cancelled = true; };
  }, [artistName]);

  useEffect(() => {
    if (!enableBandsintown || !artistName) { setTourEvents([]); return; }
    const cached = tourCache.get(artistName);
    if (cached !== undefined) { setTourEvents(cached); setTourLoading(false); return; }
    let cancelled = false;
    setTourLoading(true);
    fetchBandsintownEvents(artistName)
      .then(v => { if (!cancelled) { tourCache.set(artistName, v); setTourEvents(v); } })
      .finally(() => { if (!cancelled) setTourLoading(false); });
    return () => { cancelled = true; };
  }, [enableBandsintown, artistName]);

  // Discography via getArtist
  useEffect(() => {
    if (!artistId) { setDiscography([]); return; }
    const cached = discographyCache.get(artistId);
    if (cached !== undefined) { setDiscography(cached); return; }
    let cancelled = false;
    getArtist(artistId)
      .then(v => { if (!cancelled) { discographyCache.set(artistId, v.albums); setDiscography(v.albums); } })
      .catch(() => { if (!cancelled) { discographyCache.set(artistId, []); setDiscography([]); } });
    return () => { cancelled = true; };
  }, [artistId]);

  // Last.fm track info (per-track)
  const lfmTrackKey = currentTrack ? `${currentTrack.artist}${currentTrack.title}${lastfmUsername}` : '';
  useEffect(() => {
    if (!lastfmIsConfigured() || !currentTrack) { setLfmTrack(null); return; }
    const cached = lfmTrackCache.get(lfmTrackKey);
    if (cached !== undefined) { setLfmTrack(cached); return; }
    let cancelled = false;
    lastfmGetTrackInfo(currentTrack.artist, currentTrack.title, lastfmUsername || undefined)
      .then(v => { if (!cancelled) { lfmTrackCache.set(lfmTrackKey, v); setLfmTrack(v); } })
      .catch(() => { if (!cancelled) { lfmTrackCache.set(lfmTrackKey, null); setLfmTrack(null); } });
    return () => { cancelled = true; };
  }, [lfmTrackKey, currentTrack, lastfmUsername]);

  // Last.fm artist stats (per-artist — shared across same-artist tracks)
  const lfmArtistKey = artistName ? `${artistName}${lastfmUsername}` : '';
  useEffect(() => {
    if (!lastfmIsConfigured() || !artistName) { setLfmArtist(null); return; }
    const cached = lfmArtistCache.get(lfmArtistKey);
    if (cached !== undefined) { setLfmArtist(cached); return; }
    let cancelled = false;
    lastfmGetArtistStats(artistName, lastfmUsername || undefined)
      .then(v => { if (!cancelled) { lfmArtistCache.set(lfmArtistKey, v); setLfmArtist(v); } })
      .catch(() => { if (!cancelled) { lfmArtistCache.set(lfmArtistKey, null); setLfmArtist(null); } });
    return () => { cancelled = true; };
  }, [lfmArtistKey, artistName, lastfmUsername]);

  // Star
  const [starred, setStarred] = useState(false);
  useEffect(() => { setStarred(!!songMeta?.starred); }, [songMeta]);
  const toggleStar = useCallback(async () => {
    if (!currentTrack) return;
    if (starred) { await unstar(currentTrack.id, 'song'); setStarred(false); }
    else         { await star(currentTrack.id,   'song'); setStarred(true);  }
  }, [currentTrack, starred]);

  // Last.fm love (seeded from track.getInfo, toggle via love/unlove)
  const lfmLoveEnabled = Boolean(lastfmUsername && lastfmSessionKey);
  const [lfmLoved, setLfmLoved] = useState(false);
  useEffect(() => { setLfmLoved(!!lfmTrack?.userLoved); }, [lfmTrack]);
  const toggleLfmLove = useCallback(async () => {
    if (!currentTrack || !lfmLoveEnabled) return;
    const track = { title: currentTrack.title, artist: currentTrack.artist };
    if (lfmLoved) { await lastfmUnloveTrack(track, lastfmSessionKey); setLfmLoved(false); }
    else          { await lastfmLoveTrack  (track, lastfmSessionKey); setLfmLoved(true);  }
  }, [currentTrack, lfmLoved, lfmLoveEnabled, lastfmSessionKey]);

  const openLyrics = useCallback(() => {
    if (!isQueueVisible) toggleQueue();
    showLyrics();
  }, [isQueueVisible, toggleQueue, showLyrics]);

  // Cover
  const coverFetchUrl   = currentTrack?.coverArt ? buildCoverArtUrl(currentTrack.coverArt, 800) : '';
  const coverKey        = currentTrack?.coverArt ? coverArtCacheKey(currentTrack.coverArt, 800) : '';
  const resolvedCover   = useCachedUrl(coverFetchUrl, coverKey);

  const radioCoverFetchUrl = currentRadio?.coverArt ? buildCoverArtUrl(`ra-${currentRadio.id}`, 800) : '';
  const radioCoverKey      = currentRadio?.coverArt ? coverArtCacheKey(`ra-${currentRadio.id}`, 800) : '';
  const resolvedRadioCover = useCachedUrl(radioCoverFetchUrl, radioCoverKey);

  const contributorRows = useMemo(
    () => buildContributorRows(songMeta, artistName),
    [songMeta, artistName],
  );

  // Merge Subsonic artistInfo with Last.fm fallback: if Subsonic has no bio,
  // use Last.fm's artist bio so the card doesn't show up empty.
  const effectiveArtistInfo = useMemo<SubsonicArtistInfo | null>(() => {
    if (!artistInfo && !lfmArtist?.bio) return null;
    if (artistInfo?.biography) return artistInfo;
    if (!lfmArtist?.bio) return artistInfo;
    return {
      ...(artistInfo ?? {}),
      biography: lfmArtist.bio,
    };
  }, [artistInfo, lfmArtist]);

  const handleEnableBandsintown = useCallback(() => setEnableBandsintown(true), [setEnableBandsintown]);

  const handlePlayTopSong = useCallback((song: SubsonicSong) => {
    if (topSongs.length === 0) return;
    const queue = topSongs.map(songToTrack);
    const hit = queue.find(q => q.id === song.id);
    if (hit) playTrackFn(hit, queue);
  }, [topSongs, playTrackFn]);

  // ── Widget layout (drag-to-reorder, hide/show, reset) ────────────────────
  const layoutCards   = useNpLayoutStore(s => s.cards);
  const moveCard      = useNpLayoutStore(s => s.moveCard);
  const setCardVisible = useNpLayoutStore(s => s.setVisible);
  const resetLayout   = useNpLayoutStore(s => s.reset);
  const { isDragging: dndActive, payload: dndPayload } = useDragDrop();

  const [dragOver, setDragOver] = useState<{ col: NpColumn; idx: number } | null>(null);
  const [layoutMenuOpen, setLayoutMenuOpen] = useState(false);

  // Parse the current drag payload to know whether it's an np-card drag
  const draggingCardId: NpCardId | null = useMemo(() => {
    if (!dndActive || !dndPayload) return null;
    try {
      const parsed = JSON.parse(dndPayload.data);
      if (parsed?.kind === 'np-card' && NP_CARD_IDS.includes(parsed.id)) return parsed.id as NpCardId;
    } catch { /* not a card payload */ }
    return null;
  }, [dndActive, dndPayload]);

  // Clear the drop indicator when the drag ends (no psy-drop on our target)
  useEffect(() => { if (!draggingCardId) setDragOver(null); }, [draggingCardId]);

  const toggleCardVisible = useCallback((id: NpCardId, next: boolean) => {
    setCardVisible(id, next);
  }, [setCardVisible]);

  const onColumnHover = useCallback((col: NpColumn, idx: number) => {
    setDragOver(prev => (prev && prev.col === col && prev.idx === idx) ? prev : { col, idx });
  }, []);

  // Ref mirror of dragOver so the document-level psy-drop handler always sees
  // the latest hovered column/index regardless of closure timing.
  const dragOverRef = useRef(dragOver);
  dragOverRef.current = dragOver;

  // Global psy-drop listener: catches drops anywhere on the page (even below a
  // column when the cursor left the column bounds), then uses dragOverRef to
  // decide which column/index the user actually meant.
  useEffect(() => {
    if (!draggingCardId) return;
    const onPsyDrop = (evt: Event) => {
      const ce = evt as CustomEvent<{ data: string }>;
      try {
        const parsed = JSON.parse(ce.detail?.data ?? '');
        if (parsed?.kind !== 'np-card' || !NP_CARD_IDS.includes(parsed.id)) return;
        const over = dragOverRef.current;
        if (over) {
          moveCard(parsed.id as NpCardId, over.col, over.idx);
        }
      } catch { /* ignore non-card drops */ }
      setDragOver(null);
    };
    document.addEventListener('psy-drop', onPsyDrop as EventListener);
    return () => document.removeEventListener('psy-drop', onPsyDrop as EventListener);
  }, [draggingCardId, moveCard]);

  // Close layout menu on outside click
  useEffect(() => {
    if (!layoutMenuOpen) return;
    const onDoc = (e: MouseEvent) => {
      const el = e.target as HTMLElement | null;
      if (!el?.closest('.np-dash-toolbar-menu-wrap')) setLayoutMenuOpen(false);
    };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [layoutMenuOpen]);

  // ── Render ────────────────────────────────────────────────────────────────
  return (
    <div className="np-page">
      <OverlayScrollArea
        className="np-main"
        viewportClassName="np-main__viewport"
        railInset="panel"
        measureDeps={[
          !!currentTrack,
          !!currentRadio,
          layoutCards,
          enableBandsintown,
          tourEvents.length,
          discography.length,
          topSongs.length,
        ]}
      >
        {currentRadio && !currentTrack ? (
          <RadioView radioMeta={radioMeta} currentRadio={currentRadio} resolvedCover={resolvedRadioCover} />
        ) : currentTrack ? (
          <div className="np-dash">
            <Hero
              track={{
                id: currentTrack.id,
                title: currentTrack.title,
                artist: currentTrack.artist,
                album: currentTrack.album,
                year: currentTrack.year,
                duration: currentTrack.duration,
                suffix: currentTrack.suffix,
                bitRate: currentTrack.bitRate,
                samplingRate: songMeta?.samplingRate,
                bitDepth: songMeta?.bitDepth,
                artistId: currentTrack.artistId,
                albumId: currentTrack.albumId,
                userRating: currentTrack.userRating,
              }}
              genre={songMeta?.genre ?? undefined}
              playCount={(songMeta as (SubsonicSong & { playCount?: number }) | null)?.playCount}
              userRatingOverride={userRatingOverrides[currentTrack.id]}
              lfmTrack={lfmTrack}
              lfmArtist={lfmArtist}
              starred={starred}
              lfmLoved={lfmLoved}
              lfmLoveEnabled={lfmLoveEnabled}
              activeLyricsTab={activeTab === 'lyrics' && isQueueVisible}
              coverUrl={resolvedCover}
              onNavigate={stableNavigate}
              onToggleStar={toggleStar}
              onToggleLfmLove={toggleLfmLove}
              onOpenLyrics={openLyrics}
            />

            {(() => {
              const renderCard = (id: NpCardId): React.ReactNode => {
                switch (id) {
                  case 'album': return (
                    <AlbumCard
                      album={albumData?.album ?? null}
                      songs={albumData?.songs ?? []}
                      currentTrackId={currentTrack.id}
                      albumName={currentTrack.album}
                      albumId={albumId}
                      albumYear={currentTrack.year}
                      onNavigate={stableNavigate}
                    />
                  );
                  case 'topSongs': return (
                    <TopSongsCard
                      artistName={artistName}
                      artistId={artistId}
                      songs={topSongs}
                      currentTrackId={currentTrack.id}
                      onNavigate={stableNavigate}
                      onPlay={handlePlayTopSong}
                    />
                  );
                  case 'credits': return <CreditsCard rows={contributorRows} />;
                  case 'artist': return (
                    <ArtistCard
                      artistName={artistName}
                      artistId={artistId}
                      artistInfo={effectiveArtistInfo}
                      onNavigate={stableNavigate}
                    />
                  );
                  case 'discography': return (
                    <DiscographyCard
                      artistId={artistId}
                      albums={discography}
                      currentAlbumId={albumId}
                      onNavigate={stableNavigate}
                    />
                  );
                  case 'tour': return (
                    <TourCard
                      artistName={artistName}
                      enabled={enableBandsintown}
                      loading={tourLoading}
                      events={tourEvents}
                      onEnable={handleEnableBandsintown}
                    />
                  );
                }
              };
              const cardLabel = (id: NpCardId): string => {
                const k: Record<NpCardId, string> = {
                  album: 'nowPlaying.fromAlbum',
                  topSongs: 'nowPlaying.topSongs',
                  credits: 'nowPlayingInfo.songInfo',
                  artist: 'nowPlaying.aboutArtist',
                  discography: 'nowPlaying.discography',
                  tour: 'nowPlayingInfo.onTour',
                };
                return t(k[id]);
              };
              const visibleCards = layoutCards.filter(c => c.visible);
              const hiddenCards  = layoutCards.filter(c => !c.visible);
              const renderColumn = (col: NpColumn) => {
                const cards = layoutCards.filter(c =>
                  c.column === col && c.visible && c.id !== draggingCardId,
                );
                const isOver = dragOver?.col === col;
                return (
                  <NpColumnEl
                    col={col}
                    empty={cards.length === 0}
                    emptyLabel={t('nowPlaying.emptyColumn', 'Drop cards here')}
                    isDndActive={!!draggingCardId}
                    draggingCardId={draggingCardId}
                    onHover={onColumnHover}
                    isOverHere={!!isOver}
                  >
                    {cards.map((c, idx) => (
                      <React.Fragment key={c.id}>
                        {isOver && dragOver.idx === idx && <div className="np-dash-drop-indicator" />}
                        <NpCardWrap
                          id={c.id}
                          label={cardLabel(c.id)}
                          isDraggingThis={draggingCardId === c.id}
                        >
                          {renderCard(c.id)}
                        </NpCardWrap>
                      </React.Fragment>
                    ))}
                    {isOver && dragOver.idx === cards.length && <div className="np-dash-drop-indicator" />}
                  </NpColumnEl>
                );
              };
              return (
                <>
                  <div className="np-dash-toolbar">
                    <div className="np-dash-toolbar-menu-wrap">
                      <button
                        className="np-dash-toolbar-btn"
                        onClick={() => setLayoutMenuOpen(v => !v)}
                        data-tooltip={t('nowPlaying.layoutMenu', 'Layout')}
                      >
                        <LayoutGrid size={14} />
                        <span>{t('nowPlaying.layoutMenu', 'Layout')}</span>
                        {hiddenCards.length > 0 && (
                          <span className="np-dash-toolbar-badge">{hiddenCards.length}</span>
                        )}
                      </button>
                      {layoutMenuOpen && (
                        <div className="np-dash-toolbar-menu" role="menu">
                          <div className="np-dash-toolbar-section">
                            {t('nowPlaying.visibleCards', 'Visible cards')}
                          </div>
                          {visibleCards.map(c => (
                            <button
                              key={c.id}
                              className="np-dash-toolbar-item"
                              onClick={() => toggleCardVisible(c.id, false)}
                            >
                              <Eye size={13} /> <span className="np-dash-toolbar-item-label">{cardLabel(c.id)}</span>
                            </button>
                          ))}
                          {hiddenCards.length > 0 && (
                            <>
                              <div className="np-dash-toolbar-section">
                                {t('nowPlaying.hiddenCards', 'Hidden cards')}
                              </div>
                              {hiddenCards.map(c => (
                                <button
                                  key={c.id}
                                  className="np-dash-toolbar-item is-hidden"
                                  onClick={() => toggleCardVisible(c.id, true)}
                                >
                                  <EyeOff size={13} /> <span className="np-dash-toolbar-item-label">{cardLabel(c.id)}</span>
                                </button>
                              ))}
                            </>
                          )}
                          <div className="np-dash-toolbar-divider" />
                          <button
                            className="np-dash-toolbar-item"
                            onClick={() => { resetLayout(); setLayoutMenuOpen(false); }}
                          >
                            <RotateCcw size={13} /> <span className="np-dash-toolbar-item-label">{t('nowPlaying.resetLayout', 'Reset layout')}</span>
                          </button>
                        </div>
                      )}
                    </div>
                  </div>
                  <div className="np-dash-grid">
                    {renderColumn('left')}
                    {renderColumn('right')}
                  </div>
                </>
              );
            })()}
          </div>
        ) : (
          <div className="np-empty-state">
            <Music size={48} style={{ opacity: 0.3 }} />
            <p>{t('nowPlaying.nothingPlaying')}</p>
          </div>
        )}
      </OverlayScrollArea>
    </div>
  );
}
