import Foundation
import AVFoundation
#if canImport(UIKit)
import UIKit
#endif
import MediaPlayer

enum PlaybackContext: String, Codable {
    case primary
    case hooks
}

private struct SnapshotSong: Codable {
    let id: String
    let name: String
    let artistName: String
    let artistId: String
    let createdAt: Date
    let imageURL: String?
    let audioURL: String?
    let rootRemixSongId: String?
    let isCreatedByCurrentUser: Bool
}

private struct PrimaryPlaybackSnapshot: Codable {
    let playlist: [SnapshotSong]
    let currentIndex: Int
    let currentTime: Double
    let wasPlaying: Bool
}

@Observable
class AudioManager {
    static let shared = AudioManager()

    var currentlyPlayingSong: UploadedSong?
    var isCurrentlyPlaying: Bool = false
    var isBuffering: Bool = false
    var showBufferingIndicator: Bool = false
    var playbackContext: PlaybackContext = .primary

    // Playback progress is separated into its own object to prevent
    // unnecessary UI re-renders (updates 10x per second while playing)
    let progress = PlaybackProgress()
    
    // Playlist management
    var currentPlaylist: [UploadedSong] = []
    var currentPlaylistIndex: Int = 0
    
    var hasNextSong: Bool {
        return currentPlaylistIndex < currentPlaylist.count - 1
    }

    var hasPreviousSong: Bool {
        return currentPlaylistIndex > 0
    }

    private var player: AVPlayer?
    private var timeObserverToken: Any?
    private var endObserver: NSObjectProtocol?
    private var wasPlayingBeforeScrubbing: Bool = false
    private var originalVolume: Float = 1.0
    private var wasManuallyPaused: Bool = false

    // Debug observers for tracking loading states
    private var statusObserver: NSKeyValueObservation?
    private var bufferEmptyObserver: NSKeyValueObservation?
    private var bufferLikelyToKeepUpObserver: NSKeyValueObservation?
    private var bufferFullObserver: NSKeyValueObservation?
    private var timeControlStatusObserver: NSKeyValueObservation?
    private var rateObserver: NSKeyValueObservation?

    // Buffering indicator debounce
    private var showBufferingTask: Task<Void, Never>?
    private var hideBufferingTask: Task<Void, Never>?

    // Preloading for hooks playback
    private var preloadedItems: [Int: AVPlayerItem] = [:]
    private let maxPreloadedItems = 3

    // Primary playback snapshot (for restoring after hooks / app relaunch)
    private var primarySnapshot: PrimaryPlaybackSnapshot?
    private let snapshotDefaultsKey = "AudioManager.PrimaryPlaybackSnapshot"

    // Now Playing / Remote Commands
    #if os(iOS)
    private var nowPlayingInfo: [String: Any] = [:]
    private let artworkCache = NSCache<NSString, UIImage>()
    #endif

    private init() {
#if os(iOS)
        let session = AVAudioSession.sharedInstance()
        do {
            try session.setCategory(.playback, mode: .default, options: [])
            try session.setActive(true)
        } catch {
            print("Audio session error: \(error)")
        }
        configureRemoteCommands()
        subscribeToAudioSessionNotifications()
#endif
        restorePrimarySnapshotIfAvailable()
    }
    
    // MARK: - Snapshot Management

    private func capturePrimarySnapshotIfNeeded() {
        // Only capture when we are in primary context with an active playlist
        guard playbackContext == .primary,
              !currentPlaylist.isEmpty,
              currentPlaylistIndex >= 0,
              currentPlaylistIndex < currentPlaylist.count else {
            return
        }

        let snapshotSongs: [SnapshotSong] = currentPlaylist.map { song in
            SnapshotSong(
                id: song.id,
                name: song.name,
                artistName: song.artistName,
                artistId: song.artistId,
                createdAt: song.createdAt,
                imageURL: song.imageURL,
                audioURL: song.audioURL,
                rootRemixSongId: song.rootRemixSongId,
                isCreatedByCurrentUser: song.isCreatedByCurrentUser
            )
        }

        let snapshot = PrimaryPlaybackSnapshot(
            playlist: snapshotSongs,
            currentIndex: currentPlaylistIndex,
            currentTime: progress.currentTime,
            wasPlaying: isCurrentlyPlaying
        )

        primarySnapshot = snapshot
        persistPrimarySnapshot()
        print("💾 Captured primary playback snapshot (index: \(snapshot.currentIndex), time: \(snapshot.currentTime), wasPlaying: \(snapshot.wasPlaying))")
    }

    private func persistPrimarySnapshot() {
        guard let snapshot = primarySnapshot else {
            UserDefaults.standard.removeObject(forKey: snapshotDefaultsKey)
            return
        }

        do {
            let data = try JSONEncoder().encode(snapshot)
            UserDefaults.standard.set(data, forKey: snapshotDefaultsKey)
        } catch {
            print("❌ Failed to encode primary playback snapshot: \(error)")
        }
    }

