import { readdir } from "fs/promises";
import { Channel, type Track, type PersistenceCallback } from "./channel";
import { Library } from "./library";
import {
  saveChannel,
  updateChannelState,
  loadAllChannels,
  saveChannelQueue,
  loadChannelQueue,
  removeTrackFromQueues,
  addTracksToPlaylist,
} from "./db";
import { config, MUSIC_DIR, DEFAULT_CONFIG } from "./config";
import { state, setLibrary } from "./state";
import { broadcastToAll, broadcastChannelList, sendToUser } from "./broadcast";
import {
  initYtdlp,
  setProgressCallback,
  setTrackReadyCallback,
  skipSlowQueueItem,
  getQueuedSlowItems,
  type QueueItem,
} from "./ytdlp";

// Auto-discover tracks if queue is empty
export async function discoverTracks(): Promise<string[]> {
  try {
    const files = await readdir(MUSIC_DIR);
    return files.filter((f) => /\.(mp3|ogg|flac|wav|m4a|aac)$/i.test(f)).sort();
  } catch {
    return [];
  }
}

// Generate unique channel ID
export function generateChannelId(): string {
  return Math.random().toString(36).slice(2, 10);
}

// Persistence callback for channels
const persistChannel: PersistenceCallback = (channel, type) => {
  if (type === "state") {
    updateChannelState(channel.id, {
      currentIndex: channel.currentIndex,
      startedAt: channel.startedAt,
      paused: channel.paused,
      pausedAt: channel.pausedAt,
      playbackMode: channel.playbackMode,
    });
  } else if (type === "queue") {
    saveChannelQueue(channel.id, channel.queue.map(t => t.id));
  }
};

// Helper to build Track objects from track IDs using library
export function buildTracksFromIds(trackIds: string[], lib: Library): Track[] {
  const tracks: Track[] = [];
  for (const tid of trackIds) {
    const libTrack = lib.getTrack(tid);
    if (libTrack && libTrack.duration > 0) {
      tracks.push({
        id: libTrack.id,
        filename: libTrack.filename,
        title: libTrack.title || libTrack.filename.replace(/\.[^.]+$/, ""),
        duration: libTrack.duration,
        replayGainDb: libTrack.replayGainDb,
        replayPeak: libTrack.replayPeak,
      });
    }
  }
  return tracks;
}

// Helper to get all library tracks as Track objects
export function getAllLibraryTracks(lib: Library): Track[] {
  return lib.getAllTracks()
    .filter(t => t.duration > 0)
    .map(t => ({
      id: t.id,
      filename: t.filename,
      title: t.title || t.filename.replace(/\.[^.]+$/, ""),
      duration: t.duration,
      replayGainDb: t.replayGainDb,
      replayPeak: t.replayPeak,
    }));
}

