
// ==================================================================================
// PLAYLIST EXPLORER — Visual Discovery Tree
// ==================================================================================

const _explorer = {
    initialized: false,
    mode: 'albums',
    artists: [],
    selectedAlbums: new Set(),
    expandedArtists: new Set(),
    building: false,
    playlistId: null,
    meta: null,
    _resizeTimer: null,
};

function initExplorer() {
    if (_explorer.initialized) return;
    _explorer.initialized = true;
    _explorer._playlists = [];
    _explorer._activeSource = null;

    _explorerLoadPlaylists();

    // Listen for discovery completion to auto-refresh playlist cards
    if (typeof socket !== 'undefined') {
        socket.on('discovery:progress', (data) => {
            if (!document.getElementById('playlist-explorer-page')?.classList.contains('active')) return;
            // Match mirrored playlist discovery events
            if (data.phase === 'discovered' || data.phase === 'sync_complete' || data.complete) {
                // Discovery finished — refresh playlists after brief delay for DB commit
                setTimeout(() => _explorerLoadPlaylists(), 1500);
            }
            // Live progress update on cards during discovery
            if (data.id && data.id.startsWith('mirrored_')) {
                const plId = parseInt(data.id.replace('mirrored_', ''));
                const card = document.querySelector(`.explorer-picker-card[data-id="${plId}"]`);
                if (card) {
                    const meta = card.querySelector('.explorer-picker-card-meta');
                    if (meta && data.progress != null) {
                        meta.innerHTML = `<span class="explorer-discovering-live">Discovering... ${Math.round(data.progress)}%</span>`;
                    }
                }
            }
        });
    }
}

function _explorerLoadPlaylists() {
    fetch('/api/mirrored-playlists')
        .then(r => r.json())
        .then(data => {
            const playlists = Array.isArray(data) ? data : (data.playlists || []);
            _explorer._playlists = playlists;

            if (playlists.length === 0) {
                const scroll = document.getElementById('explorer-picker-scroll');
                if (scroll) scroll.innerHTML = '<div class="explorer-picker-empty">No mirrored playlists found. Sync a playlist first.</div>';
                return;
            }

            // Group by source
            const groups = {};
            playlists.forEach(p => {
                const src = (p.source || 'other').toLowerCase();
                if (!groups[src]) groups[src] = [];
                groups[src].push(p);
            });

            // Render source tabs
            const tabsEl = document.getElementById('explorer-picker-tabs');
            if (tabsEl) {
                const sourceNames = { spotify: 'Spotify', tidal: 'Tidal', deezer: 'Deezer', youtube: 'YouTube', beatport: 'Beatport', file: 'File', other: 'Other' };
                const sources = Object.keys(groups);
                if (sources.length <= 1) {
                    tabsEl.style.display = 'none';
                } else {
                    tabsEl.innerHTML = sources.map((src, i) => {
                        const label = sourceNames[src] || src.charAt(0).toUpperCase() + src.slice(1);
                        const count = groups[src].length;
                        const isActive = _explorer._activeSource === src || (!_explorer._activeSource && i === 0);
                        return `<button class="explorer-picker-tab ${isActive ? 'active' : ''}" data-source="${src}" onclick="explorerSwitchPickerTab('${src}')">${label} <span class="explorer-picker-tab-count">${count}</span></button>`;
                    }).join('');
                }

                // Show active or first source
                const activeSource = _explorer._activeSource || sources[0];
                _explorer._activeSource = activeSource;
                explorerRenderPickerCards(activeSource);
            }
        })
        .catch(() => { });
}

function explorerSwitchPickerTab(source) {
    _explorer._activeSource = source;
    document.querySelectorAll('.explorer-picker-tab').forEach(t => t.classList.toggle('active', t.dataset.source === source));
    explorerRenderPickerCards(source);
}

function explorerRenderPickerCards(source) {
    const scroll = document.getElementById('explorer-picker-scroll');
    if (!scroll) return;

    const filtered = _explorer._playlists.filter(p => (p.source || 'other').toLowerCase() === source);
    scroll.innerHTML = filtered.map(p => {
        const img = p.image_url || '';
        const total = p.total_count || p.track_count || 0;
        const discovered = p.discovered_count || 0;
        const pct = total > 0 ? Math.round((discovered / total) * 100) : 0;
        const isReady = pct >= 50;
        const isActive = _explorer.playlistId === p.id;
        const isFullyDiscovered = pct === 100;
        const wasExplored = !!(p.explored_at || p.explored);
        const wishlisted = p.wishlisted_count || 0;
        const inLibrary = p.in_library_count || 0;

        // Status badge: checkmark if explored/in-library, star if ready, % if needs discovery
        let statusBadge = '';
        if (inLibrary > 0 && inLibrary >= total * 0.8) {
            statusBadge = '<div class="explorer-picker-card-badge downloaded" title="Most tracks in library">&#10003;</div>';
        } else if (wasExplored) {
            statusBadge = '<div class="explorer-picker-card-badge explored" title="Already explored">&#10003;</div>';
        } else if (wishlisted > 0) {
            statusBadge = '<div class="explorer-picker-card-badge wishlisted" title="Tracks wishlisted">&#9829;</div>';
        } else if (isFullyDiscovered) {
            statusBadge = '<div class="explorer-picker-card-badge ready" title="Ready to explore">&#9733;</div>';
        } else if (!isReady) {
            statusBadge = `<div class="explorer-picker-card-badge needs-discovery" title="Needs discovery (${pct}%)">${pct}%</div>`;
        }

        // Meta line with status indicators
        let metaHTML;
        const statusParts = [];
        if (inLibrary > 0) statusParts.push(`<span class="explorer-picker-in-library">${inLibrary} in library</span>`);
        if (wishlisted > 0) statusParts.push(`<span class="explorer-picker-wishlisted">${wishlisted} wishlisted</span>`);

        if (isFullyDiscovered) {
            metaHTML = `${total} tracks &middot; <span class="explorer-picker-discovered">Fully discovered</span>`;
        } else if (isReady) {
            metaHTML = `${total} tracks &middot; ${pct}% discovered`;
        } else {
            metaHTML = `${total} tracks &middot; <span class="explorer-picker-not-ready">${pct}% discovered</span>`;
        }
        if (statusParts.length > 0) {
            metaHTML += `<br>${statusParts.join(' &middot; ')}`;
        }

        // Discover button for undiscovered playlists (replaces redirect to Sync)
        const discoverBtn = !isReady ? `<button class="explorer-picker-discover-btn" onclick="event.stopPropagation(); explorerStartDiscovery(${p.id})" title="Start discovery">Discover</button>` : '';

        return `
            <div class="explorer-picker-card ${isActive ? 'active' : ''} ${!isReady ? 'not-ready' : ''} ${wasExplored ? 'explored' : ''}"
                 data-id="${p.id}"
                 onclick="${isReady ? `explorerSelectPlaylist(${p.id}, this)` : ''}">
                <div class="explorer-picker-card-art">
                    ${img ? `<img src="${img}" alt="" loading="lazy">` : '<div class="explorer-picker-card-art-placeholder">&#9835;</div>'}
                </div>
                <div class="explorer-picker-card-info">
                    <div class="explorer-picker-card-name-row">
                        <div class="explorer-picker-card-name">${p.name || 'Untitled'}</div>
                        ${statusBadge}
                    </div>
                    <div class="explorer-picker-card-meta">${metaHTML}</div>
                    ${discoverBtn ? `<div class="explorer-picker-card-actions">${discoverBtn}</div>` : ''}
                </div>
            </div>
        `;
    }).join('');
}

function explorerSelectPlaylist(id, el) {
    _explorer.playlistId = id;
    document.querySelectorAll('.explorer-picker-card').forEach(c => c.classList.remove('active'));
    if (el) el.classList.add('active');
    // Update hint text
    const hint = document.getElementById('explorer-build-hint');
    const pl = _explorer._playlists.find(p => p.id === id);
    if (hint && pl) hint.textContent = `Ready: ${pl.name}`;
    else if (hint) hint.textContent = '';
}

function explorerRedirectToDiscover(playlistId) {
    showToast('This playlist needs more tracks discovered before exploring. Redirecting to Sync...', 'info');
    navigateToPage('sync');
    setTimeout(() => {
        const mirroredBtn = document.querySelector('.sync-tab-button[data-tab="mirrored"]');
        if (mirroredBtn) mirroredBtn.click();
    }, 200);
}

async function explorerStartDiscovery(playlistId) {
    const card = document.querySelector(`.explorer-picker-card[data-id="${playlistId}"]`);
    const btn = card?.querySelector('.explorer-picker-discover-btn');
    if (btn) { btn.disabled = true; btn.textContent = 'Starting...'; }

    try {
        if (typeof discoverMirroredPlaylist === 'function') {
            await discoverMirroredPlaylist(playlistId);
            if (btn) { btn.disabled = false; btn.textContent = 'Open'; btn.title = 'Reopen discovery modal'; }

            // Poll for card updates while discovery is in progress
            _explorerStartDiscoveryPoller(playlistId);
        } else {
            explorerRedirectToDiscover(playlistId);
        }
    } catch (err) {
        showToast(`Discovery failed: ${err.message}`, 'error');
        if (btn) { btn.disabled = false; btn.textContent = 'Discover'; }
    }
}

function _explorerStartDiscoveryPoller(playlistId) {
    // Poll every 5s to refresh playlist cards until this playlist is ready
    if (_explorer._discoveryPoller) clearInterval(_explorer._discoveryPoller);
    _explorer._discoveryPoller = setInterval(async () => {
        // Stop polling if Explorer page isn't active
        if (!document.getElementById('playlist-explorer-page')?.classList.contains('active')) {
            clearInterval(_explorer._discoveryPoller);
            _explorer._discoveryPoller = null;
            return;
        }
        // Check if the mirrored playlist state shows discovery is done
        const tempHash = `mirrored_${playlistId}`;
        const state = youtubePlaylistStates[tempHash];
        const isDone = state && (state.phase === 'discovered' || state.phase === 'sync_complete');

        // Refresh cards from API
        await _explorerLoadPlaylists();

        // Stop polling once discovery is complete
        if (isDone) {
            clearInterval(_explorer._discoveryPoller);
            _explorer._discoveryPoller = null;
        }
    }, 5000);
}

function explorerSetMode(mode) {
    _explorer.mode = mode;
    document.querySelectorAll('.explorer-mode-btn').forEach(btn => {
        btn.classList.toggle('active', btn.dataset.mode === mode);
    });
}

async function explorerBuildTree() {
    const playlistId = _explorer.playlistId;
    if (!playlistId) {
        showToast('Select a playlist first', 'error');
        return;
    }
    if (_explorer.building) return;

    _explorer.building = true;
    _explorer.artists = [];
    _explorer.selectedAlbums.clear();
    _explorer.expandedArtists.clear();
    _explorer.playlistId = playlistId;

    const tree = document.getElementById('explorer-tree');
    const svg = document.getElementById('explorer-svg');
    const progress = document.getElementById('explorer-progress');
    const actionBar = document.getElementById('explorer-action-bar');
    const empty = document.getElementById('explorer-empty');
    const buildBtn = document.getElementById('explorer-build-btn');

    if (empty) empty.style.display = 'none';
    if (actionBar) actionBar.style.display = 'none';
    if (progress) progress.style.display = 'flex';
    if (buildBtn) { buildBtn.disabled = true; buildBtn.textContent = 'Building...'; }
    // Clear tree but preserve the SVG element (it lives inside the tree)
    tree.innerHTML = '<svg class="explorer-svg" id="explorer-svg"></svg>';
    _explorer._zoom = 1;
    tree.style.transform = '';

    try {
        const response = await fetch('/api/playlist-explorer/build-tree', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ playlist_id: parseInt(playlistId), mode: _explorer.mode })
        });

        if (!response.ok) {
            const err = await response.json();
            throw new Error(err.error || 'Failed to build tree');
        }

        // Stream NDJSON
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';
        let artistCount = 0;
        let totalArtists = 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.type === 'meta') {
                        _explorer.meta = data;
                        totalArtists = data.total_artists;
                        _explorerRenderRoot(data);
                    } else if (data.type === 'artist') {
                        artistCount++;
                        _explorer.artists.push(data);
                        _explorerRenderArtistNode(data, artistCount);

                        // Lines drawn after streaming completes (not during — flex reflow drifts positions)

                        // Update progress
                        const pct = Math.round((artistCount / totalArtists) * 100);
                        const fill = document.getElementById('explorer-progress-fill');
                        const text = document.getElementById('explorer-progress-text');
                        if (fill) fill.style.width = pct + '%';
                        if (text) text.textContent = `Discovering artists... ${artistCount} of ${totalArtists}`;
                    } else if (data.type === 'complete') {
                        // Done
                    }
                } catch (e) {
                    console.warn('Explorer: failed to parse NDJSON line', e);
                }
            }
        }

        // Tree built — show action bar, hide progress
        if (actionBar) actionBar.style.display = 'flex';
        if (progress) progress.style.display = 'none';
        _explorerUpdateCount();

        // Mark playlist as explored (server persists via explored_at; update local copy too)
        const exploredPl = _explorer._playlists.find(p => p.id === playlistId);
        if (exploredPl) {
            exploredPl.explored_at = new Date().toISOString();
            // Update card badge without full re-render
            const card = document.querySelector(`.explorer-picker-card[data-id="${playlistId}"]`);
            if (card) {
                card.classList.add('explored');
                const oldBadge = card.querySelector('.explorer-picker-card-badge');
                const badgeHTML = '<div class="explorer-picker-card-badge explored" title="Already explored">&#10003;</div>';
                if (oldBadge) {
                    oldBadge.outerHTML = badgeHTML;
                } else {
                    // Insert badge into the name row
                    const nameRow = card.querySelector('.explorer-picker-card-name-row');
                    if (nameRow) {
                        nameRow.insertAdjacentHTML('beforeend', badgeHTML);
                    }
                }
                // Remove discover button if present (no longer needed)
                const discoverBtn = card.querySelector('.explorer-picker-card-actions');
                if (discoverBtn) discoverBtn.remove();
            }
        }

        // Draw all connections now that the tree is stable
        setTimeout(() => _explorerRedrawAllConnections(true), 100);

    } catch (err) {
        showToast('Explorer: ' + err.message, 'error');
        if (empty) { empty.style.display = 'flex'; }
        if (progress) progress.style.display = 'none';
    } finally {
        _explorer.building = false;
        if (buildBtn) { buildBtn.disabled = false; buildBtn.textContent = 'Explore'; }
    }
}

function _explorerRenderRoot(meta) {
    const tree = document.getElementById('explorer-tree');
    const rootHtml = `
        <div class="explorer-tier explorer-tier-root">
            <div class="explorer-node explorer-node-root" id="explorer-root">
                <div class="explorer-node-glow"></div>
                ${meta.playlist_image
            ? `<img class="explorer-node-img" src="${meta.playlist_image}" alt="">`
            : '<div class="explorer-node-img-placeholder">&#9835;</div>'
        }
                <div class="explorer-node-label">
                    <div class="explorer-node-label-sub">SOURCE</div>
                    <div class="explorer-node-label-main">${meta.playlist_name}</div>
                    <div class="explorer-node-label-meta">${meta.total_tracks} tracks &middot; ${meta.total_artists} artists</div>
                </div>
            </div>
        </div>
        <div class="explorer-artist-tiers" id="explorer-artist-tiers"></div>
    `;
    tree.insertAdjacentHTML('afterbegin', rootHtml);
    _explorer._artistRowSizes = []; // Track row capacities: [2, 3, 4, ...]
    _explorer._artistCount = 0;
    _explorer._currentRowIndex = 0;
}