    private func restorePrimarySnapshotIfAvailable() {
        guard let data = UserDefaults.standard.data(forKey: snapshotDefaultsKey) else {
            return
        }

        do {
            let snapshot = try JSONDecoder().decode(PrimaryPlaybackSnapshot.self, from: data)
            primarySnapshot = snapshot

            // Initialize state but do not auto-play; user interaction should start playback
            let restoredSongs: [UploadedSong] = snapshot.playlist.map { snapshotSong in
                UploadedSong(
                    id: snapshotSong.id,
                    name: snapshotSong.name,
                    artistName: snapshotSong.artistName,
                    artistId: snapshotSong.artistId,
                    createdAt: snapshotSong.createdAt,
                    imageURL: snapshotSong.imageURL,
                    audioURL: snapshotSong.audioURL,
                    originalPrompt: nil,
                    rewrittenPrompt: nil,
                    rootRemixSongId: snapshotSong.rootRemixSongId,
                    isCreatedByCurrentUser: snapshotSong.isCreatedByCurrentUser
                )
            }

            guard !restoredSongs.isEmpty,
                  snapshot.currentIndex >= 0,
                  snapshot.currentIndex < restoredSongs.count else {
                print("❌ Snapshot invalid or empty when restoring primary playback")
                return
            }

            currentPlaylist = restoredSongs
            currentPlaylistIndex = snapshot.currentIndex
            currentlyPlayingSong = restoredSongs[snapshot.currentIndex]
            progress.currentTime = snapshot.currentTime
            progress.currentProgress = 0.0 // will be updated once playback starts
            progress.totalDuration = 0.0
            playbackContext = .primary

            print("🔁 Restored primary playback snapshot (index: \(snapshot.currentIndex), time: \(snapshot.currentTime), wasPlaying: \(snapshot.wasPlaying))")
        } catch {
            print("❌ Failed to decode primary playback snapshot: \(error)")
        }
    }

    func restorePrimaryPlaybackFromSnapshot(autoResume: Bool = false) {
        guard let snapshot = primarySnapshot else { return }

        playbackContext = .primary

        let restoredSongs: [UploadedSong] = snapshot.playlist.map { snapshotSong in
            UploadedSong(
                id: snapshotSong.id,
                name: snapshotSong.name,
                artistName: snapshotSong.artistName,
                artistId: snapshotSong.artistId,
                createdAt: snapshotSong.createdAt,
                imageURL: snapshotSong.imageURL,
                audioURL: snapshotSong.audioURL,
                originalPrompt: nil,
                rewrittenPrompt: nil,
                rootRemixSongId: snapshotSong.rootRemixSongId,
                isCreatedByCurrentUser: snapshotSong.isCreatedByCurrentUser
            )
        }

        guard !restoredSongs.isEmpty,
              snapshot.currentIndex >= 0,
              snapshot.currentIndex < restoredSongs.count else {
            print("❌ Snapshot invalid or empty when restoring primary playback for playback")
            return
        }

        currentPlaylist = restoredSongs
        currentPlaylistIndex = snapshot.currentIndex

        let songToPlay = restoredSongs[snapshot.currentIndex]
        playAudio(song: songToPlay, seekTo: snapshot.currentTime, shouldStartPlaying: autoResume && snapshot.wasPlaying)

        print("▶️ Restored primary playback from snapshot (autoResume: \(autoResume), wasPlaying: \(snapshot.wasPlaying))")
    }

    func clearPrimarySnapshot() {
        primarySnapshot = nil
        persistPrimarySnapshot()
    }

    // MARK: - Hooks Context Management

    func playHooksPlaylist(_ songs: [UploadedSong], startingAt index: Int = 0) {
        guard !songs.isEmpty && index >= 0 && index < songs.count else {
            print("❌ Invalid hooks playlist or index")
            return
        }

        // If we are currently in primary context, capture snapshot before switching
        if playbackContext == .primary {
            capturePrimarySnapshotIfNeeded()
        }

        playbackContext = .hooks
        currentPlaylist = songs
        currentPlaylistIndex = index

        let songToPlay = songs[index]

        // If the song has a hooksStartPoint, seek to that position
        let startTime: Double? = if let hooksStartPoint = songToPlay.hooksStartPoint, TweaksManager.shared.isHooksStartAtChorusEnabled {
            Double(hooksStartPoint) - 0.2
        } else {
            nil
        }
        
        print("Start Time \(startTime)")

        print("🔍 [DEBUG] [hooks] playHooksPlaylist starting | songId: \(songToPlay.id) | hooksStartPoint: \(songToPlay.hooksStartPoint?.description ?? "nil") | calculated startTime: \(startTime?.description ?? "nil")")
        playAudio(song: songToPlay, seekTo: startTime, shouldStartPlaying: true)

        print("🎵 Started hooks playlist with \(songs.count) songs, starting at index \(index)")
    }

