// MEDIA PLAYER FUNCTIONALITY
// ===============================

function initializeMediaPlayer() {
    const trackTitle = document.getElementById('track-title');
    const playButton = document.getElementById('play-button');
    const stopButton = document.getElementById('stop-button');
    const volumeSlider = document.getElementById('volume-slider');

    // Start in idle state (no track playing)
    const player = document.getElementById('media-player');
    if (player && !currentTrack) player.classList.add('idle');

    // Initialize HTML5 audio player
    audioPlayer = document.getElementById('audio-player');
    if (audioPlayer) {
        // Set up audio event listeners
        audioPlayer.addEventListener('timeupdate', updateAudioProgress);
        audioPlayer.addEventListener('ended', onAudioEnded);
        audioPlayer.addEventListener('error', onAudioError);
        audioPlayer.addEventListener('loadstart', onAudioLoadStart);
        audioPlayer.addEventListener('canplay', onAudioCanPlay);

        // Set initial volume
        audioPlayer.volume = 0.7; // 70%
        if (volumeSlider) volumeSlider.value = 70;
    }

    // Track title click handled by initExpandedPlayer's media-player click handler

    // Media controls
    playButton.addEventListener('click', handlePlayPause);
    stopButton.addEventListener('click', handleStop);
    if (volumeSlider) volumeSlider.addEventListener('input', handleVolumeChange);

    // Progress bar controls
    const progressBar = document.getElementById('progress-bar');
    if (progressBar) {
        // Handle seeking
        progressBar.addEventListener('input', handleProgressBarChange);
        progressBar.addEventListener('mousedown', () => {
            progressBar.dataset.seeking = 'true';
        });
        progressBar.addEventListener('mouseup', () => {
            delete progressBar.dataset.seeking;
        });
    }

    // Update volume slider styling
    if (volumeSlider) volumeSlider.addEventListener('input', updateVolumeSliderAppearance);

    // Mini player prev / next buttons
    const miniPrevBtn = document.getElementById('mini-prev-btn');
    const miniNextBtn = document.getElementById('mini-next-btn');
    if (miniPrevBtn) miniPrevBtn.addEventListener('click', (e) => { e.stopPropagation(); playPreviousInQueue(); });
    if (miniNextBtn) miniNextBtn.addEventListener('click', (e) => { e.stopPropagation(); playNextInQueue(); });
}

function toggleMediaPlayerExpansion() {
    // No-op: controls are always visible in the new layout.
    // Kept for backward compatibility with any callers.
}

function extractTrackTitle(filename) {
    if (!filename) return null;

    // Strip the ``<source_id>||<display>`` prefix used by YouTube /
    // Tidal / Qobuz / torrent / usenet plugins to thread the source-
    // side identifier through ``filename`` without polluting the
    // display string. The id always comes first, the human title
    // after. If no separator is present, fall through with the raw
    // value so existing slskd / streaming-source paths are untouched.
    let title = filename;
    const sepIdx = title.indexOf('||');
    if (sepIdx >= 0) {
        title = title.slice(sepIdx + 2);
    }

    // Remove file extension
    title = title.replace(/\.[^/.]+$/, '');

    // Remove path components, keep only the filename
    title = title.split('/').pop().split('\\').pop();

    // Clean up common filename patterns
    title = title
        .replace(/^\d+\.?\s*/, '') // Remove track numbers at start
        .replace(/^\d+\s*-\s*/, '') // Remove "01 - " patterns
        .replace(/\s*-\s*\d{4}\s*$/, '') // Remove years at end
        .replace(/\s*\[\d+kbps\].*$/, '') // Remove bitrate info
        .replace(/\s*\(.*?\)\s*$/, '') // Remove parenthetical info at end
        .trim();

    return title || null;
}

function _stripSourceIdPrefix(value) {
    // Defensive cleanup for callers that pass a raw ``<source_id>||<display>``
    // string straight into setTrackInfo without first running
    // extractTrackTitle. The id always precedes the separator; the display
    // string follows. Strings with no separator pass through unchanged.
    if (!value || typeof value !== 'string') return value;
    const idx = value.indexOf('||');
    if (idx < 0) return value;
    return value.slice(idx + 2);
}

function setTrackInfo(track) {
    currentTrack = track;

    const trackTitleElement = document.getElementById('track-title');
    const trackTitle = _stripSourceIdPrefix(track.title) || 'Unknown Track';

    // Set up the HTML structure for scrolling
    trackTitleElement.innerHTML = `<span class="title-text">${escapeHtml(trackTitle)}</span>`;

    document.getElementById('artist-name').textContent = _stripSourceIdPrefix(track.artist) || 'Unknown Artist';
    document.getElementById('album-name').textContent = _stripSourceIdPrefix(track.album) || 'Unknown Album';

    // Check if title needs scrolling (similar to GUI app)
    setTimeout(() => {
        checkAndEnableScrolling(trackTitleElement, trackTitle);
    }, 100); // Allow DOM to settle

    // Enable controls
    document.getElementById('play-button').disabled = false;
    document.getElementById('stop-button').disabled = false;

    // Hide no track message and expand player
    document.getElementById('no-track-message').classList.add('hidden');
    document.getElementById('media-player').classList.remove('idle');

    const gotoArtistBtn = document.getElementById('np-goto-artist');
    if (gotoArtistBtn) {
        if (track.artist_id) {
            gotoArtistBtn.href = buildArtistDetailPath(track.artist_id, track.artist_source || null);
            gotoArtistBtn.style.pointerEvents = '';
            gotoArtistBtn.setAttribute('aria-disabled', 'false');
            gotoArtistBtn.tabIndex = 0;
        } else {
            gotoArtistBtn.href = '#';
            gotoArtistBtn.style.pointerEvents = 'none';
            gotoArtistBtn.setAttribute('aria-disabled', 'true');
            gotoArtistBtn.tabIndex = -1;
        }
        // Close the expanded now-playing modal when the user navigates
        // to the artist page — otherwise the modal sits open over the
        // page they just opened. ``_npGotoArtistHandlerAttached`` flag
        // keeps us from binding multiple listeners across setTrackInfo
        // calls (fires on every track change).
        if (!gotoArtistBtn._npGotoArtistHandlerAttached) {
            gotoArtistBtn.addEventListener('click', () => {
                if (gotoArtistBtn.getAttribute('aria-disabled') === 'true') return;
                try { closeNowPlayingModal(); } catch (e) { console.debug('closeNowPlayingModal failed:', e); }
            });
            gotoArtistBtn._npGotoArtistHandlerAttached = true;
        }
    }

    // Sync expanded player and media session
    updateNpTrackInfo();
    updateMediaSessionMetadata();
    updateMediaSessionPlaybackState();

    // Kick off lyrics fetch for the new track. The panel stays
    // collapsed by default — fetching in the background means the
    // user gets instant lyrics the first time they expand it.
    _npLyricsLoadForTrack({
        title: track.title,
        artist: track.artist,
        album: track.album,
        is_library: track.is_library,
        filename: track.filename,
    });
}

function checkAndEnableScrolling(element, text) {
    // Remove any existing scrolling class and reset styles
    element.classList.remove('scrolling');
    element.style.removeProperty('--scroll-distance');

    // Force a layout to get accurate measurements
    element.offsetWidth;

    // Get the inner text element
    const titleTextElement = element.querySelector('.title-text');
    if (!titleTextElement) return;

    // Check if text is wider than container
    const containerWidth = element.offsetWidth;
    const textWidth = titleTextElement.scrollWidth;

    // Enable scrolling if text is significantly wider than container
    if (textWidth > containerWidth + 15) {
        const scrollDistance = containerWidth - textWidth;
        element.style.setProperty('--scroll-distance', `${scrollDistance}px`);
        element.classList.add('scrolling');
        console.log(`📜 Enabled scrolling for title: "${text}"`);
        console.log(`📜 Container: ${containerWidth}px, Text: ${textWidth}px, Scroll: ${scrollDistance}px`);
    }
}


function clearTrack() {
    // Clear track state
    currentTrack = null;
    isPlaying = false;

    const trackTitleElement = document.getElementById('track-title');
    trackTitleElement.innerHTML = '<span class="title-text">No track</span>';
    trackTitleElement.classList.remove('scrolling'); // Remove scrolling animation
    trackTitleElement.style.removeProperty('--scroll-distance'); // Clear CSS variable

    document.getElementById('artist-name').textContent = 'Unknown Artist';
    document.getElementById('album-name').textContent = 'Unknown Album';
    // Reset play button SVGs (don't use textContent — it destroys SVG children)
    const clearPlayBtn = document.getElementById('play-button');
    const clearPlayIcon = clearPlayBtn.querySelector('.play-icon');
    const clearPauseIcon = clearPlayBtn.querySelector('.pause-icon');
    if (clearPlayIcon) clearPlayIcon.style.display = '';
    if (clearPauseIcon) clearPauseIcon.style.display = 'none';
    clearPlayBtn.disabled = true;
    document.getElementById('stop-button').disabled = true;

    // Reset progress bar and time displays
    const progressBar = document.getElementById('progress-bar');
    const progressFill = document.getElementById('progress-fill');
    if (progressBar) {
        progressBar.value = 0;
        delete progressBar.dataset.seeking;
    }
    if (progressFill) {
        progressFill.style.width = '0%';
    }

    const currentTimeElement = document.getElementById('current-time');
    const totalTimeElement = document.getElementById('total-time');
    if (currentTimeElement) currentTimeElement.textContent = '0:00';
    if (totalTimeElement) totalTimeElement.textContent = '0:00';

    // Hide loading animation
    hideLoadingAnimation();

    // Show no track message and collapse player
    document.getElementById('no-track-message').classList.remove('hidden');
    document.getElementById('media-player').classList.add('idle');

    const gotoArtistBtn = document.getElementById('np-goto-artist');
    if (gotoArtistBtn) {
        gotoArtistBtn.href = '#';
        gotoArtistBtn.style.pointerEvents = 'none';
        gotoArtistBtn.setAttribute('aria-disabled', 'true');
        gotoArtistBtn.tabIndex = -1;
    }

    // Reset queue state
    npQueue = [];
    npQueueIndex = -1;

    // Sync expanded player and media session
    updateNpTrackInfo();
    updateNpPlayButton();
    updateNpProgress();
    renderNpQueue();
    updateNpPrevNextButtons();
    updateMediaSessionPlaybackState();
    stopSidebarVisualizer();
    if (npModalOpen) closeNowPlayingModal();

    console.log('🧹 Track cleared and media player reset');
}

function setPlayingState(playing) {
    isPlaying = playing;
    const playButton = document.getElementById('play-button');
    // Toggle SVG icons (don't use textContent — it destroys SVG children)
    const playIcon = playButton.querySelector('.play-icon');
    const pauseIcon = playButton.querySelector('.pause-icon');
    if (playIcon) playIcon.style.display = playing ? 'none' : '';
    if (pauseIcon) pauseIcon.style.display = playing ? '' : 'none';
    updateNpPlayButton();
    updateMediaSessionPlaybackState();

    // Sidebar audio visualizer
    if (playing) {
        npInitVisualizer();
        startSidebarVisualizer();
    } else {
        stopSidebarVisualizer();
    }
}

async function handlePlayPause() {
    // Use new streaming system toggle function
    togglePlayback();
}

async function handleStop() {
    // Use new streaming system stop function
    await stopStream();
    clearTrack();
}

function handleVolumeChange(event) {
    const volume = event.target.value;
    updateVolumeSliderAppearance();

    // Update HTML5 audio player volume
    if (audioPlayer) {
        audioPlayer.volume = volume / 100;
    }

    // Sync modal volume and clear mute state
    npMuted = false;
    const npVol = document.getElementById('np-volume-slider');
    const npFill = document.getElementById('np-volume-fill');
    if (npVol) npVol.value = volume;
    if (npFill) npFill.style.width = volume + '%';
    updateNpMuteIcon();
}

