import AVFoundation
import Foundation

/// Reads audio file metadata (ID3 tags, etc.) using AVFoundation.
struct MetadataService {

    struct AudioMetadata {
        var title: String
        var artist: String
        var album: String
        var genre: String
        var year: Int?
        var duration: TimeInterval
        var sampleRate: Double
        var bitDepth: Int
        var channels: Int
        var fileFormat: String
        var fileSizeBytes: Int64
    }

    // MARK: - Read Metadata

    static func readMetadata(from url: URL) async throws -> AudioMetadata {
        // OGG/Opus files need special handling since AVFoundation may not support them
        let ext = url.pathExtension.lowercased()
        if ext == "ogg" {
            return readOGGMetadata(from: url)
        }
        #if !DISABLE_OPUS
        if ext == "opus" {
            return readOpusMetadata(from: url)
        }
        #endif

        let asset = AVURLAsset(url: url)

        let metadata = try await asset.load(.metadata)
        let duration = try await asset.load(.duration)

        var title = url.deletingPathExtension().lastPathComponent
        var artist = ""
        var album = ""
        var genre = ""
        var year: Int?

        for item in metadata {
            guard let key = item.commonKey else { continue }
            switch key {
            case .commonKeyTitle:
                if let val = try? await item.load(.stringValue) { title = val }
            case .commonKeyArtist:
                if let val = try? await item.load(.stringValue) { artist = val }
            case .commonKeyAlbumName:
                if let val = try? await item.load(.stringValue) { album = val }
            case .commonKeyType:
                if let val = try? await item.load(.stringValue) { genre = val }
            case .commonKeyCreationDate:
                if let val = try? await item.load(.stringValue) {
                    year = Int(val.prefix(4))
                }
            default:
                break
            }
        }

        // Also check format-specific date tags if year not found via common keys.
        // AVFoundation identifiers vary by format:
        //   ID3v2: "id3/%00TDRC", "id3/%00TYER", "id3/%00TDAT", "id3/%00DATE"
        //   iTunes/M4A: "itsk/©day"
        //   Vorbis/FLAC: "org.xiph.vorbiscomment/DATE"
        if year == nil {
            for item in metadata {
                if let val = try? await item.load(.stringValue),
                   let y = Int(val.prefix(4)),
                   y > 1900 && y < 2100 {
                    // Check by identifier
                    if let identifier = item.identifier {
                        let idStr = identifier.rawValue.uppercased()
                        if idStr.contains("TDRC") || idStr.contains("TYER") || idStr.contains("DATE")
                            || idStr.contains("YEAR") || idStr.contains("©DAY") || idStr.contains("DAY") {
                            year = y
                            break
                        }
                    }
                    // Also check by key space — some items don't have identifiers
                    // but have keys in their key space that indicate a date
                    if let key = item.key as? String {
                        let keyUpper = key.uppercased()
                        if keyUpper.contains("DATE") || keyUpper.contains("YEAR")
                            || keyUpper.contains("TDRC") || keyUpper.contains("TYER")
                            || keyUpper.contains("©DAY") || keyUpper == "DAY" {
                            year = y
                            break
                        }
                    }
                }
            }
        }

        // Final fallback: scan ALL metadata string values for anything that looks like a year
        if year == nil {
            for item in metadata {
                if let val = try? await item.load(.stringValue),
                   val.count >= 4 && val.count <= 10,
                   let y = Int(val.prefix(4)),
                   y > 1900 && y < 2100 {
                    // Only accept if the value looks like a pure date (not a random string containing digits)
                    let trimmed = val.trimmingCharacters(in: .whitespaces)
                    if trimmed.range(of: #"^\d{4}(-\d{2}(-\d{2})?)?$"#, options: .regularExpression) != nil {
                        year = y
                        break
                    }
                }
            }
        }

        let audioFile = try AVAudioFile(forReading: url)
        let format = audioFile.processingFormat
        let sampleRate = format.sampleRate
        let channels = Int(format.channelCount)
        let bitDepth: Int
        switch format.commonFormat {
        case .pcmFormatFloat32: bitDepth = 32
        case .pcmFormatFloat64: bitDepth = 64
        case .pcmFormatInt16: bitDepth = 16
        case .pcmFormatInt32: bitDepth = 32
        default: bitDepth = 16
        }

        let fileFormat = url.pathExtension.uppercased()
        let fileSize = (try? FileManager.default.attributesOfItem(atPath: url.path)[.size] as? Int64) ?? 0

        return AudioMetadata(
            title: title,
            artist: artist,
            album: album,
            genre: genre,
            year: year,
            duration: CMTimeGetSeconds(duration),
            sampleRate: sampleRate,
            bitDepth: bitDepth,
            channels: channels,
            fileFormat: fileFormat,
            fileSizeBytes: fileSize
        )
    }