function _explorerGetOrCreateRow() {
    const container = document.getElementById('explorer-artist-tiers');
    if (!container) return null;

    // Determine row sizes: 2, 3, 4, 5... (tree shape)
    const rowCapacity = _explorer._currentRowIndex + 2;
    const existingRows = container.querySelectorAll('.explorer-tier-artists');
    let currentRow = existingRows[existingRows.length - 1];

    if (!currentRow || currentRow.children.length >= (_explorer._currentRowIndex + 2)) {
        // Need a new row
        _explorer._currentRowIndex = existingRows.length;
        const newRow = document.createElement('div');
        newRow.className = 'explorer-tier explorer-tier-artists';
        container.appendChild(newRow);
        return newRow;
    }
    return currentRow;
}

function _explorerRenderArtistNode(artist, index) {
    const row = _explorerGetOrCreateRow();
    if (!row) return;

    _explorer._artistCount++;
    const albumCount = artist.albums ? artist.albums.length : 0;
    const safeKey = (artist.name || '').replace(/[^a-zA-Z0-9]/g, '_');
    const hasError = !!artist.error;

    const html = `
        <div class="explorer-branch" id="explorer-branch-${safeKey}" style="--enter-delay: ${(index % 5) * 0.1}s">
            <div class="explorer-node explorer-node-artist ${hasError ? 'error' : ''}"
                 id="explorer-node-${safeKey}" data-key="${safeKey}"
                 onclick="${hasError || albumCount === 0 ? '' : `explorerToggleArtist('${safeKey}')`}">
                ${artist.image_url
            ? `<img class="explorer-node-img" src="${artist.image_url}" alt="" loading="lazy">`
            : ''
        }
                <div class="explorer-node-label">
                    <div class="explorer-node-label-main">${artist.name || 'Unknown'}</div>
                    <div class="explorer-node-label-meta">${hasError ? 'Not found' : albumCount + ' album' + (albumCount !== 1 ? 's' : '')}</div>
                </div>
                ${!hasError && albumCount > 0 ? '<div class="explorer-node-expand-hint">&#9662;</div>' : ''}
                ${hasError ? '<div class="explorer-node-error-ring"></div>' : ''}
            </div>
            <div class="explorer-children" id="explorer-children-${safeKey}"></div>
        </div>
    `;
    row.insertAdjacentHTML('beforeend', html);
}

function explorerToggleArtist(key) {
    const children = document.getElementById(`explorer-children-${key}`);
    const node = document.getElementById(`explorer-node-${key}`);
    if (!children || !node) return;

    const isExpanded = _explorer.expandedArtists.has(key);
    if (isExpanded) {
        _explorer.expandedArtists.delete(key);
        children.innerHTML = '';
        node.classList.remove('expanded');
    } else {
        _explorer.expandedArtists.add(key);
        node.classList.add('expanded');

        const artist = _explorer.artists.find(a => (a.name || '').replace(/[^a-zA-Z0-9]/g, '_') === key);
        if (artist && artist.albums) {
            const albumsHtml = artist.albums.map((album, i) => {
                const id = album.spotify_id || `${key}_${i}`;
                const selected = _explorer.selectedAlbums.has(id);
                const owned = album.owned;
                const inPlaylist = album.in_playlist;

                const typeLabel = album.album_type === 'single' ? 'Single' : album.album_type === 'ep' ? 'EP' : 'Album';
                return `
                    <div class="explorer-branch" style="--enter-delay: ${i * 0.06}s">
                        <div class="explorer-node explorer-node-album ${selected ? 'selected' : ''} ${owned ? 'owned' : ''} ${inPlaylist ? 'in-playlist' : ''}"
                             data-id="${id}" data-key="${id}"
                             onclick="explorerToggleAlbum('${id}'); event.stopPropagation();"
                             title="${(album.title || '').replace(/"/g, '&quot;')}\n${album.year || ''} · ${typeLabel} · ${album.track_count || '?'} tracks${owned ? '\n✓ Already in library' : ''}${inPlaylist ? '\n♫ Track from this playlist' : ''}\nClick to select · Double-click for tracklist">
                            ${album.image_url
                        ? `<img class="explorer-node-img" src="${album.image_url}" alt="" loading="lazy">`
                        : ''
                    }
                            <div class="explorer-node-label">
                                <div class="explorer-node-label-main">${album.title || 'Unknown'}</div>
                                <div class="explorer-node-label-meta">${album.year || ''} &middot; ${album.track_count || '?'} tracks</div>
                            </div>
                            <div class="explorer-node-select ${selected ? 'active' : ''}">
                                <svg viewBox="0 0 20 20"><polyline points="4 10 8 14 16 6" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
                            </div>
                            ${owned ? '<div class="explorer-node-badge-float owned">Owned</div>' : ''}
                            ${inPlaylist ? '<div class="explorer-node-badge-float playlist">♫</div>' : ''}
                        </div>
                        <div class="explorer-children" id="explorer-tracks-${id}"></div>
                    </div>
                `;
            }).join('');
            children.innerHTML = albumsHtml;
        }
    }

    // Redraw SVG after DOM settles
    requestAnimationFrame(() => setTimeout(() => _explorerRedrawAllConnections(), 50));
}

async function explorerExpandAlbumTracks(spotifyAlbumId, nodeKey) {
    if (!spotifyAlbumId) return;
    const tracksContainer = document.getElementById(`explorer-tracks-${nodeKey}`);
    if (!tracksContainer) return;

    // Toggle: if already has content, collapse
    if (tracksContainer.innerHTML) {
        tracksContainer.innerHTML = '';
        requestAnimationFrame(() => setTimeout(() => _explorerRedrawAllConnections(), 50));
        return;
    }

    try {
        const response = await fetch(`/api/playlist-explorer/album-tracks/${spotifyAlbumId}`);
        const data = await response.json();
        if (!data.success || !data.tracks) return;

        const tracksHtml = data.tracks.map((t, i) => `
            <div class="explorer-branch" style="--enter-delay: ${i * 0.03}s">
                <div class="explorer-node explorer-node-track">
                    <div class="explorer-node-label">
                        <div class="explorer-node-label-main">${t.track_number}. ${t.name}</div>
                        <div class="explorer-node-label-meta">${_formatDuration(t.duration_ms)}</div>
                    </div>
                </div>
            </div>
        `).join('');
        tracksContainer.innerHTML = tracksHtml;
        requestAnimationFrame(() => setTimeout(() => _explorerRedrawAllConnections(), 50));
    } catch (e) {
        console.error('Failed to load album tracks:', e);
    }
}

function _formatDuration(ms) {
    if (!ms) return '';
    const m = Math.floor(ms / 60000);
    const s = Math.floor((ms % 60000) / 1000);
    return `${m}:${s.toString().padStart(2, '0')}`;
}

// Track double-click vs single-click on album nodes
let _explorerClickTimer = null;
let _explorerLastClickId = null;

function explorerToggleAlbum(id) {
    // Double-click detection: expand tracks
    if (_explorerLastClickId === id && _explorerClickTimer) {
        clearTimeout(_explorerClickTimer);
        _explorerClickTimer = null;
        _explorerLastClickId = null;
        // Double-click — expand tracks
        const node = document.querySelector(`.explorer-node-album[data-id="${id}"]`);
        const spotifyId = id.includes('_') ? '' : id; // Only real IDs, not fallback keys
        explorerExpandAlbumTracks(spotifyId, id);
        return;
    }

    _explorerLastClickId = id;
    _explorerClickTimer = setTimeout(() => {
        _explorerClickTimer = null;
        _explorerLastClickId = null;

        // Single click — toggle selection
        if (_explorer.selectedAlbums.has(id)) {
            _explorer.selectedAlbums.delete(id);
        } else {
            _explorer.selectedAlbums.add(id);
        }

        const node = document.querySelector(`.explorer-node-album[data-id="${id}"]`);
        if (node) {
            const isSelected = _explorer.selectedAlbums.has(id);
            node.classList.toggle('selected', isSelected);
            const check = node.querySelector('.explorer-node-select');
            if (check) check.classList.toggle('active', isSelected);
        }

        _explorerUpdateCount();
    }, 250);

    _explorerUpdateCount();
}

function explorerSelectAll() {
    _explorer.artists.forEach(a => {
        (a.albums || []).forEach(album => {
            if (album.spotify_id && !album.owned) _explorer.selectedAlbums.add(album.spotify_id);
        });
    });
    _explorerRefreshAllCards();
    _explorerUpdateCount();
}

function explorerDeselectAll() {
    _explorer.selectedAlbums.clear();
    _explorerRefreshAllCards();
    _explorerUpdateCount();
}

function _explorerRefreshAllCards() {
    document.querySelectorAll('.explorer-node-album').forEach(node => {
        const id = node.dataset.id;
        const selected = _explorer.selectedAlbums.has(id);
        node.classList.toggle('selected', selected);
        const check = node.querySelector('.explorer-node-select');
        if (check) check.classList.toggle('active', selected);
    });
}

function _explorerUpdateCount() {
    const el = document.getElementById('explorer-selection-count');
    const count = _explorer.selectedAlbums.size;
    if (el) el.textContent = `${count} album${count !== 1 ? 's' : ''} selected`;
    _explorerRefreshArtistIndicators();
}

function _explorerRefreshArtistIndicators() {
    // For each artist, check if any of their albums are selected — add visual indicator
    _explorer.artists.forEach(artist => {
        const key = (artist.name || '').replace(/[^a-zA-Z0-9]/g, '_');
        const node = document.getElementById(`explorer-node-${key}`);
        if (!node) return;
        const hasSelected = (artist.albums || []).some(a => a.spotify_id && _explorer.selectedAlbums.has(a.spotify_id));
        node.classList.toggle('has-selection', hasSelected);
    });
}

function explorerAddToWishlist() {
    if (_explorer.selectedAlbums.size === 0) {
        showToast('No albums selected', 'error');
        return;
    }

    // Group selected albums by artist with full metadata
    const artistSections = [];
    for (const artist of _explorer.artists) {
        const artistId = artist.artist_id || artist.spotify_id;
        if (!artist.albums) continue;
        const selected = artist.albums.filter(a => a.spotify_id && _explorer.selectedAlbums.has(a.spotify_id));
        if (selected.length === 0) continue;
        artistSections.push({ artistId, name: artist.name, image: artist.image_url, albums: selected });
    }

    if (artistSections.length === 0) { showToast('No valid albums selected', 'error'); return; }

    // Build confirmation modal (mirrors discog-modal pattern)
    const overlay = document.createElement('div');
    overlay.className = 'discog-modal-overlay';
    overlay.id = 'explorer-wishlist-overlay';

    const totalAlbums = artistSections.reduce((s, a) => s + a.albums.length, 0);
    const totalTracks = artistSections.reduce((s, a) => s + a.albums.reduce((t, al) => t + (al.track_count || 0), 0), 0);

    let cardsHtml = '';
    artistSections.forEach(section => {
        cardsHtml += `<div class="discog-section-header">${_esc(section.name)}</div>`;
        section.albums.forEach((album, i) => {
            const year = album.year || '';
            const typeLabel = album.album_type === 'single' ? 'Single' : album.album_type === 'ep' ? 'EP' : 'Album';
            cardsHtml += `
                <label class="discog-card ${album.owned ? 'owned' : ''}" data-type="${album.album_type || 'album'}" data-artist-id="${section.artistId}" style="animation-delay:${i * 0.03}s">
                    <input type="checkbox" class="discog-card-cb" data-album-id="${album.spotify_id}" data-tracks="${album.track_count || 0}" ${!album.owned ? 'checked' : ''} onchange="_explorerWishlistUpdateCount()">
                    <div class="discog-card-art">
                        ${album.image_url ? `<img src="${album.image_url}" alt="" loading="lazy">` : '<div class="discog-card-art-placeholder">&#9835;</div>'}
                        ${album.owned ? '<span class="discog-card-status">&#10003;</span>' : ''}
                    </div>
                    <div class="discog-card-info">
                        <div class="discog-card-title">${_esc(album.title || 'Unknown')}</div>
                        <div class="discog-card-meta">${year}${year ? ' · ' : ''}${typeLabel} · ${album.track_count || '?'} tracks</div>
                    </div>
                    <div class="discog-card-check"></div>
                </label>
            `;
        });
    });

    overlay.innerHTML = `
        <div class="discog-modal">
            <div class="discog-modal-hero" style="background-image:url('${artistSections[0]?.image || ''}')">
                <div class="discog-modal-hero-overlay"></div>
                <div class="discog-modal-hero-content">
                    <h2 class="discog-modal-title">Add to Wishlist</h2>
                    <p class="discog-modal-artist">${artistSections.length} artist${artistSections.length !== 1 ? 's' : ''} · ${totalAlbums} releases</p>
                </div>
                <button class="discog-modal-close" onclick="document.getElementById('explorer-wishlist-overlay')?.remove()">&times;</button>
            </div>
            <div class="discog-filter-bar">
                <div class="discog-filters">
                    <button class="discog-filter active" data-type="album" onclick="_explorerWishlistToggleFilter(this)">Albums</button>
                    <button class="discog-filter active" data-type="ep" onclick="_explorerWishlistToggleFilter(this)">EPs</button>
                    <button class="discog-filter active" data-type="single" onclick="_explorerWishlistToggleFilter(this)">Singles</button>
                </div>
                <div class="discog-select-actions">
                    <button class="discog-select-btn" onclick="document.querySelectorAll('#explorer-wishlist-overlay .discog-card-cb').forEach(c=>{c.checked=true}); _explorerWishlistUpdateCount()">Select All</button>
                    <button class="discog-select-btn" onclick="document.querySelectorAll('#explorer-wishlist-overlay .discog-card-cb').forEach(c=>{c.checked=false}); _explorerWishlistUpdateCount()">Deselect</button>
                </div>
            </div>
            <div class="discog-grid" id="explorer-wishlist-grid">${cardsHtml}</div>
            <div class="discog-progress" id="explorer-wishlist-progress" style="display:none;"></div>
            <div class="discog-footer" id="explorer-wishlist-footer">
                <div class="discog-footer-info" id="explorer-wishlist-info"></div>
                <div class="discog-footer-actions">
                    <button class="discog-cancel-btn" onclick="document.getElementById('explorer-wishlist-overlay')?.remove()">Cancel</button>
                    <button class="discog-submit-btn" id="explorer-wishlist-submit">
                        <span class="discog-submit-icon">&#11015;</span>
                        <span id="explorer-wishlist-submit-text">Add to Wishlist</span>
                    </button>
                </div>
            </div>
        </div>
    `;

    document.body.appendChild(overlay);
    requestAnimationFrame(() => overlay.classList.add('visible'));
    _explorerWishlistUpdateCount();

    document.getElementById('explorer-wishlist-submit')?.addEventListener('click', () => _explorerWishlistSubmit(artistSections));
}