function handleProgressBarChange(event) {
    // Handle seeking in the audio track
    if (!audioPlayer || !audioPlayer.duration) return;

    const progress = parseFloat(event.target.value);
    const newTime = (progress / 100) * audioPlayer.duration;

    console.log(`🎯 Seeking to ${formatTime(newTime)} (${progress.toFixed(1)}%)`);

    try {
        audioPlayer.currentTime = newTime;

        // Update visual progress immediately
        const progressFill = document.getElementById('progress-fill');
        if (progressFill) {
            progressFill.style.width = `${progress}%`;
        }

        // Update time displays immediately
        const currentTimeElement = document.getElementById('current-time');
        if (currentTimeElement) {
            currentTimeElement.textContent = formatTime(newTime);
        }

        // Sync modal progress
        const npBar = document.getElementById('np-progress-bar');
        const npFill = document.getElementById('np-progress-fill');
        const npTime = document.getElementById('np-current-time');
        if (npBar) npBar.value = progress;
        if (npFill) npFill.style.width = progress + '%';
        if (npTime) npTime.textContent = formatTime(newTime);
    } catch (error) {
        console.warn('⚠️ Seek failed:', error.message);
        // Reset progress bar to current position
        const actualProgress = (audioPlayer.currentTime / audioPlayer.duration) * 100;
        event.target.value = actualProgress;
        const progressFill = document.getElementById('progress-fill');
        if (progressFill) {
            progressFill.style.width = `${actualProgress}%`;
        }
    }
}

function updateVolumeSliderAppearance() {
    const slider = document.getElementById('volume-slider');
    if (!slider) return;
    const value = slider.value;
    slider.style.setProperty('--volume-percent', `${value}%`);
}

function showLoadingAnimation() {
    document.getElementById('loading-animation').classList.remove('hidden');
}

function hideLoadingAnimation() {
    document.getElementById('loading-animation').classList.add('hidden');
}

function setLoadingProgress(percentage) {
    const loadingAnimation = document.getElementById('loading-animation');
    const progressBar = loadingAnimation.querySelector('.loading-progress');
    const loadingText = loadingAnimation.querySelector('.loading-text');

    loadingAnimation.classList.remove('hidden');
    progressBar.style.width = `${percentage}%`;
    loadingText.textContent = `${Math.round(percentage)}%`;
}

// ===============================
// STREAMING FUNCTIONALITY
// ===============================

let _streamLock = false;

async function startStream(searchResult) {
    // Start streaming a track - handles same track toggle and new track streaming
    try {
        // Prevent multiple concurrent stream starts (rapid clicking)
        if (_streamLock) {
            console.log('⏳ Stream already starting, ignoring duplicate click');
            return;
        }

        console.log(`🎮 startStream() called with data:`, searchResult);

        // Check if this is the same track that's currently playing/loading
        const currentTrackId = currentTrack ? `${currentTrack.username}:${currentTrack.filename}` : null;
        const newTrackId = `${searchResult.username}:${searchResult.filename}`;

        console.log(`🎮 startStream() called for: ${searchResult.filename}`);
        console.log(`🎮 Current track ID: ${currentTrackId}`);
        console.log(`🎮 New track ID: ${newTrackId}`);

        if (currentTrackId === newTrackId && audioPlayer && !audioPlayer.paused) {
            // Same track clicked while playing - toggle pause
            console.log("🔄 Toggling playback for same track");
            togglePlayback();
            return;
        }

        // Lock to prevent duplicate stream starts
        _streamLock = true;

        // Different track or no current track - start new stream
        console.log("🎵 Starting new stream");

        // Stop current streaming/playback if any
        await stopStream();

        // Set track info and show loading state
        setTrackInfo({
            title: extractTrackTitle(searchResult.filename) || searchResult.title || 'Unknown Track',
            artist: searchResult.artist || searchResult.username || 'Unknown Artist',
            album: searchResult.album || 'Unknown Album',
            username: searchResult.username,
            filename: searchResult.filename,
            image_url: searchResult.image_url || searchResult.album_cover_url || null
        });

        showLoadingAnimation();
        setLoadingProgress(0);

        // Start streaming request
        const response = await fetch(API.stream.start, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(searchResult)
        });

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        const data = await response.json();

        if (!data.success) {
            throw new Error(data.error || 'Failed to start streaming');
        }

        console.log("✅ Stream started successfully");

        // Start status polling
        startStreamStatusPolling();

    } catch (error) {
        console.error('Error starting stream:', error);
        showToast(`Failed to start stream: ${error.message}`, 'error');
        hideLoadingAnimation();
        clearTrack();
    } finally {
        _streamLock = false;
    }
}

function startStreamStatusPolling() {
    // Start polling for stream status updates with retry logic
    if (streamStatusPoller) {
        clearInterval(streamStatusPoller);
    }

    // Reset polling state
    streamPollingRetries = 0;
    streamPollingInterval = 1000; // Reset to 1-second interval

    console.log('🔄 Starting enhanced stream status polling');
    updateStreamStatus(); // Initial check
    streamStatusPoller = setInterval(updateStreamStatus, streamPollingInterval);
}

function stopStreamStatusPolling() {
    // Stop polling for stream status updates
    if (streamStatusPoller) {
        clearInterval(streamStatusPoller);
        streamStatusPoller = null;
        streamPollingRetries = 0;
        streamPollingInterval = 1000; // Reset interval
        console.log('⏹️ Stopped stream status polling');
    }
}

// Phase 4: Track last known tool statuses to prevent repeated toasts on terminal states
let _lastToolStatus = {};

// Phase 5: Sync/Discovery/Scan WebSocket router functions
function updateSyncProgressFromData(data) {
    const pid = data.playlist_id;
    const callback = _syncProgressCallbacks[pid];
    if (callback) callback(data);
}

function updateDiscoveryProgressFromData(data) {
    const id = data.id;
    const callback = _discoveryProgressCallbacks[id];
    if (callback) callback(data);
}

function updateWatchlistScanFromData(data) {
    if (!data.success) return;
    if (_lastWatchlistScanStatus === data.status && data.status !== 'scanning') return;
    _lastWatchlistScanStatus = data.status;
    handleWatchlistScanData(data);
}

function updateMediaScanFromData(data) {
    if (!data.success || !data.status) return;
    const status = data.status;
    const statusKey = status.is_scanning ? 'scanning' : (status.status || 'unknown');
    if (_lastMediaScanStatus === statusKey && statusKey !== 'scanning') return;
    _lastMediaScanStatus = statusKey;

    const phaseLabel = document.getElementById('media-scan-phase-label');
    const progressLabel = document.getElementById('media-scan-progress-label');
    const button = document.getElementById('media-scan-btn');
    const progressBar = document.getElementById('media-scan-progress-bar');
    const statusValue = document.getElementById('media-scan-status');

    if (status.is_scanning) {
        if (phaseLabel) phaseLabel.textContent = 'Media server scanning...';
        if (progressLabel) progressLabel.textContent = status.progress_message || 'Scan in progress';
    } else if (status.status === 'idle') {
        if (button) button.disabled = false;
        if (phaseLabel) phaseLabel.textContent = 'Scan completed successfully';
        if (progressBar) progressBar.style.width = '0%';
        if (progressLabel) progressLabel.textContent = 'Ready for next scan';
        if (statusValue) {
            statusValue.textContent = 'Idle';
            statusValue.style.color = '#b3b3b3';
        }
        showToast('✅ Media scan completed', 'success', 3000);
    }
}

let _wishlistAutoProcessingNotified = false;
function updateWishlistStatsFromData(data) {
    // Auto-processing detection: close modal and notify (once only)
    if (data.is_auto_processing) {
        if (!_wishlistAutoProcessingNotified) {
            if (currentPage === 'wishlist') navigateToPage('active-downloads');
            showToast('Wishlist auto-processing started. View progress in Download Manager.', 'info');
            _wishlistAutoProcessingNotified = true;
        }
        return;
    }
    // Reset flag when auto-processing ends
    _wishlistAutoProcessingNotified = false;
    // Store latest stats for countdown timer refresh
    _lastWishlistStats = data;
}

async function updateStreamStatus() {
    if (socketConnected) return; // WebSocket handles this
    // Poll server for streaming progress and handle state changes with enhanced error recovery
    try {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), 10000); // 10-second timeout

        const response = await fetch(API.stream.status, {
            signal: controller.signal
        });

        clearTimeout(timeoutId);

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        const data = await response.json();

        // Reset retry count on successful response
        streamPollingRetries = 0;
        streamPollingInterval = 1000; // Reset to normal interval

        // Update current stream state
        currentStream.status = data.status;
        currentStream.progress = data.progress;

        switch (data.status) {
            case 'loading':
                setLoadingProgress(data.progress);
                // Update loading text with progress
                const loadingText = document.querySelector('.loading-text');
                if (loadingText && data.progress > 0) {
                    loadingText.textContent = `Downloading... ${Math.round(data.progress)}%`;
                }
                break;

            case 'queued':
                // Show queue status with better messaging
                const queueText = document.querySelector('.loading-text');
                if (queueText) {
                    queueText.textContent = 'Queuing with uploader...';
                }
                setLoadingProgress(0); // Reset progress for queue state
                break;

            case 'ready':
                // Stream is ready - start audio playback
                console.log('🎵 Stream ready, starting audio playback');
                stopStreamStatusPolling();
                // Restore player UI if JS state was wiped (e.g. page refresh)
                if (!currentTrack && data.track_info) {
                    const ti = data.track_info;
                    setTrackInfo({
                        title: ti.name || ti.title || 'Unknown Track',
                        artist: ti.artist || 'Unknown Artist',
                        album: ti.album || 'Unknown Album',
                        filename: ti.filename || '',
                        is_library: !!ti.is_library,
                        image_url: ti.image_url || null,
                        id: ti.id || null,
                        artist_id: ti.artist_id || null,
                        album_id: ti.album_id || null,
                    });
                }
                await startAudioPlayback();
                break;

            case 'error':
                console.error('❌ Streaming error:', data.error_message);
                stopStreamStatusPolling();
                hideLoadingAnimation();
                showToast(`Streaming error: ${data.error_message || 'Unknown error'}`, 'error');
                clearTrack();
                break;

            case 'stopped':
                // Handle stopped state — do NOT clear track here; explicit stop (handleStop)
                // calls clearTrack() directly. Clearing here collapses the player mid-playback
                // when the backend transitions to 'stopped' after audio naturally ends or during
                // queue track transitions.
                console.log('🛑 Stream stopped');
                stopStreamStatusPolling();
                hideLoadingAnimation();
                break;
        }

    } catch (error) {
        streamPollingRetries++;
        console.warn(`Stream status polling error (attempt ${streamPollingRetries}):`, error.message);

        if (streamPollingRetries >= maxStreamPollingRetries) {
            // Too many consecutive failures - give up
            console.error('❌ Stream status polling failed after maximum retries');
            stopStreamStatusPolling();
            hideLoadingAnimation();
            showToast('Lost connection to streaming server', 'error');
            clearTrack();
        } else {
            // Implement exponential backoff for retries
            const backoffMultiplier = Math.min(streamPollingRetries, 5); // Max 5x backoff
            streamPollingInterval = 1000 * backoffMultiplier;

            // Restart polling with new interval
            if (streamStatusPoller) {
                clearInterval(streamStatusPoller);
                streamStatusPoller = setInterval(updateStreamStatus, streamPollingInterval);
                console.log(`🔄 Retrying stream status polling with ${streamPollingInterval}ms interval`);
            }
        }
    }
}