    func exitHooksContextAndRestorePrimary(autoResume: Bool = false) {
        guard playbackContext == .hooks else { return }

        // Stop hooks playback completely
        stopAudio(clearSnapshot: false)
        playbackContext = .primary

        // Clear preloaded items
        clearPreloadedItems()

        // Restore primary snapshot if available
        if primarySnapshot != nil {
            restorePrimaryPlaybackFromSnapshot(autoResume: autoResume)
        } else {
            print("ℹ️ No primary snapshot to restore after exiting hooks context")
        }
    }

    func stopHooksPlayback() {
        guard playbackContext == .hooks else { return }
        
        if TweaksManager.shared.isHooksBackgroundPlayEnabled {
            if TweaksManager.shared.isHooksBackgroundPlayStartPausedEnabled {
                pauseAudio()
            }
            return
        }

        stopAudio(clearSnapshot: false)
        // Keep playbackContext as .hooks so we can resume when returning from background

        // Clear preloaded items
        clearPreloadedItems()
    }

    // MARK: - Playlist Management
    
    func playPlaylist(_ songs: [UploadedSong], startingAt index: Int = 0) {
        guard !songs.isEmpty && index >= 0 && index < songs.count else {
            print("❌ Invalid playlist or index")
            return
        }
        
        currentPlaylist = songs
        currentPlaylistIndex = index
        
        // This is explicitly primary playback
        playbackContext = .primary

        // Capture snapshot for potential restoration across app sessions
        capturePrimarySnapshotIfNeeded()

        let songToPlay = songs[index]
        playAudio(song: songToPlay)
        
        print("🎵 Started playlist with \(songs.count) songs, starting at index \(index)")
    }
    
    func nextSong() {
        guard hasNextSong else {
            print("⏭️ No next song available")
            return
        }
        
        currentPlaylistIndex += 1
        let nextSong = currentPlaylist[currentPlaylistIndex]
        playAudio(song: nextSong)
        
        print("⏭️ Playing next song: \(nextSong.name) (index \(currentPlaylistIndex))")
    }
    
    func previousSong() {
        guard hasPreviousSong else {
            print("⏮️ No previous song available")
            return
        }
        
        currentPlaylistIndex -= 1
        let previousSong = currentPlaylist[currentPlaylistIndex]
        playAudio(song: previousSong)
        
        print("⏮️ Playing previous song: \(previousSong.name) (index \(currentPlaylistIndex))")
    }
    
    func playFromPlaylist(at index: Int) {
        guard index >= 0 && index < currentPlaylist.count else {
            print("❌ Invalid playlist index: \(index)")
            return
        }
        
        currentPlaylistIndex = index
        let songToPlay = currentPlaylist[index]
        playAudio(song: songToPlay)
        
        print("🎵 Playing song from playlist at index \(index): \(songToPlay.name)")
    }

    func playAudio(song: UploadedSong) {
        // In hooks context, use hooksStartPoint if available
        let seekTime: Double? = if playbackContext == .hooks, let hooksStartPoint = song.hooksStartPoint, UIApplication.shared.applicationState != .background, TweaksManager.shared.isHooksStartAtChorusEnabled {
            Double(hooksStartPoint) - 0.2
        } else {
            nil
        }
        print("Start Time \(seekTime)")
        playAudio(song: song, seekTo: seekTime, shouldStartPlaying: true)
    }

