import Foundation
import os
import SwiftData

private let logger = Logger(subsystem: "com.mixboard.MixBoardiOS", category: "LibraryManager")

/// Manages the music library — importing tracks from the document picker and managing files.
@MainActor
final class LibraryManager: ObservableObject {
    @Published var isScanning = false
    @Published var scanProgress: Double = 0
    @Published var scanStatus: String = ""
    @Published var lastError: String?

    private var modelContext: ModelContext?

    /// App documents directory where imported music files are stored.
    static var musicDirectory: URL {
        let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let musicDir = docs.appendingPathComponent("Music", isDirectory: true)
        try? FileManager.default.createDirectory(at: musicDir, withIntermediateDirectories: true)
        return musicDir
    }

    func setModelContext(_ context: ModelContext) {
        self.modelContext = context
    }

    // MARK: - Import from URLs (files and/or folders)

    /// Import audio files or folders selected from a document picker.
    /// Files get copied into the app's Music directory.
    func importFiles(_ urls: [URL]) async {
        guard let context = modelContext else { return }
        isScanning = true
        scanProgress = 0
        scanStatus = "Collecting files..."
        lastError = nil

        // Resolve folders into individual audio file URLs
        var audioURLs: [URL] = []
        for url in urls {
            let accessing = url.startAccessingSecurityScopedResource()
            defer { if accessing { url.stopAccessingSecurityScopedResource() } }

            var isDir: ObjCBool = false
            if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir), isDir.boolValue {
                // It's a folder — enumerate audio files recursively
                let folderFiles = collectAudioFiles(in: url)
                audioURLs.append(contentsOf: folderFiles)
            } else if MetadataService.isSupportedAudioFile(url) {
                audioURLs.append(url)
            }
        }

        guard !audioURLs.isEmpty else {
            isScanning = false
            scanStatus = ""
            lastError = "No supported audio files found"
            return
        }

        scanStatus = "Importing \(audioURLs.count) files..."

        for (index, url) in audioURLs.enumerated() {
            scanProgress = Double(index) / Double(audioURLs.count)
            scanStatus = "Importing \(index + 1)/\(audioURLs.count): \(url.lastPathComponent)"

            let accessing = url.startAccessingSecurityScopedResource()
            defer { if accessing { url.stopAccessingSecurityScopedResource() } }

            do {
                try await importFile(url, context: context)
            } catch {
                print("LibraryManager: Failed to import \(url.lastPathComponent): \(error)")
            }
        }