function _explorerWishlistToggleFilter(btn) {
    btn.classList.toggle('active');
    const type = btn.dataset.type;
    // Scoped to explorer wishlist modal only
    document.querySelectorAll(`#explorer-wishlist-overlay .discog-card[data-type="${type}"]`).forEach(card => {
        card.style.display = btn.classList.contains('active') ? '' : 'none';
    });
    _explorerWishlistUpdateCount();
}

function _explorerWishlistUpdateCount() {
    const checked = document.querySelectorAll('#explorer-wishlist-overlay .discog-card-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('explorer-wishlist-info');
    const btn = document.getElementById('explorer-wishlist-submit-text');
    if (info) info.textContent = `${releases} release${releases !== 1 ? 's' : ''} · ${tracks} tracks`;
    if (btn) btn.textContent = releases > 0 ? `Add ${releases} to Wishlist` : 'Select releases';
    const submitBtn = document.getElementById('explorer-wishlist-submit');
    if (submitBtn) submitBtn.disabled = releases === 0;
}

async function _explorerWishlistSubmit(artistSections) {
    const grid = document.getElementById('explorer-wishlist-grid');
    const progress = document.getElementById('explorer-wishlist-progress');
    const filterBar = document.querySelector('#explorer-wishlist-overlay .discog-filter-bar');
    const submitBtn = document.getElementById('explorer-wishlist-submit');

    // Collect checked albums grouped by artist
    const byArtist = {};
    document.querySelectorAll('#explorer-wishlist-overlay .discog-card-cb:checked').forEach(cb => {
        if (cb.closest('.discog-card').style.display === 'none') return;
        const card = cb.closest('.discog-card');
        const artistId = card.dataset.artistId;
        const albumId = cb.dataset.albumId;
        const title = card.querySelector('.discog-card-title')?.textContent || '';
        const img = card.querySelector('.discog-card-art img')?.src || '';
        if (!byArtist[artistId]) byArtist[artistId] = { albums: [], name: '' };
        byArtist[artistId].albums.push({ id: albumId, title, img, tracks: parseInt(cb.dataset.tracks) || 0 });
    });

    // Fill in artist names
    artistSections.forEach(s => { if (byArtist[s.artistId]) byArtist[s.artistId].name = s.name; });

    // Switch to progress view
    if (grid) grid.style.display = 'none';
    if (filterBar) filterBar.style.display = 'none';
    if (submitBtn) submitBtn.style.display = 'none';
    if (progress) {
        progress.style.display = '';
        progress.innerHTML = '';
        for (const [artistId, data] of Object.entries(byArtist)) {
            data.albums.forEach(album => {
                const item = document.createElement('div');
                item.className = 'discog-progress-item active';
                item.id = `explorer-prog-${album.id}`;
                item.innerHTML = `
                    <div class="discog-prog-art">${album.img ? `<img src="${album.img}">` : '&#9835;'}</div>
                    <div class="discog-prog-info">
                        <div class="discog-prog-title">${_esc(album.title)}</div>
                        <div class="discog-prog-status">Waiting...</div>
                    </div>
                    <div class="discog-prog-icon"><div class="discog-spinner"></div></div>
                `;
                progress.appendChild(item);
            });
        }
    }

    const info = document.getElementById('explorer-wishlist-info');
    if (info) info.textContent = 'Processing...';

    let totalAdded = 0;

    for (const [artistId, data] of Object.entries(byArtist)) {
        // Sort by track count descending (deluxe editions first) BEFORE extracting IDs
        data.albums.sort((a, b) => b.tracks - a.tracks);
        // Per-album metadata so the backend can resolve each album through its
        // own source even when the explorer doesn't carry per-album source info.
        const albumsPayload = data.albums.map(a => ({
            id: a.id,
            name: a.title || '',
            artist_name: data.name,
            source: null,
        }));

        try {
            const response = await fetch(`/api/artist/${artistId}/download-discography`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    albums: albumsPayload,
                    artist_name: data.name,
                })
            });

            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let buffer = '';
            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 result = JSON.parse(line);
                        if (result.status === 'complete') continue; // Summary line, skip
                        const item = document.getElementById(`explorer-prog-${result.album_id}`);
                        if (item) {
                            const statusEl = item.querySelector('.discog-prog-status');
                            const iconEl = item.querySelector('.discog-prog-icon');
                            if (result.status === 'done') {
                                const added = result.tracks_added || 0;
                                const skipped = result.tracks_skipped || 0;
                                totalAdded += added;
                                if (statusEl) statusEl.textContent = `Added ${added} track${added !== 1 ? 's' : ''}${skipped > 0 ? `, ${skipped} skipped` : ''}`;
                                if (iconEl) iconEl.innerHTML = '<span style="color:#4CAF50">&#10003;</span>';
                                item.classList.remove('active');
                                item.classList.add('done');
                            } else if (result.status === 'error') {
                                if (statusEl) statusEl.textContent = result.message || 'Error';
                                if (iconEl) iconEl.innerHTML = '<span style="color:#ff4757">&#10007;</span>';
                                item.classList.remove('active');
                                item.classList.add('error');
                            }
                        }
                    } catch (e) { }
                }
            }
        } catch (e) {
            console.error(`Explorer wishlist: failed for ${data.name}:`, e);
        }
    }

    if (info) info.textContent = `Done — ${totalAdded} tracks added to wishlist`;
    // Change cancel button label to "Close"
    const cancelBtn = document.querySelector('#explorer-wishlist-overlay .discog-cancel-btn');
    if (cancelBtn) cancelBtn.textContent = 'Close';
    showToast(`Added ${totalAdded} tracks to wishlist`, 'success');

    // Mark albums as added on the tree
    _explorer.selectedAlbums.forEach(id => {
        const node = document.querySelector(`.explorer-node-album[data-id="${id}"]`);
        if (node) { node.classList.add('added'); node.classList.remove('selected'); }
    });
    _explorer.selectedAlbums.clear();
    _explorerUpdateCount();
    _explorerRefreshArtistIndicators();
}

function _explorerEnsureDefs() {
    const svg = document.getElementById('explorer-svg');
    if (!svg || svg.querySelector('defs')) return;
    // Read accent color from CSS custom property
    const accentRgb = getComputedStyle(document.documentElement).getPropertyValue('--accent-rgb').trim() || '100,200,255';
    const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
    defs.innerHTML = `
        <linearGradient id="explorer-grad-root" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stop-color="rgba(${accentRgb}, 0.25)"/>
            <stop offset="100%" stop-color="rgba(${accentRgb}, 0.06)"/>
        </linearGradient>
        <linearGradient id="explorer-grad-album" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stop-color="rgba(${accentRgb}, 0.15)"/>
            <stop offset="100%" stop-color="rgba(${accentRgb}, 0.04)"/>
        </linearGradient>
    `;
    svg.appendChild(defs);
}

function _explorerDrawConnectionToArtist(artistIndex) {
    // Incremental: draw ONLY this artist's connection. Don't clear existing.
    // Flex reflow within the current row may shift siblings, but the visual drift
    // is minor and gets corrected by the final redraw after streaming completes.
    const svg = document.getElementById('explorer-svg');
    const root = document.getElementById('explorer-root');
    if (!svg || !root) return;

    _explorerEnsureDefs();
    _explorerSizeSvg();

    const artistNodes = document.querySelectorAll('.explorer-node-artist');
    const artistNode = artistNodes[artistIndex];
    if (!artistNode) return;

    const rc = _explorerGetPos(root);
    const ac = _explorerGetPos(artistNode);
    _explorerDrawCurve(svg, rc.cx, rc.bottom, ac.cx, ac.top, 'root', true);
}

function _explorerRedrawAllConnections(animate = false) {
    const svg = document.getElementById('explorer-svg');
    const root = document.getElementById('explorer-root');
    if (!svg || !root) return;

    _explorerEnsureDefs();
    _explorerSizeSvg();

    // Clear existing lines but keep defs
    svg.querySelectorAll('path').forEach(p => p.remove());

    const rc = _explorerGetPos(root);

    document.querySelectorAll('.explorer-node-artist').forEach(artistNode => {
        const ac = _explorerGetPos(artistNode);
        _explorerDrawCurve(svg, rc.cx, rc.bottom, ac.cx, ac.top, 'root', animate);

        if (artistNode.classList.contains('expanded')) {
            const branch = artistNode.closest('.explorer-branch');
            if (!branch) return;
            branch.querySelectorAll(':scope > .explorer-children > .explorer-branch > .explorer-node-album').forEach(albumNode => {
                const alc = _explorerGetPos(albumNode);
                _explorerDrawCurve(svg, ac.cx, ac.bottom, alc.cx, alc.top, 'album', animate);

                const albumBranch = albumNode.closest('.explorer-branch');
                if (albumBranch) {
                    albumBranch.querySelectorAll(':scope > .explorer-children > .explorer-branch > .explorer-node-track').forEach(trackNode => {
                        const tc = _explorerGetPos(trackNode);
                        _explorerDrawCurve(svg, alc.cx, alc.bottom, tc.cx, tc.top, 'track', animate);
                    });
                }
            });
        }
    });
}

function _explorerSizeSvg() {
    const svg = document.getElementById('explorer-svg');
    const tree = document.getElementById('explorer-tree');
    if (!svg || !tree) return;
    // SVG is inside the tree. Use scrollWidth/scrollHeight which are unscaled.
    // Add padding to ensure lines near edges aren't clipped.
    const w = Math.max(tree.scrollWidth, tree.offsetWidth) + 40;
    const h = Math.max(tree.scrollHeight, tree.offsetHeight) + 40;
    svg.setAttribute('width', w);
    svg.setAttribute('height', h);
    svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
}

function _explorerGetPos(el) {
    // SVG is inside the tree — positions are relative to tree, unscaled
    const tree = document.getElementById('explorer-tree');
    if (!tree) return { cx: 0, top: 0, bottom: 0 };
    const tRect = tree.getBoundingClientRect();
    const r = el.getBoundingClientRect();
    const scale = _explorer._zoom || 1;
    // getBoundingClientRect returns scaled coords; divide by scale to get unscaled tree-space coords
    return {
        cx: (r.left + r.width / 2 - tRect.left) / scale,
        top: (r.top - tRect.top) / scale,
        bottom: (r.bottom - tRect.top) / scale,
    };
}

function _explorerDrawCurve(svg, x1, y1, x2, y2, type, animate) {
    const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    const midY = y1 + (y2 - y1) * 0.45;
    path.setAttribute('d', `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`);

    if (type === 'root') {
        path.setAttribute('stroke', 'url(#explorer-grad-root)');
        path.setAttribute('stroke-width', '1.5');
    } else if (type === 'album') {
        path.setAttribute('stroke', 'url(#explorer-grad-album)');
        path.setAttribute('stroke-width', '1');
    } else {
        path.setAttribute('stroke', 'rgba(255,255,255,0.05)');
        path.setAttribute('stroke-width', '0.8');
    }
    path.setAttribute('fill', 'none');

    svg.appendChild(path);

    if (animate) {
        const len = path.getTotalLength();
        path.setAttribute('class', 'explorer-line explorer-line-animated');
        path.style.strokeDasharray = len;
        path.style.strokeDashoffset = len;
    } else {
        path.setAttribute('class', 'explorer-line');
    }
}

// ── Zoom & Pan ──
_explorer._zoom = 1;
_explorer._panX = 0;
_explorer._panY = 0;
_explorer._isPanning = false;
_explorer._panStartX = 0;
_explorer._panStartY = 0;
_explorer._panStartScrollX = 0;
_explorer._panStartScrollY = 0;

function _explorerApplyTransform() {
    const tree = document.getElementById('explorer-tree');
    if (tree) {
        tree.style.transform = `scale(${_explorer._zoom})`;
        tree.style.transformOrigin = 'top center';
    }
    _explorerSizeSvg();
    requestAnimationFrame(() => _explorerRedrawAllConnections());
}

function explorerZoom(delta) {
    _explorer._zoom = Math.max(0.2, Math.min(3, _explorer._zoom + delta));
    _explorerApplyTransform();
}

function explorerFitToView() {
    const viewport = document.getElementById('explorer-viewport');
    const tree = document.getElementById('explorer-tree');
    if (!viewport || !tree) return;

    // Reset zoom to measure natural size
    _explorer._zoom = 1;
    tree.style.transform = 'scale(1)';

    requestAnimationFrame(() => {
        const treeW = tree.scrollWidth;
        const treeH = tree.scrollHeight;
        const vpW = viewport.clientWidth - 40;
        const vpH = viewport.clientHeight - 40;

        if (treeW > 0 && treeH > 0) {
            _explorer._zoom = Math.min(vpW / treeW, vpH / treeH, 1.5);
            _explorer._zoom = Math.max(0.2, Math.min(3, _explorer._zoom));
        }

        _explorerApplyTransform();
        viewport.scrollTop = 0;
        viewport.scrollLeft = Math.max(0, (tree.scrollWidth * _explorer._zoom - vpW) / 2);
    });
}

// Scroll wheel zoom (no modifier needed inside viewport)
document.addEventListener('wheel', (e) => {
    const viewport = document.getElementById('explorer-viewport');
    if (!viewport || !viewport.contains(e.target)) return;
    // Check if we're on the explorer page
    const page = document.getElementById('playlist-explorer-page');
    if (!page || !page.classList.contains('active')) return;

    e.preventDefault();
    const step = e.deltaY > 0 ? -0.08 : 0.08;
    explorerZoom(step);
}, { passive: false });

// Middle-click / right-click drag to pan
document.addEventListener('mousedown', (e) => {
    const viewport = document.getElementById('explorer-viewport');
    if (!viewport || !viewport.contains(e.target)) return;
    // Middle click (button 1) or right click (button 2)
    if (e.button !== 1 && e.button !== 2) return;

    e.preventDefault();
    _explorer._isPanning = true;
    _explorer._panStartX = e.clientX;
    _explorer._panStartY = e.clientY;
    _explorer._panStartScrollX = viewport.scrollLeft;
    _explorer._panStartScrollY = viewport.scrollTop;
    viewport.style.cursor = 'grabbing';
});

document.addEventListener('mousemove', (e) => {
    if (!_explorer._isPanning) return;
    const viewport = document.getElementById('explorer-viewport');
    if (!viewport) return;
    const dx = e.clientX - _explorer._panStartX;
    const dy = e.clientY - _explorer._panStartY;
    viewport.scrollLeft = _explorer._panStartScrollX - dx;
    viewport.scrollTop = _explorer._panStartScrollY - dy;
});

document.addEventListener('mouseup', (e) => {
    if (!_explorer._isPanning) return;
    _explorer._isPanning = false;
    const viewport = document.getElementById('explorer-viewport');
    if (viewport) viewport.style.cursor = '';
});

// Suppress context menu on right-click inside viewport (for panning)
document.addEventListener('contextmenu', (e) => {
    const viewport = document.getElementById('explorer-viewport');
    if (viewport && viewport.contains(e.target)) {
        e.preventDefault();
    }
});

// Debounced redraw on resize
window.addEventListener('resize', () => {
    if (_explorer.artists.length === 0) return;
    clearTimeout(_explorer._resizeTimer);
    _explorer._resizeTimer = setTimeout(() => _explorerRedrawAllConnections(), 150);
});


// ==================================================================================
// DASHBOARD — Recent Syncs Section
// ==================================================================================

// ==================================================================================
// SERVER PLAYLIST MANAGER — Sync Page Server Tab
// ==================================================================================