function updateStreamStatusFromData(data) {
    const prev = _lastToolStatus['stream'];
    _lastToolStatus['stream'] = data.status;
    // Skip repeated terminal states to avoid duplicate toasts/actions
    if (prev !== undefined && data.status === prev && data.status !== 'loading' && data.status !== 'queued') return;

    currentStream.status = data.status;
    currentStream.progress = data.progress;

    switch (data.status) {
        case 'loading':
            setLoadingProgress(data.progress);
            const loadingText = document.querySelector('.loading-text');
            if (loadingText && data.progress > 0) {
                loadingText.textContent = `Downloading... ${Math.round(data.progress)}%`;
            }
            break;
        case 'queued':
            const queueText = document.querySelector('.loading-text');
            if (queueText) {
                queueText.textContent = 'Queuing with uploader...';
            }
            setLoadingProgress(0);
            break;
        case 'ready':
            console.log('🎵 Stream ready, starting audio playback');
            stopStreamStatusPolling();
            // Restore player UI if JS state was wiped (e.g. page refresh)
            if (!currentTrack && data.track_info) {
                const ti = data.track_info;
                setTrackInfo({
                    title: ti.name || ti.title || 'Unknown Track',
                    artist: ti.artist || 'Unknown Artist',
                    album: ti.album || 'Unknown Album',
                    filename: ti.filename || '',
                    is_library: !!ti.is_library,
                    image_url: ti.image_url || null,
                    id: ti.id || null,
                    artist_id: ti.artist_id || null,
                    album_id: ti.album_id || null,
                });
            }
            startAudioPlayback();
            break;
        case 'error':
            console.error('❌ Streaming error:', data.error_message);
            stopStreamStatusPolling();
            hideLoadingAnimation();
            showToast(`Streaming error: ${data.error_message || 'Unknown error'}`, 'error');
            clearTrack();
            break;
        case 'stopped':
            // Do NOT clear track here — explicit stop (handleStop) calls clearTrack() directly.
            // Clearing here collapses the player after audio naturally ends or during queue transitions.
            console.log('🛑 Stream stopped');
            stopStreamStatusPolling();
            hideLoadingAnimation();
            break;
    }
}

async function startAudioPlayback() {
    // Start HTML5 audio playback of the streamed file with enhanced state management
    try {
        if (!audioPlayer) {
            throw new Error('Audio player not initialized');
        }

        // Show loading state while preparing audio
        const loadingText = document.querySelector('.loading-text');
        if (loadingText) {
            loadingText.textContent = 'Preparing playback...';
        }

        // Set audio source with cache-busting timestamp
        const audioUrl = `/stream/audio?t=${new Date().getTime()}`;
        console.log(`🎵 Loading audio from: ${audioUrl}`);

        // Clear any existing source first
        audioPlayer.pause();
        audioPlayer.currentTime = 0;
        audioPlayer.src = '';

        // Set new source
        audioPlayer.src = audioUrl;
        audioPlayer.load(); // Force reload

        // Wait for audio to be ready with promise-based approach
        await new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                reject(new Error('Audio loading timeout'));
            }, 15000); // 15-second timeout

            const onCanPlay = () => {
                clearTimeout(timeout);
                audioPlayer.removeEventListener('canplay', onCanPlay);
                audioPlayer.removeEventListener('error', onError);
                resolve();
            };

            const onError = (event) => {
                clearTimeout(timeout);
                audioPlayer.removeEventListener('canplay', onCanPlay);
                audioPlayer.removeEventListener('error', onError);
                const error = event.target.error || new Error('Audio loading failed');
                reject(error);
            };

            audioPlayer.addEventListener('canplay', onCanPlay);
            audioPlayer.addEventListener('error', onError);

            // If already ready, resolve immediately
            if (audioPlayer.readyState >= 3) { // HAVE_FUTURE_DATA
                onCanPlay();
            }
        });

        console.log('✅ Audio loaded and ready for playback');

        // Try to start playback with retry logic
        let retryCount = 0;
        const maxRetries = 3;

        while (retryCount < maxRetries) {
            try {
                await audioPlayer.play();
                console.log('✅ Audio playback started successfully');

                // Update UI to playing state
                hideLoadingAnimation();
                setPlayingState(true);

                // Show media player if hidden
                const noTrackMessage = document.getElementById('no-track-message');
                if (noTrackMessage) {
                    noTrackMessage.classList.add('hidden');
                }

                // Update volume to current slider value
                const volumeSlider = document.getElementById('volume-slider');
                if (volumeSlider) {
                    audioPlayer.volume = volumeSlider.value / 100;
                }

                // Enable play/stop buttons
                const playButton = document.getElementById('play-button');
                const stopButton = document.getElementById('stop-button');
                if (playButton) playButton.disabled = false;
                if (stopButton) stopButton.disabled = false;

                return; // Success!

            } catch (playError) {
                retryCount++;
                console.warn(`⚠️ Audio play attempt ${retryCount} failed:`, playError.message);

                if (retryCount >= maxRetries) {
                    throw playError; // Re-throw after max retries
                }

                // Wait before retry with exponential backoff
                await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
            }
        }

    } catch (error) {
        console.error('❌ Error starting audio playback:', error);
        hideLoadingAnimation();

        // Provide user-friendly error messages
        let userMessage = 'Playback failed';

        if (error.message.includes('no supported source') ||
            error.message.includes('Not supported') ||
            error.message.includes('MEDIA_ELEMENT_ERROR')) {
            userMessage = 'Audio format not supported by your browser. Try downloading instead.';
        } else if (error.message.includes('network') || error.message.includes('fetch')) {
            userMessage = 'Network error - please check your connection';
        } else if (error.message.includes('decode')) {
            userMessage = 'Audio file is corrupted or incompatible';
        } else if (error.message.includes('timeout')) {
            userMessage = 'Audio loading timeout - file may be too large';
        } else if (error.message.includes('AbortError')) {
            userMessage = 'Playback was interrupted';
        }

        showToast(userMessage, 'error');
        // Only clear track if not in queue playback mode — queue handles its own error recovery
        if (npQueue.length === 0) {
            clearTrack();
        }
    }
}

async function stopStream() {
    // Stop streaming and clean up all state
    try {
        // Stop status polling
        stopStreamStatusPolling();

        // Stop audio playback
        if (audioPlayer) {
            audioPlayer.pause();
            audioPlayer.src = '';
        }

        // Call backend stop endpoint
        const response = await fetch(API.stream.stop, { method: 'POST' });
        if (response.ok) {
            const data = await response.json();
            console.log('🛑 Stream stopped:', data.message);
        }

        // Reset UI state
        hideLoadingAnimation();
        setPlayingState(false);

        // Reset stream state
        currentStream = {
            status: 'stopped',
            progress: 0,
            track: null
        };

    } catch (error) {
        console.error('Error stopping stream:', error);
    }
}

function togglePlayback() {
    // Toggle play/pause for currently loaded audio
    if (!audioPlayer || !currentTrack) {
        console.log('⚠️ No audio player or track to toggle');
        return;
    }

    if (audioPlayer.paused) {
        audioPlayer.play()
            .then(() => {
                setPlayingState(true);
                console.log('▶️ Resumed playback');
            })
            .catch(error => {
                console.error('Error resuming playback:', error);
                showToast('Failed to resume playback', 'error');
            });
    } else {
        audioPlayer.pause();
        setPlayingState(false);
        console.log('⏸️ Paused playback');
    }
}

// ===============================
// AUDIO EVENT HANDLERS
// ===============================

function updateAudioProgress() {
    // Update progress bar based on audio playback time
    if (!audioPlayer || !audioPlayer.duration) return;

    const progress = (audioPlayer.currentTime / audioPlayer.duration) * 100;

    // Update progress bar
    const progressBar = document.getElementById('progress-bar');
    const progressFill = document.getElementById('progress-fill');
    if (progressBar && !progressBar.dataset.seeking) {
        progressBar.value = progress;
        // Update visual progress fill
        if (progressFill) {
            progressFill.style.width = `${progress}%`;
        }
    }

    // Update time display
    const currentTimeElement = document.getElementById('current-time');
    const totalTimeElement = document.getElementById('total-time');

    if (currentTimeElement) {
        currentTimeElement.textContent = formatTime(audioPlayer.currentTime);
    }
    if (totalTimeElement) {
        totalTimeElement.textContent = formatTime(audioPlayer.duration);
    }

    // Sync expanded player modal
    if (npModalOpen) updateNpProgress();

    // Sync lyrics highlight when synced LRC is loaded.
    if (_npLyricsState.synced && _npLyricsState.lines.length) {
        _npLyricsHighlight(audioPlayer.currentTime);
    }
}

// ─────────────────────────────────────────────────────────────────
// Lyrics panel (now-playing modal)
// ─────────────────────────────────────────────────────────────────

// Module-level state for the currently-loaded lyrics. Reset on each
// track change. ``lines`` is an array of {time, text} for synced
// lyrics or null for plain text. ``activeIndex`` tracks the last
// highlighted line to avoid re-rendering on every timeupdate tick.
const _npLyricsState = {
    trackKey: null,
    lines: [],
    synced: false,
    activeIndex: -1,
    fetchInFlight: false,
    autoOpen: false,
};

function _npLyricsResetUI() {
    const content = document.getElementById('np-lyrics-content');
    const status = document.getElementById('np-lyrics-status');
    if (content) content.innerHTML = '<div class="np-lyrics-empty">No lyrics loaded</div>';
    if (status) status.textContent = '';
}

function _npLyricsParseLrc(synced) {
    // Parse a standard LRC string. Lines without a timestamp are
    // dropped (metadata tags like ``[ti:Title]`` aren't lyrics). The
    // same line can carry multiple timestamps — emit one entry per
    // timestamp so seeks land correctly when a chorus repeats.
    const out = [];
    if (!synced) return out;
    const re = /\[(\d+):(\d+(?:\.\d+)?)\]/g;
    synced.split(/\r?\n/).forEach(raw => {
        const stamps = [];
        let m;
        re.lastIndex = 0;
        while ((m = re.exec(raw)) !== null) {
            const minutes = parseInt(m[1], 10);
            const seconds = parseFloat(m[2]);
            if (!Number.isFinite(minutes) || !Number.isFinite(seconds)) continue;
            stamps.push(minutes * 60 + seconds);
        }
        if (!stamps.length) return;
        const text = raw.replace(re, '').trim();
        stamps.forEach(t => out.push({ time: t, text }));
    });
    out.sort((a, b) => a.time - b.time);
    return out;
}

