import Foundation
import os
import SwiftData
import SwiftUI

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

/// ViewModel wrapping the AudioEngine with additional UI state.
@MainActor
@Observable
final class PlayerViewModel {
    let audioEngine = AudioEngine()
    let streamingPlayer = StreamingPlayer()

    // MARK: - UI State

    var showingWaveform = true
    var waveformSamples: [WaveformGenerator.WaveformSample] = []
    var isLoadingWaveform = false
    var showNowPlaying = false
    var showQueue = false

    /// ID of the currently playing playlist entry.
    var currentPlayingEntryID: UUID?

    /// The playlist currently being played through.
    var currentPlaylist: Playlist?

    /// Shuffle mode.
    var shuffleEnabled: Bool = false {
        didSet {
            if shuffleEnabled {
                upNext.shuffle()
            } else {
                rebuildUpNextFromSource()
            }
        }
    }

    /// True when playing a cloud track via StreamingPlayer (vs local via AudioEngine).
    var isCloudPlayback = false

    /// The cloud track info when streaming (non-SwiftData, transient).
    var currentCloudTrack: ChadTrack?

    var isBuffering: Bool { streamingPlayer.isBuffering }

    /// Repeat mode.
    enum RepeatMode: String, CaseIterable {
        case off = "Off"
        case all = "Repeat All"
        case one = "Repeat One"

        var icon: String {
            switch self {
            case .off: return "repeat"
            case .all: return "repeat"
            case .one: return "repeat.1"
            }
        }
    }
    var repeatMode: RepeatMode = .off

    // MARK: - Queue

    var nowPlayingEntry: QueueEntry?
    var userQueue: [QueueEntry] = []
    var upNext: [QueueEntry] = []
    var history: [QueueEntry] = []

    /// Undo state for queue replacement
    var showUndoToast = false
    var undoMessage = ""
    @ObservationIgnored private var previousQueueSnapshot: QueueSnapshot?
    @ObservationIgnored private var undoTimer: Timer?

    private struct QueueSnapshot {
        let nowPlaying: QueueEntry?
        let userQueue: [QueueEntry]
        let upNext: [QueueEntry]
    }

    /// ModelContext for resolving SwiftData track IDs — set from the view layer
    @ObservationIgnored var modelContext: ModelContext?

    // MARK: - Synced State

    var isPlaying: Bool = false
    var currentTime: TimeInterval = 0
    var duration: TimeInterval = 0
    var currentTrack: Track?

    var volume: Float {
        get { audioEngine.volume }
        set { audioEngine.volume = newValue }
    }

    var progress: Double {
        guard duration > 0 else { return 0 }
        return currentTime / duration
    }

    var currentTimeFormatted: String {
        formatTime(currentTime)
    }

    var durationFormatted: String {
        formatTime(duration)
    }

    var remainingTimeFormatted: String {
        "-" + formatTime(duration - currentTime)
    }

    @ObservationIgnored private var syncTimer: Timer?
    @ObservationIgnored private var stateSaveCounter = 0
    @ObservationIgnored private var nowPlayingCounter = 0

    init() {
        restoreQueue()
        startSyncTimer()
        audioEngine.onPlaybackFinished = { [weak self] in
            self?.playNext()
        }
        streamingPlayer.onPlaybackFinished = { [weak self] in
            self?.playNext()
        }
    }