        try? context.save()
        isScanning = false
        scanProgress = 1.0
        scanStatus = ""
    }

    /// Recursively collect audio files from a directory.
    private func collectAudioFiles(in folderURL: URL) -> [URL] {
        var result: [URL] = []
        let fm = FileManager.default

        guard let enumerator = fm.enumerator(
            at: folderURL,
            includingPropertiesForKeys: [.isRegularFileKey],
            options: [.skipsHiddenFiles]
        ) else { return result }

        for case let fileURL as URL in enumerator {
            if MetadataService.isSupportedAudioFile(fileURL) {
                result.append(fileURL)
            }
        }

        return result
    }

    // MARK: - Auto-scan Documents folder

    /// Scan the app's entire Documents directory (recursively) for any audio files
    /// that aren't yet in the library. This picks up files placed via the Files app.
    func scanMusicDirectory() async {
        guard let context = modelContext else {
            logger.error("scanMusicDirectory: No model context")
            return
        }

        let fm = FileManager.default
        let docsDir = fm.urls(for: .documentDirectory, in: .userDomainMask).first!.standardizedFileURL
        logger.notice("scanMusicDirectory: Scanning \(docsDir.path)")

        // Collect audio files from the entire Documents directory recursively
        var audioFiles: [URL] = []
        if let enumerator = fm.enumerator(
            at: docsDir,
            includingPropertiesForKeys: [.isRegularFileKey],
            options: [.skipsHiddenFiles]
        ) {
            for case let fileURL as URL in enumerator {
                if MetadataService.isSupportedAudioFile(fileURL) {
                    audioFiles.append(fileURL)
                }
            }
        }

        logger.notice("scanMusicDirectory: Found \(audioFiles.count) audio files on disk")
        guard !audioFiles.isEmpty else { return }

        var newCount = 0
        isScanning = true
        scanStatus = "Scanning for music files..."

        for (index, fileURL) in audioFiles.enumerated() {
            scanProgress = Double(index) / Double(audioFiles.count)

            let fileName = fileURL.lastPathComponent
            // Check by relative path (not just filename) so same-named files in different folders are all imported
            let standardizedFile = fileURL.standardizedFileURL.path
            let standardizedDocs = docsDir.path
            let checkPath = standardizedFile.hasPrefix(standardizedDocs + "/")
                ? String(standardizedFile.dropFirst(standardizedDocs.count + 1))
                : fileName
            let descriptor = FetchDescriptor<Track>(predicate: #Predicate { $0.filePath == checkPath })
            let existing = (try? context.fetch(descriptor)) ?? []

            if existing.isEmpty {
                do {
                    let metadata = try await MetadataService.readMetadata(from: fileURL)
                    // Store the path relative to Documents (standardize to handle /private/var vs /var)
                    let standardizedFile = fileURL.standardizedFileURL.path
                    let standardizedDocs = docsDir.path
                    let relativePath = standardizedFile.hasPrefix(standardizedDocs + "/")
                        ? String(standardizedFile.dropFirst(standardizedDocs.count + 1))
                        : fileName
                    logger.notice("scanMusicDirectory: Importing \(fileName) → relativePath: \(relativePath)")
                    let track = Track(
                        title: metadata.title,
                        artist: metadata.artist,
                        album: metadata.album,
                        genre: metadata.genre,
                        filePath: relativePath,
                        fileName: fileName,
                        duration: metadata.duration,
                        sampleRate: metadata.sampleRate,
                        bitDepth: metadata.bitDepth,
                        channels: metadata.channels,
                        fileFormat: metadata.fileFormat,
                        fileSizeBytes: metadata.fileSizeBytes
                    )
                    track.year = metadata.year
                    context.insert(track)
                    newCount += 1
                    scanStatus = "Found: \(fileName)"
                } catch {
                    logger.error("scanMusicDirectory: Failed to scan \(fileName): \(error)")
                }
            }
        }

        if newCount > 0 {
            try? context.save()
        }

        isScanning = false
        scanProgress = 1.0
        scanStatus = newCount > 0 ? "Found \(newCount) new tracks" : ""
    }

    /// Fix tracks with bad paths. Call once at startup, not during scans.
    func fixBadPathsIfNeeded() {
        guard let context = modelContext else { return }
        let hasRun = UserDefaults.standard.bool(forKey: "pathFixV1")
        guard !hasRun else { return }
        fixBadPaths(context: context)
        UserDefaults.standard.set(true, forKey: "pathFixV1")
    }

    /// Fix tracks that have absolute or /private-prefixed paths from a previous scan bug.
    private func fixBadPaths(context: ModelContext) {
        let descriptor = FetchDescriptor<Track>()
        guard let allTracks = try? context.fetch(descriptor) else { return }
        let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.standardizedFileURL

        var fixed = 0
        for track in allTracks {
            var path = track.filePath
            // Remove leading /private prefix
            if path.hasPrefix("/private") {
                path = String(path.dropFirst("/private".count))
            }
            // Remove absolute Documents path prefix if present
            if path.hasPrefix(docsDir.path + "/") {
                path = String(path.dropFirst(docsDir.path.count + 1))
            }
            // Remove leading / if still absolute
            if path.hasPrefix("/") && !path.hasPrefix(docsDir.path) {
                // Check if the file actually exists relative to Documents
                let candidateURL = docsDir.appendingPathComponent(String(path.dropFirst()))
                if FileManager.default.fileExists(atPath: candidateURL.path) {
                    path = String(path.dropFirst())
                }
            }
            if path != track.filePath {
                logger.notice("fixBadPaths: \(track.filePath) → \(path)")
                track.filePath = path
                fixed += 1
            }
        }
        if fixed > 0 {
            try? context.save()
            logger.notice("fixBadPaths: Fixed \(fixed) tracks")
        }
    }

    // MARK: - Import single file

    /// Import a single audio file. Copies it to the app's Music directory.
    func importFile(_ url: URL, context: ModelContext? = nil) async throws {
        let ctx = context ?? modelContext
        guard let ctx else { return }

        let fileName = url.lastPathComponent

        // Check if already imported by filename
        let descriptor = FetchDescriptor<Track>(predicate: #Predicate { $0.fileName == fileName })
        let existing = try ctx.fetch(descriptor)
        guard existing.isEmpty else { return }

        // Copy file to app's Music directory (unless it's already there)
        let destURL = Self.musicDirectory.appendingPathComponent(fileName)
        if !FileManager.default.fileExists(atPath: destURL.path) {
            try FileManager.default.copyItem(at: url, to: destURL)
        }

        // Read metadata
        let metadata = try await MetadataService.readMetadata(from: destURL)

        let relativePath = "Music/\(fileName)"
        let track = Track(
            title: metadata.title,
            artist: metadata.artist,
            album: metadata.album,
            genre: metadata.genre,
            filePath: relativePath,
            fileName: fileName,
            duration: metadata.duration,
            sampleRate: metadata.sampleRate,
            bitDepth: metadata.bitDepth,
            channels: metadata.channels,
            fileFormat: metadata.fileFormat,
            fileSizeBytes: metadata.fileSizeBytes
        )
        track.year = metadata.year

        ctx.insert(track)
    }

    // MARK: - Analysis

    func analyzeTrack(_ track: Track) async {
        do {
            async let bpmResult = BPMDetector.detectBPM(for: track)
            async let keyResult = KeyDetector.detectKey(for: track)

            let bpm = try await bpmResult
            let key = try await keyResult

            track.bpm = bpm
            track.musicalKey = key.shortKey
            track.isAnalyzed = true
        } catch {
            print("LibraryManager: Analysis failed for \(track.title): \(error)")
        }
    }

    func analyzeAllTracks(tracks: [Track]) async {
        let unanalyzed = tracks.filter { !$0.isAnalyzed }
        for (index, track) in unanalyzed.enumerated() {
            scanProgress = Double(index) / Double(unanalyzed.count)
            await analyzeTrack(track)
        }
        scanProgress = 1.0
    }

    // MARK: - Deletion

    func removeTrack(_ track: Track, deleteFile: Bool = true) {
        if deleteFile {
            try? FileManager.default.removeItem(at: track.fileURL)
        }
        modelContext?.delete(track)
    }

    // MARK: - Rescan Metadata

    /// Re-read metadata from disk for all tracks in the library.
    /// Updates title, artist, album, genre, year, duration, and audio properties.
    /// Useful after fixing metadata readers (e.g. OGG/Opus tag reading).
    func rescanMetadata() async {
        guard let context = modelContext else { return }

        let descriptor = FetchDescriptor<Track>()
        guard let allTracks = try? context.fetch(descriptor) else { return }
        guard !allTracks.isEmpty else { return }

        isScanning = true
        scanStatus = "Rescanning metadata..."
        scanProgress = 0

        var updated = 0

        for (index, track) in allTracks.enumerated() {
            scanProgress = Double(index) / Double(allTracks.count)
            scanStatus = "Rescanning \(index + 1)/\(allTracks.count): \(track.fileName)"

            let fileURL = track.fileURL
            guard FileManager.default.fileExists(atPath: fileURL.path) else { continue }

            do {
                let metadata = try await MetadataService.readMetadata(from: fileURL)

                // Always update metadata fields
                track.title = metadata.title
                track.artist = metadata.artist
                track.album = metadata.album
                track.genre = metadata.genre
                track.year = metadata.year
                track.duration = metadata.duration
                track.sampleRate = metadata.sampleRate
                track.bitDepth = metadata.bitDepth
                track.channels = metadata.channels
                track.fileFormat = metadata.fileFormat
                track.fileSizeBytes = metadata.fileSizeBytes
                updated += 1
            } catch {
                logger.error("rescanMetadata: Failed for \(track.fileName): \(error)")
            }
        }

        if updated > 0 {
            try? context.save()
        }

        isScanning = false
        scanProgress = 1.0
        scanStatus = updated > 0 ? "Updated \(updated) tracks" : "No updates needed"
        logger.notice("rescanMetadata: Updated \(updated) of \(allTracks.count) tracks")
    }
}
