import { spotifyService, SpotifyTrack, SpotifyPlaylist } from "./spotify";
import { musicBrainzService } from "./musicbrainz";
import { lidarrService } from "./lidarr";
import { soulseekService } from "./soulseek";
import { deezerService } from "./deezer";
import {
    createPlaylistLogger,
    logPlaylistEvent,
} from "../utils/playlistLogger";
import { notificationService } from "./notificationService";
import { getSystemSettings } from "../utils/systemSettings";
import { prisma } from "../utils/db";
import { redisClient } from "../utils/redis";
import PQueue from "p-queue";
// Note: We DON'T use simpleDownloadManager here because it has same-artist fallback logic
// For Spotify Import, we need EXACT album matching - no substitutions

// Store loggers for each job
const jobLoggers = new Map<string, ReturnType<typeof createPlaylistLogger>>();

/**
 * Spotify Import Service
 *
 * Handles matching Spotify tracks to local library and managing imports
 */

export interface MatchedTrack {
    spotifyTrack: SpotifyTrack;
    localTrack: {
        id: string;
        title: string;
        albumId: string;
        albumTitle: string;
        artistName: string;
    } | null;
    matchType: "exact" | "fuzzy" | "none";
    matchConfidence: number; // 0-100
}

export interface AlbumToDownload {
    spotifyAlbumId: string;
    albumName: string;
    artistName: string;
    artistMbid: string | null;
    albumMbid: string | null;
    coverUrl: string | null;
    trackCount: number;
    tracksNeeded: SpotifyTrack[];
}

export interface ImportPreview {
    playlist: {
        id: string;
        name: string;
        description: string | null;
        owner: string;
        imageUrl: string | null;
        trackCount: number;
    };
    matchedTracks: MatchedTrack[];
    albumsToDownload: AlbumToDownload[];
    summary: {
        total: number;
        inLibrary: number;
        downloadable: number;
        notFound: number;
    };
}

export interface ImportJob {
    id: string;
    userId: string;
    spotifyPlaylistId: string;
    playlistName: string;
    status:
        | "pending"
        | "downloading"
        | "scanning"
        | "creating_playlist"
        | "matching_tracks"
        | "completed"
        | "failed"
        | "cancelled";
    progress: number;
    albumsTotal: number;
    albumsCompleted: number;
    tracksMatched: number;
    tracksTotal: number;
    tracksDownloadable: number; // Tracks from albums being downloaded
    createdPlaylistId: string | null;
    error: string | null;
    createdAt: Date;
    updatedAt: Date;
    // Store the original track list so we can match after downloads
    pendingTracks: Array<{
        artist: string;
        title: string;
        album: string;
        preMatchedTrackId: string | null; // Track ID if already matched in preview
    }>;
}

// Redis key pattern for import jobs
const IMPORT_JOB_KEY = (id: string) => `import:job:${id}`;
const IMPORT_JOB_TTL = 24 * 60 * 60; // 24 hours

/**
 * Save import job to both database and Redis cache for cross-process sharing
 */
async function saveImportJob(job: ImportJob): Promise<void> {
    // Save to database for durability
    await prisma.spotifyImportJob.upsert({
        where: { id: job.id },
        create: {
            id: job.id,
            userId: job.userId,
            spotifyPlaylistId: job.spotifyPlaylistId,
            playlistName: job.playlistName,
            status: job.status,
            progress: job.progress,
            albumsTotal: job.albumsTotal,
            albumsCompleted: job.albumsCompleted,
            tracksMatched: job.tracksMatched,
            tracksTotal: job.tracksTotal,
            tracksDownloadable: job.tracksDownloadable,
            createdPlaylistId: job.createdPlaylistId,
            error: job.error,
            pendingTracks: job.pendingTracks as any,
        },
        update: {
            status: job.status,
            progress: job.progress,
            albumsCompleted: job.albumsCompleted,
            tracksMatched: job.tracksMatched,
            createdPlaylistId: job.createdPlaylistId,
            error: job.error,
            updatedAt: new Date(),
        },
    });

    // Save to Redis for cross-process sharing
    try {
        await redisClient.setEx(
            IMPORT_JOB_KEY(job.id),
            IMPORT_JOB_TTL,
            JSON.stringify(job)
        );
    } catch (error) {
        console.warn(
            `⚠️  Failed to cache import job ${job.id} in Redis:`,
            error
        );
        // Continue - Redis is optional, DB is source of truth
    }
}

/**
 * Get import job from Redis cache or database
 * Redis provides cross-process sharing between API and worker processes
 */
async function getImportJob(importJobId: string): Promise<ImportJob | null> {
    // Try Redis cache first (shared across all processes)
    try {
        const cached = await redisClient.get(IMPORT_JOB_KEY(importJobId));
        if (cached) {
            return JSON.parse(cached);
        }
    } catch (error) {
        console.warn(
            `⚠️  Failed to read import job ${importJobId} from Redis:`,
            error
        );
        // Fall through to DB
    }

    // Load from database as fallback
    const dbJob = await prisma.spotifyImportJob.findUnique({
        where: { id: importJobId },
    });

    if (!dbJob) return null;

    // Convert database job to ImportJob format
    const job: ImportJob = {
        id: dbJob.id,
        userId: dbJob.userId,
        spotifyPlaylistId: dbJob.spotifyPlaylistId,
        playlistName: dbJob.playlistName,
        status: dbJob.status as ImportJob["status"],
        progress: dbJob.progress,
        albumsTotal: dbJob.albumsTotal,
        albumsCompleted: dbJob.albumsCompleted,
        tracksMatched: dbJob.tracksMatched,
        tracksTotal: dbJob.tracksTotal,
        tracksDownloadable: dbJob.tracksDownloadable,
        createdPlaylistId: dbJob.createdPlaylistId,
        error: dbJob.error,
        createdAt: dbJob.createdAt,
        updatedAt: dbJob.updatedAt,
        pendingTracks: (dbJob.pendingTracks as any) || [],
    };

    // Populate Redis for next time
    try {
        await redisClient.setEx(
            IMPORT_JOB_KEY(importJobId),
            IMPORT_JOB_TTL,
            JSON.stringify(job)
        );
    } catch (error) {
        console.warn(
            `⚠️  Failed to cache import job ${importJobId} in Redis:`,
            error
        );
        // Continue - Redis is optional
    }

    return job;
}

/**
 * Normalize a string for fuzzy matching
 * Handles: special characters, punctuation, remaster suffixes, etc.
 */
function normalizeString(str: string): string {
    return (
        str
            .toLowerCase()
            // Normalize special characters (ö→o, é→e, etc.)
            .normalize("NFD")
            .replace(/[\u0300-\u036f]/g, "")
            // Remove punctuation but keep spaces
            .replace(/[^\w\s]/g, "")
            .replace(/\s+/g, " ")
            .trim()
    );
}

/**
 * Normalize apostrophes and quotes to ASCII versions
 * Handles: ' ' ` ′ ʼ → '
 */