    private func startSyncTimer() {
        syncTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 30.0, repeats: true) { [weak self] _ in
            Task { @MainActor in
                self?.syncFromEngine()
            }
        }
    }

    private func syncFromEngine() {
        if isCloudPlayback {
            let sp = streamingPlayer
            if isPlaying != sp.isPlaying { isPlaying = sp.isPlaying }
            if abs(currentTime - sp.currentTime) > 0.01 { currentTime = sp.currentTime }
            if duration != sp.duration { duration = sp.duration }
        } else {
            let engine = audioEngine
            engine.updateCurrentTime()
            if isPlaying != engine.isPlaying { isPlaying = engine.isPlaying }
            if abs(currentTime - engine.currentTime) > 0.01 { currentTime = engine.currentTime }
            if duration != engine.duration { duration = engine.duration }
            if currentTrack !== engine.currentTrack { currentTrack = engine.currentTrack }
        }

        stateSaveCounter += 1
        if stateSaveCounter >= 60 {
            stateSaveCounter = 0
            savePlaybackState()
        }

        nowPlayingCounter += 1
        if nowPlayingCounter >= 30 {
            nowPlayingCounter = 0
            updateNowPlaying()
        }
    }

    private func savePlaybackState() {
        AppState.savePlaybackState(
            playlistID: currentPlaylist?.id,
            entryID: currentPlayingEntryID,
            trackFilePath: currentTrack?.filePath,
            playbackTime: currentTime
        )
    }

    private func updateNowPlaying() {
        MediaKeyHandler.shared.updateNowPlaying(
            track: currentTrack,
            isPlaying: isPlaying,
            currentTime: currentTime,
            duration: duration
        )
    }

    // MARK: - Track Loading & Playback

    func loadAndPlay(_ track: Track, entryID: UUID? = nil, playlist: Playlist? = nil) {
        // Cloud track — route to StreamingPlayer
        if track.isCloud, let streamPath = track.cloudStreamPath {
            let client = ChadMusicAPIClient.shared
            guard let url = client.streamURL(for: streamPath) else {
                logger.error("loadAndPlay: Failed to build stream URL for cloud track")
                return
            }
            // Stop local playback
            audioEngine.stop()
            waveformSamples = []

            isCloudPlayback = true
            currentCloudTrack = nil
            currentTrack = track
            currentPlayingEntryID = entryID
            if let playlist { currentPlaylist = playlist }
            streamingPlayer.loadAndPlay(
                track: ChadTrack(
                    id: track.cloudTrackId ?? "",
                    title: track.title,
                    artist: track.artist,
                    albumArtist: nil,
                    album: track.album,
                    duration: track.duration,
                    no: nil,
                    url: streamPath,
                    bitRate: nil,
                    year: track.year,
                    cover: nil
                ),
                streamURL: url,
                authHeaders: client.authHeaders
            )
            syncFromEngine()
            return
        }

        // Local track — use AudioEngine
        if isCloudPlayback {
            streamingPlayer.stop()
            isCloudPlayback = false
            currentCloudTrack = nil
        }

        do {
            logger.notice("loadAndPlay: \(track.title) path=\(track.filePath)")
            logger.notice("loadAndPlay: fileURL=\(track.fileURL.path)")
            logger.notice("loadAndPlay: exists=\(FileManager.default.fileExists(atPath: track.fileURL.path))")
            try audioEngine.loadTrack(track)
            audioEngine.play()
            currentPlayingEntryID = entryID
            if let playlist { currentPlaylist = playlist }
            syncFromEngine()
            savePlaybackState()
            loadWaveform(for: track)
            resetSkipCounter()
            logger.notice("loadAndPlay: success, playing")
        } catch {
            logger.error("loadAndPlay: FAILED: \(error)")
            print("PlayerViewModel: Failed to load track: \(error)")
        }
    }

    /// Play a cloud track directly via StreamingPlayer (from CloudBrowserView).
    func loadAndPlayCloud(_ track: ChadTrack, streamURL: URL, authHeaders: [String: String]) {
        // Stop local playback first
        audioEngine.stop()
        currentTrack = nil
        currentPlayingEntryID = nil
        currentPlaylist = nil
        waveformSamples = []

        isCloudPlayback = true
        currentCloudTrack = track
        streamingPlayer.loadAndPlay(track: track, streamURL: streamURL, authHeaders: authHeaders)
        resetSkipCounter()
        syncFromEngine()
    }

    func togglePlayPause() {
        if isCloudPlayback {
            streamingPlayer.togglePlayPause()
        } else {
            audioEngine.togglePlayPause()
        }
        syncFromEngine()
    }

    func stop() {
        if isCloudPlayback {
            streamingPlayer.stop()
            isCloudPlayback = false
            currentCloudTrack = nil
        }
        audioEngine.stop()
        waveformSamples = []
        currentPlayingEntryID = nil
        syncFromEngine()
    }

    // MARK: - Playlist Navigation (Queue-based)

    func playNext() {
        // Repeat One: replay current
        if repeatMode == .one, let current = nowPlayingEntry {
            playQueueEntry(current)
            return
        }

        // Push current to history
        if let current = nowPlayingEntry {
            history.insert(current, at: 0)
            if history.count > 50 { history = Array(history.prefix(50)) }
        }

        // Pop from userQueue first, then upNext
        if !userQueue.isEmpty {
            let next = userQueue.removeFirst()
            nowPlayingEntry = next
            playQueueEntry(next)
            persistQueue()
            return
        }

        if !upNext.isEmpty {
            let next = upNext.removeFirst()
            nowPlayingEntry = next
            playQueueEntry(next)
            persistQueue()
            return
        }

        // Both empty — check repeat all
        if repeatMode == .all, let playlist = currentPlaylist {
            rebuildUpNextFromPlaylist(playlist, afterEntryID: nil)
            if !upNext.isEmpty {
                let next = upNext.removeFirst()
                nowPlayingEntry = next
                playQueueEntry(next)
                persistQueue()
                return
            }
        }

        // Nothing left
        nowPlayingEntry = nil
        stop()
        persistQueue()
    }

    func playPrevious() {
        if currentTime > 3 {
            seek(to: 0)
            return
        }

        guard !history.isEmpty else { return }
        let prev = history.removeFirst()

        // Push current nowPlaying to front of upNext
        if let current = nowPlayingEntry {
            upNext.insert(current, at: 0)
        }

        nowPlayingEntry = prev
        playQueueEntry(prev)
        persistQueue()
    }

    // MARK: - Queue Actions

    /// Add entry to end of user queue.
    func addToQueue(_ entry: QueueEntry) {
        userQueue.append(entry)
        persistQueue()
        // Auto-play if nothing is currently playing
        if !isPlaying {
            playNext()
        }
    }

    /// Insert entry at front of user queue ("Play Next").
    func playNextInQueue(_ entry: QueueEntry) {
        userQueue.insert(entry, at: 0)
        persistQueue()
    }

    /// Play a track from playlist context — sets as nowPlaying, fills upNext with remainder.
    func playFromPlaylist(track: Track, entryID: UUID, playlist: Playlist) {
        let hadQueue = nowPlayingEntry != nil || !userQueue.isEmpty || !upNext.isEmpty
        if hadQueue {
            previousQueueSnapshot = QueueSnapshot(
                nowPlaying: nowPlayingEntry,
                userQueue: userQueue,
                upNext: upNext
            )
        }

        let entry = QueueEntry.from(track: track)
        nowPlayingEntry = entry
        userQueue = []
        history = []

        rebuildUpNextFromPlaylist(playlist, afterEntryID: entryID)

        currentPlaylist = playlist
        currentPlayingEntryID = entryID
        playQueueEntry(entry)
        persistQueue()

        if hadQueue {
            showUndoToastBriefly("Queue replaced")
        }
    }

    /// Play a cloud track directly — sets as nowPlaying, clears upNext.
    func playCloudTrackDirectly(_ track: ChadTrack, streamURL: URL, authHeaders: [String: String]) {
        let hadQueue = nowPlayingEntry != nil || !userQueue.isEmpty || !upNext.isEmpty
        if hadQueue {
            previousQueueSnapshot = QueueSnapshot(
                nowPlaying: nowPlayingEntry,
                userQueue: userQueue,
                upNext: upNext
            )
        }

        let entry = QueueEntry.from(cloudTrack: track)
        nowPlayingEntry = entry
        userQueue = []
        upNext = []
        history = []
        currentPlaylist = nil
        currentPlayingEntryID = nil

        loadAndPlayCloud(track, streamURL: streamURL, authHeaders: authHeaders)
        persistQueue()

        if hadQueue {
            showUndoToastBriefly("Queue replaced")
        }
    }

    func removeFromQueue(entry: QueueEntry) {
        userQueue.removeAll { $0.id == entry.id }
        upNext.removeAll { $0.id == entry.id }
        persistQueue()
    }

    func clearQueue() {
        userQueue.removeAll()
        upNext.removeAll()
        persistQueue()
    }

    func moveUserQueueEntry(from source: IndexSet, to destination: Int) {
        userQueue.move(fromOffsets: source, toOffset: destination)
        persistQueue()
    }

    func moveUpNextEntry(from source: IndexSet, to destination: Int) {
        upNext.move(fromOffsets: source, toOffset: destination)
        persistQueue()
    }

    func undoQueueReplacement() {
        guard let snapshot = previousQueueSnapshot else { return }
        nowPlayingEntry = snapshot.nowPlaying
        userQueue = snapshot.userQueue
        upNext = snapshot.upNext
        previousQueueSnapshot = nil
        showUndoToast = false
        undoTimer?.invalidate()

        if let entry = nowPlayingEntry {
            playQueueEntry(entry)
        }
        persistQueue()
    }

    // MARK: - Queue Entry Playback

    private func playQueueEntry(_ entry: QueueEntry) {
        switch entry.source {
        case .swiftDataTrack(let trackPersistentID, let isCloud, let cloudStreamPath):
            guard let ctx = modelContext else {
                logger.error("playQueueEntry: no modelContext set")
                return
            }
            // Fetch track by UUID string
            guard let trackID = UUID(uuidString: trackPersistentID) else {
                logger.error("playQueueEntry: invalid UUID string: \(trackPersistentID)")
                skipBrokenEntry()
                return
            }
            let descriptor = FetchDescriptor<Track>(
                predicate: #Predicate<Track> { $0.id == trackID }
            )
            guard let track = (try? ctx.fetch(descriptor))?.first else {
                logger.error("playQueueEntry: track not found for ID \(trackPersistentID)")
                skipBrokenEntry()
                return
            }
            loadAndPlay(track, entryID: currentPlayingEntryID, playlist: currentPlaylist)

        case .cloudDirect(_, let streamPath):
            let client = ChadMusicAPIClient.shared
            guard let url = client.streamURL(for: streamPath) else {
                logger.error("playQueueEntry: failed to build stream URL for \(streamPath)")
                skipBrokenEntry()
                return
            }
            let chadTrack = ChadTrack(
                id: entry.id.uuidString,
                title: entry.title,
                artist: entry.artist,
                albumArtist: nil,
                album: nil,
                duration: entry.duration,
                no: nil,
                url: streamPath,
                bitRate: nil,
                year: nil,
                cover: nil
            )
            loadAndPlayCloud(chadTrack, streamURL: url, authHeaders: client.authHeaders)
        }
    }

    @ObservationIgnored private var skipCount = 0
    private static let maxSkips = 20

    private func skipBrokenEntry() {
        skipCount += 1
        guard skipCount <= Self.maxSkips else {
            logger.error("skipBrokenEntry: exceeded \(Self.maxSkips) skips, stopping to prevent infinite loop")
            skipCount = 0
            userQueue.removeAll()
            upNext.removeAll()
            stop()
            return
        }
        playNext()
    }

    /// Reset skip counter on successful playback start
    private func resetSkipCounter() {
        skipCount = 0
    }

    // MARK: - Queue Helpers

    private func rebuildUpNextFromPlaylist(_ playlist: Playlist, afterEntryID: UUID?) {
        let entries = playlist.sortedEntries
        var startIndex = 0
        if let afterID = afterEntryID,
           let idx = entries.firstIndex(where: { $0.id == afterID }) {
            startIndex = idx + 1
        }

        upNext = entries[startIndex...].compactMap { entry -> QueueEntry? in
            guard let track = entry.track else { return nil }
            return QueueEntry.from(track: track)
        }

        if shuffleEnabled {
            upNext.shuffle()
        }
    }

    private func rebuildUpNextFromSource() {
        guard let playlist = currentPlaylist else { return }
        rebuildUpNextFromPlaylist(playlist, afterEntryID: currentPlayingEntryID)
    }

    private func showUndoToastBriefly(_ message: String) {
        undoMessage = message
        showUndoToast = true
        undoTimer?.invalidate()
        undoTimer = Timer.scheduledTimer(withTimeInterval: 4.0, repeats: false) { [weak self] _ in
            Task { @MainActor in
                self?.showUndoToast = false
                self?.previousQueueSnapshot = nil
            }
        }
    }

    // MARK: - Queue Persistence

    private static let queueKey = "mixboard.queueState"

    private struct PersistedQueue: Codable {
        let nowPlaying: QueueEntry?
        let userQueue: [QueueEntry]
        let upNext: [QueueEntry]
    }

    private func persistQueue() {
        let state = PersistedQueue(nowPlaying: nowPlayingEntry, userQueue: userQueue, upNext: upNext)
        if let data = try? JSONEncoder().encode(state) {
            UserDefaults.standard.set(data, forKey: Self.queueKey)
        }
    }

    private func restoreQueue() {
        guard let data = UserDefaults.standard.data(forKey: Self.queueKey),
              let state = try? JSONDecoder().decode(PersistedQueue.self, from: data) else { return }
        nowPlayingEntry = state.nowPlaying
        userQueue = state.userQueue
        upNext = state.upNext
    }

    func seek(to time: TimeInterval) {
        if isCloudPlayback {
            streamingPlayer.seek(to: time)
        } else {
            audioEngine.seek(to: time)
        }
    }

    func seekToProgress(_ progress: Double) {
        let time = progress * duration
        seek(to: time)
    }

    func skipForward(_ seconds: TimeInterval = 10) {
        if isCloudPlayback {
            streamingPlayer.seek(by: seconds)
        } else {
            audioEngine.seek(by: seconds)
        }
    }

    func skipBackward(_ seconds: TimeInterval = 10) {
        if isCloudPlayback {
            streamingPlayer.seek(by: -seconds)
        } else {
            audioEngine.seek(by: -seconds)
        }
    }

    // MARK: - Waveform

    func loadWaveform(for track: Track) {
        if let cached = track.waveformData,
           let decoded = WaveformGenerator.decodeCachedWaveform(from: cached) {
            waveformSamples = decoded
            return
        }

        isLoadingWaveform = true
        Task {
            do {
                let samples = try await WaveformGenerator.generateWaveform(for: track)
                waveformSamples = samples
            } catch {
                print("PlayerViewModel: Waveform generation failed: \(error)")
                waveformSamples = []
            }
            isLoadingWaveform = false
        }
    }

    // MARK: - Helpers

    private func formatTime(_ time: TimeInterval) -> String {
        let total = max(0, Int(time))
        let minutes = total / 60
        let seconds = total % 60
        return String(format: "%d:%02d", minutes, seconds)
    }
}