    // MARK: - Supported Formats

    /// Dump all raw metadata from a file for diagnostic purposes.
    /// Returns a string listing every metadata item AVFoundation can see.
    static func dumpAllMetadata(from url: URL) async -> String {
        let ext = url.pathExtension.lowercased()
        
        if ext == "ogg" {
            return dumpOGGTags(from: url)
        }
        #if !DISABLE_OPUS
        if ext == "opus" {
            return dumpOpusTags(from: url)
        }
        #endif

        var lines: [String] = ["=== Metadata for \(url.lastPathComponent) ==="]
        do {
            let asset = AVURLAsset(url: url)
            let metadata = try await asset.load(.metadata)
            lines.append("Total items: \(metadata.count)")
            for (i, item) in metadata.enumerated() {
                let commonKey = item.commonKey?.rawValue ?? "(none)"
                let identifier = item.identifier?.rawValue ?? "(none)"
                let keySpace = item.keySpace?.rawValue ?? "(none)"
                let key = (item.key as? String) ?? String(describing: item.key)
                let value = (try? await item.load(.stringValue)) ?? "(nil)"
                lines.append("[\(i)] commonKey=\(commonKey) id=\(identifier) keySpace=\(keySpace) key=\(key) value=\(value)")
            }
        } catch {
            lines.append("Error: \(error)")
        }
        return lines.joined(separator: "\n")
    }

    private static func dumpOGGTags(from url: URL) -> String {
        var lines: [String] = ["=== OGG Tags for \(url.lastPathComponent) ==="]
        var error: Int32 = 0
        guard let vorbis = stb_vorbis_open_filename(url.path, &error, nil) else {
            lines.append("Failed to open OGG file (error \(error))")
            return lines.joined(separator: "\n")
        }
        defer { stb_vorbis_close(vorbis) }
        let comment = stb_vorbis_get_comment(vorbis)
        lines.append("Vendor: \(comment.vendor.map { String(cString: $0) } ?? "(nil)")")
        lines.append("Comment count: \(comment.comment_list_length)")
        if let commentList = comment.comment_list {
            for i in 0..<Int(comment.comment_list_length) {
                if let cStr = commentList[i] {
                    lines.append("  [\(i)] \(String(cString: cStr))")
                }
            }
        }
        return lines.joined(separator: "\n")
    }

    #if !DISABLE_OPUS
    private static func dumpOpusTags(from url: URL) -> String {
        var lines: [String] = ["=== Opus Tags for \(url.lastPathComponent) ==="]
        var error: Int32 = 0
        guard let opusFile = op_open_file(url.path, &error) else {
            lines.append("Failed to open Opus file (error \(error))")
            return lines.joined(separator: "\n")
        }
        defer { op_free(opusFile) }
        if let tags = op_tags(opusFile, -1) {
            let vendor = tags.pointee.vendor.map { String(cString: $0) } ?? "(nil)"
            lines.append("Vendor: \(vendor)")
            let count = tags.pointee.comments
            lines.append("Comment count: \(count)")
            for i in 0..<Int(count) {
                if let lengths = tags.pointee.comment_lengths,
                   let comments = tags.pointee.user_comments,
                   let cStr = comments[Int(i)] {
                    let len = Int(lengths[Int(i)])
                    let str = String(cString: cStr)
                    lines.append("  [\(i)] (len=\(len)) \(str)")
                }
            }
        } else {
            lines.append("No tags found")
        }
        return lines.joined(separator: "\n")
    }
    #endif

    static let supportedExtensions: Set<String> = [
        "mp3", "wav", "aif", "aiff", "flac", "m4a", "aac", "caf", "alac", "ogg", "opus"
    ]

    static func isSupportedAudioFile(_ url: URL) -> Bool {
        supportedExtensions.contains(url.pathExtension.lowercased())
    }

    // MARK: - OGG Metadata