    private func playAudio(song: UploadedSong, seekTo: Double?, shouldStartPlaying: Bool) {
        // Stop any currently playing audio
        stopAudio(clearSnapshot: false)

        // Set the currently playing song
        currentlyPlayingSong = song
        progress.currentTime = 0.0
        progress.currentProgress = 0.0
        progress.totalDuration = 0.0

        // Try to use preloaded item if available, otherwise create new one
        let item: AVPlayerItem
        if playbackContext == .hooks, let preloadedItem = getPreloadedItem(at: currentPlaylistIndex) {
            item = preloadedItem
            print("✅ Using preloaded item for index \(currentPlaylistIndex)")
        } else {
            // Resolve the remote URL (assumes HTTP/HTTPS)
            guard let url = getAudioFileURL(for: song.audioURL) else {
                print("❌ Failed to get audio URL for: \(song.audioURL ?? "nil")")
                return
            }
            item = AVPlayerItem(url: url)
        }

        let player = AVPlayer(playerItem: item)
        player.automaticallyWaitsToMinimizeStalling = false
        self.player = player

        addProgressObservation()
        addEndOfPlaybackObserver(for: item)
        addDebugObservers(for: item, player: player)

        if let seekTo = seekTo, seekTo > 0 {
            let time = CMTime(seconds: seekTo, preferredTimescale: 600)
            print("Start Time \(time)")
            print("🔍 [DEBUG] [\(playbackContext)] About to seek to: \(seekTo)s | item.status: \(statusString(for: item.status)) | bufferEmpty: \(item.isPlaybackBufferEmpty) | likelyToKeepUp: \(item.isPlaybackLikelyToKeepUp)")

            player.seek(to: time, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero) { [weak self] finished in
                guard let self = self else { return }
                print("🔍 [DEBUG] [\(self.playbackContext)] Seek completed: finished=\(finished) | currentTime=\(CMTimeGetSeconds(player.currentTime())) | item.status: \(self.statusString(for: item.status)) | bufferEmpty: \(item.isPlaybackBufferEmpty) | likelyToKeepUp: \(item.isPlaybackLikelyToKeepUp)")
            }

            progress.currentTime = seekTo
        }

        if shouldStartPlaying {
            print("🔍 [DEBUG] [\(playbackContext)] Calling player.play() | item.status: \(statusString(for: item.status)) | bufferEmpty: \(item.isPlaybackBufferEmpty) | likelyToKeepUp: \(item.isPlaybackLikelyToKeepUp)")
            player.play()
            isCurrentlyPlaying = true
            wasManuallyPaused = false
        } else {
            print("🔍 [DEBUG] [\(playbackContext)] Not starting playback (shouldStartPlaying=false)")
            isCurrentlyPlaying = false
        }

        #if os(iOS)
        if playbackContext == .primary || TweaksManager.shared.isHooksBackgroundPlayEnabled {
            configureNowPlayingInfo(for: song)
            updateNowPlayingPlaybackState()
        } else if playbackContext == .hooks {
            MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
        }
        #endif

        print("🎵 Started streaming audio for songId: \(song.id) [context: \(playbackContext)]")

        // Preload next songs if in hooks context
        if playbackContext == .hooks {
            preloadNextHooksSongs()
        }
    }
    
    // AVPlayer streams remote audio directly; no local download required.
    
    func pauseAudio() {
        wasManuallyPaused = true
        player?.pause()
        isCurrentlyPlaying = false
        #if os(iOS)
        updateNowPlayingPlaybackState()
        #endif
        // Keep currentlyPlayingSongId - don't set to nil when pausing
        print("⏸️ Paused audio")
    }
    
    func stopAudio() {
        stopAudio(clearSnapshot: true)
    }

    private func stopAudio(clearSnapshot: Bool) {
        guard let player = player else {
            currentlyPlayingSong = nil
            isCurrentlyPlaying = false
            isBuffering = false
            showBufferingIndicator = false
            showBufferingTask?.cancel()
            hideBufferingTask?.cancel()
            showBufferingTask = nil
            hideBufferingTask = nil
            progress.currentTime = 0.0
            progress.currentProgress = 0.0
            progress.totalDuration = 0.0
        #if os(iOS)
            MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
        #endif
            return
        }
        player.pause()
        player.seek(to: .zero)
        removeProgressObservation()
        removeEndOfPlaybackObserver()
        removeDebugObservers()
        self.player = nil
        currentlyPlayingSong = nil
        isCurrentlyPlaying = false
        isBuffering = false
        showBufferingIndicator = false
        showBufferingTask?.cancel()
        hideBufferingTask?.cancel()
        showBufferingTask = nil
        hideBufferingTask = nil
        progress.currentTime = 0.0
        progress.currentProgress = 0.0
        progress.totalDuration = 0.0
        #if os(iOS)
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
        #endif

        if clearSnapshot && playbackContext == .primary {
            clearPrimarySnapshot()
        }

        print("⏹️ Stopped audio [context: \(playbackContext), clearSnapshot: \(clearSnapshot)]")
    }
    
    func togglePlayback(song: UploadedSong) {
        if let currentlyPlayingSong, currentlyPlayingSong.id == song.id {
            guard let player = player else { return }
            switch player.timeControlStatus {
            case .playing:
                player.pause()
                isCurrentlyPlaying = false
                wasManuallyPaused = true
                #if os(iOS)
                updateNowPlayingPlaybackState()
                #endif
                print("⏸️ Paused audio (keeping position)")
            case .paused:
                player.play()
                isCurrentlyPlaying = true
                wasManuallyPaused = false
                #if os(iOS)
                if playbackContext == .primary || TweaksManager.shared.isHooksBackgroundPlayEnabled {
                    updateNowPlayingPlaybackState()
                }
                #endif
                print("▶️ Resumed audio from current position")
            case .waitingToPlayAtSpecifiedRate:
                // Still buffering; no action
                break
            @unknown default:
                break
            }
        } else {
            // Different song or no song playing, start playing
            playAudio(song: song)
        }
    }
    