function _npLyricsRenderSynced(lines) {
    const content = document.getElementById('np-lyrics-content');
    if (!content) return;
    if (!lines.length) {
        content.innerHTML = '<div class="np-lyrics-empty">No timestamped lyrics for this track</div>';
        return;
    }
    content.innerHTML = lines.map((line, idx) => {
        const safe = (line.text || '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])) || '&nbsp;';
        return `<div class="np-lyrics-line" data-idx="${idx}">${safe}</div>`;
    }).join('');
}

function _npLyricsRenderPlain(text) {
    const content = document.getElementById('np-lyrics-content');
    if (!content) return;
    const safe = (text || '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
    content.innerHTML = `<div class="np-lyrics-plain">${safe.replace(/\n/g, '<br>')}</div>`;
}

function _npLyricsHighlight(currentTime) {
    const { lines } = _npLyricsState;
    if (!lines.length) return;
    let idx = -1;
    // Binary-search style linear scan — N is small (200 lines max).
    for (let i = 0; i < lines.length; i++) {
        if (lines[i].time <= currentTime) idx = i;
        else break;
    }
    if (idx === _npLyricsState.activeIndex) return;
    _npLyricsState.activeIndex = idx;
    const content = document.getElementById('np-lyrics-content');
    if (!content) return;
    content.querySelectorAll('.np-lyrics-line').forEach((el, i) => {
        el.classList.remove('active', 'passed', 'upcoming');
        if (i === idx) el.classList.add('active');
        else if (i < idx) el.classList.add('passed');
        else el.classList.add('upcoming');
    });
    const activeEl = content.querySelector('.np-lyrics-line.active');
    if (activeEl) {
        // Smooth-scroll the active line into the middle of the lyrics body.
        const body = document.getElementById('np-lyrics-body');
        if (body) {
            const bodyRect = body.getBoundingClientRect();
            const lineRect = activeEl.getBoundingClientRect();
            const targetTop = (lineRect.top - bodyRect.top) - (bodyRect.height / 2) + (lineRect.height / 2);
            body.scrollTo({ top: body.scrollTop + targetTop, behavior: 'smooth' });
        }
    }
}

function _npLyricsTrackKey(track) {
    if (!track) return null;
    return `${track.title || ''}|${track.artist || ''}|${track.album || ''}`;
}

async function _npLyricsLoadForTrack(track) {
    const key = _npLyricsTrackKey(track);
    if (!key) return;
    if (_npLyricsState.trackKey === key) return;     // already loaded
    if (_npLyricsState.fetchInFlight) return;
    _npLyricsState.trackKey = key;
    _npLyricsState.lines = [];
    _npLyricsState.synced = false;
    _npLyricsState.activeIndex = -1;
    _npLyricsResetUI();
    const status = document.getElementById('np-lyrics-status');
    if (status) status.textContent = 'Fetching…';
    _npLyricsState.fetchInFlight = true;
    try {
        const resp = await fetch('/api/lyrics/fetch', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                title: _stripSourceIdPrefix(track.title) || '',
                artist: _stripSourceIdPrefix(track.artist) || '',
                album: _stripSourceIdPrefix(track.album) || '',
                duration: Math.round(audioPlayer?.duration || 0),
                file_path: track.is_library ? track.filename : null,
            }),
        });
        const data = await resp.json();
        if (_npLyricsState.trackKey !== key) return;    // track changed mid-fetch
        if (data && data.success) {
            if (data.synced) {
                const parsed = _npLyricsParseLrc(data.synced);
                if (parsed.length) {
                    _npLyricsState.synced = true;
                    _npLyricsState.lines = parsed;
                    _npLyricsRenderSynced(parsed);
                    if (status) status.textContent = 'Synced';
                    return;
                }
            }
            if (data.plain) {
                _npLyricsState.synced = false;
                _npLyricsState.lines = [];
                _npLyricsRenderPlain(data.plain);
                if (status) status.textContent = 'Plain';
                return;
            }
        }
        const content = document.getElementById('np-lyrics-content');
        if (content) content.innerHTML = '<div class="np-lyrics-empty">No lyrics found</div>';
        if (status) status.textContent = '';
    } catch (e) {
        console.debug('lyrics fetch failed:', e);
        const content = document.getElementById('np-lyrics-content');
        if (content) content.innerHTML = '<div class="np-lyrics-empty">Lyrics unavailable</div>';
        if (status) status.textContent = '';
    } finally {
        _npLyricsState.fetchInFlight = false;
    }
}

function _npLyricsTogglePanel(forceOpen = null) {
    const panel = document.getElementById('np-lyrics-panel');
    const body = document.getElementById('np-lyrics-body');
    const toggle = document.getElementById('np-lyrics-toggle');
    if (!panel || !body || !toggle) return;
    const willOpen = forceOpen === null ? body.classList.contains('hidden') : forceOpen;
    if (willOpen) {
        body.classList.remove('hidden');
        panel.classList.remove('collapsed');
        toggle.setAttribute('aria-expanded', 'true');
    } else {
        body.classList.add('hidden');
        panel.classList.add('collapsed');
        toggle.setAttribute('aria-expanded', 'false');
    }
}

function _npLyricsInit() {
    const toggle = document.getElementById('np-lyrics-toggle');
    if (toggle && !toggle._lyricsBound) {
        toggle.addEventListener('click', () => _npLyricsTogglePanel());
        toggle._lyricsBound = true;
    }
}

function onAudioEnded() {
    // Handle audio playback completion
    console.log('🏁 Audio playback ended');
    setPlayingState(false);

    // Reset progress to beginning
    const progressBar = document.getElementById('progress-bar');
    const progressFill = document.getElementById('progress-fill');
    if (progressBar) {
        progressBar.value = 0;
    }
    if (progressFill) {
        progressFill.style.width = '0%';
    }

    const currentTimeElement = document.getElementById('current-time');
    if (currentTimeElement) {
        currentTimeElement.textContent = '0:00';
    }

    // Repeat-one is handled by audioPlayer.loop (set in handleNpRepeat)
    // Auto-advance to next track if queue has a next item (guard against race conditions)
    if (npQueue.length > 0 && !npLoadingQueueItem) {
        const hasNext = npShuffleOn
            ? npQueue.length > 1
            : (npQueueIndex < npQueue.length - 1 || npRepeatMode === 'all');
        if (hasNext) { playNextInQueue(); return; }
    }

    // Radio mode: auto-fetch similar tracks when queue is exhausted
    if (npRadioMode && currentTrack && currentTrack.id && !npLoadingQueueItem) {
        npFetchRadioTracks();
    }
}

function onAudioError(event) {
    // Handle audio playback errors
    const error = event.target.error;
    console.error('❌ Audio error:', error);

    // Don't show error toast if it's just a format/codec issue and retrying
    if (error && error.code) {
        console.error(`Audio error code: ${error.code}, message: ${error.message || 'Unknown error'}`);

        // Only show user-facing errors for serious issues
        if (error.code === 4) { // MEDIA_ELEMENT_ERROR: Media not supported
            console.warn('⚠️ Media format not supported by browser, but streaming may still work');
            // Don't clear track or show error - let retry logic handle it
            return;
        }
    }

    hideLoadingAnimation();

    // Only clear track after a short delay to allow for recovery
    setTimeout(() => {
        if (audioPlayer && audioPlayer.error) {
            let userMessage = 'Audio format not supported by your browser. Try downloading instead.';

            if (error && error.code) {
                switch (error.code) {
                    case 1: // MEDIA_ERR_ABORTED
                        userMessage = 'Playback was stopped';
                        break;
                    case 2: // MEDIA_ERR_NETWORK
                        userMessage = 'Network error - please try again';
                        break;
                    case 3: // MEDIA_ERR_DECODE
                        userMessage = 'Audio file is corrupted or incompatible';
                        break;
                    case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED
                        userMessage = 'Audio format not supported by your browser. Try downloading instead.';
                        break;
                }
            }

            showToast(userMessage, 'error');
            // Only clear track if not in queue playback — queue handles its own recovery
            if (npQueue.length === 0) {
                clearTrack();
            }
        }
    }, 2000);
}

function onAudioLoadStart() {
    // Handle audio load start
    console.log('🔄 Audio loading started');
}

function onAudioCanPlay() {
    // Handle when audio can start playing
    console.log('✅ Audio ready to play');
}

function formatTime(seconds) {
    // Format seconds as MM:SS
    if (!seconds || !isFinite(seconds)) return '0:00';

    const minutes = Math.floor(seconds / 60);
    const secs = Math.floor(seconds % 60);
    return `${minutes}:${secs.toString().padStart(2, '0')}`;
}

function formatCountdownTime(seconds) {
    // Format seconds as countdown timer (e.g., "24m 13s", "2h 15m", "23h 59m")
    if (seconds === null || seconds === undefined || seconds < 0) return '';
    if (seconds === 0) return '0s';  // Show "0s" instead of hiding timer

    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    const secs = Math.floor(seconds % 60);

    if (hours > 0) {
        return `${hours}h ${minutes}m`;
    } else if (minutes > 0) {
        return `${minutes}m ${secs}s`;
    } else {
        return `${secs}s`;
    }
}

// ===============================
// AUDIO FORMAT SUPPORT DETECTION
// ===============================

function getFileExtension(filename) {
    if (!filename) return '';
    const ext = filename.toLowerCase().match(/\.([^.]+)$/);
    return ext ? ext[1] : '';
}

function isAudioFormatSupported(filename) {
    const ext = getFileExtension(filename);
    const supportedFormats = ['mp3', 'ogg', 'wav'];  // Most reliable formats
    const partialSupport = ['flac', 'aac', 'm4a', 'opus', 'webm'];  // Test browser support
    const unsupported = ['wma', 'ape', 'aiff'];  // Generally problematic

    if (supportedFormats.includes(ext)) {
        return true;
    }

    if (partialSupport.includes(ext)) {
        // Test if browser can actually play this format
        return canPlayAudioFormat(ext);
    }

    return false;  // Unsupported formats
}

function canPlayAudioFormat(extension) {
    const audio = document.createElement('audio');

    const mimeTypes = {
        'mp3': 'audio/mpeg',
        'ogg': 'audio/ogg; codecs="vorbis"',
        'wav': 'audio/wav',
        'flac': 'audio/flac',
        'aac': 'audio/aac',
        'm4a': 'audio/mp4; codecs="mp4a.40.2"',  // More specific M4A MIME type
        'opus': 'audio/ogg; codecs="opus"',
        'webm': 'audio/webm; codecs="opus"',
        'wma': 'audio/x-ms-wma'
    };

    const mimeType = mimeTypes[extension];
    if (!mimeType) {
        console.warn(`🎵 [FORMAT CHECK] No MIME type found for extension: ${extension}`);
        return false;
    }

    const canPlay = audio.canPlayType(mimeType);
    console.log(`🎵 [FORMAT CHECK] ${extension} (${mimeType}): ${canPlay}`);

    let isSupported = canPlay === 'probably' || canPlay === 'maybe';

    // Special handling for M4A - try fallback MIME types if first one fails
    if (!isSupported && extension === 'm4a') {
        const fallbackMimeTypes = ['audio/mp4', 'audio/x-m4a', 'audio/aac'];
        console.log(`🎵 [FORMAT CHECK] M4A failed with primary MIME type, trying fallbacks...`);

        for (const fallbackMime of fallbackMimeTypes) {
            const fallbackResult = audio.canPlayType(fallbackMime);
            console.log(`🎵 [FORMAT CHECK] M4A fallback (${fallbackMime}): ${fallbackResult}`);
            if (fallbackResult === 'probably' || fallbackResult === 'maybe') {
                isSupported = true;
                console.log(`🎵 [FORMAT CHECK] M4A supported with fallback MIME type: ${fallbackMime}`);
                break;
            }
        }
    }

    console.log(`🎵 [FORMAT CHECK] ${extension} final support result: ${isSupported}`);
    return isSupported;
}

// ===============================
// EXPANDED NOW PLAYING MODAL
// ===============================

let npModalOpen = false;
let npRepeatMode = 'off';     // 'off' | 'all' | 'one'
let npShuffleOn = false;
let npQueue = [];
let npQueueIndex = -1;
let npMuted = false;
let npPreMuteVolume = 70;
let npMediaSessionThrottle = 0;
let npLoadingQueueItem = false;
let npRadioMode = false;
let npRecentlyPlayedIds = [];
let npAudioContext = null;
let npAnalyser = null;
let npMediaSource = null;
let npVizAnimFrame = null;
let npVizInitialized = false;

