import Foundation
import AVFoundation
import MediaPlayer
import Observation

#if canImport(UIKit)
import UIKit
private typealias PlayerArtworkImage = UIImage
#else
import AppKit
private typealias PlayerArtworkImage = NSImage
#endif

enum SleepTimerMode: Equatable, Hashable {
    case off
    case minutes(Int)
    case endOfBook
    case endOfChapter
}

@Observable
@MainActor
final class PlayerEngine {
    var isPlaying: Bool = false
    var absoluteCurrentTime: Double = 0
    var totalDuration: Double = 0
    var rate: Float = 1.0
    var isReady: Bool = false
    var errorMessage: String?

    var sleepTimer: SleepTimerMode = .off
    /// Verbleibende Wallclock-Sekunden bis der Sleep-Timer auslöst (0 wenn off).
    /// Pausiert mit der Wiedergabe; bei `.endOfBook`/`.endOfChapter` rate-skaliert aus der Restspielzeit.
    var sleepRemainingSeconds: Double = 0
    var chapters: [Chapter] = []
    /// Set by AppState. Fired when a remote/system control (lockscreen, Control
    /// Center, headphones) triggers a skip, so AppState can record history and
    /// run its own seek path instead of bypassing it.
    var onRemoteSkip: ((Double) -> Void)?
    /// Set by AppState. Fired when a remote control changes playback position.
    var onRemoteSeek: ((Double) -> Void)?
    /// Set by AppState. Fired by the AVPlayer periodic time observer whenever
    /// playback crosses into a new chapter — works reliably in background and
    /// with the device locked, unlike a runloop-scheduled Timer.
    var onChapterChanged: ((Chapter) -> Void)?
    private var lastObservedChapterId: Int?

    var currentChapter: Chapter? {
        chapters.last { $0.start <= absoluteCurrentTime }
    }

    private var player: AVQueuePlayer?
    private var trackDurations: [Double] = []
    private var trackPlayerItems: [AVPlayerItem] = []
    private var currentTrackIndex: Int = 0
    private var timeObserver: Any?
    private var endObservers: [NSObjectProtocol] = []
    private var isSeeking: Bool = false
    private var sleepTickTask: Task<Void, Never>?

    var itemId: String?

    private var currentTitle: String = ""
    private var currentAuthor: String = ""
    private var currentCoverURL: URL?
    private var remoteCommandsConfigured: Bool = false
    private var artworkSession: URLSession?

    #if os(iOS)
    private var audioSessionObserversConfigured: Bool = false
    private var wasPlayingBeforeInterruption: Bool = false
    #endif

    nonisolated init() {}

    func load(item: LibraryItem, client: ABSClient, downloads: DownloadManager, startAt absoluteTime: Double) {
        teardown()
        self.itemId = item.id
        self.errorMessage = nil
        self.artworkSession = client.session

        let useLocal = downloads.isDownloaded(downloadKey: item.downloadKey)
        let urls: [URL]

        if useLocal, let localURLs = downloads.localTrackURLs(for: item.downloadKey), !localURLs.isEmpty {
            urls = localURLs
            trackDurations = (0..<localURLs.count).map { idx in
                idx < item.audioFiles.count ? item.audioFiles[idx].durationSeconds : 0
            }
        } else {
            guard !item.audioFiles.isEmpty else {
                errorMessage = "Dieses Hörbuch enthält keine abspielbaren Audiodateien."
                return
            }
            urls = item.audioFiles.compactMap { client.audioFileURL(itemId: item.id, ino: $0.ino) }
            trackDurations = item.audioFiles.map { $0.durationSeconds }
        }

        // When audio files carry no duration (e.g. some podcast episodes or
        // freshly-scanned items), fall back to the item's reported total.
        // Distribute equally across all tracks so that trackDurations.count
        // always matches trackPlayerItems.count.
        if trackDurations.allSatisfy({ $0 <= 0 }) && item.durationSeconds > 0 {
            let count = max(1, trackDurations.count)
            let perTrack = item.durationSeconds / Double(count)
            trackDurations = Array(repeating: perTrack, count: count)
        }

        totalDuration = trackDurations.reduce(0, +)

        trackPlayerItems = urls.map { AVPlayerItem(url: $0) }

        let queue = AVQueuePlayer(items: trackPlayerItems)
        queue.rate = rate
        // Enable AirPlay / Bluetooth / CarPlay routing. Without this, AVPlayer
        // refuses to send audio to external devices even when the user picks
        // them in Control Center / the route picker.
        queue.allowsExternalPlayback = true
        #if os(iOS)
        queue.usesExternalPlaybackWhileExternalScreenIsActive = true
        #endif
        self.player = queue

        let center = NotificationCenter.default
        for (idx, playerItem) in trackPlayerItems.enumerated() {
            let token = center.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: .main) { [weak self] _ in
                Task { @MainActor [weak self] in
                    self?.handleTrackEnd(finishedIndex: idx)
                }
            }
            endObservers.append(token)
        }