    func seekToProgress(_ progressValue: Double) {
        guard let player = player, self.progress.totalDuration > 0 else { return }
        let seconds = progressValue * self.progress.totalDuration
        let time = CMTime(seconds: seconds, preferredTimescale: 600)
        player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
        self.progress.currentTime = seconds
        self.progress.currentProgress = progressValue
        #if os(iOS)
        updateNowPlayingElapsedTime()
        #endif
    }
    
    // Seek by absolute seconds (used by remote command center)
    private func seekTo(seconds: Double) {
        guard let player = player else { return }
        let time = CMTime(seconds: seconds, preferredTimescale: 600)
        player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
        #if os(iOS)
        updateNowPlayingElapsedTime()
        #endif
    }
    
    func startScrubbing() {
        guard let player = player else {
            print("❌ startScrubbing: No player")
            return
        }
        
        // Remember if audio was playing and current volume
        wasPlayingBeforeScrubbing = (player.timeControlStatus == .playing)
        originalVolume = player.volume
        
        // Mute the audio during scrubbing
        player.volume = 0.0
        
        print("🎚️ Started scrubbing - muted audio (was playing: \(wasPlayingBeforeScrubbing), original volume: \(originalVolume))")
    }
    
    func endScrubbing() {
        guard let player = player else {
            print("❌ endScrubbing: No player")
            return
        }

        // Restore original volume
        player.volume = originalVolume

        print("🎚️ Ending scrubbing - wasPlayingBeforeScrubbing: \(wasPlayingBeforeScrubbing), timeControlStatus: \(player.timeControlStatus), restored volume: \(originalVolume)")

        // Resume playback if it was playing before scrubbing
        if wasPlayingBeforeScrubbing && player.timeControlStatus != .playing {
            player.play()
            isCurrentlyPlaying = true
            print("🎚️ Ended scrubbing - resumed audio playback")
        } else if wasPlayingBeforeScrubbing && player.timeControlStatus == .playing {
            print("🎚️ Ended scrubbing - audio was already playing")
        } else {
            print("🎚️ Ended scrubbing - restored audio volume (was not playing before)")
        }
    }

    // MARK: - Preloading

    /// Preload songs at specific playlist indices for smoother playback
    func preloadSongs(at indices: [Int]) {
        guard playbackContext == .hooks else { return }

        for index in indices {
            guard index >= 0 && index < currentPlaylist.count else { continue }

            // Skip if already preloaded
            guard preloadedItems[index] == nil else { continue }

            let song = currentPlaylist[index]
            guard let url = getAudioFileURL(for: song.audioURL) else { continue }

            let item = AVPlayerItem(url: url)
            preloadedItems[index] = item

            print("📦 Preloaded song at index \(index): \(song.name)")
        }

        // Clean up old preloaded items if we have too many
        cleanupPreloadedItems()
    }

    /// Get a preloaded item if available, otherwise return nil
    private func getPreloadedItem(at index: Int) -> AVPlayerItem? {
        return preloadedItems.removeValue(forKey: index)
    }

    /// Remove preloaded items that are too far from current position
    private func cleanupPreloadedItems() {
        guard preloadedItems.count > maxPreloadedItems else { return }

        let currentIndex = currentPlaylistIndex

        // Keep only items within range of current position
        let indicesToKeep = Set((currentIndex - 1)...(currentIndex + maxPreloadedItems))

        preloadedItems = preloadedItems.filter { index, _ in
            indicesToKeep.contains(index)
        }
    }

    /// Clear all preloaded items
    private func clearPreloadedItems() {
        preloadedItems.removeAll()
        print("🧹 Cleared all preloaded items")
    }

    /// Automatically preload next songs in hooks context
    private func preloadNextHooksSongs() {
        guard playbackContext == .hooks else { return }

        var indicesToPreload: [Int] = []

        // Always preload next song in playlist
        if currentPlaylistIndex + 1 < currentPlaylist.count {
            indicesToPreload.append(currentPlaylistIndex + 1)
        }

        // Preload one more if available
        if currentPlaylistIndex + 2 < currentPlaylist.count {
            indicesToPreload.append(currentPlaylistIndex + 2)
        }

        preloadSongs(at: indicesToPreload)
    }
    
    private func getAudioFileURL(for audioURL: String?) -> URL? {
        // For streaming, assume a valid remote URL (http/https)
        if let audioURL = audioURL, let url = URL(string: audioURL) {
            print("🌐 Using remote audio URL: \(audioURL)")
            return url
        }
        print("❌ Invalid or missing remote audio URL")
        return nil
    }
    