let _serverPlaylists = [];
let _serverEditorState = { playlistId: null, playlistName: '', tracks: [] };

async function loadServerPlaylists() {
    const container = document.getElementById('server-playlist-container');
    const editor = document.getElementById('server-editor');
    const btn = document.getElementById('server-refresh-btn');

    if (editor) editor.style.display = 'none';
    if (container) container.style.display = '';
    if (btn) { btn.disabled = true; btn.textContent = '🔄 Loading...'; }

    // Show skeleton loader
    if (container) {
        container.innerHTML = `<div class="server-pl-grid">${Array.from({ length: 6 }, (_, i) => `
            <div class="server-pl-card server-pl-skeleton" style="animation-delay: ${i * 0.06}s">
                <div class="server-pl-card-top">
                    <div class="skeleton-box" style="width:44px;height:44px;border-radius:12px"></div>
                    <div class="skeleton-box" style="width:28px;height:28px;border-radius:8px"></div>
                </div>
                <div class="server-pl-card-body">
                    <div class="skeleton-box" style="width:${60 + Math.random() * 30}%;height:14px;border-radius:4px;margin-bottom:8px"></div>
                    <div class="skeleton-box" style="width:40%;height:11px;border-radius:4px"></div>
                </div>
                <div class="server-pl-card-footer" style="border-top:1px solid rgba(255,255,255,0.05);padding-top:12px">
                    <div class="skeleton-box" style="width:60px;height:10px;border-radius:3px"></div>
                </div>
            </div>`).join('')}</div>`;
    }

    try {
        // Fetch server playlists, mirrored playlists, and sync history names in parallel
        const [serverRes, mirroredRes, historyNamesRes] = await Promise.all([
            fetch('/api/server/playlists'),
            fetch('/api/mirrored-playlists'),
            fetch('/api/sync/history/names'),
        ]);
        const data = await serverRes.json();
        let mirroredAll = [];
        try { mirroredAll = await mirroredRes.json(); } catch (_) { }
        if (!Array.isArray(mirroredAll)) mirroredAll = [];
        let historyNames = [];
        try { historyNames = await historyNamesRes.json(); } catch (_) { }
        if (!Array.isArray(historyNames)) historyNames = [];

        if (!data.success || !data.playlists) {
            if (container) container.innerHTML = `<div class="playlist-placeholder">${data.error || 'Could not load server playlists'}</div>`;
            return;
        }

        // Separate synced vs non-synced playlists
        const mirroredNames = new Set(mirroredAll.map(p => p.name.trim().toLowerCase()));
        const syncedNames = new Set(historyNames.map(n => n.trim().toLowerCase()));
        const synced = [];
        const unsynced = [];
        for (const pl of data.playlists) {
            const key = pl.name.trim().toLowerCase();
            if (mirroredNames.has(key) || syncedNames.has(key)) {
                pl._synced = true;
                synced.push(pl);
            } else {
                pl._synced = false;
                unsynced.push(pl);
            }
        }

        _serverPlaylists = [...synced, ...unsynced];
        const title = document.getElementById('server-tab-title');
        const serverName = data.server_type ? data.server_type.charAt(0).toUpperCase() + data.server_type.slice(1) : '';
        if (title) title.textContent = `Server Playlists (${serverName})`;

        if (synced.length === 0 && unsynced.length === 0) {
            if (container) container.innerHTML = '<div class="playlist-placeholder">No playlists found on your media server.</div>';
            return;
        }

        // Server type icon SVG
        const serverIcons = {
            plex: '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M11.643 0H4.68l7.679 12L4.68 24h6.963L19.32 12z"/></svg>',
            jellyfin: '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 2C8.5 2 6 5.1 6 9c0 2.4 1.2 5.5 3.3 8.7.7 1 1.5 2 2.2 2.9.2.3.4.3.5.4.1 0 .3-.1.5-.4.7-.9 1.5-1.9 2.2-2.9C16.8 14.5 18 11.4 18 9c0-3.9-2.5-7-6-7z"/></svg>',
            navidrome: '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>'
        };
        const sIcon = serverIcons[data.server_type] || serverIcons.plex;

        function _renderPlCard(pl, i, isSynced) {
            const hue = (i * 37 + 200) % 360;
            const safeName = _esc(pl.name).replace(/'/g, "\\'");
            const cardClass = isSynced ? 'server-pl-card' : 'server-pl-card server-pl-unsynced';
            const action = isSynced ? 'Open Editor' : 'View Tracks';
            return `
            <div class="${cardClass}" onclick="openServerPlaylistEditor('${pl.id}', '${safeName}')" style="animation-delay: ${i * 0.04}s; --card-hue: ${hue}">
                <div class="server-pl-card-glow"></div>
                <div class="server-pl-card-top">
                    <div class="server-pl-card-icon-wrap">
                        <div class="server-pl-card-bars">
                            <span></span><span></span><span></span><span></span>
                        </div>
                    </div>
                    <div class="server-pl-card-badge">${sIcon}</div>
                </div>
                <div class="server-pl-card-body">
                    <div class="server-pl-card-name">${_esc(pl.name)}</div>
                    <div class="server-pl-card-meta">
                        <span class="server-pl-track-count">${pl.track_count}</span> tracks
                        ${isSynced ? '<span class="server-pl-synced-badge">Synced</span>' : ''}
                    </div>
                </div>
                <div class="server-pl-card-footer">
                    <span class="server-pl-card-action">${action}</span>
                    <span class="server-pl-card-arrow">
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
                    </span>
                </div>
            </div>`;
        }

        let html = '';

        if (synced.length > 0) {
            html += `<div class="server-pl-section">
                <div class="server-pl-section-header">
                    <span class="server-pl-section-icon">&#128279;</span>
                    <span class="server-pl-section-title">Synced Playlists</span>
                    <span class="server-pl-section-count">${synced.length}</span>
                </div>
                <div class="server-pl-grid">${synced.map((pl, i) => _renderPlCard(pl, i, true)).join('')}</div>
            </div>`;
        }

        if (unsynced.length > 0) {
            html += `<div class="server-pl-section server-pl-section-unsynced">
                <div class="server-pl-section-header">
                    <span class="server-pl-section-icon">&#127925;</span>
                    <span class="server-pl-section-title">Other Server Playlists</span>
                    <span class="server-pl-section-count">${unsynced.length}</span>
                </div>
                <div class="server-pl-grid">${unsynced.map((pl, i) => _renderPlCard(pl, i + synced.length, false)).join('')}</div>
            </div>`;
        }

        container.innerHTML = html;

    } catch (e) {
        if (container) container.innerHTML = `<div class="playlist-placeholder">Error: ${e.message}</div>`;
    } finally {
        if (btn) { btn.disabled = false; btn.textContent = '🔄 Refresh'; }
    }
}

async function openServerPlaylistEditor(playlistId, playlistName) {
    // Step 1: Look up mirrored playlists by name
    let mirroredPlaylists = [];
    try {
        const res = await fetch('/api/mirrored-playlists');
        const all = await res.json();
        mirroredPlaylists = (Array.isArray(all) ? all : []).filter(p =>
            p.name.trim().toLowerCase() === playlistName.trim().toLowerCase()
        );
    } catch (e) {
        console.error('Failed to fetch mirrored playlists:', e);
    }

    if (mirroredPlaylists.length === 1) {
        // Single match — go straight to compare
        _openServerCompareView(playlistId, playlistName, mirroredPlaylists[0]);
    } else if (mirroredPlaylists.length === 0) {
        // No match — server-only view
        _openServerCompareView(playlistId, playlistName, null);
    } else {
        // Multiple — disambiguation
        _showServerDisambig(playlistId, playlistName, mirroredPlaylists);
    }
}

// ── Disambiguation ──

function _showServerDisambig(playlistId, playlistName, candidates) {
    const overlay = document.getElementById('server-disambig-overlay');
    const list = document.getElementById('server-disambig-list');
    const subtitle = document.getElementById('server-disambig-subtitle');
    if (!overlay || !list) return;

    if (subtitle) subtitle.textContent = `"${playlistName}" was found on ${candidates.length} sources. Which one do you want to compare against?`;

    const sourceIcons = { spotify: '🟢', tidal: '🌊', youtube: '▶️', beatport: '🎛️', deezer: '🟣', file: '📄' };

    list.innerHTML = candidates.map((p, i) => {
        const icon = sourceIcons[p.source] || '📋';
        const ago = timeAgo(p.mirrored_at || p.updated_at);
        return `
        <div class="server-disambig-card" onclick="selectDisambigPlaylist('${playlistId}', '${_esc(playlistName).replace(/'/g, "\\'")}', ${p.id})" style="animation-delay: ${i * 0.06}s">
            <div class="server-disambig-icon">${icon}</div>
            <div class="server-disambig-info">
                <div class="server-disambig-name">${_esc(p.name)}</div>
                <div class="server-disambig-details">
                    <span class="source-badge">${_esc(p.source)}</span>
                    <span>${p.track_count || 0} tracks</span>
                    ${p.owner ? `<span>by ${_esc(p.owner)}</span>` : ''}
                    <span>Mirrored ${ago}</span>
                </div>
            </div>
            <div class="server-disambig-arrow">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
            </div>
        </div>`;
    }).join('');

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

    // Escape key + click backdrop to close
    overlay.onclick = e => { if (e.target === overlay) closeServerDisambig(); };
    window._disambigEsc = e => { if (e.key === 'Escape') closeServerDisambig(); };
    document.addEventListener('keydown', window._disambigEsc);
}

function closeServerDisambig() {
    const overlay = document.getElementById('server-disambig-overlay');
    if (overlay) {
        overlay.classList.remove('visible');
        setTimeout(() => overlay.classList.add('hidden'), 250);
    }
    if (window._disambigEsc) { document.removeEventListener('keydown', window._disambigEsc); window._disambigEsc = null; }
}

async function selectDisambigPlaylist(playlistId, playlistName, mirroredId) {
    closeServerDisambig();
    try {
        const res = await fetch(`/api/mirrored-playlists/${mirroredId}`);
        const mirrored = await res.json();
        _openServerCompareView(playlistId, playlistName, mirrored);
    } catch (e) {
        showToast('Failed to load mirrored playlist: ' + e.message, 'error');
    }
}

// ── Compare View ──

async function _openServerCompareView(playlistId, playlistName, mirroredPlaylist) {
    const container = document.getElementById('server-playlist-container');
    const editor = document.getElementById('server-editor');
    if (!editor) return;

    if (container) container.style.display = 'none';
    editor.style.display = '';

    const nameEl = document.getElementById('server-editor-name');
    const metaEl = document.getElementById('server-editor-meta');
    const banner = document.getElementById('server-no-source-banner');
    const sourceScroll = document.getElementById('server-col-source-scroll');
    const serverScroll = document.getElementById('server-col-server-scroll');

    if (nameEl) nameEl.textContent = playlistName;
    if (metaEl) metaEl.textContent = 'Loading comparison...';
    if (banner) banner.style.display = 'none';
    if (sourceScroll) sourceScroll.innerHTML = '<div style="text-align:center;padding:30px;color:rgba(255,255,255,0.2);font-size:12px">Loading...</div>';
    if (serverScroll) serverScroll.innerHTML = '<div style="text-align:center;padding:30px;color:rgba(255,255,255,0.2);font-size:12px">Loading...</div>';

    // Store state
    _serverEditorState = {
        playlistId,
        playlistName,
        mirroredPlaylist,
        tracks: [],
    };

    // Build API URL
    let url = `/api/server/playlist/${playlistId}/tracks?name=${encodeURIComponent(playlistName)}`;
    if (mirroredPlaylist && mirroredPlaylist.id) {
        url += `&mirrored_playlist_id=${mirroredPlaylist.id}`;
    }

    try {
        const response = await fetch(url);
        const data = await response.json();
        if (!data.success) {
            if (metaEl) metaEl.textContent = data.error || 'Failed to load';
            return;
        }

        _serverEditorState.tracks = data.tracks || [];
        _serverEditorState.serverType = data.server_type;

        const tracks = _serverEditorState.tracks;
        const serverLabel = data.server_type ? data.server_type.charAt(0).toUpperCase() + data.server_type.slice(1) : 'Server';

        // Header metadata
        if (metaEl) metaEl.textContent = `${serverLabel} · ${data.server_track_count || 0} server tracks · ${data.source_track_count || 0} source tracks`;

        // Show no-source banner if needed
        if (!mirroredPlaylist && banner) {
            banner.style.display = '';
        }

        // Stats, filter counts, footer
        _updateCompareStats(tracks);

        // Column headers
        const sourceLabel = mirroredPlaylist ? (mirroredPlaylist.source || 'source').charAt(0).toUpperCase() + (mirroredPlaylist.source || 'source').slice(1) : 'Source';
        const sourceIconMap = { spotify: '🟢', tidal: '🌊', youtube: '▶️', beatport: '🎛️', deezer: '🟣', file: '📄' };
        const serverIconMap = { plex: '🟠', jellyfin: '🟣', navidrome: '🔵' };

        const srcIconEl = document.getElementById('server-col-source-icon');
        const srcLabelEl = document.getElementById('server-col-source-label');
        const srcCountEl = document.getElementById('server-col-source-count');
        const svrIconEl = document.getElementById('server-col-server-icon');
        const svrLabelEl = document.getElementById('server-col-server-label');
        const svrCountEl = document.getElementById('server-col-server-count');

        if (srcIconEl) srcIconEl.textContent = mirroredPlaylist ? (sourceIconMap[mirroredPlaylist.source] || '📋') : '📋';
        if (srcLabelEl) srcLabelEl.textContent = sourceLabel;
        if (srcCountEl) srcCountEl.textContent = `${data.source_track_count || 0} tracks`;
        if (svrIconEl) svrIconEl.textContent = serverIconMap[data.server_type] || '💻';
        if (svrLabelEl) svrLabelEl.textContent = serverLabel;
        if (svrCountEl) svrCountEl.textContent = `${data.server_track_count || 0} tracks`;

        // Render columns
        _renderCompareColumns(tracks);

        // Scroll linking
        _setupScrollLinking();

    } catch (e) {
        if (metaEl) metaEl.textContent = 'Error: ' + e.message;
    }
}

function _updateCompareStats(tracks) {
    const matched = tracks.filter(t => t.match_status === 'matched').length;
    const missing = tracks.filter(t => t.match_status === 'missing').length;
    const extra = tracks.filter(t => t.match_status === 'extra').length;

    const statsEl = document.getElementById('server-editor-stats');
    if (statsEl) {
        statsEl.innerHTML = `
            <div class="server-editor-stat"><div class="server-editor-stat-num matched">${matched}</div><div class="server-editor-stat-label">Matched</div></div>
            <div class="server-editor-stat"><div class="server-editor-stat-num missing">${missing}</div><div class="server-editor-stat-label">Missing</div></div>
            ${extra > 0 ? `<div class="server-editor-stat"><div class="server-editor-stat-num extra">${extra}</div><div class="server-editor-stat-label">Extra</div></div>` : ''}
        `;
    }

    const editor = document.getElementById('server-editor');
    if (editor) {
        editor.querySelectorAll('.discog-filter').forEach(btn => {
            const f = btn.dataset.filter;
            if (f === 'all') btn.textContent = `All (${tracks.length})`;
            else if (f === 'matched') btn.textContent = `Matched (${matched})`;
            else if (f === 'missing') btn.textContent = `Missing (${missing})`;
            else if (f === 'extra') btn.textContent = `Extra (${extra})`;
        });
    }

    const footer = document.getElementById('server-editor-footer');
    if (footer) footer.textContent = `${matched}/${matched + missing} matched${extra > 0 ? ` · ${extra} extra on server` : ''}`;
}

function _formatDurationMs(ms) {
    if (!ms) return '';
    const s = Math.round(ms / 1000);
    return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
}

function _renderCompareColumns(tracks) {
    const sourceScroll = document.getElementById('server-col-source-scroll');
    const serverScroll = document.getElementById('server-col-server-scroll');
    if (!sourceScroll || !serverScroll) return;

    let sourceHTML = '';
    let serverHTML = '';

    tracks.forEach((t, i) => {
        const src = t.source_track;
        const svr = t.server_track;
        const status = t.match_status;
        const pairId = `pair-${i}`;

        // ── Source (left) column ──
        if (src) {
            const dur = _formatDurationMs(src.duration_ms);
            sourceHTML += `
            <div class="server-track-item ${status}" data-pair-id="${pairId}" data-index="${i}" data-status="${status}"
                 onclick="_compareTrackClick('source', ${i})">
                <div class="server-track-num">${src.position != null ? src.position : i + 1}</div>
                <div class="server-track-art">
                    ${src.image_url ? `<img src="${src.image_url}" alt="" loading="lazy">` : '<div class="server-track-art-empty"></div>'}
                </div>
                <div class="server-track-info">
                    <div class="server-track-title">${_esc(src.name)}</div>
                    <div class="server-track-artist">${_esc(src.artist || '')}</div>
                </div>
                <div class="server-track-duration">${dur}</div>
                <div class="server-track-status-dot"></div>
            </div>`;
        } else {
            // Extra track — no source
            sourceHTML += `
            <div class="server-track-item extra-gap" data-pair-id="${pairId}" data-index="${i}" data-status="${status}">
                <div class="server-track-empty-slot extra">
                    <span class="empty-slot-label">No source track</span>
                </div>
            </div>`;
        }

        // ── Server (right) column ──
        if (svr) {
            const dur = _formatDurationMs(svr.duration);
            const conf = t.confidence != null ? t.confidence : null;
            let confBadge = '';
            if (status === 'matched' && conf != null) {
                const pct = Math.round(conf * 100);
                const cls = pct >= 100 ? 'exact' : pct >= 90 ? 'high' : 'fuzzy';
                confBadge = `<span class="server-track-conf ${cls}" title="Title similarity">${pct}%</span>`;
            }
            serverHTML += `
            <div class="server-track-item ${status}" data-pair-id="${pairId}" data-index="${i}" data-status="${status}"
                 onclick="_compareTrackClick('server', ${i})">
                <div class="server-track-num">${i + 1}</div>
                <div class="server-track-art">
                    ${svr.thumb ? `<img src="${svr.thumb}" alt="" loading="lazy">` : '<div class="server-track-art-empty"></div>'}
                </div>
                <div class="server-track-info">
                    <div class="server-track-title">${_esc(svr.title)}</div>
                    <div class="server-track-artist">${_esc(svr.artist || '')}</div>
                </div>
                ${confBadge}
                <div class="server-track-duration">${dur}</div>
                <div class="server-track-actions">
                    ${status === 'matched' ? `<button class="server-track-swap-btn" onclick="event.stopPropagation(); serverSearchReplace(${i}, 'replace')" title="Swap for different version">
                        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M7 16V4m0 0L3 8m4-4l4 4M17 8v12m0 0l4-4m-4 4l-4-4"/></svg>
                    </button>` : ''}
                    <button class="server-track-remove-btn" onclick="event.stopPropagation(); _serverRemoveTrack(${i}, '${svr.id}')" title="Remove from playlist">
                        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
                    </button>
                </div>
                <div class="server-track-status-dot"></div>
            </div>`;
        } else {
            // Missing on server — clickable empty slot
            const hint = src ? `${src.artist || ''} — ${src.name}` : '';
            serverHTML += `
            <div class="server-track-item empty-slot-wrap" data-pair-id="${pairId}" data-index="${i}" data-status="${status}"
                 onclick="serverSearchReplace(${i}, 'add')">
                <div class="server-track-empty-slot missing">
                    <div class="empty-slot-icon">
                        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
                    </div>
                    <span class="empty-slot-label">Find &amp; add</span>
                    <span class="empty-slot-hint">${_esc(hint)}</span>
                </div>
            </div>`;
        }
    });

    sourceScroll.innerHTML = sourceHTML;
    serverScroll.innerHTML = serverHTML;
}

function _setupScrollLinking() {
    const sourceScroll = document.getElementById('server-col-source-scroll');
    const serverScroll = document.getElementById('server-col-server-scroll');
    if (!sourceScroll || !serverScroll) return;

    // Remove old listeners to prevent accumulation on refresh
    if (window._serverScrollAC) window._serverScrollAC.abort();
    window._serverScrollAC = new AbortController();
    const signal = window._serverScrollAC.signal;

    let syncing = false;

    const syncScroll = (from, to) => {
        if (syncing) return;
        syncing = true;
        const maxFrom = from.scrollHeight - from.clientHeight;
        const maxTo = to.scrollHeight - to.clientHeight;
        if (maxFrom > 0 && maxTo > 0) {
            to.scrollTop = (from.scrollTop / maxFrom) * maxTo;
        }
        requestAnimationFrame(() => { syncing = false; });
    };

    sourceScroll.addEventListener('scroll', () => syncScroll(sourceScroll, serverScroll), { signal });
    serverScroll.addEventListener('scroll', () => syncScroll(serverScroll, sourceScroll), { signal });
}

function _compareTrackClick(side, index) {
    const otherSide = side === 'source' ? 'server' : 'source';
    const otherScroll = document.getElementById(`server-col-${otherSide}-scroll`);
    const pairId = `pair-${index}`;

    // Clear previous highlights
    document.querySelectorAll('.server-track-item.highlighted').forEach(el => el.classList.remove('highlighted'));

    // Highlight both paired items
    document.querySelectorAll(`[data-pair-id="${pairId}"]`).forEach(el => el.classList.add('highlighted'));

    // Scroll the OTHER column to show the paired item
    const target = otherScroll?.querySelector(`[data-pair-id="${pairId}"]`);
    if (target) {
        target.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
}

function _serverEditorRefresh() {
    _openServerCompareView(_serverEditorState.playlistId, _serverEditorState.playlistName, _serverEditorState.mirroredPlaylist);
}

function serverEditorBack() {
    const container = document.getElementById('server-playlist-container');
    const editor = document.getElementById('server-editor');
    if (editor) editor.style.display = 'none';
    if (container) container.style.display = '';
}

function _serverEditorFilter(btn, filter) {
    btn.closest('.server-editor-filters').querySelectorAll('.discog-filter').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');

    // Filter both columns simultaneously
    ['server-col-source-scroll', 'server-col-server-scroll'].forEach(colId => {
        document.querySelectorAll(`#${colId} .server-track-item`).forEach(item => {
            const status = item.dataset.status;
            item.style.display = (filter === 'all' || status === filter) ? '' : 'none';
        });
    });
}

// ── Track Search / Replace ──

async function serverSearchReplace(trackIndex, mode) {
    const track = _serverEditorState.tracks[trackIndex];
    if (!track) return;

    const src = track.source_track || {};
    const svr = track.server_track || {};
    // Search by track name only first (more reliable than "artist trackname" blob)
    const searchQuery = src.name ? src.name.trim() : (svr.title || '').trim();
    const contextArtist = src.artist || svr.artist || '';
    const contextName = src.name || svr.title || '';

    const existing = document.getElementById('server-search-overlay');
    if (existing) existing.remove();

    const overlay = document.createElement('div');
    overlay.id = 'server-search-overlay';
    overlay.className = 'server-search-overlay';
    overlay.innerHTML = `
        <div class="server-search-popover" id="server-search-popover">
            <div class="server-search-header">
                <div>
                    <div class="server-search-title">${mode === 'replace' ? 'Swap Track' : 'Add Track to Server'}</div>
                    ${contextName ? `<div class="server-search-context">
                        <span class="server-search-context-label">Source:</span>
                        <span class="server-search-context-artist">${_esc(contextArtist)}</span>
                        <span class="server-search-context-sep">—</span>
                        <span class="server-search-context-name">${_esc(contextName)}</span>
                    </div>` : ''}
                </div>
                <button class="server-search-close" onclick="document.getElementById('server-search-overlay')?.remove()">&times;</button>
            </div>
            <div class="server-search-input-wrap">
                <div class="server-search-input-icon">
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
                </div>
                <input type="text" class="server-search-input" id="server-search-input" value="${_esc(searchQuery)}" placeholder="Search by track name, artist, or album..." onkeydown="if(event.key==='Enter') _serverSearchExecute()">
            </div>
            <div class="server-search-results-header" id="server-search-results-header"></div>
            <div class="server-search-results" id="server-search-results">
                <div class="server-search-hint">
                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" style="margin-bottom:6px;opacity:0.4"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
                    <br>Searching...
                </div>
            </div>
        </div>
    `;
    // Click overlay background or press Escape to close
    overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
    overlay._escHandler = e => { if (e.key === 'Escape') overlay.remove(); };
    document.addEventListener('keydown', overlay._escHandler);
    // Clean up Escape listener when overlay is removed
    const obs = new MutationObserver(() => {
        if (!document.body.contains(overlay)) { document.removeEventListener('keydown', overlay._escHandler); obs.disconnect(); }
    });
    obs.observe(document.body, { childList: true });

    const popover = overlay.querySelector('.server-search-popover');
    popover.dataset.trackIndex = trackIndex;
    popover.dataset.mode = mode;

    document.body.appendChild(overlay);
    requestAnimationFrame(() => overlay.classList.add('visible'));
    document.getElementById('server-search-input')?.focus();
    document.getElementById('server-search-input')?.select();

    _serverSearchExecute();
}

async function _serverSearchExecute() {
    const input = document.getElementById('server-search-input');
    const results = document.getElementById('server-search-results');
    const resultsHeader = document.getElementById('server-search-results-header');
    const popover = document.getElementById('server-search-popover');
    if (!input || !results || !popover) return;

    const query = input.value.trim();
    if (!query) {
        results.innerHTML = '<div class="server-search-hint">Type a search query</div>';
        if (resultsHeader) resultsHeader.textContent = '';
        return;
    }

    results.innerHTML = '<div class="server-search-hint"><div class="server-search-spinner"></div>Searching library...</div>';
    if (resultsHeader) resultsHeader.textContent = '';

    try {
        const response = await fetch(`/api/library/search-tracks?q=${encodeURIComponent(query)}&limit=20`);
        const data = await response.json();

        if (!data.success || !data.tracks || data.tracks.length === 0) {
            results.innerHTML = `<div class="server-search-hint">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" style="margin-bottom:6px;opacity:0.3"><path d="M9.172 14.828a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                <br>No results found<br><span style="font-size:10px;opacity:0.5">Try different keywords or a shorter query</span>
            </div>`;
            return;
        }

        const trackIndex = parseInt(popover.dataset.trackIndex);
        const mode = popover.dataset.mode;

        if (resultsHeader) resultsHeader.textContent = `${data.tracks.length} result${data.tracks.length !== 1 ? 's' : ''}`;

        results.innerHTML = data.tracks.map((t, i) => {
            const ext = (t.file_path || '').split('.').pop().toUpperCase();
            const format = ['FLAC', 'MP3', 'OPUS', 'OGG', 'M4A', 'AAC', 'WAV'].includes(ext) ? (ext === 'M4A' ? 'AAC' : ext) : '';
            const dur = _formatDurationMs(t.duration);
            const bitrateStr = t.bitrate ? `${t.bitrate}k` : '';
            return `
                <div class="server-search-result" onclick="_serverSelectTrack(${trackIndex}, '${mode}', '${t.id}', this)" style="animation-delay:${i * 0.03}s">
                    <div class="server-search-result-art">
                        ${t.album_thumb_url ? `<img src="${t.album_thumb_url}" alt="" loading="lazy">` : '<div class="server-search-result-art-empty"></div>'}
                    </div>
                    <div class="server-search-result-info">
                        <div class="server-search-result-title">${_esc(t.title)}</div>
                        <div class="server-search-result-meta">${_esc(t.artist_name)}${t.album_title ? ` · ${_esc(t.album_title)}` : ''}</div>
                    </div>
                    <div class="server-search-result-details">
                        ${format ? `<span class="server-search-format">${format}</span>` : ''}
                        ${bitrateStr ? `<span class="server-search-bitrate">${bitrateStr}</span>` : ''}
                        ${dur ? `<span class="server-search-dur">${dur}</span>` : ''}
                    </div>
                    <button class="server-search-select-btn">Select</button>
                </div>
            `;
        }).join('');

    } catch (e) {
        results.innerHTML = `<div class="server-search-hint">Error: ${e.message}</div>`;
    }
}

async function _serverSelectTrack(trackIndex, mode, newTrackId, el) {
    const track = _serverEditorState.tracks[trackIndex];
    if (!track) return;

    const btn = el.querySelector('.server-search-select-btn');
    if (btn) { btn.disabled = true; btn.textContent = '...'; }

    try {
        let response;
        if (mode === 'replace') {
            response = await fetch(`/api/server/playlist/${_serverEditorState.playlistId}/replace-track`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    old_track_id: track.server_track?.id,
                    new_track_id: newTrackId,
                    playlist_name: _serverEditorState.playlistName,
                })
            });
        } else {
            // Calculate the server-side position for this track
            // Count how many server tracks exist before this index
            let serverPos = 0;
            for (let k = 0; k < trackIndex; k++) {
                if (_serverEditorState.tracks[k]?.server_track) serverPos++;
            }
            // source_track carries source_track_id (Spotify ID) when this
            // came from a mirrored playlist — the backend uses it to
            // persist the Find & Add selection as a permanent match
            // override so future syncs auto-pair without user action.
            const srcTrack = track.source_track || {};
            response = await fetch(`/api/server/playlist/${_serverEditorState.playlistId}/add-track`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    track_id: newTrackId,
                    playlist_name: _serverEditorState.playlistName,
                    position: serverPos,
                    source_track_id: srcTrack.source_track_id || '',
                    source_title: srcTrack.name || '',
                    source_artist: srcTrack.artist || '',
                })
            });
        }

        const data = await response.json();
        if (data.success) {
            showToast(data.message || 'Track updated', 'success');
            document.getElementById('server-search-overlay')?.remove();
            // Update playlist ID if server recreated it (Plex deletes+recreates)
            if (data.new_playlist_id) _serverEditorState.playlistId = data.new_playlist_id;

            // Re-fetch from server so the compare view reflects the actual server state
            // and the matching algorithm can correctly wire up the newly added/replaced track
            _openServerCompareView(_serverEditorState.playlistId, _serverEditorState.playlistName, _serverEditorState.mirroredPlaylist);
        } else {
            showToast(data.error || 'Failed to update track', 'error');
            if (btn) { btn.disabled = false; btn.textContent = 'Select'; }
        }
    } catch (e) {
        showToast('Error: ' + e.message, 'error');
        if (btn) { btn.disabled = false; btn.textContent = 'Select'; }
    }
}

