import Foundation
import AVFoundation

// With the project-wide `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor` setting,
// this class is implicitly @MainActor. That's intentional: @Observable properties
// (isScanning, scanProgress) drive the UI and must be updated on the main actor.
// Static utility methods are marked `nonisolated` so they can run off the main actor.
@Observable
final class ScannerService {
    var isScanning = false
    var scanProgress: (current: Int, total: Int) = (0, 0)

    let db: DatabaseService

    init(db: DatabaseService) {
        self.db = db
    }

    nonisolated static let audioExtensions: Set<String> = ["mp3", "m4a", "aac", "wav", "aiff", "alac", "flac"]

    nonisolated static func discoverAudioFiles(in folder: URL) -> [URL] {
        var results: [URL] = []
        guard let enumerator = FileManager.default.enumerator(
            at: folder,
            includingPropertiesForKeys: [.isRegularFileKey],
            options: [.skipsHiddenFiles]
        ) else { return results }

        while let url = enumerator.nextObject() as? URL {
            if audioExtensions.contains(url.pathExtension.lowercased()) {
                results.append(url)
            }
        }
        return results
    }

    func scanFolder(_ folder: URL) async {
        isScanning = true
        defer { isScanning = false }

        let audioFiles = Self.discoverAudioFiles(in: folder)
        scanProgress = (0, audioFiles.count)

        let existingURLs = (try? db.allFileURLs()) ?? []
        let batchSize = 50

        for batchStart in stride(from: 0, to: audioFiles.count, by: batchSize) {
            let batchEnd = min(batchStart + batchSize, audioFiles.count)
            let batch = Array(audioFiles[batchStart..<batchEnd])

            var tracks: [Track] = []
            for fileURL in batch {
                let urlString = fileURL.absoluteString
                if existingURLs.contains(urlString) { continue }
                if let track = await Self.extractMetadata(from: fileURL) {
                    tracks.append(track)
                }
            }

            if !tracks.isEmpty {
                try? db.insertBatch(tracks)
            }
            scanProgress = (batchEnd, audioFiles.count)
        }
    }

    func rescan(_ folder: URL) async {
        let audioFiles = Self.discoverAudioFiles(in: folder)
        let currentFileURLs = Set(audioFiles.map { $0.absoluteString })
        let storedURLs = (try? db.allFileURLs()) ?? []

        let removedURLs = storedURLs.subtracting(currentFileURLs)
        if !removedURLs.isEmpty {
            try? db.deleteTracksWithURLs(removedURLs)
        }

        await scanFolder(folder)
    }

    nonisolated static func extractMetadata(from url: URL) async -> Track? {
        let asset = AVURLAsset(url: url)

        var title = url.deletingPathExtension().lastPathComponent
        var artist = "Unknown"
        var albumArtist = "Unknown"
        var album = "Unknown"
        var genre = ""
        var year: Int?
        var trackNumber: Int?
        var discNumber: Int?
        var bpm: Int?
        var composer = ""
        do {
            let metadata = try await asset.load(.metadata)

            for item in metadata {
                guard let key = item.commonKey else { continue }
                switch key {
                case .commonKeyTitle:
                    title = (try? await item.load(.stringValue)) ?? title
                case .commonKeyArtist:
                    let val = (try? await item.load(.stringValue)) ?? "Unknown"
                    artist = val
                    if albumArtist == "Unknown" { albumArtist = val }
                case .commonKeyAlbumName:
                    album = (try? await item.load(.stringValue)) ?? "Unknown"
                case .commonKeyCreator:
                    composer = (try? await item.load(.stringValue)) ?? ""
                default:
                    break
                }
            }

            // Format-specific metadata (ID3 / iTunes)
            for item in metadata {
                if let identifier = item.identifier {
                    switch identifier {
                    case .id3MetadataContentType, .iTunesMetadataUserGenre:
                        genre = (try? await item.load(.stringValue)) ?? genre
                    case .id3MetadataTrackNumber:
                        if let val = try? await item.load(.stringValue),
                           let num = Int(val.split(separator: "/").first ?? "") {
                            trackNumber = num
                        }
                    case .id3MetadataPartOfASet:
                        if let val = try? await item.load(.stringValue),
                           let num = Int(val.split(separator: "/").first ?? "") {
                            discNumber = num
                        }
                    case .id3MetadataBeatsPerMinute:
                        if let val = try? await item.load(.stringValue) {
                            bpm = Int(val)
                        }
                    case .id3MetadataBand:
                        albumArtist = (try? await item.load(.stringValue)) ?? albumArtist
                    case .id3MetadataYear:
                        if let val = try? await item.load(.stringValue) {
                            year = Int(val)
                        }
                    default:
                        break
                    }
                }
            }

            let duration = try await asset.load(.duration)
            let durationSeconds = CMTimeGetSeconds(duration)

            var bitrate: Int?
            var sampleRate: Int?
            if let audioTrack = try await asset.loadTracks(withMediaType: .audio).first {
                let estimatedRate = try await audioTrack.load(.estimatedDataRate)
                bitrate = Int(estimatedRate / 1000)
                let descriptions = try await audioTrack.load(.formatDescriptions)
                if let desc = descriptions.first {
                    if let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(desc) {
                        sampleRate = Int(asbd.pointee.mSampleRate)
                    }
                }
            }

            let attrs = try FileManager.default.attributesOfItem(atPath: url.path)
            let fileSize = attrs[.size] as? Int64 ?? 0
            let modDate = attrs[.modificationDate] as? Date ?? Date()

            return Track(
                fileURL: url.absoluteString,
                title: title,
                artist: artist,
                albumArtist: albumArtist,
                album: album,
                genre: genre,
                year: year,
                trackNumber: trackNumber,
                discNumber: discNumber,
                duration: durationSeconds.isNaN ? 0 : durationSeconds,
                bpm: bpm,
                composer: composer,
                fileFormat: url.pathExtension.lowercased(),
                bitrate: bitrate,
                sampleRate: sampleRate,
                fileSize: fileSize,
                playCount: 0,
                lastPlayedAt: nil,
                rating: 0,
                dateAdded: Date(),
                dateModified: modDate,
                fileHash: Track.computeHash(fileSize: fileSize, modificationDate: modDate)
            )
        } catch {
            print("Failed to extract metadata from \(url.lastPathComponent): \(error)")
            return nil
        }
    }
}