        seekAbsolute(absoluteTime)

        timeObserver = queue.addPeriodicTimeObserver(
            forInterval: CMTime(seconds: 0.5, preferredTimescale: 600),
            queue: .main
        ) { [weak self] _ in
            Task { @MainActor [weak self] in
                self?.refreshAbsoluteTime()
            }
        }

        self.chapters = item.chapters
        // Reset chapter tracking — first observation after load is "initial",
        // not a transition.
        lastObservedChapterId = nil
        currentTitle = item.title
        currentAuthor = item.author
        currentCoverURL = client.coverURL(itemId: item.id)

        #if os(iOS)
        configureAudioSessionObserversIfNeeded()
        #endif
        configureRemoteCommandsIfNeeded()
        updateNowPlayingInfo()
        fetchAndAttachArtwork()

        isReady = true
    }

    func play() {
        #if os(iOS)
        try? AVAudioSession.sharedInstance().setActive(true)
        #endif
        player?.play()
        player?.rate = rate
        isPlaying = true
        if case .minutes = sleepTimer, sleepRemainingSeconds > 0, sleepTickTask == nil {
            startSleepTickTask()
        }
        updateNowPlayingInfo()
    }

    func pause() {
        player?.pause()
        isPlaying = false
        cancelSleepTickTask()
        updateNowPlayingInfo()
    }

    func togglePlay() {
        isPlaying ? pause() : play()
    }

    func setRate(_ newRate: Float) {
        rate = newRate
        if isPlaying { player?.rate = newRate }
        switch sleepTimer {
        case .endOfBook: sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
        case .endOfChapter: sleepRemainingSeconds = wallclockRemainingUntilEndOfChapter()
        default: break
        }
        updateNowPlayingInfo()
    }

    func skip(by seconds: Double) {
        seekAbsolute(absoluteCurrentTime + seconds)
    }

    func seekAbsolute(_ target: Double) {
        let clamped = max(0, min(target, max(totalDuration - 0.5, 0)))
        var remaining = clamped
        var trackIndex = 0
        for (idx, dur) in trackDurations.enumerated() {
            if remaining <= dur || idx == trackDurations.count - 1 {
                trackIndex = idx
                break
            }
            remaining -= dur
        }
        switchToTrack(index: trackIndex)
        absoluteCurrentTime = clamped
        guard let currentItem = player?.currentItem else {
            // No item to seek: don't leave isSeeking stuck, which would freeze the scrubber.
            return
        }
        isSeeking = true
        let cmTime = CMTime(seconds: max(0, remaining), preferredTimescale: 600)
        // Small tolerance so seeking succeeds on VBR MP3s without a Xing header.
        // Zero-tolerance seeks fail silently on such files, snapping the slider back.
        let tolerance = CMTime(seconds: 0.5, preferredTimescale: 600)
        currentItem.seek(to: cmTime, toleranceBefore: tolerance, toleranceAfter: tolerance) { [weak self] _ in
            Task { @MainActor [weak self] in
                self?.isSeeking = false
                self?.refreshAbsoluteTime()
                self?.updateNowPlayingInfo()
            }
        }
    }

    private func switchToTrack(index: Int) {
        guard index < trackPlayerItems.count, let player else { return }
        if index == currentTrackIndex, player.currentItem === trackPlayerItems[index] { return }

        let wasPlaying = isPlaying
        player.removeAllItems()
        for i in index..<trackPlayerItems.count {
            let it = trackPlayerItems[i]
            it.seek(to: .zero, completionHandler: nil)
            if player.canInsert(it, after: nil) {
                player.insert(it, after: nil)
            }
        }
        currentTrackIndex = index
        if wasPlaying { player.play(); player.rate = rate }
    }

    private func handleTrackEnd(finishedIndex: Int) {
        if finishedIndex < trackDurations.count - 1 {
            currentTrackIndex = finishedIndex + 1
        } else {
            isPlaying = false
            updateNowPlayingInfo()
        }
    }

    private func refreshAbsoluteTime() {
        guard let player, let current = player.currentItem else { return }
        if isSeeking { return }
        if let idx = trackPlayerItems.firstIndex(where: { $0 === current }) {
            currentTrackIndex = idx
        }
        let trackTime = current.currentTime().seconds
        let prior = trackDurations.prefix(currentTrackIndex).reduce(0, +)
        let absolute = prior + (trackTime.isFinite ? trackTime : 0)
        let cap = totalDuration > 0 ? totalDuration : absolute
        absoluteCurrentTime = max(0, min(absolute, cap))
        let wasPlaying = isPlaying
        isPlaying = player.timeControlStatus == .playing
        if wasPlaying != isPlaying { updateNowPlayingInfo() }
        updateEndOfBookSleep()
        updateEndOfChapterSleep()
        detectChapterTransition()
    }

    private func detectChapterTransition() {
        let currentId = currentChapter?.id
        guard currentId != lastObservedChapterId else { return }
        let priorId = lastObservedChapterId
        lastObservedChapterId = currentId
        // Skip the very first observation after load (priorId == nil) — that's
        // the initial position, not a real transition. AppState records that
        // separately in `play(item:)`.
        guard priorId != nil, let chapter = currentChapter else { return }
        onChapterChanged?(chapter)
    }

    func teardown() {
        cancelSleepTickTask()
        sleepTimer = .off
        sleepRemainingSeconds = 0
        if let token = timeObserver { player?.removeTimeObserver(token) }
        timeObserver = nil
        for obs in endObservers { NotificationCenter.default.removeObserver(obs) }
        endObservers.removeAll()
        player?.pause()
        player?.removeAllItems()
        player = nil
        trackPlayerItems.removeAll()
        trackDurations.removeAll()
        isPlaying = false
        isReady = false
        absoluteCurrentTime = 0
        totalDuration = 0
        currentTrackIndex = 0
        itemId = nil
        errorMessage = nil
        isSeeking = false
        chapters = []
        lastObservedChapterId = nil
        currentTitle = ""
        currentAuthor = ""
        currentCoverURL = nil
        artworkSession = nil
        clearNowPlayingInfo()
    }

    // MARK: - Sleep timer

    func setSleepTimer(_ mode: SleepTimerMode) {
        cancelSleepTickTask()
        sleepTimer = mode
        switch mode {
        case .off:
            sleepRemainingSeconds = 0
        case .minutes(let m):
            sleepRemainingSeconds = Double(m * 60)
            if isPlaying { startSleepTickTask() }
        case .endOfBook:
            sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
        case .endOfChapter:
            sleepRemainingSeconds = wallclockRemainingUntilEndOfChapter()
        }
    }

    private func startSleepTickTask() {
        cancelSleepTickTask()
        sleepTickTask = Task { @MainActor [weak self] in
            while !Task.isCancelled {
                try? await Task.sleep(nanoseconds: 500_000_000)
                guard !Task.isCancelled, let self else { return }
                self.sleepRemainingSeconds = max(0, self.sleepRemainingSeconds - 0.5)
                if self.sleepRemainingSeconds <= 0 {
                    self.sleepTimer = .off
                    self.pause()
                    return
                }
            }
        }
    }

    private func cancelSleepTickTask() {
        sleepTickTask?.cancel()
        sleepTickTask = nil
    }

    private func updateEndOfBookSleep() {
        guard case .endOfBook = sleepTimer else { return }
        sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
        if sleepRemainingSeconds <= 0 {
            sleepTimer = .off
        }
    }

    private func wallclockRemainingUntilEndOfBook() -> Double {
        let playback = max(0, totalDuration - absoluteCurrentTime)
        let r = max(0.1, Double(rate))
        return playback / r
    }

    private func updateEndOfChapterSleep() {
        guard case .endOfChapter = sleepTimer else { return }
        sleepRemainingSeconds = wallclockRemainingUntilEndOfChapter()
        if sleepRemainingSeconds <= 0 {
            sleepTimer = .off
            pause()
        }
    }

    private func wallclockRemainingUntilEndOfChapter() -> Double {
        let chapterEnd = currentChapter?.end ?? totalDuration
        let playback = max(0, chapterEnd - absoluteCurrentTime)
        let r = max(0.1, Double(rate))
        return playback / r
    }

    // MARK: - Now-playing / remote commands

    private func configureRemoteCommandsIfNeeded() {
        guard !remoteCommandsConfigured else { return }
        remoteCommandsConfigured = true

        let center = MPRemoteCommandCenter.shared()

        center.playCommand.addTarget { [weak self] _ in
            Task { @MainActor in self?.play() }
            return .success
        }
        center.pauseCommand.addTarget { [weak self] _ in
            Task { @MainActor in self?.pause() }
            return .success
        }
        center.togglePlayPauseCommand.addTarget { [weak self] _ in
            Task { @MainActor in self?.togglePlay() }
            return .success
        }
        applyRemoteSkipInterval(seconds: Self.currentSkipSeconds())
        center.skipForwardCommand.addTarget { [weak self] _ in
            let s = Double(Self.currentSkipSeconds())
            Task { @MainActor in
                if let handler = self?.onRemoteSkip {
                    handler(s)
                } else {
                    self?.skip(by: s)
                }
            }
            return .success
        }
        center.skipBackwardCommand.addTarget { [weak self] _ in
            let s = Double(Self.currentSkipSeconds())
            Task { @MainActor in
                if let handler = self?.onRemoteSkip {
                    handler(-s)
                } else {
                    self?.skip(by: -s)
                }
            }
            return .success
        }
        NotificationCenter.default.addObserver(
            forName: UserDefaults.didChangeNotification, object: nil, queue: .main
        ) { [weak self] _ in
            Task { @MainActor [weak self] in
                self?.applyRemoteSkipInterval(seconds: Self.currentSkipSeconds())
            }
        }
        center.changePlaybackPositionCommand.addTarget { [weak self] event in
            guard let posEvent = event as? MPChangePlaybackPositionCommandEvent else {
                return .commandFailed
            }
            let target = posEvent.positionTime
            Task { @MainActor in
                if let handler = self?.onRemoteSeek {
                    handler(target)
                } else {
                    self?.seekAbsolute(target)
                }
            }
            return .success
        }
        center.changePlaybackRateCommand.supportedPlaybackRates = [0.75, 1.0, 1.25, 1.5, 1.75, 2.0]
        center.changePlaybackRateCommand.addTarget { [weak self] event in
            guard let rateEvent = event as? MPChangePlaybackRateCommandEvent else { return .commandFailed }
            Task { @MainActor in self?.setRate(rateEvent.playbackRate) }
            return .success
        }
    }

    private func updateNowPlayingInfo() {
        guard itemId != nil else {
            clearNowPlayingInfo()
            return
        }
        var info: [String: Any] = [
            MPMediaItemPropertyTitle: currentTitle,
            MPMediaItemPropertyArtist: currentAuthor,
            MPMediaItemPropertyPlaybackDuration: totalDuration,
            MPNowPlayingInfoPropertyElapsedPlaybackTime: absoluteCurrentTime,
            MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? Double(rate) : 0.0,
            MPNowPlayingInfoPropertyDefaultPlaybackRate: 1.0,
            MPNowPlayingInfoPropertyMediaType: MPNowPlayingInfoMediaType.audio.rawValue,
        ]
        if let existing = MPNowPlayingInfoCenter.default().nowPlayingInfo,
           let art = existing[MPMediaItemPropertyArtwork] {
            info[MPMediaItemPropertyArtwork] = art
        }
        MPNowPlayingInfoCenter.default().nowPlayingInfo = info
    }

    private func fetchAndAttachArtwork() {
        guard let url = currentCoverURL else { return }
        let session = artworkSession ?? URLSession.shared
        Task.detached {
            do {
                let (data, _) = try await session.data(from: url)
                guard let img = PlayerArtworkImage(data: data) else { return }
                let artwork = MPMediaItemArtwork(boundsSize: img.size) { _ in img }
                await MainActor.run { [weak self] in
                    guard let self, self.currentCoverURL == url else { return }
                    var info = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
                    info[MPMediaItemPropertyArtwork] = artwork
                    MPNowPlayingInfoCenter.default().nowPlayingInfo = info
                }
            } catch { }
        }
    }

    private func clearNowPlayingInfo() {
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
    }

    private func applyRemoteSkipInterval(seconds: Int) {
        let center = MPRemoteCommandCenter.shared()
        center.skipForwardCommand.preferredIntervals = [NSNumber(value: seconds)]
        center.skipBackwardCommand.preferredIntervals = [NSNumber(value: seconds)]
    }

    static func currentSkipSeconds() -> Int {
        let raw = UserDefaults.standard.integer(forKey: "skipDurationSeconds")
        return raw > 0 ? raw : 30
    }

    // MARK: - iOS audio session observers

    #if os(iOS)
    private func configureAudioSessionObserversIfNeeded() {
        guard !audioSessionObserversConfigured else { return }
        audioSessionObserversConfigured = true

        let center = NotificationCenter.default

        center.addObserver(
            forName: AVAudioSession.interruptionNotification,
            object: AVAudioSession.sharedInstance(),
            queue: .main
        ) { [weak self] notification in
            // Already on .main queue — assumeIsolated hops to MainActor without
            // a Task, which would otherwise capture Notification (non-Sendable).
            MainActor.assumeIsolated {
                self?.handleAudioInterruption(notification: notification)
            }
        }

        center.addObserver(
            forName: AVAudioSession.routeChangeNotification,
            object: AVAudioSession.sharedInstance(),
            queue: .main
        ) { [weak self] notification in
            MainActor.assumeIsolated {
                self?.handleRouteChange(notification: notification)
            }
        }
    }

    private func handleAudioInterruption(notification: Notification) {
        guard let info = notification.userInfo,
              let typeRaw = info[AVAudioSessionInterruptionTypeKey] as? UInt,
              let type = AVAudioSession.InterruptionType(rawValue: typeRaw) else { return }

        switch type {
        case .began:
            wasPlayingBeforeInterruption = isPlaying
            if isPlaying {
                player?.pause()
                isPlaying = false
                updateNowPlayingInfo()
            }
        case .ended:
            let optionsRaw = info[AVAudioSessionInterruptionOptionKey] as? UInt ?? 0
            let options = AVAudioSession.InterruptionOptions(rawValue: optionsRaw)
            try? AVAudioSession.sharedInstance().setActive(true)
            if wasPlayingBeforeInterruption && options.contains(.shouldResume) {
                play()
            }
        @unknown default:
            break
        }
    }

    private func handleRouteChange(notification: Notification) {
        guard let info = notification.userInfo,
              let reasonRaw = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
              let reason = AVAudioSession.RouteChangeReason(rawValue: reasonRaw) else { return }
        // Pause when headphones are unplugged (Apple's recommended behavior).
        if reason == .oldDeviceUnavailable {
            pause()
        }
    }
    #endif
}