    private func addProgressObservation() {
        guard let player = player else { return }
        removeProgressObservation()
        let interval = CMTime(seconds: 0.1, preferredTimescale: 600)
        timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] _ in
            guard let self = self, let item = player.currentItem else { return }
            let duration = item.duration
            if duration.isNumeric {
                let seconds = CMTimeGetSeconds(duration)
                if seconds.isFinite && seconds > 0 {
                    self.progress.totalDuration = seconds
                }
            }
            let currentTime = CMTimeGetSeconds(player.currentTime())
            self.progress.currentTime = currentTime
            if self.progress.totalDuration > 0 {
                self.progress.currentProgress = max(0.0, min(1.0, currentTime / self.progress.totalDuration))
            } else {
                self.progress.currentProgress = 0.0
            }
            #if os(iOS)
            self.updateNowPlayingElapsedTime()
            #endif
        }
    }
    
    private func removeProgressObservation() {
        if let token = timeObserverToken, let player = player {
            player.removeTimeObserver(token)
        }
        timeObserverToken = nil
    }

    private func addEndOfPlaybackObserver(for item: AVPlayerItem) {
        removeEndOfPlaybackObserver()
        endObserver = NotificationCenter.default.addObserver(
            forName: .AVPlayerItemDidPlayToEndTime,
            object: item,
            queue: .main
        ) { [weak self] _ in
            guard let self = self else { return }
            self.isCurrentlyPlaying = false
            self.progress.currentTime = self.progress.totalDuration
            self.progress.currentProgress = 1.0
            #if os(iOS)
            self.updateNowPlayingPlaybackState()
            #endif
            print("🔚 Track finished naturally")
            
            // Auto-play next song if available
            if self.hasNextSong {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                    self.nextSong()
                }
            }
        }
    }

    private func removeEndOfPlaybackObserver() {
        if let endObserver = endObserver {
            NotificationCenter.default.removeObserver(endObserver)
        }
        endObserver = nil
    }

    // MARK: - Debug Observers for Loading States

    private func addDebugObservers(for item: AVPlayerItem, player: AVPlayer) {
        removeDebugObservers()

        let songId = currentlyPlayingSong?.id ?? "unknown"
        let context = playbackContext.rawValue

        // Observe AVPlayerItem status
        statusObserver = item.observe(\.status, options: [.new, .old]) { [weak self] item, change in
            guard let self = self else { return }
            let oldStatus = change.oldValue?.rawValue ?? -1
            let newStatus = change.newValue?.rawValue ?? -1
            let statusString = self.statusString(for: item.status)
            print("🔍 [DEBUG] [\(context)] AVPlayerItem.status changed: \(oldStatus) -> \(newStatus) (\(statusString)) | songId: \(songId)")

            if item.status == .failed, let error = item.error {
                print("❌ [DEBUG] [\(context)] AVPlayerItem failed with error: \(error.localizedDescription) | songId: \(songId)")
                self.isBuffering = false
            } else if item.status == .unknown && self.isCurrentlyPlaying {
                // Show buffering when item is loading and we want to play
                self.isBuffering = true
            } else if item.status == .readyToPlay {
                // Item is ready, check other conditions before hiding buffering
                self.updateBufferingState(for: item, player: player)
            }
        }

        // Observe playbackBufferEmpty
        bufferEmptyObserver = item.observe(\.isPlaybackBufferEmpty, options: [.new]) { [weak self] item, change in
            guard let self = self, let player = self.player else { return }
            let isEmpty = change.newValue ?? false
            print("🔍 [DEBUG] [\(context)] AVPlayerItem.isPlaybackBufferEmpty: \(isEmpty) | songId: \(songId)")
            self.updateBufferingState(for: item, player: player)
        }

        // Observe playbackLikelyToKeepUp
        bufferLikelyToKeepUpObserver = item.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) { [weak self] item, change in
            guard let self = self, let player = self.player else { return }
            let likelyToKeepUp = change.newValue ?? false
            print("🔍 [DEBUG] [\(context)] AVPlayerItem.isPlaybackLikelyToKeepUp: \(likelyToKeepUp) | songId: \(songId)")
            self.updateBufferingState(for: item, player: player)
        }

        // Observe playbackBufferFull
        bufferFullObserver = item.observe(\.isPlaybackBufferFull, options: [.new]) { [weak self] item, change in
            let isFull = change.newValue ?? false
            print("🔍 [DEBUG] [\(context)] AVPlayerItem.isPlaybackBufferFull: \(isFull) | songId: \(songId)")
        }

        // Observe AVPlayer timeControlStatus
        timeControlStatusObserver = player.observe(\.timeControlStatus, options: [.new, .old]) { [weak self] player, change in
            guard let self = self, let item = player.currentItem else { return }
            let oldStatus = change.oldValue?.rawValue ?? -1
            let newStatus = change.newValue?.rawValue ?? -1
            let statusString = self.timeControlStatusString(for: player.timeControlStatus)
            print("🔍 [DEBUG] [\(context)] AVPlayer.timeControlStatus changed: \(oldStatus) -> \(newStatus) (\(statusString)) | songId: \(songId)")

            if player.timeControlStatus == .waitingToPlayAtSpecifiedRate {
                if let reason = player.reasonForWaitingToPlay {
                    print("🔍 [DEBUG] [\(context)] Waiting reason: \(reason.rawValue) | songId: \(songId)")
                }
            }

            self.updateBufferingState(for: item, player: player)
        }

        // Observe AVPlayer rate
        rateObserver = player.observe(\.rate, options: [.new, .old]) { [weak self] player, change in
            let oldRate = change.oldValue ?? -1
            let newRate = change.newValue ?? -1
            print("🔍 [DEBUG] [\(context)] AVPlayer.rate changed: \(oldRate) -> \(newRate) | songId: \(songId)")
        }

        print("🔍 [DEBUG] [\(context)] Attached all debug observers | songId: \(songId)")
    }

    private func updateBufferingState(for item: AVPlayerItem, player: AVPlayer) {
        // Show buffering if:
        // 1. Item is not ready to play yet
        // 2. Player is waiting to play at specified rate
        // 3. Buffer is empty and we're trying to play
        let shouldBuffer = item.status != .readyToPlay ||
                          player.timeControlStatus == .waitingToPlayAtSpecifiedRate ||
                          (item.isPlaybackBufferEmpty && isCurrentlyPlaying)

        if isBuffering != shouldBuffer {
            print("🔍 [DEBUG] [\(playbackContext)] isBuffering changed: \(isBuffering) -> \(shouldBuffer) | status: \(statusString(for: item.status)) | timeControlStatus: \(timeControlStatusString(for: player.timeControlStatus)) | bufferEmpty: \(item.isPlaybackBufferEmpty)")
            isBuffering = shouldBuffer

            // Update UI indicator with grace period
            if shouldBuffer {
                // Starting to buffer - cancel any pending hide task
                hideBufferingTask?.cancel()
                hideBufferingTask = nil

                // Show indicator after 100ms grace period
                showBufferingTask?.cancel()
                showBufferingTask = Task { @MainActor [weak self] in
                    try? await Task.sleep(nanoseconds: 200_000_000) // 200ms
                    guard let self = self, !Task.isCancelled else { return }
                    self.showBufferingIndicator = true
                }
            } else {
                // Stopped buffering - cancel any pending show task
                showBufferingTask?.cancel()
                showBufferingTask = nil

                // Hide indicator after 100ms grace period (if it's currently showing)
                if showBufferingIndicator {
                    hideBufferingTask?.cancel()
                    hideBufferingTask = Task { @MainActor [weak self] in
                        try? await Task.sleep(nanoseconds: 100_000_000) // 200ms
                        guard let self = self, !Task.isCancelled else { return }
                        self.showBufferingIndicator = false
                    }
                }
            }
        }

        // Auto-resume playback if buffering is complete and we should be playing
        // This handles the case where AVPlayer auto-pauses during buffering
        // Only resume if the pause was NOT intentional (i.e., not manually paused by user)
        if !shouldBuffer && isCurrentlyPlaying && player.rate == 0.0 && !wasManuallyPaused {
            print("🔍 [DEBUG] [\(playbackContext)] Auto-resuming playback after buffering | rate: \(player.rate) | isCurrentlyPlaying: \(isCurrentlyPlaying)")
            player.play()
            #if os(iOS)
            if playbackContext == .primary || TweaksManager.shared.isHooksBackgroundPlayEnabled {
                updateNowPlayingPlaybackState()
            }
            #endif
        }
    }

    private func removeDebugObservers() {
        statusObserver?.invalidate()
        bufferEmptyObserver?.invalidate()
        bufferLikelyToKeepUpObserver?.invalidate()
        bufferFullObserver?.invalidate()
        timeControlStatusObserver?.invalidate()
        rateObserver?.invalidate()

        statusObserver = nil
        bufferEmptyObserver = nil
        bufferLikelyToKeepUpObserver = nil
        bufferFullObserver = nil
        timeControlStatusObserver = nil
        rateObserver = nil
    }

    private func statusString(for status: AVPlayerItem.Status) -> String {
        switch status {
        case .unknown: return "unknown"
        case .readyToPlay: return "readyToPlay"
        case .failed: return "failed"
        @unknown default: return "unknown_default"
        }
    }

    private func timeControlStatusString(for status: AVPlayer.TimeControlStatus) -> String {
        switch status {
        case .paused: return "paused"
        case .playing: return "playing"
        case .waitingToPlayAtSpecifiedRate: return "waitingToPlayAtSpecifiedRate"
        @unknown default: return "unknown_default"
        }
    }

    // MARK: - Now Playing Info
    #if os(iOS)
    private func configureNowPlayingInfo(for song: UploadedSong) {
        nowPlayingInfo = [
            MPMediaItemPropertyTitle: song.name,
            MPMediaItemPropertyArtist: song.artistName
        ]
        if progress.totalDuration > 0 {
            nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = progress.totalDuration
        }
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentElapsedTime()
        nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isCurrentlyPlaying ? 1.0 : 0.0
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
        loadArtworkIfNeeded(for: song)
    }

    private func updateNowPlayingPlaybackState() {
        nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isCurrentlyPlaying ? 1.0 : 0.0
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentElapsedTime()
        if progress.totalDuration > 0 {
            nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = progress.totalDuration
        }
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }

    private func updateNowPlayingElapsedTime() {
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentElapsedTime()
        if progress.totalDuration > 0 {
            nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = progress.totalDuration
        }
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }
    
    private func currentElapsedTime() -> Double {
        guard let player = player else { return 0.0 }
        return CMTimeGetSeconds(player.currentTime())
    }
    
    private func loadArtworkIfNeeded(for song: UploadedSong) {
        guard let urlString = song.imageURL, let url = URL(string: urlString) else { return }
        if let cached = artworkCache.object(forKey: urlString as NSString) {
            let artwork = MPMediaItemArtwork(boundsSize: cached.size) { _ in cached }
            nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
            MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
            return
        }
        URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
            guard let self = self, let data = data, let image = UIImage(data: data) else { return }
            self.artworkCache.setObject(image, forKey: urlString as NSString)
            let artwork = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
            DispatchQueue.main.async {
                self.nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
                MPNowPlayingInfoCenter.default().nowPlayingInfo = self.nowPlayingInfo
            }
        }.resume()
    }
    #endif
    
    // MARK: - Remote Command Center
    #if os(iOS)
    private func configureRemoteCommands() {
        let commandCenter = MPRemoteCommandCenter.shared()
        
        commandCenter.playCommand.isEnabled = true
        commandCenter.playCommand.addTarget { [weak self] _ in
            guard let self = self else { return .commandFailed }
            if self.player?.timeControlStatus != .playing {
                print("playing due to system play command")
                self.player?.play()
                self.isCurrentlyPlaying = true
                self.wasManuallyPaused = false
                self.updateNowPlayingPlaybackState()
            }
            return .success
        }
        
        commandCenter.pauseCommand.isEnabled = true
        commandCenter.pauseCommand.addTarget { [weak self] _ in
            guard let self = self else { return .commandFailed }
            self.pauseAudio()
            return .success
        }
        
        commandCenter.togglePlayPauseCommand.isEnabled = true
        commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
            guard let self = self else { return .commandFailed }
            if self.isCurrentlyPlaying {
                self.pauseAudio()
            } else if let song = self.currentlyPlayingSong {
                print("playing due to system toggle play/pause command")
                if self.player == nil {
                    self.playAudio(song: song)
                } else {
                    self.player?.play()
                    self.isCurrentlyPlaying = true
                    self.wasManuallyPaused = false
                    self.updateNowPlayingPlaybackState()
                }
            }
            return .success
        }
        
        commandCenter.nextTrackCommand.isEnabled = true
        commandCenter.nextTrackCommand.addTarget { [weak self] _ in
            guard let self = self else { return .commandFailed }
            guard self.hasNextSong else { return .noActionableNowPlayingItem }
            self.nextSong()
            return .success
        }
        
        commandCenter.previousTrackCommand.isEnabled = true
        commandCenter.previousTrackCommand.addTarget { [weak self] _ in
            guard let self = self else { return .commandFailed }
            guard self.hasPreviousSong else { return .noActionableNowPlayingItem }
            self.previousSong()
            return .success
        }
        
        if #available(iOS 13.0, *) {
            commandCenter.changePlaybackPositionCommand.isEnabled = true
            commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
                guard let self = self, let e = event as? MPChangePlaybackPositionCommandEvent else { return .commandFailed }
                self.seekTo(seconds: e.positionTime)
                return .success
            }
        }
    }
    #endif
    
    // MARK: - Audio Session Notifications
    #if os(iOS)
    private func subscribeToAudioSessionNotifications() {
        NotificationCenter.default.addObserver(self, selector: #selector(handleInterruption(_:)), name: AVAudioSession.interruptionNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(handleRouteChange(_:)), name: AVAudioSession.routeChangeNotification, object: nil)
    }
    
    @objc private func handleInterruption(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
              let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
              let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
        
        switch type {
        case .began:
            print("pausing audio due to interruption")
            pauseAudio()
        case .ended:
            print("interruption ended")
            if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
                let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
                if options.contains(.shouldResume) {
                    print("playing audio due to system shouldResume option")
                    player?.play()
                    isCurrentlyPlaying = true
                    wasManuallyPaused = false
                    updateNowPlayingPlaybackState()
                }
            }
        @unknown default:
            break
        }
    }
    
    @objc private func handleRouteChange(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
              let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
              let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return }
        
        if reason == .oldDeviceUnavailable {
            // E.g. headphones unplugged
            pauseAudio()
        }
    }
    #endif
}