function initExpandedPlayer() {
    const closeBtn = document.getElementById('np-close-btn');
    const overlay = document.getElementById('np-modal-overlay');
    const playBtn = document.getElementById('np-play-btn');
    const stopBtn = document.getElementById('np-stop-btn');
    const shuffleBtn = document.getElementById('np-shuffle-btn');
    const repeatBtn = document.getElementById('np-repeat-btn');
    const muteBtn = document.getElementById('np-mute-btn');
    const npProgressBar = document.getElementById('np-progress-bar');
    const npVolumeSlider = document.getElementById('np-volume-slider');

    if (!overlay) return;

    // Close handlers
    closeBtn.addEventListener('click', closeNowPlayingModal);
    overlay.addEventListener('click', (e) => { if (e.target === overlay) closeNowPlayingModal(); });

    // Control handlers
    playBtn.addEventListener('click', () => { togglePlayback(); });
    stopBtn.addEventListener('click', async () => { await handleStop(); closeNowPlayingModal(); });
    shuffleBtn.addEventListener('click', handleNpShuffle);
    repeatBtn.addEventListener('click', handleNpRepeat);
    muteBtn.addEventListener('click', handleNpMuteToggle);

    // Progress bar (mouse)
    npProgressBar.addEventListener('input', handleNpProgressBarChange);
    npProgressBar.addEventListener('mousedown', () => { npProgressBar.dataset.seeking = 'true'; });
    npProgressBar.addEventListener('mouseup', () => { delete npProgressBar.dataset.seeking; });

    // Progress bar (touch)
    npProgressBar.addEventListener('touchstart', () => { npProgressBar.dataset.seeking = 'true'; }, { passive: true });
    npProgressBar.addEventListener('touchmove', (e) => {
        const touch = e.touches[0];
        const rect = npProgressBar.getBoundingClientRect();
        const pct = Math.max(0, Math.min(100, ((touch.clientX - rect.left) / rect.width) * 100));
        npProgressBar.value = pct;
        npProgressBar.dispatchEvent(new Event('input'));
    }, { passive: true });
    npProgressBar.addEventListener('touchend', () => { delete npProgressBar.dataset.seeking; }, { passive: true });

    // Volume slider
    npVolumeSlider.addEventListener('input', handleNpVolumeChange);

    // Keyboard shortcuts (global)
    document.addEventListener('keydown', handlePlayerKeyboardShortcuts);

    // Make sidebar media player clickable to open modal
    const mediaPlayer = document.getElementById('media-player');
    if (mediaPlayer) {
        mediaPlayer.style.cursor = 'pointer';
        mediaPlayer.addEventListener('click', (e) => {
            // Don't open modal when clicking controls (let expand-hint through)
            if (e.target.closest('.play-button, .stop-button, .volume-slider, .volume-control, .progress-bar, .volume-icon, .mini-nav-btn') && !e.target.closest('.expand-hint')) return;
            if (currentTrack) openNowPlayingModal();
        });
    }

    // Prev / Next buttons
    const prevBtn = document.getElementById('np-prev-btn');
    const nextBtn = document.getElementById('np-next-btn');
    if (prevBtn) prevBtn.addEventListener('click', () => { playPreviousInQueue(); });
    if (nextBtn) nextBtn.addEventListener('click', () => { playNextInQueue(); });

    // Queue panel toggle + clear
    const queueToggle = document.getElementById('np-queue-toggle');
    if (queueToggle) {
        queueToggle.addEventListener('click', () => {
            const body = document.getElementById('np-queue-body');
            if (body) body.classList.toggle('hidden');
            queueToggle.classList.toggle('active');
        });
    }
    const queueClearBtn = document.getElementById('np-queue-clear');
    if (queueClearBtn) queueClearBtn.addEventListener('click', () => { clearQueue(); });

    // Radio mode button
    const radioBtn = document.getElementById('np-radio-btn');
    if (radioBtn) {
        radioBtn.addEventListener('click', () => {
            npRadioMode = !npRadioMode;
            radioBtn.classList.toggle('active', npRadioMode);
            showToast(npRadioMode ? 'Radio mode on — similar tracks will auto-queue' : 'Radio mode off', 'success');
            // Immediately fetch radio tracks if turned on while playing with empty/exhausted queue
            if (npRadioMode && currentTrack && currentTrack.id && !npLoadingQueueItem) {
                const hasNext = npQueue.length > 0 && (npShuffleOn
                    ? npQueue.length > 1
                    : (npQueueIndex < npQueue.length - 1 || npRepeatMode === 'all'));
                if (!hasNext) {
                    // Add current track to queue first so it appears as "now playing" in context
                    if (npQueue.length === 0 && currentTrack.is_library) {
                        npQueue.push({
                            title: currentTrack.title,
                            artist: currentTrack.artist,
                            album: currentTrack.album,
                            file_path: currentTrack.filename || currentTrack.file_path,
                            filename: currentTrack.filename || currentTrack.file_path,
                            is_library: true,
                            image_url: currentTrack.image_url,
                            id: currentTrack.id,
                            artist_id: currentTrack.artist_id,
                            album_id: currentTrack.album_id,
                            bitrate: currentTrack.bitrate
                        });
                        npQueueIndex = 0;
                        renderNpQueue();
                        updateNpPrevNextButtons();
                    }
                    npFetchRadioTracks();
                }
            }
        });
    }

    // Action link (Go to Artist)
    const gotoArtistBtn = document.getElementById('np-goto-artist');
    if (gotoArtistBtn) {
        gotoArtistBtn.style.textDecoration = 'none';
        gotoArtistBtn.style.color = 'inherit';
    }
    // Buffering state listeners on audioPlayer
    if (audioPlayer) {
        audioPlayer.addEventListener('waiting', () => {
            const ring = document.getElementById('np-buffering-ring');
            if (ring) ring.classList.remove('hidden');
        });
        audioPlayer.addEventListener('canplay', () => {
            const ring = document.getElementById('np-buffering-ring');
            if (ring) ring.classList.add('hidden');
        });
        audioPlayer.addEventListener('playing', () => {
            const ring = document.getElementById('np-buffering-ring');
            if (ring) ring.classList.add('hidden');
        });
    }

    // Init Media Session API
    initMediaSession();
}

function openNowPlayingModal() {
    const overlay = document.getElementById('np-modal-overlay');
    if (!overlay) return;
    npModalOpen = true;
    overlay.classList.remove('hidden');
    document.body.style.overflow = 'hidden';
    syncExpandedPlayerUI();
    // Bind lyrics toggle (idempotent — only attaches once). Lyrics
    // fetch fires from setTrackInfo so by the time the modal opens
    // the panel is usually already populated.
    _npLyricsInit();
    // Start visualizer if already playing
    if (isPlaying) { npInitVisualizer(); npStartVisualizerLoop(); }
}

function closeNowPlayingModal() {
    const overlay = document.getElementById('np-modal-overlay');
    if (!overlay) return;
    npModalOpen = false;
    overlay.classList.add('hidden');
    document.body.style.overflow = '';
    npStopVisualizerLoop();
}

function syncExpandedPlayerUI() {
    if (!npModalOpen) return;

    // Track info
    updateNpTrackInfo();

    // Play state
    updateNpPlayButton();

    // Progress
    updateNpProgress();

    // Volume
    const sidebarVol = document.getElementById('volume-slider');
    const npVol = document.getElementById('np-volume-slider');
    const npVolFill = document.getElementById('np-volume-fill');
    if (sidebarVol && npVol) {
        npVol.value = sidebarVol.value;
        if (npVolFill) npVolFill.style.width = sidebarVol.value + '%';
    }

    // Visualizer
    const viz = document.getElementById('np-visualizer');
    if (viz) viz.classList.toggle('playing', isPlaying);

    // Queue
    renderNpQueue();
    updateNpPrevNextButtons();
}

function updateNpTrackInfo() {
    const titleEl = document.getElementById('np-track-title');
    const artistEl = document.getElementById('np-artist-name');
    const albumEl = document.getElementById('np-album-name');
    const artImg = document.getElementById('np-album-art');
    const artPlaceholder = document.getElementById('np-album-art-placeholder');
    const badgesEl = document.getElementById('np-format-badges');
    const actionBtns = document.getElementById('np-action-buttons');

    if (!titleEl) return;

    // Sidebar album art
    const sidebarArt = document.getElementById('sidebar-album-art');

    if (currentTrack) {
        // Track text transition animation
        const textEls = [titleEl, artistEl, albumEl];
        const oldTitle = titleEl.textContent;
        const newTitle = currentTrack.title || 'Unknown Track';
        const trackChanged = oldTitle !== newTitle && oldTitle !== 'No track';

        titleEl.textContent = newTitle;
        artistEl.textContent = currentTrack.artist || 'Unknown Artist';
        albumEl.textContent = currentTrack.album || 'Unknown Album';

        if (trackChanged) {
            textEls.forEach(el => {
                el.classList.remove('np-text-transition');
                void el.offsetWidth; // force reflow
                el.classList.add('np-text-transition');
            });
        }

        // Album art (modal + sidebar) + ambient glow extraction
        const artUrl = getNpAlbumArtUrl();
        if (artUrl && artImg) {
            // Only set crossOrigin for external URLs — local paths break with CORS headers
            if (artUrl.startsWith('http')) {
                artImg.crossOrigin = 'anonymous';
            } else {
                artImg.removeAttribute('crossOrigin');
            }
            artImg.src = artUrl;
            artImg.classList.remove('hidden');
            artImg.onerror = () => { artImg.classList.add('hidden'); npResetAmbientGlow(); };
            artImg.onload = () => { npExtractAmbientColor(artImg); };
        } else if (artImg) {
            artImg.classList.add('hidden');
            npResetAmbientGlow();
        }
        if (sidebarArt) {
            if (artUrl) {
                sidebarArt.src = artUrl;
                sidebarArt.style.display = '';
                sidebarArt.onerror = () => { sidebarArt.src = '/static/trans2.png'; };
            } else {
                sidebarArt.src = '/static/trans2.png';
            }
        }

        // Format badges (richer: include bitrate/sample_rate)
        if (badgesEl) {
            badgesEl.innerHTML = '';
            const filename = currentTrack.filename || '';
            if (filename) {
                const ext = getFileExtension(filename);
                if (ext) {
                    let label = ext.toUpperCase();
                    if (currentTrack.sample_rate) {
                        const khz = (currentTrack.sample_rate / 1000);
                        label += ' ' + (khz % 1 === 0 ? khz.toFixed(0) : khz.toFixed(1)) + 'kHz';
                    }
                    const badge = document.createElement('span');
                    badge.className = 'np-format-badge' + (ext === 'flac' ? ' flac' : '');
                    badge.textContent = label;
                    badgesEl.appendChild(badge);
                }
                if (currentTrack.bitrate) {
                    const brBadge = document.createElement('span');
                    brBadge.className = 'np-format-badge';
                    brBadge.textContent = currentTrack.bitrate + 'k';
                    badgesEl.appendChild(brBadge);
                }
            }
        }

        // Action buttons visibility
        if (actionBtns) {
            const hasArtist = currentTrack.artist_id;
            actionBtns.classList.toggle('hidden', !hasArtist);
        }

        // Track recently played for radio mode
        if (currentTrack.id && !npRecentlyPlayedIds.includes(currentTrack.id)) {
            npRecentlyPlayedIds.push(currentTrack.id);
            if (npRecentlyPlayedIds.length > 50) npRecentlyPlayedIds.shift();
        }
    } else {
        titleEl.textContent = 'No track';
        artistEl.textContent = 'Unknown Artist';
        albumEl.textContent = 'Unknown Album';
        if (artImg) artImg.classList.add('hidden');
        if (sidebarArt) sidebarArt.src = '/static/trans2.png';
        if (badgesEl) badgesEl.innerHTML = '';
        if (actionBtns) actionBtns.classList.add('hidden');
        npResetAmbientGlow();
    }
}

function npExtractAmbientColor(imgEl) {
    try {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        canvas.width = 50;
        canvas.height = 50;
        ctx.drawImage(imgEl, 0, 0, 50, 50);
        const data = ctx.getImageData(0, 0, 50, 50).data;
        let rSum = 0, gSum = 0, bSum = 0, count = 0;
        for (let i = 0; i < data.length; i += 16) { // sample every 4th pixel
            const r = data[i], g = data[i + 1], b = data[i + 2];
            const brightness = (r + g + b) / 3;
            if (brightness > 20 && brightness < 230) {
                rSum += r; gSum += g; bSum += b; count++;
            }
        }
        if (count > 0) {
            const modal = document.querySelector('.np-modal');
            if (modal) {
                modal.style.setProperty('--np-ambient-r', Math.round(rSum / count));
                modal.style.setProperty('--np-ambient-g', Math.round(gSum / count));
                modal.style.setProperty('--np-ambient-b', Math.round(bSum / count));
            }
        }
    } catch (e) {
        // Cross-origin or canvas error — ignore silently
    }
}