async function _serverRemoveTrack(trackIndex, serverTrackId) {
    if (!serverTrackId) return;

    const track = _serverEditorState.tracks[trackIndex];
    const trackTitle = track?.server_track?.title || 'this track';

    if (!await showConfirmDialog({ title: 'Remove Track', message: `Remove "${trackTitle}" from this playlist?`, confirmText: 'Remove', destructive: true })) return;

    try {
        const response = await fetch(`/api/server/playlist/${_serverEditorState.playlistId}/remove-track`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                track_id: serverTrackId,
                playlist_name: _serverEditorState.playlistName,
            })
        });

        const data = await response.json();
        if (data.success) {
            showToast(data.message || 'Track removed', 'success');
            const pid = data.new_playlist_id || _serverEditorState.playlistId;
            _serverEditorState.playlistId = pid;
            _openServerCompareView(pid, _serverEditorState.playlistName, _serverEditorState.mirroredPlaylist);
        } else {
            showToast(data.error || 'Failed to remove track', 'error');
        }
    } catch (e) {
        showToast('Error: ' + e.message, 'error');
    }
}


// Auto-refresh sync cards every 30 seconds when on dashboard
setInterval(() => {
    if (typeof currentPage !== 'undefined' && currentPage === 'dashboard') {
        loadDashboardSyncHistory();
    }
}, 30000);

