// == DISCOVER PAGE                          ==
// ============================================

let discoverHeroIndex = 0;
let discoverHeroArtists = [];
let discoverHeroInterval = null;
let discoverPageInitialized = false;

// Store discover playlist tracks for download/sync functionality
let discoverReleaseRadarTracks = [];
let discoverWeeklyTracks = [];
let discoverRecentAlbums = [];
let discoverSeasonalAlbums = [];
let discoverSeasonalTracks = [];
let currentSeasonKey = null;

// Personalized playlists storage
let personalizedPopularPicks = [];
let personalizedHiddenGems = [];
let personalizedDailyMixes = [];
let personalizedDiscoveryShuffle = [];
let buildPlaylistSelectedArtists = [];

async function loadDiscoverPage() {
    console.log('Loading discover page...');

    // Load all sections
    await Promise.all([
        loadDiscoverHero(),
        loadYourArtists(),
        loadYourAlbums(),
        loadDiscoverRecentReleases(),
        loadSeasonalContent(),  // Seasonal discovery
        // loadPersonalizedDailyMixes(),  // NEW: Daily Mix playlists (HIDDEN)
        loadDiscoverReleaseRadar(),
        loadDiscoverWeekly(),
        loadPersonalizedPopularPicks(),  // NEW: Popular picks from discovery pool
        loadPersonalizedHiddenGems(),  // NEW: Hidden gems from discovery pool
        loadDiscoveryShuffle(),  // NEW: Discovery Shuffle
        loadBecauseYouListenTo(),  // Personalized by listening stats
        loadCacheUndiscoveredAlbums(),  // From metadata cache
        loadCacheGenreNewReleases(),    // From metadata cache
        loadCacheLabelExplorer(),       // From metadata cache
        loadCacheDeepCuts(),            // From metadata cache
        loadCacheGenreExplorer(),       // From metadata cache
        initializeLastfmRadioSection(),  // Last.fm Radio section (gated on API key)
        initializeListenBrainzTabs(),  // ListenBrainz playlists (tabbed)
        loadDecadeBrowserTabs(),  // Time Machine (tabbed by decade)
        loadGenreBrowserTabs(),  // Browse by Genre (tabbed by genre)
        loadListenBrainzPlaylistsFromBackend(),  // Load ListenBrainz playlist states for persistence
        loadDiscoveryBlacklist()  // Blocked artists list
    ]);

    // Check for active syncs after page load
    checkForActiveDiscoverSyncs();
}

async function checkForActiveDiscoverSyncs() {
    // Check if Fresh Tape sync is active
    try {
        const releaseRadarResponse = await fetch('/api/sync/status/discover_release_radar');
        if (releaseRadarResponse.ok) {
            const data = await releaseRadarResponse.json();
            if (data.status === 'syncing' || data.status === 'starting') {
                console.log('🔄 Resuming Fresh Tape sync polling after page refresh');

                // Show status display
                const statusDisplay = document.getElementById('release-radar-sync-status');
                if (statusDisplay) {
                    statusDisplay.style.display = 'block';
                }

                // Disable button
                const syncButton = document.getElementById('release-radar-sync-btn');
                if (syncButton) {
                    syncButton.disabled = true;
                    syncButton.style.opacity = '0.5';
                    syncButton.style.cursor = 'not-allowed';
                }

                // Resume polling
                startDiscoverSyncPolling('release_radar', 'discover_release_radar');
            }
        }
    } catch (error) {
        // Sync not active, ignore
    }

    // Check if The Archives sync is active
    try {
        const discoveryWeeklyResponse = await fetch('/api/sync/status/discover_discovery_weekly');
        if (discoveryWeeklyResponse.ok) {
            const data = await discoveryWeeklyResponse.json();
            if (data.status === 'syncing' || data.status === 'starting') {
                console.log('🔄 Resuming The Archives sync polling after page refresh');

                // Show status display
                const statusDisplay = document.getElementById('discovery-weekly-sync-status');
                if (statusDisplay) {
                    statusDisplay.style.display = 'block';
                }

                // Disable button
                const syncButton = document.getElementById('discovery-weekly-sync-btn');
                if (syncButton) {
                    syncButton.disabled = true;
                    syncButton.style.opacity = '0.5';
                    syncButton.style.cursor = 'not-allowed';
                }

                // Resume polling
                startDiscoverSyncPolling('discovery_weekly', 'discover_discovery_weekly');
            }
        }
    } catch (error) {
        // Sync not active, ignore
    }

    // Check if Seasonal Playlist sync is active
    try {
        const seasonalResponse = await fetch('/api/sync/status/discover_seasonal_playlist');
        if (seasonalResponse.ok) {
            const data = await seasonalResponse.json();
            if (data.status === 'syncing' || data.status === 'starting') {
                console.log('🔄 Resuming Seasonal Playlist sync polling after page refresh');

                const statusDisplay = document.getElementById('seasonal-playlist-sync-status');
                if (statusDisplay) {
                    statusDisplay.style.display = 'block';
                }

                const syncButton = document.getElementById('seasonal-playlist-sync-btn');
                if (syncButton) {
                    syncButton.disabled = true;
                    syncButton.style.opacity = '0.5';
                    syncButton.style.cursor = 'not-allowed';
                }

                startDiscoverSyncPolling('seasonal_playlist', 'discover_seasonal_playlist');
            }
        }
    } catch (error) {
        // Sync not active, ignore
    }
}

async function loadDiscoverHero() {
    try {
        const response = await fetch('/api/discover/hero');
        if (!response.ok) {
            console.error('Failed to fetch discover hero');
            return;
        }

        const data = await response.json();
        if (!data.success || !data.artists || data.artists.length === 0) {
            console.log('No hero artists available');
            showDiscoverHeroEmpty();
            return;
        }

        discoverHeroArtists = data.artists;
        discoverHeroIndex = 0;

        // Display first artist
        displayDiscoverHeroArtist(discoverHeroArtists[0]);

        // Start slideshow (change every 8 seconds)
        if (discoverHeroInterval) {
            clearInterval(discoverHeroInterval);
        }
        if (discoverHeroArtists.length > 1) {
            discoverHeroInterval = setInterval(() => {
                discoverHeroIndex = (discoverHeroIndex + 1) % discoverHeroArtists.length;
                displayDiscoverHeroArtist(discoverHeroArtists[discoverHeroIndex]);
            }, 8000);
        }

        // Check if all hero artists are already watched
        checkAllHeroWatchlistStatus();

    } catch (error) {
        console.error('Error loading discover hero:', error);
        showDiscoverHeroEmpty();
    }
}

function displayDiscoverHeroArtist(artist) {
    const titleEl = document.getElementById('discover-hero-title');
    const subtitleEl = document.getElementById('discover-hero-subtitle');
    const metaEl = document.getElementById('discover-hero-meta');
    const imageEl = document.getElementById('discover-hero-image');
    const bgEl = document.getElementById('discover-hero-bg');

    if (titleEl) {
        titleEl.textContent = artist.artist_name;
    }

    if (subtitleEl) {
        // Show recommendation context based on occurrence count
        let subtitle = '';
        if (artist.occurrence_count > 1) {
            subtitle = `Similar to ${artist.occurrence_count} artists in your watchlist`;
        } else {
            subtitle = 'Similar to an artist in your watchlist';
        }
        subtitleEl.textContent = subtitle;
    }

    // Build metadata section with popularity and genres
    if (metaEl) {
        let metaHTML = '<div class="discover-hero-meta-content">';

        // Add popularity indicator
        if (artist.popularity !== undefined && artist.popularity > 0) {
            const popularityClass = artist.popularity >= 80 ? 'high' :
                artist.popularity >= 50 ? 'medium' : 'low';
            metaHTML += `
                <div class="hero-meta-item hero-popularity ${popularityClass}">
                    <span class="meta-icon">⭐</span>
                    <span class="meta-value">${artist.popularity}/100</span>
                    <span class="meta-label">Popularity</span>
                </div>
            `;
        }

        // Add genre tags
        if (artist.genres && artist.genres.length > 0) {
            metaHTML += '<div class="hero-meta-item hero-genres">';
            artist.genres.slice(0, 3).forEach(genre => {
                metaHTML += `<span class="genre-tag">${genre}</span>`;
            });
            metaHTML += '</div>';
        }

        metaHTML += '</div>';
        metaEl.innerHTML = metaHTML;
    }

    if (imageEl && artist.image_url) {
        imageEl.innerHTML = `<img src="${artist.image_url}" alt="${artist.artist_name}">`;
    } else if (imageEl) {
        imageEl.innerHTML = '<div class="hero-image-placeholder">🎧</div>';
    }

    if (bgEl && artist.image_url) {
        bgEl.style.backgroundImage = `url('${artist.image_url}')`;
        bgEl.style.backgroundSize = 'cover';
        bgEl.style.backgroundPosition = 'center';
    }

    // Store artist ID for both buttons and update watchlist state
    // Use artist_id which is set by the backend to the appropriate ID for the active source
    const addBtn = document.getElementById('discover-hero-add');
    const discographyBtn = document.getElementById('discover-hero-discography');
    const artistId = artist.artist_id || artist.spotify_artist_id || artist.itunes_artist_id;

    if (addBtn && artistId) {
        addBtn.setAttribute('data-artist-id', artistId);
        addBtn.setAttribute('data-artist-name', artist.artist_name);
        // Also store both IDs for cross-source operations
        if (artist.spotify_artist_id) addBtn.setAttribute('data-spotify-id', artist.spotify_artist_id);
        if (artist.itunes_artist_id) addBtn.setAttribute('data-itunes-id', artist.itunes_artist_id);

        // Check if this artist is already in watchlist and update button appearance
        checkAndUpdateDiscoverHeroWatchlistButton(artistId);
    }

    if (discographyBtn && artistId) {
        discographyBtn.setAttribute('data-artist-id', artistId);
        discographyBtn.setAttribute('data-artist-name', artist.artist_name);
        discographyBtn.href = buildArtistDetailPath(artistId, artist.source || null);
        // Keep the source on the link so source-only hero artists resolve to
        // the correct artist-detail URL instead of being treated as library IDs.
        if (artist.source) discographyBtn.setAttribute('data-source', artist.source);
        else discographyBtn.removeAttribute('data-source');
        // Also store both IDs for cross-source operations
        if (artist.spotify_artist_id) discographyBtn.setAttribute('data-spotify-id', artist.spotify_artist_id);
        if (artist.itunes_artist_id) discographyBtn.setAttribute('data-itunes-id', artist.itunes_artist_id);
    }

    // Update slideshow indicators
    updateDiscoverHeroIndicators();
}

async function checkAndUpdateDiscoverHeroWatchlistButton(artistId) {
    try {
        const response = await fetch('/api/watchlist/check', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ artist_id: artistId })
        });

        const data = await response.json();
        if (!data.success) return;

        const addBtn = document.getElementById('discover-hero-add');
        if (!addBtn) return;

        const icon = addBtn.querySelector('.watchlist-icon');
        const text = addBtn.querySelector('.watchlist-text');

        if (data.is_watching) {
            // Artist is in watchlist
            if (icon) icon.textContent = '👁️';
            if (text) text.textContent = 'Watching...';
            addBtn.classList.add('watching');
        } else {
            // Artist not in watchlist
            if (icon) icon.textContent = '👁️';
            if (text) text.textContent = 'Add to Watchlist';
            addBtn.classList.remove('watching');
        }
    } catch (error) {
        console.error('Error checking watchlist status for hero:', error);
    }
}

function toggleDiscoverHeroWatchlist(event) {
    event.stopPropagation();

    const button = document.getElementById('discover-hero-add');
    if (!button) return;

    const artistId = button.getAttribute('data-artist-id');
    const artistName = button.getAttribute('data-artist-name');

    if (!artistId || !artistName) {
        console.error('No artist data found on discover hero button');
        return;
    }

    // Call the existing toggleWatchlist function
    toggleWatchlist(event, artistId, artistName);
}

async function watchAllHeroArtists(btn) {
    if (!discoverHeroArtists || discoverHeroArtists.length === 0) return;
    if (btn.classList.contains('all-watched')) return;

    const textEl = btn.querySelector('.watch-all-text');
    const originalText = textEl ? textEl.textContent : '';

    // Loading state
    btn.disabled = true;
    if (textEl) textEl.textContent = 'Adding...';

    try {
        const artists = discoverHeroArtists.map(a => ({
            artist_id: a.artist_id,
            artist_name: a.artist_name
        }));

        const response = await fetch('/api/watchlist/add-batch', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ artists })
        });

        const data = await response.json();
        if (data.success) {
            if (textEl) textEl.textContent = 'All Watched';
            btn.classList.add('all-watched');
            btn.disabled = true;

            // Sync the per-slide watchlist button for current artist
            const currentArtist = discoverHeroArtists[discoverHeroIndex];
            if (currentArtist) {
                checkAndUpdateDiscoverHeroWatchlistButton(currentArtist.artist_id);
            }

            // Update watchlist count badge
            if (typeof updateWatchlistButtonCount === 'function') {
                updateWatchlistButtonCount();
            }
        } else {
            if (textEl) textEl.textContent = originalText;
            btn.disabled = false;
        }
    } catch (error) {
        console.error('Error watching all hero artists:', error);
        if (textEl) textEl.textContent = originalText;
        btn.disabled = false;
    }
}

// Cache for recommended artists data so reopening is instant
let _recommendedArtistsCache = null;
let _recommendedArtistsSource = null;

async function openRecommendedArtistsModal() {
    let modal = document.getElementById('recommended-artists-modal');
    if (!modal) {
        modal = document.createElement('div');
        modal.id = 'recommended-artists-modal';
        modal.className = 'modal-overlay';
        document.body.appendChild(modal);

        modal.addEventListener('click', function (e) {
            if (e.target === modal) closeRecommendedArtistsModal();
        });
    }

    // If cached, render instantly and refresh watchlist statuses
    if (_recommendedArtistsCache) {
        modal.style.display = 'flex';
        renderRecommendedArtistsModal(modal, _recommendedArtistsCache, _recommendedArtistsSource);
        checkRecommendedWatchlistStatuses(_recommendedArtistsCache);
        return;
    }

    // Show loading
    modal.innerHTML = `
        <div class="modal-container playlist-modal recommended-modal">
            <div class="playlist-modal-header">
                <div class="playlist-header-content" style="width: 100%;">
                    <h2>Recommended Artists</h2>
                    <div class="playlist-quick-info">
                        <span class="playlist-track-count">Loading...</span>
                    </div>
                </div>
                <span class="playlist-modal-close" onclick="closeRecommendedArtistsModal()">&times;</span>
            </div>
            <div class="playlist-modal-body">
                <div class="recommended-loading">Loading recommended artists...</div>
            </div>
        </div>
    `;
    modal.style.display = 'flex';

    try {
        // Phase 1: Fetch basic data (instant — no API enrichment)
        const response = await fetch('/api/discover/similar-artists');
        const data = await response.json();

        if (!data.success || !data.artists || data.artists.length === 0) {
            modal.querySelector('.playlist-modal-body').innerHTML = `
                <div class="recommended-empty-state">
                    No recommended artists yet.<br>
                    Run a watchlist scan to generate recommendations.
                </div>
            `;
            modal.querySelector('.playlist-track-count').textContent = '0 artists';
            return;
        }

        // Phase 2: Enrich with images/genres progressively in batches of 50
        // Skip artists that already have cached metadata from the initial response
        const source = data.source || 'spotify';
        // Render cards immediately with fallback images
        _recommendedArtistsCache = data.artists;
        _recommendedArtistsSource = source;
        renderRecommendedArtistsModal(modal, data.artists, source);

        const idKey = source === 'spotify' ? 'spotify_artist_id' : source === 'deezer' ? 'deezer_artist_id' : 'itunes_artist_id';
        const allIds = data.artists
            .filter(a => !a.image_url)  // Only enrich artists without cached images
            .map(a => a[idKey]).filter(Boolean);

        for (let i = 0; i < allIds.length; i += 50) {
            const batchIds = allIds.slice(i, i + 50);
            try {
                const enrichResp = await fetch('/api/discover/similar-artists/enrich', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ artist_ids: batchIds, source })
                });
                const enrichData = await enrichResp.json();
                if (enrichData.success && enrichData.artists) {
                    // Update cards and cache as each batch arrives
                    for (const [aid, info] of Object.entries(enrichData.artists)) {
                        // Update the card in DOM
                        const card = modal.querySelector(`.recommended-artist-card[data-artist-id="${aid}"]`);
                        if (card && info.image_url) {
                            const imgContainer = card.querySelector('.recommended-card-image');
                            if (imgContainer) {
                                imgContainer.innerHTML = `<img src="${info.image_url}" alt="" loading="lazy"
                                    onerror="this.parentElement.innerHTML='<div class=\\'recommended-card-image-fallback\\'>🎤</div>';">`;
                            }
                        }
                        if (card && info.genres && info.genres.length > 0) {
                            const genresContainer = card.querySelector('.recommended-card-genres');
                            if (genresContainer) {
                                genresContainer.innerHTML = info.genres.map(g =>
                                    `<span class="recommended-card-genre">${escapeHtml(g)}</span>`
                                ).join('');
                            } else {
                                const infoDiv = card.querySelector('.recommended-card-info');
                                if (infoDiv) {
                                    const genreDiv = document.createElement('div');
                                    genreDiv.className = 'recommended-card-genres';
                                    genreDiv.innerHTML = info.genres.map(g =>
                                        `<span class="recommended-card-genre">${escapeHtml(g)}</span>`
                                    ).join('');
                                    infoDiv.appendChild(genreDiv);
                                }
                            }
                        }

                        // Update cache
                        const cached = _recommendedArtistsCache.find(a => a.artist_id === aid || a.spotify_artist_id === aid || a.itunes_artist_id === aid);
                        if (cached) {
                            if (info.image_url) cached.image_url = info.image_url;
                            if (info.genres) cached.genres = info.genres;
                            if (info.artist_name) cached.artist_name = info.artist_name;
                        }
                    }
                }
            } catch (enrichErr) {
                console.error('Error enriching batch:', enrichErr);
            }
        }

        // Phase 3: Check watchlist statuses
        checkRecommendedWatchlistStatuses(data.artists);

    } catch (error) {
        console.error('Error loading recommended artists:', error);
        modal.querySelector('.playlist-modal-body').innerHTML = `
            <div class="recommended-empty-state">Error loading recommended artists.</div>
        `;
    }
}

function renderRecommendedArtistsModal(modal, artists, source = null) {
    modal.innerHTML = `
        <div class="modal-container playlist-modal recommended-modal">
            <div class="playlist-modal-header">
                <div class="playlist-header-content" style="width: 100%;">
                    <h2>Recommended Artists</h2>
                    <div class="playlist-quick-info">
                        <span class="playlist-track-count">${artists.length} artist${artists.length !== 1 ? 's' : ''}</span>
                    </div>
                </div>
                <span class="playlist-modal-close" onclick="closeRecommendedArtistsModal()">&times;</span>
            </div>
            <div class="playlist-modal-body">
                <div class="recommended-actions-bar">
                    <div class="recommended-search-container">
                        <input type="text"
                               class="recommended-search-input"
                               id="recommended-search-input"
                               placeholder="Search recommended artists..."
                               oninput="filterRecommendedArtists()">
                    </div>
                    <button class="recommended-add-all-btn" id="recommended-add-all-btn"
                            onclick="addAllRecommendedToWatchlist(this)">
                        Add All to Watchlist
                    </button>
                </div>
                <div class="recommended-artists-grid" id="recommended-artists-grid">
                    ${artists.map(artist => {
        const genreTags = (artist.genres || []).slice(0, 3).map(g =>
            `<span class="recommended-card-genre">${escapeHtml(g)}</span>`
        ).join('');
        const similarText = artist.occurrence_count > 1
            ? `Similar to ${artist.occurrence_count} in your watchlist`
            : 'Similar to an artist in your watchlist';
                const artistSource = artist.source || source || _recommendedArtistsSource || '';
        return `
                            <div class="recommended-artist-card"
                                 data-artist-name="${escapeHtml(artist.artist_name).toLowerCase()}"
                                 data-artist-id="${artist.artist_id}"
                                 data-artist-source="${escapeHtml(artistSource)}">
                                <button class="recommended-card-watchlist-btn"
                                        data-artist-id="${artist.artist_id}"
                                        data-artist-name="${escapeHtml(artist.artist_name)}">
                                    Add to Watchlist
                                </button>
                                <a class="recommended-card-link" href="${buildArtistDetailPath(artist.artist_id, artistSource || null)}"
                                   onclick="closeRecommendedArtistsModal()"
                                   style="display:block;text-decoration:none;color:inherit;">
                                    <div class="recommended-card-image">
                                        ${artist.image_url ? `
                                            <img src="${artist.image_url}"
                                                 alt="${escapeHtml(artist.artist_name)}"
                                                 loading="lazy"
                                                 onerror="this.parentElement.innerHTML='<div class=\\'recommended-card-image-fallback\\'>🎤</div>';">
                                        ` : `
                                            <div class="recommended-card-image-fallback">🎤</div>
                                        `}
                                    </div>
                                    <div class="recommended-card-info">
                                        <span class="recommended-card-name">${escapeHtml(artist.artist_name)}</span>
                                        <span class="recommended-card-similarity">${similarText}</span>
                                        <div class="recommended-card-genres">${genreTags}</div>
                                    </div>
                                </a>
                            </div>
                        `;
    }).join('')}
                </div>
            </div>
        </div>
    `;

    // Event delegation for card clicks and watchlist buttons
    const grid = modal.querySelector('#recommended-artists-grid');
    if (grid) {
        grid.addEventListener('click', function (e) {
            const watchlistBtn = e.target.closest('.recommended-card-watchlist-btn');
            if (watchlistBtn) {
                e.stopPropagation();
                toggleRecommendedWatchlist(watchlistBtn);
            }
        });
    }
}

async function addAllRecommendedToWatchlist(btn) {
    if (!_recommendedArtistsCache || _recommendedArtistsCache.length === 0) return;
    if (btn.classList.contains('all-added')) return;

    const originalText = btn.textContent;
    btn.disabled = true;
    btn.textContent = 'Adding...';

    try {
        const artists = _recommendedArtistsCache.map(a => ({
            artist_id: a.artist_id,
            artist_name: a.artist_name
        }));

        const resp = await fetch('/api/watchlist/add-batch', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ artists })
        });
        const data = await resp.json();

        if (data.success) {
            btn.textContent = `All Added (${data.added} new)`;
            btn.classList.add('all-added');
            btn.disabled = true;

            // Update all watchlist buttons in the modal to "Watching"
            document.querySelectorAll('.recommended-card-watchlist-btn').forEach(wBtn => {
                wBtn.classList.add('watching');
                wBtn.textContent = 'Watching';
            });

            if (typeof updateWatchlistButtonCount === 'function') updateWatchlistButtonCount();
        } else {
            btn.textContent = originalText;
            btn.disabled = false;
        }
    } catch (error) {
        console.error('Error adding all recommended to watchlist:', error);
        btn.textContent = originalText;
        btn.disabled = false;
    }
}

function closeRecommendedArtistsModal() {
    const modal = document.getElementById('recommended-artists-modal');
    if (modal) modal.style.display = 'none';
}

function filterRecommendedArtists() {
    const query = (document.getElementById('recommended-search-input')?.value || '').toLowerCase();
    const cards = document.querySelectorAll('.recommended-artist-card');
    cards.forEach(card => {
        const name = card.getAttribute('data-artist-name') || '';
        card.style.display = name.includes(query) ? '' : 'none';
    });
}

async function toggleRecommendedWatchlist(btn) {
    const artistId = btn.getAttribute('data-artist-id');
    const artistName = btn.getAttribute('data-artist-name');
    if (!artistId || !artistName) return;

    btn.disabled = true;
    const wasWatching = btn.classList.contains('watching');

    try {
        if (wasWatching) {
            const resp = await fetch('/api/watchlist/remove', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ artist_id: artistId })
            });
            const data = await resp.json();
            if (data.success) {
                btn.classList.remove('watching');
                btn.textContent = 'Add to Watchlist';
            }
        } else {
            const resp = await fetch('/api/watchlist/add', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ artist_id: artistId, artist_name: artistName })
            });
            const data = await resp.json();
            if (data.success) {
                btn.classList.add('watching');
                btn.textContent = 'Watching';
            }
        }
        if (typeof updateWatchlistButtonCount === 'function') updateWatchlistButtonCount();
    } catch (error) {
        console.error('Error toggling recommended watchlist:', error);
    } finally {
        btn.disabled = false;
    }
}

async function checkRecommendedWatchlistStatuses(artists) {
    try {
        const artistIds = artists.map(a => a.artist_id).filter(Boolean);
        if (!artistIds.length) return;

        const resp = await fetch('/api/watchlist/check-batch', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ artist_ids: artistIds })
        });
        const data = await resp.json();
        if (data.success && data.results) {
            for (const [aid, isWatching] of Object.entries(data.results)) {
                if (isWatching) {
                    const btn = document.querySelector(`.recommended-card-watchlist-btn[data-artist-id="${aid}"]`);
                    if (btn) {
                        btn.classList.add('watching');
                        btn.textContent = 'Watching';
                    }
                }
            }
        }
    } catch (e) {
        // Non-critical
    }
}

async function checkAllHeroWatchlistStatus() {
    const btn = document.getElementById('discover-hero-watch-all');
    if (!btn || !discoverHeroArtists || discoverHeroArtists.length === 0) return;

    try {
        let allWatched = true;
        for (const artist of discoverHeroArtists) {
            const response = await fetch('/api/watchlist/check', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ artist_id: artist.artist_id })
            });
            const data = await response.json();
            if (!data.success || !data.is_watching) {
                allWatched = false;
                break;
            }
        }

        const textEl = btn.querySelector('.watch-all-text');
        if (allWatched) {
            if (textEl) textEl.textContent = 'All Watched';
            btn.classList.add('all-watched');
            btn.disabled = true;
        } else {
            if (textEl) textEl.textContent = 'Watch All';
            btn.classList.remove('all-watched');
            btn.disabled = false;
        }
    } catch (error) {
        console.error('Error checking hero watchlist status:', error);
    }
}

function navigateDiscoverHero(direction) {
    if (!discoverHeroArtists || discoverHeroArtists.length === 0) return;

    // Update index with wrapping
    discoverHeroIndex = (discoverHeroIndex + direction + discoverHeroArtists.length) % discoverHeroArtists.length;

    // Display the artist
    displayDiscoverHeroArtist(discoverHeroArtists[discoverHeroIndex]);

    // Update indicators
    updateDiscoverHeroIndicators();
}

function updateDiscoverHeroIndicators() {
    const indicatorsContainer = document.getElementById('discover-hero-indicators');
    if (!indicatorsContainer || !discoverHeroArtists || discoverHeroArtists.length === 0) return;

    // Create indicator dots
    indicatorsContainer.innerHTML = discoverHeroArtists.map((_, index) => `
        <button class="hero-indicator ${index === discoverHeroIndex ? 'active' : ''}"
                onclick="jumpToDiscoverHeroSlide(${index})"
                aria-label="Go to slide ${index + 1}"></button>
    `).join('');
}

function jumpToDiscoverHeroSlide(index) {
    if (!discoverHeroArtists || index < 0 || index >= discoverHeroArtists.length) return;

    discoverHeroIndex = index;
    displayDiscoverHeroArtist(discoverHeroArtists[discoverHeroIndex]);
    updateDiscoverHeroIndicators();
}

function showDiscoverHeroEmpty() {
    const titleEl = document.getElementById('discover-hero-title');
    const subtitleEl = document.getElementById('discover-hero-subtitle');

    if (titleEl) titleEl.textContent = 'No Recommendations Yet';
    if (subtitleEl) subtitleEl.textContent = 'Run a watchlist scan to generate personalized recommendations';
}

// Recent Releases — first section migrated to the shared
// `createDiscoverSectionController`. The controller owns the
// loading / empty / error / refresh lifecycle that every other
// discover section currently re-implements by hand. This function
// stays as the public entry-point so existing callers don't change;
// internally it builds (or reuses) the controller and triggers a
// load. See `discover-section-controller.js` for the contract.
let _recentReleasesCtrl = null;

function _renderRecentReleaseCard(album, index) {
    const coverUrl = album.album_cover_url || '/static/placeholder-album.png';
    return `
        <div class="discover-card" onclick="openDownloadModalForRecentAlbum(${index})" style="cursor: pointer;">
            <div class="discover-card-image">
                <img src="${coverUrl}" alt="${album.album_name}" loading="lazy">
            </div>
            <div class="discover-card-info">
                <h4 class="discover-card-title">${album.album_name}</h4>
                <p class="discover-card-subtitle">${album.artist_name}</p>
                <p class="discover-card-meta">${album.release_date}</p>
            </div>
        </div>
    `;
}

async function loadDiscoverRecentReleases() {
    if (!_recentReleasesCtrl) {
        _recentReleasesCtrl = createDiscoverSectionController({
            id: 'recent-releases',
            contentEl: '#recent-releases-carousel',
            fetchUrl: '/api/discover/recent-releases',
            extractItems: (data) => data.albums || [],
            renderItems: (items) => {
                // Module-level `discoverRecentAlbums` is what the click
                // handler reads to look up the album by index. Keep it
                // in sync so `openDownloadModalForRecentAlbum(index)`
                // still resolves correctly after re-renders.
                discoverRecentAlbums = items;
                return items.map((album, i) => _renderRecentReleaseCard(album, i)).join('');
            },
            loadingMessage: 'Loading recent releases...',
            emptyMessage: 'No recent releases found',
            errorMessage: 'Failed to load recent releases',
            showErrorToast: true,
        });
    }
    return _recentReleasesCtrl.load();
}

// ===============================
// ===============================
// YOUR ALBUMS SECTION
// ===============================

let yourAlbums = [];
let yourAlbumsPage = 1;
let yourAlbumsTotal = 0;
const YOUR_ALBUMS_PAGE_SIZE = 48;
let _yourAlbumsSearchTimeout = null;

function debouncedYourAlbumsSearch() {
    clearTimeout(_yourAlbumsSearchTimeout);
    _yourAlbumsSearchTimeout = setTimeout(() => {
        yourAlbumsPage = 1;
        loadYourAlbumsGrid();
    }, 400);
}

let _yourAlbumsCtrl = null;

async function loadYourAlbums() {
    if (!_yourAlbumsCtrl) {
        _yourAlbumsCtrl = createDiscoverSectionController({
            id: 'your-albums',
            sectionEl: '#your-albums-section',
            contentEl: '#your-albums-grid',
            fetchUrl: '/api/discover/your-albums?page=1&per_page=48&status=all',
            extractItems: (data) => data.albums || [],
            // Truly empty (no data + not stale) \u2192 hide the whole section
            // (matches the legacy "Nothing to show yet" early-return). The
            // outer hideWhenEmpty + sectionEl handle the visibility flip.
            isEmpty: (items, data) => {
                const total = (data && data.stats && data.stats.total) || 0;
                return total === 0 && !data.stale;
            },
            hideWhenEmpty: true,
            // Stale + no albums yet \u2192 show the "fetching from connected
            // services" UI and start the poller. Fires before isEmpty.
            isStale: (items, data) => {
                const total = (data && data.stats && data.stats.total) || 0;
                return Boolean(data && data.stale) && total === 0;
            },
            renderStale: () =>
                '<div class="discover-loading"><div class="loading-spinner"></div><p>Fetching your albums from connected services...</p></div>',
            onStale: () => _pollYourAlbums(),
            // Side-effects against sibling DOM (subtitle / filters /
            // download button) belong here, not in renderItems.
            onSuccess: (data) => {
                const subtitle = document.getElementById('your-albums-subtitle');
                if (subtitle && data.stats) {
                    const s = data.stats;
                    subtitle.textContent = `${s.total} albums \u00B7 ${s.owned} owned \u00B7 ${s.missing} missing`;
                }
                const totalCount = (data.stats && data.stats.total) || 0;
                const filters = document.getElementById('your-albums-filters');
                if (filters && totalCount > 0) filters.style.display = '';
                const downloadBtn = document.getElementById('your-albums-download-btn');
                if (downloadBtn && data.stats && data.stats.missing > 0) downloadBtn.style.display = '';
            },
            // Renderer delegates to the existing grid renderer, which
            // writes its own DOM into `#your-albums-grid`. `manualDom`
            // tells the controller not to clobber it.
            manualDom: true,
            renderItems: (items, data) => {
                yourAlbums = items;
                yourAlbumsTotal = data.total || 0;
                yourAlbumsPage = 1;
                _renderYourAlbumsGrid(yourAlbums);
                _renderYourAlbumsPagination(yourAlbumsTotal, yourAlbumsPage);
            },
            errorMessage: 'Failed to load your albums',
            verboseErrors: true,
            showErrorToast: true,
        });
    }
    return _yourAlbumsCtrl.load();
}

function _pollYourAlbums() {
    let attempts = 0;
    const poll = setInterval(async () => {
        attempts++;
        if (attempts > 12) { clearInterval(poll); return; }
        try {
            const resp = await fetch('/api/discover/your-albums?page=1&per_page=48&status=all');
            if (!resp.ok) return;
            const data = await resp.json();
            if (!data.success) return;
            const total = (data.stats && data.stats.total) || 0;
            if (total > 0) {
                clearInterval(poll);
                loadYourAlbums();
            }
        } catch (e) { }
    }, 5000);
}

async function loadYourAlbumsGrid() {
    const grid = document.getElementById('your-albums-grid');
    if (!grid) return;
    grid.innerHTML = '<div class="discover-loading"><div class="loading-spinner"></div><p>Loading...</p></div>';
    try {
        const search = (document.getElementById('your-albums-search')?.value || '').trim();
        const status = document.getElementById('your-albums-status-filter')?.value || 'all';
        const sort = document.getElementById('your-albums-sort')?.value || 'artist_name';
        const params = new URLSearchParams({ page: yourAlbumsPage, per_page: YOUR_ALBUMS_PAGE_SIZE, sort, status });
        if (search) params.set('search', search);
        const resp = await fetch(`/api/discover/your-albums?${params}`);
        const data = await resp.json();
        if (!data.success) throw new Error(data.error);
        yourAlbums = data.albums || [];
        yourAlbumsTotal = data.total || 0;
        const subtitle = document.getElementById('your-albums-subtitle');
        if (subtitle && data.stats) {
            const s = data.stats;
            subtitle.textContent = `${s.total} albums \u00B7 ${s.owned} owned \u00B7 ${s.missing} missing`;
        }
        _renderYourAlbumsGrid(yourAlbums);
        _renderYourAlbumsPagination(yourAlbumsTotal, yourAlbumsPage);
    } catch (e) {
        console.error('Error loading your albums grid:', e);
        grid.innerHTML = '<div class="spotify-library-empty"><p>Failed to load albums</p></div>';
    }
}

function _renderYourAlbumsGrid(albums) {
    const grid = document.getElementById('your-albums-grid');
    if (!grid) return;
    if (!albums || albums.length === 0) {
        grid.innerHTML = '<div class="spotify-library-empty"><p>No albums found</p></div>';
        return;
    }
    let html = '';
    albums.forEach((album, index) => {
        const coverUrl = album.image_url || '/static/placeholder-album.png';
        const year = album.release_date ? album.release_date.substring(0, 4) : '';
        const badgeClass = album.in_library ? 'owned' : 'missing';
        const badgeIcon = album.in_library ? '\u2713' : '\u2193';
        const trackInfo = album.total_tracks ? `${album.total_tracks} tracks` : '';
        const meta = [year, trackInfo].filter(Boolean).join(' \u00B7 ');
        const sources = (album.source_services || []).join(', ');
        html += `
            <div class="spotify-library-card" onclick="openYourAlbumDownload(${index})" title="${escapeHtml(album.album_name)} \u2014 ${escapeHtml(album.artist_name)}">
                <div class="spotify-library-card-img">
                    <img src="${coverUrl}" alt="${escapeHtml(album.album_name)}" loading="lazy">
                    <div class="spotify-library-card-badge ${badgeClass}">${badgeIcon}</div>
                </div>
                <div class="spotify-library-card-info">
                    <p class="spotify-library-card-title">${escapeHtml(album.album_name)}</p>
                    <p class="spotify-library-card-artist">${escapeHtml(album.artist_name)}</p>
                    <p class="spotify-library-card-meta">${escapeHtml(meta)}</p>
                </div>
            </div>`;
    });
    grid.innerHTML = html;
}

function _renderYourAlbumsPagination(total, page) {
    const container = document.getElementById('your-albums-pagination');
    if (!container) return;
    if (total <= YOUR_ALBUMS_PAGE_SIZE) { container.style.display = 'none'; return; }
    container.style.display = '';
    const totalPages = Math.ceil(total / YOUR_ALBUMS_PAGE_SIZE);
    const start = (page - 1) * YOUR_ALBUMS_PAGE_SIZE + 1;
    const end = Math.min(page * YOUR_ALBUMS_PAGE_SIZE, total);
    container.innerHTML = `
        <button class="spotify-library-page-btn" onclick="_yourAlbumsPrevPage()" ${page <= 1 ? 'disabled' : ''}>&larr; Previous</button>
        <span class="spotify-library-page-info">${start}\u2013${end} of ${total}</span>
        <button class="spotify-library-page-btn" onclick="_yourAlbumsNextPage()" ${page >= totalPages ? 'disabled' : ''}>Next &rarr;</button>
    `;
}

function _yourAlbumsPrevPage() {
    if (yourAlbumsPage > 1) { yourAlbumsPage--; loadYourAlbumsGrid(); }
}
function _yourAlbumsNextPage() {
    const totalPages = Math.ceil(yourAlbumsTotal / YOUR_ALBUMS_PAGE_SIZE);
    if (yourAlbumsPage < totalPages) { yourAlbumsPage++; loadYourAlbumsGrid(); }
}

async function openYourAlbumDownload(index) {
    const album = yourAlbums[index];
    if (!album) { showToast('Album data not found', 'error'); return; }
    showLoadingOverlay(`Loading tracks for ${album.album_name}...`);
    try {
        // Per-source dispatch: open with whichever source has an ID for
        // this album. For pure-Discogs collection items (no Spotify/
        // Deezer match), dispatch goes straight to Discogs so the
        // modal opens with Discogs context (vinyl/CD release detail,
        // tracklist from Discogs). For Spotify saved albums (no
        // discogs id), goes to Spotify. For multi-source albums
        // (album exists in BOTH Spotify saved and Discogs collection,
        // rare), tries streaming sources first since they have
        // tracklists with proper IDs ready for download.
        let albumData = null;
        const nameParams = new URLSearchParams({ name: album.album_name || '', artist: album.artist_name || '' });
        const discogsId = album.discogs_release_id || album.discogs_id;

        const trySources = [];
        if (album.spotify_album_id) trySources.push(['spotify', album.spotify_album_id]);
        if (album.deezer_album_id) trySources.push(['deezer', album.deezer_album_id]);
        if (album.tidal_album_id) trySources.push(['tidal', album.tidal_album_id]);
        if (discogsId) trySources.push(['discogs', discogsId]);

        for (const [src, id] of trySources) {
            const r = await fetch(`/api/discover/album/${src}/${id}?${nameParams}`);
            if (r.ok) {
                albumData = await r.json();
                if (albumData && albumData.tracks && albumData.tracks.length > 0) break;
                albumData = null;  // empty payload — try next
            }
        }

        if (!albumData) {
            // Last resort — search by name
            const r = await fetch(`/api/discover/album/spotify/search?${nameParams}`);
            if (r.ok) albumData = await r.json();
        }
        if (!albumData || !albumData.tracks || albumData.tracks.length === 0) {
            throw new Error('No tracks found for this album');
        }
        const tracks = albumData.tracks.map(track => {
            let artists = track.artists || albumData.artists || [{ name: album.artist_name }];
            if (Array.isArray(artists)) artists = artists.map(a => a.name || a);
            return {
                id: track.id, name: track.name, artists,
                album: {
                    id: albumData.id, name: albumData.name,
                    album_type: albumData.album_type || 'album',
                    total_tracks: albumData.total_tracks || 0,
                    release_date: albumData.release_date || '',
                    images: albumData.images || []
                },
                duration_ms: track.duration_ms || 0,
                track_number: track.track_number || 0
            };
        });
        const virtualId = `discover_album_${album.spotify_album_id || album.deezer_album_id || album.tidal_album_id || index}`;
        const albumObj = {
            id: albumData.id, name: albumData.name, album_type: albumData.album_type || 'album',
            total_tracks: albumData.total_tracks || 0, release_date: albumData.release_date || '',
            images: albumData.images || [], artists: [{ name: album.artist_name }]
        };
        const artistObj = { id: null, name: album.artist_name };
        await openDownloadMissingModalForArtistAlbum(virtualId, albumData.name, tracks, albumObj, artistObj, false);
        hideLoadingOverlay();
    } catch (e) {
        console.error('Error opening your album download:', e);
        showToast(`Failed to load album: ${e.message}`, 'error');
        hideLoadingOverlay();
    }
}

async function refreshYourAlbums() {
    const btn = document.getElementById('your-albums-refresh-btn');
    if (btn) btn.disabled = true;
    const subtitle = document.getElementById('your-albums-subtitle');
    if (subtitle) subtitle.textContent = 'Refreshing from connected services...';
    try {
        await fetch('/api/discover/your-albums/refresh?clear=true', { method: 'POST' });
        showToast('Refresh started — checking for new albums...', 'info');
        const poll = setInterval(async () => {
            try {
                const resp = await fetch('/api/discover/your-albums?page=1&per_page=48');
                const data = await resp.json();
                if (data.success && data.stats && data.stats.total > 0) {
                    clearInterval(poll);
                    loadYourAlbums();
                    if (btn) btn.disabled = false;
                }
            } catch (e) { }
        }, 4000);
        setTimeout(() => { clearInterval(poll); if (btn) btn.disabled = false; }, 60000);
    } catch (e) {
        showToast('Failed to start refresh', 'error');
        if (btn) btn.disabled = false;
    }
}

async function openYourAlbumsSourcesModal() {
    const existing = document.getElementById('ya-albums-sources-modal-overlay');
    if (existing) existing.remove();

    let enabled = ['spotify', 'tidal', 'deezer'];
    let connected = [];
    try {
        const resp = await fetch('/api/discover/your-albums/sources');
        if (resp.ok) {
            const data = await resp.json();
            if (data.enabled) enabled = data.enabled;
            if (data.connected) connected = data.connected;
        }
    } catch (e) { }

    const sourceInfo = [
        { id: 'spotify', label: 'Spotify', icon: '\uD83C\uDFB5' },
        { id: 'tidal', label: 'Tidal', icon: '\uD83C\uDF0A' },
        { id: 'deezer', label: 'Deezer', icon: '\uD83C\uDFB6' },
        { id: 'discogs', label: 'Discogs', icon: '\uD83D\uDCBF' },
    ];
    const state = {};
    sourceInfo.forEach(s => { state[s.id] = enabled.includes(s.id); });

    const overlay = document.createElement('div');
    overlay.id = 'ya-albums-sources-modal-overlay';
    overlay.className = 'modal-overlay';
    overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };

    const rows = sourceInfo.map(s => {
        const isConnected = connected.includes(s.id);
        const isOn = state[s.id];
        return `
            <div class="ya-source-row${isConnected ? '' : ' disconnected'}" data-yaa-source="${s.id}" onclick="_yaaSourceRowClick('${s.id}')">
                <div class="ya-source-row-left">
                    <span style="font-size:18px">${s.icon}</span>
                    <div>
                        <div class="ya-source-name">${s.label}</div>
                        <div class="ya-source-status">${isConnected ? 'Connected' : 'Not connected'}</div>
                    </div>
                </div>
                <button class="ya-source-toggle${isOn ? ' on' : ''}" id="yaa-toggle-${s.id}" onclick="event.stopPropagation();_yaaSourceToggle('${s.id}')"></button>
            </div>`;
    }).join('');

    overlay.innerHTML = `
        <div class="ya-sources-modal">
            <h2>Your Albums Sources</h2>
            <p class="ya-sources-desc">Choose which connected services contribute albums to this section.</p>
            <div class="ya-sources-list">${rows}</div>
            <div class="ya-sources-footer">
                <button class="ya-sources-cancel-btn" onclick="document.getElementById('ya-albums-sources-modal-overlay').remove()">Cancel</button>
                <button class="ya-sources-save-btn" onclick="_yaaSourcesSave()">Save</button>
            </div>
        </div>
    `;
    document.body.appendChild(overlay);
    window._yaaSourcesState = state;
}

// Source-id → human label + setup hint shown when user tries to enable
// a disconnected source. Without this, the toggle silently bailed and
// users saw no feedback — just a non-responsive switch.
const _YAA_DISCONNECTED_HINTS = {
    spotify: 'Spotify not connected — log in at Settings → Connections first',
    tidal: 'Tidal not connected — set up Tidal in Settings → Connections first',
    deezer: 'Deezer not connected — log in or set ARL token at Settings → Connections first',
    discogs: 'Discogs not connected — paste your personal access token at Settings → Connections first',
};

function _yaaShowDisconnectedHint(id) {
    const msg = _YAA_DISCONNECTED_HINTS[id]
        || `${id} not connected — set it up in Settings → Connections first`;
    if (typeof showToast === 'function') showToast(msg, 'warning');
}

function _yaaSourceRowClick(id) {
    const row = document.querySelector(`.ya-source-row[data-yaa-source="${id}"]`);
    if (row && row.classList.contains('disconnected')) {
        _yaaShowDisconnectedHint(id);
        return;
    }
    _yaaSourceToggle(id);
}
function _yaaSourceToggle(id) {
    const row = document.querySelector(`.ya-source-row[data-yaa-source="${id}"]`);
    if (row && row.classList.contains('disconnected')) {
        _yaaShowDisconnectedHint(id);
        return;
    }
    window._yaaSourcesState[id] = !window._yaaSourcesState[id];
    const btn = document.getElementById(`yaa-toggle-${id}`);
    if (btn) btn.classList.toggle('on', window._yaaSourcesState[id]);
}
async function _yaaSourcesSave() {
    const enabledArr = Object.entries(window._yaaSourcesState).filter(([, v]) => v).map(([k]) => k);
    if (enabledArr.length === 0) { showToast('Select at least one source', 'error'); return; }
    try {
        const resp = await fetch('/api/settings', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ discover: { your_albums_sources: enabledArr.join(',') } })
        });
        if (resp.ok) {
            document.getElementById('ya-albums-sources-modal-overlay')?.remove();
            showToast('Sources saved — refresh to apply', 'success');
            const sourceNames = { spotify: 'Spotify', tidal: 'Tidal', deezer: 'Deezer', discogs: 'Discogs' };
            const subtitle = document.getElementById('your-albums-subtitle');
            if (subtitle) {
                const names = enabledArr.map(s => sourceNames[s] || s).join(' and ');
                subtitle.textContent = `Albums you\u2019ve saved on ${names}`;
            }
        } else {
            showToast('Failed to save sources', 'error');
        }
    } catch (e) {
        showToast('Failed to save sources', 'error');
    }
}

async function downloadMissingYourAlbums() {
    // Opens the same selectable-grid modal pattern used by Download
    // Discography on the library page. User picks which missing albums
    // they want, clicks Add to Wishlist, each album's tracks get
    // resolved + added to the wishlist for the existing auto-download
    // processor to pick up. Replaces the prior per-album direct-download
    // loop which was silently failing — actual downloads should go
    // through the wishlist queue, not bypass it.
    try {
        const resp = await fetch('/api/discover/your-albums?page=1&per_page=1000&status=missing');
        const data = await resp.json();
        if (!data.success || !data.albums || data.albums.length === 0) {
            showToast('No missing albums to download', 'info');
            return;
        }
        const missing = data.albums.filter(a => !a.in_library);
        if (missing.length === 0) {
            showToast('All albums are already in your library!', 'success');
            return;
        }
        _openYourAlbumsBatchModal(missing);
    } catch (e) {
        console.error('Error loading missing your albums:', e);
        showToast(`Error: ${e.message}`, 'error');
    }
}


// Map a Your Albums row to the single best source-id the
// /api/artist/<id>/download-discography endpoint can resolve. Each row
// in the missing list typically only has one populated source-id (the
// service it was saved on), so this is just a priority pick.
function _yourAlbumsPickSource(album) {
    if (album.spotify_album_id) return { id: String(album.spotify_album_id), source: 'spotify' };
    if (album.deezer_album_id) return { id: String(album.deezer_album_id), source: 'deezer' };
    if (album.tidal_album_id) return { id: String(album.tidal_album_id), source: 'tidal' };
    const discogsId = album.discogs_release_id || album.discogs_id;
    if (discogsId) return { id: String(discogsId), source: 'discogs' };
    return null;
}


function _openYourAlbumsBatchModal(missingAlbums) {
    // Reuses the .discog-modal styling from the library Download
    // Discography flow — same checkboxes, same Select All / Deselect
    // All semantics, same footer. Single difference: each card carries
    // its own artist+source (multi-artist) instead of all being one
    // artist's discography.
    const existing = document.getElementById('your-albums-batch-modal-overlay');
    if (existing) existing.remove();

    // Stash the source-id picks on the cards so the submit handler
    // can build the per-album payload without re-mapping the array.
    const rows = missingAlbums
        .map((a, i) => ({ ...a, _src: _yourAlbumsPickSource(a), _index: i }))
        .filter(a => a._src);  // Skip albums with no usable source-id

    if (rows.length === 0) {
        showToast('No missing albums have a usable source ID to resolve', 'warning');
        return;
    }

    const overlay = document.createElement('div');
    overlay.className = 'discog-modal-overlay';
    overlay.id = 'your-albums-batch-modal-overlay';
    overlay.innerHTML = `
        <div class="discog-modal">
            <div class="discog-modal-hero">
                <div class="discog-modal-hero-overlay"></div>
                <div class="discog-modal-hero-content">
                    <h2 class="discog-modal-title">Add Missing Albums to Wishlist</h2>
                    <p class="discog-modal-artist">${rows.length} albums missing from your library</p>
                </div>
                <button class="discog-modal-close" onclick="_closeYourAlbumsBatchModal()">&times;</button>
            </div>
            <div class="discog-filter-bar">
                <div class="discog-filters"></div>
                <div class="discog-select-actions">
                    <button class="discog-select-btn" onclick="_yourAlbumsBatchSelectAll(true)">Select All</button>
                    <button class="discog-select-btn" onclick="_yourAlbumsBatchSelectAll(false)">Deselect All</button>
                </div>
            </div>
            <div class="discog-grid" id="your-albums-batch-grid">
                ${rows.map((r, i) => _renderYourAlbumsBatchCard(r, i)).join('')}
            </div>
            <div class="discog-progress" id="your-albums-batch-progress" style="display:none;"></div>
            <div class="discog-footer" id="your-albums-batch-footer">
                <div class="discog-footer-info" id="your-albums-batch-footer-info"></div>
                <div class="discog-footer-actions">
                    <button class="discog-cancel-btn" onclick="_closeYourAlbumsBatchModal()">Cancel</button>
                    <button class="discog-submit-btn" id="your-albums-batch-submit-btn">
                        <span class="discog-submit-icon">⬇</span>
                        <span id="your-albums-batch-submit-text">Add to Wishlist</span>
                    </button>
                </div>
            </div>
        </div>
    `;

    document.body.appendChild(overlay);
    // Stash row data on the overlay for the submit handler — keeps the
    // multi-artist source info available without re-fetching.
    overlay._yourAlbumsRows = rows;

    requestAnimationFrame(() => overlay.classList.add('visible'));
    _updateYourAlbumsBatchFooterCount();

    document.getElementById('your-albums-batch-submit-btn')?.addEventListener('click', (e) => {
        e.stopPropagation();
        _startYourAlbumsBatchAddToWishlist();
    });
}


function _renderYourAlbumsBatchCard(row, index) {
    const albumName = row.album_name || '';
    const artistName = row.artist_name || '';
    const year = row.release_date ? row.release_date.substring(0, 4) : '';
    const tracks = row.total_tracks || 0;
    const img = row.image_url || '';
    const src = row._src?.source || '';
    return `
        <label class="discog-card" data-type="album" style="animation-delay:${index * 0.03}s">
            <input type="checkbox" class="your-albums-batch-cb"
                   data-row-index="${row._index}" data-tracks="${tracks}" checked
                   onchange="_updateYourAlbumsBatchFooterCount()">
            <div class="discog-card-art">
                ${img ? `<img src="${escapeHtml(img)}" alt="" loading="lazy">` : '<div class="discog-card-art-placeholder">🎵</div>'}
            </div>
            <div class="discog-card-info">
                <div class="discog-card-title">${escapeHtml(albumName)}</div>
                <div class="discog-card-meta">${escapeHtml(artistName)}${year ? ' · ' + year : ''}${tracks ? ' · ' + tracks + ' tracks' : ''}${src ? ' · ' + src : ''}</div>
            </div>
            <div class="discog-card-check"></div>
        </label>
    `;
}


function _yourAlbumsBatchSelectAll(select) {
    document.querySelectorAll('.your-albums-batch-cb').forEach(cb => {
        if (cb.closest('.discog-card').style.display !== 'none') cb.checked = select;
    });
    _updateYourAlbumsBatchFooterCount();
}


function _updateYourAlbumsBatchFooterCount() {
    const checked = document.querySelectorAll('.your-albums-batch-cb:checked');
    let releases = 0, tracks = 0;
    checked.forEach(cb => {
        if (cb.closest('.discog-card').style.display !== 'none') {
            releases++;
            tracks += parseInt(cb.dataset.tracks) || 0;
        }
    });
    const info = document.getElementById('your-albums-batch-footer-info');
    const btn = document.getElementById('your-albums-batch-submit-text');
    if (info) info.textContent = `${releases} album${releases !== 1 ? 's' : ''}${tracks ? ' · ' + tracks + ' tracks' : ''}`;
    if (btn) btn.textContent = releases > 0 ? `Add ${releases} to Wishlist` : 'Select albums';
    const submitBtn = document.getElementById('your-albums-batch-submit-btn');
    if (submitBtn) submitBtn.disabled = releases === 0;
}


function _closeYourAlbumsBatchModal() {
    const overlay = document.getElementById('your-albums-batch-modal-overlay');
    if (overlay) {
        overlay.classList.remove('visible');
        setTimeout(() => overlay.remove(), 200);
    }
}


async function _startYourAlbumsBatchAddToWishlist() {
    const overlay = document.getElementById('your-albums-batch-modal-overlay');
    if (!overlay) return;
    const rows = overlay._yourAlbumsRows || [];

    // Collect selected row indices from the checked checkboxes.
    const selectedRowIndices = [];
    document.querySelectorAll('.your-albums-batch-cb:checked').forEach(cb => {
        if (cb.closest('.discog-card').style.display !== 'none') {
            selectedRowIndices.push(parseInt(cb.dataset.rowIndex));
        }
    });
    const selected = rows.filter(r => selectedRowIndices.includes(r._index));
    if (selected.length === 0) return;

    // Switch to progress view.
    const grid = document.getElementById('your-albums-batch-grid');
    const progress = document.getElementById('your-albums-batch-progress');
    const footer = document.getElementById('your-albums-batch-footer');
    const filterBar = overlay.querySelector('.discog-filter-bar');

    if (grid) grid.style.display = 'none';
    if (filterBar) filterBar.style.display = 'none';
    if (progress) {
        progress.style.display = '';
        progress.innerHTML = '';
    }

    selected.forEach(row => {
        const item = document.createElement('div');
        item.className = 'discog-progress-item active';
        item.id = `your-albums-batch-prog-${row._src.source}-${row._src.id}`;
        item.innerHTML = `
            <div class="discog-prog-art">${row.image_url ? `<img src="${escapeHtml(row.image_url)}">` : '🎵'}</div>
            <div class="discog-prog-info">
                <div class="discog-prog-title">${escapeHtml(row.album_name || '')}</div>
                <div class="discog-prog-status">Waiting...</div>
            </div>
            <div class="discog-prog-icon"><div class="discog-spinner"></div></div>
        `;
        progress.appendChild(item);
    });

    const submitBtn = document.getElementById('your-albums-batch-submit-btn');
    if (submitBtn) submitBtn.style.display = 'none';
    if (footer) {
        const info = document.getElementById('your-albums-batch-footer-info');
        if (info) info.textContent = 'Processing... this may take a moment';
    }

    // Build per-album payload matching the discography endpoint contract.
    // URL artist_id is functionally unused by the endpoint when per-album
    // metadata is supplied — backend resolves each album through its own
    // `source` + `artist_name`. Placeholder 'your-albums' makes the route
    // match without picking an arbitrary library artist.
    const albumsPayload = selected.map(r => ({
        id: r._src.id,
        name: r.album_name || '',
        artist_name: r.artist_name || '',
        source: r._src.source,
    }));

    try {
        const response = await fetch(`/api/artist/your-albums/download-discography`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                albums: albumsPayload,
                artist_name: 'Your Albums',
                source: null,
            }),
        });

        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';
        let totalAdded = 0, totalSkipped = 0;

        while (true) {
            const { done, value } = await reader.read();
            if (done) break;
            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split('\n');
            buffer = lines.pop();
            for (const line of lines) {
                if (!line.trim()) continue;
                try {
                    const data = JSON.parse(line);
                    if (data.status === 'complete') {
                        totalAdded = data.total_added || 0;
                        totalSkipped = data.total_skipped || 0;
                    } else if (data.album_id) {
                        // Find the matching progress card — match by composite source-id
                        // pair since the same album_id could appear across sources.
                        const matching = selected.find(s => s._src.id === String(data.album_id));
                        if (matching) {
                            const item = document.getElementById(`your-albums-batch-prog-${matching._src.source}-${matching._src.id}`);
                            if (item) {
                                const status = item.querySelector('.discog-prog-status');
                                const icon = item.querySelector('.discog-prog-icon');
                                if (data.status === 'done') {
                                    if (status) status.textContent = `${data.tracks_added || 0} added · ${data.tracks_skipped || 0} skipped`;
                                    if (icon) icon.innerHTML = '✓';
                                    item.classList.add('done');
                                    item.classList.remove('active');
                                } else if (data.status === 'error') {
                                    if (status) status.textContent = `Error: ${data.message || 'unknown'}`;
                                    if (icon) icon.innerHTML = '✗';
                                    item.classList.add('error');
                                    item.classList.remove('active');
                                }
                            }
                        }
                    }
                } catch (parseErr) {
                    console.debug('your-albums batch ndjson parse:', parseErr);
                }
            }
        }

        if (footer) {
            const info = document.getElementById('your-albums-batch-footer-info');
            if (info) info.textContent = `${totalAdded} tracks added to wishlist · ${totalSkipped} skipped`;
        }
        if (submitBtn) {
            submitBtn.style.display = '';
            submitBtn.disabled = true;
            const txt = document.getElementById('your-albums-batch-submit-text');
            if (txt) txt.textContent = 'Done';
        }
        showToast(`${totalAdded} tracks added to wishlist`, totalAdded > 0 ? 'success' : 'info');
    } catch (e) {
        console.error('Error adding your albums to wishlist:', e);
        showToast(`Error: ${e.message}`, 'error');
    }
}


function _renderCompactTrackRow(track, index) {
    const coverUrl = track.album_cover_url || '/static/placeholder-album.png';
    const durationMin = Math.floor(track.duration_ms / 60000);
    const durationSec = Math.floor((track.duration_ms % 60000) / 1000);
    const duration = `${durationMin}:${durationSec.toString().padStart(2, '0')}`;
    return `
        <div class="discover-playlist-track-compact" data-track-index="${index}">
            <div class="track-compact-number">${index + 1}</div>
            <div class="track-compact-image">
                <img src="${coverUrl}" alt="${track.album_name}" loading="lazy">
            </div>
            <div class="track-compact-info">
                <div class="track-compact-name">${track.track_name}</div>
                <div class="track-compact-artist">${track.artist_name}</div>
            </div>
            <div class="track-compact-album">${track.album_name}</div>
            <div class="track-compact-duration">${duration}</div>
        </div>
    `;
}

let _releaseRadarCtrl = null;

async function loadDiscoverReleaseRadar() {
    if (!_releaseRadarCtrl) {
        _releaseRadarCtrl = createDiscoverSectionController({
            id: 'release-radar',
            contentEl: '#release-radar-playlist',
            fetchUrl: '/api/discover/release-radar',
            extractItems: (data) => data.tracks || [],
            renderItems: (items) => {
                discoverReleaseRadarTracks = items;
                const rows = items.map((t, i) => _renderCompactTrackRow(t, i)).join('');
                return `<div class="discover-playlist-tracks-compact">${rows}</div>`;
            },
            loadingMessage: 'Loading release radar...',
            emptyMessage: 'No new releases available',
            errorMessage: 'Failed to load release radar',
            verboseErrors: true,
            showErrorToast: true,
        });
    }
    return _releaseRadarCtrl.load();
}

let _weeklyCtrl = null;

async function loadDiscoverWeekly() {
    if (!_weeklyCtrl) {
        _weeklyCtrl = createDiscoverSectionController({
            id: 'discovery-weekly',
            contentEl: '#discovery-weekly-playlist',
            fetchUrl: '/api/discover/weekly',
            extractItems: (data) => data.tracks || [],
            renderItems: (items) => {
                discoverWeeklyTracks = items;
                const rows = items.map((t, i) => _renderCompactTrackRow(t, i)).join('');
                return `<div class="discover-playlist-tracks-compact">${rows}</div>`;
            },
            loadingMessage: 'Curating your discovery playlist...',
            emptyMessage: 'No tracks available yet',
            errorMessage: 'Failed to load discovery weekly',
            verboseErrors: true,
            showErrorToast: true,
        });
    }
    return _weeklyCtrl.load();
}

// ===============================
// DECADE BROWSER
// ===============================

let selectedDecade = null;
let decadeTracks = [];

function _renderDecadeCard(decade) {
    const icon = getDecadeIcon(decade.year);
    const label = `${decade.year}s`;
    return `
        <div class="discover-card decade-card-modern" onclick="openDecadePlaylist(${decade.year})">
            <div class="discover-card-image decade-card-image">
                <div class="decade-icon-large">${icon}</div>
            </div>
            <div class="discover-card-info">
                <h4 class="discover-card-title">${label}</h4>
                <p class="discover-card-subtitle">${decade.track_count} tracks</p>
                <p class="discover-card-meta">Classics</p>
            </div>
        </div>
    `;
}

let _decadeBrowserCtrl = null;

async function loadDecadeBrowser() {
    if (!_decadeBrowserCtrl) {
        _decadeBrowserCtrl = createDiscoverSectionController({
            id: 'decade-browser',
            contentEl: '#decade-browser-carousel',
            fetchUrl: '/api/discover/decades/available',
            extractItems: (data) => data.decades || [],
            renderItems: (items) => items.map(d => _renderDecadeCard(d)).join(''),
            loadingMessage: 'Loading decades...',
            emptyMessage: 'No decade content available yet. Run a watchlist scan to populate your discovery pool!',
            errorMessage: 'Failed to load decades',
            verboseErrors: true,
            showErrorToast: true,
        });
    }
    return _decadeBrowserCtrl.load();
}

function getDecadeIcon(year) {
    const icons = {
        1950: '🎺',
        1960: '🎸',
        1970: '🕺',
        1980: '📻',
        1990: '💿',
        2000: '📱',
        2010: '🎧',
        2020: '🌐'
    };
    return icons[year] || '🎵';
}

async function openDecadePlaylist(decade) {
    try {
        showLoadingOverlay(`Loading ${decade}s playlist...`);

        const response = await fetch(`/api/discover/decade/${decade}`);
        if (!response.ok) {
            throw new Error('Failed to fetch decade playlist');
        }

        const data = await response.json();
        if (!data.success || !data.tracks || data.tracks.length === 0) {
            const message = data.message || `No tracks found for the ${decade}s`;
            showToast(message, 'info');
            hideLoadingOverlay();
            return;
        }

        selectedDecade = decade;
        decadeTracks = data.tracks;

        // Open download modal
        const playlistName = `${decade}s Classics`;
        const virtualPlaylistId = `decade_${decade}`;

        await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, data.tracks);
        hideLoadingOverlay();

    } catch (error) {
        console.error(`Error opening ${decade}s playlist:`, error);
        showToast(`Failed to load ${decade}s playlist`, 'error');
        hideLoadingOverlay();
    }
}

// ===============================
// GENRE BROWSER
// ===============================

let selectedGenre = null;
let genreTracks = [];

function _renderGenreCard(genre) {
    const icon = getGenreIcon(genre.name);
    const displayName = capitalizeGenre(genre.name);
    return `
        <div class="discover-card genre-card-modern" onclick="openGenrePlaylist('${escapeForInlineJs(genre.name)}')">
            <div class="discover-card-image genre-card-image">
                <div class="genre-icon-large">${icon}</div>
            </div>
            <div class="discover-card-info">
                <h4 class="discover-card-title">${displayName}</h4>
                <p class="discover-card-subtitle">${genre.track_count} tracks</p>
                <p class="discover-card-meta">Curated</p>
            </div>
        </div>
    `;
}

let _genreBrowserCtrl = null;

async function loadGenreBrowser() {
    if (!_genreBrowserCtrl) {
        _genreBrowserCtrl = createDiscoverSectionController({
            id: 'genre-browser',
            contentEl: '#genre-browser-carousel',
            fetchUrl: '/api/discover/genres/available',
            extractItems: (data) => data.genres || [],
            renderItems: (items) => items.map(g => _renderGenreCard(g)).join(''),
            loadingMessage: 'Loading genres...',
            emptyMessage: 'No genre content available yet. Run a watchlist scan to populate your discovery pool!',
            errorMessage: 'Failed to load genres',
            verboseErrors: true,
            showErrorToast: true,
        });
    }
    return _genreBrowserCtrl.load();
}

function getGenreIcon(genreName) {
    const genre = genreName.toLowerCase();

    // Parent genre exact matches (consolidated categories)
    if (genre === 'electronic/dance') return '🎹';
    if (genre === 'hip hop/rap') return '🎤';
    if (genre === 'rock') return '🎸';
    if (genre === 'pop') return '🎵';
    if (genre === 'r&b/soul') return '🎙️';
    if (genre === 'jazz') return '🎺';
    if (genre === 'classical') return '🎻';
    if (genre === 'metal') return '🤘';
    if (genre === 'country') return '🪕';
    if (genre === 'folk/indie') return '🎧';
    if (genre === 'latin') return '💃';
    if (genre === 'reggae/dancehall') return '🌴';
    if (genre === 'world') return '🌍';
    if (genre === 'alternative') return '🎭';
    if (genre === 'blues') return '🎸';
    if (genre === 'funk/disco') return '🕺';

    // Fallback: partial matching for specific genres
    if (genre.includes('house') || genre.includes('techno') || genre.includes('edm') ||
        genre.includes('electro') || genre.includes('trance') || genre.includes('electronic')) {
        return '🎹';
    }
    if (genre.includes('hip hop') || genre.includes('rap') || genre.includes('trap')) {
        return '🎤';
    }
    if (genre.includes('rock') || genre.includes('punk')) {
        return '🎸';
    }
    if (genre.includes('metal')) {
        return '🤘';
    }
    if (genre.includes('jazz') || genre.includes('blues')) {
        return '🎺';
    }
    if (genre.includes('pop')) {
        return '🎵';
    }
    if (genre.includes('r&b') || genre.includes('soul')) {
        return '🎙️';
    }
    if (genre.includes('country') || genre.includes('folk')) {
        return '🪕';
    }
    if (genre.includes('classical') || genre.includes('orchestra')) {
        return '🎻';
    }
    if (genre.includes('indie') || genre.includes('alternative')) {
        return '🎧';
    }
    if (genre.includes('latin') || genre.includes('reggaeton') || genre.includes('salsa')) {
        return '💃';
    }
    if (genre.includes('reggae') || genre.includes('dancehall')) {
        return '🌴';
    }
    if (genre.includes('funk') || genre.includes('disco')) {
        return '🕺';
    }

    // Default
    return '🎶';
}

function capitalizeGenre(genre) {
    // Capitalize each word in genre, handling both spaces and slashes
    return genre.split(/(\s|\/)/g)
        .map(part => {
            if (part === ' ' || part === '/') return part;
            return part.charAt(0).toUpperCase() + part.slice(1);
        })
        .join('');
}

function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

async function openGenrePlaylist(genre) {
    try {
        showLoadingOverlay(`Loading ${capitalizeGenre(genre)} playlist...`);

        const response = await fetch(`/api/discover/genre/${encodeURIComponent(genre)}`);
        if (!response.ok) {
            throw new Error('Failed to fetch genre playlist');
        }

        const data = await response.json();
        if (!data.success || !data.tracks || data.tracks.length === 0) {
            const message = data.message || `No tracks found for ${genre}`;
            showToast(message, 'info');
            hideLoadingOverlay();
            return;
        }

        selectedGenre = genre;
        genreTracks = data.tracks;

        // Open download modal
        const playlistName = `${capitalizeGenre(genre)} Mix`;
        const virtualPlaylistId = `genre_${genre.replace(/\s+/g, '_')}`;

        await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, data.tracks);
        hideLoadingOverlay();

    } catch (error) {
        console.error(`Error opening ${genre} playlist:`, error);
        showToast(`Failed to load ${genre} playlist`, 'error');
        hideLoadingOverlay();
    }
}

// ===============================
// TIME MACHINE (TABBED BY DECADE)
// ===============================

let decadeTracksCache = {}; // Store tracks for each decade
let activeDecade = null;

// Shared sync-status display block. Used by per-tab playlists
// (decade browser, genre browser) where we show download progress
// in the standard "✓ completed | ⏳ pending | ✗ failed (N%)" format.
// ListenBrainz playlists use a different shape (total/matched/failed)
// because they show MATCHING progress against the library, not
// download progress, so they intentionally don't use this helper.
function _renderSyncStatusBlock(idPrefix) {
    return `
        <div class="discover-sync-status" id="${idPrefix}-sync-status" style="display: none;">
            <div class="sync-status-content">
                <div class="sync-status-label">
                    <span class="sync-icon">⟳</span>
                    <span>Syncing to media server...</span>
                </div>
                <div class="sync-status-stats">
                    <span class="sync-stat">✓ <span id="${idPrefix}-sync-completed">0</span></span>
                    <span class="sync-stat">⏳ <span id="${idPrefix}-sync-pending">0</span></span>
                    <span class="sync-stat">✗ <span id="${idPrefix}-sync-failed">0</span></span>
                    <span class="sync-stat">(<span id="${idPrefix}-sync-percentage">0</span>%)</span>
                </div>
            </div>
        </div>
    `;
}

// ===============================
// TABBED BROWSER HELPER
// ===============================
//
// Drives the lifecycle the decade browser ("Time Machine") and
// genre browser ("Browse by Genre") share: fetch tab list → paint
// tab strip + per-tab content shells → fetch + render content for
// the active tab → handle empty / error states.
//
// The two browsers paint slightly different markup (different CSS
// prefixes, different action buttons, different sync handlers) but
// the lifecycle is identical. Each browser registers a config; the
// helper handles the rest.
//
// Two-phase render:
//   Phase 1 (loadTabs)        — paint tab strip + N content shells,
//                                each shell containing a loading
//                                spinner in its playlist container,
//                                then trigger Phase 2 for first tab.
//   Phase 2 (loadTabContent)  — fetch tracks for one tab, swap the
//                                spinner in its playlist container
//                                for the rendered track list.
//
// Renderers stay per-browser because action buttons + classes
// legitimately differ. The helper owns the lifecycle, not the look.
function createTabbedBrowserSection(config) {
    const cfg = Object.assign({
        // Diagnostic id used in console errors.
        id: 'tabbed-browser',
        // DOM IDs of the tab-strip + per-tab-contents containers.
        tabsContainerId: null,
        contentsContainerId: null,
        // Async fn returning array of tab descriptors (e.g. decades).
        fetchTabs: null,
        // (tab) => string  unique id for one tab (e.g. 'decade-1980').
        // Used as the prefix for that tab's content + playlist + sync IDs.
        tabId: null,
        // (tab) => string  HTML for one tab button. Receives `(tab, isActive)`.
        renderTabButton: null,
        // (tab) => string  HTML for one tab's content shell (action
        // buttons + sync-status block + empty playlist container).
        // The playlist container inside MUST have id `${tabId}-playlist`
        // so the helper can fill it during Phase 2.
        renderTabShell: null,
        // Async fn (tab) => array of tracks for that tab.
        fetchTabContent: null,
        // (tracks, tab) => string  HTML for the playlist container.
        renderTabTracks: null,
        // Copy / messages.
        emptyTabsMessage: 'No content available',
        emptyContentMessage: (tab) => 'No tracks found',
        errorTabsMessage: 'Failed to load',
        errorContentMessage: 'Failed to load tracks',
        // Fired after Phase 1 paints the tab strip + shells, before
        // Phase 2 is triggered for the first tab. Useful for caching
        // the tab list (e.g. `availableGenres = ...`).
        onTabsRendered: null,
    }, config || {});

    async function loadTabs() {
        try {
            const tabsContainer = document.getElementById(cfg.tabsContainerId);
            const contentsContainer = document.getElementById(cfg.contentsContainerId);
            if (!tabsContainer || !contentsContainer) return;

            const tabs = await cfg.fetchTabs();
            if (!Array.isArray(tabs) || tabs.length === 0) {
                tabsContainer.innerHTML = `<div class="discover-empty"><p>${cfg.emptyTabsMessage}</p></div>`;
                return;
            }

            let tabsHTML = '';
            let contentsHTML = '';
            tabs.forEach((tab, index) => {
                const isActive = index === 0;
                tabsHTML += cfg.renderTabButton(tab, isActive);
                contentsHTML += cfg.renderTabShell(tab, isActive);
            });

            tabsContainer.innerHTML = tabsHTML;
            contentsContainer.innerHTML = contentsHTML;

            if (typeof cfg.onTabsRendered === 'function') {
                try { cfg.onTabsRendered(tabs); }
                catch (err) { console.debug(`[${cfg.id}] onTabsRendered threw:`, err); }
            }

            // Phase 2: kick off content load for the first tab.
            await loadTabContent(tabs[0]);
        } catch (error) {
            console.error(`Error loading ${cfg.id} tabs:`, error);
            const tabsContainer = document.getElementById(cfg.tabsContainerId);
            if (tabsContainer) {
                tabsContainer.innerHTML = `<div class="discover-empty"><p>${cfg.errorTabsMessage}</p></div>`;
            }
        }
    }

    async function loadTabContent(tab) {
        const tabId = cfg.tabId(tab);
        const playlistContainer = document.getElementById(`${tabId}-playlist`);
        if (!playlistContainer) return;

        try {
            const tracks = await cfg.fetchTabContent(tab);
            if (!Array.isArray(tracks) || tracks.length === 0) {
                const msg = (typeof cfg.emptyContentMessage === 'function')
                    ? cfg.emptyContentMessage(tab)
                    : cfg.emptyContentMessage;
                playlistContainer.innerHTML = `<div class="discover-empty"><p>${msg}</p></div>`;
                return;
            }
            playlistContainer.innerHTML = cfg.renderTabTracks(tracks, tab);
        } catch (error) {
            console.error(`Error loading ${cfg.id} tab content:`, error);
            const stillThere = document.getElementById(`${tabId}-playlist`);
            if (stillThere) {
                stillThere.innerHTML = `<div class="discover-empty"><p>${cfg.errorContentMessage}</p></div>`;
            }
        }
    }

    return { loadTabs, loadTabContent };
}

// ----- Decade browser config + thin wrappers -----

let _decadeBrowserTabsCtrl = null;

function _getDecadeBrowserTabsCtrl() {
    if (_decadeBrowserTabsCtrl) return _decadeBrowserTabsCtrl;
    _decadeBrowserTabsCtrl = createTabbedBrowserSection({
        id: 'decade-browser-tabs',
        tabsContainerId: 'decade-tabs',
        contentsContainerId: 'decade-tab-contents',
        fetchTabs: async () => {
            const response = await fetch('/api/discover/decades/available');
            if (!response.ok) throw new Error('Failed to fetch available decades');
            const data = await response.json();
            if (!data.success) return [];
            return data.decades || [];
        },
        tabId: (decade) => `decade-${decade.year}`,
        renderTabButton: (decade, isActive) => {
            const icon = getDecadeIcon(decade.year);
            return `
                <button class="decade-tab ${isActive ? 'active' : ''}"
                        data-decade="${decade.year}"
                        onclick="switchDecadeTab(${decade.year})">
                    ${icon} ${decade.year}s
                </button>
            `;
        },
        renderTabShell: (decade, isActive) => {
            const tabId = `decade-${decade.year}`;
            return `
                <div class="decade-tab-content ${isActive ? 'active' : ''}" id="${tabId}-content">
                    <!-- Action Buttons -->
                    <div class="decade-actions">
                        <div class="discover-section-header">
                            <div>
                                <h3 style="margin: 0; color: #fff; font-size: 18px;">${decade.year}s Classics</h3>
                                <p id="${tabId}-subtitle" style="margin: 4px 0 0 0; color: #999; font-size: 13px;">${decade.track_count} tracks</p>
                            </div>
                            <div class="discover-section-actions">
                                <button class="action-button secondary" onclick="openDownloadModalForDecade(${decade.year})" title="Download missing tracks">
                                    <span class="button-icon">↓</span>
                                    <span class="button-text">Download</span>
                                </button>
                                <button class="action-button primary" id="${tabId}-sync-btn" onclick="startDecadeSync(${decade.year})" title="Sync to media server">
                                    <span class="button-icon">⟳</span>
                                    <span class="button-text">Sync</span>
                                </button>
                            </div>
                        </div>

                        ${_renderSyncStatusBlock(tabId)}
                    </div>

                    <!-- Track List -->
                    <div class="discover-playlist-container compact" id="${tabId}-playlist">
                        <div class="discover-loading"><div class="loading-spinner"></div><p>Loading ${decade.year}s tracks...</p></div>
                    </div>
                </div>
            `;
        },
        fetchTabContent: async (decade) => {
            const response = await fetch(`/api/discover/decade/${decade.year}`);
            if (!response.ok) throw new Error('Failed to fetch decade playlist');
            const data = await response.json();
            if (!data.success) return [];
            // Side-effect: cache + active marker, exactly as old code did.
            decadeTracksCache[decade.year] = data.tracks || [];
            activeDecade = decade.year;
            return data.tracks || [];
        },
        renderTabTracks: (tracks) => _renderTabbedTrackList(tracks),
        emptyContentMessage: (decade) => `No tracks found for the ${decade.year}s`,
        errorTabsMessage: 'Failed to load decades',
        errorContentMessage: 'Failed to load decade tracks',
        emptyTabsMessage: 'No decade content available yet. Run a watchlist scan to populate your discovery pool!',
    });
    return _decadeBrowserTabsCtrl;
}

// Shared track-row markup for tabbed browsers. Decade + genre rows
// have the same shape — both pull from `track_data_json` first then
// fall back to top-level fields. Lifted so the helper-driven
// renderers don't each carry a copy.
function _renderTabbedTrackList(tracks) {
    let html = '<div class="discover-playlist-tracks-compact">';
    tracks.forEach((track, index) => {
        let trackData = track;
        if (track.track_data_json) {
            trackData = track.track_data_json;
        }

        const trackName = trackData.name || trackData.track_name || track.track_name || 'Unknown Track';
        const artistName = trackData.artists?.[0]?.name || trackData.artists?.[0] || trackData.artist_name || track.artist_name || 'Unknown Artist';
        const albumName = trackData.album?.name || trackData.album_name || track.album_name || 'Unknown Album';
        const coverUrl = trackData.album?.images?.[0]?.url || track.album_cover_url || '/static/placeholder-album.png';
        const durationMs = trackData.duration_ms || track.duration_ms || 0;

        const durationMin = Math.floor(durationMs / 60000);
        const durationSec = Math.floor((durationMs % 60000) / 1000);
        const duration = `${durationMin}:${durationSec.toString().padStart(2, '0')}`;

        html += `
                <div class="discover-playlist-track-compact" data-track-index="${index}">
                    <div class="track-compact-number">${index + 1}</div>
                    <div class="track-compact-image">
                        <img src="${coverUrl}" alt="${albumName}" loading="lazy">
                    </div>
                    <div class="track-compact-info">
                        <div class="track-compact-name">${trackName}</div>
                        <div class="track-compact-artist">${artistName}</div>
                    </div>
                    <div class="track-compact-album">${albumName}</div>
                    <div class="track-compact-duration">${duration}</div>
                </div>
            `;
    });
    html += '</div>';
    return html;
}

async function loadDecadeBrowserTabs() {
    return _getDecadeBrowserTabsCtrl().loadTabs();
}

function switchDecadeTab(decade) {
    // Update tab buttons
    const tabs = document.querySelectorAll('.decade-tab');
    tabs.forEach(tab => {
        if (parseInt(tab.getAttribute('data-decade')) === decade) {
            tab.classList.add('active');
        } else {
            tab.classList.remove('active');
        }
    });

    // Update tab content
    const tabContents = document.querySelectorAll('.decade-tab-content');
    tabContents.forEach(content => {
        if (content.id === `decade-${decade}-content`) {
            content.classList.add('active');
        } else {
            content.classList.remove('active');
        }
    });

    // Load tracks if not already loaded
    if (!decadeTracksCache[decade]) {
        loadDecadeTracks(decade);
    }
}

async function loadDecadeTracks(decade) {
    return _getDecadeBrowserTabsCtrl().loadTabContent({ year: decade });
}

async function startDecadeSync(decade) {
    const tracks = decadeTracksCache[decade];
    if (!tracks || tracks.length === 0) {
        showToast('No tracks available for this decade', 'warning');
        return;
    }

    // Convert to format expected by sync API
    const spotifyTracks = tracks.map(track => {
        // Extract track data from track_data_json if available
        let trackData = track;
        if (track.track_data_json) {
            trackData = track.track_data_json;
        }

        // Build properly formatted Spotify track object
        let spotifyTrack = {
            id: trackData.id || track.spotify_track_id,
            name: trackData.name || trackData.track_name || track.track_name,
            artists: trackData.artists || [{ name: trackData.artist_name || track.artist_name }],
            album: trackData.album || {
                name: trackData.album_name || track.album_name,
                images: trackData.album?.images || (track.album_cover_url ? [{ url: track.album_cover_url }] : [])
            },
            duration_ms: trackData.duration_ms || track.duration_ms || 0
        };

        // Normalize artists to array of strings for sync compatibility
        if (spotifyTrack.artists && Array.isArray(spotifyTrack.artists)) {
            spotifyTrack.artists = spotifyTrack.artists.map(a => a.name || a);
        }

        return spotifyTrack;
    });

    const virtualPlaylistId = `discover_decade_${decade}`;
    playlistTrackCache[virtualPlaylistId] = spotifyTracks;

    const virtualPlaylist = {
        id: virtualPlaylistId,
        name: `${decade}s Classics`,
        track_count: spotifyTracks.length
    };

    if (!spotifyPlaylists.find(p => p.id === virtualPlaylistId)) {
        spotifyPlaylists.push(virtualPlaylist);
    }

    // Show sync status display
    const statusDisplay = document.getElementById(`decade-${decade}-sync-status`);
    if (statusDisplay) statusDisplay.style.display = 'block';

    // Disable sync button
    const syncButton = document.getElementById(`decade-${decade}-sync-btn`);
    if (syncButton) {
        syncButton.disabled = true;
        syncButton.style.opacity = '0.5';
        syncButton.style.cursor = 'not-allowed';
    }

    // Start sync
    await startPlaylistSync(virtualPlaylistId);

    // Start polling
    startDecadeSyncPolling(decade, virtualPlaylistId);
}

function startDecadeSyncPolling(decade, virtualPlaylistId) {
    const pollerId = `decade_${decade}`;

    if (discoverSyncPollers[pollerId]) {
        clearInterval(discoverSyncPollers[pollerId]);
    }

    // Phase 5: Subscribe via WebSocket
    if (socketConnected) {
        socket.emit('sync:subscribe', { playlist_ids: [virtualPlaylistId] });
        _syncProgressCallbacks[virtualPlaylistId] = (data) => {
            const progress = data.progress || {};
            const total = progress.total_tracks || 0;
            const matched = progress.matched_tracks || 0;
            const failed = progress.failed_tracks || 0;
            const processed = matched + failed;
            const pending = total - processed;
            const pct = total > 0 ? Math.round((processed / total) * 100) : 0;
            const el = (id) => document.getElementById(id);
            if (el(`decade-${decade}-sync-completed`)) el(`decade-${decade}-sync-completed`).textContent = matched;
            if (el(`decade-${decade}-sync-pending`)) el(`decade-${decade}-sync-pending`).textContent = pending;
            if (el(`decade-${decade}-sync-failed`)) el(`decade-${decade}-sync-failed`).textContent = failed;
            if (el(`decade-${decade}-sync-percentage`)) el(`decade-${decade}-sync-percentage`).textContent = pct;
            if (data.status === 'finished') {
                if (discoverSyncPollers[pollerId]) { clearInterval(discoverSyncPollers[pollerId]); delete discoverSyncPollers[pollerId]; }
                socket.emit('sync:unsubscribe', { playlist_ids: [virtualPlaylistId] });
                delete _syncProgressCallbacks[virtualPlaylistId];
                const syncButton = el(`decade-${decade}-sync-btn`);
                if (syncButton) { syncButton.disabled = false; syncButton.style.opacity = '1'; syncButton.style.cursor = 'pointer'; }
                showToast(`${decade}s Classics sync complete!`, 'success');
                setTimeout(() => { const sd = el(`decade-${decade}-sync-status`); if (sd) sd.style.display = 'none'; }, 3000);
            }
        };
    }

    discoverSyncPollers[pollerId] = setInterval(async () => {
        // Always poll — no dedicated WebSocket events for discovery progress
        try {
            const response = await fetch(`/api/sync/status/${virtualPlaylistId}`);
            if (!response.ok) return;

            const data = await response.json();
            const progress = data.progress || {};

            const completedEl = document.getElementById(`decade-${decade}-sync-completed`);
            const pendingEl = document.getElementById(`decade-${decade}-sync-pending`);
            const failedEl = document.getElementById(`decade-${decade}-sync-failed`);
            const percentageEl = document.getElementById(`decade-${decade}-sync-percentage`);

            const total = progress.total_tracks || 0;
            const matched = progress.matched_tracks || 0;
            const failed = progress.failed_tracks || 0;
            const processed = matched + failed;
            const pending = total - processed;
            const completionPercentage = total > 0 ? Math.round((processed / total) * 100) : 0;

            if (completedEl) completedEl.textContent = matched;
            if (pendingEl) pendingEl.textContent = pending;
            if (failedEl) failedEl.textContent = failed;
            if (percentageEl) percentageEl.textContent = completionPercentage;

            if (data.status === 'finished') {
                clearInterval(discoverSyncPollers[pollerId]);
                delete discoverSyncPollers[pollerId];

                const syncButton = document.getElementById(`decade-${decade}-sync-btn`);
                if (syncButton) {
                    syncButton.disabled = false;
                    syncButton.style.opacity = '1';
                    syncButton.style.cursor = 'pointer';
                }

                showToast(`${decade}s Classics sync complete!`, 'success');

                setTimeout(() => {
                    const statusDisplay = document.getElementById(`decade-${decade}-sync-status`);
                    if (statusDisplay) statusDisplay.style.display = 'none';
                }, 3000);
            }
        } catch (error) {
            console.error(`Error polling sync status for decade ${decade}:`, error);
        }
    }, 500);
}

async function openDownloadModalForDecade(decade) {
    const tracks = decadeTracksCache[decade];
    if (!tracks || tracks.length === 0) {
        showToast('No tracks available for this decade', 'warning');
        return;
    }

    // Convert to format expected by download modal
    const spotifyTracks = tracks.map(track => {
        // Extract track data from track_data_json if available
        let trackData = track;
        if (track.track_data_json) {
            trackData = track.track_data_json;
        }

        // Build properly formatted Spotify track object
        let spotifyTrack = {
            id: trackData.id || track.spotify_track_id,
            name: trackData.name || trackData.track_name || track.track_name,
            artists: trackData.artists || [{ name: trackData.artist_name || track.artist_name }],
            album: trackData.album || {
                name: trackData.album_name || track.album_name,
                images: trackData.album?.images || (track.album_cover_url ? [{ url: track.album_cover_url }] : [])
            },
            duration_ms: trackData.duration_ms || track.duration_ms || 0
        };

        return spotifyTrack;
    });

    const playlistName = `${decade}s Classics`;
    const virtualPlaylistId = `decade_${decade}`;

    await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, spotifyTracks);
}

// ===============================
// BROWSE BY GENRE (TABBED BY GENRE)
// ===============================

let genreTracksCache = {}; // Store tracks for each genre
let activeGenre = null;
let availableGenres = [];

// Helper: derive the URL/DOM-safe id used for a genre tab. Both
// the tab shell and the playlist-container element are keyed on this.
function _genreTabId(genreName) {
    const safe = genreName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9-]/g, '');
    return `genre-${safe}`;
}

let _genreBrowserTabsCtrl = null;

function _getGenreBrowserTabsCtrl() {
    if (_genreBrowserTabsCtrl) return _genreBrowserTabsCtrl;
    _genreBrowserTabsCtrl = createTabbedBrowserSection({
        id: 'genre-browser-tabs',
        tabsContainerId: 'genre-tabs',
        contentsContainerId: 'genre-tab-contents',
        fetchTabs: async () => {
            const response = await fetch('/api/discover/genres/available');
            if (!response.ok) throw new Error('Failed to fetch available genres');
            const data = await response.json();
            if (!data.success) return [];
            // Cap at 10 to avoid overcrowding; old behavior preserved.
            return (data.genres || []).slice(0, 10);
        },
        onTabsRendered: (genres) => { availableGenres = genres; },
        tabId: (genre) => _genreTabId(genre.name),
        renderTabButton: (genre, isActive) => {
            const icon = getGenreIcon(genre.name);
            return `
                <button class="genre-tab ${isActive ? 'active' : ''}"
                        data-genre="${escapeHtml(genre.name)}"
                        onclick="switchGenreTab('${escapeForInlineJs(genre.name)}')">
                    ${icon} ${capitalizeGenre(genre.name)}
                </button>
            `;
        },
        renderTabShell: (genre, isActive) => {
            const tabId = _genreTabId(genre.name);
            return `
                <div class="genre-tab-content ${isActive ? 'active' : ''}" id="${tabId}-content" data-genre="${escapeHtml(genre.name)}">
                    <!-- Action Buttons -->
                    <div class="genre-actions">
                        <div class="discover-section-header">
                            <div>
                                <h3 style="margin: 0; color: #fff; font-size: 18px;">${capitalizeGenre(genre.name)} Mix</h3>
                                <p id="${tabId}-subtitle" style="margin: 4px 0 0 0; color: #999; font-size: 13px;">${genre.track_count} tracks</p>
                            </div>
                            <div class="discover-section-actions">
                                <button class="action-button secondary" onclick="openDownloadModalForGenre('${escapeForInlineJs(genre.name)}')" title="Download missing tracks">
                                    <span class="button-icon">↓</span>
                                    <span class="button-text">Download</span>
                                </button>
                                <button class="action-button primary" id="${tabId}-sync-btn" onclick="startGenreSync('${escapeForInlineJs(genre.name)}')" title="Sync to media server">
                                    <span class="button-icon">⟳</span>
                                    <span class="button-text">Sync</span>
                                </button>
                            </div>
                        </div>

                        ${_renderSyncStatusBlock(tabId)}
                    </div>

                    <!-- Track List -->
                    <div class="discover-playlist-container compact" id="${tabId}-playlist">
                        <div class="discover-loading"><div class="loading-spinner"></div><p>Loading ${capitalizeGenre(genre.name)} tracks...</p></div>
                    </div>
                </div>
            `;
        },
        fetchTabContent: async (genre) => {
            const response = await fetch(`/api/discover/genre/${encodeURIComponent(genre.name)}`);
            if (!response.ok) throw new Error('Failed to fetch genre playlist');
            const data = await response.json();
            if (!data.success) return [];
            // Side-effect: cache + active marker, exactly as old code did.
            genreTracksCache[genre.name] = data.tracks || [];
            activeGenre = genre.name;
            return data.tracks || [];
        },
        renderTabTracks: (tracks) => _renderTabbedTrackList(tracks),
        emptyContentMessage: (genre) => `No tracks found for ${capitalizeGenre(genre.name)}`,
        errorTabsMessage: 'Failed to load genres',
        errorContentMessage: 'Failed to load genre tracks',
        emptyTabsMessage: 'No genre content available yet. Run a watchlist scan to populate your discovery pool!',
    });
    return _genreBrowserTabsCtrl;
}

async function loadGenreBrowserTabs() {
    return _getGenreBrowserTabsCtrl().loadTabs();
}

function switchGenreTab(genreName) {
    // Update tab buttons
    const tabs = document.querySelectorAll('.genre-tab');
    tabs.forEach(tab => {
        if (tab.getAttribute('data-genre') === genreName) {
            tab.classList.add('active');
        } else {
            tab.classList.remove('active');
        }
    });

    // Update tab content
    const tabContents = document.querySelectorAll('.genre-tab-content');
    tabContents.forEach(content => {
        if (content.getAttribute('data-genre') === genreName) {
            content.classList.add('active');
        } else {
            content.classList.remove('active');
        }
    });

    // Load tracks if not already loaded
    if (!genreTracksCache[genreName]) {
        loadGenreTracks(genreName);
    }
}

async function loadGenreTracks(genreName) {
    return _getGenreBrowserTabsCtrl().loadTabContent({ name: genreName });
}

async function startGenreSync(genreName) {
    const tracks = genreTracksCache[genreName];
    if (!tracks || tracks.length === 0) {
        showToast('No tracks available for this genre', 'warning');
        return;
    }

    const genreId = genreName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9-]/g, '');

    // Convert to format expected by sync API
    const spotifyTracks = tracks.map(track => {
        // Extract track data from track_data_json if available
        let trackData = track;
        if (track.track_data_json) {
            trackData = track.track_data_json;
        }

        // Build properly formatted Spotify track object
        let spotifyTrack = {
            id: trackData.id || track.spotify_track_id,
            name: trackData.name || trackData.track_name || track.track_name,
            artists: trackData.artists || [{ name: trackData.artist_name || track.artist_name }],
            album: trackData.album || {
                name: trackData.album_name || track.album_name,
                images: trackData.album?.images || (track.album_cover_url ? [{ url: track.album_cover_url }] : [])
            },
            duration_ms: trackData.duration_ms || track.duration_ms || 0
        };

        // Normalize artists to array of strings for sync compatibility
        if (spotifyTrack.artists && Array.isArray(spotifyTrack.artists)) {
            spotifyTrack.artists = spotifyTrack.artists.map(a => a.name || a);
        }

        return spotifyTrack;
    });

    const virtualPlaylistId = `discover_genre_${genreName.replace(/\s+/g, '_')}`;
    playlistTrackCache[virtualPlaylistId] = spotifyTracks;

    const virtualPlaylist = {
        id: virtualPlaylistId,
        name: `${capitalizeGenre(genreName)} Mix`,
        track_count: spotifyTracks.length
    };

    if (!spotifyPlaylists.find(p => p.id === virtualPlaylistId)) {
        spotifyPlaylists.push(virtualPlaylist);
    }

    // Show sync status display
    const statusDisplay = document.getElementById(`genre-${genreId}-sync-status`);
    if (statusDisplay) statusDisplay.style.display = 'block';

    // Disable sync button
    const syncButton = document.getElementById(`genre-${genreId}-sync-btn`);
    if (syncButton) {
        syncButton.disabled = true;
        syncButton.style.opacity = '0.5';
        syncButton.style.cursor = 'not-allowed';
    }

    // Start sync
    await startPlaylistSync(virtualPlaylistId);

    // Start polling
    startGenreSyncPolling(genreName, genreId, virtualPlaylistId);
}

function startGenreSyncPolling(genreName, genreId, virtualPlaylistId) {
    const pollerId = `genre_${genreId}`;

    if (discoverSyncPollers[pollerId]) {
        clearInterval(discoverSyncPollers[pollerId]);
    }

    // Phase 5: Subscribe via WebSocket
    if (socketConnected) {
        socket.emit('sync:subscribe', { playlist_ids: [virtualPlaylistId] });
        _syncProgressCallbacks[virtualPlaylistId] = (data) => {
            const progress = data.progress || {};
            const total = progress.total_tracks || 0;
            const matched = progress.matched_tracks || 0;
            const failed = progress.failed_tracks || 0;
            const processed = matched + failed;
            const pending = total - processed;
            const pct = total > 0 ? Math.round((processed / total) * 100) : 0;
            const el = (id) => document.getElementById(id);
            if (el(`genre-${genreId}-sync-completed`)) el(`genre-${genreId}-sync-completed`).textContent = matched;
            if (el(`genre-${genreId}-sync-pending`)) el(`genre-${genreId}-sync-pending`).textContent = pending;
            if (el(`genre-${genreId}-sync-failed`)) el(`genre-${genreId}-sync-failed`).textContent = failed;
            if (el(`genre-${genreId}-sync-percentage`)) el(`genre-${genreId}-sync-percentage`).textContent = pct;
            if (data.status === 'finished') {
                if (discoverSyncPollers[pollerId]) { clearInterval(discoverSyncPollers[pollerId]); delete discoverSyncPollers[pollerId]; }
                socket.emit('sync:unsubscribe', { playlist_ids: [virtualPlaylistId] });
                delete _syncProgressCallbacks[virtualPlaylistId];
                const syncButton = el(`genre-${genreId}-sync-btn`);
                if (syncButton) { syncButton.disabled = false; syncButton.style.opacity = '1'; syncButton.style.cursor = 'pointer'; }
                showToast(`${capitalizeGenre(genreName)} Mix sync complete!`, 'success');
                setTimeout(() => { const sd = el(`genre-${genreId}-sync-status`); if (sd) sd.style.display = 'none'; }, 3000);
            }
        };
    }

    discoverSyncPollers[pollerId] = setInterval(async () => {
        // Always poll — no dedicated WebSocket events for discovery progress
        try {
            const response = await fetch(`/api/sync/status/${virtualPlaylistId}`);
            if (!response.ok) return;

            const data = await response.json();
            const progress = data.progress || {};

            const completedEl = document.getElementById(`genre-${genreId}-sync-completed`);
            const pendingEl = document.getElementById(`genre-${genreId}-sync-pending`);
            const failedEl = document.getElementById(`genre-${genreId}-sync-failed`);
            const percentageEl = document.getElementById(`genre-${genreId}-sync-percentage`);

            const total = progress.total_tracks || 0;
            const matched = progress.matched_tracks || 0;
            const failed = progress.failed_tracks || 0;
            const processed = matched + failed;
            const pending = total - processed;
            const completionPercentage = total > 0 ? Math.round((processed / total) * 100) : 0;

            if (completedEl) completedEl.textContent = matched;
            if (pendingEl) pendingEl.textContent = pending;
            if (failedEl) failedEl.textContent = failed;
            if (percentageEl) percentageEl.textContent = completionPercentage;

            if (data.status === 'finished') {
                clearInterval(discoverSyncPollers[pollerId]);
                delete discoverSyncPollers[pollerId];

                const syncButton = document.getElementById(`genre-${genreId}-sync-btn`);
                if (syncButton) {
                    syncButton.disabled = false;
                    syncButton.style.opacity = '1';
                    syncButton.style.cursor = 'pointer';
                }

                showToast(`${capitalizeGenre(genreName)} Mix sync complete!`, 'success');

                setTimeout(() => {
                    const statusDisplay = document.getElementById(`genre-${genreId}-sync-status`);
                    if (statusDisplay) statusDisplay.style.display = 'none';
                }, 3000);
            }
        } catch (error) {
            console.error(`Error polling sync status for genre ${genreName}:`, error);
        }
    }, 500);
}

async function openDownloadModalForGenre(genreName) {
    const tracks = genreTracksCache[genreName];
    if (!tracks || tracks.length === 0) {
        showToast('No tracks available for this genre', 'warning');
        return;
    }

    // Convert to format expected by download modal
    const spotifyTracks = tracks.map(track => {
        // Extract track data from track_data_json if available
        let trackData = track;
        if (track.track_data_json) {
            trackData = track.track_data_json;
        }

        // Build properly formatted Spotify track object
        let spotifyTrack = {
            id: trackData.id || track.spotify_track_id,
            name: trackData.name || trackData.track_name || track.track_name,
            artists: trackData.artists || [{ name: trackData.artist_name || track.artist_name }],
            album: trackData.album || {
                name: trackData.album_name || track.album_name,
                images: trackData.album?.images || (track.album_cover_url ? [{ url: track.album_cover_url }] : [])
            },
            duration_ms: trackData.duration_ms || track.duration_ms || 0
        };

        return spotifyTrack;
    });

    const playlistName = `${capitalizeGenre(genreName)} Mix`;
    const virtualPlaylistId = `genre_${genreName.replace(/\s+/g, '_')}`;

    await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, spotifyTracks);
}

// ===============================
// LISTENBRAINZ PLAYLISTS
// ===============================

let listenbrainzPlaylistsCache = {}; // Store playlists by type
let listenbrainzTracksCache = {}; // Store tracks for each playlist
let activeListenBrainzTab = 'recommendations'; // Track active tab
let activeListenBrainzSubTab = null; // Track active sub-tab within recommendations

// ── Last.fm Track Radio ──────────────────────────────────────────────────────

let _lastfmRadioDebounceTimer = null;
let _lastfmRadioSelected = null; // {name, artist}

function debouncedLastfmTrackSearch(query) {
    clearTimeout(_lastfmRadioDebounceTimer);
    const q = (query || '').trim();
    if (!q) {
        document.getElementById('lastfm-radio-dropdown').style.display = 'none';
        return;
    }
    _lastfmRadioDebounceTimer = setTimeout(() => _runLastfmTrackSearch(q), 400);
}

async function _runLastfmTrackSearch(q) {
    if (q.length < 2) return;
    const dropdown = document.getElementById('lastfm-radio-dropdown');
    // Show a mini spinner while fetching
    dropdown.innerHTML = '<div class="lastfm-radio-searching"><div class="server-search-spinner" style="width:14px;height:14px;margin:0 auto;"></div></div>';
    dropdown.style.display = 'block';
    try {
        const res = await fetch(`/api/lastfm/search/tracks?q=${encodeURIComponent(q)}`);
        if (!res.ok) { dropdown.style.display = 'none'; return; }
        const data = await res.json();
        if (!data.results || data.results.length === 0) {
            dropdown.style.display = 'none';
            return;
        }
        dropdown.innerHTML = data.results.map(t => {
            const imgHtml = t.image_url
                ? `<img src="${t.image_url}" alt="" loading="lazy" onerror="this.parentElement.innerHTML='<div class=\\'lastfm-radio-art-empty\\'></div>'">`
                : '<div class="lastfm-radio-art-empty"></div>';
            const listeners = t.listeners > 0
                ? `<span class="lastfm-radio-result-listeners">${(t.listeners / 1000).toFixed(0)}k listeners</span>`
                : '';
            return `
                <div class="lastfm-radio-result" onclick="selectLastfmRadioTrack(decodeURIComponent('${encodeURIComponent(t.name)}'), decodeURIComponent('${encodeURIComponent(t.artist)}'))">
                    <div class="lastfm-radio-result-art">${imgHtml}</div>
                    <div class="lastfm-radio-result-meta">
                        <span class="lastfm-radio-result-track">${t.name}</span>
                        <span class="lastfm-radio-result-artist">${t.artist}${listeners ? ' · ' + t.listeners.toLocaleString() + ' listeners' : ''}</span>
                    </div>
                </div>`;
        }).join('');
        dropdown.style.display = 'block';
    } catch (e) {
        console.error('Last.fm search error:', e);
        dropdown.style.display = 'none';
    }
}

function selectLastfmRadioTrack(name, artist) {
    // Close dropdown and update input to show selection
    document.getElementById('lastfm-radio-dropdown').style.display = 'none';
    document.getElementById('lastfm-radio-input').value = `${name} — ${artist}`;
    document.getElementById('lastfm-radio-input').blur();
    // Immediately kick off generation
    _generateLastfmRadioFor(name, artist);
}

function clearLastfmRadioSelection() {
    document.getElementById('lastfm-radio-input').value = '';
    document.getElementById('lastfm-radio-dropdown').style.display = 'none';
}

// Keep generateLastfmRadio as public alias (called by nothing now but harmless)
async function generateLastfmRadio() {
    const input = (document.getElementById('lastfm-radio-input').value || '').trim();
    if (!input) return;
    // Parse "Track — Artist" format if present
    const parts = input.split(' — ');
    if (parts.length >= 2) {
        await _generateLastfmRadioFor(parts[0].trim(), parts[1].trim());
    }
}

async function _generateLastfmRadioFor(name, artist) {
    const container = document.getElementById('lastfm-radio-playlists');
    const input = document.getElementById('lastfm-radio-input');

    // Show loading state in the playlists area
    if (container) {
        container.innerHTML = `
            <div class="lastfm-radio-generating">
                <div class="server-search-spinner"></div>
                <p>Building radio for <strong>${name}</strong> by <strong>${artist}</strong>…</p>
            </div>`;
    }
    if (input) input.disabled = true;

    try {
        const res = await fetch('/api/lastfm/radio/generate', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ track_name: name, artist_name: artist }),
        });
        const data = await res.json();
        if (!data.success) {
            if (container) container.innerHTML = '';
            showToast(data.error || 'Failed to generate radio', 'error');
            return;
        }
        // Reload all radio playlist cards
        await _loadLastfmRadioPlaylists();
    } catch (e) {
        if (container) container.innerHTML = '';
        showToast('Error generating Last.fm radio', 'error');
        console.error(e);
    } finally {
        if (input) input.disabled = false;
    }
}

async function initializeLastfmRadioSection() {
    try {
        const cfgRes = await fetch('/api/lastfm/configured');
        if (!cfgRes.ok) return;
        const { configured } = await cfgRes.json();
        const section = document.getElementById('lastfm-radio-section');
        if (!section) return;
        if (!configured) {
            section.style.display = 'none';
            return;
        }
        section.style.display = '';
        await _loadLastfmRadioPlaylists();
    } catch (e) {
        console.error('Error initializing Last.fm Radio section:', e);
    }
}

async function _loadLastfmRadioPlaylists() {
    const container = document.getElementById('lastfm-radio-playlists');
    if (!container) return;
    try {
        const res = await fetch('/api/discover/listenbrainz/lastfm-radio');
        if (!res.ok) return;
        const data = await res.json();
        if (!data.success || !data.playlists || data.playlists.length === 0) {
            container.innerHTML = '';
            return;
        }
        // Reuse the same LB playlist card builder — cards are identical
        container.innerHTML = buildListenBrainzPlaylistsHtml(data.playlists, 'lastfm_radio');
        loadTracksForPlaylists(data.playlists);
    } catch (e) {
        console.error('Error loading Last.fm radio playlists:', e);
    }
}

// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
    const section = document.getElementById('lastfm-radio-search-section');
    if (section && !section.contains(e.target)) {
        const dd = document.getElementById('lastfm-radio-dropdown');
        if (dd) dd.style.display = 'none';
    }
});

// ────────────────────────────────────────────────────────────────────────────

async function initializeListenBrainzTabs() {
    try {
        console.log('🧠 Initializing ListenBrainz tabs...');

        // Fetch all playlist types
        const [createdForRes, userPlaylistsRes, collaborativeRes] = await Promise.all([
            fetch('/api/discover/listenbrainz/created-for'),
            fetch('/api/discover/listenbrainz/user-playlists'),
            fetch('/api/discover/listenbrainz/collaborative'),
        ]);

        console.log('📡 API Responses:', {
            createdFor: createdForRes.status,
            userPlaylists: userPlaylistsRes.status,
            collaborative: collaborativeRes.status,
        });

        const tabs = [
            { id: 'recommendations', label: '🎁 Recommendations', hasData: false },
            { id: 'user', label: '📚 Your Playlists', hasData: false },
            { id: 'collaborative', label: '🤝 Collaborative', hasData: false },
        ];

        // Track LB username for header display
        let lbUsername = null;

        // Check which tabs have data
        if (createdForRes.ok) {
            const data = await createdForRes.json();
            console.log('📋 Created For data:', data);
            if (data.username) lbUsername = data.username;
            if (data.success && data.playlists && data.playlists.length > 0) {
                listenbrainzPlaylistsCache['recommendations'] = data.playlists;
                tabs[0].hasData = true;
                console.log(`✅ Found ${data.playlists.length} recommendation playlists`);
            }
        }

        if (userPlaylistsRes.ok) {
            const data = await userPlaylistsRes.json();
            console.log('📚 User Playlists data:', data);
            if (data.username && !lbUsername) lbUsername = data.username;
            if (data.success && data.playlists && data.playlists.length > 0) {
                listenbrainzPlaylistsCache['user'] = data.playlists;
                tabs[1].hasData = true;
                console.log(`✅ Found ${data.playlists.length} user playlists`);
            }
        }

        if (collaborativeRes.ok) {
            const data = await collaborativeRes.json();
            console.log('🤝 Collaborative data:', data);
            if (data.username && !lbUsername) lbUsername = data.username;
            if (data.success && data.playlists && data.playlists.length > 0) {
                listenbrainzPlaylistsCache['collaborative'] = data.playlists;
                tabs[2].hasData = true;
                console.log(`✅ Found ${data.playlists.length} collaborative playlists`);
            }
        }

        // Build tabs HTML
        const tabsContainer = document.getElementById('listenbrainz-tabs');
        console.log('🔧 Building tabs. Available tabs:', tabs.filter(t => t.hasData).map(t => t.label));

        let tabsHtml = '<div class="decade-tabs-inner">'; // Reuse decade tabs styling

        tabs.forEach(tab => {
            if (tab.hasData) {
                const isActive = tab.id === activeListenBrainzTab;
                tabsHtml += `
                    <button class="decade-tab${isActive ? ' active' : ''}"
                            onclick="switchListenBrainzTab('${tab.id}')"
                            data-tab="${tab.id}">
                        ${tab.label}
                    </button>
                `;
            }
        });
        tabsHtml += '</div>';

        if (tabs.every(t => !t.hasData)) {
            console.log('⚠️ No tabs have data');
            tabsContainer.innerHTML = `
                <div class="lb-empty-state">
                    <div class="lb-empty-icon">&#129504;</div>
                    <h3>Connect ListenBrainz</h3>
                    <p>Link your ListenBrainz account to see personalized playlists, recommendations, and collaborative playlists.</p>
                    <button class="action-button primary lb-connect-btn" onclick="openPersonalSettings()">
                        Connect ListenBrainz
                    </button>
                    <p class="lb-empty-help">Get your token from <a href="https://listenbrainz.org/profile/" target="_blank">listenbrainz.org/profile</a></p>
                </div>`;
            return;
        }

        tabsContainer.innerHTML = tabsHtml;

        // Update section subtitle with username
        const lbSubtitle = document.getElementById('listenbrainz-section-subtitle');
        if (lbSubtitle) {
            lbSubtitle.textContent = lbUsername ? `Playlists for ${lbUsername}` : 'Playlists from ListenBrainz';
        }

        // Load first available tab
        const firstTab = tabs.find(t => t.hasData);
        if (firstTab) {
            console.log(`🎯 Loading first tab: ${firstTab.label} (${firstTab.id})`);
            activeListenBrainzTab = firstTab.id;
            loadListenBrainzTabContent(firstTab.id);
        } else {
            console.log('❌ No first tab found');
        }

    } catch (error) {
        console.error('Error initializing ListenBrainz tabs:', error);
        const tabsContainer = document.getElementById('listenbrainz-tabs');
        if (tabsContainer) {
            tabsContainer.innerHTML = '<div class="discover-empty"><p>Failed to load playlists</p></div>';
        }
    }
}

function switchListenBrainzTab(tabId) {
    // Update active tab
    activeListenBrainzTab = tabId;

    // Update tab buttons
    const tabs = document.querySelectorAll('#listenbrainz-tabs .decade-tab');
    tabs.forEach(tab => {
        if (tab.dataset.tab === tabId) {
            tab.classList.add('active');
        } else {
            tab.classList.remove('active');
        }
    });

    // Load content
    loadListenBrainzTabContent(tabId);
}

function groupListenBrainzPlaylists(playlists) {
    const groups = {};
    const groupOrder = [];

    playlists.forEach(playlist => {
        const playlistData = playlist.playlist || playlist;
        const title = (playlistData.title || '').toLowerCase();

        let groupName;
        if (title.includes('weekly jams')) {
            groupName = 'Weekly Jams';
        } else if (title.includes('weekly exploration')) {
            groupName = 'Weekly Exploration';
        } else if (title.includes('top discoveries')) {
            groupName = 'Top Discoveries';
        } else if (title.includes('top missed recordings')) {
            groupName = 'Top Missed Recordings';
        } else if (title.includes('daily jams')) {
            groupName = 'Daily Jams';
        } else {
            groupName = 'Other';
        }

        if (!groups[groupName]) {
            groups[groupName] = [];
            groupOrder.push(groupName);
        }
        groups[groupName].push(playlist);
    });

    // Move "Other" to the end if it exists
    const otherIdx = groupOrder.indexOf('Other');
    if (otherIdx !== -1 && otherIdx !== groupOrder.length - 1) {
        groupOrder.splice(otherIdx, 1);
        groupOrder.push('Other');
    }

    return { groups, groupOrder };
}

function buildListenBrainzPlaylistsHtml(playlists, tabId) {
    let html = '';
    playlists.forEach((playlist, index) => {
        const playlistData = playlist.playlist || playlist;
        const identifier = playlistData.identifier?.split('/').pop() || '';
        console.log(`📋 Playlist ${index}:`, {
            title: playlistData.title,
            fullIdentifier: playlistData.identifier,
            extractedIdentifier: identifier
        });
        const title = playlistData.title || 'Untitled Playlist';
        const creator = playlistData.creator || 'ListenBrainz';

        let trackCount = 50;
        if (playlistData.annotation?.track_count && playlistData.annotation.track_count > 0) {
            trackCount = playlistData.annotation.track_count;
        } else if (playlistData.track && Array.isArray(playlistData.track) && playlistData.track.length > 0) {
            trackCount = playlistData.track.length;
        }

        const playlistId = `discover-lb-playlist-${identifier}`;  // Use consistent MBID-based ID
        const virtualPlaylistId = `discover_lb_${tabId}_${identifier}`;

        html += `
            <div class="discover-section-subsection">
                <div class="discover-section-header">
                    <div>
                        <h3 class="discover-section-subtitle-large">${title}</h3>
                        <p class="discover-section-meta" id="${playlistId}-meta">by ${creator} • Loading tracks...</p>
                    </div>
                    <div class="discover-section-actions">
                        <button class="action-button secondary"
                                onclick="openDownloadModalForListenBrainzPlaylist('${identifier}', '${escapeForInlineJs(title)}')"
                                title="Download missing tracks">
                            <span class="button-icon">↓</span>
                            <span class="button-text">Download</span>
                        </button>
                        <span class="wing-it-wrap">
                        <button class="action-button wing-it-btn-sm"
                                onclick="_toggleWingItDropdownLB(this, '${identifier}', '${escapeForInlineJs(title)}')"
                                title="Download or sync using raw track names — no metadata discovery">
                            <span class="button-icon">⚡</span>
                            <span class="button-text">Wing It</span>
                        </button>
                        </span>
                        <button class="action-button primary"
                                id="${playlistId}-sync-btn"
                                onclick="startListenBrainzPlaylistSync('${identifier}')"
                                title="Sync to media server"
                                style="display: none;">
                            <span class="button-icon">⟳</span>
                            <span class="button-text">Sync</span>
                        </button>
                    </div>
                </div>
                <!-- Sync Status Display -->
                <div class="discover-sync-status" id="${playlistId}-sync-status" style="display: none;">
                    <div class="sync-status-content">
                        <div class="sync-status-label">
                            <span class="sync-icon">⟳</span>
                            <span>Syncing to media server...</span>
                        </div>
                        <div class="sync-status-stats">
                            <span class="sync-stat">♪ <span id="${playlistId}-sync-total">0</span></span>
                            <span class="sync-separator">/</span>
                            <span class="sync-stat">✓ <span id="${playlistId}-sync-matched">0</span></span>
                            <span class="sync-separator">/</span>
                            <span class="sync-stat">✗ <span id="${playlistId}-sync-failed">0</span></span>
                            <span class="sync-stat">(<span id="${playlistId}-sync-percentage">0</span>%)</span>
                        </div>
                    </div>
                </div>
                <div class="discover-playlist-container compact" id="${playlistId}-playlist">
                    <div class="discover-loading"><div class="loading-spinner"></div><p>Loading tracks...</p></div>
                </div>
            </div>
        `;
    });
    return html;
}

function loadTracksForPlaylists(playlists) {
    playlists.forEach((playlist) => {
        const playlistData = playlist.playlist || playlist;
        const identifier = playlistData.identifier?.split('/').pop() || '';
        const playlistId = `discover-lb-playlist-${identifier}`;
        loadListenBrainzPlaylistTracks(identifier, playlistId);
    });
}

function switchListenBrainzSubTab(groupId) {
    activeListenBrainzSubTab = groupId;

    // Update sub-tab buttons
    const subTabs = document.querySelectorAll('#lb-subtabs-bar .lb-subtab');
    subTabs.forEach(tab => {
        if (tab.dataset.group === groupId) {
            tab.classList.add('active');
        } else {
            tab.classList.remove('active');
        }
    });

    // Show/hide sub-tab content panels
    const panels = document.querySelectorAll('.lb-subtab-panel');
    panels.forEach(panel => {
        if (panel.dataset.group === groupId) {
            panel.style.display = 'block';
            // Load tracks for playlists in this panel if not already loaded
            const unloaded = panel.querySelectorAll('.discover-loading');
            if (unloaded.length > 0) {
                const groupPlaylists = panel._playlists;
                if (groupPlaylists) {
                    loadTracksForPlaylists(groupPlaylists);
                }
            }
        } else {
            panel.style.display = 'none';
        }
    });
}

async function loadListenBrainzTabContent(tabId) {
    const container = document.getElementById('listenbrainz-tab-content');
    if (!container) return;

    const playlists = listenbrainzPlaylistsCache[tabId] || [];
    if (playlists.length === 0) {
        container.innerHTML = '<div class="discover-empty"><p>No playlists in this category</p></div>';
        return;
    }

    // For recommendations tab with multiple playlists, group into sub-tabs
    if (tabId === 'recommendations' && playlists.length > 1) {
        const { groups, groupOrder } = groupListenBrainzPlaylists(playlists);

        // If only one group, no need for sub-tabs
        if (groupOrder.length <= 1) {
            const html = buildListenBrainzPlaylistsHtml(playlists, tabId);
            container.innerHTML = html;
            loadTracksForPlaylists(playlists);
            return;
        }

        // Build sub-tabs bar
        const firstGroup = activeListenBrainzSubTab && groupOrder.includes(activeListenBrainzSubTab)
            ? activeListenBrainzSubTab
            : groupOrder[0];
        activeListenBrainzSubTab = firstGroup;

        let subTabsHtml = '<div class="decade-tabs-inner" id="lb-subtabs-bar" style="margin-bottom: 16px;">';
        groupOrder.forEach(groupName => {
            const isActive = groupName === firstGroup;
            const count = groups[groupName].length;
            subTabsHtml += `
                <button class="decade-tab lb-subtab${isActive ? ' active' : ''}"
                        onclick="switchListenBrainzSubTab('${groupName}')"
                        data-group="${groupName}"
                        style="font-size: 13px; padding: 8px 16px;">
                    ${groupName} (${count})
                </button>
            `;
        });
        subTabsHtml += '</div>';

        // Build content panels for each group
        let panelsHtml = '';
        groupOrder.forEach(groupName => {
            const isActive = groupName === firstGroup;
            panelsHtml += `<div class="lb-subtab-panel" data-group="${groupName}" style="display: ${isActive ? 'block' : 'none'};">`;
            panelsHtml += buildListenBrainzPlaylistsHtml(groups[groupName], tabId);
            panelsHtml += '</div>';
        });

        container.innerHTML = subTabsHtml + panelsHtml;

        // Store playlist references on panels for lazy loading
        groupOrder.forEach(groupName => {
            const panel = container.querySelector(`.lb-subtab-panel[data-group="${groupName}"]`);
            if (panel) {
                panel._playlists = groups[groupName];
            }
        });

        // Load tracks only for the active sub-tab
        loadTracksForPlaylists(groups[firstGroup]);
        return;
    }

    // Default: flat list for user/collaborative tabs (or single-group recommendations)
    const html = buildListenBrainzPlaylistsHtml(playlists, tabId);
    container.innerHTML = html;
    loadTracksForPlaylists(playlists);
}

async function loadListenBrainzPlaylistTracks(identifier, playlistId) {
    try {
        const playlistContainer = document.getElementById(`${playlistId}-playlist`);
        if (!playlistContainer) return;

        // Check cache first
        if (listenbrainzTracksCache[identifier]) {
            displayListenBrainzTracks(listenbrainzTracksCache[identifier], playlistId);
            return;
        }

        console.log(`🔄 Fetching tracks for playlist: ${identifier}`);
        const response = await fetch(`/api/discover/listenbrainz/playlist/${identifier}`);
        console.log(`📡 Response status: ${response.status}`);

        if (!response.ok) {
            const errorText = await response.text();
            console.error(`❌ Failed to fetch playlist: ${response.status} - ${errorText}`);
            throw new Error('Failed to fetch playlist tracks');
        }

        const data = await response.json();
        console.log(`📋 Received data:`, data);
        console.log(`📊 Tracks count: ${data.tracks?.length || 0}`);

        if (!data.success || !data.tracks || data.tracks.length === 0) {
            playlistContainer.innerHTML = '<div class="discover-empty"><p>No tracks available</p></div>';
            return;
        }

        // Cache the tracks
        listenbrainzTracksCache[identifier] = data.tracks;

        // Display tracks
        displayListenBrainzTracks(data.tracks, playlistId);

    } catch (error) {
        console.error('Error loading ListenBrainz playlist tracks:', error);
        const playlistContainer = document.getElementById(`${playlistId}-playlist`);
        if (playlistContainer) {
            playlistContainer.innerHTML = '<div class="discover-empty"><p>Failed to load tracks</p></div>';
        }
    }
}

/**
 * Clean artist name by removing featured artists
 * e.g., "Blackstreet feat. Dr. Dre & Queen Pen" -> "Blackstreet"
 */
function cleanArtistName(artistName) {
    if (!artistName) return artistName;

    // Remove everything after common featuring patterns (case insensitive)
    const patterns = [
        /\s+feat\.?\s+.*/i,      // "feat." or "feat"
        /\s+featuring\s+.*/i,    // "featuring"
        /\s+ft\.?\s+.*/i,        // "ft." or "ft"
        /\s+with\s+.*/i,         // "with"
        /\s+x\s+.*/i             // " x " (common in collaborations)
    ];

    let cleaned = artistName;
    for (const pattern of patterns) {
        cleaned = cleaned.replace(pattern, '');
    }

    return cleaned.trim();
}

function displayListenBrainzTracks(tracks, playlistId) {
    const playlistContainer = document.getElementById(`${playlistId}-playlist`);
    if (!playlistContainer) return;

    console.log(`🎨 Displaying ${tracks.length} tracks for ${playlistId}`);
    if (tracks.length > 0) {
        console.log('Sample track data:', tracks[0]);
    }

    // Update track count in the metadata section
    const metaElement = document.getElementById(`${playlistId}-meta`);
    if (metaElement) {
        // Extract creator from existing text (before the bullet)
        const currentText = metaElement.textContent;
        const creatorMatch = currentText.match(/by (.+?) •/);
        const creator = creatorMatch ? creatorMatch[1] : 'ListenBrainz';
        metaElement.textContent = `by ${creator} • ${tracks.length} track${tracks.length !== 1 ? 's' : ''}`;
    }

    // Simple SVG placeholder for missing album art (music note icon)
    const placeholderImage = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9IiMyYTJhMmEiLz48cGF0aCBkPSJNMjQgMTJ2MTIuNUEzLjUgMy41IDAgMSAxIDIwLjUgMjFWMTZsLTUgMXY5YTMuNSAzLjUgMCAxIDEtMy41LTMuNVYxM2wxMi0zeiIgZmlsbD0iIzU1NSIvPjwvc3ZnPg==';

    let html = '<div class="discover-playlist-tracks-compact">';
    tracks.forEach((track, index) => {
        const coverUrl = track.album_cover_url || placeholderImage;
        const durationMin = Math.floor(track.duration_ms / 60000);
        const durationSec = Math.floor((track.duration_ms % 60000) / 1000);
        const duration = track.duration_ms > 0 ? `${durationMin}:${durationSec.toString().padStart(2, '0')}` : '';

        const albumName = track.album_name ? escapeHtml(track.album_name) : '';

        html += `
            <div class="discover-playlist-track-compact" data-track-index="${index}">
                <div class="track-compact-number">${index + 1}</div>
                <div class="track-compact-image">
                    <img src="${coverUrl}" alt="${albumName}" loading="lazy" onerror="this.src='${placeholderImage}'">
                </div>
                <div class="track-compact-info">
                    <div class="track-compact-name">${escapeHtml(track.track_name || 'Unknown Track')}</div>
                    <div class="track-compact-artist">${escapeHtml(cleanArtistName(track.artist_name) || 'Unknown Artist')}</div>
                </div>
                <div class="track-compact-album">${albumName}</div>
                <div class="track-compact-duration">${duration}</div>
            </div>
        `;
    });
    html += '</div>';

    playlistContainer.innerHTML = html;
}

function _toggleWingItDropdownLB(btn, identifier, title) {
    const existing = document.querySelector('.wing-it-dropdown.visible');
    if (existing) { existing.classList.remove('visible'); setTimeout(() => existing.remove(), 150); return; }

    const wrap = btn.closest('.wing-it-wrap');
    if (!wrap) return;

    const dropdown = document.createElement('div');
    dropdown.className = 'wing-it-dropdown';
    dropdown.innerHTML = `
        <button class="wing-it-dropdown-item" data-action="download">
            <span class="wing-it-dropdown-icon">⬇️</span>
            <span class="wing-it-dropdown-label">Download</span>
            <span class="wing-it-dropdown-hint">Raw names</span>
        </button>
        <button class="wing-it-dropdown-item" data-action="sync">
            <span class="wing-it-dropdown-icon">🔄</span>
            <span class="wing-it-dropdown-label">Sync to Server</span>
            <span class="wing-it-dropdown-hint">Best-effort</span>
        </button>
    `;

    dropdown.querySelectorAll('.wing-it-dropdown-item').forEach(item => {
        item.addEventListener('click', () => {
            dropdown.classList.remove('visible');
            setTimeout(() => dropdown.remove(), 150);
            const tracks = listenbrainzTracksCache[identifier];
            if (!tracks || tracks.length === 0) {
                showToast('No tracks cached. Try opening the playlist first.', 'error');
                return;
            }
            if (item.dataset.action === 'download') {
                wingItDownload(tracks, title, 'ListenBrainz', identifier, true);
            } else {
                _wingItSync(tracks, title, 'ListenBrainz', identifier);
            }
        });
    });

    const btnRect2 = btn.getBoundingClientRect();
    if (btnRect2.top < 200) dropdown.classList.add('flip-down');

    wrap.appendChild(dropdown);
    requestAnimationFrame(() => dropdown.classList.add('visible'));

    setTimeout(() => {
        const closeHandler = e => {
            if (!dropdown.contains(e.target) && e.target !== btn) {
                dropdown.classList.remove('visible');
                setTimeout(() => dropdown.remove(), 150);
                document.removeEventListener('click', closeHandler);
            }
        };
        document.addEventListener('click', closeHandler);
    }, 50);
}

async function _wingItFromLBCard(identifier, title) {
    // Legacy — kept for backward compat
    const tracks = listenbrainzTracksCache[identifier];
    if (!tracks || tracks.length === 0) {
        showToast('No tracks cached for this playlist. Try opening the discovery modal first.', 'error');
        return;
    }
    wingItDownload(tracks, title, 'ListenBrainz', identifier);
}

async function openDownloadModalForListenBrainzPlaylist(identifier, title) {
    try {
        const tracks = listenbrainzTracksCache[identifier];
        if (!tracks || tracks.length === 0) {
            showToast('No tracks to download', 'error');
            return;
        }

        console.log(`🎵 Opening ListenBrainz discovery modal: ${title}`);
        console.log(`🔍 Looking for existing state with identifier: ${identifier}`);
        console.log(`📋 All ListenBrainz states:`, Object.keys(listenbrainzPlaylistStates));

        // Check if state already exists from backend hydration (like Beatport does)
        const existingState = listenbrainzPlaylistStates[identifier];
        console.log(`🔍 Existing state found:`, existingState ? `Phase: ${existingState.phase}` : 'None');

        if (existingState && existingState.phase !== 'fresh') {
            // State exists - rehydrate the modal with existing data
            console.log(`🔄 Rehydrating existing ListenBrainz state (Phase: ${existingState.phase})`);

            // If downloading/download_complete, rehydrate download modal instead
            if ((existingState.phase === 'downloading' || existingState.phase === 'download_complete') &&
                existingState.convertedSpotifyPlaylistId && existingState.download_process_id) {

                console.log(`📥 Rehydrating download modal for ListenBrainz playlist: ${title}`);

                // Implement download modal rehydration (like Beatport does)
                const convertedPlaylistId = existingState.convertedSpotifyPlaylistId;

                try {
                    // Check if modal already exists (user just closed it)
                    if (activeDownloadProcesses[convertedPlaylistId]) {
                        console.log(`✅ Download modal already exists, just showing it`);
                        const process = activeDownloadProcesses[convertedPlaylistId];
                        if (process.modalElement) {
                            process.modalElement.style.display = 'flex';
                        }
                        return;
                    }

                    // Create the download modal using the ListenBrainz state
                    console.log(`🆕 Creating new download modal for rehydration`);
                    // Get tracks from the existing state
                    let spotifyTracks = [];

                    if (existingState && existingState.discovery_results) {
                        spotifyTracks = existingState.discovery_results
                            .filter(result => result.spotify_data)
                            .map(result => {
                                const track = result.spotify_data;
                                // Ensure artists is an array of strings
                                if (track.artists && Array.isArray(track.artists)) {
                                    track.artists = track.artists.map(artist =>
                                        typeof artist === 'string' ? artist : (artist.name || artist)
                                    );
                                } else if (track.artists && typeof track.artists === 'string') {
                                    track.artists = [track.artists];
                                } else {
                                    track.artists = ['Unknown Artist'];
                                }
                                return {
                                    id: track.id,
                                    name: track.name,
                                    artists: track.artists,
                                    album: track.album || 'Unknown Album',
                                    duration_ms: track.duration_ms || 0,
                                    external_urls: track.external_urls || {}
                                };
                            });
                    }

                    if (spotifyTracks.length > 0) {
                        await openDownloadMissingModalForYouTube(
                            convertedPlaylistId,
                            title,
                            spotifyTracks
                        );

                        // Set the modal to running state with the correct batch ID
                        const process = activeDownloadProcesses[convertedPlaylistId];
                        if (process) {
                            process.status = existingState.phase === 'download_complete' ? 'complete' : 'running';
                            process.batchId = existingState.download_process_id;

                            // Update UI to running state
                            const beginBtn = document.getElementById(`begin-analysis-btn-${convertedPlaylistId}`);
                            const cancelBtn = document.getElementById(`cancel-all-btn-${convertedPlaylistId}`);
                            if (beginBtn) beginBtn.style.display = 'none';
                            if (cancelBtn) cancelBtn.style.display = 'inline-block';

                            // Start polling for this process
                            startModalDownloadPolling(convertedPlaylistId);

                            // Add to discover download sidebar if this has discoverMetadata
                            if (process.discoverMetadata) {
                                const playlistName = title;
                                const imageUrl = process.discoverMetadata.imageUrl;
                                const type = process.discoverMetadata.type || 'album';
                                addDiscoverDownload(convertedPlaylistId, playlistName, type, imageUrl);
                                console.log(`📥 [REHYDRATION] Added ListenBrainz download to sidebar: ${playlistName}`);
                            }

                            // Show modal since user clicked the download button (different from background rehydration)
                            if (process.modalElement) {
                                process.modalElement.style.display = 'flex';
                            }
                            console.log(`✅ Rehydrated download modal for ListenBrainz playlist: ${title}`);
                        }
                    } else {
                        console.warn(`⚠️ No Spotify tracks found for ListenBrainz download modal: ${title}`);
                    }
                } catch (error) {
                    console.warn(`⚠️ Error setting up download process for ListenBrainz playlist "${title}":`, error.message);
                }

                return;
            }

            // Open discovery modal with existing state
            openYouTubeDiscoveryModal(identifier);

            // If still discovering, resume polling
            if (existingState.phase === 'discovering') {
                console.log(`🔄 Resuming discovery polling for: ${title}`);
                startListenBrainzDiscoveryPolling(identifier);
            }

            return;
        }

        // No existing state - create fresh state and start discovery
        console.log(`🆕 Creating fresh ListenBrainz state for: ${title}`);

        // Create YouTube-style state entry for this ListenBrainz playlist (like Beatport does)
        const listenbrainzState = {
            phase: 'fresh',
            playlist: {
                name: title,
                tracks: tracks.map(track => ({
                    track_name: track.track_name,
                    artist_name: track.artist_name,
                    album_name: track.album_name,
                    duration_ms: track.duration_ms || 0,
                    mbid: track.mbid,
                    release_mbid: track.release_mbid,
                    album_cover_url: track.album_cover_url
                })),
                description: `${tracks.length} tracks from ${title}`,
                source: 'listenbrainz'
            },
            is_listenbrainz_playlist: true,
            playlist_mbid: identifier,  // Link to ListenBrainz playlist
            // Initialize discovery state properties (both naming conventions for modal compatibility)
            discovery_results: [],
            discoveryResults: [],
            discovery_progress: 0,
            discoveryProgress: 0,
            spotify_matches: 0,
            spotifyMatches: 0,
            spotify_total: tracks.length,
            spotifyTotal: tracks.length
        };

        // Store in ListenBrainz playlist states
        listenbrainzPlaylistStates[identifier] = listenbrainzState;

        // Start discovery automatically (like Beatport and Tidal do)
        try {
            console.log(`🔍 Starting ListenBrainz discovery for: ${title}`);

            // Call the discovery start endpoint with playlist data
            const response = await fetch(`/api/listenbrainz/discovery/start/${identifier}`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    playlist: listenbrainzState.playlist
                })
            });

            const result = await response.json();
            if (result.success) {
                // Update state to discovering
                listenbrainzPlaylistStates[identifier].phase = 'discovering';

                // Start polling for progress
                startListenBrainzDiscoveryPolling(identifier);

                console.log(`✅ Started ListenBrainz discovery for: ${title}`);
            } else {
                console.error('❌ Error starting ListenBrainz discovery:', result.error);
                showToast(`Error starting discovery: ${result.error}`, 'error');
            }
        } catch (error) {
            console.error('❌ Error starting ListenBrainz discovery:', error);
            showToast(`Error starting discovery: ${error.message}`, 'error');
        }

        // Open the existing YouTube discovery modal infrastructure
        openYouTubeDiscoveryModal(identifier);

        console.log(`✅ ListenBrainz discovery modal opened for ${title} with ${tracks.length} tracks`);

    } catch (error) {
        console.error('Error opening discovery modal for ListenBrainz playlist:', error);
        showToast('Failed to open discovery modal', 'error');
    }
}

async function openListenBrainzPlaylist(playlistMbid, playlistName) {
    try {
        showLoadingOverlay(`Loading ${playlistName}...`);

        const response = await fetch(`/api/discover/listenbrainz/playlist/${playlistMbid}`);
        if (!response.ok) {
            throw new Error('Failed to fetch playlist');
        }

        const data = await response.json();
        if (!data.success || !data.playlist) {
            showToast('Failed to load playlist', 'error');
            hideLoadingOverlay();
            return;
        }

        const playlist = data.playlist;
        const tracks = playlist.tracks || [];

        if (tracks.length === 0) {
            showToast('This playlist is empty', 'info');
            hideLoadingOverlay();
            return;
        }

        // Convert to Spotify-like format for compatibility with download modal
        const spotifyTracks = tracks.map(track => ({
            id: track.recording_mbid || `listenbrainz_${track.title}_${track.creator}`.replace(/[^a-z0-9]/gi, '_'),  // Generate ID if missing
            name: track.title || 'Unknown',
            artists: [{ name: cleanArtistName(track.creator || 'Unknown') }], // Proper Spotify format
            album: {
                name: track.album || 'Unknown Album',
                images: track.album_cover_url ? [{ url: track.album_cover_url }] : []
            },
            duration_ms: track.duration_ms || 0,
            listenbrainz_metadata: track.additional_metadata
        }));

        const virtualPlaylistId = `listenbrainz_${playlistMbid}`;
        await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, spotifyTracks);
        hideLoadingOverlay();

    } catch (error) {
        console.error(`Error opening ListenBrainz playlist:`, error);
        showToast(`Failed to load playlist`, 'error');
        hideLoadingOverlay();
    }
}

async function refreshListenBrainzPlaylists() {
    const button = document.getElementById('listenbrainz-refresh-btn');
    if (!button) return;

    try {
        // Show loading state on button
        const originalContent = button.innerHTML;
        button.disabled = true;
        button.innerHTML = '<span class="button-icon">⏳</span><span class="button-text">Refreshing...</span>';

        console.log('🔄 Refreshing ListenBrainz playlists...');
        showToast('Refreshing ListenBrainz playlists...', 'info');

        const response = await fetch('/api/discover/listenbrainz/refresh', {
            method: 'POST'
        });

        if (!response.ok) {
            throw new Error(`Failed to refresh: ${response.statusText}`);
        }

        const data = await response.json();

        if (data.success) {
            const summary = data.summary || {};
            let message = 'ListenBrainz playlists refreshed!';

            // Build summary message
            const updates = [];
            for (const [type, stats] of Object.entries(summary)) {
                const total = (stats.new || 0) + (stats.updated || 0);
                if (total > 0) {
                    updates.push(`${total} ${type}`);
                }
            }

            if (updates.length > 0) {
                message += ` Updated: ${updates.join(', ')}`;
            } else {
                message = 'All playlists are up to date';
            }

            console.log('✅ Refresh complete:', data.summary);
            showToast(message, 'success');

            // Reload the tabs to show updated data
            await initializeListenBrainzTabs();

        } else {
            throw new Error(data.error || 'Unknown error');
        }

        // Restore button
        button.disabled = false;
        button.innerHTML = originalContent;

    } catch (error) {
        console.error('Error refreshing ListenBrainz playlists:', error);
        showToast(`Failed to refresh: ${error.message}`, 'error');

        // Restore button
        button.disabled = false;
        button.innerHTML = '<span class="button-icon">🔄</span><span class="button-text">Refresh</span>';
    }
}

// ===============================
// SEASONAL DISCOVERY
// ===============================

async function loadSeasonalContent() {
    try {
        const response = await fetch('/api/discover/seasonal/current');
        if (!response.ok) {
            console.error('Failed to fetch seasonal content');
            return;
        }

        const data = await response.json();

        // If no active season, hide seasonal sections
        if (!data.success || !data.season) {
            hideSeasonalSections();
            return;
        }

        currentSeasonKey = data.season;

        // Load seasonal albums
        await loadSeasonalAlbums(data);

        // Load seasonal playlist if available
        if (data.playlist_available) {
            await loadSeasonalPlaylist(data);
        }

    } catch (error) {
        console.error('Error loading seasonal content:', error);
        hideSeasonalSections();
    }
}

function _renderSeasonalAlbumCard(album, index) {
    const coverUrl = album.album_cover_url || '/static/placeholder-album.png';
    return `
        <div class="discover-card" onclick="openDownloadModalForSeasonalAlbum(${index})" style="cursor: pointer;">
            <div class="discover-card-image">
                <img src="${coverUrl}" alt="${album.album_name}" loading="lazy">
            </div>
            <div class="discover-card-info">
                <h4 class="discover-card-title">${album.album_name}</h4>
                <p class="discover-card-subtitle">${album.artist_name}</p>
                ${album.release_date ? `<p class="discover-card-meta">${album.release_date}</p>` : ''}
            </div>
        </div>
    `;
}

// Seasonal Albums uses no-fetch `data:` mode — the parent
// `loadSeasonalContent` already fetched the season payload and passes
// the album list directly. Controller is recreated per call so the
// per-season `data` snapshot is current.
async function loadSeasonalAlbums(seasonData) {
    const albums = (seasonData && seasonData.albums) || [];
    const ctrl = createDiscoverSectionController({
        id: 'seasonal-albums',
        sectionEl: '#seasonal-albums-section',
        contentEl: '#seasonal-albums-carousel',
        data: { success: true, albums },
        extractItems: (data) => data.albums,
        beforeLoad: () => {
            const section = document.getElementById('seasonal-albums-section');
            if (section) section.style.display = 'block';
            const titleEl = document.getElementById('seasonal-albums-title');
            const subtitleEl = document.getElementById('seasonal-albums-subtitle');
            if (titleEl && seasonData) titleEl.textContent = `${seasonData.icon} ${seasonData.name}`;
            if (subtitleEl && seasonData) subtitleEl.textContent = seasonData.description;
        },
        renderItems: (items) => {
            discoverSeasonalAlbums = items;
            return items.map((album, i) => _renderSeasonalAlbumCard(album, i)).join('');
        },
        emptyMessage: 'No seasonal albums found',
        errorMessage: 'Failed to load seasonal albums',
        verboseErrors: true,
            showErrorToast: true,
    });
    return ctrl.load();
}

let _seasonalPlaylistCtrl = null;
let _seasonalPlaylistCtrlKey = null;

async function loadSeasonalPlaylist(seasonData) {
    const playlistContainer = document.getElementById('seasonal-playlist');
    if (!playlistContainer) return;

    // Show seasonal playlist section
    const seasonalPlaylistSection = document.getElementById('seasonal-playlist-section');
    if (seasonalPlaylistSection) {
        seasonalPlaylistSection.style.display = 'block';
    }

    // Update header
    const playlistTitle = document.getElementById('seasonal-playlist-title');
    const playlistSubtitle = document.getElementById('seasonal-playlist-subtitle');

    if (playlistTitle) {
        playlistTitle.textContent = `${seasonData.icon} ${seasonData.name} Mix`;
    }
    if (playlistSubtitle) {
        playlistSubtitle.textContent = `Curated playlist for ${seasonData.name.toLowerCase()}`;
    }

    // Re-create the controller when the season key changes so the
    // fetchUrl always points at the active season's endpoint.
    if (!_seasonalPlaylistCtrl || _seasonalPlaylistCtrlKey !== currentSeasonKey) {
        _seasonalPlaylistCtrl = createDiscoverSectionController({
            id: 'seasonal-playlist',
            contentEl: '#seasonal-playlist',
            fetchUrl: `/api/discover/seasonal/${currentSeasonKey}/playlist`,
            extractItems: (data) => data.tracks || [],
            renderItems: (items) => {
                discoverSeasonalTracks = items;
                const rows = items.map((t, i) => _renderCompactTrackRow(t, i)).join('');
                return `<div class="discover-playlist-tracks-compact">${rows}</div>`;
            },
            loadingMessage: 'Loading playlist...',
            emptyMessage: 'No tracks available yet',
            errorMessage: 'Failed to load playlist',
            verboseErrors: true,
            showErrorToast: true,
        });
        _seasonalPlaylistCtrlKey = currentSeasonKey;
    }
    return _seasonalPlaylistCtrl.load();
}

function hideSeasonalSections() {
    const seasonalAlbumsSection = document.getElementById('seasonal-albums-section');
    const seasonalPlaylistSection = document.getElementById('seasonal-playlist-section');

    if (seasonalAlbumsSection) {
        seasonalAlbumsSection.style.display = 'none';
    }
    if (seasonalPlaylistSection) {
        seasonalPlaylistSection.style.display = 'none';
    }
}

function _buildDiscoverArtistContext(source, artistName, sourceData = {}, albumData = {}) {
    const normalizedSource = (source || '').toString().toLowerCase();
    const albumArtist = Array.isArray(albumData.artists) ? albumData.artists[0] : null;
    const activeSource = (source || sourceData.active_source || sourceData.source || '').toString().toLowerCase();
    const context = {
        ...sourceData,
        id: sourceData.active_source_id || sourceData.artist_id || albumArtist?.id || '',
        name: artistName || sourceData.artist_name || sourceData.name || albumArtist?.name || '',
        source: normalizedSource || activeSource || '',
        spotify_artist_id: sourceData.spotify_artist_id || sourceData.artist_spotify_id || ((normalizedSource || activeSource) === 'spotify' ? albumArtist?.id : '') || '',
        itunes_artist_id: sourceData.itunes_artist_id || sourceData.artist_itunes_id || ((normalizedSource || activeSource) === 'itunes' ? albumArtist?.id : '') || '',
        deezer_artist_id: sourceData.deezer_artist_id || sourceData.artist_deezer_id || sourceData.deezer_id || ((normalizedSource || activeSource) === 'deezer' ? albumArtist?.id : '') || '',
        discogs_artist_id: sourceData.discogs_artist_id || sourceData.artist_discogs_id || sourceData.discogs_id || '',
        amazon_artist_id: sourceData.amazon_artist_id || sourceData.artist_amazon_id || sourceData.amazon_id || '',
        soul_id: sourceData.soul_id || sourceData.hydrabase_artist_id || '',
    };

    const sourceIdBySource = {
        spotify: context.spotify_artist_id,
        itunes: context.itunes_artist_id,
        deezer: context.deezer_artist_id,
        discogs: context.discogs_artist_id,
        amazon: context.amazon_artist_id,
        hydrabase: context.soul_id,
    };
    context.id = sourceIdBySource[normalizedSource || activeSource] || context.id;
    return context;
}

async function openDownloadModalForSeasonalAlbum(albumIndex) {
    const album = discoverSeasonalAlbums[albumIndex];
    if (!album) {
        showToast('Album data not found', 'error');
        return;
    }

    console.log(`📥 Opening Download Missing Tracks modal for seasonal album: ${album.album_name}`);
    showLoadingOverlay(`Loading tracks for ${album.album_name}...`);

    try {
        // Determine source and album ID - use source-agnostic endpoint (matches Recent Releases)
        const source = album.source || (album.spotify_album_id && !album.spotify_album_id.match(/^\d+$/) ? 'spotify' : 'itunes');
        const albumId = album.spotify_album_id;

        if (!albumId) {
            throw new Error('No album ID available');
        }

        // Fetch album tracks from appropriate source (pass name/artist for Hydrabase support)
        const _dap1 = new URLSearchParams({ name: album.album_name || '', artist: album.artist_name || '' });
        const response = await fetch(`/api/discover/album/${source}/${albumId}?${_dap1}`);
        if (!response.ok) {
            throw new Error('Failed to fetch album tracks');
        }

        const albumData = await response.json();
        if (!albumData.tracks || albumData.tracks.length === 0) {
            throw new Error('No tracks found in album');
        }

        // Convert to expected format with full album context (matches Recent Releases)
        const spotifyTracks = albumData.tracks.map(track => {
            let artists = track.artists || albumData.artists || [{ name: album.artist_name }];
            if (Array.isArray(artists)) {
                artists = artists.map(a => a.name || a);
            }

            return {
                id: track.id,
                name: track.name,
                artists: artists,
                album: {
                    id: albumData.id,
                    name: albumData.name,
                    album_type: albumData.album_type || 'album',
                    total_tracks: albumData.total_tracks || 0,
                    release_date: albumData.release_date || '',
                    images: albumData.images || []
                },
                duration_ms: track.duration_ms || 0,
                track_number: track.track_number || 0
            };
        });

        // Create virtual playlist ID
        const virtualPlaylistId = `seasonal_album_${albumId}`;

        // Pass proper artist/album context for album download (1 worker + source reuse)
        const artistContext = _buildDiscoverArtistContext(source, album.artist_name, album, albumData);

        const albumContext = {
            id: albumData.id,
            name: albumData.name,
            source: source,
            album_type: albumData.album_type || 'album',
            total_tracks: albumData.total_tracks || 0,
            release_date: albumData.release_date || '',
            images: albumData.images || []
        };

        // Open download modal with album context (same as Recent Releases)
        await openDownloadMissingModalForYouTube(virtualPlaylistId, albumData.name, spotifyTracks, artistContext, albumContext);

        hideLoadingOverlay();

    } catch (error) {
        console.error(`Error loading seasonal album: ${error.message}`);
        hideLoadingOverlay();
        showToast(`Failed to load album tracks: ${error.message}`, 'error');
    }
}

async function openDownloadModalForSeasonalPlaylist() {
    if (!discoverSeasonalTracks || discoverSeasonalTracks.length === 0) {
        alert('No seasonal tracks available');
        return;
    }

    // Convert to track format expected by modal
    const tracks = discoverSeasonalTracks.map(track => ({
        id: track.spotify_track_id,
        name: track.track_name,
        artists: [{ name: track.artist_name }],
        album: { name: track.album_name }
    }));

    openDownloadMissingModal(tracks, `${currentSeasonKey} Seasonal Mix`);
}

async function syncSeasonalPlaylist() {
    if (!currentSeasonKey) {
        alert('No active season');
        return;
    }

    // Use the same sync logic as other discover playlists
    // Create a virtual playlist ID for tracking
    const virtualPlaylistId = `discover_seasonal_${currentSeasonKey}`;

    // Build playlist data from seasonal tracks
    const playlistData = {
        id: virtualPlaylistId,
        name: `${currentSeasonKey.charAt(0).toUpperCase() + currentSeasonKey.slice(1)} Mix`,
        tracks: discoverSeasonalTracks.map(track => ({
            id: track.spotify_track_id,
            name: track.track_name,
            artists: [{ name: track.artist_name }],
            album: { name: track.album_name },
            duration_ms: track.duration_ms
        }))
    };

    // Trigger sync (reuse existing sync infrastructure)
    await syncPlaylistToLibrary(playlistData);
}

// ===============================
// PERSONALIZED PLAYLISTS
// ===============================

async function loadPersonalizedPopularPicks() {
    try {
        const container = document.getElementById('personalized-popular-picks');
        if (!container) return;

        const response = await fetch('/api/discover/personalized/popular-picks');
        if (!response.ok) return;

        const data = await response.json();
        if (!data.success || !data.tracks || data.tracks.length === 0) {
            container.closest('.discover-section').style.display = 'none';
            return;
        }

        personalizedPopularPicks = data.tracks;
        renderCompactPlaylist(container, data.tracks);
        container.closest('.discover-section').style.display = 'block';

    } catch (error) {
        console.error('Error loading popular picks:', error);
    }
}

async function loadPersonalizedHiddenGems() {
    try {
        const container = document.getElementById('personalized-hidden-gems');
        if (!container) return;

        const response = await fetch('/api/discover/personalized/hidden-gems');
        if (!response.ok) return;

        const data = await response.json();
        if (!data.success || !data.tracks || data.tracks.length === 0) {
            container.closest('.discover-section').style.display = 'none';
            return;
        }

        personalizedHiddenGems = data.tracks;
        renderCompactPlaylist(container, data.tracks);
        container.closest('.discover-section').style.display = 'block';

    } catch (error) {
        console.error('Error loading hidden gems:', error);
    }
}

async function loadPersonalizedDailyMixes() {
    try {
        const container = document.getElementById('daily-mixes-grid');
        if (!container) return;

        const response = await fetch('/api/discover/personalized/daily-mixes');
        if (!response.ok) return;

        const data = await response.json();
        if (!data.success || !data.mixes || data.mixes.length === 0) {
            container.closest('.discover-section').style.display = 'none';
            return;
        }

        personalizedDailyMixes = data.mixes;

        // Render Daily Mix cards
        let html = '';
        data.mixes.forEach((mix, index) => {
            const coverUrl = mix.tracks && mix.tracks.length > 0 ?
                (mix.tracks[0].album_cover_url || '/static/placeholder-album.png') :
                '/static/placeholder-album.png';

            html += `
                <div class="discover-playlist-card" onclick="openDailyMix(${index})">
                    <div class="discover-playlist-cover">
                        <img src="${coverUrl}" alt="${mix.name}" loading="lazy">
                        <div class="playlist-play-overlay">▶</div>
                    </div>
                    <div class="discover-playlist-info">
                        <h4 class="discover-playlist-name">${mix.name}</h4>
                        <p class="discover-playlist-description">${mix.description}</p>
                        <p class="discover-playlist-count">${mix.track_count} tracks</p>
                    </div>
                </div>
            `;
        });

        container.innerHTML = html;
        container.closest('.discover-section').style.display = 'block';

    } catch (error) {
        console.error('Error loading daily mixes:', error);
    }
}

function renderCompactPlaylist(container, tracks) {
    let html = '<div class="discover-playlist-tracks-compact">';

    tracks.forEach((track, index) => {
        const coverUrl = track.album_cover_url || '/static/placeholder-album.png';
        const durationMin = Math.floor(track.duration_ms / 60000);
        const durationSec = Math.floor((track.duration_ms % 60000) / 1000);
        const duration = `${durationMin}:${durationSec.toString().padStart(2, '0')}`;
        const artistEsc = (track.artist_name || '').replace(/'/g, "\\'").replace(/"/g, '&quot;');

        html += `
            <div class="discover-playlist-track-compact" data-track-index="${index}">
                <div class="track-compact-number">${index + 1}</div>
                <div class="track-compact-image">
                    <img src="${coverUrl}" alt="${track.album_name}" loading="lazy">
                </div>
                <div class="track-compact-info">
                    <div class="track-compact-name">${track.track_name}</div>
                    <div class="track-compact-artist">${track.artist_name}</div>
                </div>
                <div class="track-compact-album">${track.album_name}</div>
                <div class="track-compact-duration">${duration}</div>
                <button class="track-compact-block" onclick="event.stopPropagation(); blockDiscoveryArtist('${artistEsc}')" title="Block ${artistEsc} from discovery">✕</button>
            </div>
        `;
    });

    html += '</div>';
    container.innerHTML = html;
}

async function blockDiscoveryArtist(artistName) {
    if (!confirm(`Block "${artistName}" from all discovery playlists?`)) return;
    try {
        const res = await fetch('/api/discover/artist-blacklist', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ artist_name: artistName })
        });
        const data = await res.json();
        if (data.success) {
            showToast(`Blocked ${artistName} from discovery`, 'success');
            // Refresh discovery sections to remove the artist. Daily Mixes
            // is intentionally paused (see loadDiscoverPage), so don't
            // refresh it — the section isn't on the page anyway.
            loadPersonalizedHiddenGems();
            loadDiscoveryShuffle();
        } else {
            showToast(data.error || 'Failed to block artist', 'error');
        }
    } catch (e) {
        showToast('Error blocking artist', 'error');
    }
}

async function openDiscoveryBlacklistModal() {
    if (document.getElementById('discovery-blacklist-modal-overlay')) return;

    const overlay = document.createElement('div');
    overlay.id = 'discovery-blacklist-modal-overlay';
    overlay.className = 'modal-overlay';
    overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };

    overlay.innerHTML = `
        <div class="discover-blacklist-modal">
            <div class="discover-blacklist-modal-header">
                <h2>Blocked Artists</h2>
                <p>These artists won't appear in any discovery playlist across all sources</p>
                <button class="watch-all-close" onclick="document.getElementById('discovery-blacklist-modal-overlay').remove()">&times;</button>
            </div>
            <div class="discover-blacklist-modal-search">
                <input type="text" id="dbl-search-input" placeholder="Search for an artist to block..." autocomplete="off">
                <div id="dbl-search-results" class="dbl-search-results" style="display:none"></div>
            </div>
            <div class="discover-blacklist-modal-list" id="dbl-list">
                <div class="discover-blacklist-empty">Loading...</div>
            </div>
            <div class="discover-blacklist-modal-footer">
                <button class="watch-all-btn watch-all-btn-cancel" onclick="document.getElementById('discovery-blacklist-modal-overlay').remove()">Close</button>
            </div>
        </div>
    `;
    document.body.appendChild(overlay);

    // Wire up search
    let searchTimer = null;
    const input = document.getElementById('dbl-search-input');
    input.addEventListener('input', () => {
        clearTimeout(searchTimer);
        const q = input.value.trim();
        if (q.length < 2) { document.getElementById('dbl-search-results').style.display = 'none'; return; }
        searchTimer = setTimeout(() => _dblSearch(q), 300);
    });

    _dblLoadList();
}

async function _dblSearch(query) {
    const resultsEl = document.getElementById('dbl-search-results');
    if (!resultsEl) return;
    try {
        // Use existing enhanced search to find artists
        const res = await fetch('/api/enhanced-search', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ query, limit: 8 })
        });
        const data = await res.json();
        const artists = data.spotify_artists || data.artists || [];
        if (artists.length === 0) {
            resultsEl.innerHTML = '<div class="dbl-search-empty">No artists found</div>';
            resultsEl.style.display = 'block';
            return;
        }
        resultsEl.innerHTML = artists.map(a => {
            const name = _escToast(a.name || '');
            const img = a.image_url ? `<img src="${a.image_url}" class="dbl-search-img">` : '<div class="dbl-search-img-placeholder">🎤</div>';
            return `<div class="dbl-search-item" onclick="_dblBlockFromSearch('${name.replace(/'/g, "\\'")}')">
                ${img}
                <span class="dbl-search-name">${name}</span>
                <span class="dbl-search-action">Block</span>
            </div>`;
        }).join('');
        resultsEl.style.display = 'block';
    } catch (e) {
        resultsEl.style.display = 'none';
    }
}

async function _dblBlockFromSearch(artistName) {
    try {
        const res = await fetch('/api/discover/artist-blacklist', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ artist_name: artistName })
        });
        const data = await res.json();
        if (data.success) {
            showToast(`Blocked ${artistName} from discovery`, 'success');
            document.getElementById('dbl-search-results').style.display = 'none';
            const input = document.getElementById('dbl-search-input');
            if (input) input.value = '';
            _dblLoadList();
        }
    } catch (e) {
        showToast('Error blocking artist', 'error');
    }
}

async function _dblLoadList() {
    const container = document.getElementById('dbl-list');
    if (!container) return;
    try {
        const res = await fetch('/api/discover/artist-blacklist');
        const data = await res.json();
        if (!data.success || !data.entries || data.entries.length === 0) {
            container.innerHTML = '<div class="discover-blacklist-empty">No blocked artists yet — search above to block one</div>';
            return;
        }
        container.innerHTML = data.entries.map(e => `
            <div class="discover-blacklist-item">
                <span class="discover-blacklist-name">${_escToast(e.artist_name)}</span>
                <span class="discover-blacklist-date">${e.created_at ? new Date(e.created_at).toLocaleDateString() : ''}</span>
                <button class="discover-blacklist-remove" onclick="unblockDiscoveryArtist(${e.id}, '${_escAttr(e.artist_name)}')" title="Unblock">✕</button>
            </div>
        `).join('');
    } catch (e) {
        container.innerHTML = '<div class="discover-blacklist-empty">Failed to load</div>';
    }
}

async function unblockDiscoveryArtist(id, name) {
    try {
        const res = await fetch(`/api/discover/artist-blacklist/${id}`, { method: 'DELETE' });
        const data = await res.json();
        if (data.success) {
            showToast(`Unblocked ${name}`, 'success');
            _dblLoadList();
        }
    } catch (e) {
        showToast('Error unblocking artist', 'error');
    }
}

// Backwards compat — called during page init but now a no-op (modal handles it)
// ── Your Artists (Liked Artists Pool) ──

let _yourArtistsCtrl = null;

function _pickArtistDetailSource(artist) {
    if (!artist) return { id: '', source: '' };
    const sourceFields = {
        spotify: 'spotify_artist_id',
        deezer: 'deezer_artist_id',
        itunes: 'itunes_artist_id',
        discogs: 'discogs_artist_id',
        amazon: 'amazon_artist_id',
        musicbrainz: 'musicbrainz_artist_id',
        hydrabase: 'soul_id',
    };
    const active = String(artist.active_source || artist.source || '').toLowerCase();
    const activeField = sourceFields[active];
    if (activeField && artist[activeField]) {
        return { id: String(artist[activeField]), source: active };
    }
    for (const [source, field] of Object.entries(sourceFields)) {
        if (artist[field]) return { id: String(artist[field]), source };
    }
    if (artist.active_source_id && activeField) {
        return { id: String(artist.active_source_id), source: active };
    }
    return { id: '', source: '' };
}

async function loadYourArtists() {
    if (!_yourArtistsCtrl) {
        _yourArtistsCtrl = createDiscoverSectionController({
            id: 'your-artists',
            sectionEl: '#your-artists-section',
            contentEl: '#your-artists-carousel',
            fetchUrl: '/api/discover/your-artists',
            extractItems: (data) => data.artists || [],
            // Only treat as "truly empty" when there's no data AND the
            // upstream isn't still discovering. When stale + empty, the
            // renderer shows a custom in-progress message and a poller
            // is started in onRendered.
            isEmpty: (items, data) => items.length === 0 && !data.stale,
            hideWhenEmpty: true,
            renderItems: (items, data) => {
                const subtitle = document.getElementById('your-artists-subtitle');

                // Stale + empty — show custom "still fetching" message
                if (items.length === 0 && data.stale) {
                    if (subtitle) subtitle.textContent = 'Discovering your artists across connected services...';
                    return `
                        <div class="ya-loading">
                            <div class="watch-all-loading-spinner"></div>
                            <span>Fetching and matching artists from your services...</span>
                        </div>
                    `;
                }

                // Update subtitle with source info
                const sources = new Set();
                items.forEach(a => (a.source_services || []).forEach(s => sources.add(s)));
                const sourceNames = { spotify: 'Spotify', lastfm: 'Last.fm', tidal: 'Tidal', deezer: 'Deezer' };
                const sourceList = [...sources].map(s => sourceNames[s] || s).join(' and ');
                if (subtitle) {
                    subtitle.textContent = `Artists you follow on ${sourceList || 'your music services'}`;
                    if (data.stale) subtitle.textContent += ' (updating...)';
                }

                // Store for modal access and render carousel cards
                window._yaArtists = {};
                window._yaActiveSource = data.active_source || 'spotify';
                items.forEach(a => { window._yaArtists[a.id] = a; });
                return items.map(a => _renderYourArtistCard(a)).join('');
            },
            onRendered: ({ data }) => {
                // Continue polling while upstream is still discovering.
                if (data.stale) _pollYourArtists();
            },
            loadingMessage: 'Loading your artists...',
            emptyMessage: 'No followed artists found',
            errorMessage: 'Failed to load your artists',
            verboseErrors: true,
            showErrorToast: true,
        });
    }
    return _yourArtistsCtrl.load();
}

function _pollYourArtists() {
    // Poll every 5s until artists appear, then stop
    if (window._yaPoller) clearInterval(window._yaPoller);
    let attempts = 0;
    window._yaPoller = setInterval(async () => {
        attempts++;
        if (attempts > 60) { clearInterval(window._yaPoller); window._yaPoller = null; return; }
        try {
            const resp = await fetch('/api/discover/your-artists');
            if (!resp.ok) return;
            const data = await resp.json();
            if (data.artists && data.artists.length > 0) {
                clearInterval(window._yaPoller);
                window._yaPoller = null;
                loadYourArtists(); // Re-render with real data
            }
        } catch (e) { }
    }, 5000);
}

function _renderYourArtistCard(artist) {
    const _esc = (s) => escapeHtml(s || '');
    const img = artist.image_url || '';

    // Build metadata source badges (same pattern as library page)
    const badges = [];
    if (artist.spotify_artist_id) badges.push({ logo: SPOTIFY_LOGO_URL, fb: 'SP', title: 'Spotify' });
    if (artist.itunes_artist_id) badges.push({ logo: ITUNES_LOGO_URL, fb: 'IT', title: 'Apple Music' });
    if (artist.deezer_artist_id) badges.push({ logo: DEEZER_LOGO_URL, fb: 'Dz', title: 'Deezer' });
    if (artist.discogs_artist_id) badges.push({ logo: DISCOGS_LOGO_URL, fb: 'DC', title: 'Discogs' });
    const badgeHTML = badges.map(b =>
        `<div class="ya-badge" title="${b.title}">${b.logo ? `<img src="${b.logo}" onerror="this.parentNode.textContent='${b.fb}'">` : `<span>${b.fb}</span>`}</div>`
    ).join('');

    // Origin dots (which services the artist came from)
    const sources = artist.source_services || [];
    const sourceColors = { spotify: '#1DB954', lastfm: '#D51007', tidal: '#00FFFF', deezer: '#A238FF' };
    const originDots = sources.map(s =>
        `<span class="ya-origin-dot" style="background:${sourceColors[s] || '#666'}" title="From ${s}"></span>`
    ).join('');

    const watchlistClass = artist.on_watchlist ? 'active' : '';
    const detailSource = _pickArtistDetailSource(artist);
    const hasId = detailSource.id && detailSource.id !== '';

    const detailHref = hasId ? buildArtistDetailPath(detailSource.id, detailSource.source) : '';

    // Open info modal (card body click) — pass pool ID so we can look up all data
    const infoAction = hasId
        ? `openYourArtistInfoModal(${artist.id})`
        : '';

    // Deezer fallback for images
    const deezerFb = artist.deezer_artist_id ? `onerror="if(!this.dataset.tried){this.dataset.tried='1';this.src='https://api.deezer.com/artist/${artist.deezer_artist_id}/image?size=big'}else{this.style.display='none';this.nextElementSibling.style.display='flex'}"` : `onerror="this.style.display='none';this.nextElementSibling.style.display='flex'"`;

    return `
        <div class="ya-card" ${infoAction ? `onclick="${infoAction}"` : ''}>
            <div class="ya-card-img">
                ${img ? `<img src="${img}" alt="" loading="lazy" ${deezerFb}>` : ''}
                <div class="ya-card-placeholder" ${img ? 'style="display:none"' : ''}>&#9835;</div>
            </div>
            <div class="ya-card-gradient"></div>
            <div class="ya-card-badges">${badgeHTML}</div>
            <button class="ya-watchlist-btn ${watchlistClass}" title="${artist.on_watchlist ? 'On watchlist' : 'Add to watchlist'}"
                    onclick="event.stopPropagation(); toggleYourArtistWatchlist(${artist.id}, '${escapeForInlineJs(artist.artist_name)}', '${escapeForInlineJs(artist.active_source_id || '')}', '${escapeForInlineJs(artist.active_source || '')}', this)">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="${artist.on_watchlist ? 'currentColor' : 'none'}" stroke="currentColor" stroke-width="2">
                    <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>
                </svg>
            </button>
            <div class="ya-card-info">
                <div class="ya-card-info-row">
                    <div class="ya-origin-dots">${originDots}</div>
                </div>
                ${hasId
            ? `<a class="ya-card-name" href="${detailHref}" onclick="event.stopPropagation(); document.getElementById('ya-info-modal-overlay')?.remove(); document.getElementById('your-artists-modal-overlay')?.remove();" style="display:block;text-decoration:none;color:inherit;">${_esc(artist.artist_name)}</a>`
            : `<div class="ya-card-name">${_esc(artist.artist_name)}</div>`}
            </div>
        </div>
    `;
}

async function openYourArtistInfoModal(poolId) {
    const pool = (window._yaArtists || {})[poolId];
    if (!pool) return;

    const artistId = pool.active_source_id;
    const artistName = pool.artist_name;
    const imageUrl = pool.image_url || '';

    const existing = document.getElementById('ya-info-modal-overlay');
    if (existing) existing.remove();

    const overlay = document.createElement('div');
    overlay.id = 'ya-info-modal-overlay';
    overlay.className = 'modal-overlay';
    overlay.style.zIndex = '10001';
    overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };

    // Build matched source badges from pool data
    const _mb = (logo, fb, title) => `<div class="ya-info-badge" title="${title}">${logo ? `<img src="${logo}" onerror="this.parentNode.textContent='${fb}'">` : `<span>${fb}</span>`}</div>`;
    const matchBadges = [];
    if (pool.spotify_artist_id) matchBadges.push(_mb(SPOTIFY_LOGO_URL, 'SP', 'Matched on Spotify'));
    if (pool.itunes_artist_id) matchBadges.push(_mb(ITUNES_LOGO_URL, 'IT', 'Matched on Apple Music'));
    if (pool.deezer_artist_id) matchBadges.push(_mb(DEEZER_LOGO_URL, 'Dz', 'Matched on Deezer'));
    if (pool.discogs_artist_id) matchBadges.push(_mb(DISCOGS_LOGO_URL, 'DC', 'Matched on Discogs'));

    // Origin info
    const sources = pool.source_services || [];
    const sourceNames = { spotify: 'Spotify', lastfm: 'Last.fm', tidal: 'Tidal', deezer: 'Deezer' };
    const originText = sources.map(s => sourceNames[s] || s).join(', ');

    overlay.innerHTML = `
        <div class="ya-info-modal">
            <button class="watch-all-close" onclick="document.getElementById('ya-info-modal-overlay').remove()">&times;</button>
            <div class="ya-info-hero">
                <div class="ya-info-hero-bg" ${imageUrl ? `style="background-image:url('${escapeHtml(imageUrl)}')"` : ''}></div>
                <div class="ya-info-hero-content">
                    <div class="ya-info-hero-img">
                        ${imageUrl ? `<img src="${escapeHtml(imageUrl)}" alt="">` : '<div class="ya-info-img-fallback">&#9835;</div>'}
                    </div>
                    <div class="ya-info-hero-text">
                        <h2 class="ya-info-name">${escapeHtml(artistName)}</h2>
                        <div class="ya-info-badges">${matchBadges.join('')}</div>
                        ${originText ? `<div class="ya-info-origin">Followed on ${escapeHtml(originText)}</div>` : ''}
                    </div>
                </div>
            </div>
            <div class="ya-info-body" id="ya-info-body">
                <div class="cache-health-loading"><div class="watch-all-loading-spinner"></div><div>Loading artist info...</div></div>
            </div>
            <div class="ya-info-footer" id="ya-info-footer"></div>
        </div>
    `;
    document.body.appendChild(overlay);

    // Fetch enrichment data (with timeout)
    try {
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), 8000);
        const lookupId = artistId || encodeURIComponent(artistName);
        const resp = await fetch(`/api/discover/your-artists/info/${lookupId}?name=${encodeURIComponent(artistName)}`, { signal: controller.signal });
        clearTimeout(timeout);
        const artist = resp.ok ? await resp.json() : {};
        const bodyEl = document.getElementById('ya-info-body');
        const footerEl = document.getElementById('ya-info-footer');

        const genres = artist.genres || [];
        const bio = artist.summary || '';
        const listeners = artist.lastfm_listeners || artist.followers || 0;
        const playcount = artist.lastfm_playcount || 0;
        const popularity = artist.popularity || 0;

        let bodyHTML = '';

        // Stats
        if (listeners || playcount || popularity) {
            bodyHTML += `<div class="ya-info-stats">
                ${listeners ? `<div class="ya-info-stat"><span class="ya-info-stat-value">${Number(listeners).toLocaleString()}</span><span class="ya-info-stat-label">listeners</span></div>` : ''}
                ${playcount ? `<div class="ya-info-stat"><span class="ya-info-stat-value">${Number(playcount).toLocaleString()}</span><span class="ya-info-stat-label">plays</span></div>` : ''}
                ${popularity ? `<div class="ya-info-stat"><span class="ya-info-stat-value">${popularity}</span><span class="ya-info-stat-label">popularity</span></div>` : ''}
            </div>`;
        }

        // Genres
        if (genres.length > 0) {
            bodyHTML += `<div class="ya-info-section">
                <div class="ya-info-genres">${genres.map(g => `<span class="ya-info-genre">${escapeHtml(g)}</span>`).join('')}</div>
            </div>`;
        }

        // Bio
        if (bio) {
            const cleanBio = bio.replace(/<a[^>]*>.*?<\/a>/gi, '').replace(/<[^>]+>/g, '').trim();
            if (cleanBio) {
                bodyHTML += `<div class="ya-info-section">
                    <div class="ya-info-section-title">About</div>
                    <div class="ya-info-bio">${escapeHtml(cleanBio.length > 600 ? cleanBio.substring(0, 600) + '...' : cleanBio)}</div>
                </div>`;
            }
        }

        // Related artists from map connections
        const related = pool._related || [];
        if (related.length > 0) {
            const relLabel = pool.on_watchlist ? 'Similar Artists' : 'Connected To';
            bodyHTML += `<div class="ya-info-section">
                <div class="ya-info-section-title">${relLabel}</div>
                <div class="ya-info-related">
                    ${related.slice(0, 12).map(r => {
                const rImg = r.image_url || '';
                const rType = r.type === 'watchlist';
                return `<div class="ya-info-related-item" onclick="document.getElementById('ya-info-modal-overlay')?.remove(); setTimeout(() => openYourArtistInfoModal_direct(${JSON.stringify({
                    id: r.id, name: r.name, image_url: rImg,
                    spotify_id: r.spotify_id || '', itunes_id: r.itunes_id || '',
                    deezer_id: r.deezer_id || '', discogs_id: r.discogs_id || '',
                    type: r.type
                }).replace(/"/g, '&quot;')}), 100)">
                            <div class="ya-info-related-img">
                                ${rImg ? `<img src="${escapeHtml(rImg)}" alt="">` : '<span>&#9835;</span>'}
                            </div>
                            <div class="ya-info-related-text">
                                <div class="ya-info-related-name">${escapeHtml(r.name)}</div>
                                ${rType ? '<div class="ya-info-related-badge">★ Watchlist</div>' : ''}
                            </div>
                        </div>`;
            }).join('')}
                    ${related.length > 12 ? `<div class="ya-info-related-more">+${related.length - 12} more</div>` : ''}
                </div>
            </div>`;
        }

        if (!bodyHTML) bodyHTML = '<div class="ya-info-empty">No additional info available</div>';
        if (bodyEl) bodyEl.innerHTML = bodyHTML;

        // Footer
        if (footerEl) {
            const watchBtn = pool.on_watchlist
                ? `<button class="ya-header-btn" onclick="toggleYourArtistWatchlist(${pool.id}, '${escapeForInlineJs(artistName)}', '${escapeForInlineJs(artistId)}', '${escapeForInlineJs(pool.active_source || '')}', this); this.textContent='Done'; this.disabled=true">Remove from Watchlist</button>`
                : `<button class="ya-header-btn" onclick="toggleYourArtistWatchlist(${pool.id}, '${escapeForInlineJs(artistName)}', '${escapeForInlineJs(artistId)}', '${escapeForInlineJs(pool.active_source || '')}', this); this.textContent='Added!'; this.disabled=true">Add to Watchlist</button>`;
            footerEl.innerHTML = `
                ${watchBtn}
                <button class="ya-header-btn" onclick="document.getElementById('ya-info-modal-overlay')?.remove(); document.getElementById('your-artists-modal-overlay')?.remove(); openArtistMapExplorerDirect('${escapeForInlineJs(artistName)}')">
                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
                    <span>Explore</span>
                </button>
                <a class="ya-header-btn ya-viewall-btn" href="${buildArtistDetailPath(artistId, pool.active_source || null)}" onclick="document.getElementById('ya-info-modal-overlay')?.remove(); document.getElementById('your-artists-modal-overlay')?.remove();" style="text-decoration:none;color:inherit;">
                    <span>View Discography</span>
                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>
                </a>
            `;
        }
    } catch (err) {
        console.error('[Artist Info] Error loading artist info:', err);
        const bodyEl = document.getElementById('ya-info-body');
        if (bodyEl) bodyEl.innerHTML = `<div class="ya-info-empty">Could not load artist info</div>`;
    }
}

async function toggleYourArtistWatchlist(poolId, artistName, sourceId, source, btnEl) {
    const isWatched = btnEl && btnEl.classList.contains('active');
    try {
        if (isWatched) {
            const resp = await fetch('/api/watchlist/remove', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ artist_id: sourceId })
            });
            if (resp.ok) {
                if (btnEl) {
                    btnEl.classList.remove('active');
                    const svg = btnEl.querySelector('svg');
                    if (svg) svg.setAttribute('fill', 'none');
                }
                showToast(`Removed ${artistName} from watchlist`, 'info');
                // Sync card eye icon
                _syncYaCardWatchlist(poolId, false);
            }
        } else {
            const resp = await fetch('/api/watchlist/add', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ artist_id: sourceId, artist_name: artistName, source: source })
            });
            if (resp.ok) {
                if (btnEl) {
                    btnEl.classList.add('active');
                    const svg = btnEl.querySelector('svg');
                    if (svg) svg.setAttribute('fill', 'currentColor');
                }
                showToast(`Added ${artistName} to watchlist`, 'success');
                _syncYaCardWatchlist(poolId, true);
            }
        }
    } catch (err) {
        showToast('Failed to update watchlist', 'error');
    }
}

function _syncYaCardWatchlist(poolId, watched) {
    // Sync the card's eye icon with watchlist state (covers modal → card sync)
    document.querySelectorAll('.ya-card .ya-watchlist-btn').forEach(btn => {
        const card = btn.closest('.ya-card');
        if (!card) return;
        // Match by onclick containing the poolId
        const onclick = btn.getAttribute('onclick') || '';
        if (onclick.includes(`(${poolId},`)) {
            if (watched) {
                btn.classList.add('active');
                const svg = btn.querySelector('svg');
                if (svg) svg.setAttribute('fill', 'currentColor');
            } else {
                btn.classList.remove('active');
                const svg = btn.querySelector('svg');
                if (svg) svg.setAttribute('fill', 'none');
            }
        }
    });
    // Update pool data
    if (window._yaArtists && window._yaArtists[poolId]) {
        window._yaArtists[poolId].on_watchlist = watched ? 1 : 0;
    }
}

async function refreshYourArtists() {
    const btn = document.getElementById('your-artists-refresh-btn');
    if (btn) { btn.disabled = true; btn.style.opacity = '0.5'; }
    const subtitle = document.getElementById('your-artists-subtitle');
    if (subtitle) subtitle.textContent = 'Refreshing from your services...';

    try {
        await fetch('/api/discover/your-artists/refresh?clear=true', { method: 'POST' });
        // Poll until done
        let attempts = 0;
        const poll = setInterval(async () => {
            attempts++;
            if (attempts > 60) { clearInterval(poll); return; } // 5 min max
            try {
                const resp = await fetch('/api/discover/your-artists');
                const data = await resp.json();
                if (!data.stale && data.artists && data.artists.length > 0) {
                    clearInterval(poll);
                    loadYourArtists();
                    if (btn) { btn.disabled = false; btn.style.opacity = ''; }
                    showToast(`Found ${data.total} artists from your services`, 'success');
                }
            } catch (e) { }
        }, 5000);
    } catch (err) {
        showToast('Failed to start refresh', 'error');
        if (btn) { btn.disabled = false; btn.style.opacity = ''; }
    }
}

async function openYourArtistsSourcesModal() {
    const existing = document.getElementById('ya-sources-modal-overlay');
    if (existing) existing.remove();

    // Fetch current config + connected services
    let enabled = ['spotify', 'tidal', 'lastfm', 'deezer'];
    let connected = [];
    try {
        const resp = await fetch('/api/discover/your-artists/sources');
        if (resp.ok) {
            const data = await resp.json();
            if (data.enabled) enabled = data.enabled;
            if (data.connected) connected = data.connected;
        }
    } catch (e) { }

    const sourceInfo = [
        { id: 'spotify', label: 'Spotify', icon: '🎵' },
        { id: 'tidal', label: 'Tidal', icon: '🌊' },
        { id: 'lastfm', label: 'Last.fm', icon: '📻' },
        { id: 'deezer', label: 'Deezer', icon: '🎶' },
    ];

    const state = {};
    sourceInfo.forEach(s => { state[s.id] = enabled.includes(s.id); });

    const overlay = document.createElement('div');
    overlay.id = 'ya-sources-modal-overlay';
    overlay.className = 'modal-overlay';
    overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };

    const rows = sourceInfo.map(s => {
        const isConnected = connected.includes(s.id);
        const isOn = state[s.id];
        return `
            <div class="ya-source-row${isConnected ? '' : ' disconnected'}" data-source="${s.id}" onclick="_yaSourceRowClick('${s.id}')">
                <div class="ya-source-row-left">
                    <span style="font-size:18px">${s.icon}</span>
                    <div>
                        <div class="ya-source-name">${s.label}</div>
                        <div class="ya-source-status">${isConnected ? 'Connected' : 'Not connected'}</div>
                    </div>
                </div>
                <button class="ya-source-toggle${isOn ? ' on' : ''}" id="ya-toggle-${s.id}" onclick="event.stopPropagation();_yaSourceToggle('${s.id}')"></button>
            </div>`;
    }).join('');

    overlay.innerHTML = `
        <div class="ya-sources-modal">
            <h2>Your Artists Sources</h2>
            <p class="ya-sources-desc">Choose which connected services contribute artists to this section.</p>
            <div class="ya-sources-list">${rows}</div>
            <div class="ya-sources-footer">
                <button class="ya-sources-cancel-btn" onclick="document.getElementById('ya-sources-modal-overlay').remove()">Cancel</button>
                <button class="ya-sources-save-btn" onclick="_yaSourcesSave()">Save</button>
            </div>
        </div>
    `;
    document.body.appendChild(overlay);
    window._yaSourcesState = state;
}

function _yaSourceRowClick(id) {
    // Don't allow toggling disconnected services
    const row = document.querySelector(`.ya-source-row[data-source="${id}"]`);
    if (row && row.classList.contains('disconnected')) return;
    _yaSourceToggle(id);
}

function _yaSourceToggle(id) {
    // Don't allow toggling disconnected services
    const row = document.querySelector(`.ya-source-row[data-source="${id}"]`);
    if (row && row.classList.contains('disconnected')) return;
    window._yaSourcesState[id] = !window._yaSourcesState[id];
    const btn = document.getElementById(`ya-toggle-${id}`);
    if (btn) btn.classList.toggle('on', window._yaSourcesState[id]);
}

async function _yaSourcesSave() {
    const enabledArr = Object.entries(window._yaSourcesState)
        .filter(([, v]) => v).map(([k]) => k);
    if (enabledArr.length === 0) {
        showToast('Select at least one source', 'error');
        return;
    }
    const enabled = enabledArr.join(',');
    try {
        const resp = await fetch('/api/settings', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ discover: { your_artists_sources: enabled } })
        });
        if (resp.ok) {
            document.getElementById('ya-sources-modal-overlay')?.remove();
            showToast('Sources saved — refresh to apply', 'success');
            // Update subtitle immediately
            const sourceNames = { spotify: 'Spotify', tidal: 'Tidal', lastfm: 'Last.fm', deezer: 'Deezer' };
            const subtitle = document.getElementById('your-artists-subtitle');
            if (subtitle) {
                const names = enabledArr.map(s => sourceNames[s] || s).join(' and ');
                subtitle.textContent = `Artists you follow on ${names}`;
            }
        } else {
            showToast('Failed to save sources', 'error');
        }
    } catch (e) {
        showToast('Failed to save sources', 'error');
    }
}

async function openYourArtistsModal() {
    const existing = document.getElementById('your-artists-modal-overlay');
    if (existing) existing.remove();

    const overlay = document.createElement('div');
    overlay.id = 'your-artists-modal-overlay';
    overlay.className = 'modal-overlay';
    overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };

    overlay.innerHTML = `
        <div class="ya-modal">
            <div class="ya-modal-header">
                <div>
                    <h2 class="ya-modal-title">Your Artists</h2>
                    <p class="ya-modal-subtitle" id="ya-modal-subtitle">Loading...</p>
                </div>
                <button class="watch-all-close" onclick="document.getElementById('your-artists-modal-overlay').remove()">&times;</button>
            </div>
            <div class="ya-modal-toolbar">
                <input type="text" id="ya-modal-search" class="ya-modal-search" placeholder="Search artists...">
                <div class="ya-modal-filters">
                    <button class="ya-filter-btn active" data-source="" onclick="_yaFilterSource('')">All</button>
                    <button class="ya-filter-btn" data-source="spotify" onclick="_yaFilterSource('spotify')">Spotify</button>
                    <button class="ya-filter-btn" data-source="tidal" onclick="_yaFilterSource('tidal')">Tidal</button>
                    <button class="ya-filter-btn" data-source="lastfm" onclick="_yaFilterSource('lastfm')">Last.fm</button>
                    <button class="ya-filter-btn" data-source="deezer" onclick="_yaFilterSource('deezer')">Deezer</button>
                </div>
                <select class="ya-modal-sort" id="ya-modal-sort" onchange="_yaLoadModal()">
                    <option value="name">A-Z</option>
                    <option value="recent">Recently Added</option>
                    <option value="source">By Source</option>
                </select>
            </div>
            <div class="ya-modal-body" id="ya-modal-body">
                <div class="cache-health-loading"><div class="watch-all-loading-spinner"></div><div>Loading...</div></div>
            </div>
            <div class="ya-modal-footer" id="ya-modal-footer"></div>
        </div>
    `;
    document.body.appendChild(overlay);

    // Search debounce
    let searchTimer = null;
    overlay.querySelector('#ya-modal-search').addEventListener('input', () => {
        clearTimeout(searchTimer);
        searchTimer = setTimeout(() => _yaLoadModal(), 300);
    });

    window._yaModalState = { page: 1, source: '', sort: 'name' };
    _yaLoadModal();
}

function _yaFilterSource(source) {
    window._yaModalState.source = source;
    window._yaModalState.page = 1;
    document.querySelectorAll('.ya-filter-btn').forEach(b => b.classList.toggle('active', b.dataset.source === source));
    _yaLoadModal();
}

async function _yaLoadModal() {
    const body = document.getElementById('ya-modal-body');
    const footer = document.getElementById('ya-modal-footer');
    const subtitle = document.getElementById('ya-modal-subtitle');
    if (!body) return;

    const state = window._yaModalState || { page: 1, source: '', sort: 'name' };
    const search = document.getElementById('ya-modal-search')?.value || '';
    const sort = document.getElementById('ya-modal-sort')?.value || 'name';
    state.sort = sort;

    const params = new URLSearchParams({ page: state.page, per_page: 60, sort: state.sort });
    if (state.source) params.set('source', state.source);
    if (search) params.set('search', search);

    try {
        const resp = await fetch(`/api/discover/your-artists/all?${params}`);
        const data = await resp.json();

        if (subtitle) subtitle.textContent = `${data.total} artists matched`;

        if (!data.artists || data.artists.length === 0) {
            body.innerHTML = '<div class="failed-mb-empty">No artists found</div>';
            if (footer) footer.innerHTML = '';
            return;
        }

        // Store for info modal access
        if (!window._yaArtists) window._yaArtists = {};
        data.artists.forEach(a => { window._yaArtists[a.id] = a; });
        body.innerHTML = `<div class="ya-modal-grid">${data.artists.map(a => _renderYourArtistCard(a)).join('')}</div>`;

        // Pagination
        const totalPages = Math.ceil(data.total / 60);
        if (footer && totalPages > 1) {
            footer.innerHTML = `
                <div class="failed-mb-pagination">
                    <button class="failed-mb-btn-sm" ${state.page <= 1 ? 'disabled' : ''} onclick="window._yaModalState.page--; _yaLoadModal()">Prev</button>
                    <span>Page ${state.page} of ${totalPages}</span>
                    <button class="failed-mb-btn-sm" ${state.page >= totalPages ? 'disabled' : ''} onclick="window._yaModalState.page++; _yaLoadModal()">Next</button>
                </div>
            `;
        } else if (footer) {
            footer.innerHTML = '';
        }
    } catch (err) {
        body.innerHTML = '<div class="failed-mb-empty">Failed to load</div>';
    }
}

function loadDiscoveryBlacklist() { }

// ── Artist Map — Circle-packed staged canvas visualization ──
const _artMap = {
    placed: [],
    edges: [],
    images: {},
    canvas: null, ctx: null,
    offscreen: null, offCtx: null, // offscreen buffer for fast pan/zoom
    width: 0, height: 0,
    offsetX: 0, offsetY: 0, zoom: 0.15,
    hoveredNode: null, animFrame: null,
    dirty: true, // true = need to rebuild offscreen buffer
    WATCHLIST_R: 320,
    BUFFER: 8,
};

async function openArtistMap() {
    const container = document.getElementById('artist-map-container');
    if (!container) return;

    // Hide discover sections, show map
    document.querySelectorAll('#discover-page > .discover-container > *:not(#artist-map-container)').forEach(el => {
        el._prevDisplay = el.style.display;
        el.style.display = 'none';
    });
    container.style.display = 'flex';

    const canvas = document.getElementById('artist-map-canvas');
    _artMap.canvas = canvas;
    _artMap.ctx = canvas.getContext('2d');
    _artMap.width = container.clientWidth;
    _artMap.height = container.clientHeight - 50;
    canvas.width = _artMap.width * window.devicePixelRatio;
    canvas.height = _artMap.height * window.devicePixelRatio;
    canvas.style.width = _artMap.width + 'px';
    canvas.style.height = _artMap.height + 'px';
    _artMap.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
    _artMap.offsetX = _artMap.width / 2;
    _artMap.offsetY = _artMap.height / 2;
    _artMap.placed = [];
    _artMap.images = {};
    _artMap._nodeById = null;

    // Loading screen
    _artMap.ctx.fillStyle = '#0a0a14';
    _artMap.ctx.fillRect(0, 0, _artMap.width, _artMap.height);
    _artMap.ctx.fillStyle = 'rgba(255,255,255,0.3)';
    _artMap.ctx.font = '14px system-ui';
    _artMap.ctx.textAlign = 'center';
    _artMap.ctx.fillText('Building artist map...', _artMap.width / 2, _artMap.height / 2);

    try {
        const resp = await fetch('/api/discover/artist-map');
        const data = await resp.json();
        if (!data.success || !data.nodes.length) {
            _artMap.ctx.fillText('No watchlist artists. Add artists to your watchlist first.', _artMap.width / 2, _artMap.height / 2 + 30);
            return;
        }

        document.getElementById('artist-map-stats').textContent =
            `${data.watchlist_count} watchlist · ${data.similar_count} similar`;

        _artMap.edges = data.edges;
        const WR = _artMap.WATCHLIST_R;
        const BUF = _artMap.BUFFER;

        // ── PHASE 1: Place watchlist artists with guaranteed no-overlap ──
        const wNodes = data.nodes.filter(n => n.type === 'watchlist');
        // Minimum center-to-center distance between watchlist nodes
        const minCenterDist = WR * 3.5; // WR*2 for radii + WR*1.5 gap — similar artists fill the gaps via spiral packing

        // Place watchlist nodes in a spiral — deterministic, guaranteed spacing
        wNodes.forEach((n, i) => {
            n.radius = WR;
            n.opacity = 0;
            if (i === 0) {
                n.x = 0; n.y = 0;
            } else {
                // Golden angle spiral for even distribution
                const angle = i * 2.399963; // golden angle in radians
                const r = minCenterDist * Math.sqrt(i) * 0.7;
                n.x = Math.cos(angle) * r;
                n.y = Math.sin(angle) * r;
            }
        });

        // Post-process: push apart any watchlist nodes that ended up too close
        for (let pass = 0; pass < 50; pass++) {
            let moved = false;
            for (let i = 0; i < wNodes.length; i++) {
                for (let j = i + 1; j < wNodes.length; j++) {
                    const dx = wNodes[j].x - wNodes[i].x;
                    const dy = wNodes[j].y - wNodes[i].y;
                    const dist = Math.sqrt(dx * dx + dy * dy) || 1;
                    if (dist < minCenterDist) {
                        const push = (minCenterDist - dist) / 2 + 1;
                        const nx = (dx / dist) * push;
                        const ny = (dy / dist) * push;
                        wNodes[i].x -= nx; wNodes[i].y -= ny;
                        wNodes[j].x += nx; wNodes[j].y += ny;
                        moved = true;
                    }
                }
            }
            if (!moved) break;
        }

        wNodes.forEach(n => { _artMap.placed.push(n); });

        // ── PHASE 2: Place similar artists around their source watchlist nodes ──
        const sNodes = data.nodes.filter(n => n.type === 'similar');
        sNodes.forEach(n => {
            const occ = n.occurrence || 1;
            const rank = n.rank || 5;
            // Bigger overall: min 25% of WR, max 55%, scaled by relevance
            n.radius = Math.min(WR * 0.55, Math.max(WR * 0.25, WR * 0.2 + occ * WR * 0.06 + (10 - rank) * WR * 0.025));
        });
        sNodes.sort((a, b) => b.radius - a.radius);

        // Build edge lookup: target_id → source node (O(1) instead of .find())
        const edgeMap = {};
        _artMap.edges.forEach(e => { edgeMap[e.target] = e.source; });
        const nodeById = {};
        _artMap.placed.forEach(n => { nodeById[n.id] = n; });

        // Spatial grid for fast collision detection
        // Cell size must cover the largest possible bubble diameter + buffer
        const CELL = WR * 2 + BUF * 2;
        const grid = {};
        function _gridKey(x, y) { return `${Math.floor(x / CELL)},${Math.floor(y / CELL)}`; }
        function _gridAdd(n) {
            const k = _gridKey(n.x, n.y);
            if (!grid[k]) grid[k] = [];
            grid[k].push(n);
        }
        function _gridCheck(x, y, r) {
            const cx = Math.floor(x / CELL);
            const cy = Math.floor(y / CELL);
            // Search wider radius to catch large watchlist bubbles
            for (let dx = -3; dx <= 3; dx++) {
                for (let dy = -3; dy <= 3; dy++) {
                    const cell = grid[`${cx + dx},${cy + dy}`];
                    if (!cell) continue;
                    for (const p of cell) {
                        const ddx = x - p.x, ddy = y - p.y;
                        const minD = r + p.radius + BUF;
                        if (ddx * ddx + ddy * ddy < minD * minD) return true;
                    }
                }
            }
            return false;
        }
        // Add watchlist nodes to grid
        _artMap.placed.forEach(n => _gridAdd(n));

        // Place similar nodes with spatial grid collision
        for (const sn of sNodes) {
            const srcId = edgeMap[sn.id];
            const src = srcId != null ? nodeById[srcId] : null;
            const cx = src ? src.x : 0;
            const cy = src ? src.y : 0;
            const startDist = (src ? src.radius : WR) + sn.radius + BUF;

            let placed = false;
            for (let dist = startDist; dist < startDist + WR * 3; dist += sn.radius * 0.5) {
                const steps = Math.max(8, Math.floor(dist * 0.1));
                const off = Math.random() * Math.PI * 2;
                for (let a = 0; a < steps; a++) {
                    const angle = off + (a / steps) * Math.PI * 2;
                    const tx = cx + Math.cos(angle) * dist;
                    const ty = cy + Math.sin(angle) * dist;
                    if (!_gridCheck(tx, ty, sn.radius)) {
                        sn.x = tx; sn.y = ty; sn.opacity = 0;
                        _artMap.placed.push(sn);
                        nodeById[sn.id] = sn;
                        _gridAdd(sn);
                        placed = true;
                        break;
                    }
                }
                if (placed) break;
            }
        }

        // Auto-zoom to fit all nodes
        let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
        _artMap.placed.forEach(n => {
            minX = Math.min(minX, n.x - n.radius);
            maxX = Math.max(maxX, n.x + n.radius);
            minY = Math.min(minY, n.y - n.radius);
            maxY = Math.max(maxY, n.y + n.radius);
        });
        const mapW = maxX - minX + 200;
        const mapH = maxY - minY + 200;
        _artMap.zoom = Math.min(_artMap.width / mapW, _artMap.height / mapH, 1);
        _artMap.offsetX = _artMap.width / 2 - ((minX + maxX) / 2) * _artMap.zoom;
        _artMap.offsetY = _artMap.height / 2 - ((minY + maxY) / 2) * _artMap.zoom;

        // Setup interaction
        _artMapSetupInteraction(canvas);

        // ── PHASE 3: Set all visible, build buffer, render ──
        // Show loading overlay while buffer builds
        const loadingEl = document.createElement('div');
        loadingEl.id = 'artist-map-loading';
        loadingEl.innerHTML = `
            <div class="artist-map-loading-content">
                <div class="watch-all-loading-spinner"></div>
                <div class="artist-map-loading-text" id="artmap-loading-text">Placing ${_artMap.placed.length} artists on the map...</div>
            </div>
        `;
        container.appendChild(loadingEl);

        // Defer heavy work so loading overlay renders first
        setTimeout(async () => {
            _artMap.placed.forEach(n => { n.opacity = 1; });

            // Load images in parallel using createImageBitmap (non-blocking)
            const loadingText = container.querySelector('.artist-map-loading-text');
            const imgNodes = _artMap.placed.filter(n => n.image_url);
            let loaded = 0;

            if (loadingText) loadingText.textContent = `Loading ${imgNodes.length} artist images...`;

            // Batch image loading — 20 concurrent fetches
            const CONCURRENT = 20;
            let idx = 0;

            async function loadNextBatch() {
                const batch = [];
                while (idx < imgNodes.length && batch.length < CONCURRENT) {
                    const n = imgNodes[idx++];
                    if (_artMap.images[n.id]) { loaded++; continue; }
                    batch.push(
                        _artMapLoadImage(n.image_url)
                            .then(bmp => { if (bmp) _artMap.images[n.id] = bmp; })
                            .finally(() => {
                                loaded++;
                                if (loadingText && loaded % 50 === 0) {
                                    loadingText.textContent = `Loading images... ${loaded}/${imgNodes.length}`;
                                }
                            })
                    );
                }
                if (batch.length) await Promise.all(batch);
                if (idx < imgNodes.length) return loadNextBatch();
            }

            await loadNextBatch();

            // Build buffer and render
            if (loadingText) loadingText.textContent = 'Rendering map...';
            await new Promise(r => setTimeout(r, 20)); // let text update render

            _artMap.dirty = true;
            _artMapRender();

            const le = document.getElementById('artist-map-loading');
            if (le) le.remove();
        }, 50);

    } catch (err) {
        console.error('Artist map error:', err);
    }
}

function artMapZoom(factor) {
    const cx = _artMap.width / 2;
    const cy = _artMap.height / 2;
    const targetZoom = Math.max(0.02, Math.min(3, _artMap.zoom * factor));
    const targetOX = cx - (cx - _artMap.offsetX) * (targetZoom / _artMap.zoom);
    const targetOY = cy - (cy - _artMap.offsetY) * (targetZoom / _artMap.zoom);
    _artMapAnimateTo(targetZoom, targetOX, targetOY);
}

function artMapFitToView() {
    if (!_artMap.placed.length) return;
    let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
    _artMap.placed.forEach(n => {
        if ((n.opacity || 0) < 0.01) return;
        minX = Math.min(minX, n.x - n.radius);
        maxX = Math.max(maxX, n.x + n.radius);
        minY = Math.min(minY, n.y - n.radius);
        maxY = Math.max(maxY, n.y + n.radius);
    });
    const mapW = maxX - minX + 100;
    const mapH = maxY - minY + 100;
    const targetZoom = Math.min(_artMap.width / mapW, _artMap.height / mapH, 1);
    const targetOX = _artMap.width / 2 - ((minX + maxX) / 2) * targetZoom;
    const targetOY = _artMap.height / 2 - ((minY + maxY) / 2) * targetZoom;
    _artMapAnimateTo(targetZoom, targetOX, targetOY);
}

function _artMapAnimateTo(targetZoom, targetOX, targetOY) {
    if (_artMap._animating) cancelAnimationFrame(_artMap._animating);
    const startZoom = _artMap.zoom;
    const startOX = _artMap.offsetX;
    const startOY = _artMap.offsetY;
    const duration = 250;
    const start = performance.now();

    function step(now) {
        const t = Math.min(1, (now - start) / duration);
        // Ease out cubic
        const e = 1 - Math.pow(1 - t, 3);
        _artMap.zoom = startZoom + (targetZoom - startZoom) * e;
        _artMap.offsetX = startOX + (targetOX - startOX) * e;
        _artMap.offsetY = startOY + (targetOY - startOY) * e;
        _artMapRender(); // blit only, no rebuild
        if (t < 1) {
            _artMap._animating = requestAnimationFrame(step);
        } else {
            _artMap._animating = null;
            _artMap.dirty = true;
            _artMapRender(); // rebuild at final zoom level
        }
    }
    _artMap._animating = requestAnimationFrame(step);
}

function closeArtistMap() {
    const container = document.getElementById('artist-map-container');
    if (container) container.style.display = 'none';
    const sidebar = document.getElementById('artmap-genre-sidebar');
    if (sidebar) sidebar.style.display = 'none';
    if (_artMap.animFrame) cancelAnimationFrame(_artMap.animFrame);
    if (_artMap._keyHandler) window.removeEventListener('keydown', _artMap._keyHandler);
    _artMapHideContextMenu();

    // Restore discover sections
    document.querySelectorAll('#discover-page > .discover-container > *:not(#artist-map-container)').forEach(el => {
        el.style.display = el._prevDisplay !== undefined ? el._prevDisplay : '';
    });
}

// No force simulation — layout is pre-computed via circle packing

function _artMapRebuildBuffer() {
    /**Render ALL nodes once to offscreen canvas. Only called on data changes, not pan/zoom.**/
    const placed = _artMap.placed;
    if (!placed.length) return;

    const visible = placed.filter(n => (n.opacity || 0) > 0.01);
    if (!visible.length) return;

    // World bounds
    let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
    visible.forEach(n => {
        minX = Math.min(minX, n.x - n.radius - 10);
        maxX = Math.max(maxX, n.x + n.radius + 10);
        minY = Math.min(minY, n.y - n.radius - 10);
        maxY = Math.max(maxY, n.y + n.radius + 10);
    });

    const bw = maxX - minX;
    const bh = maxY - minY;
    // Scale based on zoom — higher zoom = higher res buffer, capped for memory
    const z = _artMap.zoom || 0.1;
    const scale = Math.min(z * 2, 1.0, 10240 / Math.max(bw, bh));

    if (!_artMap.offscreen) _artMap.offscreen = document.createElement('canvas');
    const oc = _artMap.offscreen;
    oc.width = Math.ceil(bw * scale);
    oc.height = Math.ceil(bh * scale);
    const octx = oc.getContext('2d');
    _artMap._bufferScale = scale;
    _artMap._bufferMinX = minX;
    _artMap._bufferMinY = minY;

    octx.scale(scale, scale);
    octx.translate(-minX, -minY);

    // Build node lookup
    if (!_artMap._nodeById) {
        _artMap._nodeById = {};
        placed.forEach(n => { _artMap._nodeById[n.id] = n; });
    }

    // Draw edges (connection lines between related nodes)
    if (_artMap.edges && _artMap.edges.length > 0) {
        octx.lineWidth = 1;
        octx.strokeStyle = 'rgba(138,43,226,0.08)';
        octx.beginPath();
        for (const edge of _artMap.edges) {
            const s = _artMap._nodeById[edge.source];
            const t = _artMap._nodeById[edge.target];
            if (!s || !t || (s.opacity || 0) < 0.05 || (t.opacity || 0) < 0.05) continue;
            octx.moveTo(s.x, s.y);
            octx.lineTo(t.x, t.y);
        }
        octx.stroke();
    }

    // Draw ALL nodes — genre labels first, similar next, watchlist on top
    const hideSimilar = _artMap._hideSimilar || false;
    // Pass 0: genre labels, Pass 1: similar/ring2, Pass 2: watchlist/center/ring1
    for (let pass = 0; pass < 3; pass++) {
        for (const n of visible) {
            if (pass === 0 && n._isLabel) { /* draw */ }
            else if (pass === 1 && !n._isLabel && n.type !== 'watchlist' && n.type !== 'center' && n.ring !== 1) { /* draw */ }
            else if (pass === 2 && !n._isLabel && (n.type === 'watchlist' || n.type === 'center' || n.ring === 1)) { /* draw */ }
            else continue;
            if (hideSimilar && n.type !== 'watchlist' && n.type !== 'center' && !n._isLabel) continue;
            const op = n.opacity || 0;
            if (op < 0.01) continue;
            const r = n.radius;
            const isW = n.type === 'watchlist' || n.type === 'center';
            octx.globalAlpha = op;

            // Genre label node — transparent circle with large text
            if (n._isLabel) {
                octx.globalAlpha = 0.6;
                octx.beginPath();
                octx.arc(n.x, n.y, n.radius, 0, Math.PI * 2);
                octx.fillStyle = 'rgba(138,43,226,0.04)';
                octx.fill();
                octx.strokeStyle = 'rgba(138,43,226,0.08)';
                octx.lineWidth = 1;
                octx.stroke();
                const labelSize = Math.max(12, n.radius * 0.25);
                octx.font = `800 ${labelSize}px system-ui`;
                octx.textAlign = 'center';
                octx.textBaseline = 'middle';
                octx.fillStyle = 'rgba(138,43,226,0.35)';
                octx.fillText(n.name, n.x, n.y - labelSize * 0.3);
                octx.font = `500 ${labelSize * 0.5}px system-ui`;
                octx.fillStyle = 'rgba(255,255,255,0.15)';
                octx.fillText(`${n._count || 0} artists`, n.x, n.y + labelSize * 0.5);
                octx.globalAlpha = 1;
                continue;
            }

            // Render quality based on node size in buffer pixels
            const rScaled = r * scale;
            const isSmall = rScaled < 8;
            const isTiny = rScaled < 3;

            // Tiny nodes: just a colored dot (no clip, no image, no text)
            if (isTiny) {
                octx.beginPath();
                octx.arc(n.x, n.y, r, 0, Math.PI * 2);
                octx.fillStyle = isW ? '#6b21a8' : '#2a2a40';
                octx.fill();
                continue;
            }

            // Small nodes: filled circle + border, no image clip
            if (isSmall) {
                octx.beginPath();
                octx.arc(n.x, n.y, r, 0, Math.PI * 2);
                const img = _artMap.images[n.id];
                if (img) {
                    octx.save(); octx.clip();
                    octx.drawImage(img, n.x - r, n.y - r, r * 2, r * 2);
                    octx.restore();
                } else {
                    octx.fillStyle = isW ? '#1a0a30' : '#141420';
                    octx.fill();
                }
                octx.strokeStyle = isW ? 'rgba(138,43,226,0.3)' : 'rgba(255,255,255,0.06)';
                octx.lineWidth = isW ? 1.5 : 0.5;
                octx.stroke();
                continue;
            }

            // Full quality: glow + clip + image + text
            if (isW) {
                octx.beginPath();
                octx.arc(n.x, n.y, r + 4, 0, Math.PI * 2);
                octx.strokeStyle = 'rgba(138,43,226,0.25)';
                octx.lineWidth = 5;
                octx.stroke();
            }

            octx.save();
            octx.beginPath();
            octx.arc(n.x, n.y, r, 0, Math.PI * 2);
            octx.closePath();
            octx.clip();

            const img = _artMap.images[n.id];
            if (img) {
                octx.drawImage(img, n.x - r, n.y - r, r * 2, r * 2);
                octx.fillStyle = 'rgba(0,0,0,0.45)';
                octx.fillRect(n.x - r, n.y - r, r * 2, r * 2);
            } else {
                octx.fillStyle = isW ? '#1a0a30' : '#141420';
                octx.fillRect(n.x - r, n.y - r, r * 2, r * 2);
            }
            octx.restore();

            octx.beginPath();
            octx.arc(n.x, n.y, r, 0, Math.PI * 2);
            octx.strokeStyle = isW ? 'rgba(138,43,226,0.4)' : 'rgba(255,255,255,0.08)';
            octx.lineWidth = isW ? 2 : 0.5;
            octx.stroke();

            const fontSize = isW ? Math.max(16, r * 0.14) : Math.max(8, r * 0.3);
            octx.font = `${isW ? '700' : '600'} ${fontSize}px system-ui`;
            octx.textAlign = 'center';
            octx.textBaseline = 'middle';
            octx.fillStyle = '#fff';
            const maxC = isW ? 20 : 12;
            const label = n.name.length > maxC ? n.name.substring(0, maxC - 1) + '…' : n.name;
            octx.fillText(label, n.x, n.y);
        }
    }

    octx.globalAlpha = 1;

    _artMap.dirty = false;
}

function _artMapRender() {
    /**Blit offscreen buffer to screen canvas with pan/zoom. Near-zero cost.**/
    const ctx = _artMap.ctx;
    const w = _artMap.width;
    const h = _artMap.height;

    ctx.fillStyle = '#0a0a14';
    ctx.fillRect(0, 0, w, h);

    if (_artMap.dirty || !_artMap.offscreen) _artMapRebuildBuffer();
    if (!_artMap.offscreen) return;

    const oc = _artMap.offscreen;
    const s = _artMap._bufferScale;
    const mx = _artMap._bufferMinX;
    const my = _artMap._bufferMinY;
    const z = _artMap.zoom;

    // Blit offscreen buffer: the buffer was drawn with scale(s) + translate(-minX,-minY)
    // So buffer pixel (bx,by) corresponds to world (bx/s + minX, by/s + minY)
    // Screen position of world (wx,wy) = offsetX + wx*zoom, offsetY + wy*zoom
    // Therefore buffer origin on screen = offsetX + minX*zoom, offsetY + minY*zoom
    // And buffer is drawn at size (bufferWidth * zoom/s, bufferHeight * zoom/s)
    ctx.drawImage(oc,
        _artMap.offsetX + mx * z,
        _artMap.offsetY + my * z,
        oc.width * z / s,
        oc.height * z / s
    );

    // ── Interactive overlay (drawn on main canvas, not buffer) ──
    const cFade = _artMap._constellationFade || 0;
    if (cFade > 0 && (_artMap.hoveredNode || _artMap._constellationCache)) {
        const n = _artMap.hoveredNode || (_artMap._constellationCache ? (_artMap._nodeById || {})[_artMap._constellationCache.nodeId] : null);
        if (!n) { _artMap._constellationFade = 0; _artMap._constellationCache = null; }
        if (n) {
            ctx.save();
            ctx.translate(_artMap.offsetX, _artMap.offsetY);
            ctx.scale(z, z);

            // Cache connected node lookup (don't recompute every frame)
            if (!_artMap._constellationCache || _artMap._constellationCache.nodeId !== n.id) {
                const connectedIds = new Set();
                if (n.type === 'watchlist') {
                    for (const e of _artMap.edges) {
                        if (e.source === n.id) connectedIds.add(e.target);
                    }
                } else {
                    const sourceIds = new Set();
                    for (const e of _artMap.edges) {
                        if (e.target === n.id) sourceIds.add(e.source);
                    }
                    for (const sid of sourceIds) {
                        connectedIds.add(sid);
                        for (const e of _artMap.edges) {
                            if (e.source === sid) connectedIds.add(e.target);
                        }
                    }
                }
                const nById = _artMap._nodeById || {};
                _artMap._constellationCache = {
                    nodeId: n.id,
                    nodes: [n, ...[...connectedIds].map(id => nById[id]).filter(Boolean)],
                };
            }

            const highlightNodes = _artMap._constellationCache.nodes;

            if (highlightNodes.length > 1) {
                // Semi-transparent dark overlay on entire visible area
                ctx.save();
                ctx.resetTransform();
                ctx.globalAlpha = 0.6 * cFade;
                ctx.fillStyle = '#0a0a14';
                ctx.fillRect(0, 0, _artMap.canvas.width, _artMap.canvas.height);
                ctx.globalAlpha = 1;
                ctx.restore();

                // Draw glowing connection lines
                for (const cn of highlightNodes) {
                    if (cn === n) continue;
                    ctx.beginPath();
                    ctx.moveTo(n.x, n.y);
                    ctx.lineTo(cn.x, cn.y);
                    // Gradient line
                    const lineGrad = ctx.createLinearGradient(n.x, n.y, cn.x, cn.y);
                    lineGrad.addColorStop(0, `rgba(138,43,226,${0.5 * cFade})`);
                    lineGrad.addColorStop(1, `rgba(138,43,226,${0.15 * cFade})`);
                    ctx.strokeStyle = lineGrad;
                    ctx.lineWidth = 2;
                    ctx.stroke();
                }

                // Redraw highlighted nodes on top
                ctx.globalAlpha = cFade;
                for (const hn of highlightNodes) {
                    const r = hn.radius;
                    const isW = hn.type === 'watchlist';
                    const isHov = hn === n;

                    // Glow
                    if (isHov) {
                        ctx.beginPath();
                        ctx.arc(hn.x, hn.y, r + 8, 0, Math.PI * 2);
                        ctx.strokeStyle = 'rgba(138,43,226,0.4)';
                        ctx.lineWidth = 6;
                        ctx.stroke();
                    }

                    // Circle + image
                    ctx.save();
                    ctx.beginPath();
                    ctx.arc(hn.x, hn.y, r, 0, Math.PI * 2);
                    ctx.closePath();
                    ctx.clip();

                    const img = _artMap.images[hn.id];
                    if (img) {
                        ctx.drawImage(img, hn.x - r, hn.y - r, r * 2, r * 2);
                        ctx.fillStyle = 'rgba(0,0,0,0.35)';
                        ctx.fillRect(hn.x - r, hn.y - r, r * 2, r * 2);
                    } else {
                        ctx.fillStyle = isW ? '#1a0a30' : '#141420';
                        ctx.fillRect(hn.x - r, hn.y - r, r * 2, r * 2);
                    }
                    ctx.restore();

                    // Border
                    ctx.beginPath();
                    ctx.arc(hn.x, hn.y, r, 0, Math.PI * 2);
                    ctx.strokeStyle = isHov ? 'rgba(255,255,255,0.7)' : isW ? 'rgba(138,43,226,0.5)' : 'rgba(255,255,255,0.3)';
                    ctx.lineWidth = isHov ? 3 : 1.5;
                    ctx.stroke();

                    // Name
                    const fontSize = isW ? Math.max(14, r * 0.14) : Math.max(8, r * 0.3);
                    ctx.font = `${isW ? '700' : '600'} ${fontSize}px system-ui`;
                    ctx.textAlign = 'center';
                    ctx.textBaseline = 'middle';
                    ctx.fillStyle = '#fff';
                    const maxC = isW ? 20 : 12;
                    const label = hn.name.length > maxC ? hn.name.substring(0, maxC - 1) + '…' : hn.name;
                    ctx.fillText(label, hn.x, hn.y);
                }
                ctx.globalAlpha = 1;
            } else {
                // Single node, no connections
                ctx.beginPath();
                ctx.arc(n.x, n.y, n.radius + 4, 0, Math.PI * 2);
                ctx.strokeStyle = 'rgba(255,255,255,0.5)';
                ctx.lineWidth = 3;
                ctx.stroke();
            }

            ctx.restore();
        } // end if(n)
    } else if (_artMap.hoveredNode && !_artMap._constellationActive) {
        // Pre-constellation: just show a simple highlight ring (instant, no delay)
        const n = _artMap.hoveredNode;
        ctx.save();
        ctx.translate(_artMap.offsetX, _artMap.offsetY);
        ctx.scale(z, z);
        ctx.beginPath();
        ctx.arc(n.x, n.y, n.radius + 3, 0, Math.PI * 2);
        ctx.strokeStyle = 'rgba(255,255,255,0.35)';
        ctx.lineWidth = 2;
        ctx.stroke();
        ctx.restore();
    }

    // Click ripple animation
    if (_artMap._ripple) {
        const rip = _artMap._ripple;
        const elapsed = performance.now() - rip.start;
        const progress = elapsed / 400; // 400ms duration
        if (progress < 1) {
            ctx.save();
            ctx.translate(_artMap.offsetX, _artMap.offsetY);
            ctx.scale(z, z);
            const ripR = rip.radius + rip.radius * progress * 0.5;
            ctx.beginPath();
            ctx.arc(rip.x, rip.y, ripR, 0, Math.PI * 2);
            ctx.strokeStyle = `rgba(138,43,226,${0.5 * (1 - progress)})`;
            ctx.lineWidth = 3 * (1 - progress);
            ctx.stroke();
            ctx.restore();
            requestAnimationFrame(() => _artMapRender());
        } else {
            _artMap._ripple = null;
        }
    }
}

function artMapSearch(query) {
    const results = document.getElementById('artist-map-search-results');
    if (!results) return;
    if (!query || query.length < 2) { results.style.display = 'none'; return; }

    const q = query.toLowerCase();
    const matches = _artMap.placed.filter(n => (n.opacity || 0) > 0.5 && n.name.toLowerCase().includes(q)).slice(0, 8);

    if (!matches.length) { results.style.display = 'none'; return; }

    results.style.display = 'block';
    results.innerHTML = matches.map(n =>
        `<div class="artist-map-search-item" onclick="artMapZoomToNode(${n.id})">
            <span class="artist-map-search-type ${n.type}">${n.type === 'watchlist' ? '★' : '○'}</span>
            ${escapeHtml(n.name)}
        </div>`
    ).join('');
}

function artMapZoomToNode(nodeId) {
    const n = _artMap.placed.find(p => p.id === nodeId);
    if (!n) return;
    // Zoom to show this node centered, at a comfortable zoom level
    const targetZoom = Math.max(0.3, Math.min(1, 200 / n.radius));
    const targetOX = _artMap.width / 2 - n.x * targetZoom;
    const targetOY = _artMap.height / 2 - n.y * targetZoom;
    _artMapAnimateTo(targetZoom, targetOX, targetOY);
    // Highlight briefly after animation
    setTimeout(() => { _artMap.hoveredNode = n; _artMapRender(); }, 300);
    setTimeout(() => { _artMap.hoveredNode = null; _artMapRender(); }, 2500);
    // Close search
    const results = document.getElementById('artist-map-search-results');
    if (results) results.style.display = 'none';
    const input = document.getElementById('artist-map-search');
    if (input) input.value = '';
}

function _artMapShowTooltip(e, node) {
    const tip = document.getElementById('artist-map-tooltip');
    if (!tip) return;
    if (!node) { tip.style.display = 'none'; return; }

    const img = node.image_url ? `<img class="artmap-tip-img" src="${escapeHtml(node.image_url)}" alt="">` : '<div class="artmap-tip-img artmap-tip-img-fallback">&#9835;</div>';
    const genres = (node.genres || []).slice(0, 3);
    const genreHTML = genres.length ? `<div class="artmap-tip-genres">${genres.map(g => `<span>${escapeHtml(g)}</span>`).join('')}</div>` : '';
    const typeLabel = node.type === 'watchlist' ? '<span class="artmap-tip-badge">★ Watchlist</span>' : '';

    tip.innerHTML = `
        <div class="artmap-tip-row">
            ${img}
            <div class="artmap-tip-info">
                <div class="artmap-tip-name">${escapeHtml(node.name)}</div>
                ${typeLabel}
                ${genreHTML}
            </div>
        </div>
    `;
    tip.style.display = 'block';

    // Position — keep on screen
    const x = Math.min(e.clientX + 16, window.innerWidth - tip.offsetWidth - 10);
    const y = Math.min(e.clientY - 10, window.innerHeight - tip.offsetHeight - 10);
    tip.style.left = x + 'px';
    tip.style.top = y + 'px';
}

function _artMapAnimateConstellation() {
    if (_artMap._constellationActive && _artMap._constellationFade < 1) {
        _artMap._constellationFade = Math.min(1, (_artMap._constellationFade || 0) + 0.08);
        _artMapRender();
        requestAnimationFrame(_artMapAnimateConstellation);
    } else if (!_artMap._constellationActive && _artMap._constellationFade > 0) {
        _artMap._constellationFade = Math.max(0, _artMap._constellationFade - 0.1);
        _artMapRender();
        if (_artMap._constellationFade > 0) {
            requestAnimationFrame(_artMapAnimateConstellation);
        } else {
            _artMap._constellationCache = null;
        }
    }
}

function artMapShowShortcuts() {
    const existing = document.getElementById('artmap-shortcuts-overlay');
    if (existing) { existing.remove(); return; }

    const overlay = document.createElement('div');
    overlay.id = 'artmap-shortcuts-overlay';
    overlay.className = 'modal-overlay';
    overlay.style.zIndex = '10002';
    overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };

    overlay.innerHTML = `
        <div class="artmap-shortcuts-modal">
            <div class="artmap-shortcuts-header">
                <h3>Keyboard Shortcuts</h3>
                <button class="watch-all-close" onclick="document.getElementById('artmap-shortcuts-overlay').remove()">&times;</button>
            </div>
            <div class="artmap-shortcuts-grid">
                <div class="artmap-shortcut"><kbd>Esc</kbd><span>Close map</span></div>
                <div class="artmap-shortcut"><kbd>+</kbd> / <kbd>-</kbd><span>Zoom in / out</span></div>
                <div class="artmap-shortcut"><kbd>F</kbd><span>Fit to view</span></div>
                <div class="artmap-shortcut"><kbd>S</kbd><span>Focus search</span></div>
                <div class="artmap-shortcut"><kbd>H</kbd><span>Toggle similar artists</span></div>
                <div class="artmap-shortcut"><kbd>Scroll</kbd><span>Zoom at cursor</span></div>
                <div class="artmap-shortcut"><kbd>Click</kbd><span>Artist info</span></div>
                <div class="artmap-shortcut"><kbd>Right-click</kbd><span>Context menu</span></div>
                <div class="artmap-shortcut"><kbd>Drag</kbd><span>Pan around</span></div>
                <div class="artmap-shortcut"><kbd>Hover 1s</kbd><span>Show connections</span></div>
            </div>
        </div>
    `;
    document.body.appendChild(overlay);
}

async function openArtistMapGenre() {
    // Show picker immediately — uses lightweight genre list endpoint
    const genre = await _showGenrePickerModal();
    if (!genre) return;
    _openGenreMapWithSelection(genre);
}

async function _showGenrePickerModal() {
    return new Promise(resolve => {
        const existing = document.getElementById('artmap-genre-picker');
        if (existing) existing.remove();

        const overlay = document.createElement('div');
        overlay.id = 'artmap-genre-picker';
        overlay.className = 'modal-overlay';
        overlay.onclick = (e) => { if (e.target === overlay) { overlay.remove(); resolve(null); } };

        overlay.innerHTML = `
            <div class="artmap-genre-picker-modal">
                <div class="artmap-genre-picker-header">
                    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
                        <circle cx="12" cy="12" r="10"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/><line x1="2" y1="12" x2="22" y2="12"/>
                    </svg>
                    <div>
                        <h3>Select a Genre</h3>
                        <p>Choose a genre to explore its artists</p>
                    </div>
                </div>
                <input type="text" class="artmap-genre-picker-search" placeholder="Filter genres..." oninput="_filterGenrePicker(this.value)">
                <div class="artmap-genre-picker-list" id="artmap-genre-picker-list">
                    <div class="cache-health-loading"><div class="watch-all-loading-spinner"></div><div>Loading genres...</div></div>
                </div>
            </div>
        `;
        document.body.appendChild(overlay);

        // Use cached data or fetch
        const renderGenreList = (data) => {
            if (!data?.success || !data?.genres?.length) {
                document.getElementById('artmap-genre-picker-list').innerHTML = '<div class="ya-info-empty">No genres found</div>';
                return;
            }
            const list = document.getElementById('artmap-genre-picker-list');
            list.innerHTML = data.genres.map(g => `
                <div class="artmap-genre-picker-item" data-genre="${escapeHtml(g.name)}" onclick="document.getElementById('artmap-genre-picker')._resolve('${escapeForInlineJs(g.name)}')">
                    <div class="artmap-genre-picker-name">${escapeHtml(g.name)}</div>
                    <div class="artmap-genre-picker-count">${g.count} artists</div>
                </div>
            `).join('');
        };

        if (window._artMapGenreList) {
            renderGenreList(window._artMapGenreList);
        } else {
            fetch('/api/discover/artist-map/genre-list')
                .then(r => r.json())
                .then(data => { window._artMapGenreList = data; renderGenreList(data); })
                .catch(() => { document.getElementById('artmap-genre-picker-list').innerHTML = '<div class="ya-info-empty">Error loading genres</div>'; });
        }

        overlay._resolve = (genre) => { overlay.remove(); resolve(genre); };
    });
}

function _switchGenre(genre) {
    _artMap._skipSectionToggle = true;
    _openGenreMapWithSelection(genre);
}

function _filterGenreSidebar(query) {
    const q = query.toLowerCase();
    document.querySelectorAll('.artmap-genre-sidebar-item').forEach(el => {
        el.style.display = el.dataset.genre.toLowerCase().includes(q) ? '' : 'none';
    });
}

async function _changeGenre() {
    const genre = await _showGenrePickerModal();
    if (!genre) return;
    _artMap._skipSectionToggle = true;
    _openGenreMapWithSelection(genre);
}

function _filterGenrePicker(query) {
    const q = query.toLowerCase();
    document.querySelectorAll('.artmap-genre-picker-item').forEach(el => {
        el.style.display = el.dataset.genre.toLowerCase().includes(q) ? '' : 'none';
    });
}

async function _openGenreMapWithSelection(selectedGenre) {
    const container = document.getElementById('artist-map-container');
    if (!container) return;

    const skipToggle = _artMap._skipSectionToggle;
    _artMap._skipSectionToggle = false;

    if (!skipToggle) {
        document.querySelectorAll('#discover-page > .discover-container > *:not(#artist-map-container)').forEach(el => {
            el._prevDisplay = el.style.display;
            el.style.display = 'none';
        });
    }
    container.style.display = 'flex';

    // Show + populate genre sidebar
    const sidebar = document.getElementById('artmap-genre-sidebar');
    const genreListData = window._artMapGenreList || window._artMapGenreData;
    if (sidebar && genreListData?.genres) {
        sidebar.style.display = 'flex';
        const list = document.getElementById('artmap-genre-sidebar-list');
        if (list) {
            list.innerHTML = genreListData.genres.map(g => `
                <div class="artmap-genre-sidebar-item ${g.name === selectedGenre ? 'active' : ''}"
                     data-genre="${escapeHtml(g.name)}"
                     onclick="_switchGenre('${escapeForInlineJs(g.name)}')">
                    <span class="artmap-genre-sidebar-name">${escapeHtml(g.name)}</span>
                    <span class="artmap-genre-sidebar-count">${g.count}</span>
                </div>
            `).join('');
        }
    }

    const canvas = document.getElementById('artist-map-canvas');
    const contentRow = canvas.parentElement;
    _artMap.canvas = canvas;
    _artMap.ctx = canvas.getContext('2d');
    _artMap.width = canvas.clientWidth || (container.clientWidth - (sidebar?.offsetWidth || 0));
    _artMap.height = contentRow?.clientHeight || (container.clientHeight - 50);
    canvas.width = _artMap.width * window.devicePixelRatio;
    canvas.height = _artMap.height * window.devicePixelRatio;
    canvas.style.width = _artMap.width + 'px';
    canvas.style.height = _artMap.height + 'px';
    _artMap.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
    _artMap.offsetX = _artMap.width / 2;
    _artMap.offsetY = _artMap.height / 2;
    _artMap.placed = [];
    _artMap.edges = [];
    _artMap.images = {};
    _artMap._nodeById = null;
    _artMap.dirty = true;

    // Show loading
    const loadingEl = document.createElement('div');
    loadingEl.id = 'artist-map-loading';
    loadingEl.innerHTML = `<div class="artist-map-loading-content"><div class="watch-all-loading-spinner"></div><div class="artist-map-loading-text" id="artmap-genre-loading-text">Loading genre map...</div></div>`;
    container.appendChild(loadingEl);

    // Update toolbar
    document.querySelector('.artmap-brand-text').textContent = 'Genre Map';
    document.getElementById('artist-map-stats').textContent = 'Loading...';

    try {
        // Use cached data from picker or fetch fresh
        const data = window._artMapGenreData || await fetch('/api/discover/artist-map/genres').then(r => r.json());
        const loadingText = document.getElementById('artmap-genre-loading-text');
        if (!data.success || !data.nodes.length) {
            if (loadingText) loadingText.textContent = 'No artists with genre data found.';
            return;
        }

        // Find the selected genre + closely related genres (high artist overlap)
        const allGenres = data.genres;
        const primary = allGenres.find(g => g.name === selectedGenre);
        if (!primary) {
            if (loadingText) loadingText.textContent = `Genre "${selectedGenre}" not found.`;
            return;
        }
        const primarySet = new Set(primary.artist_ids);

        // Find up to 4 related genres by artist overlap
        const related = allGenres
            .filter(g => g.name !== selectedGenre)
            .map(g => {
                const overlap = g.artist_ids.filter(id => primarySet.has(id)).length;
                return { ...g, overlap };
            })
            .filter(g => g.overlap > primarySet.size * 0.1) // At least 10% overlap
            .sort((a, b) => b.overlap - a.overlap)
            .slice(0, 4);

        const genres = [primary, ...related];
        const totalArtists = genres.reduce((sum, g) => sum + g.artist_ids.length, 0);

        document.getElementById('artist-map-stats').innerHTML =
            `<span class="artmap-genre-change" onclick="event.stopPropagation(); _changeGenre()" title="Change genre">${escapeHtml(selectedGenre)} ▾</span> · ${genres.length} genre${genres.length > 1 ? 's' : ''} · ${totalArtists} artists`;

        const WR = _artMap.WATCHLIST_R;
        const BUF = _artMap.BUFFER;

        const maxPerGenre = 500;
        const nodeR = WR * 0.2;

        // Calculate actual cluster radius for each genre based on ring count
        function getClusterRadius(artistCount) {
            const count = Math.min(artistCount, maxPerGenre);
            let ringDist = WR + nodeR * 2 + BUF;
            let placed = 0;
            while (placed < count) {
                const circ = 2 * Math.PI * ringDist;
                const inRing = Math.max(1, Math.floor(circ / (nodeR * 2 + BUF)));
                placed += Math.min(inRing, count - placed);
                ringDist += nodeR * 2 + BUF;
            }
            return ringDist;
        }

        // Pre-compute cluster radii
        genres.forEach(g => { g._clusterR = getClusterRadius(g.artist_ids.length); });

        // Golden spiral placement
        genres.forEach((g, i) => {
            if (i === 0) { g._cx = 0; g._cy = 0; }
            else {
                const angle = i * 2.399963;
                const r = g._clusterR * Math.sqrt(i) * 0.9;
                g._cx = Math.cos(angle) * r;
                g._cy = Math.sin(angle) * r;
            }
        });

        // Push apart using actual cluster radii — no overlap possible
        for (let pass = 0; pass < 80; pass++) {
            let moved = false;
            for (let i = 0; i < genres.length; i++) {
                for (let j = i + 1; j < genres.length; j++) {
                    const dx = genres[j]._cx - genres[i]._cx;
                    const dy = genres[j]._cy - genres[i]._cy;
                    const dist = Math.sqrt(dx * dx + dy * dy) || 1;
                    const minDist = genres[i]._clusterR + genres[j]._clusterR + BUF * 4;
                    if (dist < minDist) {
                        const push = (minDist - dist) / 2 + 1;
                        genres[i]._cx -= (dx / dist) * push; genres[i]._cy -= (dy / dist) * push;
                        genres[j]._cx += (dx / dist) * push; genres[j]._cy += (dy / dist) * push;
                        moved = true;
                    }
                }
            }
            if (!moved) break;
        }

        let placedCount = 0;

        // Place genre labels as big watchlist-style bubbles
        for (const g of genres) {
            _artMap.placed.push({
                id: `genre_${g.name}`, name: g.name.toUpperCase(),
                x: g._cx, y: g._cy, radius: WR, opacity: 1,
                type: 'genre_label', image_url: '', genres: [g.name],
                _isLabel: true, _count: g.count
            });
        }

        // Place artists in concentric rings — deterministic O(1) per node, handles 10K+ instantly
        let genreIdx = 0;

        async function placeGenreArtists() {
            for (; genreIdx < genres.length; genreIdx++) {
                const genre = genres[genreIdx];
                const artists = genre.artist_ids.slice(0, maxPerGenre);
                const sorted = artists.map(nid => data.nodes[nid]).filter(Boolean).sort((a, b) => (b.popularity || 0) - (a.popularity || 0));

                let ringDist = WR + nodeR * 2 + BUF;
                let ringNum = 0;
                let placed = 0;

                while (placed < sorted.length) {
                    const circumference = 2 * Math.PI * ringDist;
                    const nodesInRing = Math.max(1, Math.floor(circumference / (nodeR * 2 + BUF)));
                    const count = Math.min(nodesInRing, sorted.length - placed);
                    const angleStep = (2 * Math.PI) / nodesInRing;
                    const angleOffset = ringNum * 0.618;

                    for (let i = 0; i < count; i++) {
                        const n = sorted[placed + i];
                        if (!n) continue;
                        const isW = n.type === 'watchlist' || n.type === 'center';
                        const r = isW ? nodeR * 1.5 : nodeR;
                        const angle = angleOffset + i * angleStep;

                        _artMap.placed.push({
                            id: placedCount + 1000, _origId: n.id, name: n.name,
                            x: genre._cx + Math.cos(angle) * ringDist,
                            y: genre._cy + Math.sin(angle) * ringDist,
                            radius: r, opacity: 1,
                            type: isW ? 'watchlist' : 'similar',
                            image_url: n.image_url || '', genres: n.genres || [],
                            spotify_id: n.spotify_id || '', itunes_id: n.itunes_id || '',
                            deezer_id: n.deezer_id || '', discogs_id: n.discogs_id || '',
                        });
                        placedCount++;
                    }
                    placed += count;
                    ringDist += nodeR * 2 + BUF;
                    ringNum++;
                }

                if (loadingText) loadingText.textContent = `Placing artists... ${genreIdx + 1}/${genres.length} genres (${placedCount} placed)`;
                if (genreIdx % 5 === 0) await new Promise(r => setTimeout(r, 0));
            }
        }
        await placeGenreArtists();

        // Build edges: connect artists that appear in multiple genre clusters
        _artMap.edges = [];
        const artistNodes = {};
        _artMap.placed.forEach(n => {
            if (n._origId != null) {
                if (!artistNodes[n._origId]) artistNodes[n._origId] = [];
                artistNodes[n._origId].push(n.id);
            }
        });
        Object.values(artistNodes).forEach(ids => {
            if (ids.length > 1) {
                for (let i = 0; i < ids.length - 1; i++) {
                    _artMap.edges.push({ source: ids[i], target: ids[i + 1], weight: 5 });
                }
            }
        });

        _artMap._nodeById = {};
        _artMap.placed.forEach(n => { _artMap._nodeById[n.id] = n; });

        // Auto-zoom
        let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
        _artMap.placed.forEach(n => {
            minX = Math.min(minX, n.x - n.radius);
            maxX = Math.max(maxX, n.x + n.radius);
            minY = Math.min(minY, n.y - n.radius);
            maxY = Math.max(maxY, n.y + n.radius);
        });
        const mapW = maxX - minX + 200, mapH = maxY - minY + 200;
        _artMap.zoom = Math.min(_artMap.width / mapW, _artMap.height / mapH, 1);
        _artMap.offsetX = _artMap.width / 2 - ((minX + maxX) / 2) * _artMap.zoom;
        _artMap.offsetY = _artMap.height / 2 - ((minY + maxY) / 2) * _artMap.zoom;

        _artMapSetupInteraction(canvas);

        // Load images + render
        if (loadingText) loadingText.textContent = `Rendering ${placedCount} artists...`;

        setTimeout(async () => {
            const imgNodes = _artMap.placed.filter(n => n.image_url && !n._isLabel);
            let loaded = 0;
            const CONCURRENT = 20;
            let idx = 0;
            async function loadBatch() {
                const batch = [];
                while (idx < imgNodes.length && batch.length < CONCURRENT) {
                    const n = imgNodes[idx++];
                    batch.push(_artMapLoadImage(n.image_url)
                        .then(bmp => { if (bmp) _artMap.images[n.id] = bmp; })
                        .finally(() => { loaded++; }));
                }
                if (batch.length) await Promise.all(batch);
                if (idx < imgNodes.length) return loadBatch();
            }
            await loadBatch();
            _artMap.dirty = true;
            _artMapRender();
            const le = document.getElementById('artist-map-loading');
            if (le) le.remove();
        }, 50);

        _artMap.dirty = true;
        _artMapRender();

    } catch (err) {
        console.error('Genre map error:', err);
        const lt = container.querySelector('.artist-map-loading-text');
        if (lt) lt.textContent = 'Error loading genre map';
    }
}

function openArtistMapExplorerDirect(name) {
    if (!name) return;
    // Already in map — just reload with new data, don't re-hide sections
    _artMap._skipSectionToggle = true;
    _openArtistMapExplorerWithName(name);
}

async function openArtistMapExplorer() {
    const name = await _showArtistMapSearchPrompt();
    if (!name) return;
    _openArtistMapExplorerWithName(name);
}

async function _openArtistMapExplorerWithName(name) {

    const container = document.getElementById('artist-map-container');
    if (!container) return;

    const skipToggle = _artMap._skipSectionToggle;
    _artMap._skipSectionToggle = false;

    if (!skipToggle) {
        document.querySelectorAll('#discover-page > .discover-container > *:not(#artist-map-container)').forEach(el => {
            el._prevDisplay = el.style.display;
            el.style.display = 'none';
        });
    }
    container.style.display = 'flex';

    const canvas = document.getElementById('artist-map-canvas');
    _artMap.canvas = canvas;
    _artMap.ctx = canvas.getContext('2d');
    _artMap.width = container.clientWidth;
    _artMap.height = container.clientHeight - 50;
    canvas.width = _artMap.width * window.devicePixelRatio;
    canvas.height = _artMap.height * window.devicePixelRatio;
    canvas.style.width = _artMap.width + 'px';
    canvas.style.height = _artMap.height + 'px';
    _artMap.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
    _artMap.offsetX = _artMap.width / 2;
    _artMap.offsetY = _artMap.height / 2;
    _artMap.placed = [];
    _artMap.edges = [];
    _artMap.images = {};
    _artMap._nodeById = null;
    _artMap.dirty = true;

    const loadingEl = document.createElement('div');
    loadingEl.id = 'artist-map-loading';
    loadingEl.innerHTML = `<div class="artist-map-loading-content"><div class="watch-all-loading-spinner"></div><div class="artist-map-loading-text">Exploring ${escapeHtml(name)}...</div></div>`;
    container.appendChild(loadingEl);

    document.querySelector('.artmap-brand-text').textContent = 'Artist Explorer';

    try {
        const resp = await fetch(`/api/discover/artist-map/explore?name=${encodeURIComponent(name.trim())}`);
        const data = await resp.json();
        if (!data.success || !data.nodes.length) {
            const lt = document.querySelector('.artist-map-loading-text');
            if (lt) {
                lt.textContent = resp.status === 404
                    ? `"${name}" doesn't appear to be a real artist. Try a different name.`
                    : `No data found for "${name}". Try a different artist.`;
            }
            setTimeout(() => {
                const le = document.getElementById('artist-map-loading');
                if (le) le.remove();
                closeArtistMap();
            }, 2500);
            return;
        }

        const ring1Count = data.nodes.filter(n => n.ring === 1).length;
        const ring2Count = data.nodes.filter(n => n.ring === 2).length;
        document.getElementById('artist-map-stats').textContent =
            `${data.center} · ${ring1Count} similar · ${ring2Count} extended`;

        _artMap.edges = data.edges;
        const WR = _artMap.WATCHLIST_R;
        const BUF = _artMap.BUFFER;

        // Layout: center node at origin, ring 1 in circle around it, ring 2 around ring 1
        const centerNode = data.nodes[0];
        centerNode.x = 0; centerNode.y = 0;
        centerNode.radius = WR * 1.2; // Extra large center
        centerNode.opacity = 1;
        centerNode.type = 'center';
        _artMap.placed.push(centerNode);

        const CELL = WR * 2 + BUF * 2;
        const grid = {};
        function _gk(x, y) { return `${Math.floor(x / CELL)},${Math.floor(y / CELL)}`; }
        function _ga(n) { const k = _gk(n.x, n.y); if (!grid[k]) grid[k] = []; grid[k].push(n); }
        function _gc(x, y, r) {
            const cx = Math.floor(x / CELL), cy = Math.floor(y / CELL);
            for (let dx = -3; dx <= 3; dx++) for (let dy = -3; dy <= 3; dy++) {
                const cell = grid[`${cx + dx},${cy + dy}`];
                if (!cell) continue;
                for (const p of cell) {
                    const ddx = x - p.x, ddy = y - p.y;
                    if (ddx * ddx + ddy * ddy < (r + p.radius + BUF) * (r + p.radius + BUF)) return true;
                }
            }
            return false;
        }
        _ga(centerNode);

        // Place ring 1 in a circle
        const ring1 = data.nodes.filter(n => n.ring === 1);
        const ring1Dist = WR * 2.5;
        ring1.forEach((n, i) => {
            const angle = (i / ring1.length) * Math.PI * 2;
            const rank = n.rank || 5;
            n.radius = WR * 0.4 + (10 - rank) * WR * 0.03;
            n.opacity = 1;

            // Find non-colliding position near ideal
            let placed = false;
            for (let dist = ring1Dist; dist < ring1Dist + WR * 3; dist += n.radius * 0.5) {
                for (let ao = 0; ao < 6; ao++) {
                    const a = angle + (ao * 0.1 * (ao % 2 ? 1 : -1));
                    const tx = Math.cos(a) * dist;
                    const ty = Math.sin(a) * dist;
                    if (!_gc(tx, ty, n.radius)) {
                        n.x = tx; n.y = ty;
                        _artMap.placed.push(n);
                        _ga(n);
                        placed = true;
                        break;
                    }
                }
                if (placed) break;
            }
        });

        // Place ring 2 around their ring 1 sources
        const ring2 = data.nodes.filter(n => n.ring === 2);
        const nodeById = {};
        _artMap.placed.forEach(n => { nodeById[n.id] = n; });

        ring2.forEach(n => {
            // Find the ring 1 node that connects to this
            const edge = data.edges.find(e => e.target === n.id);
            const src = edge ? nodeById[edge.source] : null;
            const cx = src ? src.x : 0;
            const cy = src ? src.y : 0;

            n.radius = WR * 0.2 + (n.popularity || 0) / 100 * WR * 0.1;
            n.opacity = 1;

            const startDist = (src ? src.radius : WR) + n.radius + BUF;
            let placed = false;
            for (let dist = startDist; dist < startDist + WR * 2; dist += n.radius * 0.5) {
                const steps = Math.max(8, Math.floor(dist * 0.08));
                const off = Math.random() * Math.PI * 2;
                for (let a = 0; a < steps; a++) {
                    const angle = off + (a / steps) * Math.PI * 2;
                    const tx = cx + Math.cos(angle) * dist;
                    const ty = cy + Math.sin(angle) * dist;
                    if (!_gc(tx, ty, n.radius)) {
                        n.x = tx; n.y = ty;
                        _artMap.placed.push(n);
                        _ga(n);
                        placed = true;
                        break;
                    }
                }
                if (placed) break;
            }
        });

        // Build node lookup for edges
        _artMap._nodeById = {};
        _artMap.placed.forEach(n => { _artMap._nodeById[n.id] = n; });

        // Auto-zoom
        let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
        _artMap.placed.forEach(n => {
            minX = Math.min(minX, n.x - n.radius);
            maxX = Math.max(maxX, n.x + n.radius);
            minY = Math.min(minY, n.y - n.radius);
            maxY = Math.max(maxY, n.y + n.radius);
        });
        const mapW = maxX - minX + 200, mapH = maxY - minY + 200;
        _artMap.zoom = Math.min(_artMap.width / mapW, _artMap.height / mapH, 1);
        _artMap.offsetX = _artMap.width / 2 - ((minX + maxX) / 2) * _artMap.zoom;
        _artMap.offsetY = _artMap.height / 2 - ((minY + maxY) / 2) * _artMap.zoom;

        _artMapSetupInteraction(canvas);

        // Load images
        const loadingText = container.querySelector('.artist-map-loading-text');
        if (loadingText) loadingText.textContent = `Loading ${_artMap.placed.length} artists...`;

        setTimeout(async () => {
            const imgNodes = _artMap.placed.filter(n => n.image_url);
            let loaded = 0;
            const CONCURRENT = 20;
            let idx = 0;
            async function loadBatch() {
                const batch = [];
                while (idx < imgNodes.length && batch.length < CONCURRENT) {
                    const n = imgNodes[idx++];
                    batch.push(_artMapLoadImage(n.image_url)
                        .then(bmp => { if (bmp) _artMap.images[n.id] = bmp; })
                        .finally(() => { loaded++; }));
                }
                if (batch.length) await Promise.all(batch);
                if (idx < imgNodes.length) return loadBatch();
            }
            await loadBatch();
            _artMap.dirty = true;
            _artMapRender();
            const le = document.getElementById('artist-map-loading');
            if (le) le.remove();
        }, 50);

        _artMap.dirty = true;
        _artMapRender();

    } catch (err) {
        console.error('Artist explorer error:', err);
        const lt = container.querySelector('.artist-map-loading-text');
        if (lt) lt.textContent = 'Error loading explorer';
    }
}

function _showArtistMapSearchPrompt() {
    return new Promise(resolve => {
        const existing = document.getElementById('artmap-search-prompt');
        if (existing) existing.remove();

        const overlay = document.createElement('div');
        overlay.id = 'artmap-search-prompt';
        overlay.className = 'modal-overlay';
        overlay.onclick = (e) => { if (e.target === overlay) { overlay.remove(); resolve(null); } };

        overlay.innerHTML = `
            <div class="artmap-search-prompt-modal">
                <div class="artmap-search-prompt-header">
                    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
                        <circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
                        <line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/>
                    </svg>
                    <div>
                        <h3>Artist Explorer</h3>
                        <p>Enter an artist to explore their connections</p>
                    </div>
                </div>
                <input type="text" id="artmap-explore-input" class="artmap-explore-input" placeholder="Artist name..." autofocus>
                <div class="artmap-search-prompt-actions">
                    <button class="ya-header-btn" onclick="document.getElementById('artmap-search-prompt').remove()">Cancel</button>
                    <button class="ya-header-btn ya-viewall-btn" id="artmap-explore-go">
                        <span>Explore</span>
                        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>
                    </button>
                </div>
            </div>
        `;
        document.body.appendChild(overlay);

        const input = overlay.querySelector('#artmap-explore-input');
        const goBtn = overlay.querySelector('#artmap-explore-go');

        const submit = () => {
            const val = input.value.trim();
            overlay.remove();
            resolve(val || null);
        };

        goBtn.onclick = submit;
        input.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); });
        setTimeout(() => input.focus(), 50);
    });
}

function artMapToggleSimilar() {
    _artMap._hideSimilar = !_artMap._hideSimilar;
    _artMap.dirty = true;
    _artMapRender();
    const btn = document.getElementById('artmap-toggle-similar');
    if (btn) btn.style.opacity = _artMap._hideSimilar ? '0.4' : '1';
    showToast(_artMap._hideSimilar ? 'Showing watchlist only' : 'Showing all artists', 'info', 1500);
}

function _artMapLoadImage(url) {
    // Try direct CORS fetch first (zero server load, works for Spotify/iTunes/Discogs)
    return fetch(url, { mode: 'cors' })
        .then(r => r.ok ? r.blob() : Promise.reject('not ok'))
        .then(b => createImageBitmap(b))
        .catch(() => {
            // Fallback: server proxy for CDNs without CORS headers
            return fetch('/api/image-proxy?url=' + encodeURIComponent(url))
                .then(r => r.ok ? r.blob() : null)
                .then(b => b ? createImageBitmap(b) : null)
                .catch(() => null);
        });
}

function _artMapHideContextMenu() {
    const m = document.getElementById('artist-map-context');
    if (m) m.style.display = 'none';
}

function _artMapSetupInteraction(canvas) {
    // Prevent stacking listeners on repeated opens
    if (canvas._artMapListenersAttached) return;
    canvas._artMapListenersAttached = true;

    let isPanning = false, panStartX = 0, panStartY = 0;

    canvas.addEventListener('wheel', (e) => {
        e.preventDefault();
        const delta = e.deltaY > 0 ? 0.9 : 1.1;
        const newZoom = Math.max(0.02, Math.min(5, _artMap.zoom * delta));
        // Zoom toward mouse
        const rect = canvas.getBoundingClientRect();
        const mx = e.clientX - rect.left;
        const my = e.clientY - rect.top;
        _artMap.offsetX = mx - (mx - _artMap.offsetX) * (newZoom / _artMap.zoom);
        _artMap.offsetY = my - (my - _artMap.offsetY) * (newZoom / _artMap.zoom);
        _artMap.zoom = newZoom;
        _artMapRender(); // fast blit
        // Debounce hi-res rebuild after zoom settles
        clearTimeout(_artMap._zoomRebuild);
        _artMap._zoomRebuild = setTimeout(() => { _artMap.dirty = true; _artMapRender(); }, 300);
    }, { passive: false });

    let clickStart = null;

    // Keyboard shortcuts
    function _artMapKeyHandler(e) {
        if (!document.getElementById('artist-map-container') || document.getElementById('artist-map-container').style.display === 'none') return;
        if (e.target.tagName === 'INPUT') return; // don't intercept search typing
        if (e.key === 'Escape') { closeArtistMap(); e.preventDefault(); }
        else if (e.key === '=' || e.key === '+') { artMapZoom(1.3); e.preventDefault(); }
        else if (e.key === '-') { artMapZoom(0.7); e.preventDefault(); }
        else if (e.key === '0') { artMapFitToView(); e.preventDefault(); }
        else if (e.key === 'f' || e.key === 'F') { artMapFitToView(); e.preventDefault(); }
        else if (e.key === 's' || e.key === 'S') {
            const input = document.getElementById('artist-map-search');
            if (input) { input.focus(); e.preventDefault(); }
        }
        else if (e.key === 'h' || e.key === 'H') {
            // Toggle similar artists visibility
            _artMap._hideSimilar = !_artMap._hideSimilar;
            _artMap.dirty = true;
            _artMapRender();
        }
    }
    window.addEventListener('keydown', _artMapKeyHandler);
    _artMap._keyHandler = _artMapKeyHandler;

    // Right-click context menu
    canvas.addEventListener('contextmenu', (e) => {
        e.preventDefault();
        const { nx, ny } = _artMapScreenToWorld(e, canvas);
        const node = _artMapHitTest(nx, ny);
        if (!node || node._isLabel) { _artMapHideContextMenu(); return; }

        const menu = document.getElementById('artist-map-context') || (() => {
            const m = document.createElement('div');
            m.id = 'artist-map-context';
            m.className = 'artmap-context-menu';
            document.getElementById('artist-map-container').appendChild(m);
            return m;
        })();

        const hasId = node.spotify_id || node.itunes_id || node.deezer_id;
        const activeSource = window._yaActiveSource || 'spotify';
        const bestId = node[activeSource + '_id'] || node.spotify_id || node.itunes_id || node.deezer_id || '';
        const bestSource = node[activeSource + '_id'] ? activeSource : node.spotify_id ? 'spotify' : node.itunes_id ? 'itunes' : 'deezer';

        menu.innerHTML = `
            <div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); ${hasId ? `openYourArtistInfoModal_direct(${JSON.stringify(node).replace(/"/g, '&quot;')})` : ''}">
                <span>&#9432;</span> Artist Info
            </div>
            <a class="artmap-ctx-item" href="${bestId ? buildArtistDetailPath(bestId, bestSource) : '#'}" onclick="_artMapHideContextMenu()" ${bestId ? '' : 'aria-disabled="true" style="pointer-events:none;opacity:0.5;text-decoration:none;color:inherit;"'}>
                <span>&#128191;</span> View Discography
            </a>
            <div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); toggleYourArtistWatchlist(0,'${escapeForInlineJs(node.name)}','${escapeForInlineJs(bestId)}','${bestSource}',null)">
                <span>&#128065;</span> ${node.type === 'watchlist' ? 'On Watchlist' : 'Add to Watchlist'}
            </div>
        `;
        menu.style.display = 'block';
        menu.style.left = Math.min(e.clientX, window.innerWidth - 200) + 'px';
        menu.style.top = Math.min(e.clientY, window.innerHeight - 200) + 'px';

        // Close on next click anywhere
        setTimeout(() => {
            window.addEventListener('click', _artMapHideContextMenu, { once: true });
        }, 10);
    });

    canvas.addEventListener('mousedown', (e) => {
        if (e.button !== 0) return; // left button only
        clickStart = { x: e.clientX, y: e.clientY, time: Date.now() };
        isPanning = true;
        panStartX = e.clientX;
        panStartY = e.clientY;
    });

    canvas.addEventListener('mousemove', (e) => {
        if (isPanning) {
            _artMap.offsetX += e.clientX - panStartX;
            _artMap.offsetY += e.clientY - panStartY;
            panStartX = e.clientX;
            panStartY = e.clientY;
            _artMapRender();
        } else {
            const { nx, ny } = _artMapScreenToWorld(e, canvas);
            const prev = _artMap.hoveredNode;
            _artMap.hoveredNode = _artMapHitTest(nx, ny);
            canvas.style.cursor = _artMap.hoveredNode ? 'pointer' : 'grab';
            _artMapShowTooltip(e, _artMap.hoveredNode);
            if (prev !== _artMap.hoveredNode) {
                // Reset constellation highlight timer
                clearTimeout(_artMap._constellationTimer);
                if (_artMap._constellationActive) {
                    _artMap._constellationActive = false;
                    _artMapAnimateConstellation(); // fade out
                }
                if (_artMap.hoveredNode) {
                    // Delay constellation effect by 800ms of sustained hover
                    _artMap._constellationTimer = setTimeout(() => {
                        if (_artMap.hoveredNode) {
                            _artMap._constellationActive = true;
                            _artMap._constellationFade = 0;
                            _artMap._constellationCache = null;
                            _artMapAnimateConstellation();
                        }
                    }, 800);
                }
                _artMapRender();
            }
        }
    });

    canvas.addEventListener('mouseup', (e) => {
        if (e.button !== 0) return; // left button only
        const wasDrag = clickStart && (Math.abs(e.clientX - clickStart.x) > 5 || Math.abs(e.clientY - clickStart.y) > 5);
        isPanning = false;

        if (!wasDrag && clickStart) {
            // It was a click — find the node under cursor
            const { nx, ny } = _artMapScreenToWorld(e, canvas);
            const node = _artMapHitTest(nx, ny);
            if (node) {
                _artMap._ripple = { x: node.x, y: node.y, radius: node.radius, start: performance.now() };
                _artMapRender();
                if (node.spotify_id || node.itunes_id || node.deezer_id) {
                    setTimeout(() => openYourArtistInfoModal_direct(node), 200);
                }
            }
        }

        clickStart = null;
        _artMapShowTooltip(e, null);
    });

    canvas.addEventListener('mouseleave', () => {
        _artMapShowTooltip(null, null);
        clearTimeout(_artMap._constellationTimer);
        if (_artMap._constellationActive) {
            _artMap._constellationActive = false;
            _artMapAnimateConstellation();
        }
        _artMap.hoveredNode = null;
        _artMapRender();
    });

    // Touch support — single finger pan, pinch to zoom
    let lastTouches = null;
    canvas.addEventListener('touchstart', (e) => {
        e.preventDefault();
        lastTouches = [...e.touches];
    }, { passive: false });

    canvas.addEventListener('touchmove', (e) => {
        e.preventDefault();
        if (!lastTouches) return;
        const touches = [...e.touches];

        if (touches.length === 1 && lastTouches.length === 1) {
            // Pan
            _artMap.offsetX += touches[0].clientX - lastTouches[0].clientX;
            _artMap.offsetY += touches[0].clientY - lastTouches[0].clientY;
            _artMapRender();
        } else if (touches.length === 2 && lastTouches.length === 2) {
            // Pinch zoom
            const prevDist = Math.hypot(lastTouches[1].clientX - lastTouches[0].clientX, lastTouches[1].clientY - lastTouches[0].clientY);
            const curDist = Math.hypot(touches[1].clientX - touches[0].clientX, touches[1].clientY - touches[0].clientY);
            const factor = curDist / prevDist;
            const cx = (touches[0].clientX + touches[1].clientX) / 2;
            const cy = (touches[0].clientY + touches[1].clientY) / 2;
            const newZoom = Math.max(0.02, Math.min(3, _artMap.zoom * factor));
            _artMap.offsetX = cx - (cx - _artMap.offsetX) * (newZoom / _artMap.zoom);
            _artMap.offsetY = cy - (cy - _artMap.offsetY) * (newZoom / _artMap.zoom);
            _artMap.zoom = newZoom;
            _artMap.dirty = true;
            _artMapRender();
        }
        lastTouches = touches;
    }, { passive: false });

    canvas.addEventListener('touchend', (e) => {
        e.preventDefault();
        // Tap to click
        if (lastTouches && lastTouches.length === 1 && e.changedTouches.length === 1) {
            const t = e.changedTouches[0];
            const rect = canvas.getBoundingClientRect();
            const wx = (t.clientX - rect.left - _artMap.offsetX) / _artMap.zoom;
            const wy = (t.clientY - rect.top - _artMap.offsetY) / _artMap.zoom;
            const node = _artMapHitTest(wx, wy);
            if (node && (node.spotify_id || node.itunes_id || node.deezer_id)) {
                openYourArtistInfoModal_direct(node);
            }
        }
        lastTouches = null;
    }, { passive: false });

    // Handle resize
    window.addEventListener('resize', () => {
        const container = document.getElementById('artist-map-container');
        if (!container || container.style.display === 'none') return;
        _artMap.width = container.clientWidth;
        _artMap.height = container.clientHeight - 50;
        canvas.width = _artMap.width * window.devicePixelRatio;
        canvas.height = _artMap.height * window.devicePixelRatio;
        canvas.style.width = _artMap.width + 'px';
        canvas.style.height = _artMap.height + 'px';
        _artMap.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
    });
}

function _artMapScreenToWorld(e, canvas) {
    const rect = canvas.getBoundingClientRect();
    const sx = e.clientX - rect.left;
    const sy = e.clientY - rect.top;
    // Inverse of: translate(offsetX, offsetY) → scale(zoom)
    return {
        nx: (sx - _artMap.offsetX) / _artMap.zoom,
        ny: (sy - _artMap.offsetY) / _artMap.zoom,
    };
}

function _artMapHitTest(wx, wy) {
    // Check watchlist first (drawn on top), then similar
    const sorted = [..._artMap.placed].sort((a, b) =>
        (b.type === 'watchlist' ? 1 : 0) - (a.type === 'watchlist' ? 1 : 0));
    for (const n of sorted) {
        if ((n.opacity || 0) < 0.3) continue;
        const dx = wx - n.x;
        const dy = wy - n.y;
        if (dx * dx + dy * dy <= n.radius * n.radius) return n;
    }
    return null;
}

async function openYourArtistInfoModal_direct(node) {
    // Determine best source ID — prefer active metadata source
    let bestId = '', bestSource = '';
    // Check what the active source is
    const activeSource = window._yaActiveSource || 'spotify';
    const sourceOrder = activeSource === 'spotify' ? ['spotify_id', 'itunes_id', 'deezer_id', 'discogs_id', 'musicbrainz_id']
        : activeSource === 'itunes' ? ['itunes_id', 'spotify_id', 'deezer_id', 'discogs_id', 'musicbrainz_id']
            : activeSource === 'deezer' ? ['deezer_id', 'spotify_id', 'itunes_id', 'discogs_id', 'musicbrainz_id']
                : activeSource === 'musicbrainz' ? ['musicbrainz_id', 'spotify_id', 'itunes_id', 'deezer_id', 'discogs_id']
                    : ['spotify_id', 'itunes_id', 'deezer_id', 'discogs_id', 'musicbrainz_id'];
    const sourceMap = { spotify_id: 'spotify', itunes_id: 'itunes', deezer_id: 'deezer', discogs_id: 'discogs', musicbrainz_id: 'musicbrainz' };
    for (const key of sourceOrder) {
        if (node[key]) { bestId = node[key]; bestSource = sourceMap[key]; break; }
    }

    // Gather ALL connected artists from map edges (both directions)
    const related = [];
    const relatedIds = new Set();
    const nById = _artMap._nodeById || {};
    _artMap.edges.forEach(e => {
        if (e.source === node.id && nById[e.target] && !relatedIds.has(e.target)) {
            related.push(nById[e.target]);
            relatedIds.add(e.target);
        }
        if (e.target === node.id && nById[e.source] && !relatedIds.has(e.source)) {
            related.push(nById[e.source]);
            relatedIds.add(e.source);
        }
    });

    const poolEntry = {
        id: node.id,
        artist_name: node.name,
        active_source_id: bestId,
        active_source: bestSource,
        image_url: node.image_url || '',
        spotify_artist_id: node.spotify_id || '',
        itunes_artist_id: node.itunes_id || '',
        deezer_artist_id: node.deezer_id || '',
        discogs_artist_id: node.discogs_id || '',
        source_services: [],
        on_watchlist: node.type === 'watchlist' ? 1 : 0,
        _related: related,
    };
    if (!window._yaArtists) window._yaArtists = {};
    window._yaArtists[node.id] = poolEntry;
    openYourArtistInfoModal(node.id);
}

async function loadDiscoveryShuffle() {
    try {
        const container = document.getElementById('personalized-discovery-shuffle');
        if (!container) return;

        const response = await fetch('/api/discover/personalized/discovery-shuffle?limit=50');
        if (!response.ok) return;

        const data = await response.json();
        if (!data.success || !data.tracks || data.tracks.length === 0) {
            container.closest('.discover-section').style.display = 'none';
            return;
        }

        personalizedDiscoveryShuffle = data.tracks;
        renderCompactPlaylist(container, data.tracks);
        container.closest('.discover-section').style.display = 'block';

    } catch (error) {
        console.error('Error loading discovery shuffle:', error);
    }
}

// ===============================
// BECAUSE YOU LISTEN TO
// ===============================

function _renderByltSection(section, idx) {
    return `
        <div class="discover-section bylt-section">
            <div class="discover-section-header">
                <div class="bylt-header">
                    ${section.artist_image ? `<img class="bylt-artist-img" src="${section.artist_image}" alt="" onerror="this.style.display='none'">` : ''}
                    <div>
                        <div class="discover-section-subtitle">Because you listen to</div>
                        <h3 class="discover-section-title">${_esc(section.artist_name)}</h3>
                    </div>
                </div>
            </div>
            <div class="discover-carousel" id="bylt-carousel-${idx}"></div>
        </div>
    `;
}

function _renderByltTrackCard(t) {
    return `
        <div class="discover-card">
            <div class="discover-card-image">
                ${t.image_url ? `<img src="${t.image_url}" alt="" loading="lazy" onerror="this.src='/static/placeholder-album.png'">` : '<div class="discover-card-placeholder">🎵</div>'}
            </div>
            <div class="discover-card-title">${_esc(t.name)}</div>
            <div class="discover-card-artist">${_esc(t.artist)}</div>
        </div>
    `;
}

let _byltCtrl = null;

async function loadBecauseYouListenTo() {
    // Ensure the BYLT container exists in the DOM. It's dynamically
    // inserted after the release radar section because the markup
    // doesn't ship a placeholder for it. Bail if anchor section
    // isn't present.
    let byltContainer = document.getElementById('discover-bylt-sections');
    if (!byltContainer) {
        const releaseRadar = document.getElementById('discover-release-radar');
        if (!releaseRadar) return;
        const parent = releaseRadar.closest('.discover-section');
        if (!parent) return;

        byltContainer = document.createElement('div');
        byltContainer.id = 'discover-bylt-sections';
        parent.parentNode.insertBefore(byltContainer, parent.nextSibling);
    }

    if (!_byltCtrl) {
        _byltCtrl = createDiscoverSectionController({
            id: 'because-you-listen-to',
            contentEl: '#discover-bylt-sections',
            fetchUrl: '/api/discover/because-you-listen-to',
            extractItems: (data) => data.sections || [],
            // No per-section empty/loading copy — when there's nothing
            // to show we leave the container blank rather than render a
            // placeholder, matching the original no-op behavior.
            renderEmptyState: false,
            loadingMessage: '',
            renderItems: (items) => items.map((s, i) => _renderByltSection(s, i)).join(''),
            onRendered: ({ items }) => {
                // Inject track cards into each section's carousel after
                // the section wrappers exist in the DOM.
                items.forEach((section, idx) => {
                    const carousel = document.getElementById(`bylt-carousel-${idx}`);
                    if (!carousel) return;
                    carousel.innerHTML = section.tracks.map(t => _renderByltTrackCard(t)).join('');
                });
            },
        });
    }
    return _byltCtrl.load();
}

// ===============================
// CACHE DISCOVERY SECTIONS
// ===============================

// Global arrays for cache discovery click handlers
let _cacheDiscoverData = {};

function _cacheDiscoverCard(item, type, sectionKey, index) {
    const _esc = (s) => (s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
    const coverUrl = item.image_url || '/static/placeholder-album.png';
    const title = item.name || '';
    const subtitle = item.artist_name || '';
    const meta = item.release_date ? item.release_date.substring(0, 10) : (item.label || '');
    const onclick = `openCacheDiscoverAlbum('${sectionKey}',${index})`;
    const libBadge = item.in_library ? '<div class="discover-card-lib-badge">In Library</div>' : '';
    return `<div class="discover-card" onclick="${onclick}" style="cursor:pointer">
        <div class="discover-card-image">
            <img src="${_esc(coverUrl)}" alt="${_esc(title)}" loading="lazy" onerror="this.src='/static/placeholder-album.png'">
            ${libBadge}
        </div>
        <div class="discover-card-info">
            <h4 class="discover-card-title">${_esc(title)}</h4>
            <p class="discover-card-subtitle">${_esc(subtitle)}</p>
            ${meta ? `<p class="discover-card-meta">${_esc(meta)}</p>` : ''}
        </div>
    </div>`;
}

async function openCacheDiscoverAlbum(sectionKey, index) {
    const items = _cacheDiscoverData[sectionKey];
    if (!items || !items[index]) return;
    const item = items[index];
    const source = item.source || 'spotify';
    const albumId = item.entity_id;

    // Deep cuts / genre dive tracks — find the real album by searching the cache
    if (sectionKey === 'deep_cuts' || sectionKey === 'genre_dive_tracks') {
        document.getElementById('genre-deep-dive-modal')?.remove();
        const albumName = item.album_name || item.name || '';
        const artistName = item.artist_name || '';
        const trackAlbumId = item.album_id || '';
        const trackSource = item.source || source;

        if (!artistName) {
            showToast('No artist data available for this track', 'error');
            return;
        }

        showLoadingOverlay(`Loading ${albumName}...`);
        try {
            let resolvedSource = trackSource;
            let resolvedId = trackAlbumId;
            let response;

            // If we have an album_id, use it directly
            if (trackAlbumId) {
                const _params = new URLSearchParams({ name: albumName, artist: artistName });
                response = await fetch(`/api/discover/album/${trackSource}/${trackAlbumId}?${_params}`);
            }

            // Fallback: resolve by name+artist if no album_id or direct fetch failed
            if (!trackAlbumId || (response && !response.ok)) {
                const searchResp = await fetch(`/api/discover/resolve-cache-album?name=${encodeURIComponent(albumName)}&artist=${encodeURIComponent(artistName)}`);
                if (searchResp.ok) {
                    const searchData = await searchResp.json();
                    if (searchData.success && searchData.entity_id) {
                        resolvedSource = searchData.source || trackSource;
                        resolvedId = searchData.entity_id;
                        const _params = new URLSearchParams({ name: albumName, artist: artistName });
                        response = await fetch(`/api/discover/album/${resolvedSource}/${resolvedId}?${_params}`);
                    }
                }
            }

            if (!response || !response.ok) throw new Error('Failed to fetch album tracks');
            const albumData = await response.json();
            if (!albumData.tracks || albumData.tracks.length === 0) throw new Error('No tracks found');

            const spotifyTracks = albumData.tracks.map(track => {
                let artists = track.artists || albumData.artists || [{ name: artistName }];
                if (Array.isArray(artists)) artists = artists.map(a => a.name || a);
                return {
                    id: track.id, name: track.name, artists,
                    album: { id: albumData.id, name: albumData.name, album_type: albumData.album_type || 'album', total_tracks: albumData.total_tracks || 0, release_date: albumData.release_date || '', images: albumData.images || [] },
                    duration_ms: track.duration_ms || 0, track_number: track.track_number || 0,
                };
            });
            const artistContext = _buildDiscoverArtistContext(resolvedSource, artistName, item, albumData);
            const albumContext = { id: albumData.id, name: albumData.name, album_type: albumData.album_type || 'album', total_tracks: albumData.total_tracks || 0, release_date: albumData.release_date || '', images: albumData.images || [] };
            await openDownloadMissingModalForYouTube(`discover_cache_${resolvedId}`, albumData.name, spotifyTracks, artistContext, albumContext);
            hideLoadingOverlay();
        } catch (error) {
            console.error('Error opening deep cut album:', error);
            showToast(`Failed to load album: ${error.message}`, 'error');
            hideLoadingOverlay();
        }
        return;
    }

    if (!albumId) {
        showToast('No album ID available', 'error');
        return;
    }

    // Close genre deep dive modal if open
    document.getElementById('genre-deep-dive-modal')?.remove();

    showLoadingOverlay(`Loading ${item.name || 'album'}...`);
    try {
        const _params = new URLSearchParams({ name: item.name || '', artist: item.artist_name || '' });
        let response = await fetch(`/api/discover/album/${source}/${albumId}?${_params}`);

        // If 404 (stale cache entry), try resolving via name+artist
        if (response.status === 404) {
            const resolveResp = await fetch(`/api/discover/resolve-cache-album?name=${encodeURIComponent(item.name || '')}&artist=${encodeURIComponent(item.artist_name || '')}`);
            if (resolveResp.ok) {
                const resolved = await resolveResp.json();
                if (resolved.success && resolved.entity_id && resolved.entity_id !== albumId) {
                    response = await fetch(`/api/discover/album/${resolved.source || source}/${resolved.entity_id}?${_params}`);
                }
            }
        }

        if (!response.ok) throw new Error('Album not available — it may have been removed from the source');
        const albumData = await response.json();
        if (!albumData.tracks || albumData.tracks.length === 0) throw new Error('No tracks found');

        const spotifyTracks = albumData.tracks.map(track => {
            let artists = track.artists || albumData.artists || [{ name: item.artist_name }];
            if (Array.isArray(artists)) artists = artists.map(a => a.name || a);
            return {
                id: track.id,
                name: track.name,
                artists: artists,
                album: {
                    id: albumData.id,
                    name: albumData.name,
                    album_type: albumData.album_type || 'album',
                    total_tracks: albumData.total_tracks || 0,
                    release_date: albumData.release_date || '',
                    images: albumData.images || [],
                },
                duration_ms: track.duration_ms || 0,
                track_number: track.track_number || 0,
            };
        });

        const artistContext = _buildDiscoverArtistContext(source, item.artist_name || albumData.artists?.[0]?.name || '', item, albumData);
        const albumContext = {
            id: albumData.id,
            name: albumData.name,
            album_type: albumData.album_type || 'album',
            total_tracks: albumData.total_tracks || 0,
            release_date: albumData.release_date || '',
            images: albumData.images || [],
        };

        await openDownloadMissingModalForYouTube(
            `discover_cache_${albumId}`, albumData.name, spotifyTracks, artistContext, albumContext
        );
        hideLoadingOverlay();
    } catch (error) {
        console.error('Error opening cache discover album:', error);
        showToast(`Failed to load album: ${error.message}`, 'error');
        hideLoadingOverlay();
    }
}

function _insertCacheSection(id, title, subtitle, html, position) {
    const container = document.getElementById('discover-bylt-sections') || document.querySelector('.discover-container');
    if (!container) return;
    let section = document.getElementById(id);
    if (!section) {
        section = document.createElement('div');
        section.id = id;
        section.className = 'discover-section';
        if (position === 'top') {
            // Insert after the hero section (first child), not before it
            const hero = container.querySelector('.discover-hero');
            if (hero && hero.nextSibling) {
                container.insertBefore(section, hero.nextSibling);
            } else {
                container.prepend(section);
            }
        } else {
            container.appendChild(section);
        }
    }
    section.innerHTML = `
        <div class="discover-section-header">
            <div>
                <div class="discover-section-subtitle">${subtitle}</div>
                <h3 class="discover-section-title">${title}</h3>
            </div>
        </div>
        <div class="discover-carousel">${html}</div>
    `;
}

async function loadCacheUndiscoveredAlbums() {
    try {
        const resp = await fetch('/api/discover/undiscovered-albums');
        if (!resp.ok) return;
        const data = await resp.json();
        if (!data.success || !data.albums || !data.albums.length) return;
        _cacheDiscoverData['undiscovered'] = data.albums;
        _insertCacheSection('cache-undiscovered',
            'Undiscovered Albums', 'From artists you love',
            data.albums.map((a, i) => _cacheDiscoverCard(a, 'album', 'undiscovered', i)).join(''));
    } catch (e) { console.debug('Cache undiscovered albums:', e); }
}

async function loadCacheGenreNewReleases() {
    try {
        const resp = await fetch('/api/discover/genre-new-releases');
        if (!resp.ok) return;
        const data = await resp.json();
        if (!data.success || !data.albums || !data.albums.length) return;
        _cacheDiscoverData['genre_releases'] = data.albums;
        _insertCacheSection('cache-genre-releases',
            'New In Your Genres', 'Released in the last 90 days',
            data.albums.map((a, i) => _cacheDiscoverCard(a, 'album', 'genre_releases', i)).join(''));
    } catch (e) { console.debug('Cache genre new releases:', e); }
}

async function loadCacheLabelExplorer() {
    try {
        const resp = await fetch('/api/discover/label-explorer');
        if (!resp.ok) return;
        const data = await resp.json();
        if (!data.success || !data.albums || !data.albums.length) return;
        _cacheDiscoverData['label_explorer'] = data.albums;
        _insertCacheSection('cache-label-explorer',
            'From Your Labels', 'Popular on labels in your library',
            data.albums.map((a, i) => _cacheDiscoverCard(a, 'album', 'label_explorer', i)).join(''));
    } catch (e) { console.debug('Cache label explorer:', e); }
}

async function loadCacheDeepCuts() {
    try {
        const resp = await fetch('/api/discover/deep-cuts');
        if (!resp.ok) return;
        const data = await resp.json();
        if (!data.success || !data.tracks || !data.tracks.length) return;
        _cacheDiscoverData['deep_cuts'] = data.tracks;
        _insertCacheSection('cache-deep-cuts',
            'Deep Cuts', 'Hidden tracks from artists you know',
            data.tracks.map((t, i) => _cacheDiscoverCard(t, 'track', 'deep_cuts', i)).join(''));
    } catch (e) { console.debug('Cache deep cuts:', e); }
}

async function loadCacheGenreExplorer() {
    try {
        const resp = await fetch('/api/discover/genre-explorer');
        if (!resp.ok) return;
        const data = await resp.json();
        if (!data.success || !data.genres || !data.genres.length) return;
        const _esc = (s) => (s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/'/g, '&#39;');
        const html = `<div class="genre-explorer-grid">${data.genres.map(g => `
            <div class="genre-explorer-pill ${g.explored ? 'explored' : 'unexplored'}" onclick="openGenreDeepDive('${_esc(g.genre)}')" style="cursor:pointer">
                <span class="genre-pill-name">${_esc(g.genre)}</span>
                <span class="genre-pill-count">${g.artist_count} artist${g.artist_count !== 1 ? 's' : ''}</span>
                ${!g.explored ? '<span class="genre-pill-badge">New</span>' : ''}
            </div>
        `).join('')}</div>`;
        _insertCacheSection('cache-genre-explorer',
            'Genre Explorer', 'Tap a genre to explore', html, 'top');
    } catch (e) { console.debug('Cache genre explorer:', e); }
}

async function openGenreDeepDive(genre) {
    document.getElementById('genre-deep-dive-modal')?.remove();

    const _esc = (s) => (s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
    const _fmtNum = (n) => {
        if (!n) return '';
        if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
        if (n >= 1000) return (n / 1000).toFixed(0) + 'K';
        return n.toString();
    };
    const _fmtDur = (ms) => {
        if (!ms) return '';
        const m = Math.floor(ms / 60000);
        const s = Math.floor((ms % 60000) / 1000);
        return `${m}:${s.toString().padStart(2, '0')}`;
    };

    const overlay = document.createElement('div');
    overlay.id = 'genre-deep-dive-modal';
    overlay.className = 'genre-dive-overlay';
    overlay.innerHTML = `
        <div class="genre-dive-modal">
            <div class="genre-dive-header">
                <div>
                    <div class="genre-dive-subtitle">Genre Deep Dive</div>
                    <h2 class="genre-dive-title">${_esc(genre)}</h2>
                </div>
                <button class="genre-dive-close" onclick="document.getElementById('genre-deep-dive-modal').remove()">&times;</button>
            </div>
            <div class="genre-dive-body" id="genre-dive-body">
                <div class="genre-dive-loading"><div class="genre-dive-spinner"></div>Exploring ${_esc(genre)}...</div>
            </div>
        </div>
    `;
    overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
    document.body.appendChild(overlay);

    try {
        const resp = await fetch(`/api/discover/genre-deep-dive?genre=${encodeURIComponent(genre)}`);
        if (!resp.ok) throw new Error('Failed to load');
        const data = await resp.json();
        if (!data.success) throw new Error('Failed');

        const body = document.getElementById('genre-dive-body');
        if (!body) return;

        // Update header with counts
        const subtitle = document.querySelector('.genre-dive-subtitle');
        if (subtitle) {
            const parts = [];
            if (data.artists?.length) parts.push(`${data.artists.length} artist${data.artists.length !== 1 ? 's' : ''}`);
            if (data.tracks?.length) parts.push(`${data.tracks.length} track${data.tracks.length !== 1 ? 's' : ''}`);
            if (data.albums?.length) parts.push(`${data.albums.length} album${data.albums.length !== 1 ? 's' : ''}`);
            subtitle.textContent = parts.length ? parts.join(' · ') : 'Genre Deep Dive';
        }

        let html = '';

        // Related genres — clickable pills that reload the modal
        if (data.related_genres && data.related_genres.length) {
            html += `<div class="genre-dive-related">
                <div class="genre-dive-related-label">Related Genres</div>
                ${data.related_genres.map(rg => `
                    <button class="genre-dive-related-pill" onclick="document.getElementById('genre-deep-dive-modal').remove();openGenreDeepDive('${_esc(rg.genre)}')">${_esc(rg.genre)}</button>
                `).join('')}
            </div>`;
        }

        // Artists section — clickable, navigates to artist page
        // Uses library_id for in-library artists (source-agnostic), falls back to search by name
        if (data.artists && data.artists.length) {
            html += `<div class="genre-dive-section">
                <h3 class="genre-dive-section-title"><span class="genre-dive-icon">🎤</span> Artists in ${_esc(genre)}</h3>
                <div class="genre-dive-artists">
                    ${data.artists.map(a => {
                // Always open on Artists page with discography — pass source for correct routing
                const imgUrl = _esc(a.image_url || '');
                const artSource = _esc(a.source || '');
                const detailHref = a.entity_id ? buildArtistDetailPath(a.entity_id, a.source || null) : '#';
                const srcClass = (a.source || '').toLowerCase();
                return `<a class="genre-dive-artist" href="${detailHref}" onclick="document.getElementById('genre-deep-dive-modal').remove()" style="text-decoration:none;color:inherit;">
                            <div class="genre-dive-artist-img" style="${a.image_url ? `background-image:url('${_esc(a.image_url)}')` : ''}">
                                ${!a.image_url ? '<span>🎤</span>' : ''}
                            </div>
                            <span class="genre-dive-src-dot genre-dive-src-${srcClass}"></span>
                            <div class="genre-dive-artist-name">${_esc(a.name)}</div>
                            ${a.followers ? `<div class="genre-dive-artist-meta">${_fmtNum(a.followers)} followers</div>` : ''}
                            ${a.library_id ? '<div class="genre-dive-artist-badge">In Library</div>' : ''}
                        </a>`;
            }).join('')}
                </div>
            </div>`;
        }

        // Tracks section — clickable, opens album download
        if (data.tracks && data.tracks.length) {
            _cacheDiscoverData['genre_dive_tracks'] = data.tracks;
            html += `<div class="genre-dive-section">
                <h3 class="genre-dive-section-title"><span class="genre-dive-icon">🎵</span> Popular Tracks</h3>
                <div class="genre-dive-tracks">
                    ${data.tracks.map((t, i) => {
                const tSrcClass = (t.source || '').toLowerCase();
                return `
                        <div class="genre-dive-track" onclick="document.getElementById('genre-deep-dive-modal').remove();openCacheDiscoverAlbum('genre_dive_tracks',${i})">
                            <div class="genre-dive-track-num">${i + 1}</div>
                            <div class="genre-dive-track-img" style="${t.image_url ? `background-image:url('${_esc(t.image_url)}')` : ''}">
                                ${!t.image_url ? '🎵' : ''}
                            </div>
                            <div class="genre-dive-track-info">
                                <div class="genre-dive-track-name">${_esc(t.name)}</div>
                                <div class="genre-dive-track-artist">${_esc(t.artist_name)}${t.album_name ? ' · ' + _esc(t.album_name) : ''}</div>
                            </div>
                            <span class="genre-dive-src-dot genre-dive-src-${tSrcClass}" style="flex-shrink:0"></span>
                            <div class="genre-dive-track-duration">${_fmtDur(t.duration_ms)}</div>
                        </div>
                    `}).join('')}
                </div>
            </div>`;
        }

        // Albums section
        if (data.albums && data.albums.length) {
            _cacheDiscoverData['genre_dive_albums'] = data.albums;
            html += `<div class="genre-dive-section">
                <h3 class="genre-dive-section-title"><span class="genre-dive-icon">💿</span> Albums</h3>
                <div class="discover-carousel">${data.albums.map((a, i) => _cacheDiscoverCard(a, 'album', 'genre_dive_albums', i)).join('')}</div>
            </div>`;
        }

        if (!html) {
            html = '<div class="genre-dive-empty"><div class="genre-dive-empty-icon">🔍</div><p>No cached data found for this genre yet</p><p class="genre-dive-empty-hint">Search for artists in this genre to build up the cache</p></div>';
        }

        body.innerHTML = html;
    } catch (e) {
        const body = document.getElementById('genre-dive-body');
        if (body) body.innerHTML = '<div style="color:rgba(255,100,100,0.6);text-align:center;padding:40px;">Failed to load genre data</div>';
    }
}

// ===============================
// BUILD A PLAYLIST FEATURE
// ===============================

let buildPlaylistSearchTimeout = null;

async function searchBuildPlaylistArtists() {
    const searchInput = document.getElementById('build-playlist-search');
    const resultsContainer = document.getElementById('build-playlist-search-results');
    const spinner = document.getElementById('bp-search-spinner');
    const query = searchInput.value.trim();

    if (!query) {
        resultsContainer.innerHTML = '';
        resultsContainer.style.display = 'none';
        if (spinner) spinner.style.display = 'none';
        return;
    }

    // Debounce search
    clearTimeout(buildPlaylistSearchTimeout);
    buildPlaylistSearchTimeout = setTimeout(async () => {
        if (spinner) spinner.style.display = 'flex';
        try {
            const response = await fetch(`/api/discover/build-playlist/search-artists?query=${encodeURIComponent(query)}`);
            const data = await response.json();
            if (!response.ok) {
                showToast(data.error || 'Search failed', 'error');
                return;
            }
            if (!data.success || !data.artists || data.artists.length === 0) {
                resultsContainer.innerHTML = '<div class="build-playlist-no-results">No artists found for "' + query.replace(/</g, '&lt;') + '"</div>';
                resultsContainer.style.display = 'block';
                return;
            }

            // Filter out already-selected artists
            const selectedIds = new Set(buildPlaylistSelectedArtists.map(a => a.id));
            const filtered = data.artists.filter(a => !selectedIds.has(a.id));

            if (filtered.length === 0) {
                resultsContainer.innerHTML = '<div class="build-playlist-no-results">All results already selected</div>';
                resultsContainer.style.display = 'block';
                return;
            }

            // Render search results
            let html = '';
            filtered.forEach(artist => {
                const imageUrl = artist.image_url || '/static/placeholder-album.png';
                const escapedName = artist.name.replace(/'/g, "\\'").replace(/"/g, '&quot;');
                html += `
                    <div class="build-playlist-search-result" onclick="addBuildPlaylistArtist('${artist.id}', '${escapedName}', '${imageUrl}')">
                        <img src="${imageUrl}" alt="${artist.name}" loading="lazy" onerror="this.src='/static/placeholder-album.png'">
                        <span class="bp-result-name">${artist.name}</span>
                        <span class="bp-result-add">+ Add</span>
                    </div>
                `;
            });

            resultsContainer.innerHTML = html;
            resultsContainer.style.display = 'block';

        } catch (error) {
            console.error('Error searching artists:', error);
        } finally {
            if (spinner) spinner.style.display = 'none';
        }
    }, 400);
}

function addBuildPlaylistArtist(artistId, artistName, imageUrl) {
    if (buildPlaylistSelectedArtists.some(a => a.id === artistId)) {
        showToast('Artist already selected', 'warning');
        return;
    }
    if (buildPlaylistSelectedArtists.length >= 5) {
        showToast('Maximum 5 seed artists', 'warning');
        return;
    }

    buildPlaylistSelectedArtists.push({
        id: artistId,
        name: artistName,
        image_url: imageUrl
    });

    renderBuildPlaylistSelectedArtists();

    // Clear search
    document.getElementById('build-playlist-search').value = '';
    document.getElementById('build-playlist-search-results').innerHTML = '';
    document.getElementById('build-playlist-search-results').style.display = 'none';
}

function removeBuildPlaylistArtist(artistId) {
    buildPlaylistSelectedArtists = buildPlaylistSelectedArtists.filter(a => a.id !== artistId);
    renderBuildPlaylistSelectedArtists();
}

function renderBuildPlaylistSelectedArtists() {
    const container = document.getElementById('build-playlist-selected-artists');
    const generateBtn = document.getElementById('build-playlist-generate-btn');
    const counter = document.getElementById('bp-selected-counter');
    const count = buildPlaylistSelectedArtists.length;

    if (counter) counter.textContent = `${count} / 5`;

    if (count === 0) {
        container.innerHTML = `
            <div class="build-playlist-no-selection">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width: 32px; height: 32px; opacity: 0.4; margin-bottom: 8px;"><path d="M12 4.5v15m7.5-7.5h-15"/></svg>
                <span>Search above to add seed artists</span>
            </div>`;
        generateBtn.disabled = true;
        return;
    }

    let html = '';
    buildPlaylistSelectedArtists.forEach(artist => {
        const escapedId = artist.id.replace(/'/g, "\\'");
        html += `
            <div class="build-playlist-selected-artist">
                <img src="${artist.image_url || '/static/placeholder-album.png'}" alt="${artist.name}" loading="lazy" onerror="this.src='/static/placeholder-album.png'">
                <span>${artist.name}</span>
                <button onclick="removeBuildPlaylistArtist('${escapedId}')" class="build-playlist-remove-artist" title="Remove">×</button>
            </div>
        `;
    });

    container.innerHTML = html;
    generateBtn.disabled = false;
}

let buildPlaylistTracks = [];

async function generateBuildPlaylist() {
    if (buildPlaylistSelectedArtists.length === 0) {
        showToast('Please select at least 1 artist', 'warning');
        return;
    }

    const generateBtn = document.getElementById('build-playlist-generate-btn');
    const resultsContainer = document.getElementById('build-playlist-results');
    const resultsWrapper = document.getElementById('build-playlist-results-wrapper');
    const loadingIndicator = document.getElementById('build-playlist-loading');
    const metadataDisplay = document.getElementById('build-playlist-metadata-display');
    const titleEl = document.getElementById('build-playlist-results-title');
    const subtitleEl = document.getElementById('build-playlist-results-subtitle');

    // Show loading, hide search area
    generateBtn.disabled = true;
    loadingIndicator.style.display = 'flex';
    resultsWrapper.style.display = 'none';
    resultsContainer.innerHTML = '';

    try {
        const seedIds = buildPlaylistSelectedArtists.map(a => a.id);
        const response = await fetch('/api/discover/build-playlist/generate', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                seed_artist_ids: seedIds,
                playlist_size: 50
            })
        });

        const data = await response.json();
        if (!response.ok || !data.success) {
            throw new Error(data.error || 'Failed to generate playlist');
        }
        if (!data.playlist || !data.playlist.tracks || data.playlist.tracks.length === 0) {
            throw new Error(data.playlist?.error || 'No tracks found. Try different seed artists.');
        }

        // Store tracks globally
        buildPlaylistTracks = data.playlist.tracks;

        // Update title and subtitle
        const artistNames = buildPlaylistSelectedArtists.map(a => a.name).join(', ');
        titleEl.textContent = 'Custom Playlist';
        subtitleEl.textContent = `Based on: ${artistNames}`;

        // Render metadata
        const metadata = data.playlist.metadata;
        metadataDisplay.innerHTML = `
            <div class="build-playlist-metadata">
                <div class="bp-meta-stat">
                    <span class="bp-meta-value">${metadata.total_tracks}</span>
                    <span class="bp-meta-label">Tracks</span>
                </div>
                <div class="bp-meta-stat">
                    <span class="bp-meta-value">${metadata.similar_artists_count}</span>
                    <span class="bp-meta-label">Similar Artists</span>
                </div>
                <div class="bp-meta-stat">
                    <span class="bp-meta-value">${metadata.albums_count}</span>
                    <span class="bp-meta-label">Albums Sampled</span>
                </div>
            </div>
        `;

        // Render playlist
        renderCompactPlaylist(resultsContainer, data.playlist.tracks);

        // Show results wrapper
        resultsWrapper.style.display = 'block';

    } catch (error) {
        console.error('Error generating playlist:', error);
        resultsWrapper.style.display = 'none';
        showToast(error.message || 'Failed to generate playlist', 'error');
    } finally {
        loadingIndicator.style.display = 'none';
        generateBtn.disabled = false;
    }
}

async function openDownloadModalForBuildPlaylist() {
    if (!buildPlaylistTracks || buildPlaylistTracks.length === 0) {
        showToast('No playlist tracks available', 'warning');
        return;
    }

    const artistNames = buildPlaylistSelectedArtists.map(a => a.name).join(', ');
    const playlistName = `Custom Playlist - ${artistNames}`;
    const virtualPlaylistId = 'build_playlist_custom';

    // Open download modal
    await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, buildPlaylistTracks);
}

function openDailyMix(mixIndex) {
    const mix = personalizedDailyMixes[mixIndex];
    if (!mix || !mix.tracks) return;

    // TODO: Open modal or dedicated view for Daily Mix
    console.log('Opening Daily Mix:', mix.name);
}

// ===============================
// DISCOVER PLAYLIST ACTIONS
// ===============================

async function openDownloadModalForDiscoverPlaylist(playlistType, playlistName) {
    console.log(`📥 Opening Download Missing Tracks modal for ${playlistName}`);

    try {
        // Get tracks based on playlist type
        let tracks = [];
        if (playlistType === 'release_radar') {
            tracks = discoverReleaseRadarTracks;
        } else if (playlistType === 'discovery_weekly') {
            tracks = discoverWeeklyTracks;
        } else if (playlistType === 'seasonal_playlist') {
            tracks = discoverSeasonalTracks;
        } else if (playlistType === 'popular_picks') {
            tracks = personalizedPopularPicks;
        } else if (playlistType === 'hidden_gems') {
            tracks = personalizedHiddenGems;
        } else if (playlistType === 'discovery_shuffle') {
            tracks = personalizedDiscoveryShuffle;
        } else if (playlistType === 'build_playlist') {
            tracks = buildPlaylistTracks;
        }

        if (!tracks || tracks.length === 0) {
            showToast(`No tracks available for ${playlistName}`, 'warning');
            return;
        }

        // Convert discover tracks to format expected by download modal
        const spotifyTracks = tracks.map(track => {
            let spotifyTrack;

            // Use track_data_json if available, otherwise construct from track data
            if (track.track_data_json) {
                spotifyTrack = track.track_data_json;
            } else {
                // Fallback: construct track object from available data
                spotifyTrack = {
                    id: track.spotify_track_id,
                    name: track.track_name,
                    artists: [{ name: track.artist_name }],
                    album: {
                        name: track.album_name,
                        images: track.album_cover_url ? [{ url: track.album_cover_url }] : []
                    },
                    duration_ms: track.duration_ms || 0
                };
            }

            // Normalize artists to array of strings for modal compatibility
            if (spotifyTrack.artists && Array.isArray(spotifyTrack.artists)) {
                spotifyTrack.artists = spotifyTrack.artists.map(a => a.name || a);
            }

            return spotifyTrack;
        });

        // Create virtual playlist ID
        const virtualPlaylistId = `discover_${playlistType}`;

        // Use existing modal system (same as YouTube/Tidal playlists)
        await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, spotifyTracks);

    } catch (error) {
        console.error('Error opening download modal for discover playlist:', error);
        showToast(`Failed to open download modal: ${error.message}`, 'error');
        hideLoadingOverlay();  // Ensure overlay is hidden on error
    }
}

function updateDiscoverDownloadButton(playlistType, state) {
    /**
     * Update the download button appearance based on download state
     * @param {string} playlistType - 'release_radar' or 'discovery_weekly'
     * @param {string} state - 'idle', 'downloading', or 'complete'
     */
    const buttonId = `${playlistType}-download-btn`;
    const button = document.getElementById(buttonId);

    if (!button) return;

    const icon = button.querySelector('.button-icon');
    const text = button.querySelector('.button-text');

    if (state === 'downloading') {
        if (icon) icon.textContent = '⏳';
        if (text) text.textContent = 'View Progress';
        button.title = 'View download progress';
    } else {
        if (icon) icon.textContent = '↓';
        if (text) text.textContent = 'Download';
        button.title = 'Download missing tracks';
    }
}

function checkForActiveDiscoverDownloads() {
    /**
     * Check for active download processes and update button states
     * Only runs if discover page is actually loaded in the DOM
     */
    // Check if discover page is loaded by looking for a discover-specific element
    const discoverPage = document.getElementById('release-radar-download-btn') ||
        document.getElementById('discovery-weekly-download-btn');

    if (!discoverPage) return;

    const discoverPlaylists = [
        { id: 'discover_release_radar', type: 'release_radar' },
        { id: 'discover_discovery_weekly', type: 'discovery_weekly' }
    ];

    discoverPlaylists.forEach(({ id, type }) => {
        if (activeDownloadProcesses[id]) {
            const process = activeDownloadProcesses[id];
            if (process.status === 'running' || process.status === 'idle') {
                updateDiscoverDownloadButton(type, 'downloading');
            }
        }
    });
}

async function startDiscoverPlaylistSync(playlistType, playlistName) {
    console.log(`🔄 Starting sync for ${playlistName}`);

    // Get tracks based on playlist type
    let tracks = [];
    if (playlistType === 'release_radar') {
        tracks = discoverReleaseRadarTracks;
    } else if (playlistType === 'discovery_weekly') {
        tracks = discoverWeeklyTracks;
    } else if (playlistType === 'seasonal_playlist') {
        tracks = discoverSeasonalTracks;
    } else if (playlistType === 'popular_picks') {
        tracks = personalizedPopularPicks;
    } else if (playlistType === 'hidden_gems') {
        tracks = personalizedHiddenGems;
    } else if (playlistType === 'discovery_shuffle') {
        tracks = personalizedDiscoveryShuffle;
    } else if (playlistType === 'build_playlist') {
        tracks = buildPlaylistTracks;
    }

    if (!tracks || tracks.length === 0) {
        showToast(`No tracks available for ${playlistName}`, 'warning');
        return;
    }

    // Convert to format expected by sync API
    const spotifyTracks = tracks.map(track => {
        let spotifyTrack;

        // Use track_data_json if available
        if (track.track_data_json) {
            spotifyTrack = track.track_data_json;
        } else {
            // Fallback: construct track object
            spotifyTrack = {
                id: track.spotify_track_id,
                name: track.track_name,
                artists: [{ name: track.artist_name }],
                album: {
                    name: track.album_name,
                    images: track.album_cover_url ? [{ url: track.album_cover_url }] : []
                },
                duration_ms: track.duration_ms || 0
            };
        }

        // Normalize artists to array of strings for sync compatibility
        if (spotifyTrack.artists && Array.isArray(spotifyTrack.artists)) {
            spotifyTrack.artists = spotifyTrack.artists.map(a => a.name || a);
        }

        return spotifyTrack;
    });

    // Create virtual playlist ID
    const virtualPlaylistId = `discover_${playlistType}`;

    // Store in cache for sync function
    playlistTrackCache[virtualPlaylistId] = spotifyTracks;

    // Create virtual playlist object
    const virtualPlaylist = {
        id: virtualPlaylistId,
        name: playlistName,
        track_count: spotifyTracks.length
    };

    // Add to spotify playlists array if not already there
    if (!spotifyPlaylists.find(p => p.id === virtualPlaylistId)) {
        spotifyPlaylists.push(virtualPlaylist);
    }

    // Show sync status display (convert underscores to hyphens for ID)
    const statusId = playlistType.replace(/_/g, '-') + '-sync-status';
    const statusDisplay = document.getElementById(statusId);
    if (statusDisplay) {
        statusDisplay.style.display = 'block';
    }

    // Disable sync button to prevent duplicate syncs (convert underscores to hyphens for ID)
    const buttonId = playlistType.replace(/_/g, '-') + '-sync-btn';
    const syncButton = document.getElementById(buttonId);
    if (syncButton) {
        syncButton.disabled = true;
        syncButton.style.opacity = '0.5';
        syncButton.style.cursor = 'not-allowed';
    }

    // Start sync using existing function
    await startPlaylistSync(virtualPlaylistId);

    // Extract image URL from first track for download bar bubble
    let imageUrl = null;
    if (spotifyTracks && spotifyTracks.length > 0) {
        const firstTrack = spotifyTracks[0];
        if (firstTrack.album && firstTrack.album.images && firstTrack.album.images.length > 0) {
            imageUrl = firstTrack.album.images[0].url;
        }
    }

    // Add to discover download bar
    addDiscoverDownload(virtualPlaylistId, playlistName, playlistType, imageUrl);

    // Start polling for progress updates
    startDiscoverSyncPolling(playlistType, virtualPlaylistId);
}

// Track active discover sync pollers
const discoverSyncPollers = {};

function startDiscoverSyncPolling(playlistType, virtualPlaylistId) {
    // Stop any existing poller for this playlist type
    if (discoverSyncPollers[playlistType]) {
        clearInterval(discoverSyncPollers[playlistType]);
    }

    console.log(`🔄 Starting sync polling for ${playlistType} (${virtualPlaylistId})`);

    // Phase 5: Subscribe via WebSocket
    if (socketConnected) {
        socket.emit('sync:subscribe', { playlist_ids: [virtualPlaylistId] });
        _syncProgressCallbacks[virtualPlaylistId] = (data) => {
            const prefix = playlistType.replace(/_/g, '-');
            const progress = data.progress || {};
            const total = progress.total_tracks || 0;
            const matched = progress.matched_tracks || 0;
            const failed = progress.failed_tracks || 0;
            const processed = matched + failed;
            const pending = total - processed;
            const pct = total > 0 ? Math.round((processed / total) * 100) : 0;
            const el = (id) => document.getElementById(id);
            if (el(`${prefix}-sync-completed`)) el(`${prefix}-sync-completed`).textContent = matched;
            if (el(`${prefix}-sync-pending`)) el(`${prefix}-sync-pending`).textContent = pending;
            if (el(`${prefix}-sync-failed`)) el(`${prefix}-sync-failed`).textContent = failed;
            if (el(`${prefix}-sync-percentage`)) el(`${prefix}-sync-percentage`).textContent = pct;
            if (data.status === 'finished') {
                if (discoverSyncPollers[playlistType]) { clearInterval(discoverSyncPollers[playlistType]); delete discoverSyncPollers[playlistType]; }
                socket.emit('sync:unsubscribe', { playlist_ids: [virtualPlaylistId] });
                delete _syncProgressCallbacks[virtualPlaylistId];
                const buttonId = playlistType.replace(/_/g, '-') + '-sync-btn';
                const syncButton = el(buttonId);
                if (syncButton) { syncButton.disabled = false; syncButton.style.opacity = '1'; syncButton.style.cursor = 'pointer'; }
                const playlistNames = {
                    'release_radar': 'Fresh Tape', 'discovery_weekly': 'The Archives',
                    'seasonal_playlist': 'Seasonal Mix', 'popular_picks': 'Popular Picks',
                    'hidden_gems': 'Hidden Gems', 'discovery_shuffle': 'Discovery Shuffle',
                    'build_playlist': 'Custom Playlist'
                };
                showToast(`${playlistNames[playlistType] || playlistType} sync complete!`, 'success');
                setTimeout(() => { const sd = el(`${prefix}-sync-status`); if (sd) sd.style.display = 'none'; }, 3000);
            }
        };
    }

    // Poll every 500ms for progress updates
    discoverSyncPollers[playlistType] = setInterval(async () => {
        // Always poll — no dedicated WebSocket events for discovery progress
        try {
            const response = await fetch(`/api/sync/status/${virtualPlaylistId}`);
            if (!response.ok) {
                console.log(`⚠️ Sync status response not OK: ${response.status}`);
                return;
            }

            const data = await response.json();
            console.log(`📊 Sync status for ${playlistType}:`, data);

            // Update UI with progress (data structure: {status: ..., progress: {...}})
            // Convert underscores to hyphens for HTML IDs
            const prefix = playlistType.replace(/_/g, '-');
            const progress = data.progress || {};

            const completedEl = document.getElementById(`${prefix}-sync-completed`);
            const pendingEl = document.getElementById(`${prefix}-sync-pending`);
            const failedEl = document.getElementById(`${prefix}-sync-failed`);
            const percentageEl = document.getElementById(`${prefix}-sync-percentage`);

            const total = progress.total_tracks || 0;
            const matched = progress.matched_tracks || 0;
            const failed = progress.failed_tracks || 0;
            const processed = matched + failed;
            const pending = total - processed;
            const completionPercentage = total > 0 ? Math.round((processed / total) * 100) : 0;

            if (completedEl) completedEl.textContent = matched;
            if (pendingEl) pendingEl.textContent = pending;
            if (failedEl) failedEl.textContent = failed;
            if (percentageEl) percentageEl.textContent = completionPercentage;

            // If complete, stop polling and hide status after delay
            if (data.status === 'finished') {
                console.log(`✅ Sync complete for ${playlistType}`);
                clearInterval(discoverSyncPollers[playlistType]);
                delete discoverSyncPollers[playlistType];

                // Re-enable sync button
                const buttonId = playlistType.replace(/_/g, '-') + '-sync-btn';
                const syncButton = document.getElementById(buttonId);
                if (syncButton) {
                    syncButton.disabled = false;
                    syncButton.style.opacity = '1';
                    syncButton.style.cursor = 'pointer';
                }

                // Show completion toast with playlist name
                const playlistNames = {
                    'release_radar': 'Fresh Tape',
                    'discovery_weekly': 'The Archives',
                    'seasonal_playlist': 'Seasonal Mix',
                    'popular_picks': 'Popular Picks',
                    'hidden_gems': 'Hidden Gems',
                    'discovery_shuffle': 'Discovery Shuffle',
                    'build_playlist': 'Custom Playlist'
                };
                const displayName = playlistNames[playlistType] || playlistType;
                showToast(`${displayName} sync complete!`, 'success');

                // Hide status display after 3 seconds
                setTimeout(() => {
                    const statusDisplay = document.getElementById(`${prefix}-sync-status`);
                    if (statusDisplay) {
                        statusDisplay.style.display = 'none';
                    }
                }, 3000);
            }

        } catch (error) {
            console.error(`❌ Error polling sync status for ${playlistType}:`, error);
        }
    }, 500);
}

async function openDownloadModalForRecentAlbum(albumIndex) {
    const album = discoverRecentAlbums[albumIndex];
    if (!album) {
        showToast('Album data not found', 'error');
        return;
    }

    console.log(`📥 Opening Download Missing Tracks modal for album: ${album.album_name}`);
    showLoadingOverlay(`Loading tracks for ${album.album_name}...`);

    try {
        // Determine source and album ID - use source-agnostic endpoint
        const source = album.source || (album.album_spotify_id ? 'spotify' : album.album_deezer_id ? 'deezer' : 'itunes');
        const albumId = source === 'spotify' ? album.album_spotify_id : source === 'deezer' ? album.album_deezer_id : album.album_itunes_id;

        if (!albumId) {
            throw new Error(`No ${source} album ID available`);
        }

        // Fetch album tracks from appropriate source (pass name/artist for Hydrabase support)
        const _dap2 = new URLSearchParams({ name: album.album_name || '', artist: album.artist_name || '' });
        const response = await fetch(`/api/discover/album/${source}/${albumId}?${_dap2}`);
        if (!response.ok) {
            throw new Error('Failed to fetch album tracks');
        }

        const albumData = await response.json();
        if (!albumData.tracks || albumData.tracks.length === 0) {
            throw new Error('No tracks found in album');
        }

        // Convert to expected format - CRITICAL FIX: Use fresh albumData from Spotify, not cached album
        const spotifyTracks = albumData.tracks.map(track => {
            // Normalize artists to array of strings
            let artists = track.artists || albumData.artists || [{ name: album.artist_name }];
            if (Array.isArray(artists)) {
                artists = artists.map(a => a.name || a);
            }

            return {
                id: track.id,
                name: track.name,
                artists: artists,
                album: {
                    id: albumData.id,                              // ✅ Album ID for proper tracking
                    name: albumData.name,                          // ✅ Use fresh data, not cached
                    album_type: albumData.album_type || 'album',   // ✅ Critical: Album type for classification
                    total_tracks: albumData.total_tracks || 0,     // ✅ Total tracks for context
                    release_date: albumData.release_date || '',    // ✅ Release date
                    images: albumData.images || []                 // ✅ Use Spotify images
                },
                duration_ms: track.duration_ms || 0,
                track_number: track.track_number || 0
            };
        });

        // Create virtual playlist ID using the appropriate album ID
        const virtualPlaylistId = `discover_album_${albumId}`;

        // CRITICAL FIX: Pass proper artist/album context for modal display
        const artistContext = _buildDiscoverArtistContext(source, album.artist_name, album, albumData);

        const albumContext = {
            id: albumData.id,
            name: albumData.name,
            album_type: albumData.album_type || 'album',
            total_tracks: albumData.total_tracks || 0,
            release_date: albumData.release_date || '',
            images: albumData.images || []
        };

        // Open download modal with artist/album context
        await openDownloadMissingModalForYouTube(virtualPlaylistId, albumData.name, spotifyTracks, artistContext, albumContext);

        hideLoadingOverlay();

    } catch (error) {
        console.error('Error opening album download modal:', error);
        showToast(`Failed to load album: ${error.message}`, 'error');
        hideLoadingOverlay();
    }
}

// ===============================
// DISCOVER DOWNLOAD BAR
// ===============================

// Track discover page downloads
let discoverDownloads = {}; // playlistId -> { name, type, status, virtualPlaylistId, startTime }

/**
 * Add a download to the discover download bar
 */
function addDiscoverDownload(playlistId, playlistName, playlistType, imageUrl = null) {
    console.log(`📥 [DOWNLOAD SIDEBAR] Adding discover download: ${playlistName} (${playlistId}) type: ${playlistType}, image: ${imageUrl}`);

    // Always register the download in state (needed for dashboard even when not on discover page)
    discoverDownloads[playlistId] = {
        name: playlistName,
        type: playlistType,
        status: 'in_progress',
        virtualPlaylistId: playlistId,
        imageUrl: imageUrl,
        startTime: new Date()
    };

    console.log(`📊 [DOWNLOAD SIDEBAR] Active downloads:`, Object.keys(discoverDownloads));

    // Update discover page sidebar if it exists (user is on discover page)
    const downloadSidebar = document.getElementById('discover-download-sidebar');
    if (downloadSidebar) {
        updateDiscoverDownloadBar(); // Also saves snapshot internally
    } else {
        console.log('ℹ️ [DOWNLOAD SIDEBAR] Sidebar not present - skipping sidebar UI update');
        saveDiscoverDownloadSnapshot(); // Persist state even when sidebar is absent
    }

    updateDashboardDownloads();
    monitorDiscoverDownload(playlistId);
}

/**
 * Monitor a discover download for completion
 */
function monitorDiscoverDownload(playlistId) {
    let notFoundCount = 0;
    const maxNotFoundAttempts = 5; // Give sync 10 seconds to start (5 checks * 2 seconds)

    // Phase 5: Subscribe via WebSocket for sync status updates
    if (socketConnected) {
        socket.emit('sync:subscribe', { playlist_ids: [playlistId] });
        _syncProgressCallbacks[playlistId] = (data) => {
            if (!discoverDownloads[playlistId]) return;
            if (data.status === 'complete' || data.status === 'finished') {
                discoverDownloads[playlistId].status = 'completed';
                updateDiscoverDownloadBar();
                updateDashboardDownloads();
                socket.emit('sync:unsubscribe', { playlist_ids: [playlistId] });
                delete _syncProgressCallbacks[playlistId];
                setTimeout(() => {
                    if (discoverDownloads[playlistId] && discoverDownloads[playlistId].status === 'completed') {
                        removeDiscoverDownload(playlistId);
                    }
                }, 30000);
            }
        };
    }

    const checkInterval = setInterval(async () => {
        try {
            // Check if download still exists
            if (!discoverDownloads[playlistId]) {
                clearInterval(checkInterval);
                if (_syncProgressCallbacks[playlistId]) {
                    if (socketConnected) socket.emit('sync:unsubscribe', { playlist_ids: [playlistId] });
                    delete _syncProgressCallbacks[playlistId];
                }
                return;
            }

            // First check if there's an active download process (modal-based downloads)
            const activeProcess = activeDownloadProcesses[playlistId];
            if (activeProcess) {
                console.log(`📂 [DOWNLOAD BAR] Found active process for ${playlistId}, status: ${activeProcess.status}`);

                if (activeProcess.status === 'complete') {
                    console.log(`✅ [DOWNLOAD BAR] Process completed: ${discoverDownloads[playlistId].name}`);
                    discoverDownloads[playlistId].status = 'completed';
                    updateDiscoverDownloadBar();
                    updateDashboardDownloads();
                    clearInterval(checkInterval);

                    // Auto-remove completed downloads after 30 seconds
                    setTimeout(() => {
                        if (discoverDownloads[playlistId] && discoverDownloads[playlistId].status === 'completed') {
                            removeDiscoverDownload(playlistId);
                        }
                    }, 30000);
                }
                return; // Continue monitoring
            }

            // Check sync status API (for sync-based downloads)
            if (socketConnected) return; // Phase 5: WS handles sync status
            const response = await fetch(`/api/sync/status/${playlistId}`);
            if (response.ok) {
                const data = await response.json();
                notFoundCount = 0; // Reset counter if found

                console.log(`🔄 [DOWNLOAD BAR] Sync status for ${playlistId}: ${data.status}`);

                if (data.status === 'complete') {
                    console.log(`✅ [DOWNLOAD BAR] Sync completed: ${discoverDownloads[playlistId].name}`);
                    discoverDownloads[playlistId].status = 'completed';
                    updateDiscoverDownloadBar();
                    updateDashboardDownloads();
                    clearInterval(checkInterval);

                    // Auto-remove completed downloads after 30 seconds
                    setTimeout(() => {
                        if (discoverDownloads[playlistId] && discoverDownloads[playlistId].status === 'completed') {
                            removeDiscoverDownload(playlistId);
                        }
                    }, 30000);
                }
            } else if (response.status === 404) {
                notFoundCount++;
                console.log(`🔍 [DOWNLOAD BAR] Sync not found for ${playlistId} (attempt ${notFoundCount}/${maxNotFoundAttempts})`);

                // Only remove after multiple attempts (give it time to start)
                if (notFoundCount >= maxNotFoundAttempts) {
                    console.log(`⏹️ [DOWNLOAD BAR] Sync not found after ${maxNotFoundAttempts} attempts, removing`);
                    clearInterval(checkInterval);
                    removeDiscoverDownload(playlistId);
                }
            }
        } catch (error) {
            console.error(`❌ [DOWNLOAD BAR] Error monitoring ${playlistId}:`, error);
        }
    }, 2000); // Check every 2 seconds
}

/**
 * Remove a download from the bar
 */
function removeDiscoverDownload(playlistId) {
    console.log(`🗑️ Removing discover download: ${playlistId}`);
    delete discoverDownloads[playlistId];
    updateDiscoverDownloadBar();
    updateDashboardDownloads();
    saveDiscoverDownloadSnapshot(); // Save state after removal
}

/**
 * Update the discover download sidebar UI
 */
function updateDiscoverDownloadBar() {
    const downloadSidebar = document.getElementById('discover-download-sidebar');
    const bubblesContainer = document.getElementById('discover-download-bubbles');
    const countElement = document.getElementById('discover-download-count');

    console.log(`🔄 [DOWNLOAD SIDEBAR] Updating sidebar - found elements:`, {
        downloadSidebar: !!downloadSidebar,
        bubblesContainer: !!bubblesContainer,
        countElement: !!countElement
    });

    if (!downloadSidebar || !bubblesContainer || !countElement) {
        console.warn('⚠️ [DOWNLOAD SIDEBAR] Missing elements, cannot update');
        return;
    }

    const activeDownloads = Object.keys(discoverDownloads);
    const count = activeDownloads.length;

    console.log(`📊 [DOWNLOAD SIDEBAR] Updating with ${count} active downloads`);

    // Update count
    countElement.textContent = count;

    // Show/hide sidebar
    if (count === 0) {
        console.log(`👁️ [DOWNLOAD SIDEBAR] No downloads, hiding sidebar`);
        downloadSidebar.classList.add('hidden');
        return;
    } else {
        console.log(`👁️ [DOWNLOAD SIDEBAR] ${count} downloads, showing sidebar`);
        downloadSidebar.classList.remove('hidden');
    }

    // Update bubbles
    bubblesContainer.innerHTML = activeDownloads.map(playlistId => {
        const download = discoverDownloads[playlistId];
        const isCompleted = download.status === 'completed';
        const icon = isCompleted ? '✅' : '⏳';

        // Use image if available, otherwise gradient background
        const imageUrl = download.imageUrl || '';
        const backgroundStyle = imageUrl ?
            `background-image: url('${imageUrl}');` :
            `background: linear-gradient(135deg, rgba(29, 185, 84, 0.3) 0%, rgba(24, 156, 71, 0.2) 100%);`;

        return `
            <div class="discover-download-bubble">
                <div class="discover-download-bubble-card ${isCompleted ? 'completed' : ''}"
                     onclick="openDiscoverDownloadModal('${playlistId}')"
                     title="${escapeHtml(download.name)} - Click to view">
                    <div class="discover-download-bubble-image" style="${backgroundStyle}"></div>
                    <div class="discover-download-bubble-overlay"></div>
                    <div class="discover-download-bubble-content">
                        <span class="discover-download-bubble-icon">${icon}</span>
                    </div>
                </div>
                <div class="discover-download-bubble-name">${escapeHtml(download.name)}</div>
            </div>
        `;
    }).join('');

    console.log(`📊 Updated discover download sidebar: ${count} active downloads`);

    // Save snapshot after UI update
    saveDiscoverDownloadSnapshot();
}

/**
 * Open download modal for a discover playlist
 */
async function openDiscoverDownloadModal(playlistId) {
    console.log(`📂 [DOWNLOAD BAR] Opening download modal for: ${playlistId}`);

    // Check if there's an active download process with modal
    let process = activeDownloadProcesses[playlistId];

    console.log(`📋 [DOWNLOAD BAR] Process found:`, {
        exists: !!process,
        hasModalElement: !!(process && process.modalElement),
        hasModalId: !!(process && process.modalId)
    });

    if (process) {
        // Try modalElement first (album downloads)
        if (process.modalElement) {
            console.log(`✅ [DOWNLOAD BAR] Opening modal via modalElement`);
            process.modalElement.style.display = 'flex';
            return;
        }

        // Try modalId (sync downloads)
        if (process.modalId) {
            const modal = document.getElementById(process.modalId);
            if (modal) {
                console.log(`✅ [DOWNLOAD BAR] Opening modal via modalId: ${process.modalId}`);
                modal.style.display = 'flex';
                return;
            }
        }
    }

    // If no process found, try to rehydrate from backend
    console.log(`💧 [DOWNLOAD BAR] No modal found, attempting to rehydrate from backend...`);
    const rehydrated = await rehydrateDiscoverDownloadModal(playlistId);

    if (rehydrated) {
        console.log(`✅ [DOWNLOAD BAR] Successfully rehydrated modal, opening it...`);
        // Try again after rehydration
        process = activeDownloadProcesses[playlistId];
        if (process && process.modalElement) {
            process.modalElement.style.display = 'flex';
            return;
        }
    }

    // Fallback: show toast
    const download = discoverDownloads[playlistId];
    if (download) {
        console.log(`ℹ️ [DOWNLOAD BAR] No modal found after rehydration attempt, showing toast`);
        showToast(`Download: ${download.name} - ${download.status}`, 'info');
    } else {
        console.warn(`⚠️ [DOWNLOAD BAR] No download or process found for: ${playlistId}`);
    }
}

/**
 * Initialize discover download sidebar on page load
 */
function initializeDiscoverDownloadBar() {
    console.log('🎵 Initializing discover download sidebar...');

    // Start with sidebar hidden (will be shown if downloads exist after hydration)
    const downloadSidebar = document.getElementById('discover-download-sidebar');
    if (downloadSidebar) {
        downloadSidebar.classList.add('hidden');
    }
}

// --- Discover Download Modal Rehydration ---

async function rehydrateDiscoverDownloadModal(playlistId) {
    /**
     * Rehydrates a discover download modal from backend process data.
     * Fetches tracks from backend API and recreates the modal (user-requested).
     */
    try {
        console.log(`💧 [REHYDRATE] Attempting to rehydrate modal for: ${playlistId}`);

        // Check if there's an active backend process for this playlist
        const batchResponse = await fetch(`/api/download_status/batch`);
        if (!batchResponse.ok) {
            console.log(`⚠️ [REHYDRATE] Failed to fetch batch info`);
            return false;
        }

        const batchData = await batchResponse.json();
        const batches = batchData.batches || {};

        // Find the batch for this playlist (batches is an object with batch_id keys)
        let batchId = null;
        let batch = null;
        for (const [id, batchStatus] of Object.entries(batches)) {
            if (batchStatus.playlist_id === playlistId) {
                batchId = id;
                batch = batchStatus;
                break;
            }
        }

        if (!batch || !batchId) {
            console.log(`⚠️ [REHYDRATE] No active batch found for ${playlistId}`);
            return false;
        }

        console.log(`✅ [REHYDRATE] Found active batch for ${playlistId}: ${batchId}`, batch);

        // Get the download metadata from discoverDownloads
        const downloadData = discoverDownloads[playlistId];
        if (!downloadData) {
            console.log(`⚠️ [REHYDRATE] No download metadata found for ${playlistId}`);
            return false;
        }

        // Handle album downloads from Recent Releases
        if (playlistId.startsWith('discover_album_')) {
            const albumId = playlistId.replace('discover_album_', '');
            console.log(`💧 [REHYDRATE] Album download - fetching album ${albumId}...`);

            try {
                const albumResponse = await fetch(`/api/spotify/album/${albumId}`);
                if (!albumResponse.ok) {
                    console.error(`❌ [REHYDRATE] Failed to fetch album: ${albumResponse.status}`);
                    return false;
                }

                const albumData = await albumResponse.json();
                if (!albumData.tracks || albumData.tracks.length === 0) {
                    console.error(`❌ [REHYDRATE] No tracks in album`);
                    return false;
                }

                // Convert tracks to expected format
                const spotifyTracks = albumData.tracks.map(track => {
                    let artists = track.artists || [];
                    if (Array.isArray(artists)) {
                        artists = artists.map(a => a.name || a);
                    }

                    return {
                        id: track.id,
                        name: track.name,
                        artists: artists,
                        album: {
                            name: albumData.name || downloadData.name.split(' - ')[0],
                            images: downloadData.imageUrl ? [{ url: downloadData.imageUrl }] : []
                        },
                        duration_ms: track.duration_ms || 0
                    };
                });

                console.log(`✅ [REHYDRATE] Retrieved ${spotifyTracks.length} tracks for album`);

                // Create modal
                await openDownloadMissingModalForYouTube(playlistId, downloadData.name, spotifyTracks);

                // Update process
                const process = activeDownloadProcesses[playlistId];
                if (process) {
                    process.status = 'running';
                    process.batchId = batchId;
                    subscribeToDownloadBatch(batchId);
                    const beginBtn = document.getElementById(`begin-analysis-btn-${playlistId}`);
                    const cancelBtn = document.getElementById(`cancel-all-btn-${playlistId}`);
                    if (beginBtn) beginBtn.style.display = 'none';
                    if (cancelBtn) cancelBtn.style.display = 'inline-block';

                    // Start polling for status updates
                    startModalDownloadPolling(playlistId);
                    console.log(`✅ [REHYDRATE] Successfully rehydrated album modal with polling`);
                    return true;
                }
                return false;

            } catch (error) {
                console.error(`❌ [REHYDRATE] Error fetching album:`, error);
                return false;
            }
        }

        // Determine API endpoint based on playlist ID
        let apiEndpoint;
        if (playlistId === 'discover_release_radar') {
            apiEndpoint = '/api/discover/release-radar';
        } else if (playlistId === 'discover_discovery_weekly') {
            apiEndpoint = '/api/discover/discovery-weekly';
        } else if (playlistId === 'discover_seasonal_playlist') {
            apiEndpoint = '/api/discover/seasonal-playlist';
        } else if (playlistId === 'discover_popular_picks') {
            apiEndpoint = '/api/discover/popular-picks';
        } else if (playlistId === 'discover_hidden_gems') {
            apiEndpoint = '/api/discover/hidden-gems';
        } else if (playlistId === 'discover_discovery_shuffle') {
            apiEndpoint = '/api/discover/discovery-shuffle';
        } else if (playlistId === 'build_playlist_custom') {
            apiEndpoint = '/api/discover/build-playlist';
        } else if (playlistId.startsWith('discover_lb_')) {
            // ListenBrainz playlist - fetch from cache
            const identifier = playlistId.replace('discover_lb_', '');
            const tracks = listenbrainzTracksCache[identifier];
            if (!tracks || tracks.length === 0) {
                console.log(`⚠️ [REHYDRATE] No ListenBrainz tracks in cache for ${identifier}`);
                return false;
            }

            // Convert to Spotify format
            const spotifyTracks = tracks.map(track => ({
                id: track.mbid || `listenbrainz_${track.track_name}_${track.artist_name}`.replace(/[^a-z0-9]/gi, '_'),  // Generate ID if missing
                name: track.track_name,
                artists: [{ name: cleanArtistName(track.artist_name) }], // Proper Spotify format
                album: {
                    name: track.album_name,
                    images: track.album_cover_url ? [{ url: track.album_cover_url }] : []
                },
                duration_ms: track.duration_ms || 0,
                mbid: track.mbid
            }));

            // Create modal and update process
            await openDownloadMissingModalForYouTube(playlistId, downloadData.name, spotifyTracks);
            const process = activeDownloadProcesses[playlistId];
            if (process) {
                process.status = 'running';
                process.batchId = batchId;
                subscribeToDownloadBatch(batchId);
                const beginBtn = document.getElementById(`begin-analysis-btn-${playlistId}`);
                const cancelBtn = document.getElementById(`cancel-all-btn-${playlistId}`);
                if (beginBtn) beginBtn.style.display = 'none';
                if (cancelBtn) cancelBtn.style.display = 'inline-block';

                // Start polling for status updates
                startModalDownloadPolling(playlistId);
                console.log(`✅ [REHYDRATE] Successfully rehydrated ListenBrainz modal with polling`);
                return true;
            }
            return false;
        } else if (playlistId.startsWith('listenbrainz_')) {
            // ListenBrainz download from discovery modal - get from backend state
            const mbid = playlistId.replace('listenbrainz_', '');
            console.log(`💧 [REHYDRATE] ListenBrainz download - fetching state for MBID: ${mbid}`);

            try {
                // Fetch ListenBrainz state from backend
                const stateResponse = await fetch(`/api/listenbrainz/state/${mbid}`);
                if (!stateResponse.ok) {
                    console.log(`⚠️ [REHYDRATE] Failed to fetch ListenBrainz state`);
                    return false;
                }

                const stateData = await stateResponse.json();
                if (!stateData || !stateData.discovery_results) {
                    console.log(`⚠️ [REHYDRATE] No discovery results in ListenBrainz state`);
                    return false;
                }

                // Convert discovery results to Spotify tracks
                const spotifyTracks = stateData.discovery_results
                    .filter(result => result.spotify_data)
                    .map(result => {
                        const track = result.spotify_data;
                        // Ensure artists is in proper Spotify format: [{name: ...}]
                        let artistsArray = [];
                        if (track.artists && Array.isArray(track.artists)) {
                            artistsArray = track.artists.map(artist => {
                                if (typeof artist === 'string') {
                                    return { name: artist };
                                } else if (artist && artist.name) {
                                    return { name: artist.name };
                                } else {
                                    return { name: String(artist || 'Unknown Artist') };
                                }
                            });
                        } else if (track.artists && typeof track.artists === 'string') {
                            artistsArray = [{ name: track.artists }];
                        } else {
                            artistsArray = [{ name: 'Unknown Artist' }];
                        }
                        return {
                            id: track.id,
                            name: track.name,
                            artists: artistsArray,
                            album: track.album || { name: 'Unknown Album', images: [] },
                            duration_ms: track.duration_ms || 0,
                            external_urls: track.external_urls || {}
                        };
                    });

                if (spotifyTracks.length === 0) {
                    console.log(`⚠️ [REHYDRATE] No Spotify tracks in ListenBrainz discovery results`);
                    return false;
                }

                console.log(`✅ [REHYDRATE] Retrieved ${spotifyTracks.length} tracks from ListenBrainz state`);

                // Create modal and update process
                await openDownloadMissingModalForYouTube(playlistId, downloadData.name, spotifyTracks);
                const process = activeDownloadProcesses[playlistId];
                if (process) {
                    process.status = 'running';
                    process.batchId = batchId;
                    subscribeToDownloadBatch(batchId);
                    const beginBtn = document.getElementById(`begin-analysis-btn-${playlistId}`);
                    const cancelBtn = document.getElementById(`cancel-all-btn-${playlistId}`);
                    if (beginBtn) beginBtn.style.display = 'none';
                    if (cancelBtn) cancelBtn.style.display = 'inline-block';

                    // Start polling for status updates
                    startModalDownloadPolling(playlistId);
                    console.log(`✅ [REHYDRATE] Successfully rehydrated ListenBrainz download modal with polling`);
                    return true;
                }
                return false;

            } catch (error) {
                console.error(`❌ [REHYDRATE] Error fetching ListenBrainz state:`, error);
                return false;
            }
        } else {
            console.error(`❌ [REHYDRATE] Unknown discover playlist type: ${playlistId}`);
            return false;
        }

        // Fetch tracks from API
        console.log(`📡 [REHYDRATE] Fetching tracks from ${apiEndpoint}...`);
        const response = await fetch(apiEndpoint);
        if (!response.ok) {
            console.error(`❌ [REHYDRATE] Failed to fetch tracks: ${response.status}`);
            return false;
        }

        const data = await response.json();
        if (!data.success || !data.tracks) {
            console.error(`❌ [REHYDRATE] Invalid track data:`, data);
            return false;
        }

        const tracks = data.tracks;
        console.log(`✅ [REHYDRATE] Retrieved ${tracks.length} tracks`);

        // Transform tracks to Spotify format
        const spotifyTracks = tracks.map(track => {
            let spotifyTrack;
            if (track.track_data_json) {
                spotifyTrack = track.track_data_json;
            } else {
                spotifyTrack = {
                    id: track.spotify_track_id,
                    name: track.track_name,
                    artists: [{ name: track.artist_name }],
                    album: {
                        name: track.album_name,
                        images: track.album_cover_url ? [{ url: track.album_cover_url }] : []
                    },
                    duration_ms: track.duration_ms || 0
                };
            }
            if (spotifyTrack.artists && Array.isArray(spotifyTrack.artists)) {
                spotifyTrack.artists = spotifyTrack.artists.map(a => a.name || a);
            }
            return spotifyTrack;
        });

        // Create the modal
        await openDownloadMissingModalForYouTube(playlistId, downloadData.name, spotifyTracks);

        // Update process with batch info
        const process = activeDownloadProcesses[playlistId];
        if (process) {
            process.status = 'running';
            process.batchId = batchId;
            subscribeToDownloadBatch(batchId);

            // Update button states
            const beginBtn = document.getElementById(`begin-analysis-btn-${playlistId}`);
            const cancelBtn = document.getElementById(`cancel-all-btn-${playlistId}`);
            if (beginBtn) beginBtn.style.display = 'none';
            if (cancelBtn) cancelBtn.style.display = 'inline-block';

            // Start polling for status updates
            startModalDownloadPolling(playlistId);

            // Don't hide the modal - user clicked to open it
            console.log(`✅ [REHYDRATE] Successfully rehydrated modal for ${downloadData.name} with polling`);
            return true;
        } else {
            console.error(`❌ [REHYDRATE] Failed to find rehydrated process for ${playlistId}`);
            return false;
        }

    } catch (error) {
        console.error(`❌ [REHYDRATE] Error rehydrating discover download modal:`, error);
        return false;
    }
}

// --- Discover Download Snapshot System ---

let discoverSnapshotSaveTimeout = null; // Debounce snapshot saves

async function saveDiscoverDownloadSnapshot() {
    /**
     * Saves current discoverDownloads state to backend for persistence.
     * Debounced to prevent excessive backend calls.
     */

    // Clear any existing timeout
    if (discoverSnapshotSaveTimeout) {
        clearTimeout(discoverSnapshotSaveTimeout);
    }

    // Debounce the actual save
    discoverSnapshotSaveTimeout = setTimeout(async () => {
        try {
            const downloadCount = Object.keys(discoverDownloads).length;

            // Don't save empty state
            if (downloadCount === 0) {
                console.log('📸 Skipping discover snapshot save - no downloads to save');
                return;
            }

            console.log(`📸 Saving discover download snapshot: ${downloadCount} downloads`);

            // Prepare snapshot data (clean format)
            const cleanDownloads = {};
            for (const [playlistId, downloadData] of Object.entries(discoverDownloads)) {
                cleanDownloads[playlistId] = {
                    name: downloadData.name,
                    type: downloadData.type,
                    status: downloadData.status,
                    virtualPlaylistId: downloadData.virtualPlaylistId,
                    imageUrl: downloadData.imageUrl,
                    startTime: downloadData.startTime instanceof Date ? downloadData.startTime.toISOString() : downloadData.startTime
                };
            }

            const response = await fetch('/api/discover_downloads/snapshot', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    downloads: cleanDownloads
                })
            });

            const data = await response.json();

            if (data.success) {
                console.log(`✅ Discover download snapshot saved: ${downloadCount} downloads`);
            } else {
                console.error('❌ Failed to save discover download snapshot:', data.error);
            }

        } catch (error) {
            console.error('❌ Error saving discover download snapshot:', error);
        }
    }, 1000); // 1 second debounce
}

async function hydrateDiscoverDownloadsFromSnapshot() {
    /**
     * Hydrates discover downloads from backend snapshot with live status.
     * Called on page load to restore download state.
     */
    try {
        console.log('🔄 Loading discover download snapshot from backend...');

        const response = await fetch('/api/discover_downloads/hydrate');
        const data = await response.json();

        if (!data.success) {
            console.error('❌ Failed to load discover download snapshot:', data.error);
            return;
        }

        const downloads = data.downloads || {};
        const stats = data.stats || {};

        console.log(`🔄 Loaded discover snapshot: ${stats.total_downloads || 0} downloads, ${stats.active_downloads || 0} active, ${stats.completed_downloads || 0} completed`);

        if (Object.keys(downloads).length === 0) {
            console.log('ℹ️ No discover downloads to hydrate');
            return;
        }

        // Clear existing state
        discoverDownloads = {};

        // Restore discoverDownloads with hydrated data
        for (const [playlistId, downloadData] of Object.entries(downloads)) {
            discoverDownloads[playlistId] = {
                name: downloadData.name,
                type: downloadData.type,
                status: downloadData.status, // Live status from backend
                virtualPlaylistId: downloadData.virtualPlaylistId,
                imageUrl: downloadData.imageUrl,
                startTime: new Date(downloadData.startTime)
            };

            console.log(`🔄 Hydrated download: ${downloadData.name} (${downloadData.status})`);

            // Start monitoring for any in-progress downloads
            if (downloadData.status === 'in_progress') {
                console.log(`📡 Starting monitoring for: ${downloadData.name}`);
                monitorDiscoverDownload(playlistId);
            }
        }

        // Don't update UI here - it will be updated when user navigates to discover page
        // This allows hydration to work even if page loads on a different tab

        const totalDownloads = Object.keys(discoverDownloads).length;
        console.log(`✅ Successfully hydrated ${totalDownloads} discover downloads (UI will update on discover page navigation)`);

    } catch (error) {
        console.error('❌ Error hydrating discover downloads from snapshot:', error);
    }
}

// Initialize on page load
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initializeDiscoverDownloadBar);
} else {
    initializeDiscoverDownloadBar();
}

// ============================================================================