function npResetAmbientGlow() {
    const modal = document.querySelector('.np-modal');
    if (modal) {
        modal.style.setProperty('--np-ambient-r', '29');
        modal.style.setProperty('--np-ambient-g', '185');
        modal.style.setProperty('--np-ambient-b', '84');
    }
}

function updateNpPlayButton() {
    const playIcon = document.querySelector('.np-icon-play');
    const pauseIcon = document.querySelector('.np-icon-pause');
    if (playIcon && pauseIcon) {
        playIcon.classList.toggle('hidden', isPlaying);
        pauseIcon.classList.toggle('hidden', !isPlaying);
    }

    const viz = document.getElementById('np-visualizer');
    if (viz) viz.classList.toggle('playing', isPlaying);

    // Drive Web Audio visualizer (only when modal is open to save CPU)
    if (isPlaying && npModalOpen) {
        npInitVisualizer();
        npStartVisualizerLoop();
    } else {
        npStopVisualizerLoop();
    }
}

function updateNpProgress() {
    if (!npModalOpen || !audioPlayer) return;

    const npProgressBar = document.getElementById('np-progress-bar');
    const npProgressFill = document.getElementById('np-progress-fill');
    const npCurrentTime = document.getElementById('np-current-time');
    const npTotalTime = document.getElementById('np-total-time');

    if (audioPlayer.duration) {
        const progress = (audioPlayer.currentTime / audioPlayer.duration) * 100;
        if (npProgressBar && !npProgressBar.dataset.seeking) {
            npProgressBar.value = progress;
        }
        if (npProgressFill) npProgressFill.style.width = progress + '%';
        if (npCurrentTime) npCurrentTime.textContent = formatTime(audioPlayer.currentTime);
        if (npTotalTime) npTotalTime.textContent = formatTime(audioPlayer.duration);
    } else {
        if (npProgressBar) npProgressBar.value = 0;
        if (npProgressFill) npProgressFill.style.width = '0%';
        if (npCurrentTime) npCurrentTime.textContent = '0:00';
        if (npTotalTime) npTotalTime.textContent = '0:00';
    }
}

function handleNpProgressBarChange(event) {
    if (!audioPlayer || !audioPlayer.duration) return;
    const progress = parseFloat(event.target.value);
    const newTime = (progress / 100) * audioPlayer.duration;

    try {
        audioPlayer.currentTime = newTime;

        // Sync sidebar progress
        const sidebarBar = document.getElementById('progress-bar');
        const sidebarFill = document.getElementById('progress-fill');
        if (sidebarBar) sidebarBar.value = progress;
        if (sidebarFill) sidebarFill.style.width = progress + '%';

        // Sync modal progress fill
        const npFill = document.getElementById('np-progress-fill');
        if (npFill) npFill.style.width = progress + '%';

        // Update time displays
        const sidebarTime = document.getElementById('current-time');
        const npTime = document.getElementById('np-current-time');
        if (sidebarTime) sidebarTime.textContent = formatTime(newTime);
        if (npTime) npTime.textContent = formatTime(newTime);
    } catch (error) {
        console.warn('Seek failed:', error.message);
    }
}

function handleNpVolumeChange(event) {
    const volume = parseInt(event.target.value);
    if (audioPlayer) audioPlayer.volume = volume / 100;

    // Sync sidebar volume slider
    const sidebarVol = document.getElementById('volume-slider');
    if (sidebarVol) {
        sidebarVol.value = volume;
        sidebarVol.style.setProperty('--volume-percent', volume + '%');
    }

    // Update modal volume fill
    const npFill = document.getElementById('np-volume-fill');
    if (npFill) npFill.style.width = volume + '%';

    // Update mute state
    npMuted = volume === 0;
    updateNpMuteIcon();
}

function handleNpMuteToggle() {
    const npVol = document.getElementById('np-volume-slider');
    if (!npVol) return;

    if (npMuted) {
        // Unmute — restore previous volume
        npVol.value = npPreMuteVolume;
        npVol.dispatchEvent(new Event('input'));
        npMuted = false;
    } else {
        // Mute — save current volume, set to 0
        npPreMuteVolume = parseInt(npVol.value) || 70;
        npVol.value = 0;
        npVol.dispatchEvent(new Event('input'));
        npMuted = true;
    }
    updateNpMuteIcon();
}

function updateNpMuteIcon() {
    const muteBtn = document.getElementById('np-mute-btn');
    const volIcon = muteBtn ? muteBtn.querySelector('.np-icon-vol') : null;
    const mutedIcon = muteBtn ? muteBtn.querySelector('.np-icon-muted') : null;
    if (volIcon && mutedIcon) {
        volIcon.classList.toggle('hidden', npMuted);
        mutedIcon.classList.toggle('hidden', !npMuted);
    }
    if (muteBtn) muteBtn.classList.toggle('muted', npMuted);
}

function handleNpShuffle() {
    npShuffleOn = !npShuffleOn;
    const btn = document.getElementById('np-shuffle-btn');
    if (btn) btn.classList.toggle('active', npShuffleOn);
    updateNpPrevNextButtons();
}

function handleNpRepeat() {
    const badge = document.getElementById('np-repeat-one-badge');
    if (npRepeatMode === 'off') {
        npRepeatMode = 'all';
        if (audioPlayer) audioPlayer.loop = false;
    } else if (npRepeatMode === 'all') {
        npRepeatMode = 'one';
        if (audioPlayer) audioPlayer.loop = true;
    } else {
        npRepeatMode = 'off';
        if (audioPlayer) audioPlayer.loop = false;
    }
    const btn = document.getElementById('np-repeat-btn');
    if (btn) btn.classList.toggle('active', npRepeatMode !== 'off');
    if (badge) badge.classList.toggle('hidden', npRepeatMode !== 'one');
    updateNpPrevNextButtons();
}

// ===============================
// QUEUE MANAGEMENT
// ===============================

function addToQueue(track) {
    npQueue.push(track);
    showToast('Added to queue', 'success');
    renderNpQueue();
    updateNpPrevNextButtons();
    // If nothing is currently playing, auto-play the first queued track
    if (!currentTrack) {
        playQueueItem(npQueue.length - 1);
    }
}

function removeFromQueue(index) {
    if (index < 0 || index >= npQueue.length) return;
    const wasCurrentTrack = (index === npQueueIndex);
    npQueue.splice(index, 1);
    // Adjust current index
    if (npQueue.length === 0) {
        npQueueIndex = -1;
        // Current track keeps playing but queue is now empty — that's OK
    } else if (index < npQueueIndex) {
        npQueueIndex--;
    } else if (wasCurrentTrack) {
        // Removed the currently playing item
        if (npQueueIndex >= npQueue.length) {
            npQueueIndex = npQueue.length - 1;
        }
        // Play the next track at the adjusted index
        playQueueItem(npQueueIndex);
    }
    renderNpQueue();
    updateNpPrevNextButtons();
}

function clearQueue() {
    npQueue = [];
    npQueueIndex = -1;
    renderNpQueue();
    updateNpPrevNextButtons();
}

function playNextInQueue() {
    if (npQueue.length === 0) return;
    if (npShuffleOn) {
        // Pick a random index that is not the current one
        const candidates = [];
        for (let i = 0; i < npQueue.length; i++) {
            if (i !== npQueueIndex) candidates.push(i);
        }
        if (candidates.length === 0) return;
        const next = candidates[Math.floor(Math.random() * candidates.length)];
        playQueueItem(next);
    } else {
        const next = npQueueIndex + 1;
        if (next >= npQueue.length) {
            // End of queue — repeat-all wraps to start
            if (npRepeatMode === 'all') {
                playQueueItem(0);
            }
            return;
        }
        playQueueItem(next);
    }
}

function playPreviousInQueue() {
    // If more than 3 seconds in, restart current track
    if (audioPlayer && audioPlayer.currentTime > 3) {
        audioPlayer.currentTime = 0;
        if (audioPlayer.paused) audioPlayer.play();
        return;
    }
    if (npQueue.length === 0) return;
    const prev = npQueueIndex - 1;
    if (prev < 0) {
        // At start — restart current track
        if (audioPlayer) {
            audioPlayer.currentTime = 0;
            if (audioPlayer.paused) audioPlayer.play();
        }
        return;
    }
    playQueueItem(prev);
}

async function playQueueItem(index) {
    if (index < 0 || index >= npQueue.length) return;
    if (npLoadingQueueItem) return; // Prevent race condition from double-advance
    npLoadingQueueItem = true;
    npQueueIndex = index;
    const track = npQueue[index];

    try {
        if (track.is_library) {
            // Library track playback flow
            await stopStream();
            setTrackInfo({
                title: track.title,
                artist: track.artist,
                album: track.album,
                filename: track.file_path,
                is_library: true,
                image_url: track.image_url,
                id: track.id,
                artist_id: track.artist_id,
                album_id: track.album_id,
                bitrate: track.bitrate,
                sample_rate: track.sample_rate
            });
            showLoadingAnimation();

            const response = await fetch('/api/library/play', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    file_path: track.file_path,
                    title: track.title || '',
                    artist: track.artist || '',
                    album: track.album || ''
                })
            });
            const result = await response.json();
            if (!result.success) throw new Error(result.error || 'Failed to start playback');
            // Re-apply repeat-one loop property
            if (audioPlayer) audioPlayer.loop = (npRepeatMode === 'one');
            await startAudioPlayback();
        } else {
            // Non-library (stream) tracks cannot be queued for auto-advance
            // Just show track info — the stream flow handles its own playback
            setTrackInfo({
                title: track.title,
                artist: track.artist,
                album: track.album,
                filename: track.filename || track.file_path,
                is_library: false,
                image_url: track.image_url,
                id: track.id,
                artist_id: track.artist_id,
                album_id: track.album_id,
                bitrate: track.bitrate,
                sample_rate: track.sample_rate
            });
        }
    } catch (error) {
        console.error('Queue playback error:', error);
        showToast(`Skipping track: ${error.message}`, 'error');
        hideLoadingAnimation();
        // Auto-skip to next track on failure instead of stopping the queue
        npLoadingQueueItem = false;
        const nextIdx = npQueueIndex + 1;
        if (nextIdx < npQueue.length) {
            setTimeout(() => playQueueItem(nextIdx), 500);
        }
        return;
    } finally {
        npLoadingQueueItem = false;
    }

    renderNpQueue();
    updateNpPrevNextButtons();
}

function renderNpQueue() {
    const listEl = document.getElementById('np-queue-list');
    const emptyEl = document.getElementById('np-queue-empty');
    const countEl = document.getElementById('np-queue-count');
    if (!listEl) return;

    if (countEl) countEl.textContent = npQueue.length > 0 ? `(${npQueue.length})` : '';

    if (npQueue.length === 0) {
        listEl.innerHTML = '';
        if (emptyEl) emptyEl.classList.remove('hidden');
        return;
    }

    if (emptyEl) emptyEl.classList.add('hidden');
    listEl.innerHTML = '';

    npQueue.forEach((track, i) => {
        const item = document.createElement('div');
        item.className = 'np-queue-item' + (i === npQueueIndex ? ' active' : '');
        item.onclick = () => playQueueItem(i);

        const info = document.createElement('div');
        info.className = 'np-queue-item-info';

        const title = document.createElement('div');
        title.className = 'np-queue-item-title';
        title.textContent = track.title || 'Unknown Track';

        const artist = document.createElement('div');
        artist.className = 'np-queue-item-artist';
        artist.textContent = track.artist || 'Unknown Artist';

        info.appendChild(title);
        info.appendChild(artist);
        item.appendChild(info);

        const removeBtn = document.createElement('button');
        removeBtn.className = 'np-queue-item-remove';
        removeBtn.innerHTML = '&#10005;';
        removeBtn.title = 'Remove from queue';
        removeBtn.onclick = (e) => {
            e.stopPropagation();
            removeFromQueue(i);
        };
        item.appendChild(removeBtn);

        listEl.appendChild(item);
    });
}

