import Foundation
import GRDB

// `nonisolated` opts this struct out of the project-wide `default-isolation = MainActor`.
nonisolated struct MonthlyCount: Sendable {
    let month: Date
    let count: Int
}

// `nonisolated` opts this class out of the project-wide `default-isolation = MainActor`
// setting so database operations run off the main actor and can be used from Sendable contexts.
nonisolated final class DatabaseService: Sendable {
    let dbPool: DatabaseWriter

    init(path: String) throws {
        let dbPool = try DatabasePool(path: path)
        self.dbPool = dbPool
        try Self.migrate(dbPool)
    }

    init(inMemory: Bool) throws {
        let dbQueue = try DatabaseQueue()
        self.dbPool = dbQueue
        try Self.migrate(dbQueue)
    }

    private static func migrate(_ db: DatabaseWriter) throws {
        var migrator = DatabaseMigrator()

        migrator.registerMigration("v1-create-tracks") { db in
            try db.create(table: "tracks") { t in
                t.autoIncrementedPrimaryKey("id")
                t.column("fileURL", .text).notNull().unique()
                t.column("title", .text).notNull()
                t.column("artist", .text).notNull()
                t.column("albumArtist", .text).notNull()
                t.column("album", .text).notNull()
                t.column("genre", .text).notNull()
                t.column("year", .integer)
                t.column("trackNumber", .integer)
                t.column("discNumber", .integer)
                t.column("duration", .double).notNull()
                t.column("bpm", .integer)
                t.column("composer", .text).notNull()
                t.column("fileFormat", .text).notNull()
                t.column("bitrate", .integer)
                t.column("sampleRate", .integer)
                t.column("fileSize", .integer).notNull()
                t.column("artworkData", .blob)
                t.column("playCount", .integer).notNull().defaults(to: 0)
                t.column("lastPlayedAt", .datetime)
                t.column("rating", .integer).notNull().defaults(to: 0)
                t.column("dateAdded", .datetime).notNull()
                t.column("dateModified", .datetime).notNull()
                t.column("fileHash", .text).notNull()
            }

            try db.create(index: "idx_tracks_artist", on: "tracks", columns: ["artist"])
            try db.create(index: "idx_tracks_album", on: "tracks", columns: ["album"])
            try db.create(index: "idx_tracks_genre", on: "tracks", columns: ["genre"])
            try db.create(index: "idx_tracks_year", on: "tracks", columns: ["year"])
            try db.create(
                index: "idx_tracks_album_order",
                on: "tracks",
                columns: ["albumArtist", "album", "discNumber", "trackNumber"]
            )

            try db.create(virtualTable: "tracks_ft", using: FTS5()) { t in
                t.synchronize(withTable: "tracks")
                t.tokenizer = .unicode61()
                t.column("title")
                t.column("artist")
                t.column("albumArtist")
                t.column("album")
                t.column("genre")
                t.column("composer")
            }
        }

        migrator.registerMigration("v2-create-playlists") { db in
            try db.create(table: "playlists") { t in
                t.autoIncrementedPrimaryKey("id")
                t.column("name", .text).notNull().unique()
                t.column("createdAt", .datetime).notNull()
            }

            try db.create(table: "playlist_tracks") { t in
                t.autoIncrementedPrimaryKey("id")
                t.column("playlistId", .integer).notNull()
                    .references("playlists", onDelete: .cascade)
                t.column("trackId", .integer).notNull()
                    .references("tracks", onDelete: .cascade)
                t.column("position", .integer).notNull()
                t.uniqueKey(["playlistId", "trackId"])
            }

            try db.create(
                index: "idx_playlist_tracks_order",
                on: "playlist_tracks",
                columns: ["playlistId", "position"]
            )
        }

        migrator.registerMigration("v3-create-smart-playlists") { db in
            try db.create(table: "smart_playlists") { t in
                t.autoIncrementedPrimaryKey("id")
                t.column("name", .text).notNull().unique()
                t.column("searchQuery", .text).notNull()
                t.column("createdAt", .datetime).notNull()
            }
        }

        migrator.registerMigration("v4-drop-artworkData") { db in
            try db.alter(table: "tracks") { t in
                t.drop(column: "artworkData")
            }
        }

        try migrator.migrate(db)
    }

    // MARK: - Maintenance

    /// Create a self-contained copy of the database at the given path using
    /// SQLite's online backup API. The copy includes all WAL data and is safe
    /// to serve or transfer without additional files.
    func backup(to destinationPath: String) throws {
        try dbPool.backup(to: DatabaseQueue(path: destinationPath))
    }

    // MARK: - Write

    func insert(_ track: inout Track) throws {
        try dbPool.write { db in
            try track.insert(db)
        }
    }

    func insertBatch(_ tracks: [Track]) throws {
        try dbPool.write { db in
            for var track in tracks {
                try track.insert(db, onConflict: .ignore)
            }
        }
    }

    func updatePlayStats(trackId: Int64, playCount: Int, lastPlayedAt: Date) throws {
        try dbPool.write { db in
            try db.execute(
                sql: "UPDATE tracks SET playCount = ?, lastPlayedAt = ? WHERE id = ?",
                arguments: [playCount, lastPlayedAt, trackId]
            )
        }
    }

    func deleteTracksWithURLs(_ urls: Set<String>) throws {
        try dbPool.write { db in
            let placeholders = databaseQuestionMarks(count: urls.count)
            try db.execute(
                sql: "DELETE FROM tracks WHERE fileURL IN (\(placeholders))",
                arguments: StatementArguments(Array(urls))
            )
        }
    }

    // MARK: - Read

    private static let validSortColumns: Set<String> = [
        "title", "artist", "albumArtist", "album", "genre", "year", "duration",
        "trackNumber", "dateAdded", "playCount", "rating", "bpm"
    ]

    func fetchTracks(search: String, sortColumn: String, ascending: Bool) throws -> [Track] {
        try dbPool.read { db in
            try self.fetchTracks(db: db, search: search, sortColumn: sortColumn, ascending: ascending)
        }
    }

    // Used by ValueObservation which already holds a Database access
    func fetchTracks(db: Database, search: String, sortColumn: String, ascending: Bool) throws -> [Track] {
        let col = Self.validSortColumns.contains(sortColumn) ? sortColumn : "title"
        let order = ascending ? "ASC" : "DESC"

        if search.trimmingCharacters(in: .whitespaces).isEmpty {
            return try Track.fetchAll(
                db,
                sql: "SELECT * FROM tracks ORDER BY \(col) COLLATE NOCASE \(order)"
            )
        }

        let terms = search.split(separator: " ").map { "\($0)*" }
        let pattern = terms.joined(separator: " ")

        return try Track.fetchAll(
            db,
            sql: """
                SELECT tracks.* FROM tracks
                JOIN tracks_ft ON tracks_ft.rowid = tracks.id
                WHERE tracks_ft MATCH ?
                ORDER BY \(col) COLLATE NOCASE \(order)
            """,
            arguments: [pattern]
        )
    }

    func allFileURLs() throws -> Set<String> {
        try dbPool.read { db in
            let urls = try String.fetchAll(db, sql: "SELECT fileURL FROM tracks")
            return Set(urls)
        }
    }

    func trackCount() throws -> Int {
        try dbPool.read { db in
            try Track.fetchCount(db)
        }
    }

    func fetchTracksByIds(_ ids: [Int64]) throws -> [Track] {
        guard !ids.isEmpty else { return [] }
        let tracks = try dbPool.read { db in
            let placeholders = databaseQuestionMarks(count: ids.count)
            return try Track.fetchAll(
                db,
                sql: "SELECT * FROM tracks WHERE id IN (\(placeholders))",
                arguments: StatementArguments(ids)
            )
        }
        let trackMap = Dictionary(uniqueKeysWithValues: tracks.compactMap { t in t.id.map { ($0, t) } })
        return ids.compactMap { trackMap[$0] }
    }

    func fetchRecentlyAdded(limit: Int) throws -> [Track] {
        try dbPool.read { db in
            try Track.fetchAll(
                db,
                sql: "SELECT * FROM tracks ORDER BY dateAdded DESC LIMIT ?",
                arguments: [limit]
            )
        }
    }

    func totalDuration() throws -> Double {
        try dbPool.read { db in
            try Double.fetchOne(db, sql: "SELECT COALESCE(SUM(duration), 0) FROM tracks") ?? 0
        }
    }

    func fetchMonthlyAdditions(months: Int) throws -> [MonthlyCount] {
        // Use a UTC calendar throughout: GRDB stores Date values as UTC ISO8601 strings,
        // so SQLite's strftime('%Y-%m', …) returns UTC months. We align Swift's month
        // boundaries to UTC as well to ensure consistent bucketing regardless of the
        // device's local timezone.
        var utcCalendar = Calendar(identifier: .gregorian)
        utcCalendar.timeZone = TimeZone(identifier: "UTC")!
        let now = Date()
        let thisMonth = utcCalendar.dateInterval(of: .month, for: now)!.start
        let startMonth = utcCalendar.date(byAdding: .month, value: -(months - 1), to: thisMonth)!

        let rows: [(String, Int)] = try dbPool.read { db in
            let rows = try Row.fetchAll(
                db,
                sql: """
                    SELECT strftime('%Y-%m', dateAdded) AS month, COUNT(*) AS cnt
                    FROM tracks
                    WHERE dateAdded >= ?
                    GROUP BY month
                    ORDER BY month ASC
                """,
                arguments: [startMonth]
            )
            return rows.map { ($0["month"] as String, $0["cnt"] as Int) }
        }

        let rowDict = Dictionary(uniqueKeysWithValues: rows)
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM"
        formatter.timeZone = TimeZone(identifier: "UTC")!

        var results: [MonthlyCount] = []
        for i in 0..<months {
            let month = utcCalendar.date(byAdding: .month, value: i, to: startMonth)!
            let key = formatter.string(from: month)
            let count = rowDict[key] ?? 0
            results.append(MonthlyCount(month: month, count: count))
        }
        return results
    }

    // MARK: - Playlists

    func createPlaylist(name: String) throws -> Playlist {
        try dbPool.write { db in
            var playlist = Playlist(id: nil, name: name, createdAt: Date())
            try playlist.insert(db)
            return playlist
        }
    }

    func renamePlaylist(id: Int64, name: String) throws {
        try dbPool.write { db in
            try db.execute(
                sql: "UPDATE playlists SET name = ? WHERE id = ?",
                arguments: [name, id]
            )
        }
    }

    func deletePlaylist(id: Int64) throws {
        try dbPool.write { db in
            try db.execute(sql: "DELETE FROM playlists WHERE id = ?", arguments: [id])
        }
    }

    func fetchPlaylists() throws -> [Playlist] {
        try dbPool.read { db in
            try Playlist.fetchAll(db, sql: "SELECT * FROM playlists ORDER BY name COLLATE NOCASE ASC")
        }
    }

    func fetchPlaylists(db: Database) throws -> [Playlist] {
        try Playlist.fetchAll(db, sql: "SELECT * FROM playlists ORDER BY name COLLATE NOCASE ASC")
    }

    func addTrackToPlaylist(trackId: Int64, playlistId: Int64) throws {
        try dbPool.write { db in
            let maxPos = try Int.fetchOne(
                db,
                sql: "SELECT MAX(position) FROM playlist_tracks WHERE playlistId = ?",
                arguments: [playlistId]
            ) ?? -1
            var entry = PlaylistTrack(
                id: nil, playlistId: playlistId, trackId: trackId, position: maxPos + 1
            )
            try entry.insert(db, onConflict: .ignore)
        }
    }

    func removeTrackFromPlaylist(trackId: Int64, playlistId: Int64) throws {
        try dbPool.write { db in
            let position = try Int.fetchOne(
                db,
                sql: "SELECT position FROM playlist_tracks WHERE playlistId = ? AND trackId = ?",
                arguments: [playlistId, trackId]
            )
            try db.execute(
                sql: "DELETE FROM playlist_tracks WHERE playlistId = ? AND trackId = ?",
                arguments: [playlistId, trackId]
            )
            if let pos = position {
                try db.execute(
                    sql: """
                        UPDATE playlist_tracks SET position = position - 1
                        WHERE playlistId = ? AND position > ?
                    """,
                    arguments: [playlistId, pos]
                )
            }
        }
    }

    func reorderPlaylistTrack(playlistId: Int64, fromPosition: Int, toPosition: Int) throws {
        try dbPool.write { db in
            let trackId = try Int64.fetchOne(
                db,
                sql: "SELECT trackId FROM playlist_tracks WHERE playlistId = ? AND position = ?",
                arguments: [playlistId, fromPosition]
            )
            guard let trackId else { return }

            if fromPosition < toPosition {
                try db.execute(
                    sql: """
                        UPDATE playlist_tracks SET position = position - 1
                        WHERE playlistId = ? AND position > ? AND position <= ?
                    """,
                    arguments: [playlistId, fromPosition, toPosition]
                )
            } else {
                try db.execute(
                    sql: """
                        UPDATE playlist_tracks SET position = position + 1
                        WHERE playlistId = ? AND position >= ? AND position < ?
                    """,
                    arguments: [playlistId, toPosition, fromPosition]
                )
            }

            try db.execute(
                sql: """
                    UPDATE playlist_tracks SET position = ?
                    WHERE playlistId = ? AND trackId = ?
                """,
                arguments: [toPosition, playlistId, trackId]
            )
        }
    }

    func fetchPlaylistTracks(playlistId: Int64, search: String = "") throws -> [Track] {
        try dbPool.read { db in
            try self.fetchPlaylistTracks(db: db, playlistId: playlistId, search: search)
        }
    }

    func fetchPlaylistTracks(db: Database, playlistId: Int64, search: String = "") throws -> [Track] {
        if search.trimmingCharacters(in: .whitespaces).isEmpty {
            return try Track.fetchAll(
                db,
                sql: """
                    SELECT tracks.* FROM tracks
                    JOIN playlist_tracks ON playlist_tracks.trackId = tracks.id
                    WHERE playlist_tracks.playlistId = ?
                    ORDER BY playlist_tracks.position ASC
                """,
                arguments: [playlistId]
            )
        }

        let terms = search.split(separator: " ").map { "\($0)*" }
        let pattern = terms.joined(separator: " ")

        return try Track.fetchAll(
            db,
            sql: """
                SELECT tracks.* FROM tracks
                JOIN playlist_tracks ON playlist_tracks.trackId = tracks.id
                JOIN tracks_ft ON tracks_ft.rowid = tracks.id
                WHERE playlist_tracks.playlistId = ? AND tracks_ft MATCH ?
                ORDER BY playlist_tracks.position ASC
            """,
            arguments: [playlistId, pattern]
        )
    }

    func playlistName(id: Int64) throws -> String? {
        try dbPool.read { db in
            try String.fetchOne(
                db,
                sql: "SELECT name FROM playlists WHERE id = ?",
                arguments: [id]
            )
        }
    }

    // MARK: - Smart Playlists

    func createSmartPlaylist(name: String, searchQuery: String) throws -> SmartPlaylist {
        try dbPool.write { db in
            var smartPlaylist = SmartPlaylist(
                id: nil, name: name, searchQuery: searchQuery, createdAt: Date()
            )
            try smartPlaylist.insert(db)
            return smartPlaylist
        }
    }

    func renameSmartPlaylist(id: Int64, name: String) throws {
        try dbPool.write { db in
            try db.execute(
                sql: "UPDATE smart_playlists SET name = ? WHERE id = ?",
                arguments: [name, id]
            )
        }
    }

    func updateSmartPlaylistQuery(id: Int64, searchQuery: String) throws {
        try dbPool.write { db in
            try db.execute(
                sql: "UPDATE smart_playlists SET searchQuery = ? WHERE id = ?",
                arguments: [searchQuery, id]
            )
        }
    }

    func deleteSmartPlaylist(id: Int64) throws {
        try dbPool.write { db in
            try db.execute(sql: "DELETE FROM smart_playlists WHERE id = ?", arguments: [id])
        }
    }

    func fetchSmartPlaylists() throws -> [SmartPlaylist] {
        try dbPool.read { db in
            try SmartPlaylist.fetchAll(
                db, sql: "SELECT * FROM smart_playlists ORDER BY name COLLATE NOCASE ASC"
            )
        }
    }

    func fetchSmartPlaylists(db: Database) throws -> [SmartPlaylist] {
        try SmartPlaylist.fetchAll(
            db, sql: "SELECT * FROM smart_playlists ORDER BY name COLLATE NOCASE ASC"
        )
    }
}