async function loadDashboardSyncHistory() {
    const container = document.getElementById('sync-history-cards');
    if (!container) return;

    try {
        const response = await fetch('/api/sync/history?limit=10');
        if (!response.ok) return;

        const data = await response.json();
        // Filter to only show playlist syncs — not album downloads or wishlist processing
        const entries = (data.entries || []).filter(e => e.sync_type === 'playlist' || !e.sync_type);

        if (entries.length === 0) {
            container.innerHTML = '<div class="sync-history-empty">No syncs yet</div>';
            return;
        }

        container.innerHTML = entries.map((entry, cardIndex) => {
            const found = entry.tracks_found || 0;
            const total = entry.total_tracks || 0;
            const downloaded = entry.tracks_downloaded || 0;
            const failed = entry.tracks_failed || 0;
            const pct = total > 0 ? Math.round((found / total) * 100) : 0;

            // Health color
            let healthClass = 'health-good';
            if (pct < 50) healthClass = 'health-bad';
            else if (pct < 80) healthClass = 'health-warn';

            // Source badge
            const sourceLabels = { spotify: 'Spotify', tidal: 'Tidal', deezer: 'Deezer', youtube: 'YouTube', beatport: 'Beatport', wishlist: 'Wishlist' };
            const sourceLabel = sourceLabels[entry.source] || entry.source || 'Unknown';

            // Time
            const timeStr = entry.started_at ? _relativeTime(entry.started_at) : '';

            // Name
            const name = entry.artist_name
                ? `${entry.artist_name} — ${entry.album_name || entry.playlist_name}`
                : entry.playlist_name || 'Unknown';

            return `
                <div class="sync-history-card ${healthClass}" onclick="openSyncDetailModal(${entry.id})" style="animation-delay: ${cardIndex * 0.05}s">
                    <button class="sync-card-delete" onclick="event.stopPropagation(); deleteSyncHistoryCard(${entry.id}, this)" title="Remove">&times;</button>
                    <div class="sync-card-thumb">
                        ${entry.thumb_url ? `<img src="${entry.thumb_url}" alt="" loading="lazy">` : '<div class="sync-card-thumb-placeholder">&#9835;</div>'}
                    </div>
                    <div class="sync-card-info">
                        <div class="sync-card-name">${typeof _esc === 'function' ? _esc(name) : name}</div>
                        <div class="sync-card-meta">
                            <span class="sync-card-source">${sourceLabel}</span>
                            <span class="sync-card-time">${timeStr}</span>
                        </div>
                    </div>
                    <div class="sync-card-stats">
                        <div class="sync-card-pct">${pct}%</div>
                        <div class="sync-card-bar">
                            <div class="sync-card-bar-fill" style="width: ${pct}%"></div>
                        </div>
                        <div class="sync-card-counts">${found}/${total} matched${downloaded > 0 ? ` · ${downloaded} ⬇` : ''}${failed > 0 ? ` · ${failed} ✗` : ''}</div>
                    </div>
                </div>
            `;
        }).join('');

    } catch (e) {
        console.warn('Failed to load sync history for dashboard:', e);
    }
}

function _relativeTime(dateStr) {
    try {
        const d = new Date(dateStr);
        const now = new Date();
        const diffMs = now - d;
        const mins = Math.floor(diffMs / 60000);
        if (mins < 1) return 'just now';
        if (mins < 60) return `${mins}m ago`;
        const hrs = Math.floor(mins / 60);
        if (hrs < 24) return `${hrs}h ago`;
        const days = Math.floor(hrs / 24);
        if (days < 7) return `${days}d ago`;
        return d.toLocaleDateString();
    } catch (e) { return ''; }
}

async function openSyncDetailModal(entryId) {
    try {
        showLoadingOverlay('Loading sync details...');
        const response = await fetch(`/api/sync/history/${entryId}`);
        const data = await response.json();
        hideLoadingOverlay();

        if (!data.success || !data.entry) {
            showToast('Could not load sync details', 'error');
            return;
        }

        const entry = data.entry;
        const trackResults = entry.track_results || [];
        const name = entry.artist_name
            ? `${entry.artist_name} — ${entry.album_name || entry.playlist_name}`
            : entry.playlist_name || 'Unknown';

        // Build modal
        const overlay = document.createElement('div');
        overlay.className = 'discog-modal-overlay';
        overlay.id = 'sync-detail-overlay';

        const found = entry.tracks_found || 0;
        const total = entry.total_tracks || 0;
        const downloaded = entry.tracks_downloaded || 0;

        let trackRowsHtml = '';
        if (trackResults.length > 0) {
            trackRowsHtml = trackResults.map((t, i) => {
                const statusIcon = t.status === 'found' ? '✅' : '❌';
                const statusClass = t.status === 'found' ? 'matched' : 'unmatched';
                const confPct = Math.round((t.confidence || 0) * 100);
                const confClass = confPct >= 80 ? 'conf-high' : confPct >= 50 ? 'conf-mid' : 'conf-low';
                let dlIcon = '';
                if (t.download_status === 'completed') dlIcon = '✅';
                else if (t.download_status === 'failed') dlIcon = '❌';
                else if (t.download_status === 'not_found') dlIcon = '🔇';
                else if (t.download_status === 'cancelled') dlIcon = '🚫';

                let dlDisplay = dlIcon;
                if (!dlDisplay && t.download_status === 'wishlist') dlDisplay = '<span class="sync-dl-wishlist">→ Wishlist</span>';

                return `
                    <tr class="sync-detail-row ${statusClass}">
                        <td class="sync-detail-num">${i + 1}</td>
                        <td class="sync-detail-art">
                            ${t.image_url ? `<img src="${t.image_url}" alt="" loading="lazy">` : '<div class="sync-detail-art-empty"></div>'}
                        </td>
                        <td class="sync-detail-track">${_esc(t.name || '')}</td>
                        <td class="sync-detail-artist">${_esc(t.artist || '')}</td>
                        <td class="sync-detail-album">${_esc(t.album || '')}</td>
                        <td class="sync-detail-status">${statusIcon}</td>
                        <td class="sync-detail-conf"><span class="conf-badge ${confClass}">${confPct}%</span></td>
                        <td class="sync-detail-dl">${dlDisplay}</td>
                    </tr>
                `;
            }).join('');
        } else {
            // Fallback to tracks_json if no track_results (old syncs before data caching)
            const tracks = entry.tracks || [];
            const esc = typeof _esc === 'function' ? _esc : s => s;
            trackRowsHtml = `
                <tr><td colspan="8" class="sync-detail-notice">
                    <div class="sync-detail-notice-text">Per-track match data not available for this sync.<br>Re-sync this playlist to see detailed match results.</div>
                </td></tr>
            ` + tracks.map((t, i) => {
                const artists = t.artists || [];
                const artistName = artists.length > 0 ? (typeof artists[0] === 'string' ? artists[0] : artists[0]?.name || '') : '';
                const albumName = typeof t.album === 'object' ? (t.album?.name || '') : (t.album || '');
                return `
                    <tr class="sync-detail-row no-data">
                        <td class="sync-detail-num">${i + 1}</td>
                        <td class="sync-detail-art"></td>
                        <td class="sync-detail-track">${esc(t.name || '')}</td>
                        <td class="sync-detail-artist">${esc(artistName)}</td>
                        <td class="sync-detail-album">${esc(albumName)}</td>
                        <td class="sync-detail-status" colspan="3"></td>
                    </tr>
                `;
            }).join('');
        }

        // Count stats for filter bar
        const matchedCount = trackResults.filter(t => t.status === 'found').length;
        const unmatchedCount = trackResults.filter(t => t.status !== 'found').length;
        const downloadedCount = trackResults.filter(t => t.download_status === 'completed').length;

        overlay.innerHTML = `
            <div class="discog-modal">
                <div class="discog-modal-hero" ${entry.thumb_url ? `style="background-image:url('${entry.thumb_url}')"` : ''}>
                    <div class="discog-modal-hero-overlay"></div>
                    <div class="discog-modal-hero-content">
                        <h2 class="discog-modal-title">Sync Details</h2>
                        <p class="discog-modal-artist">${_esc(name)}</p>
                    </div>
                    <button class="discog-modal-close" onclick="document.getElementById('sync-detail-overlay')?.remove()">&times;</button>
                </div>
                <div class="discog-filter-bar">
                    <div class="discog-filters">
                        <button class="discog-filter active" data-filter="all" onclick="_syncDetailFilter(this, 'all')">All (${total})</button>
                        <button class="discog-filter" data-filter="matched" onclick="_syncDetailFilter(this, 'matched')">Matched (${matchedCount})</button>
                        <button class="discog-filter" data-filter="unmatched" onclick="_syncDetailFilter(this, 'unmatched')">Unmatched (${unmatchedCount})</button>
                        ${downloadedCount > 0 ? `<button class="discog-filter" data-filter="downloaded" onclick="_syncDetailFilter(this, 'downloaded')">Downloaded (${downloadedCount})</button>` : ''}
                    </div>
                </div>
                <div class="sync-detail-table-wrap">
                    <table class="sync-detail-table">
                        <thead>
                            <tr>
                                <th>#</th>
                                <th></th>
                                <th>Track</th>
                                <th>Artist</th>
                                <th>Album</th>
                                <th>Match</th>
                                <th>Conf.</th>
                                <th>Status</th>
                            </tr>
                        </thead>
                        <tbody id="sync-detail-tbody">
                            ${trackRowsHtml}
                        </tbody>
                    </table>
                </div>
                <div class="discog-footer">
                    <div class="discog-footer-info">${found} matched · ${downloaded} downloaded · ${total} total</div>
                    <div class="discog-footer-actions">
                        <button class="discog-cancel-btn" onclick="document.getElementById('sync-detail-overlay')?.remove()">Close</button>
                    </div>
                </div>
            </div>
        `;

        document.body.appendChild(overlay);
        requestAnimationFrame(() => overlay.classList.add('visible'));

    } catch (e) {
        hideLoadingOverlay();
        showToast('Failed to load sync details', 'error');
    }
}

async function deleteSyncHistoryCard(entryId, btnEl) {
    try {
        const card = btnEl.closest('.sync-history-card');
        if (card) {
            card.style.opacity = '0';
            card.style.transform = 'scale(0.9)';
        }
        const resp = await fetch(`/api/sync/history/${entryId}`, { method: 'DELETE' });
        if (resp.ok) {
            setTimeout(() => { if (card) card.remove(); }, 200);
        }
    } catch (e) {
        console.warn('Failed to delete sync entry:', e);
    }
}

function _syncDetailFilter(btn, filter) {
    // Update active button
    btn.closest('.discog-filters').querySelectorAll('.discog-filter').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');

    // Filter rows
    document.querySelectorAll('#sync-detail-tbody .sync-detail-row').forEach(row => {
        if (filter === 'all') {
            row.style.display = '';
        } else if (filter === 'matched') {
            row.style.display = row.classList.contains('matched') ? '' : 'none';
        } else if (filter === 'unmatched') {
            row.style.display = row.classList.contains('unmatched') ? '' : 'none';
        } else if (filter === 'downloaded') {
            const dlCell = row.querySelector('.sync-detail-dl');
            row.style.display = dlCell && dlCell.textContent.trim() === '✅' ? '' : 'none';
        }
    });
}


// ============================================
// ACTIVE DOWNLOADS PAGE — Centralized Live View
// ============================================

let _adlPoller = null;
let _adlFilter = 'all';
let _adlData = [];
let _adlBatches = [];
let _adlBatchHistory = [];
let _adlExpandedBatches = new Set();
let _adlBatchHistoryPoller = null;
let _adlFilterBatchId = null; // When set, main list shows only this batch
const _batchColorMap = {};
const _batchCompletedAt = {}; // batch_id -> timestamp when first seen as complete
let _batchColorNext = 0;

function _getBatchColor(batchId) {
    if (!batchId) return -1;
    if (_batchColorMap[batchId] === undefined) {
        // Deterministic color from batch_id hash for consistency across reloads
        let hash = 0;
        for (let i = 0; i < batchId.length; i++) hash = ((hash << 5) - hash + batchId.charCodeAt(i)) | 0;
        _batchColorMap[batchId] = Math.abs(hash) % 8;
    }
    return _batchColorMap[batchId];
}

function loadActiveDownloadsPage() {
    _adlFetch();
    _adlFetchBatchHistory();
    // Poll downloads every 2 seconds, history every 60 seconds
    if (_adlPoller) clearInterval(_adlPoller);
    _adlPoller = setInterval(() => {
        if (currentPage === 'active-downloads') _adlFetch();
        else { clearInterval(_adlPoller); _adlPoller = null; }
    }, 2000);
    if (_adlBatchHistoryPoller) clearInterval(_adlBatchHistoryPoller);
    _adlBatchHistoryPoller = setInterval(() => {
        if (currentPage === 'active-downloads') _adlFetchBatchHistory();
        else { clearInterval(_adlBatchHistoryPoller); _adlBatchHistoryPoller = null; }
    }, 60000);
}

function adlSetFilter(filter) {
    _adlFilter = filter;
    document.querySelectorAll('#adl-filter-pills .adl-pill').forEach(p => p.classList.toggle('active', p.dataset.filter === filter));
    _adlRender();
}

async function _adlFetch() {
    try {
        const resp = await fetch('/api/downloads/all?limit=300');
        const data = await resp.json();
        if (data.success) {
            _adlData = data.downloads || [];
            _adlBatches = data.batches || [];
            _adlRender();
            _adlRenderBatchPanel();
            // Don't call _adlUpdateBadge() here — it counts the truncated
            // 300-item local array. The WebSocket status push already
            // maintains the badge with the real server-side active count.
        }
    } catch (e) {
        console.error('Downloads page fetch error:', e);
    }
}

function _adlUpdateBadge() {
    const activeCount = _adlData.filter(d => ['downloading', 'searching', 'queued', 'pending', 'post_processing'].includes(d.status)).length;
    _updateDlNavBadge(activeCount);
}