function updateNpPrevNextButtons() {
    const canPrev = npQueueIndex > 0 || (audioPlayer && audioPlayer.currentTime > 3);
    const canNext = npQueue.length > 0 && (npShuffleOn ? npQueue.length > 1 : (npQueueIndex < npQueue.length - 1 || npRepeatMode === 'all'));

    // Full Now Playing modal buttons
    const prevBtn = document.getElementById('np-prev-btn');
    const nextBtn = document.getElementById('np-next-btn');
    if (prevBtn) prevBtn.disabled = !canPrev;
    if (nextBtn) nextBtn.disabled = !canNext;

    // Mini player buttons
    const miniPrevBtn = document.getElementById('mini-prev-btn');
    const miniNextBtn = document.getElementById('mini-next-btn');
    if (miniPrevBtn) miniPrevBtn.disabled = !canPrev;
    if (miniNextBtn) miniNextBtn.disabled = !canNext;
}

function handlePlayerKeyboardShortcuts(event) {
    // Don't intercept when typing in inputs or when non-player modals are open
    const tag = document.activeElement.tagName.toLowerCase();
    if (tag === 'input' || tag === 'textarea' || tag === 'select' || document.activeElement.isContentEditable) return;

    // Only handle when player modal is open OR when no other modal is visible
    const otherModals = document.querySelectorAll('.modal-overlay:not(.hidden):not(#np-modal-overlay)');
    if (otherModals.length > 0 && !npModalOpen) return;

    switch (event.key) {
        case ' ':
            if (!currentTrack) return;
            event.preventDefault();
            togglePlayback();
            break;
        case 'ArrowLeft':
            if (!audioPlayer || !audioPlayer.duration) return;
            event.preventDefault();
            audioPlayer.currentTime = Math.max(0, audioPlayer.currentTime - 5);
            break;
        case 'ArrowRight':
            if (!audioPlayer || !audioPlayer.duration) return;
            event.preventDefault();
            audioPlayer.currentTime = Math.min(audioPlayer.duration, audioPlayer.currentTime + 5);
            break;
        case 'ArrowUp':
            event.preventDefault();
            if (audioPlayer) {
                const newVol = Math.min(1, audioPlayer.volume + 0.05);
                audioPlayer.volume = newVol;
                syncVolumeUI(Math.round(newVol * 100));
            }
            break;
        case 'ArrowDown':
            event.preventDefault();
            if (audioPlayer) {
                const newVol = Math.max(0, audioPlayer.volume - 0.05);
                audioPlayer.volume = newVol;
                syncVolumeUI(Math.round(newVol * 100));
            }
            break;
        case 'm':
        case 'M':
            if (npModalOpen) handleNpMuteToggle();
            break;
        case 'Escape':
            if (npModalOpen) closeNowPlayingModal();
            break;
        default:
            return; // Don't prevent default for unhandled keys
    }
}

function syncVolumeUI(volumePercent) {
    // Sync both sidebar and modal volume UIs
    const sidebarVol = document.getElementById('volume-slider');
    const npVol = document.getElementById('np-volume-slider');
    const npFill = document.getElementById('np-volume-fill');

    if (sidebarVol) {
        sidebarVol.value = volumePercent;
        sidebarVol.style.setProperty('--volume-percent', volumePercent + '%');
    }
    if (npVol) npVol.value = volumePercent;
    if (npFill) npFill.style.width = volumePercent + '%';
}

function getNpAlbumArtUrl() {
    if (!currentTrack) return null;
    return currentTrack.image_url || currentTrack.album_cover_url || currentTrack.thumb_url || null;
}

// ===============================
// WEB AUDIO VISUALIZER
// ===============================

function npInitVisualizer() {
    if (npVizInitialized || !audioPlayer) return;
    try {
        if (!npAudioContext) {
            npAudioContext = new (window.AudioContext || window.webkitAudioContext)();
        }
        if (!npMediaSource) {
            npMediaSource = npAudioContext.createMediaElementSource(audioPlayer);
            npAnalyser = npAudioContext.createAnalyser();
            npAnalyser.fftSize = 64;
            npAnalyser.smoothingTimeConstant = 0.8;
            npMediaSource.connect(npAnalyser);
            npAnalyser.connect(npAudioContext.destination);
        }
        npVizInitialized = true;
    } catch (e) {
        console.warn('Web Audio visualizer init failed, using CSS fallback:', e.message);
        // Mark as CSS fallback
        const viz = document.getElementById('np-visualizer');
        if (viz) viz.classList.add('np-viz-css-fallback');
        npVizInitialized = true; // don't retry
    }
}

function npStartVisualizerLoop() {
    if (npVizAnimFrame) return; // Already running
    if (!npAnalyser) return; // No analyser — CSS fallback handles it

    if (npAudioContext && npAudioContext.state === 'suspended') {
        npAudioContext.resume();
    }

    const bars = document.querySelectorAll('.np-viz-bar');
    if (bars.length === 0) return;
    const bufferLength = npAnalyser.frequencyBinCount;
    const dataArray = new Uint8Array(bufferLength);

    function draw() {
        npVizAnimFrame = requestAnimationFrame(draw);
        npAnalyser.getByteFrequencyData(dataArray);

        // Map 7 bars to frequency bins (skip bin 0 which is DC offset)
        const binCount = Math.min(bufferLength - 1, 7);
        for (let i = 0; i < bars.length; i++) {
            const binIndex = Math.min(i + 1, bufferLength - 1);
            const value = dataArray[binIndex] / 255; // 0..1
            const scale = Math.max(0.08, value); // minimum visible height
            bars[i].style.transform = `scaleY(${scale})`;
        }
    }
    draw();
}

function npStopVisualizerLoop() {
    if (npVizAnimFrame) {
        cancelAnimationFrame(npVizAnimFrame);
        npVizAnimFrame = null;
    }
    // Reset bars to min
    const bars = document.querySelectorAll('.np-viz-bar');
    bars.forEach(bar => { bar.style.transform = 'scaleY(0.125)'; });
}

// ===============================
// SIDEBAR AUDIO VISUALIZER
// ===============================

let sidebarVizAnimFrame = null;
let sidebarVisualizerType = 'bars'; // bars | wave | spectrum | mirror | equalizer | none
const SIDEBAR_VIZ_BAR_COUNT = 32;

let _sidebarVizBuiltType = null;

function buildSidebarVizElements(type) {
    const container = document.getElementById('sidebar-visualizer');
    if (!container) return;
    if (_sidebarVizBuiltType === type && container.children.length > 0) return;
    _sidebarVizBuiltType = type;
    container.innerHTML = '';
    container.className = 'sidebar-visualizer';

    if (type === 'bars') {
        container.classList.add('viz-bars');
        for (let i = 0; i < SIDEBAR_VIZ_BAR_COUNT; i++) {
            const bar = document.createElement('div');
            bar.className = 'sidebar-viz-bar';
            container.appendChild(bar);
        }
    } else if (type === 'wave' || type === 'spectrum') {
        container.classList.add('viz-canvas');
        const canvas = document.createElement('canvas');
        canvas.className = 'sidebar-viz-canvas';
        canvas.width = 10;
        canvas.height = 600;
        container.appendChild(canvas);
    } else if (type === 'mirror') {
        container.classList.add('viz-mirror');
        for (let i = 0; i < SIDEBAR_VIZ_BAR_COUNT; i++) {
            const bar = document.createElement('div');
            bar.className = 'sidebar-viz-mirror-bar';
            container.appendChild(bar);
        }
    } else if (type === 'equalizer') {
        container.classList.add('viz-equalizer');
        for (let i = 0; i < SIDEBAR_VIZ_BAR_COUNT; i++) {
            const wrap = document.createElement('div');
            wrap.className = 'sidebar-viz-eq-wrap';
            const bar = document.createElement('div');
            bar.className = 'sidebar-viz-eq-bar';
            const peak = document.createElement('div');
            peak.className = 'sidebar-viz-eq-peak';
            wrap.appendChild(bar);
            wrap.appendChild(peak);
            container.appendChild(wrap);
        }
    }
}

