// MusicRoom - Queue module
// Queue rendering and library display

(function() {
  const M = window.MusicRoom;
  
  // Selection state for bulk operations
  M.selectedQueueIndices = new Set();
  M.selectedLibraryIds = new Set();
  
  // Last selected index for shift-select range
  let lastSelectedQueueIndex = null;
  let lastSelectedLibraryIndex = null;
  
  // Context menu state
  let activeContextMenu = null;
  
  // Download state - only one at a time
  let isDownloading = false;
  let exportQueue = [];
  let isExporting = false;
  
  // Download a track to user's device (uses cache if available)
  async function downloadTrack(trackId, filename) {
    if (isDownloading) {
      M.showToast("Download already in progress", "warning");
      return;
    }
    
    isDownloading = true;
    M.showToast(`Downloading: ${filename}`);
    
    try {
      let blob = null;
      
      // Try to get from cache first
      if (M.cachedTracks.has(trackId)) {
        try {
          const cached = await TrackStorage.get(trackId);
          if (cached && cached.blob) {
            blob = cached.blob;
          }
        } catch (e) {
          console.log("Cache miss, fetching from server");
        }
      }
      
      // Fall back to fetching from server
      if (!blob) {
        const res = await fetch(`/api/tracks/${encodeURIComponent(trackId)}`);
        if (!res.ok) throw new Error(`Server returned ${res.status}`);
        blob = await res.blob();
      }
      
      if (!blob || blob.size === 0) {
        throw new Error("Empty blob");
      }
      
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
      
      M.showToast(`Downloaded: ${filename}`);
    } catch (e) {
      console.error("Download error:", e);
      M.showToast(`Download failed: ${e.message}`, "error");
    } finally {
      isDownloading = false;
    }
  }
  
  // Export all cached tracks
  M.exportAllCached = async function() {
    if (isExporting) {
      M.showToast("Export already in progress", "warning");
      return;
    }
    
    // Build list of cached tracks with filenames
    const cachedIds = [...M.cachedTracks];
    if (cachedIds.length === 0) {
      M.showToast("No cached tracks to export", "warning");
      return;
    }
    
    // Find filenames from library or queue
    const trackMap = new Map();
    M.library.forEach(t => { if (t.filename) trackMap.set(t.id, t.filename); });
    M.queue.forEach(t => { if (t.filename && !trackMap.has(t.id)) trackMap.set(t.id, t.filename); });
    
    // Only export tracks with known filenames
    exportQueue = cachedIds
      .filter(id => trackMap.has(id))
      .map(id => ({ id, filename: trackMap.get(id) }));
    
    const skipped = cachedIds.length - exportQueue.length;
    if (exportQueue.length === 0) {
      M.showToast("No exportable tracks (filenames unknown)", "warning");
      return;
    }
    
    isExporting = true;
    const msg = skipped > 0 
      ? `Exporting ${exportQueue.length} tracks (${skipped} skipped - not in library)`
      : `Exporting ${exportQueue.length} cached tracks...`;
    M.showToast(msg);
    
    let exported = 0;
    for (const { id, filename } of exportQueue) {
      if (!isExporting) break; // Allow cancellation
      
      try {
        const cached = await TrackStorage.get(id);
        if (cached && cached.blob) {
          const url = URL.createObjectURL(cached.blob);
          const a = document.createElement("a");
          a.href = url;
          a.download = filename;
          document.body.appendChild(a);
          a.click();
          document.body.removeChild(a);
          URL.revokeObjectURL(url);
          exported++;
          
          // Small delay between downloads to not overwhelm browser
          await new Promise(r => setTimeout(r, 500));
        }
      } catch (e) {
        console.error(`Export error for ${filename}:`, e);
      }
    }
    
    isExporting = false;
    exportQueue = [];
    M.showToast(`Exported ${exported} tracks`);
  };
  
  M.cancelExport = function() {
    if (isExporting) {
      isExporting = false;
      M.showToast("Export cancelled");
    }
  };
  
  // Close context menu when clicking elsewhere
  document.addEventListener("click", () => {
    if (activeContextMenu) {
      activeContextMenu.remove();
      activeContextMenu = null;
    }
  });
  
  // Show context menu
  function showContextMenu(e, items) {
    e.preventDefault();
    if (activeContextMenu) activeContextMenu.remove();
    
    const menu = document.createElement("div");
    menu.className = "context-menu";
    
    items.forEach(item => {
      const el = document.createElement("div");
      el.className = "context-menu-item" + (item.danger ? " danger" : "");
      el.textContent = item.label;
      el.onclick = (ev) => {
        ev.stopPropagation();
        menu.remove();
        activeContextMenu = null;
        item.action();
      };
      menu.appendChild(el);
    });
    
    document.body.appendChild(menu);
    
    // Position menu, keep within viewport
    let x = e.clientX;
    let y = e.clientY;
    const rect = menu.getBoundingClientRect();
    if (x + rect.width > window.innerWidth) x = window.innerWidth - rect.width - 5;
    if (y + rect.height > window.innerHeight) y = window.innerHeight - rect.height - 5;
    menu.style.left = x + "px";
    menu.style.top = y + "px";
    
    activeContextMenu = menu;
  }
  
  // Drag state for queue reordering
  let draggedIndices = [];
  let draggedLibraryIds = [];
  let dropTargetIndex = null;
  let dragSource = null; // 'queue' or 'library'
  
  // Insert library tracks into queue at position
  async function insertTracksAtPosition(trackIds, position) {
    if (!M.currentChannelId || trackIds.length === 0) return;
    
    // Build new queue with tracks inserted at position
    const newQueue = [...M.queue];
    const newTrackIds = [...newQueue.map(t => t.id)];
    newTrackIds.splice(position, 0, ...trackIds);
    
    const res = await fetch("/api/channels/" + M.currentChannelId + "/queue", {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ set: newTrackIds })
    });
    
    if (res.status === 403) M.flashPermissionDenied();
    else if (res.ok) {
      M.showToast(`Added ${trackIds.length} track${trackIds.length > 1 ? 's' : ''}`);
      M.clearSelections();
    }
  }
  
  // Reorder queue on server
  async function reorderQueue(fromIndices, toIndex) {
    if (!M.currentChannelId || fromIndices.length === 0) return;
    
    // Build new queue order
    const newQueue = [...M.queue];
    
    // Sort indices descending to remove from end first
    const sortedIndices = [...fromIndices].sort((a, b) => b - a);
    const movedTracks = [];
    
    // Remove items (in reverse order to preserve indices)
    for (const idx of sortedIndices) {
      movedTracks.unshift(newQueue.splice(idx, 1)[0]);
    }
    
    // Adjust target index for removed items before it
    let adjustedTarget = toIndex;
    for (const idx of fromIndices) {
      if (idx < toIndex) adjustedTarget--;
    }
    
    // Insert at new position
    newQueue.splice(adjustedTarget, 0, ...movedTracks);
    
    // Send to server
    const trackIds = newQueue.map(t => t.id);
    const res = await fetch("/api/channels/" + M.currentChannelId + "/queue", {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ set: trackIds })
    });
    
    if (res.status === 403) M.flashPermissionDenied();
    else if (res.ok) {
      M.clearSelections();
    }
  }
  
  // Toggle selection mode (with optional shift for range select)
  M.toggleQueueSelection = function(index, shiftKey = false) {
    if (shiftKey && lastSelectedQueueIndex !== null) {
      // Range select: select all between last and current
      const start = Math.min(lastSelectedQueueIndex, index);
      const end = Math.max(lastSelectedQueueIndex, index);
      for (let i = start; i <= end; i++) {
        M.selectedQueueIndices.add(i);
      }
    } else {
      if (M.selectedQueueIndices.has(index)) {
        M.selectedQueueIndices.delete(index);
      } else {
        M.selectedQueueIndices.add(index);
      }
      lastSelectedQueueIndex = index;
    }
    M.renderQueue();
  };
  
  M.toggleLibrarySelection = function(index, shiftKey = false) {
    if (shiftKey && lastSelectedLibraryIndex !== null) {
      // Range select: select all between last and current
      const start = Math.min(lastSelectedLibraryIndex, index);
      const end = Math.max(lastSelectedLibraryIndex, index);
      for (let i = start; i <= end; i++) {
        M.selectedLibraryIds.add(M.library[i].id);
      }
    } else {
      const trackId = M.library[index].id;
      if (M.selectedLibraryIds.has(trackId)) {
        M.selectedLibraryIds.delete(trackId);
      } else {
        M.selectedLibraryIds.add(trackId);
      }
      lastSelectedLibraryIndex = index;
    }
    M.renderLibrary();
  };
  
  M.clearSelections = function() {
    M.selectedQueueIndices.clear();
    M.selectedLibraryIds.clear();
    lastSelectedQueueIndex = null;
    lastSelectedLibraryIndex = null;
    M.renderQueue();
    M.renderLibrary();
  };
  
  // Update cache status for all tracks
  M.updateCacheStatus = async function() {
    const cached = await TrackStorage.list();
    
    // Migration: remove old filename-based cache entries (keep only sha256: prefixed)
    const oldEntries = cached.filter(id => !id.startsWith("sha256:"));
    if (oldEntries.length > 0) {
      console.log("[Cache] Migrating: removing", oldEntries.length, "old filename-based entries");
      for (const oldId of oldEntries) {
        await TrackStorage.remove(oldId);
      }
      // Re-fetch after cleanup
      const updated = await TrackStorage.list();
      M.cachedTracks = new Set(updated);
    } else {
      M.cachedTracks = new Set(cached);
    }
    console.log("[Cache] Updated cache status:", M.cachedTracks.size, "tracks cached");
  };
  
  // Debug: log cache status for current track
  M.debugCacheStatus = function() {
    if (!M.currentTrackId) {
      console.log("[Cache Debug] No current track");
      return;
    }
    const trackCache = M.getTrackCache(M.currentTrackId);
    const segmentsPct = Math.round((trackCache.size / M.SEGMENTS) * 100);
    const inCachedTracks = M.cachedTracks.has(M.currentTrackId);
    const hasBlobUrl = M.trackBlobs.has(M.currentTrackId);
    const bulkStarted = M.bulkDownloadStarted.get(M.currentTrackId);
    
    console.log("[Cache Debug]", {
      trackId: M.currentTrackId.slice(0, 16) + "...",
      segments: `${trackCache.size}/${M.SEGMENTS} (${segmentsPct}%)`,
      inCachedTracks,
      hasBlobUrl,
      bulkStarted,
      loadingSegments: [...M.loadingSegments],
      cachedTracksSize: M.cachedTracks.size
    });
  };
  
  // Debug: compare queue track IDs with cached track IDs
  M.debugCacheMismatch = function() {
    console.log("[Cache Mismatch Debug]");
    console.log("=== Raw State ===");
    console.log("M.cachedTracks:", M.cachedTracks);
    console.log("M.trackCaches:", M.trackCaches);
    console.log("M.trackBlobs keys:", [...M.trackBlobs.keys()]);
    console.log("M.bulkDownloadStarted:", M.bulkDownloadStarted);
    console.log("=== Queue Tracks ===");
    M.queue.forEach((t, i) => {
      const id = t.id || t.filename;
      console.log(`  [${i}] ${t.title?.slice(0, 30)} | id: ${id?.slice(0, 12)}... | cached: ${M.cachedTracks.has(id)}`);
    });
    console.log("=== Cached Track IDs ===");
    [...M.cachedTracks].forEach(id => {
      console.log(`  ${id.slice(0, 20)}...`);
    });
  };
  
  // Debug: detailed info for a specific track
  M.debugTrack = function(index) {
    const track = M.queue[index];
    if (!track) {
      console.log("[Debug] No track at index", index);
      return;
    }
    const id = track.id || track.filename;
    console.log("[Debug Track]", {
      index,
      title: track.title,
      id,
      idPrefix: id?.slice(0, 16),
      inCachedTracks: M.cachedTracks.has(id),
      inTrackCaches: M.trackCaches.has(id),
      segmentCount: M.trackCaches.get(id)?.size || 0,
      inTrackBlobs: M.trackBlobs.has(id),
      bulkStarted: M.bulkDownloadStarted.get(id)
    });
  };
  
  // Clear all caches (for debugging)
  M.clearAllCaches = async function() {
    await TrackStorage.clear();
    M.cachedTracks.clear();
    M.trackCaches.clear();
    M.trackBlobs.clear();
    M.bulkDownloadStarted.clear();
    M.renderQueue();
    M.renderLibrary();
    console.log("[Cache] All caches cleared. Refresh the page.");
  };
  
  // Render the current queue
  M.renderQueue = function() {
    const container = M.$("#queue");
    if (!container) return;
    container.innerHTML = "";
    
    const canEdit = M.canControl();
    
    // Setup container-level drag handlers for dropping from library
    if (canEdit) {
      container.ondragover = (e) => {
        if (dragSource === 'library') {
          e.preventDefault();
          e.dataTransfer.dropEffect = "copy";
          // If no tracks or hovering at bottom, show we can drop
          if (M.queue.length === 0) {
            container.classList.add("drop-target");
          }
        }
      };
      
      container.ondragleave = (e) => {
        // Only remove if leaving the container entirely
        if (!container.contains(e.relatedTarget)) {
          container.classList.remove("drop-target");
        }
      };
      
      container.ondrop = (e) => {
        container.classList.remove("drop-target");
        // Handle drop on empty queue or at the end
        if (dragSource === 'library' && draggedLibraryIds.length > 0) {
          e.preventDefault();
          const targetIndex = dropTargetIndex !== null ? dropTargetIndex : M.queue.length;
          insertTracksAtPosition(draggedLibraryIds, targetIndex);
          draggedLibraryIds = [];
          dragSource = null;
          dropTargetIndex = null;
        }
      };
    }
    
    if (M.queue.length === 0) {
      container.innerHTML = '<div class="empty drop-zone">Queue empty - drag tracks here</div>';
      M.updateNowPlayingBar();
      return;
    }
    
    // Debug: log first few track cache statuses
    if (M.queue.length > 0 && M.cachedTracks.size > 0) {
      const sample = M.queue.slice(0, 3).map(t => {
        const id = t.id || t.filename;
        return { title: t.title?.slice(0, 20), id: id?.slice(0, 12), cached: M.cachedTracks.has(id) };
      });
      console.log("[Queue Render] Sample tracks:", sample, "| cachedTracks sample:", [...M.cachedTracks].slice(0, 3).map(s => s.slice(0, 12)));
    }
    
    M.queue.forEach((track, i) => {
      const div = document.createElement("div");
      const trackId = track.id || track.filename;
      const isCached = M.cachedTracks.has(trackId);
      const isSelected = M.selectedQueueIndices.has(i);
      div.className = "track" + (i === M.currentIndex ? " active" : "") + (isCached ? " cached" : " not-cached") + (isSelected ? " selected" : "");
      div.dataset.index = i;
      const title = track.title?.trim() || (track.id || track.filename || "Unknown").replace(/\.[^.]+$/, "");
      div.title = title; // Tooltip for full name
      
      const checkmark = isSelected ? `<span class="track-checkmark">✓</span>` : '';
      const trackNum = `<span class="track-number">${i + 1}.</span>`;
      div.innerHTML = `${checkmark}<span class="cache-indicator"></span>${trackNum}<span class="track-title">${title}</span><span class="track-actions"><span class="duration">${M.fmt(track.duration)}</span></span>`;
      
      // Drag and drop for reordering (if user can edit)
      if (canEdit) {
        div.draggable = true;
        
        div.ondragstart = (e) => {
          dragSource = 'queue';
          draggedLibraryIds = [];
          // If dragging a selected item, drag all selected; otherwise just this one
          if (M.selectedQueueIndices.has(i)) {
            draggedIndices = [...M.selectedQueueIndices];
          } else {
            draggedIndices = [i];
          }
          div.classList.add("dragging");
          e.dataTransfer.effectAllowed = "move";
          e.dataTransfer.setData("text/plain", "queue:" + draggedIndices.join(","));
        };
        
        div.ondragend = () => {
          div.classList.remove("dragging");
          draggedIndices = [];
          draggedLibraryIds = [];
          dragSource = null;
          // Clear all drop indicators
          container.querySelectorAll(".drop-above, .drop-below").forEach(el => {
            el.classList.remove("drop-above", "drop-below");
          });
        };
        
        div.ondragover = (e) => {
          e.preventDefault();
          e.dataTransfer.dropEffect = "move";
          
          // Determine if dropping above or below
          const rect = div.getBoundingClientRect();
          const midY = rect.top + rect.height / 2;
          const isAbove = e.clientY < midY;
          
          // Clear other indicators
          container.querySelectorAll(".drop-above, .drop-below").forEach(el => {
            el.classList.remove("drop-above", "drop-below");
          });
          
          // Don't show indicator on dragged queue items (for reorder)
          if (dragSource === 'queue' && draggedIndices.includes(i)) return;
          
          div.classList.add(isAbove ? "drop-above" : "drop-below");
          dropTargetIndex = isAbove ? i : i + 1;
        };
        
        div.ondragleave = () => {
          div.classList.remove("drop-above", "drop-below");
        };
        
        div.ondrop = (e) => {
          e.preventDefault();
          div.classList.remove("drop-above", "drop-below");
          
          if (dragSource === 'library' && draggedLibraryIds.length > 0 && dropTargetIndex !== null) {
            // Insert library tracks at drop position
            insertTracksAtPosition(draggedLibraryIds, dropTargetIndex);
          } else if (dragSource === 'queue' && draggedIndices.length > 0 && dropTargetIndex !== null) {
            // Reorder queue
            const minDragged = Math.min(...draggedIndices);
            const maxDragged = Math.max(...draggedIndices);
            if (dropTargetIndex < minDragged || dropTargetIndex > maxDragged + 1) {
              reorderQueue(draggedIndices, dropTargetIndex);
            }
          }
          
          draggedIndices = [];
          draggedLibraryIds = [];
          dragSource = null;
          dropTargetIndex = null;
        };
      }
      
      // Click toggles selection
      div.onclick = (e) => {
        if (e.target.closest('.track-actions')) return;
        M.toggleQueueSelection(i, e.shiftKey);
      };
      
      // Right-click context menu
      div.oncontextmenu = (e) => {
        const menuItems = [];
        const hasSelection = M.selectedQueueIndices.size > 0;
        const selectedCount = hasSelection ? M.selectedQueueIndices.size : 1;
        const indicesToRemove = hasSelection ? [...M.selectedQueueIndices] : [i];
        
        // Play track option (only for single track, not bulk)
        if (!hasSelection) {
          menuItems.push({
            label: "▶ Play track",
            action: async () => {
              if (M.synced && M.currentChannelId) {
                const res = await fetch("/api/channels/" + M.currentChannelId + "/jump", {
                  method: "POST",
                  headers: { "Content-Type": "application/json" },
                  body: JSON.stringify({ index: i })
                });
                if (res.status === 403) M.flashPermissionDenied();
              } else {
                M.currentIndex = i;
                M.currentTrackId = trackId;
                M.serverTrackDuration = track.duration;
                M.setTrackTitle(title);
                M.loadingSegments.clear();
                const cachedUrl = await M.loadTrackBlob(trackId);
                M.audio.src = cachedUrl || M.getTrackUrl(trackId);
                M.audio.currentTime = 0;
                M.localTimestamp = 0;
                M.audio.play();
                M.renderQueue();
              }
            }
          });
        }
        
        // Remove track(s) option (if user can edit)
        if (canEdit) {
          const label = selectedCount > 1 ? `✕ Remove ${selectedCount} tracks` : "✕ Remove track";
          menuItems.push({
            label,
            danger: true,
            action: async () => {
              if (!M.currentChannelId) return;
              const res = await fetch("/api/channels/" + M.currentChannelId + "/queue", {
                method: "PATCH",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ remove: indicesToRemove })
              });
              if (res.status === 403) M.flashPermissionDenied();
              else if (res.ok) {
                M.showToast(selectedCount > 1 ? `Removed ${selectedCount} tracks` : "Track removed");
                M.clearSelections();
              }
            }
          });
        }
        
        // Preload track(s) option - always show
        const idsToPreload = hasSelection 
          ? [...M.selectedQueueIndices].map(idx => M.queue[idx]?.id).filter(Boolean)
          : [trackId];
        const preloadLabel = hasSelection && idsToPreload.length > 1 ? `Preload ${idsToPreload.length} tracks` : "Preload track";
        menuItems.push({
          label: preloadLabel,
          action: () => {
            const uncachedIds = idsToPreload.filter(id => !M.cachedTracks.has(id));
            if (uncachedIds.length === 0) {
              M.showToast("All tracks already cached");
              return;
            }
            M.showToast(`Preloading ${uncachedIds.length} track${uncachedIds.length > 1 ? 's' : ''}...`);
            uncachedIds.forEach(id => M.downloadAndCacheTrack(id));
          }
        });
        
        // Download track option (single track only)
        if (!hasSelection) {
          menuItems.push({
            label: "Download",
            action: () => downloadTrack(trackId, track.filename)
          });
        }
        
        // Clear selection option (if items selected)
        if (hasSelection) {
          menuItems.push({
            label: "Clear selection",
            action: () => M.clearSelections()
          });
        }
        
        showContextMenu(e, menuItems);
      };
      
      container.appendChild(div);
    });
    
    M.updateNowPlayingBar();
  };
  
  // Update the now-playing bar above the queue
  M.updateNowPlayingBar = function() {
    const bar = M.$("#now-playing-bar");
    if (!bar) return;
    
    const track = M.queue[M.currentIndex];
    if (!track) {
      bar.classList.add("hidden");
      return;
    }
    
    const title = track.title?.trim() || (track.id || track.filename || "Unknown").replace(/\.[^.]+$/, "");
    bar.innerHTML = `<span class="label">Now playing:</span> ${title}`;
    bar.title = title;
    bar.classList.remove("hidden");
  };
  
  // Scroll queue to current track
  M.scrollToCurrentTrack = function() {
    const container = M.$("#queue");
    if (!container) return;
    
    const activeTrack = container.querySelector(".track.active");
    if (activeTrack) {
      activeTrack.scrollIntoView({ behavior: "smooth", block: "center" });
    }
  };
  
  // Setup now-playing bar click handler
  document.addEventListener("DOMContentLoaded", () => {
    const bar = M.$("#now-playing-bar");
    if (bar) {
      bar.onclick = () => M.scrollToCurrentTrack();
    }
  });
  
  // Library search state
  M.librarySearchQuery = "";
  
  // Render the library
  M.renderLibrary = function() {
    const container = M.$("#library");
    if (!container) return;
    container.innerHTML = "";
    if (M.library.length === 0) {
      container.innerHTML = '<div class="empty">No tracks discovered</div>';
      return;
    }
    
    const canEdit = M.canControl();
    const query = M.librarySearchQuery.toLowerCase();
    
    // Filter library by search query
    const filteredLibrary = query 
      ? M.library.map((track, i) => ({ track, i })).filter(({ track }) => {
          const title = track.title?.trim() || track.filename;
          return title.toLowerCase().includes(query);
        })
      : M.library.map((track, i) => ({ track, i }));
    
    if (filteredLibrary.length === 0) {
      container.innerHTML = '<div class="empty">No matches</div>';
      return;
    }
    
    filteredLibrary.forEach(({ track, i }) => {
      const div = document.createElement("div");
      const isCached = M.cachedTracks.has(track.id);
      const isSelected = M.selectedLibraryIds.has(track.id);
      div.className = "track" + (isCached ? " cached" : " not-cached") + (isSelected ? " selected" : "");
      const title = track.title?.trim() || track.filename.replace(/\.[^.]+$/, "");
      div.title = title; // Tooltip for full name
      
      const checkmark = isSelected ? `<span class="track-checkmark">✓</span>` : '';
      div.innerHTML = `${checkmark}<span class="cache-indicator"></span><span class="track-title">${title}</span><span class="track-actions"><span class="duration">${M.fmt(track.duration)}</span></span>`;
      
      // Drag from library to queue (if user can edit)
      if (canEdit) {
        div.draggable = true;
        
        div.ondragstart = (e) => {
          dragSource = 'library';
          draggedIndices = [];
          // If dragging a selected item, drag all selected; otherwise just this one
          if (M.selectedLibraryIds.has(track.id)) {
            draggedLibraryIds = [...M.selectedLibraryIds];
          } else {
            draggedLibraryIds = [track.id];
          }
          div.classList.add("dragging");
          e.dataTransfer.effectAllowed = "copy";
          e.dataTransfer.setData("text/plain", "library:" + draggedLibraryIds.join(","));
        };
        
        div.ondragend = () => {
          div.classList.remove("dragging");
          draggedIndices = [];
          draggedLibraryIds = [];
          dragSource = null;
          // Clear drop indicators in queue
          const queueContainer = M.$("#queue");
          if (queueContainer) {
            queueContainer.querySelectorAll(".drop-above, .drop-below").forEach(el => {
              el.classList.remove("drop-above", "drop-below");
            });
          }
        };
      }
      
      // Click toggles selection
      div.onclick = (e) => {
        if (e.target.closest('.track-actions')) return;
        M.toggleLibrarySelection(i, e.shiftKey);
      };
      
      // Right-click context menu
      div.oncontextmenu = (e) => {
        const menuItems = [];
        const hasSelection = M.selectedLibraryIds.size > 0;
        const selectedCount = hasSelection ? M.selectedLibraryIds.size : 1;
        const idsToAdd = hasSelection ? [...M.selectedLibraryIds] : [track.id];
        
        // Play track option (local mode only, single track)
        if (!M.synced && !hasSelection) {
          menuItems.push({
            label: "▶ Play track",
            action: async () => {
              M.currentTrackId = track.id;
              M.serverTrackDuration = track.duration;
              M.setTrackTitle(title);
              M.loadingSegments.clear();
              const cachedUrl = await M.loadTrackBlob(track.id);
              M.audio.src = cachedUrl || M.getTrackUrl(track.id);
              M.audio.currentTime = 0;
              M.localTimestamp = 0;
              M.audio.play();
            }
          });
        }
        
        // Add to queue option (if user can edit)
        if (canEdit) {
          const label = selectedCount > 1 ? `＋ Add ${selectedCount} to queue` : "＋ Add to queue";
          menuItems.push({
            label,
            action: async () => {
              if (!M.currentChannelId) {
                M.showToast("No channel selected");
                return;
              }
              const res = await fetch("/api/channels/" + M.currentChannelId + "/queue", {
                method: "PATCH",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ add: idsToAdd })
              });
              if (res.status === 403) M.flashPermissionDenied();
              else if (res.ok) {
                M.showToast(selectedCount > 1 ? `Added ${selectedCount} tracks` : "Track added to queue");
                M.clearSelections();
              }
            }
          });
        }
        
        // Preload track(s) option - always show, will skip already cached
        const preloadLabel = selectedCount > 1 ? `Preload ${selectedCount} tracks` : "Preload track";
        menuItems.push({
          label: preloadLabel,
          action: () => {
            const uncachedIds = idsToAdd.filter(id => !M.cachedTracks.has(id));
            if (uncachedIds.length === 0) {
              M.showToast("All tracks already cached");
              return;
            }
            M.showToast(`Preloading ${uncachedIds.length} track${uncachedIds.length > 1 ? 's' : ''}...`);
            uncachedIds.forEach(id => M.downloadAndCacheTrack(id));
          }
        });
        
        // Download track option (single track only)
        if (!hasSelection) {
          menuItems.push({
            label: "Download",
            action: () => downloadTrack(track.id, track.filename)
          });
        }
        
        // Export all cached option (if there are cached tracks)
        if (M.cachedTracks.size > 0) {
          menuItems.push({
            label: `Preload and export ${M.cachedTracks.size} cached`,
            action: () => M.exportAllCached()
          });
        }
        
        // Clear selection option (if items selected)
        if (hasSelection) {
          menuItems.push({
            label: "Clear selection",
            action: () => M.clearSelections()
          });
        }
        
        if (menuItems.length > 0) {
          showContextMenu(e, menuItems);
        }
      };
      
      container.appendChild(div);
    });
  };
  
  // Load library from server
  M.loadLibrary = async function() {
    try {
      const res = await fetch("/api/library");
      M.library = await res.json();
      M.renderLibrary();
    } catch (e) {
      console.warn("Failed to load library");
    }
  };
  
  // Setup library search
  document.addEventListener("DOMContentLoaded", () => {
    const searchInput = M.$("#library-search");
    if (searchInput) {
      searchInput.addEventListener("input", (e) => {
        M.librarySearchQuery = e.target.value;
        M.renderLibrary();
      });
    }
  });
})();