function _updateDlNavBadge(count) {
    const badge = document.getElementById('dl-nav-badge');
    if (badge) {
        if (count > 0) {
            badge.textContent = count;
            badge.classList.remove('hidden');
        } else {
            badge.classList.add('hidden');
        }
    }
    const dlBtn = document.querySelector('.nav-button[data-page="active-downloads"]');
    if (dlBtn) {
        dlBtn.classList.toggle('nav-downloads-active', count > 0);
    }
}

function _adlRender() {
    const list = document.getElementById('adl-list');
    const empty = document.getElementById('adl-empty');
    const countEl = document.getElementById('adl-count');
    if (!list) return;

    // Apply filter
    const activeStatuses = ['downloading', 'searching', 'post_processing'];
    const queuedStatuses = ['queued'];
    const completedStatuses = ['completed', 'skipped', 'already_owned'];
    const failedStatuses = ['failed', 'not_found', 'cancelled'];

    let filtered = _adlData;

    // Batch filter: if a batch card is selected, narrow to that batch first
    if (_adlFilterBatchId) {
        filtered = filtered.filter(d => d.batch_id === _adlFilterBatchId);
    }

    if (_adlFilter === 'active') filtered = filtered.filter(d => activeStatuses.includes(d.status));
    else if (_adlFilter === 'queued') filtered = filtered.filter(d => queuedStatuses.includes(d.status));
    else if (_adlFilter === 'completed') filtered = filtered.filter(d => completedStatuses.includes(d.status));
    else if (_adlFilter === 'failed') filtered = filtered.filter(d => failedStatuses.includes(d.status));

    const completedN = _adlData.filter(d => [...completedStatuses, ...failedStatuses].includes(d.status)).length;

    if (countEl) {
        const activeN = _adlData.filter(d => activeStatuses.includes(d.status)).length;
        const queuedN = _adlData.filter(d => queuedStatuses.includes(d.status)).length;
        const total = _adlData.length;
        const parts = [];
        if (activeN > 0) parts.push(`${activeN} active`);
        if (queuedN > 0) parts.push(`${queuedN} queued`);
        parts.push(`${total} total`);
        countEl.textContent = parts.join(' / ');
    }

    // Show/hide clear button
    const clearBtn = document.getElementById('adl-clear-btn');
    if (clearBtn) clearBtn.style.display = completedN > 0 ? '' : 'none';

    // Show/hide cancel-all button — only visible when there's something to cancel
    const cancelAllBtn = document.getElementById('adl-cancel-all-btn');
    if (cancelAllBtn) {
        const hasRunningWork = _adlData.some(d =>
            [...activeStatuses, ...queuedStatuses].includes(d.status)
        );
        cancelAllBtn.style.display = hasRunningWork ? '' : 'none';
    }

    // Batch filter indicator banner
    let existingBanner = document.getElementById('adl-batch-filter-banner');
    if (_adlFilterBatchId) {
        const batchInfo = _adlBatches.find(b => b.batch_id === _adlFilterBatchId);
        const batchName = batchInfo ? batchInfo.batch_name : 'Unknown batch';
        const colorIdx = _getBatchColor(_adlFilterBatchId);
        const colorDot = colorIdx >= 0 ? `<span class="adl-filter-banner-dot" style="background:rgba(var(--batch-color-${colorIdx}),0.7)"></span>` : '';
        if (!existingBanner) {
            existingBanner = document.createElement('div');
            existingBanner.id = 'adl-batch-filter-banner';
            existingBanner.className = 'adl-batch-filter-banner';
            list.parentNode.insertBefore(existingBanner, list);
        }
        existingBanner.innerHTML = `${colorDot}Showing: <strong>${_adlEsc(batchName)}</strong> <button class="adl-filter-banner-clear" onclick="_adlFilterByBatch('${_adlFilterBatchId}')">Clear filter</button>`;
        existingBanner.style.display = '';
    } else if (existingBanner) {
        existingBanner.style.display = 'none';
    }

    if (filtered.length === 0) {
        if (empty) empty.style.display = '';
        // Clear any existing rows but keep the empty message
        list.querySelectorAll('.adl-row').forEach(r => r.remove());
        return;
    }

    if (empty) empty.style.display = 'none';

    // Group by status category for section headers
    const groups = { active: [], queued: [], completed: [], failed: [] };
    for (const dl of filtered) {
        const cls = _adlStatusClass(dl.status);
        if (cls === 'active') groups.active.push(dl);
        else if (cls === 'queued') groups.queued.push(dl);
        else if (cls === 'completed') groups.completed.push(dl);
        else groups.failed.push(dl);
    }

    let html = '';
    const sections = [
        { key: 'active', label: 'Active', items: groups.active },
        { key: 'queued', label: 'Queued', items: groups.queued },
        { key: 'completed', label: 'Completed', items: groups.completed },
        { key: 'failed', label: 'Failed', items: groups.failed },
    ];

    for (const section of sections) {
        if (section.items.length === 0) continue;
        // Only show section headers in "all" filter mode
        if (_adlFilter === 'all') {
            html += `<div class="adl-section-header">${section.label} (${section.items.length})</div>`;
        }
        for (const dl of section.items) {
            const statusClass = _adlStatusClass(dl.status);
            const statusLabel = _adlStatusLabel(dl.status);
            const title = _adlEsc(dl.title || 'Unknown Track');
            const artist = _adlEsc(dl.artist || '');
            const album = _adlEsc(dl.album || '');
            const batchName = _adlEsc(dl.batch_name || '');
            const error = dl.error ? _adlEsc(dl.error) : '';

            const meta = [artist, album].filter(Boolean).join(' \u00B7 ');
            const artHtml = dl.artwork
                ? `<img class="adl-row-art" src="${_adlEsc(dl.artwork)}" alt="" onerror="this.style.display='none'">`
                : '<div class="adl-row-art adl-row-art-empty"></div>';

            // Track position: "3 of 19"
            const posText = dl.batch_total > 1 ? `${(dl.track_index || 0) + 1} of ${dl.batch_total}` : '';

            const colorIdx = _getBatchColor(dl.batch_id);
            const colorBar = colorIdx >= 0
                ? `<div class="adl-row-batch-color" style="background:rgba(var(--batch-color-${colorIdx}),0.6)"></div>`
                : '';

            // Per-row cancel only makes sense for in-flight tasks. Terminal
            // states (completed/failed/cancelled) have nothing to cancel.
            const isCancellable = statusClass === 'active' || statusClass === 'queued';
            const cancelBtnHtml = isCancellable && dl.playlist_id && dl.track_index !== undefined
                ? `<button class="adl-row-cancel" onclick="event.stopPropagation(); adlCancelRow(this, '${_adlEsc(dl.playlist_id)}', ${dl.track_index})" title="Cancel this download" aria-label="Cancel download">
                       <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
                   </button>`
                : '';

            html += `<div class="adl-row adl-row-${statusClass}" data-task-id="${dl.task_id}" data-batch-id="${dl.batch_id || ''}">
                ${colorBar}
                ${artHtml}
                <div class="adl-row-info">
                    <div class="adl-row-title">${title}</div>
                    ${meta ? `<div class="adl-row-meta">${meta}</div>` : ''}
                    ${batchName ? `<div class="adl-row-batch">${batchName}${posText ? ' &middot; Track ' + posText : ''}</div>` : ''}
                    ${error ? `<div class="adl-row-error">${error}</div>` : ''}
                </div>
                <div class="adl-row-status ${statusClass}">
                    <span class="adl-status-dot ${statusClass}"></span>
                    ${statusLabel}
                </div>
                ${cancelBtnHtml}
            </div>`;
        }
    }

    // Preserve empty element, inject rows
    const emptyEl = document.getElementById('adl-empty');
    const emptyHtml = emptyEl ? emptyEl.outerHTML : '';
    list.innerHTML = emptyHtml + html;
    const newEmpty = document.getElementById('adl-empty');
    if (newEmpty) newEmpty.style.display = filtered.length > 0 ? 'none' : '';
}

function _adlStatusClass(status) {
    switch (status) {
        case 'downloading': case 'searching': case 'post_processing': return 'active';
        case 'queued': case 'pending': return 'queued';
        case 'completed': case 'skipped': case 'already_owned': return 'completed';
        case 'failed': case 'not_found': return 'failed';
        case 'cancelled': return 'cancelled';
        default: return 'queued';
    }
}

function _adlStatusLabel(status) {
    switch (status) {
        case 'downloading': return '<span class="adl-spinner"></span>Downloading';
        case 'searching': return '<span class="adl-spinner"></span>Searching';
        case 'post_processing': return '<span class="adl-spinner"></span>Processing';
        case 'queued': case 'pending': return 'Queued';
        case 'completed': return 'Completed';
        case 'skipped': return 'Skipped';
        case 'already_owned': return 'Owned';
        case 'failed': return 'Failed';
        case 'not_found': return 'Not Found';
        case 'cancelled': return 'Cancelled';
        default: return status;
    }
}