function startSidebarVisualizer() {
    const type = sidebarVisualizerType;
    if (type === 'none') return;

    const container = document.getElementById('sidebar-visualizer');
    if (!container) return;

    buildSidebarVizElements(type);
    container.classList.add('active');

    if (sidebarVizAnimFrame) return;
    if (!npAnalyser) return;

    const bufferLength = npAnalyser.frequencyBinCount;
    const dataArray = new Uint8Array(bufferLength);
    const hueStart = 200, hueRange = 160;

    // Helper: average frequency bins for a given segment index
    function getBinValue(i, count) {
        const binsPerSeg = Math.max(1, Math.floor((bufferLength - 1) / count));
        let sum = 0;
        const start = i * binsPerSeg + 1;
        for (let b = 0; b < binsPerSeg; b++) sum += dataArray[Math.min(start + b, bufferLength - 1)];
        return (sum / binsPerSeg) / 255;
    }

    // ── Bars ──
    if (type === 'bars') {
        const bars = container.querySelectorAll('.sidebar-viz-bar');
        if (bars.length === 0) return;
        function drawBars() {
            sidebarVizAnimFrame = requestAnimationFrame(drawBars);
            npAnalyser.getByteFrequencyData(dataArray);
            for (let i = 0; i < bars.length; i++) {
                const value = getBinValue(i, bars.length);
                const scale = Math.max(0.08, value);
                const hue = (hueStart + (i / bars.length) * hueRange + value * 30) % 360;
                bars[i].style.transform = `scaleX(${scale})`;
                bars[i].style.backgroundColor = `hsla(${hue}, 80%, ${50 + value * 15}%, ${0.5 + value * 0.5})`;
            }
        }
        drawBars();

        // ── Wave ──
    } else if (type === 'wave') {
        const canvas = container.querySelector('.sidebar-viz-canvas');
        if (!canvas) return;
        const ctx = canvas.getContext('2d');
        let hueOffset = 0;
        function drawWave() {
            sidebarVizAnimFrame = requestAnimationFrame(drawWave);
            const ch = container.clientHeight;
            if (ch > 0 && canvas.height !== ch) canvas.height = ch;
            npAnalyser.getByteFrequencyData(dataArray);
            const w = canvas.width, h = canvas.height;
            if (h === 0) return;
            ctx.clearRect(0, 0, w, h);

            let totalEnergy = 0;
            for (let i = 1; i < bufferLength; i++) totalEnergy += dataArray[i];
            const avgEnergy = totalEnergy / (bufferLength - 1) / 255;
            hueOffset = (hueOffset + 0.5) % 360;

            const segments = 64;
            ctx.lineWidth = 3;
            ctx.lineCap = 'round';
            ctx.beginPath();
            for (let i = 0; i <= segments; i++) {
                const y = (i / segments) * h;
                const binIdx = Math.min(Math.floor((i / segments) * (bufferLength - 1)) + 1, bufferLength - 1);
                const value = dataArray[binIdx] / 255;
                const x = (w / 2) + Math.sin((i / segments) * Math.PI * 4 + Date.now() * 0.003) * value * (w - 2) * 0.4;
                if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
            }
            const grad = ctx.createLinearGradient(0, 0, 0, h);
            grad.addColorStop(0, `hsla(${hueOffset + 200}, 80%, 60%, ${0.3 + avgEnergy * 0.7})`);
            grad.addColorStop(0.5, `hsla(${hueOffset + 280}, 80%, 55%, ${0.3 + avgEnergy * 0.7})`);
            grad.addColorStop(1, `hsla(${hueOffset + 360}, 80%, 60%, ${0.3 + avgEnergy * 0.7})`);
            ctx.strokeStyle = grad;
            ctx.stroke();
            ctx.lineWidth = 6;
            ctx.globalAlpha = 0.15 + avgEnergy * 0.2;
            ctx.stroke();
            ctx.globalAlpha = 1;
        }
        drawWave();

        // ── Spectrum (mountain/terrain fill) ──
    } else if (type === 'spectrum') {
        const canvas = container.querySelector('.sidebar-viz-canvas');
        if (!canvas) return;
        const ctx = canvas.getContext('2d');
        let hueOffset = 0;
        // Smoothed values for fluid motion
        const smoothed = new Float32Array(64);

        function drawSpectrum() {
            sidebarVizAnimFrame = requestAnimationFrame(drawSpectrum);
            const ch = container.clientHeight;
            if (ch > 0 && canvas.height !== ch) canvas.height = ch;
            npAnalyser.getByteFrequencyData(dataArray);
            const w = canvas.width, h = canvas.height;
            if (h === 0) return;
            ctx.clearRect(0, 0, w, h);

            hueOffset = (hueOffset + 0.3) % 360;
            const segments = smoothed.length;

            // Smooth the frequency data
            for (let i = 0; i < segments; i++) {
                const binIdx = Math.min(Math.floor((i / segments) * (bufferLength - 1)) + 1, bufferLength - 1);
                const target = dataArray[binIdx] / 255;
                smoothed[i] += (target - smoothed[i]) * 0.25;
            }

            // Draw filled mountain shape from left edge
            ctx.beginPath();
            ctx.moveTo(0, 0);
            for (let i = 0; i <= segments; i++) {
                const y = (i / segments) * h;
                const value = i < segments ? smoothed[i] : smoothed[segments - 1];
                const x = value * w * 0.95;
                ctx.lineTo(x, y);
            }
            ctx.lineTo(0, h);
            ctx.closePath();

            // Gradient fill
            const fillGrad = ctx.createLinearGradient(0, 0, 0, h);
            fillGrad.addColorStop(0, `hsla(${hueOffset + 200}, 85%, 55%, 0.7)`);
            fillGrad.addColorStop(0.25, `hsla(${hueOffset + 240}, 80%, 50%, 0.6)`);
            fillGrad.addColorStop(0.5, `hsla(${hueOffset + 290}, 85%, 50%, 0.65)`);
            fillGrad.addColorStop(0.75, `hsla(${hueOffset + 330}, 80%, 50%, 0.6)`);
            fillGrad.addColorStop(1, `hsla(${hueOffset + 360}, 85%, 55%, 0.7)`);
            ctx.fillStyle = fillGrad;
            ctx.fill();

            // Bright edge line
            ctx.beginPath();
            ctx.moveTo(0, 0);
            for (let i = 0; i <= segments; i++) {
                const y = (i / segments) * h;
                const value = i < segments ? smoothed[i] : smoothed[segments - 1];
                ctx.lineTo(value * w * 0.95, y);
            }
            const lineGrad = ctx.createLinearGradient(0, 0, 0, h);
            lineGrad.addColorStop(0, `hsla(${hueOffset + 200}, 90%, 70%, 0.9)`);
            lineGrad.addColorStop(0.5, `hsla(${hueOffset + 290}, 90%, 65%, 0.9)`);
            lineGrad.addColorStop(1, `hsla(${hueOffset + 360}, 90%, 70%, 0.9)`);
            ctx.strokeStyle = lineGrad;
            ctx.lineWidth = 1.5;
            ctx.stroke();

            // Outer glow
            ctx.lineWidth = 4;
            ctx.globalAlpha = 0.2;
            ctx.stroke();
            ctx.globalAlpha = 1;
        }
        drawSpectrum();

        // ── Mirror (bars from center outward) ──
    } else if (type === 'mirror') {
        const bars = container.querySelectorAll('.sidebar-viz-mirror-bar');
        if (bars.length === 0) return;
        function drawMirror() {
            sidebarVizAnimFrame = requestAnimationFrame(drawMirror);
            npAnalyser.getByteFrequencyData(dataArray);
            const half = Math.floor(bars.length / 2);
            for (let i = 0; i < half; i++) {
                const value = getBinValue(i, half);
                const scale = Math.max(0.06, value);
                const hue = (hueStart + (i / half) * hueRange + value * 30) % 360;
                const color = `hsla(${hue}, 80%, ${50 + value * 15}%, ${0.5 + value * 0.5})`;
                // Top half — mirror index from center
                const topIdx = half - 1 - i;
                const bottomIdx = half + i;
                bars[topIdx].style.transform = `scaleX(${scale})`;
                bars[topIdx].style.backgroundColor = color;
                if (bottomIdx < bars.length) {
                    bars[bottomIdx].style.transform = `scaleX(${scale})`;
                    bars[bottomIdx].style.backgroundColor = color;
                }
            }
        }
        drawMirror();

        // ── Equalizer (bars with falling peak indicators) ──
    } else if (type === 'equalizer') {
        const wraps = container.querySelectorAll('.sidebar-viz-eq-wrap');
        if (wraps.length === 0) return;
        const peaks = new Float32Array(wraps.length);
        const peakVelocity = new Float32Array(wraps.length);

        function drawEqualizer() {
            sidebarVizAnimFrame = requestAnimationFrame(drawEqualizer);
            npAnalyser.getByteFrequencyData(dataArray);
            for (let i = 0; i < wraps.length; i++) {
                const value = getBinValue(i, wraps.length);
                const scale = Math.max(0.06, value);
                const hue = (hueStart + (i / wraps.length) * hueRange + value * 30) % 360;
                const barEl = wraps[i].querySelector('.sidebar-viz-eq-bar');
                const peakEl = wraps[i].querySelector('.sidebar-viz-eq-peak');

                barEl.style.transform = `scaleX(${scale})`;
                barEl.style.backgroundColor = `hsla(${hue}, 80%, ${50 + value * 15}%, ${0.5 + value * 0.5})`;

                // Peak hold with gravity
                if (value > peaks[i]) {
                    peaks[i] = value;
                    peakVelocity[i] = 0;
                } else {
                    peakVelocity[i] += 0.002; // gravity
                    peaks[i] = Math.max(0, peaks[i] - peakVelocity[i]);
                }
                const peakPos = Math.max(0.06, peaks[i]);
                peakEl.style.left = `${peakPos * 100}%`;
                peakEl.style.backgroundColor = `hsla(${hue}, 90%, 75%, ${0.6 + peaks[i] * 0.4})`;
                peakEl.style.boxShadow = `0 0 4px hsla(${hue}, 90%, 70%, ${peaks[i] * 0.5})`;
            }
        }
        drawEqualizer();
    }
}

function stopSidebarVisualizer() {
    if (sidebarVizAnimFrame) {
        cancelAnimationFrame(sidebarVizAnimFrame);
        sidebarVizAnimFrame = null;
    }
    const container = document.getElementById('sidebar-visualizer');
    if (container) {
        container.classList.remove('active');
    }
}

// Listen for visualizer type changes in settings — use isPlaying (not wasRunning)
// so switching from 'none' to a real type while music plays starts the visualizer
document.addEventListener('change', (e) => {
    if (e.target.id === 'sidebar-visualizer-type') {
        const newType = e.target.value;
        stopSidebarVisualizer();
        _sidebarVizBuiltType = null; // force rebuild for new type
        sidebarVisualizerType = newType;
        if (isPlaying && newType !== 'none') {
            npInitVisualizer();
            startSidebarVisualizer();
        }
    }
});

// ===============================
// RADIO MODE
// ===============================

async function npFetchRadioTracks() {
    if (!currentTrack || !currentTrack.id) return;
    try {
        npLoadingQueueItem = true;
        const excludeIds = npRecentlyPlayedIds.join(',');
        const resp = await fetch(`/api/library/radio?track_id=${currentTrack.id}&limit=50&exclude=${encodeURIComponent(excludeIds)}`);
        if (!resp.ok) {
            console.warn('Radio endpoint returned', resp.status);
            npLoadingQueueItem = false;
            return;
        }
        const data = await resp.json();
        // Bail if radio was toggled off during the fetch
        if (!npRadioMode) { npLoadingQueueItem = false; return; }
        if (data.tracks && data.tracks.length > 0) {
            data.tracks.forEach(t => {
                npQueue.push({
                    title: t.title || 'Unknown Track',
                    artist: t.artist || 'Unknown Artist',
                    album: t.album || 'Unknown Album',
                    file_path: t.file_path,
                    filename: t.file_path,
                    is_library: true,
                    image_url: t.image_url || null,
                    id: t.id,
                    artist_id: t.artist_id,
                    album_id: t.album_id,
                    bitrate: t.bitrate,
                    sample_rate: t.sample_rate
                });
            });
            showToast(`Radio: Added ${data.tracks.length} similar tracks`, 'success');
            renderNpQueue();
            updateNpPrevNextButtons();
            npLoadingQueueItem = false;
            // Only auto-advance if nothing is currently playing (triggered by onAudioEnded)
            if (!isPlaying) {
                playNextInQueue();
            }
        } else {
            showToast('Radio: No similar tracks found', 'info');
            npLoadingQueueItem = false;
        }
    } catch (e) {
        console.warn('Radio fetch error:', e);
        npLoadingQueueItem = false;
    }
}

// Media Session API
function initMediaSession() {
    if (!('mediaSession' in navigator)) return;

    navigator.mediaSession.setActionHandler('play', () => {
        if (audioPlayer && currentTrack) {
            audioPlayer.play().then(() => setPlayingState(true));
        }
    });
    navigator.mediaSession.setActionHandler('pause', () => {
        if (audioPlayer) {
            audioPlayer.pause();
            setPlayingState(false);
        }
    });
    navigator.mediaSession.setActionHandler('stop', () => {
        handleStop();
    });
    navigator.mediaSession.setActionHandler('seekbackward', () => {
        if (audioPlayer && audioPlayer.duration) {
            audioPlayer.currentTime = Math.max(0, audioPlayer.currentTime - 10);
        }
    });
    navigator.mediaSession.setActionHandler('seekforward', () => {
        if (audioPlayer && audioPlayer.duration) {
            audioPlayer.currentTime = Math.min(audioPlayer.duration, audioPlayer.currentTime + 10);
        }
    });
    navigator.mediaSession.setActionHandler('previoustrack', () => {
        if (npQueue.length > 0) playPreviousInQueue();
    });
    navigator.mediaSession.setActionHandler('nexttrack', () => {
        if (npQueue.length > 0) playNextInQueue();
    });
}

function updateMediaSessionMetadata() {
    if (!('mediaSession' in navigator) || !currentTrack) return;
    const artwork = [];
    const artUrl = getNpAlbumArtUrl();
    if (artUrl) artwork.push({ src: artUrl, sizes: '512x512', type: 'image/jpeg' });

    navigator.mediaSession.metadata = new MediaMetadata({
        title: currentTrack.title || 'Unknown Track',
        artist: currentTrack.artist || 'Unknown Artist',
        album: currentTrack.album || 'Unknown Album',
        artwork: artwork
    });
}

function updateMediaSessionPlaybackState() {
    if (!('mediaSession' in navigator)) return;
    if (!currentTrack) {
        navigator.mediaSession.playbackState = 'none';
    } else {
        navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';
    }
}

// ===============================