export async function init(): Promise<void> {
  // Initialize yt-dlp if configured
  const ytdlpConfig = config.ytdlp || DEFAULT_CONFIG.ytdlp!;
  await initYtdlp({
    enabled: ytdlpConfig.enabled,
    command: ytdlpConfig.command,
    ffmpegCommand: ytdlpConfig.ffmpegCommand,
    musicDir: MUSIC_DIR,
    fastQueueConcurrent: ytdlpConfig.fastQueueConcurrent,
    slowQueueInterval: ytdlpConfig.slowQueueInterval,
    allowPlaylists: ytdlpConfig.allowPlaylists
  });

  // Set up ytdlp progress callback
  setProgressCallback((item) => {
    sendToUser(item.userId, {
      type: `fetch_${item.status === "downloading" ? "progress" : item.status}`,
      id: item.id,
      title: item.title,
      status: item.status,
      progress: item.progress,
      queueType: item.queueType,
      playlistId: item.playlistId,
      playlistName: item.playlistName,
      error: item.error
    });
  });

  // Initialize library
  const library = new Library(MUSIC_DIR, "./library_cache.db", config.replayGain ?? DEFAULT_CONFIG.replayGain);
  setLibrary(library);

  // Track pending playlist additions (title -> {playlistId, playlistName, userId})
  const pendingPlaylistTracks = new Map<string, { playlistId: string; playlistName: string; userId: number }>();

  // When a download completes, register it for playlist addition
  setTrackReadyCallback((item: QueueItem) => {
    if (!item.playlistId) return;
    
    // Store the pending addition - will be processed when library detects the file
    // yt-dlp saves files as "title.mp3", so use the title as key
    pendingPlaylistTracks.set(item.title.toLowerCase(), {
      playlistId: item.playlistId,
      playlistName: item.playlistName!,
      userId: item.userId
    });
    console.log(`[ytdlp] Registered pending playlist addition: "${item.title}" → ${item.playlistName}`);
  });

  // Scan library first
  await library.scan();
  library.startWatching();

  // Broadcast when scan completes
  library.onScanComplete(() => {
    broadcastToAll({ type: "scan_progress", scanning: false });
  });

  // Normalize string for matching (handle Windows filename character substitutions)
  function normalizeForMatch(s: string): string {
    return s.toLowerCase()
      .replace(/｜/g, "|")  // fullwidth vertical line → pipe
      .replace(/＂/g, '"')  // fullwidth quotation
      .replace(/＊/g, "*")  // fullwidth asterisk
      .replace(/？/g, "?")  // fullwidth question mark
      .replace(/＜/g, "<")  // fullwidth less-than
      .replace(/＞/g, ">")  // fullwidth greater-than
      .replace(/：/g, ":")  // fullwidth colon
      .replace(/／/g, "/")  // fullwidth slash
      .replace(/＼/g, "\\") // fullwidth backslash
      .trim();
  }

  // Helper to check if track matches a pending playlist addition
  function checkPendingPlaylistAddition(track: { id: string; title?: string | null; filename?: string | null }) {
    if (pendingPlaylistTracks.size === 0) return;
    
    const trackTitle = normalizeForMatch(track.title || "");
    const trackFilename = normalizeForMatch((track.filename || "").replace(/\.[^.]+$/, "")); // Remove extension
    
    // Skip if both title and filename are too short
    if ((!trackTitle || trackTitle.length < 5) && (!trackFilename || trackFilename.length < 5)) return;
    
    console.log(`[ytdlp] Checking track against ${pendingPlaylistTracks.size} pending: title="${trackTitle}" filename="${trackFilename}"`);
    
    for (const [pendingTitle, pending] of pendingPlaylistTracks) {
      const normalizedPending = normalizeForMatch(pendingTitle);
      
      // Skip if pending title is too short
      if (!normalizedPending || normalizedPending.length < 5) continue;
      
      // Match by title or filename (yt-dlp uses title as filename)
      // Require exact match or very high overlap
      const matches = 
        (trackTitle && trackTitle === normalizedPending) ||
        (trackFilename && trackFilename === normalizedPending) ||
        (trackTitle && normalizedPending && trackTitle.includes(normalizedPending) && normalizedPending.length >= trackTitle.length * 0.8) ||
        (trackTitle && normalizedPending && normalizedPending.includes(trackTitle) && trackTitle.length >= normalizedPending.length * 0.8) ||
        (trackFilename && normalizedPending && trackFilename.includes(normalizedPending) && normalizedPending.length >= trackFilename.length * 0.8) ||
        (trackFilename && normalizedPending && normalizedPending.includes(trackFilename) && trackFilename.length >= normalizedPending.length * 0.8);
      
      console.log(`[ytdlp]   vs pending="${normalizedPending}" → ${matches ? "MATCH" : "no match"}`);
      
      if (matches) {
        console.log(`[ytdlp] Adding track ${track.id} to playlist ${pending.playlistId}`);
        try {
          addTracksToPlaylist(pending.playlistId, [track.id]);
          sendToUser(pending.userId, {
            type: "toast",
            message: `Added to playlist: ${pending.playlistName}`,
            toastType: "info"
          });
          pendingPlaylistTracks.delete(pendingTitle);
        } catch (e) {
          console.error(`[ytdlp] Failed to add track to playlist:`, e);
        }
        return;
      }
    }
  }

  // Broadcast when tracks are added/updated
  library.on("added", (track) => {
    broadcastToAll({ type: "toast", message: `Added: ${track.title || track.filename}`, toastType: "info" });
    library.logActivity("scan_added", { id: track.id, filename: track.filename, title: track.title });
    
    // Check if this track was pending playlist addition (defer to ensure DB is updated)
    setTimeout(() => checkPendingPlaylistAddition(track), 100);
  });
  library.on("changed", (track) => {
    console.log(`Track metadata updated: ${track.title || track.filename}`);
    library.logActivity("scan_updated", { id: track.id, filename: track.filename, title: track.title });
  });

  // Prescan slow queue to find tracks already in library
  function prescanSlowQueue() {
    const queuedItems = getQueuedSlowItems();
    if (queuedItems.length === 0) return;
    
    const tracks = library.getAllTracks();
    if (tracks.length === 0) return;
    
    for (const item of queuedItems) {
      const itemTitle = normalizeForMatch(item.title);
      
      // Skip if title is too short (avoid false matches)
      if (!itemTitle || itemTitle.length < 5) continue;
      
      // Check if any library track matches
      for (const track of tracks) {
        const trackTitle = normalizeForMatch(track.title || "");
        const trackFilename = normalizeForMatch((track.filename || "").replace(/\.[^.]+$/, ""));
        
        // Skip if both track title and filename are too short
        if ((!trackTitle || trackTitle.length < 5) && (!trackFilename || trackFilename.length < 5)) continue;
        
        // Require exact match or very high overlap (not just substring)
        const matches = 
          (trackTitle && trackTitle === itemTitle) ||
          (trackFilename && trackFilename === itemTitle) ||
          // Only allow includes if the shorter string is at least 80% of the longer
          (trackTitle && itemTitle && trackTitle.includes(itemTitle) && itemTitle.length >= trackTitle.length * 0.8) ||
          (trackTitle && itemTitle && itemTitle.includes(trackTitle) && trackTitle.length >= itemTitle.length * 0.8) ||
          (trackFilename && itemTitle && trackFilename.includes(itemTitle) && itemTitle.length >= trackFilename.length * 0.8) ||
          (trackFilename && itemTitle && itemTitle.includes(trackFilename) && trackFilename.length >= itemTitle.length * 0.8);
        
        if (matches) {
          console.log(`[ytdlp] Prescan: "${item.title}" already exists as "${track.title || track.filename}"`);
          
          // Skip download and add to playlist
          const skipped = skipSlowQueueItem(item.id, track.id);
          if (skipped && skipped.playlistId) {
            try {
              addTracksToPlaylist(skipped.playlistId, [track.id]);
              sendToUser(skipped.userId, {
                type: "toast",
                message: `Already in library, added to: ${skipped.playlistName}`,
                toastType: "info"
              });
            } catch (e) {
              console.error(`[ytdlp] Failed to add existing track to playlist:`, e);
            }
          }
          break;
        }
      }
    }
  }
  
  // Run prescan periodically (every 30 seconds)
  setInterval(prescanSlowQueue, 30000);
  // Also run once after initial scan completes
  library.onScanComplete(() => {
    setTimeout(prescanSlowQueue, 1000);
  });

  // Load channels from database
  const savedChannels = loadAllChannels();
  let hasDefault = false;

  for (const row of savedChannels) {
    const trackIds = loadChannelQueue(row.id);
    const tracks = buildTracksFromIds(trackIds, library);

    const isDefault = row.is_default === 1;
    if (isDefault) {
      hasDefault = true;
    }

    const channelTracks = (isDefault && tracks.length === 0)
      ? getAllLibraryTracks(library)
      : tracks;

    const channel = new Channel({
      id: row.id,
      name: row.name,
      description: row.description,
      tracks: channelTracks,
      createdBy: row.created_by,
      isDefault,
      currentIndex: row.current_index,
      startedAt: row.started_at,
      paused: row.paused === 1,
      pausedAt: row.paused_at,
      playbackMode: (row.playback_mode as "repeat-all" | "repeat-one" | "shuffle") || "repeat-all",
    });

    channel.setPersistenceCallback(persistChannel);
    state.channels.set(row.id, channel);
    console.log(`Loaded channel "${row.name}" (id=${row.id}) with ${channelTracks.length} tracks`);
  }

  // Create default channel if it doesn't exist
  if (!hasDefault) {
    const tracks = getAllLibraryTracks(library);
    const defaultChannel = new Channel({
      id: "main",
      name: "Main Channel",
      description: "All tracks from the library",
      tracks,
      isDefault: true,
      createdBy: null,
    });

    defaultChannel.setPersistenceCallback(persistChannel);
    state.channels.set("main", defaultChannel);

    saveChannel({
      id: defaultChannel.id,
      name: defaultChannel.name,
      description: defaultChannel.description,
      createdBy: defaultChannel.createdBy,
      isDefault: true,
      currentIndex: defaultChannel.currentIndex,
      startedAt: defaultChannel.startedAt,
      paused: defaultChannel.paused,
      pausedAt: defaultChannel.pausedAt,
      playbackMode: defaultChannel.playbackMode,
    });
    saveChannelQueue(defaultChannel.id, tracks.map(t => t.id));

    console.log(`Default channel created: ${tracks.length} tracks`);
  }

  // Listen for library changes and notify clients
  library.on("added", (track) => {
    console.log(`New track detected: ${track.title}`);
    const allTracks = library.getAllTracks().map(t => ({
      id: t.id,
      title: t.title,
      duration: t.duration,
      replayGainDb: t.replayGainDb,
      replayPeak: t.replayPeak,
    }));
    broadcastToAll({
      type: "track_added",
      track: {
        id: track.id,
        title: track.title,
        duration: track.duration,
        replayGainDb: track.replayGainDb,
        replayPeak: track.replayPeak,
      },
      library: allTracks
    });
  });

  library.on("removed", (track) => {
    console.log(`Track removed: ${track.title}`);
    removeTrackFromQueues(track.id);
    const allTracks = library.getAllTracks().map(t => ({
      id: t.id,
      title: t.title,
      duration: t.duration,
      replayGainDb: t.replayGainDb,
      replayPeak: t.replayPeak,
    }));
    broadcastToAll({
      type: "track_removed",
      track: { id: track.id, title: track.title },
      library: allTracks
    });
  });

  // Tick interval: advance tracks when needed, broadcast every 30s
  let tickCount = 0;
  setInterval(() => {
    tickCount++;
    for (const channel of state.channels.values()) {
      const changed = channel.tick();
      if (changed) {
        console.log(`[Tick] Channel "${channel.name}" advanced to track ${channel.currentIndex}`);
      }
      if (!changed && tickCount % 30 === 0) {
        console.log(`[Tick] Broadcasting state for channel "${channel.name}" (${channel.clients.size} clients)`);
        channel.broadcast();
      }
    }

    // Broadcast scan progress every 2 seconds while scanning
    const scanProgress = library.scanProgress;
    if (scanProgress.scanning && tickCount % 2 === 0) {
      broadcastToAll({
        type: "scan_progress",
        scanning: true,
        processed: scanProgress.processed,
        total: scanProgress.total
      });
    } else if (!scanProgress.scanning && tickCount % 30 === 0) {
      broadcastToAll({ type: "scan_progress", scanning: false });
    }
  }, 1000);
}