function _adlEsc(str) {
    if (!str) return '';
    return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function _adlBundleProgressPercent(bundle) {
    if (!bundle) return 0;
    const raw = bundle.progress_percent ?? bundle.progress ?? 0;
    let progress = Number(raw);
    if (!Number.isFinite(progress)) progress = 0;
    if (progress <= 1) progress *= 100;
    return Math.max(0, Math.min(100, Math.round(progress)));
}

function _adlFormatBytes(bytes) {
    const value = Number(bytes);
    if (!Number.isFinite(value) || value <= 0) return '';
    const units = ['B', 'KB', 'MB', 'GB', 'TB'];
    let size = value;
    let unit = 0;
    while (size >= 1024 && unit < units.length - 1) {
        size /= 1024;
        unit += 1;
    }
    const decimals = size >= 10 || unit === 0 ? 0 : 1;
    return `${size.toFixed(decimals)} ${units[unit]}`;
}

function _adlFormatSpeed(bytesPerSecond) {
    const formatted = _adlFormatBytes(bytesPerSecond);
    return formatted ? `${formatted}/s` : '';
}

function _adlSourceLabel(source) {
    const labels = {
        torrent: 'Torrent',
        usenet: 'Usenet',
        soulseek: 'Soulseek',
        youtube: 'YouTube',
        tidal: 'Tidal',
        qobuz: 'Qobuz',
        hifi: 'HiFi',
        deezer_dl: 'Deezer',
        amazon: 'Amazon',
        lidarr: 'Lidarr',
        soundcloud: 'SoundCloud'
    };
    const key = String(source || '').toLowerCase();
    return labels[key] || (source ? String(source) : 'Release');
}

function _adlBundleStateLabel(state) {
    const labels = {
        searching: 'searching for release',
        downloading: 'downloading release',
        staged: 'matching tracks',
        failed: 'release failed'
    };
    const key = String(state || '').toLowerCase();
    return labels[key] || (state ? String(state).replace(/_/g, ' ') : 'downloading release');
}

function _adlBundleProgressText(bundle) {
    const pct = _adlBundleProgressPercent(bundle);
    const source = _adlSourceLabel(bundle && bundle.source);
    const state = _adlBundleStateLabel(bundle && bundle.state);
    const release = bundle && bundle.release ? ` - ${bundle.release}` : '';
    const speed = _adlFormatSpeed(bundle && bundle.speed);
    const size = _adlFormatBytes(bundle && bundle.size);
    const detail = speed || size ? ` (${[speed, size].filter(Boolean).join(' of ')})` : '';
    return `${source} ${state} ${pct}%${release}${detail}`;
}

async function adlClearCompleted() {
    try {
        const resp = await fetch('/api/downloads/clear-completed', { method: 'POST' });
        const data = await resp.json();
        if (data.success) {
            if (typeof showToast === 'function') showToast(`Cleared ${data.cleared} downloads`, 'success');
            _adlFetch();
        }
    } catch (e) {
        console.error('Error clearing completed downloads:', e);
    }
}

// ---- Batch Context Panel ----

const _BATCH_FADE_SECONDS = 15; // Remove completed batches after this many seconds

function _adlRenderBatchPanel() {
    const container = document.getElementById('adl-batch-active');
    const headerTitle = document.querySelector('.adl-batch-panel-title');
    if (!container) return;

    const now = Date.now();

    // Filter out batches that completed more than FADE seconds ago
    const visibleBatches = _adlBatches.filter(batch => {
        const isTerminal = batch.phase === 'complete' || batch.phase === 'cancelled' || batch.phase === 'error';
        if (!isTerminal) {
            delete _batchCompletedAt[batch.batch_id]; // Reset if it came back to life
            return true;
        }
        if (!_batchCompletedAt[batch.batch_id]) {
            _batchCompletedAt[batch.batch_id] = now;
        }
        const elapsed = (now - _batchCompletedAt[batch.batch_id]) / 1000;
        return elapsed < _BATCH_FADE_SECONDS;
    });

    // Update header with count
    if (headerTitle) {
        const activeCount = visibleBatches.filter(b => b.phase !== 'complete' && b.phase !== 'cancelled' && b.phase !== 'error').length;
        headerTitle.textContent = activeCount > 0 ? `Batches (${activeCount})` : 'Batches';
    }

    if (visibleBatches.length === 0) {
        container.innerHTML = `<div class="adl-batch-empty">
            <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" style="opacity:0.25;margin-bottom:6px"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
            <div>No active batches</div>
            <div style="font-size:0.7rem;margin-top:2px;opacity:0.5">Start a download from Search, Sync, or Wishlist</div>
        </div>`;
        return;
    }

    let html = '';
    for (const batch of visibleBatches) {
        const colorIdx = _getBatchColor(batch.batch_id);
        const colorStyle = colorIdx >= 0 ? `border-left-color: rgba(var(--batch-color-${colorIdx}), 0.6)` : '';
        const isExpanded = _adlExpandedBatches.has(batch.batch_id);
        const isFiltered = _adlFilterBatchId === batch.batch_id;
        const albumBundle = batch.album_bundle || null;
        const bundleProgress = _adlBundleProgressPercent(albumBundle);
        const total = batch.total || 1;
        const done = batch.completed + batch.failed;
        const pct = batch.phase === 'album_downloading'
            ? bundleProgress
            : Math.round((done / total) * 100);
        const hasFailed = batch.failed > 0;
        const isTerminal = batch.phase === 'complete' || batch.phase === 'cancelled' || batch.phase === 'error';
        const isActive = (batch.phase === 'downloading' && batch.active > 0) || batch.phase === 'album_downloading';

        // Fade progress for completing batches
        let fadeStyle = '';
        if (isTerminal && _batchCompletedAt[batch.batch_id]) {
            const elapsed = (now - _batchCompletedAt[batch.batch_id]) / 1000;
            const fadeStart = _BATCH_FADE_SECONDS * 0.6;
            if (elapsed > fadeStart) {
                const fadeProgress = Math.min(1, (elapsed - fadeStart) / (_BATCH_FADE_SECONDS - fadeStart));
                fadeStyle = `opacity: ${1 - fadeProgress};`;
            }
        }

        const sourceBadge = batch.source_page
            ? `<span class="adl-batch-card-source">${_adlEsc(batch.source_page)}</span>`
            : '';

        // Phase label with icon
        let phaseText = '';
        let phaseIcon = '';
        if (batch.phase === 'analysis') {
            phaseText = 'Analyzing...';
            phaseIcon = '<span class="adl-spinner" style="margin-right:4px"></span>';
        } else if (batch.phase === 'album_downloading') {
            phaseText = _adlBundleProgressText(albumBundle);
            phaseIcon = '<span class="adl-spinner" style="margin-right:4px"></span>';
        } else if (batch.phase === 'downloading') {
            phaseText = `${batch.completed}/${total} tracks`;
            if (batch.active > 0) phaseIcon = '<span class="adl-spinner" style="margin-right:4px"></span>';
        } else if (batch.phase === 'complete') {
            phaseText = `Done \u2014 ${batch.completed} tracks`;
            phaseIcon = '<span style="color:#22c55e;margin-right:4px">\u2713</span>';
        } else if (batch.phase === 'cancelled') {
            phaseText = 'Cancelled';
        } else if (batch.phase === 'error') {
            phaseText = 'Error';
        } else {
            phaseText = batch.phase;
        }

        // Get first track artwork for batch thumbnail, fallback to initial
        const batchTracks = _adlData.filter(d => d.batch_id === batch.batch_id);
        const artworkTrack = batchTracks.find(t => t.artwork);
        let thumbHtml;
        if (artworkTrack) {
            thumbHtml = `<img class="adl-batch-card-thumb" src="${_adlEsc(artworkTrack.artwork)}" alt="" onerror="this.outerHTML='<div class=\\'adl-batch-card-thumb adl-batch-card-thumb-fallback\\'>${_adlEsc((batch.batch_name || 'D')[0])}</div>'">`;
        } else {
            const initial = (batch.batch_name || 'D')[0].toUpperCase();
            const bgColor = colorIdx >= 0 ? `rgba(var(--batch-color-${colorIdx}), 0.15)` : 'rgba(255,255,255,0.05)';
            const fgColor = colorIdx >= 0 ? `rgba(var(--batch-color-${colorIdx}), 0.7)` : 'rgba(255,255,255,0.4)';
            thumbHtml = `<div class="adl-batch-card-thumb adl-batch-card-thumb-fallback" style="background:${bgColor};color:${fgColor}">${initial}</div>`;
        }

        // Build expanded tracks list with per-track progress
        let tracksHtml = '';
        if (isExpanded) {
            if (batchTracks.length > 0) {
                tracksHtml = batchTracks.map(t => {
                    const cls = _adlStatusClass(t.status);
                    const progress = t.progress || 0;

                    // Status indicator with detail
                    let statusHtml = '';
                    if (t.status === 'downloading' && progress > 0) {
                        statusHtml = `<span class="adl-batch-track-status active">${Math.round(progress)}%</span>`;
                    } else if (t.status === 'searching') {
                        statusHtml = `<span class="adl-batch-track-status active"><span class="adl-spinner" style="width:8px;height:8px"></span></span>`;
                    } else if (t.status === 'post_processing') {
                        statusHtml = `<span class="adl-batch-track-status active" title="Processing">proc</span>`;
                    } else if (cls === 'completed') {
                        statusHtml = `<span class="adl-batch-track-status completed">\u2713</span>`;
                    } else if (cls === 'failed') {
                        statusHtml = `<span class="adl-batch-track-status failed">\u2717</span>`;
                    } else {
                        statusHtml = `<span class="adl-batch-track-status queued">\u00B7</span>`;
                    }

                    // Mini progress bar for downloading tracks
                    const miniBar = t.status === 'downloading' && progress > 0
                        ? `<div class="adl-batch-track-progress"><div class="adl-batch-track-progress-fill" style="width:${progress}%"></div></div>`
                        : '';

                    return `<div class="adl-batch-track-row">
                        <span class="adl-batch-track-title">${_adlEsc(t.title || 'Unknown')}</span>
                        ${statusHtml}
                        ${miniBar}
                    </div>`;
                }).join('');
            } else {
                tracksHtml = batch.phase === 'album_downloading'
                    ? '<div class="adl-batch-release-note">Downloading one release first. Track matching starts after staging.</div>'
                    : '<div style="font-size:0.7rem;color:rgba(255,255,255,0.3);padding:4px 0">No tracks loaded</div>';
            }
        }

        const cardClasses = ['adl-batch-card'];
        if (isExpanded) cardClasses.push('expanded');
        if (isActive) cardClasses.push('active-glow');
        if (isFiltered) cardClasses.push('filtered');

        const playlistId = _adlEsc(batch.playlist_id || '');

        html += `<div class="${cardClasses.join(' ')}" style="${colorStyle}${fadeStyle}" data-batch-id="${batch.batch_id}" onclick="_adlToggleBatch('${batch.batch_id}')">
            <div class="adl-batch-card-top">
                ${thumbHtml}
                <div class="adl-batch-card-info">
                    <div class="adl-batch-card-name adl-batch-card-link" onclick="event.stopPropagation(); _adlOpenBatchModal('${batch.batch_id}', '${playlistId}', '${_adlEsc(batch.batch_name || 'Download')}')" title="Open download modal">${_adlEsc(batch.batch_name || 'Download')}</div>
                    <div class="adl-batch-card-meta">${phaseIcon}${phaseText}</div>
                </div>
                ${sourceBadge}
                <div class="adl-batch-card-actions">
                    <button class="adl-batch-card-filter ${isFiltered ? 'active' : ''}" onclick="event.stopPropagation(); _adlFilterByBatch('${batch.batch_id}')" title="${isFiltered ? 'Show all downloads' : 'Filter to this batch'}">
                        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
                    </button>
                    ${!isTerminal ? `<button class="adl-batch-card-cancel" onclick="event.stopPropagation(); _adlCancelBatch('${batch.batch_id}')" title="Cancel batch">
                        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
                    </button>` : ''}
                </div>
            </div>
            <div class="adl-batch-progress">
                <div class="adl-batch-progress-fill${hasFailed ? ' has-failed' : ''}" style="width:${pct}%"></div>
            </div>
            <div class="adl-batch-tracks">${tracksHtml}</div>
        </div>`;
    }

    container.innerHTML = html;
}

function _adlToggleBatch(batchId) {
    if (_adlExpandedBatches.has(batchId)) {
        _adlExpandedBatches.delete(batchId);
    } else {
        _adlExpandedBatches.add(batchId);
    }
    _adlRenderBatchPanel();
}

function _adlOpenBatchModal(batchId, playlistId, batchName) {
    // For wishlist batches, navigate to wishlist and show modal
    if (playlistId === 'wishlist') {
        const clientProcess = activeDownloadProcesses['wishlist'];
        if (clientProcess && clientProcess.modalElement && document.body.contains(clientProcess.modalElement)) {
            clientProcess.modalElement.style.display = 'flex';
            if (typeof WishlistModalState !== 'undefined') WishlistModalState.setVisible();
        } else {
            rehydrateModal({ playlist_id: playlistId, playlist_name: batchName, batch_id: batchId }, true);
        }
        return;
    }

    // For other batches, try to show existing modal or rehydrate
    for (const [pid, process] of Object.entries(activeDownloadProcesses)) {
        if (process.batchId === batchId && process.modalElement && document.body.contains(process.modalElement)) {
            process.modalElement.style.display = 'flex';
            return;
        }
    }
    // Rehydrate from server
    rehydrateModal({ playlist_id: playlistId, playlist_name: batchName, batch_id: batchId }, true);
}

function _adlFilterByBatch(batchId) {
    if (_adlFilterBatchId === batchId) {
        _adlFilterBatchId = null; // Toggle off
    } else {
        _adlFilterBatchId = batchId;
    }
    _adlRender();
    _adlRenderBatchPanel();
}

async function adlCancelRow(btnEl, playlistId, trackIndex) {
    // Per-row cancel on the Downloads page. Uses the same atomic cancel
    // endpoint the modal cancel buttons use, so worker slots free properly.
    if (!playlistId || trackIndex === undefined || trackIndex === null) {
        showToast('Cannot cancel — missing task coordinates', 'error');
        return;
    }
    // Lock the button so rapid clicks don't fire duplicate requests
    if (btnEl) {
        if (btnEl.dataset.cancelling === '1') return;
        btnEl.dataset.cancelling = '1';
        btnEl.classList.add('adl-row-cancel-pending');
    }
    try {
        const resp = await fetch('/api/downloads/cancel_task_v2', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                playlist_id: playlistId,
                track_index: trackIndex
            })
        });
        const data = await resp.json();
        if (data.success) {
            const name = data.task_info && data.task_info.track_name ? data.task_info.track_name : 'Track';
            showToast(`Cancelled "${name}"`, 'info');
            _adlFetch();
        } else {
            showToast(data.error || 'Cancel failed', 'error');
            if (btnEl) {
                btnEl.dataset.cancelling = '0';
                btnEl.classList.remove('adl-row-cancel-pending');
            }
        }
    } catch (e) {
        console.error('ADL row cancel error:', e);
        showToast('Cancel request failed', 'error');
        if (btnEl) {
            btnEl.dataset.cancelling = '0';
            btnEl.classList.remove('adl-row-cancel-pending');
        }
    }
}

async function _adlCancelBatch(batchId) {
    const batch = _adlBatches.find(b => b.batch_id === batchId);
    const batchName = batch ? batch.batch_name : 'this batch';
    const confirmed = await showConfirmDialog({
        title: 'Cancel Batch',
        message: `Cancel "${batchName}"? All active and queued downloads in this batch will be stopped.`,
        confirmText: 'Cancel Batch',
        destructive: true
    });
    if (!confirmed) return;
    try {
        const resp = await fetch(`/api/playlists/${batchId}/cancel_batch`, { method: 'POST' });
        const data = await resp.json();
        if (data.success) {
            showToast(`Cancelled ${data.cancelled_tasks} downloads`, 'info');
            _adlFetch();
        } else {
            showToast(data.error || 'Failed to cancel batch', 'error');
        }
    } catch (e) {
        showToast('Failed to cancel batch', 'error');
    }
}

async function adlCancelAll() {
    // Cancel every batch with active/queued work — equivalent to clicking
    // "Cancel All" inside each running download modal. Uses the same
    // /api/playlists/<batch_id>/cancel_batch endpoint the per-batch card
    // cancel uses, so worker slots free atomically.
    const runningBatches = _adlBatches.filter(b => (b.active || 0) > 0 || (b.queued || 0) > 0);
    if (runningBatches.length === 0) {
        showToast('No active batches to cancel', 'info');
        return;
    }

    const totalTasks = runningBatches.reduce((sum, b) => sum + (b.active || 0) + (b.queued || 0), 0);
    const batchWord = runningBatches.length === 1 ? 'batch' : 'batches';
    const taskWord = totalTasks === 1 ? 'task' : 'tasks';
    const confirmed = await showConfirmDialog({
        title: 'Cancel All Downloads',
        message: `Cancel ${totalTasks} ${taskWord} across ${runningBatches.length} ${batchWord}? Active and queued downloads will be stopped and added to the wishlist.`,
        confirmText: 'Cancel All',
        destructive: true
    });
    if (!confirmed) return;

    const btn = document.getElementById('adl-cancel-all-btn');
    if (btn) {
        btn.disabled = true;
        btn.classList.add('adl-cancel-all-pending');
    }

    let cancelled = 0;
    let failed = 0;
    // Sequential so we don't hammer the backend — cancel_batch takes a lock
    // internally and parallel calls would mostly serialize anyway.
    for (const batch of runningBatches) {
        try {
            const resp = await fetch(`/api/playlists/${batch.batch_id}/cancel_batch`, { method: 'POST' });
            const data = await resp.json();
            if (data.success) {
                cancelled += (data.cancelled_tasks || 0);
            } else {
                failed += 1;
                console.warn(`cancel_batch failed for ${batch.batch_id}:`, data.error);
            }
        } catch (e) {
            failed += 1;
            console.warn(`cancel_batch exception for ${batch.batch_id}:`, e);
        }
    }

    if (btn) {
        btn.disabled = false;
        btn.classList.remove('adl-cancel-all-pending');
    }

    if (cancelled > 0 && failed === 0) {
        showToast(`Cancelled ${cancelled} downloads`, 'success');
    } else if (cancelled > 0 && failed > 0) {
        showToast(`Cancelled ${cancelled} downloads (${failed} batches failed)`, 'info');
    } else {
        showToast('Failed to cancel any downloads', 'error');
    }

    _adlFetch();
}

// ---- Batch History ----

async function _adlFetchBatchHistory() {
    try {
        const resp = await fetch('/api/downloads/batch-history?days=7&limit=50');
        const data = await resp.json();
        if (data.success) {
            _adlBatchHistory = data.history || [];
            _adlRenderBatchHistory();
        }
    } catch (e) {
        console.debug('Batch history fetch error:', e);
    }
}

function _adlRenderBatchHistory() {
    const section = document.getElementById('adl-batch-history-section');
    const list = document.getElementById('adl-batch-history-list');
    if (!section || !list) return;

    if (_adlBatchHistory.length === 0) {
        section.style.display = 'none';
        return;
    }

    section.style.display = '';

    list.innerHTML = _adlBatchHistory.map(h => {
        const name = _adlEsc(h.playlist_name || 'Unknown');
        const downloaded = h.tracks_downloaded || 0;
        const failed = h.tracks_failed || 0;
        const total = h.total_tracks || 0;
        const statsParts = [`${downloaded}/${total}`];
        if (failed > 0) statsParts.push(`<span style="color:#ef4444">${failed} failed</span>`);

        let dateText = '';
        if (h.completed_at) {
            try {
                const d = new Date(h.completed_at);
                const now = new Date();
                const diffMs = now - d;
                const diffH = Math.floor(diffMs / 3600000);
                if (diffH < 1) dateText = 'just now';
                else if (diffH < 24) dateText = `${diffH}h ago`;
                else dateText = `${Math.floor(diffH / 24)}d ago`;
            } catch (e) {
                dateText = '';
            }
        }

        const sourceLabel = h.source_page ? `<span class="adl-batch-card-source" style="font-size:0.6rem;padding:0 4px">${_adlEsc(h.source_page)}</span>` : '';

        // Source type color dot
        const sourceColors = { wishlist: '168, 85, 247', sync: '59, 130, 246', album: '16, 185, 129' };
        const dotColor = sourceColors[h.source_page] || '255, 255, 255';
        const histDot = `<span class="adl-batch-history-dot" style="background:rgba(${dotColor}, 0.6)"></span>`;

        return `<div class="adl-batch-history-item">
            ${histDot}
            <div class="adl-batch-history-name">${name} ${sourceLabel}</div>
            <div class="adl-batch-history-stats">${statsParts.join(' ')}</div>
            <div class="adl-batch-history-date">${dateText}</div>
        </div>`;
    }).join('');
}

function adlToggleBatchHistory() {
    const section = document.getElementById('adl-batch-history-section');
    if (section) section.classList.toggle('expanded');
}

function adlToggleBatchPanel() {
    const panel = document.getElementById('adl-batch-panel');
    if (panel) panel.classList.toggle('collapsed');
}

window.adlSetFilter = adlSetFilter;
window.adlClearCompleted = adlClearCompleted;
window._adlToggleBatch = _adlToggleBatch;
window._adlOpenBatchModal = _adlOpenBatchModal;
window._adlFilterByBatch = _adlFilterByBatch;
window._adlCancelBatch = _adlCancelBatch;
window.adlCancelRow = adlCancelRow;
window.adlCancelAll = adlCancelAll;
window.adlToggleBatchHistory = adlToggleBatchHistory;
window.adlToggleBatchPanel = adlToggleBatchPanel;