    private static func readOGGMetadata(from url: URL) -> AudioMetadata {
        let fallbackTitle = url.deletingPathExtension().lastPathComponent
        let fileSize = (try? FileManager.default.attributesOfItem(atPath: url.path)[.size] as? Int64) ?? 0

        var error: Int32 = 0
        guard let vorbis = stb_vorbis_open_filename(url.path, &error, nil) else {
            return AudioMetadata(
                title: fallbackTitle, artist: "", album: "", genre: "", year: nil,
                duration: 0, sampleRate: 44100, bitDepth: 16,
                channels: 2, fileFormat: "OGG", fileSizeBytes: fileSize
            )
        }
        defer { stb_vorbis_close(vorbis) }

        let info = stb_vorbis_get_info(vorbis)
        let totalSamples = stb_vorbis_stream_length_in_samples(vorbis)
        let sampleRate = Double(info.sample_rate)
        let channels = Int(info.channels)
        let duration = sampleRate > 0 ? Double(totalSamples) / sampleRate : 0

        // Read Vorbis Comment tags
        var title = fallbackTitle
        var artist = ""
        var album = ""
        var genre = ""
        var year: Int?

        let comment = stb_vorbis_get_comment(vorbis)
        if comment.comment_list_length > 0, let commentList = comment.comment_list {
            for i in 0..<Int(comment.comment_list_length) {
                guard let cStr = commentList[i] else { continue }
                let entry = String(cString: cStr)
                // Vorbis comments are "TAG=VALUE" format (case-insensitive tag names)
                guard let eqIndex = entry.firstIndex(of: "=") else { continue }
                let tag = entry[..<eqIndex].uppercased()
                let value = String(entry[entry.index(after: eqIndex)...])
                guard !value.isEmpty else { continue }

                switch tag {
                case "TITLE": title = value
                case "ARTIST": artist = value
                case "ALBUM": album = value
                case "GENRE": genre = value
                case "DATE", "YEAR", "ORIGINALDATE", "ORIGINALYEAR":
                    if year == nil { year = Int(value.prefix(4)) }
                default: break
                }
            }
        }

        return AudioMetadata(
            title: title, artist: artist, album: album, genre: genre, year: year,
            duration: duration, sampleRate: sampleRate, bitDepth: 16,
            channels: channels, fileFormat: "OGG", fileSizeBytes: fileSize
        )
    }

    #if !DISABLE_OPUS
    private static func readOpusMetadata(from url: URL) -> AudioMetadata {
        let fallbackTitle = url.deletingPathExtension().lastPathComponent
        let fileSize = (try? FileManager.default.attributesOfItem(atPath: url.path)[.size] as? Int64) ?? 0

        var error: Int32 = 0
        guard let opusFile = op_open_file(url.path, &error) else {
            return AudioMetadata(
                title: fallbackTitle, artist: "", album: "", genre: "", year: nil,
                duration: 0, sampleRate: 48000, bitDepth: 16,
                channels: 2, fileFormat: "OPUS", fileSizeBytes: fileSize
            )
        }
        defer { op_free(opusFile) }

        let channels = Int(op_channel_count(opusFile, -1))
        let totalSamples = op_pcm_total(opusFile, -1)
        let duration = Double(totalSamples) / 48000.0

        // Read Vorbis Comment tags
        var title = fallbackTitle
        var artist = ""
        var album = ""
        var genre = ""
        var year: Int?

        if let tags = op_tags(opusFile, -1) {
            title = opusTagValue(tags, "TITLE") ?? fallbackTitle
            artist = opusTagValue(tags, "ARTIST") ?? ""
            album = opusTagValue(tags, "ALBUM") ?? ""
            genre = opusTagValue(tags, "GENRE") ?? ""
            if let dateStr = opusTagValue(tags, "DATE") ?? opusTagValue(tags, "YEAR")
                ?? opusTagValue(tags, "ORIGINALDATE") ?? opusTagValue(tags, "ORIGINALYEAR") {
                year = Int(dateStr.prefix(4))
            }
        }

        return AudioMetadata(
            title: title, artist: artist, album: album, genre: genre, year: year,
            duration: duration, sampleRate: 48000, bitDepth: 16,
            channels: channels, fileFormat: "OPUS", fileSizeBytes: fileSize
        )
    }

    /// Read a single Vorbis Comment tag from an OpusTags struct.
    private static func opusTagValue(_ tags: UnsafePointer<OpusTags>, _ tag: String) -> String? {
        let count = opus_tags_query_count(tags, tag)
        guard count > 0 else { return nil }
        guard let value = opus_tags_query(tags, tag, 0) else { return nil }
        let str = String(cString: value)
        return str.isEmpty ? nil : str
    }
    #endif
}