function normalizeApostrophes(str: string): string {
    return str
        .replace(/[''`′ʼ]/g, "'") // Various apostrophe forms → ASCII apostrophe
        .replace(/[""]/g, '"'); // Smart quotes → ASCII quotes
}

/**
 * Strip remaster/version suffixes but KEEP punctuation
 * "Ain't Gonna Rain Anymore - 2011 Remaster" → "Ain't Gonna Rain Anymore"
 * Used for database searches where we need to match punctuation
 */
function stripTrackSuffix(str: string): string {
    return (
        normalizeApostrophes(str)
            // Remove " - YEAR Remaster", " - Remastered YEAR", " - Radio Edit", etc.
            // Note: remaster(ed)? matches "remaster" or "remastered"
            .replace(
                /\s*-\s*(\d{4}\s+)?(remaster(ed)?|deluxe|bonus|single|radio edit|remix|acoustic|live|mono|stereo|version|edition|mix)(\s+\d{4})?(\s+(version|edition|mix))?.*$/i,
                ""
            )
            // Remove " - YEAR" at end
            .replace(/\s*-\s*\d{4}\s*$/, "")
            // Remove "(Live at...)", "(Live from...)", "(Recorded at...)" parenthetical content
            .replace(
                /\s*\([^)]*(?:live at|live from|recorded at|performed at)[^)]*\)\s*/gi,
                " "
            )
            // Remove parenthetical content like "(Remastered)" or "(2011 Remastered Version)"
            .replace(/\s*\([^)]*remaster[^)]*\)\s*/gi, " ")
            .replace(/\s*\([^)]*version[^)]*\)\s*/gi, " ")
            .replace(/\s*\([^)]*edition[^)]*\)\s*/gi, " ")
            // Remove general "(Live)" or "(Live 2021)" etc
            .replace(/\s*\(\s*live\s*(\d{4})?\s*\)\s*/gi, " ")
            // Remove bracketed content like "[Deluxe Edition]"
            .replace(/\s*\[[^\]]*\]\s*/g, " ")
            .replace(/\s+/g, " ")
            .trim()
    );
}

/**
 * Normalize track title - removes remaster/version suffixes AND punctuation
 * "Ain't Gonna Rain Anymore - 2011 Remaster" → "aint gonna rain anymore"
 * Used for similarity comparisons
 */
function normalizeTrackTitle(str: string): string {
    return normalizeString(stripTrackSuffix(str));
}

/**
 * Calculate similarity between two strings (0-100)
 */
function stringSimilarity(a: string, b: string): number {
    const s1 = normalizeString(a);
    const s2 = normalizeString(b);

    if (s1 === s2) return 100;

    // Check if one contains the other
    if (s1.includes(s2) || s2.includes(s1)) {
        const longer = Math.max(s1.length, s2.length);
        const shorter = Math.min(s1.length, s2.length);
        return Math.round((shorter / longer) * 100);
    }

    // Simple word overlap similarity
    const words1 = new Set(s1.split(" "));
    const words2 = new Set(s2.split(" "));
    const intersection = [...words1].filter((w) => words2.has(w)).length;
    const union = new Set([...words1, ...words2]).size;

    return Math.round((intersection / union) * 100);
}

class SpotifyImportService {
    /**
     * Match a Spotify track to the local library
     */
    private async matchTrack(
        spotifyTrack: SpotifyTrack
    ): Promise<MatchedTrack> {
        const normalizedTitle = normalizeString(spotifyTrack.title);
        const normalizedArtist = normalizeString(spotifyTrack.artist);
        const normalizedAlbum = normalizeString(spotifyTrack.album);

        // Strategy 1: Exact match by artist + album + title
        const exactMatch = await prisma.track.findFirst({
            where: {
                album: {
                    artist: {
                        normalizedName: normalizedArtist,
                    },
                    title: {
                        mode: "insensitive",
                        equals: spotifyTrack.album,
                    },
                },
                title: {
                    mode: "insensitive",
                    equals: spotifyTrack.title,
                },
            },
            include: {
                album: {
                    include: {
                        artist: true,
                    },
                },
            },
        });

        if (exactMatch) {
            return {
                spotifyTrack,
                localTrack: {
                    id: exactMatch.id,
                    title: exactMatch.title,
                    albumId: exactMatch.albumId,
                    albumTitle: exactMatch.album.title,
                    artistName: exactMatch.album.artist.name,
                },
                matchType: "exact",
                matchConfidence: 100,
            };
        }

        // Strategy 2: Fuzzy match by artist + title (any album)
        const fuzzyMatches = await prisma.track.findMany({
            where: {
                album: {
                    artist: {
                        normalizedName: {
                            contains: normalizedArtist.split(" ")[0], // First word of artist
                        },
                    },
                },
            },
            include: {
                album: {
                    include: {
                        artist: true,
                    },
                },
            },
            take: 50, // Limit for performance
        });

        let bestMatch: typeof exactMatch | null = null;
        let bestScore = 0;

        for (const track of fuzzyMatches) {
            // Use cleaned titles for comparison (strips "- 2011 Remaster", etc.)
            const titleSim = stringSimilarity(
                normalizeTrackTitle(spotifyTrack.title),
                normalizeTrackTitle(track.title)
            );
            const artistSim = stringSimilarity(
                spotifyTrack.artist,
                track.album.artist.name
            );

            // Weight: title 60%, artist 40%
            const score = titleSim * 0.6 + artistSim * 0.4;

            if (score > bestScore && score >= 70) {
                bestScore = score;
                bestMatch = track;
            }
        }

        if (bestMatch) {
            return {
                spotifyTrack,
                localTrack: {
                    id: bestMatch.id,
                    title: bestMatch.title,
                    albumId: bestMatch.albumId,
                    albumTitle: bestMatch.album.title,
                    artistName: bestMatch.album.artist.name,
                },
                matchType: "fuzzy",
                matchConfidence: Math.round(bestScore),
            };
        }

        return {
            spotifyTrack,
            localTrack: null,
            matchType: "none",
            matchConfidence: 0,
        };
    }

    /**
     * Look up album info from MusicBrainz for downloading
     */
    private async findAlbumMbid(
        artistName: string,
        albumName: string
    ): Promise<{ artistMbid: string | null; albumMbid: string | null }> {
        try {
            // Search for artist first
            const artists = await musicBrainzService.searchArtist(
                artistName,
                5
            );
            if (!artists || artists.length === 0) {
                return { artistMbid: null, albumMbid: null };
            }

            // Find best matching artist
            let bestArtist = artists[0];
            for (const artist of artists) {
                if (
                    normalizeString(artist.name) === normalizeString(artistName)
                ) {
                    bestArtist = artist;
                    break;
                }
            }

            const artistMbid = bestArtist.id;

            // Search for album by this artist
            const releaseGroups = await musicBrainzService.getReleaseGroups(
                artistMbid
            );

            for (const rg of releaseGroups || []) {
                if (stringSimilarity(rg.title, albumName) >= 80) {
                    return { artistMbid, albumMbid: rg.id };
                }
            }

            return { artistMbid, albumMbid: null };
        } catch (error) {
            console.error("MusicBrainz lookup error:", error);
            return { artistMbid: null, albumMbid: null };
        }
    }

    /**
     * Shared preview generator for any source tracklist
     */
    private async buildPreviewFromTracklist(
        tracks: SpotifyTrack[],
        playlistMeta: {
            id: string;
            name: string;
            description: string | null;
            owner: string;
            imageUrl: string | null;
            trackCount: number;
        },
        source: "Spotify" | "Deezer"
    ): Promise<ImportPreview> {
        const logPrefix =
            source === "Spotify" ? "[Spotify Import]" : "[Deezer Import]";

        const matchedTracks: MatchedTrack[] = [];
        const unmatchedByAlbum = new Map<string, SpotifyTrack[]>();

        for (const track of tracks) {
            const matched = await this.matchTrack(track);
            matchedTracks.push(matched);

            if (!matched.localTrack) {
                const key = `${track.artist}|||${track.album}`;
                const existing = unmatchedByAlbum.get(key) || [];
                existing.push(track);
                unmatchedByAlbum.set(key, existing);
            }
        }

        const albumsToDownload: AlbumToDownload[] = [];

        for (const [key, albumTracks] of unmatchedByAlbum.entries()) {
            const [artistName, albumName] = key.split("|||");

            let resolvedAlbumName = albumName;
            let artistMbid: string | null = null;
            let albumMbid: string | null = null;

            console.log(
                `\n${logPrefix} ========================================`
            );
            console.log(
                `${logPrefix} Looking up: "${artistName}" - "${albumName}"`
            );

            if (albumName && albumName !== "Unknown Album") {
                // Normalize album name to remove live/remaster suffixes
                const normalizedAlbumName = stripTrackSuffix(albumName);
                const wasNormalized = normalizedAlbumName !== albumName;

                console.log(
                    `${logPrefix} Searching for album "${albumName}" by ${artistName}...`
                );
                if (wasNormalized) {
                    console.log(
                        `${logPrefix}   → Normalized to: "${normalizedAlbumName}"`
                    );
                }

                const mbResult = await this.findAlbumMbid(
                    artistName,
                    normalizedAlbumName
                );
                artistMbid = mbResult.artistMbid;
                albumMbid = mbResult.albumMbid;

                if (albumMbid) {
                    console.log(
                        `${logPrefix} ✓ Found album directly: "${albumName}" (MBID: ${albumMbid})`
                    );
                }
            }

            if (!albumMbid) {
                console.log(
                    `${logPrefix} Album not found, trying track-based search...`
                );
                for (const track of albumTracks) {
                    // Normalize track title to remove live/remaster suffixes
                    const normalizedTrackTitle = stripTrackSuffix(track.title);
                    const wasNormalized = normalizedTrackTitle !== track.title;

                    console.log(
                        `${logPrefix}   Searching for track "${track.title}"...`
                    );
                    if (wasNormalized) {
                        console.log(
                            `${logPrefix}     → Normalized to: "${normalizedTrackTitle}"`
                        );
                    }

                    const recordingInfo =
                        await musicBrainzService.searchRecording(
                            normalizedTrackTitle,
                            artistName
                        );

                    if (recordingInfo) {
                        resolvedAlbumName = recordingInfo.albumName;
                        artistMbid = recordingInfo.artistMbid;
                        albumMbid = recordingInfo.albumMbid;

                        console.log(
                            `${logPrefix} ✓ Found via track: "${resolvedAlbumName}" (MBID: ${albumMbid})`
                        );
                        break;
                    }
                }
            }

            if (!albumMbid) {
                console.log(
                    `${logPrefix} ✗ Could not find album MBID for ${artistName} - "${resolvedAlbumName}"`
                );
                if (albumName === "Unknown Album") {
                    console.log(
                        `${logPrefix} ℹ But can still download via Soulseek (track-based search)`
                    );
                }
            }

            const albumToDownload: AlbumToDownload = {
                spotifyAlbumId: albumTracks[0].albumId,
                albumName: resolvedAlbumName,
                artistName,
                artistMbid,
                albumMbid,
                coverUrl: albumTracks[0].coverUrl,
                trackCount: albumTracks.length,
                tracksNeeded: albumTracks,
            };

            console.log(`${logPrefix} Download strategy:`);
            if (albumMbid) {
                console.log(`   Will request album from Lidarr/Soulseek:`);
                console.log(
                    `   Artist: "${artistName}" (MBID: ${artistMbid || "NONE"})`
                );
                console.log(
                    `   Album: "${resolvedAlbumName}" (MBID: ${albumMbid})`
                );
            } else {
                // No MBID - will try Soulseek track-based search
                console.log(
                    `   Will request individual tracks via Soulseek (no MBID):`
                );
                console.log(`   Artist: "${artistName}"`);
                console.log(
                    `   Tracks: ${albumTracks
                        .map((t) => `"${t.title}"`)
                        .join(", ")}`
                );
            }
            console.log(
                `${logPrefix} ========================================\n`
            );

            albumsToDownload.push(albumToDownload);
        }

        const inLibrary = matchedTracks.filter(
            (m) => m.localTrack !== null
        ).length;

        // All albums are now downloadable via Soulseek (either album-based with MBID or track-based without)
        const downloadableAlbums = albumsToDownload;

        // No albums are truly "not found" since Soulseek can search for any track
        const notFoundAlbums: AlbumToDownload[] = [];

        const downloadable = downloadableAlbums.reduce(
            (sum, a) => sum + a.tracksNeeded.length,
            0
        );
        const notFound = notFoundAlbums.reduce(
            (sum, a) => sum + a.tracksNeeded.length,
            0
        );

        return {
            playlist: playlistMeta,
            matchedTracks,
            albumsToDownload,
            summary: {
                total: playlistMeta.trackCount,
                inLibrary,
                downloadable,
                notFound,
            },
        };
    }

    /**
     * Generate a preview of what will be imported
     */
    async generatePreview(spotifyUrl: string): Promise<ImportPreview> {
        // Clear any stale null cache entries before processing
        // This ensures we retry previously failed lookups
        await musicBrainzService.clearStaleRecordingCaches();

        const playlist = await spotifyService.getPlaylist(spotifyUrl);
        if (!playlist) {
            throw new Error(
                "Could not fetch playlist from Spotify. Make sure it's a valid public playlist URL."
            );
        }

        return this.buildPreviewFromTracklist(
            playlist.tracks,
            {
                id: playlist.id,
                name: playlist.name,
                description: playlist.description,
                owner: playlist.owner,
                imageUrl: playlist.imageUrl,
                trackCount: playlist.trackCount,
            },
            "Spotify"
        );
    }

    /**
     * Generate a preview from a Deezer playlist
     * Converts Deezer tracks to Spotify format and processes them
     */
    async generatePreviewFromDeezer(
        deezerPlaylist: any
    ): Promise<ImportPreview> {
        // Clear any stale null cache entries before processing
        await musicBrainzService.clearStaleRecordingCaches();

        console.log(
            "[Deezer Debug] Sample track from Deezer:",
            JSON.stringify(deezerPlaylist.tracks[0], null, 2)
        );

        const spotifyTracks: SpotifyTrack[] = deezerPlaylist.tracks.map(
            (track: any, index: number) => ({
                spotifyId: track.deezerId,
                title: track.title,
                artist: track.artist,
                artistId: track.artistId || "",
                album: track.album || "Unknown Album",
                albumId: track.albumId || "",
                isrc: null,
                durationMs: track.durationMs,
                trackNumber: track.trackNumber || index + 1,
                previewUrl: track.previewUrl || null,
                coverUrl: track.coverUrl || deezerPlaylist.imageUrl || null,
            })
        );

        console.log(
            "[Deezer Debug] Sample converted track:",
            JSON.stringify(spotifyTracks[0], null, 2)
        );

        return this.buildPreviewFromTracklist(
            spotifyTracks,
            {
                id: deezerPlaylist.id,
                name: deezerPlaylist.title,
                description: deezerPlaylist.description || null,
                owner: deezerPlaylist.creator || "Deezer",
                imageUrl: deezerPlaylist.imageUrl || null,
                trackCount: deezerPlaylist.trackCount || spotifyTracks.length,
            },
            "Deezer"
        );
    }

    /**
     * Start an import job
     */
    async startImport(
        userId: string,
        spotifyPlaylistId: string,
        playlistName: string,
        albumMbidsToDownload: string[],
        preview: ImportPreview
    ): Promise<ImportJob> {
        const jobId = `import_${Date.now()}_${Math.random()
            .toString(36)
            .substring(7)}`;

        // Create dedicated logger for this job
        const logger = createPlaylistLogger(jobId);
        jobLoggers.set(jobId, logger);

        logger.logJobStart(playlistName, preview.summary.total, userId);
        logger.info(`Playlist ID: ${spotifyPlaylistId}`);
        logger.info(`Albums to download: ${albumMbidsToDownload.length}`);
        logger.info(`Tracks already in library: ${preview.summary.inLibrary}`);

        // Calculate tracks that will come from downloads
        const tracksFromDownloads = preview.albumsToDownload
            .filter((a) => albumMbidsToDownload.includes(a.albumMbid!))
            .reduce((sum, a) => sum + a.tracksNeeded.length, 0);

        // Extract the track info we need to match after downloads
        // Include ALL tracks, both matched and unmatched
        // IMPORTANT: Store pre-matched track IDs so we don't have to re-search them!
        // NOTE: `PlaylistPendingTrack.spotifyAlbum` should reflect Spotify's album name.
        // Only fall back to a resolved album name when Spotify returns "Unknown Album".
        const pendingTracks = preview.matchedTracks.map((m) => {
            const spotifyAlbum = m.spotifyTrack.album;
            const spotifyAlbumId = m.spotifyTrack.albumId;

            const albumToDownload = spotifyAlbumId
                ? preview.albumsToDownload.find(
                      (a) => a.spotifyAlbumId === spotifyAlbumId
                  )
                : undefined;

            const albumForDisplay =
                spotifyAlbum && spotifyAlbum !== "Unknown Album"
                    ? spotifyAlbum
                    : albumToDownload?.albumName || spotifyAlbum;

            return {
                artist: m.spotifyTrack.artist,
                title: m.spotifyTrack.title,
                album: albumForDisplay,
                preMatchedTrackId: m.localTrack?.id || null,
            };
        });

        const job: ImportJob = {
            id: jobId,
            userId,
            spotifyPlaylistId,
            playlistName,
            status: "pending",
            progress: 0,
            albumsTotal: albumMbidsToDownload.length,
            albumsCompleted: 0,
            tracksMatched: preview.summary.inLibrary,
            tracksTotal: preview.summary.total,
            tracksDownloadable: tracksFromDownloads,
            createdPlaylistId: null,
            error: null,
            createdAt: new Date(),
            updatedAt: new Date(),
            pendingTracks,
        };

        // Save to database and memory cache
        await saveImportJob(job);

        // Start processing in background
        this.processImport(job, albumMbidsToDownload, preview).catch(
            async (error) => {
                job.status = "failed";
                job.error = error.message;
                job.updatedAt = new Date();
                await saveImportJob(job);
                logger.logJobFailed(error.message);
            }
        );

        return job;
    }

    /**
     * Process the import (download albums, create playlist)
     * Uses simpleDownloadManager for proper webhook tracking and Lidarr release iteration
     */
    private async processImport(
        job: ImportJob,
        albumMbidsToDownload: string[],
        preview: ImportPreview
    ): Promise<void> {
        const logger = jobLoggers.get(job.id);

        // Import simpleDownloadManager here to avoid circular deps
        const { simpleDownloadManager } = await import(
            "./simpleDownloadManager"
        );

        try {
            // Phase 1: Download albums based on user's download preferences
            if (albumMbidsToDownload.length > 0) {
                job.status = "downloading";
                job.updatedAt = new Date();
                await saveImportJob(job);

                logger?.logAlbumDownloadStart(albumMbidsToDownload.length);

                // Get download preferences from system settings
                const settings = await getSystemSettings();
                const downloadSource = settings?.downloadSource || "soulseek";
                const soulseekFallback = settings?.soulseekFallback || "none";

                console.log(
                    `[Spotify Import] Download source: ${downloadSource}, Soulseek fallback: ${soulseekFallback}`
                );
                logger?.info(
                    `Download source: ${downloadSource}${
                        downloadSource === "soulseek"
                            ? `, fallback: ${soulseekFallback}`
                            : ""
                    }`
                );

                // Check service availability based on download source
                const lidarrEnabled = await lidarrService.isEnabled();
                const soulseekAvailable = await soulseekService.isAvailable();

                logger?.info(
                    `Service availability: lidarr=${
                        lidarrEnabled ? "yes" : "no"
                    }, soulseek=${soulseekAvailable ? "yes" : "no"}`
                );
                if (settings?.musicPath) {
                    logger?.info(`System musicPath: ${settings.musicPath}`);
                }

                if (downloadSource === "lidarr" && !lidarrEnabled) {
                    throw new Error(
                        "Lidarr is not configured. Cannot download missing albums."
                    );
                }
                if (downloadSource === "soulseek" && !soulseekAvailable) {
                    if (soulseekFallback === "lidarr" && lidarrEnabled) {
                        console.log(
                            `[Spotify Import] Soulseek not available, using Lidarr as fallback`
                        );
                        logger?.info(
                            `Soulseek not available, using Lidarr for downloads`
                        );
                    } else {
                        throw new Error(
                            "Soulseek is not available. Cannot download missing tracks."
                        );
                    }
                }

                // Process albums in parallel with concurrency limit
                const albumConcurrency = downloadSource === "soulseek" ? 4 : 2;
                const albumQueue = new PQueue({
                    concurrency: albumConcurrency,
                });

                console.log(
                    `[Spotify Import] Processing ${albumMbidsToDownload.length} albums with concurrency ${albumConcurrency}`
                );
                logger?.info(
                    `Processing ${albumMbidsToDownload.length} albums in parallel (concurrency: ${albumConcurrency})`
                );

                const albumPromises = albumMbidsToDownload.map(
                    (albumIdentifier) =>
                        albumQueue.add(async () => {
                            // albumIdentifier can be either albumMbid or spotifyAlbumId (for Unknown Album)
                            const album = preview.albumsToDownload.find(
                                (a) =>
                                    a.albumMbid === albumIdentifier ||
                                    a.spotifyAlbumId === albumIdentifier
                            );
                            if (!album) return;

                            try {
                                // Create a DownloadJob record for tracking
                                const downloadJobId = `spotify_${job.id}_${albumIdentifier}`;

                                // For Unknown Album tracks, we only support Soulseek (track-based download)
                                const isUnknownAlbum =
                                    album.albumName === "Unknown Album" ||
                                    !album.albumMbid;

                                logger?.info(
                                    `Album start: ${album.artistName} - ${
                                        album.albumName
                                    }${
                                        album.albumMbid
                                            ? ` [MBID: ${album.albumMbid}]`
                                            : " [Unknown Album]"
                                    } (tracksNeeded=${
                                        album.tracksNeeded.length
                                    }, downloadJobId=${downloadJobId})`
                                );

                                // Check if job already exists (in case of retry)
                                const existingJob =
                                    await prisma.downloadJob.findUnique({
                                        where: { id: downloadJobId },
                                    });

                                if (!existingJob) {
                                    await prisma.downloadJob.create({
                                        data: {
                                            id: downloadJobId,
                                            userId: job.userId,
                                            subject: `${album.artistName} - ${album.albumName}`,
                                            type: "album",
                                            targetMbid: album.albumMbid || null,
                                            status: "pending",
                                            artistMbid: album.artistMbid,
                                            metadata: {
                                                spotifyImportJobId: job.id,
                                                albumName: album.albumName,
                                                artistName: album.artistName,
                                                albumTitle: album.albumName,
                                                downloadType: "spotify_import",
                                                downloadSource: downloadSource,
                                                noFallback: true,
                                                isUnknownAlbum,
                                                spotifyAlbumId:
                                                    album.spotifyAlbumId,
                                            },
                                        },
                                    });
                                }

                                console.log(
                                    `[Spotify Import] Requesting: ${album.artistName} - ${album.albumName}`
                                );
                                logger?.info(
                                    `Requesting: ${album.artistName} - ${
                                        album.albumName
                                    }${
                                        album.albumMbid
                                            ? ` [MBID: ${album.albumMbid}]`
                                            : " [Unknown Album]"
                                    }`
                                );

                                let downloadSuccess = false;

                                // For Unknown Album, force Soulseek (track-based download)
                                if (isUnknownAlbum) {
                                    console.log(
                                        `[Spotify Import] Unknown Album detected - using Soulseek track-based download`
                                    );

                                    if (!soulseekAvailable) {
                                        throw new Error(
                                            "Soulseek is required for Unknown Album tracks but is not available"
                                        );
                                    }

                                    const soulseekResult =
                                        await this.trySoulseekDownload(
                                            job,
                                            album,
                                            downloadJobId,
                                            logger
                                        );

                                    if (soulseekResult.success) {
                                        downloadSuccess = true;
                                        console.log(
                                            `[Spotify Import] ✓ Soulseek completed: ${album.tracksNeeded
                                                .map((t) => t.title)
                                                .join(", ")}`
                                        );
                                        logger?.info(
                                            `Unknown Album tracks success via Soulseek: ${
                                                album.artistName
                                            } - ${
                                                album.tracksNeeded.length
                                            } tracks (${
                                                soulseekResult.tracksFound ?? 0
                                            } downloaded)`
                                        );
                                    } else {
                                        console.log(
                                            `[Spotify Import] ✗ Failed to download Unknown Album tracks: ${soulseekResult.error}`
                                        );
                                    }
                                } else if (downloadSource === "soulseek") {
                                    // === SOULSEEK PRIMARY: Per-track downloads ===
                                    console.log(
                                        `[Spotify Import] Using Soulseek for per-track download`
                                    );

                                    const soulseekResult =
                                        await this.trySoulseekDownload(
                                            job,
                                            album,
                                            downloadJobId,
                                            logger
                                        );

                                    if (soulseekResult.success) {
                                        downloadSuccess = true;
                                        console.log(
                                            `[Spotify Import] ✓ Soulseek queued: ${album.albumName}`
                                        );
                                        logger?.info(
                                            `Album success via Soulseek: ${
                                                album.artistName
                                            } - ${
                                                album.albumName
                                            } (tracksDownloaded=${
                                                soulseekResult.tracksFound ?? 0
                                            }/${album.tracksNeeded.length})`
                                        );
                                    } else if (
                                        soulseekFallback === "lidarr" &&
                                        lidarrEnabled
                                    ) {
                                        // Soulseek failed, try Lidarr as fallback for full album
                                        console.log(
                                            `[Spotify Import] Soulseek failed, trying Lidarr for full album: ${album.albumName}`
                                        );
                                        logger?.info(
                                            `Soulseek failed, falling back to Lidarr for full album`
                                        );

                                        const lidarrResult =
                                            await simpleDownloadManager.startDownload(
                                                downloadJobId,
                                                album.artistName,
                                                album.albumName,
                                                album.albumMbid!,
                                                job.userId,
                                                false
                                            );

                                        if (lidarrResult.success) {
                                            downloadSuccess = true;
                                            console.log(
                                                `[Spotify Import] ✓ Lidarr fallback queued: ${album.albumName}`
                                            );
                                            logger?.logAlbumQueued(
                                                album.albumName,
                                                album.artistName,
                                                album.albumMbid!,
                                                0
                                            );
                                        } else {
                                            logger?.warn(
                                                `Lidarr fallback failed to queue: ${
                                                    album.artistName
                                                } - ${album.albumName} (${
                                                    lidarrResult.error ||
                                                    "unknown error"
                                                })`
                                            );
                                        }
                                    }

                                    if (!downloadSuccess) {
                                        const errorMsg =
                                            soulseekResult.error ||
                                            "No download sources available";
                                        console.log(
                                            `[Spotify Import] ✗ Failed: ${album.albumName} - ${errorMsg}`
                                        );
                                        logger?.logAlbumFailed(
                                            album.albumName,
                                            album.artistName,
                                            errorMsg
                                        );

                                        // Update download job to failed status
                                        await prisma.downloadJob
                                            .update({
                                                where: { id: downloadJobId },
                                                data: {
                                                    status: "failed",
                                                    error: errorMsg,
                                                    completedAt: new Date(),
                                                },
                                            })
                                            .catch(() => {});
                                    }
                                } else {
                                    // === LIDARR PRIMARY: Full album downloads ===
                                    const result =
                                        await simpleDownloadManager.startDownload(
                                            downloadJobId,
                                            album.artistName,
                                            album.albumName,
                                            album.albumMbid!,
                                            job.userId,
                                            false
                                        );

                                    if (result.success) {
                                        downloadSuccess = true;
                                        console.log(
                                            `[Spotify Import] ✓ Queued: ${album.albumName}`
                                        );
                                        logger?.logAlbumQueued(
                                            album.albumName,
                                            album.artistName,
                                            album.albumMbid!,
                                            0
                                        );
                                    } else if (soulseekAvailable) {
                                        logger?.warn(
                                            `Lidarr failed to queue/find album, trying Soulseek fallback: ${
                                                album.artistName
                                            } - ${album.albumName} (${
                                                result.error || "unknown error"
                                            })`
                                        );
                                        // Lidarr failed - try Soulseek as fallback
                                        const soulseekFallbackResult =
                                            await this.trySoulseekDownload(
                                                job,
                                                album,
                                                downloadJobId,
                                                logger
                                            );

                                        if (soulseekFallbackResult.success) {
                                            downloadSuccess = true;
                                            logger?.info(
                                                `Album success via Soulseek fallback: ${
                                                    album.artistName
                                                } - ${
                                                    album.albumName
                                                } (tracksDownloaded=${
                                                    soulseekFallbackResult.tracksFound ??
                                                    0
                                                }/${album.tracksNeeded.length})`
                                            );
                                        } else {
                                            logger?.warn(
                                                `Soulseek fallback failed: ${
                                                    album.artistName
                                                } - ${album.albumName} (${
                                                    soulseekFallbackResult.error ||
                                                    "no results"
                                                })`
                                            );
                                        }
                                    }

                                    if (!downloadSuccess) {
                                        const errorMsg =
                                            result.error ||
                                            "No download sources available";
                                        console.log(
                                            `[Spotify Import] ✗ Failed: ${album.albumName} - ${errorMsg}`
                                        );
                                        logger?.logAlbumFailed(
                                            album.albumName,
                                            album.artistName,
                                            errorMsg
                                        );

                                        // Update download job to failed status
                                        await prisma.downloadJob
                                            .update({
                                                where: { id: downloadJobId },
                                                data: {
                                                    status: "failed",
                                                    error: errorMsg,
                                                    completedAt: new Date(),
                                                },
                                            })
                                            .catch(() => {});
                                    }
                                }

                                job.albumsCompleted++;
                                job.progress = Math.round(
                                    (job.albumsCompleted / job.albumsTotal) * 30
                                );
                                job.updatedAt = new Date();
                                await saveImportJob(job);

                                logger?.debug(
                                    `Album done: ${album.artistName} - ${
                                        album.albumName
                                    } (queuedOrDownloaded=${
                                        downloadSuccess ? "yes" : "no"
                                    })`
                                );
                            } catch (error: any) {
                                console.error(
                                    `[Spotify Import] Failed: ${album.artistName} - ${album.albumName}:`,
                                    error.message
                                );
                                logger?.logAlbumFailed(
                                    album.albumName,
                                    album.artistName,
                                    error.message
                                );

                                const downloadJobId = `spotify_${job.id}_${albumMbid}`;
                                await prisma.downloadJob
                                    .update({
                                        where: { id: downloadJobId },
                                        data: {
                                            status: "failed",
                                            error: error.message,
                                            completedAt: new Date(),
                                        },
                                    })
                                    .catch(() => {});
                            }
                        })
                );

                // Wait for all album downloads to complete
                await Promise.all(albumPromises);

                logger?.info(
                    `Initial queueing phase finished for ${albumMbidsToDownload.length} album(s). Checking completion state...`
                );

                // Check if we can complete immediately (all failed to queue)
                await this.checkImportCompletion(job.id);

                // If still downloading, wait for webhooks/organization
                if (job.status === "downloading") {
                    const waitingFor =
                        downloadSource === "soulseek" ? "Soulseek" : "Lidarr";
                    console.log(
                        `[Spotify Import] Job ${job.id}: Waiting for ${waitingFor} downloads...`
                    );
                    logger?.info(
                        `Waiting for ${waitingFor} downloads to complete...`
                    );
                }
                return;
            }

            // No downloads needed - all tracks already in library
            // Create playlist immediately
            await this.buildPlaylist(job);
        } catch (error: any) {
            job.status = "failed";
            job.error = error.message;
            job.updatedAt = new Date();
            throw error;
        }
    }

    /**
     * Try Soulseek for downloading tracks
     * Uses TRACK-based searching (more effective on Soulseek than album search)
     * Downloads directly to Singles/Artist/Album/ using slsk-client
     */
    private async trySoulseekDownload(
        job: ImportJob,
        album: AlbumToDownload,
        downloadJobId: string,
        logger: ReturnType<typeof createPlaylistLogger> | undefined
    ): Promise<{ success: boolean; error?: string; tracksFound?: number }> {
        // Check if Soulseek is available
        const soulseekAvailable = await soulseekService.isAvailable();
        if (!soulseekAvailable) {
            console.log(
                `[Spotify Import] Soulseek not available, skipping fallback`
            );
            return { success: false, error: "Soulseek not configured" };
        }

        console.log(
            `[Spotify Import] Trying Soulseek for: ${album.artistName} - ${album.albumName}`
        );
        console.log(
            `[Spotify Import] Searching for ${album.tracksNeeded.length} individual track(s)`
        );
        logger?.logSlskdFallbackStart(album.albumName, album.artistName);

        // Get music path for direct download
        const settings = await getSystemSettings();
        const musicPath = settings?.musicPath;
        if (!musicPath) {
            return { success: false, error: "Music path not configured" };
        }

        try {
            // Prepare track list for batch download - normalize titles for better search results
            const tracks = album.tracksNeeded.map((track) => ({
                artist: track.artist,
                title: stripTrackSuffix(track.title), // Remove live/remaster suffixes
                album: album.albumName,
            }));

            // Log individual track searches for debugging
            console.log(
                `[Spotify Import] Soulseek: Searching for ${tracks.length} track(s):`
            );
            album.tracksNeeded.forEach((t, i) => {
                const normalized = stripTrackSuffix(t.title);
                if (normalized !== t.title) {
                    console.log(
                        `   ${i + 1}. "${t.artist}" - "${
                            t.title
                        }" → "${normalized}"`
                    );
                } else {
                    console.log(`   ${i + 1}. "${t.artist}" - "${t.title}"`);
                }
            });

            // Use parallel batch download (4 concurrent by default)
            const result = await soulseekService.searchAndDownloadBatch(
                tracks,
                musicPath,
                4 // concurrency
            );

            if (result.successful === 0) {
                console.log(`[Spotify Import] Soulseek: No tracks downloaded`);
                logger?.logSlskdSearchResult(false);
                return { success: false, error: "No tracks found on Soulseek" };
            }

            // Calculate total size of downloaded files
            const fs = await import("fs");
            const totalSizeMB = result.files.reduce((sum, file) => {
                try {
                    const stats = fs.statSync(file);
                    return sum + stats.size / 1024 / 1024;
                } catch {
                    return sum;
                }
            }, 0);

            console.log(
                `[Spotify Import] ✓ Soulseek: Downloaded ${result.successful}/${
                    album.tracksNeeded.length
                } tracks (${Math.round(totalSizeMB)}MB)`
            );
            logger?.logSlskdSearchResult(
                true,
                "Mixed",
                "direct",
                result.successful,
                Math.round(totalSizeMB)
            );
            logger?.logSlskdDownloadQueued(result.successful, "direct");

            // Mark download job as completed immediately - files are already in place!
            await prisma.downloadJob
                .update({
                    where: { id: downloadJobId },
                    data: {
                        status: "completed",
                        completedAt: new Date(),
                        error: null,
                        metadata: {
                            spotifyImportJobId: job.id,
                            artistName: album.artistName,
                            albumTitle: album.albumName,
                            downloadType: "spotify_import",
                            source: "soulseek_direct",
                            tracksDownloaded: result.successful,
                            totalNeeded: album.tracksNeeded.length,
                            files: result.files,
                        },
                    },
                })
                .catch(() => {});

            return { success: true, tracksFound: result.successful };
        } catch (error: any) {
            console.error(`[Spotify Import] Soulseek error:`, error.message);
            logger?.logAlbumFailed(
                album.albumName,
                album.artistName,
                `Soulseek error: ${error.message}`
            );
            return { success: false, error: error.message };
        }
    }

    /**
     * Check if all downloads for this import are complete (called by webhook handler)
     */
    async checkImportCompletion(importJobId: string): Promise<void> {
        console.log(
            `\n[Spotify Import] Checking completion for job ${importJobId}...`
        );

        const job = await getImportJob(importJobId);
        if (!job) {
            console.log(`   Job not found`);
            return;
        }

        const logger = jobLoggers.get(importJobId);

        // Check download jobs for this import
        const downloadJobs = await prisma.downloadJob.findMany({
            where: {
                id: { startsWith: `spotify_${importJobId}_` },
            },
        });

        const total = downloadJobs.length;
        const completed = downloadJobs.filter(
            (j) => j.status === "completed"
        ).length;
        const failed = downloadJobs.filter((j) => j.status === "failed").length;
        const pending = total - completed - failed;

        if (total === 0 && job.albumsTotal > 0) {
            const message =
                "No download jobs were created for this import. This usually means the import preview did not include the selected albums.";
            console.log(`   ${message}`);
            logger?.warn(message);

            job.status = "failed";
            job.error = message;
            job.updatedAt = new Date();
            await saveImportJob(job);
            return;
        }

        console.log(
            `   Download status: ${completed}/${total} completed, ${failed} failed, ${pending} pending`
        );
        logger?.logDownloadProgress(completed, failed, pending);

        // Update progress
        job.progress =
            total > 0
                ? 30 + Math.round((completed / total) * 40) // 30-70% for downloads
                : 30;
        job.updatedAt = new Date();

        if (pending > 0) {
            // Check how long we've been waiting for these downloads
            const oldestPending = downloadJobs
                .filter(
                    (j) => j.status === "pending" || j.status === "processing"
                )
                .sort(
                    (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
                )[0];

            const waitTimeMs = oldestPending
                ? Date.now() - oldestPending.createdAt.getTime()
                : 0;
            const waitTimeMins = Math.round(waitTimeMs / 60000);

            // After 10 minutes of waiting, proceed anyway to avoid stuck jobs
            if (waitTimeMs < 600000) {
                // 10 minutes
                console.log(
                    `   Still waiting for ${pending} downloads... (${waitTimeMins} min elapsed)`
                );
                logger?.info(`Waiting for Soulseek downloads to complete...`);
                await saveImportJob(job);
                return;
            }

            console.log(
                `   Timeout: ${pending} downloads still pending after ${waitTimeMins} minutes, proceeding anyway`
            );
            logger?.warn(
                `Download timeout: ${pending} pending after ${waitTimeMins}m, proceeding with available tracks`
            );

            // Mark stale pending jobs as failed
            await prisma.downloadJob.updateMany({
                where: {
                    id: { startsWith: `spotify_${importJobId}_` },
                    status: { in: ["pending", "processing"] },
                },
                data: {
                    status: "failed",
                    error: "Timed out waiting for download",
                    completedAt: new Date(),
                },
            });
        }

        // All downloads finished (completed or failed)
        console.log(`   All downloads finished! Triggering library scan...`);
        logger?.info(
            `All ${total} download jobs finished (${completed} completed, ${failed} failed)`
        );

        // Trigger library scan to import the new files
        const { scanQueue } = await import("../workers/queues");
        const scanJob = await scanQueue.add("scan", {
            userId: job.userId,
            source: "spotify-import",
            spotifyImportJobId: importJobId,
        });

        logger?.info(
            `Queued library scan (bullJobId=${scanJob.id ?? "unknown"})`
        );

        job.status = "scanning";
        job.progress = 75;
        job.updatedAt = new Date();
        await saveImportJob(job);
    }

    /**
     * Build playlist after library scan completes (called by scan worker)
     */
    async buildPlaylistAfterScan(importJobId: string): Promise<void> {
        console.log(
            `\n[Spotify Import] Building playlist for job ${importJobId}...`
        );

        const job = await getImportJob(importJobId);
        if (!job) {
            console.log(`   Job not found`);
            return;
        }

        await this.buildPlaylist(job);
    }

    /**
     * Internal: Build the playlist with matched tracks
     */
    private async buildPlaylist(job: ImportJob): Promise<void> {
        const logger = jobLoggers.get(job.id);

        job.status = "creating_playlist";
        job.progress = 90;
        job.updatedAt = new Date();
        await saveImportJob(job);

        logger?.logPlaylistCreationStart();
        logger?.logTrackMatchingStart();

        // Match all pending tracks against the library
        const matchedTrackIds: string[] = [];
        let trackIndex = 0;

        for (const pendingTrack of job.pendingTracks) {
            trackIndex++;

            // FAST PATH: If already matched in preview, use that ID directly
            // This ensures tracks found during preview are included in the final playlist
            if (pendingTrack.preMatchedTrackId) {
                // Verify the track still exists
                const existingTrack = await prisma.track.findUnique({
                    where: { id: pendingTrack.preMatchedTrackId },
                    select: { id: true, title: true },
                });
                if (existingTrack) {
                    matchedTrackIds.push(existingTrack.id);
                    console.log(
                        `   ✓ Pre-matched: "${pendingTrack.title}" -> track ${existingTrack.id}`
                    );
                    logger?.logTrackMatch(
                        trackIndex,
                        job.tracksTotal,
                        pendingTrack.title,
                        pendingTrack.artist,
                        true,
                        existingTrack.id
                    );
                    continue;
                }
            }

            const normalizedArtist = normalizeString(pendingTrack.artist);
            // Get first word for fuzzy artist matching (handles "Nick Cave & The Bad Seeds" -> "nick")
            const artistFirstWord = normalizedArtist.split(" ")[0];
            // Strip suffix but keep punctuation for DB queries: "Ain't Gonna Rain Anymore - 2011 Remaster" -> "Ain't Gonna Rain Anymore"
            const strippedTitle = stripTrackSuffix(pendingTrack.title);
            // Also normalize apostrophes in the original title for searching
            const normalizedTitle = normalizeApostrophes(pendingTrack.title);
            // Fully normalized for similarity comparison: "aint gonna rain anymore"
            const cleanedTitle = normalizeTrackTitle(pendingTrack.title);

            logger?.log(
                `   Matching: "${pendingTrack.title}" by ${pendingTrack.artist}`
            );
            logger?.log(
                `   strippedTitle: "${strippedTitle}", artistFirstWord: "${artistFirstWord}"`
            );

            // Try multiple matching strategies
            let localTrack = null;

            // Strategy 1: Exact title match with fuzzy artist (contains first word)
            localTrack = await prisma.track.findFirst({
                where: {
                    title: {
                        equals: normalizedTitle,
                        mode: "insensitive",
                    },
                    album: {
                        artist: {
                            normalizedName: {
                                contains: artistFirstWord,
                                mode: "insensitive",
                            },
                        },
                    },
                },
            });

            // Strategy 2: Stripped title match (removes remaster suffix but keeps punctuation)
            // "Ain't Gonna Rain Anymore - 2011 Remaster" -> searches for "Ain't Gonna Rain Anymore"
            if (!localTrack && strippedTitle !== normalizedTitle) {
                logger?.log(
                    `   Strategy 2: Searching for stripped title "${strippedTitle}"`
                );
                localTrack = await prisma.track.findFirst({
                    where: {
                        title: {
                            equals: strippedTitle,
                            mode: "insensitive",
                        },
                        album: {
                            artist: {
                                normalizedName: {
                                    contains: artistFirstWord,
                                    mode: "insensitive",
                                },
                            },
                        },
                    },
                });
            }

            // Strategy 3: Case-insensitive CONTAINS search on title (handles slight variations)
            // e.g., database has "Ain't" but Spotify has "Ain't" (different apostrophe after normalization still differs)
            if (!localTrack && strippedTitle.length >= 5) {
                // Search for tracks where title contains the first few words
                const searchTerm = strippedTitle
                    .split(" ")
                    .slice(0, 4)
                    .join(" ");
                logger?.log(
                    `   Strategy 3: Contains search for "${searchTerm}"`
                );
                const candidates = await prisma.track.findMany({
                    where: {
                        title: {
                            contains: searchTerm,
                            mode: "insensitive",
                        },
                        album: {
                            artist: {
                                normalizedName: {
                                    contains: artistFirstWord,
                                    mode: "insensitive",
                                },
                            },
                        },
                    },
                    take: 10,
                });

                // Find best match using similarity OR containment
                for (const candidate of candidates) {
                    const candidateNormalized = normalizeTrackTitle(
                        candidate.title
                    );
                    const sim = stringSimilarity(
                        cleanedTitle,
                        candidateNormalized
                    );

                    // Direct similarity match
                    if (sim >= 80) {
                        localTrack = candidate;
                        logger?.log(
                            `      Found via contains+similarity (${sim.toFixed(
                                0
                            )}%)`
                        );
                        break;
                    }

                    // Containment match: "Sordid Affair" should match "Sordid Affair (Feat. Ryan James)"
                    // Check if one title contains the other (normalized)
                    const spotifyNorm = cleanedTitle.toLowerCase();
                    const libraryNorm = candidateNormalized.toLowerCase();
                    if (
                        libraryNorm.startsWith(spotifyNorm) ||
                        spotifyNorm.startsWith(libraryNorm)
                    ) {
                        localTrack = candidate;
                        logger?.log(
                            `      Found via containment match: "${cleanedTitle}" in "${candidateNormalized}"`
                        );
                        break;
                    }
                }
            }

            // Strategy 3.5: Same as preview - fuzzy match on artist NAME using similarity
            // This catches cases where normalizedName differs from what we expect
            if (!localTrack) {
                logger?.log(`   Strategy 3.5: Fuzzy artist+title matching`);
                const candidates = await prisma.track.findMany({
                    where: {
                        album: {
                            artist: {
                                normalizedName: {
                                    contains: artistFirstWord,
                                    mode: "insensitive",
                                },
                            },
                        },
                    },
                    include: { album: { include: { artist: true } } },
                    take: 50,
                });

                // Use same matching as preview: compare cleaned titles
                for (const candidate of candidates) {
                    const titleSim = stringSimilarity(
                        cleanedTitle,
                        normalizeTrackTitle(candidate.title)
                    );
                    const artistSim = stringSimilarity(
                        pendingTrack.artist,
                        candidate.album.artist.name
                    );
                    const score = titleSim * 0.6 + artistSim * 0.4;

                    if (score >= 70) {
                        localTrack = candidate;
                        console.log(
                            `      (preview-style match: ${score.toFixed(0)}%)`
                        );
                        break;
                    }
                }
            }

            // Strategy 4: StartsWith match with stripped title (for slight title variations)
            if (!localTrack && strippedTitle.length > 10) {
                logger?.log(`   Strategy 4: StartsWith search`);
                localTrack = await prisma.track.findFirst({
                    where: {
                        title: {
                            startsWith: strippedTitle.substring(
                                0,
                                Math.min(20, strippedTitle.length)
                            ),
                            mode: "insensitive",
                        },
                        album: {
                            artist: {
                                normalizedName: {
                                    contains: artistFirstWord,
                                    mode: "insensitive",
                                },
                            },
                        },
                    },
                });

                // Verify match
                if (localTrack) {
                    const dbTitleNormalized = normalizeTrackTitle(
                        localTrack.title
                    );
                    if (
                        stringSimilarity(cleanedTitle, dbTitleNormalized) < 70
                    ) {
                        localTrack = null;
                    } else {
                        logger?.log(`      Found via startsWith`);
                    }
                }
            }

            // Strategy 5: Very fuzzy - search and score by similarity (last resort)
            if (!localTrack) {
                logger?.log(`   Strategy 5: Fuzzy search (last resort)`);
                // Get first few words for search
                const searchWords = strippedTitle
                    .split(" ")
                    .slice(0, 3)
                    .join(" ");
                if (searchWords.length >= 4) {
                    const candidates = await prisma.track.findMany({
                        where: {
                            title: {
                                contains: searchWords.split(" ")[0], // Just first word
                                mode: "insensitive",
                            },
                            album: {
                                artist: {
                                    normalizedName: {
                                        contains: artistFirstWord,
                                        mode: "insensitive",
                                    },
                                },
                            },
                        },
                        include: { album: { include: { artist: true } } },
                        take: 20,
                    });

                    // Find best match by similarity
                    let bestMatch = null;
                    let bestScore = 0;
                    for (const candidate of candidates) {
                        const titleScore = stringSimilarity(
                            cleanedTitle,
                            normalizeTrackTitle(candidate.title)
                        );
                        const artistScore = stringSimilarity(
                            normalizedArtist,
                            normalizeString(candidate.album.artist.name)
                        );
                        const combinedScore =
                            titleScore * 0.7 + artistScore * 0.3;

                        if (combinedScore > bestScore && combinedScore >= 65) {
                            bestScore = combinedScore;
                            bestMatch = candidate;
                        }
                    }

                    if (bestMatch) {
                        localTrack = bestMatch;
                        console.log(
                            `      (fuzzy match: score ${bestScore.toFixed(
                                0
                            )}% with "${bestMatch.title}" by ${
                                bestMatch.album.artist.name
                            })`
                        );
                    }
                }
            }

            // Strategy 6: Title-only search (ignores artist entirely)
            // This handles cases where file has wrong artist metadata (e.g., "Various Artists" compilations)
            // Only used when title is distinctive enough (>10 chars) and no match found yet
            if (!localTrack && cleanedTitle.length >= 10) {
                logger?.log(
                    `   Strategy 6: Title-only search (fallback for wrong artist metadata)`
                );

                // Search for tracks with very similar title, ignore artist completely
                const titleSearchTerm = strippedTitle
                    .split(" ")
                    .slice(0, 4)
                    .join(" ");
                const candidates = await prisma.track.findMany({
                    where: {
                        title: {
                            contains: titleSearchTerm,
                            mode: "insensitive",
                        },
                    },
                    include: { album: { include: { artist: true } } },
                    take: 50,
                });

                // Find a high-confidence title match (require 85%+ similarity on title alone)
                let bestTitleMatch = null;
                let bestTitleScore = 0;

                for (const candidate of candidates) {
                    const titleScore = stringSimilarity(
                        cleanedTitle,
                        normalizeTrackTitle(candidate.title)
                    );

                    // Require very high title match since we're ignoring artist
                    if (titleScore > bestTitleScore && titleScore >= 85) {
                        bestTitleScore = titleScore;
                        bestTitleMatch = candidate;
                    }
                }

                if (bestTitleMatch) {
                    localTrack = bestTitleMatch;
                    logger?.log(
                        `      Found via title-only match (${bestTitleScore.toFixed(
                            0
                        )}%): "${bestTitleMatch.title}" by ${
                            bestTitleMatch.album.artist.name
                        }`
                    );
                    console.log(
                        `      (title-only match: ${bestTitleScore.toFixed(
                            0
                        )}% - note: artist metadata mismatch, wanted "${
                            pendingTrack.artist
                        }" got "${bestTitleMatch.album.artist.name}")`
                    );
                }
            }

            if (localTrack) {
                matchedTrackIds.push(localTrack.id);
                console.log(
                    `   ✓ Matched: "${pendingTrack.title}" -> track ${localTrack.id}`
                );
                logger?.logTrackMatch(
                    trackIndex,
                    job.tracksTotal,
                    pendingTrack.title,
                    pendingTrack.artist,
                    true,
                    localTrack.id
                );
            } else {
                // Debug: Check if artist exists at all
                const artistExists = await prisma.artist.findFirst({
                    where: {
                        normalizedName: {
                            contains: normalizedArtist.split(" ")[0],
                            mode: "insensitive",
                        },
                    },
                    select: { name: true, normalizedName: true },
                });
                if (artistExists) {
                    console.log(
                        `   ✗ No match: "${pendingTrack.title}" by ${pendingTrack.artist} (artist "${artistExists.name}" exists but track not found)`
                    );
                } else {
                    console.log(
                        `   ✗ No match: "${pendingTrack.title}" by ${pendingTrack.artist} (artist not in library)`
                    );
                }
                logger?.logTrackMatch(
                    trackIndex,
                    job.tracksTotal,
                    pendingTrack.title,
                    pendingTrack.artist,
                    false
                );
            }
        }

        const uniqueTrackIds = Array.from(new Set(matchedTrackIds));
        if (uniqueTrackIds.length < matchedTrackIds.length) {
            const removed = matchedTrackIds.length - uniqueTrackIds.length;
            console.log(
                `   Removed ${removed} duplicate track references before playlist creation`
            );
            logger?.info(
                `Removed ${removed} duplicate track references before playlist creation`
            );
        }

        console.log(
            `   Matched ${uniqueTrackIds.length}/${job.tracksTotal} tracks`
        );
        logger?.info(
            `Matched tracks after scan: ${uniqueTrackIds.length}/${job.tracksTotal}`
        );
        // Create the playlist with Spotify metadata
        const playlist = await prisma.playlist.create({
            data: {
                userId: job.userId,
                name: job.playlistName,
                isPublic: false,
                spotifyPlaylistId: job.spotifyPlaylistId,
                items:
                    uniqueTrackIds.length > 0
                        ? {
                              create: uniqueTrackIds.map((trackId, index) => ({
                                  trackId,
                                  sort: index,
                              })),
                          }
                        : undefined,
            },
        });

        // Save unmatched tracks as pending tracks for later auto-matching
        const unmatchedTracks = job.pendingTracks.filter((_, index) => {
            // We need to track which indices were matched
            // Since matchedTrackIds doesn't preserve order, we need a different approach
            return true; // We'll recalculate below
        });

        // Recalculate unmatched - tracks that weren't added to playlist
        const matchedTitlesNormalized = new Set<string>();
        for (const pendingTrack of job.pendingTracks) {
            const normalizedArtist = normalizeString(pendingTrack.artist);
            const strippedTitle = stripTrackSuffix(pendingTrack.title);

            // Check if this track was matched by looking for it in the created items
            const found = await prisma.track.findFirst({
                where: {
                    id: { in: uniqueTrackIds },
                    title: {
                        contains: strippedTitle.split(" ")[0],
                        mode: "insensitive",
                    },
                    album: {
                        artist: {
                            normalizedName: {
                                contains: normalizedArtist.split(" ")[0],
                                mode: "insensitive",
                            },
                        },
                    },
                },
            });

            if (found) {
                matchedTitlesNormalized.add(
                    `${normalizedArtist}|${strippedTitle.toLowerCase()}`
                );
            }
        }

        // Save pending tracks that weren't matched
        const pendingTracksToSave = job.pendingTracks
            .map((track, index) => ({ ...track, originalIndex: index }))
            .filter((track) => {
                const normalizedArtist = normalizeString(track.artist);
                const strippedTitle = stripTrackSuffix(
                    track.title
                ).toLowerCase();
                return !matchedTitlesNormalized.has(
                    `${normalizedArtist}|${strippedTitle}`
                );
            });

        if (pendingTracksToSave.length > 0) {
            console.log(
                `   Saving ${pendingTracksToSave.length} pending tracks for future auto-matching`
            );
            console.log(
                `   Fetching Deezer preview URLs for pending tracks...`
            );
            logger?.info(
                `Saving pending tracks: ${pendingTracksToSave.length}`
            );

            // Fetch Deezer previews in parallel for all pending tracks
            const pendingTracksWithPreviews = await Promise.all(
                pendingTracksToSave.map(async (track) => {
                    let deezerPreviewUrl: string | null = null;
                    try {
                        deezerPreviewUrl = await deezerService.getTrackPreview(
                            track.artist,
                            track.title
                        );
                    } catch (e) {
                        // Preview not critical, continue without it
                    }
                    return {
                        ...track,
                        deezerPreviewUrl,
                    };
                })
            );

            const previewsFound = pendingTracksWithPreviews.filter(
                (t) => t.deezerPreviewUrl
            ).length;
            console.log(
                `   Found ${previewsFound}/${pendingTracksToSave.length} Deezer preview URLs`
            );
            logger?.info(
                `Pending previews found: ${previewsFound}/${pendingTracksToSave.length}`
            );

            await prisma.playlistPendingTrack.createMany({
                data: pendingTracksWithPreviews.map((track) => ({
                    playlistId: playlist.id,
                    spotifyArtist: track.artist,
                    spotifyTitle: track.title,
                    spotifyAlbum: track.album,
                    deezerPreviewUrl: track.deezerPreviewUrl,
                    sort: track.originalIndex,
                })),
                skipDuplicates: true,
            });
        }

        job.createdPlaylistId = playlist.id;
        job.tracksMatched = uniqueTrackIds.length;
        job.status = "completed";
        job.progress = 100;
        job.updatedAt = new Date();
        await saveImportJob(job);

        console.log(`[Spotify Import] Job ${job.id} completed:`);
        console.log(`   Playlist created: ${playlist.id}`);
        console.log(
            `   Tracks matched: ${matchedTrackIds.length}/${job.tracksTotal}`
        );

        logger?.logPlaylistCreated(
            playlist.id,
            matchedTrackIds.length,
            job.tracksTotal
        );
        logger?.logJobComplete(
            matchedTrackIds.length,
            job.tracksTotal,
            playlist.id
        );

        // Send notification about import completion
        try {
            await notificationService.notifyImportComplete(
                job.userId,
                job.playlistName,
                playlist.id,
                matchedTrackIds.length,
                job.tracksTotal
            );
        } catch (notifError) {
            console.error("Failed to send import notification:", notifError);
        }
    }

    /**
     * Re-match pending tracks and add newly downloaded ones to the playlist
     */
    async refreshJobMatches(
        jobId: string
    ): Promise<{ added: number; total: number }> {
        const logger = jobLoggers.get(jobId);
        const job = await getImportJob(jobId);
        if (!job) {
            throw new Error("Import job not found");
        }
        if (!job.createdPlaylistId) {
            throw new Error("No playlist created for this job");
        }

        let added = 0;

        // Get existing tracks in playlist
        const existingItems = await prisma.playlistItem.findMany({
            where: { playlistId: job.createdPlaylistId },
            select: { trackId: true },
        });
        const existingTrackIds = new Set(
            existingItems.map((item) => item.trackId)
        );

        // Get next position
        const maxPosition = existingItems.length;
        let nextPosition = maxPosition;

        // Try to match each pending track
        for (const pendingTrack of job.pendingTracks) {
            const normalizedArtist = normalizeString(pendingTrack.artist);

            // Track model doesn't have normalizedTitle - use case-insensitive title matching
            const localTrack = await prisma.track.findFirst({
                where: {
                    title: {
                        equals: pendingTrack.title,
                        mode: "insensitive",
                    },
                    album: {
                        artist: {
                            normalizedName: normalizedArtist,
                        },
                    },
                },
            });

            if (localTrack && !existingTrackIds.has(localTrack.id)) {
                // Add to playlist
                await prisma.playlistItem.create({
                    data: {
                        playlistId: job.createdPlaylistId,
                        trackId: localTrack.id,
                        position: nextPosition++,
                    },
                });
                existingTrackIds.add(localTrack.id);
                added++;
            }
        }

        job.tracksMatched += added;
        job.updatedAt = new Date();

        console.log(
            `[Spotify Import] Refresh job ${jobId}: added ${added} newly downloaded tracks`
        );
        logger?.info(
            `Refresh: added ${added} newly downloaded track(s), totalMatchedNow=${job.tracksMatched}`
        );

        return { added, total: job.tracksMatched };
    }

    /**
     * Get import job status (public method for routes)
     */
    async getJob(jobId: string): Promise<ImportJob | null> {
        return await getImportJob(jobId);
    }

    /**
     * Get all jobs for a user
     */
    async getUserJobs(userId: string): Promise<ImportJob[]> {
        // Get from database to include jobs across restarts
        const dbJobs = await prisma.spotifyImportJob.findMany({
            where: { userId },
            orderBy: { createdAt: "desc" },
        });

        return dbJobs
            .map((dbJob) => ({
                id: dbJob.id,
                userId: dbJob.userId,
                spotifyPlaylistId: dbJob.spotifyPlaylistId,
                playlistName: dbJob.playlistName,
                status: dbJob.status as ImportJob["status"],
                progress: dbJob.progress,
                albumsTotal: dbJob.albumsTotal,
                albumsCompleted: dbJob.albumsCompleted,
                tracksMatched: dbJob.tracksMatched,
                tracksTotal: dbJob.tracksTotal,
                tracksDownloadable: dbJob.tracksDownloadable,
                createdPlaylistId: dbJob.createdPlaylistId,
                error: dbJob.error,
                createdAt: dbJob.createdAt,
                updatedAt: dbJob.updatedAt,
                pendingTracks: (dbJob.pendingTracks as any) || [],
            }))
            .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
    }

    /**
     * Cancel an import job without creating a playlist.
     * All pending downloads are marked as failed and the job is marked as cancelled.
     */
    async cancelJob(jobId: string): Promise<{
        playlistCreated: boolean;
        playlistId: string | null;
        tracksMatched: number;
    }> {
        const job = await getImportJob(jobId);
        if (!job) {
            throw new Error("Import job not found");
        }

        const logger = jobLoggers.get(jobId);
        console.log(`[Spotify Import] Cancelling job ${jobId}...`);
        logger?.info(`Job cancelled by user`);

        // If already completed, cancelled, or failed, nothing to do
        if (
            job.status === "completed" ||
            job.status === "failed" ||
            job.status === "cancelled"
        ) {
            return {
                playlistCreated: !!job.createdPlaylistId,
                playlistId: job.createdPlaylistId || null,
                tracksMatched: job.tracksMatched,
            };
        }

        // Mark any pending download jobs as cancelled
        await prisma.downloadJob.updateMany({
            where: {
                id: { startsWith: `spotify_${jobId}_` },
                status: { in: ["pending", "processing"] },
            },
            data: {
                status: "failed",
                error: "Import cancelled by user",
                completedAt: new Date(),
            },
        });

        // Mark job as cancelled - do NOT create a playlist
        job.status = "cancelled";
        job.updatedAt = new Date();
        logger?.info(`Import cancelled by user - no playlist created`);

        return {
            playlistCreated: false,
            playlistId: null,
            tracksMatched: 0,
        };
    }

    /**
     * Reconcile pending tracks for ALL playlists after a library scan
     * This checks if any previously unmatched tracks now have matches in the library
     * and automatically adds them to their playlists
     */
    async reconcilePendingTracks(): Promise<{
        playlistsUpdated: number;
        tracksAdded: number;
    }> {
        console.log(
            `\n[Spotify Import] Reconciling pending tracks across all playlists...`
        );

        // Get all pending tracks grouped by playlist
        const allPendingTracks = await prisma.playlistPendingTrack.findMany({
            include: {
                playlist: {
                    select: {
                        id: true,
                        name: true,
                        userId: true,
                    },
                },
            },
            orderBy: [{ playlistId: "asc" }, { sort: "asc" }],
        });

        if (allPendingTracks.length === 0) {
            console.log(`   No pending tracks to reconcile`);
            return { playlistsUpdated: 0, tracksAdded: 0 };
        }

        console.log(
            `   Found ${allPendingTracks.length} pending tracks across playlists`
        );

        let totalTracksAdded = 0;
        const playlistsWithAdditions = new Set<string>();
        const matchedPendingTrackIds: string[] = [];

        // Group by playlist for efficient processing
        const tracksByPlaylist = new Map<string, typeof allPendingTracks>();
        for (const pt of allPendingTracks) {
            const existing = tracksByPlaylist.get(pt.playlistId) || [];
            existing.push(pt);
            tracksByPlaylist.set(pt.playlistId, existing);
        }

        for (const [playlistId, pendingTracks] of tracksByPlaylist) {
            // Get current max sort position in playlist
            const maxSortResult = await prisma.playlistItem.aggregate({
                where: { playlistId },
                _max: { sort: true },
            });
            let nextSort = (maxSortResult._max.sort ?? -1) + 1;

            // Get existing track IDs in playlist to avoid duplicates
            const existingItems = await prisma.playlistItem.findMany({
                where: { playlistId },
                select: { trackId: true },
            });
            const existingTrackIds = new Set(
                existingItems.map((item) => item.trackId)
            );

            for (const pendingTrack of pendingTracks) {
                const normalizedArtist = normalizeString(
                    pendingTrack.spotifyArtist
                );
                const artistFirstWord = normalizedArtist.split(" ")[0];
                const strippedTitle = stripTrackSuffix(
                    pendingTrack.spotifyTitle
                );
                const cleanedTitle = normalizeTrackTitle(strippedTitle);

                console.log(
                    `   Trying to match: "${pendingTrack.spotifyTitle}" by ${pendingTrack.spotifyArtist}`
                );
                console.log(
                    `      strippedTitle: "${strippedTitle}", artistFirstWord: "${artistFirstWord}"`
                );

                // Debug: Check what tracks exist for this artist
                const artistTracks = await prisma.track.findMany({
                    where: {
                        album: {
                            artist: {
                                normalizedName: {
                                    contains: artistFirstWord,
                                    mode: "insensitive",
                                },
                            },
                        },
                    },
                    select: {
                        title: true,
                        album: {
                            select: {
                                artist: {
                                    select: {
                                        name: true,
                                        normalizedName: true,
                                    },
                                },
                            },
                        },
                    },
                    take: 5,
                });
                if (artistTracks.length > 0) {
                    console.log(
                        `      DEBUG: Found ${artistTracks.length}+ tracks for artist containing "${artistFirstWord}"`
                    );
                    artistTracks
                        .slice(0, 3)
                        .forEach((t) =>
                            console.log(
                                `         - "${t.title}" (artist: ${t.album.artist.name}, normalized: ${t.album.artist.normalizedName})`
                            )
                        );
                } else {
                    console.log(
                        `      DEBUG: NO tracks found for artist containing "${artistFirstWord}"`
                    );
                }

                // Try to find a matching track (using same strategies as buildPlaylist)
                // Strategy 1: Stripped title + fuzzy artist (contains first word)
                let localTrack = await prisma.track.findFirst({
                    where: {
                        title: { equals: strippedTitle, mode: "insensitive" },
                        album: {
                            artist: {
                                normalizedName: {
                                    contains: artistFirstWord,
                                    mode: "insensitive",
                                },
                            },
                        },
                    },
                    select: { id: true, title: true },
                });

                console.log(
                    `      Strategy 1 result: ${
                        localTrack ? "FOUND" : "not found"
                    }`
                );

                // Strategy 2: Contains search on first few words + similarity
                if (!localTrack && strippedTitle.length >= 5) {
                    const searchTerm = strippedTitle
                        .split(" ")
                        .slice(0, 4)
                        .join(" ");
                    console.log(
                        `      Strategy 2: Contains search for "${searchTerm}"`
                    );
                    const candidates = await prisma.track.findMany({
                        where: {
                            title: {
                                contains: searchTerm,
                                mode: "insensitive",
                            },
                            album: {
                                artist: {
                                    normalizedName: {
                                        contains: artistFirstWord,
                                        mode: "insensitive",
                                    },
                                },
                            },
                        },
                        include: { album: { include: { artist: true } } },
                        take: 10,
                    });

                    console.log(
                        `      Strategy 2: Found ${candidates.length} candidates`
                    );
                    for (const candidate of candidates) {
                        const candidateNormalized = normalizeTrackTitle(
                            candidate.title
                        );
                        const sim = stringSimilarity(
                            cleanedTitle,
                            candidateNormalized
                        );
                        console.log(
                            `         "${candidate.title}" by ${
                                candidate.album.artist.name
                            }: ${sim.toFixed(0)}%`
                        );

                        // Direct similarity match
                        if (sim >= 80) {
                            localTrack = {
                                id: candidate.id,
                                title: candidate.title,
                            };
                            break;
                        }

                        // Containment match: "Sordid Affair" should match "Sordid Affair (Feat. Ryan James)"
                        const spotifyNorm = cleanedTitle.toLowerCase();
                        const libraryNorm = candidateNormalized.toLowerCase();
                        if (
                            libraryNorm.startsWith(spotifyNorm) ||
                            spotifyNorm.startsWith(libraryNorm)
                        ) {
                            console.log(
                                `         Found via containment: "${cleanedTitle}" starts "${candidateNormalized}"`
                            );
                            localTrack = {
                                id: candidate.id,
                                title: candidate.title,
                            };
                            break;
                        }
                    }
                }

                if (!localTrack)
                    console.log(`      Strategy 2 result: not found`);

                // Strategy 3: Fuzzy match on title + artist similarity
                if (!localTrack) {
                    const firstWord = strippedTitle.split(" ")[0];
                    console.log(
                        `      Strategy 3: Fuzzy search for title containing "${firstWord}" and artist containing "${artistFirstWord}"`
                    );
                    const candidates = await prisma.track.findMany({
                        where: {
                            title: { contains: firstWord, mode: "insensitive" },
                            album: {
                                artist: {
                                    normalizedName: {
                                        contains: artistFirstWord,
                                        mode: "insensitive",
                                    },
                                },
                            },
                        },
                        include: { album: { include: { artist: true } } },
                        take: 20,
                    });

                    console.log(
                        `      Strategy 3: Found ${candidates.length} candidates`
                    );
                    for (const candidate of candidates) {
                        const titleScore = stringSimilarity(
                            cleanedTitle,
                            normalizeTrackTitle(candidate.title)
                        );
                        const artistScore = stringSimilarity(
                            pendingTrack.spotifyArtist,
                            candidate.album.artist.name
                        );
                        const combinedScore =
                            titleScore * 0.6 + artistScore * 0.4;
                        console.log(
                            `         "${candidate.title}" by ${
                                candidate.album.artist.name
                            }: title=${titleScore.toFixed(
                                0
                            )}%, artist=${artistScore.toFixed(
                                0
                            )}%, combined=${combinedScore.toFixed(0)}%`
                        );

                        if (combinedScore >= 70) {
                            localTrack = {
                                id: candidate.id,
                                title: candidate.title,
                            };
                            break;
                        }
                    }
                }

                if (localTrack && !existingTrackIds.has(localTrack.id)) {
                    // Add to playlist
                    await prisma.playlistItem.create({
                        data: {
                            playlistId,
                            trackId: localTrack.id,
                            sort: nextSort++,
                        },
                    });

                    existingTrackIds.add(localTrack.id);
                    matchedPendingTrackIds.push(pendingTrack.id);
                    totalTracksAdded++;
                    playlistsWithAdditions.add(playlistId);

                    console.log(
                        `   ✓ Matched: "${pendingTrack.spotifyTitle}" by ${pendingTrack.spotifyArtist}`
                    );
                }
            }
        }

        // Delete the matched pending tracks
        if (matchedPendingTrackIds.length > 0) {
            await prisma.playlistPendingTrack.deleteMany({
                where: { id: { in: matchedPendingTrackIds } },
            });
        }

        // Send notifications for each playlist that was updated
        if (playlistsWithAdditions.size > 0) {
            const { notificationService } = await import(
                "./notificationService"
            );

            for (const playlistId of playlistsWithAdditions) {
                const playlist = await prisma.playlist.findUnique({
                    where: { id: playlistId },
                    select: { id: true, name: true, userId: true },
                });

                if (playlist) {
                    const tracksAddedToPlaylist = matchedPendingTrackIds.filter(
                        (id) =>
                            allPendingTracks.find(
                                (pt) =>
                                    pt.id === id && pt.playlistId === playlistId
                            )
                    ).length;

                    await notificationService.create({
                        userId: playlist.userId,
                        type: "playlist_ready",
                        title: "Playlist Updated",
                        message: `${tracksAddedToPlaylist} new track${
                            tracksAddedToPlaylist !== 1 ? "s" : ""
                        } added to "${playlist.name}"`,
                        metadata: {
                            playlistId: playlist.id,
                            tracksAdded: tracksAddedToPlaylist,
                        },
                    });
                }
            }
        }

        console.log(
            `   Reconciliation complete: ${totalTracksAdded} tracks added to ${playlistsWithAdditions.size} playlists`
        );

        return {
            playlistsUpdated: playlistsWithAdditions.size,
            tracksAdded: totalTracksAdded,
        };
    }

    /**
     * Get pending tracks count for a playlist
     */
    async getPendingTracksCount(playlistId: string): Promise<number> {
        return prisma.playlistPendingTrack.count({
            where: { playlistId },
        });
    }

    /**
     * Get pending tracks for a playlist
     */
    async getPendingTracks(playlistId: string): Promise<
        Array<{
            id: string;
            artist: string;
            title: string;
            album: string;
        }>
    > {
        const tracks = await prisma.playlistPendingTrack.findMany({
            where: { playlistId },
            orderBy: { sort: "asc" },
        });

        return tracks.map((t) => ({
            id: t.id,
            artist: t.spotifyArtist,
            title: t.spotifyTitle,
            album: t.spotifyAlbum,
        }));
    }
}

export const spotifyImportService = new SpotifyImportService();
