import Foundation
import Observation

@Observable
@MainActor
final class AppState {
    let auth: AuthStore
    let client: ABSClient
    let network: NetworkMonitor
    let downloads: DownloadManager
    let sync: ProgressSyncManager
    let player: PlayerEngine

    var currentItem: LibraryItem?
    var isPreparingPlayback: Bool = false

    /// Map: PlaybackProgress.syncKey -> PlaybackProgress (server-known progress).
    /// Used to show progress bars on covers in the library views.
    var progressCache: [String: PlaybackProgress] = [:]

    private var syncTimer: Timer?
    private var lastReportedSecond: Double = -10

    init() {
        let auth = AuthStore()
        let client = ABSClient(auth: auth)
        self.auth = auth
        self.client = client
        self.network = NetworkMonitor()
        self.downloads = DownloadManager(client: client)
        self.sync = ProgressSyncManager(client: client)
        self.player = PlayerEngine()
    }

    func bootstrap() async {
        auth.restoreSession()
        network.start { [weak self] online in
            guard let self else { return }
            if online {
                Task { [weak self] in
                    await self?.sync.drain()
                    await self?.refreshProgressCache()
                }
            }
        }
        if auth.isLoggedIn {
            let ok = await client.validateToken()
            if !ok {
                auth.logout()
            } else {
                await sync.drain()
                await refreshProgressCache()
            }
        }
    }

    /// Pulls the entire progress map from the server (via /api/me).
    func refreshProgressCache() async {
        guard network.isOnline, auth.isLoggedIn else { return }
        do {
            let all = try await client.fetchAllProgress()
            progressCache = Dictionary(all.map { ($0.syncKey, $0) }, uniquingKeysWith: { _, new in new })
        } catch {
            // non-fatal
        }
    }

    /// Local update for the cache while we're actively playing.
    func cacheProgress(itemId: String, episodeId: String?, currentTime: Double, duration: Double, isFinished: Bool) {
        let p = PlaybackProgress(
            itemId: itemId, episodeId: episodeId,
            currentTime: currentTime, duration: duration,
            isFinished: isFinished, updatedAt: Date()
        )
        progressCache[p.syncKey] = p
    }

    func progress(for item: LibraryItem) -> PlaybackProgress? {
        progressCache[item.syncKey]
    }

    func progressFraction(itemId: String, episodeId: String? = nil) -> Double {
        let key = episodeId.map { "\(itemId)|\($0)" } ?? itemId
        guard let p = progressCache[key], p.duration > 0 else { return 0 }
        if p.isFinished { return 1.0 }
        return min(1, max(0, p.currentTime / p.duration))
    }

    func play(item: LibraryItem) async {
        if currentItem?.id == item.id, currentItem?.episodeId == item.episodeId, player.isReady {
            player.play()
            return
        }
        stopPlayback(reportFinal: true)
        isPreparingPlayback = true
        defer { isPreparingPlayback = false }

        var workItem = item
        // Only fetch detail for books with empty audioFiles (podcast episodes
        // arrive with their single audioFile already populated by the caller).
        if !workItem.isPodcast && workItem.audioFiles.isEmpty && network.isOnline {
            let alreadyDownloaded = downloads.isDownloaded(downloadKey: item.downloadKey)
            if !alreadyDownloaded, let detail = try? await client.fetchItemDetail(itemId: item.id) {
                workItem = detail
            }
        }

        var startAt: Double = 0
        if network.isOnline {
            if let p = try? await client.fetchProgress(itemId: item.id, episodeId: workItem.episodeId) {
                // Replaying a finished item (or one with progress essentially at the end)
                // should start from the beginning, not drop the user at the last few seconds.
                let nearEnd = p.duration > 0 && p.currentTime >= p.duration - 10
                if !p.isFinished && !nearEnd {
                    startAt = p.currentTime
                }
            }
        }

        currentItem = workItem
        player.load(item: workItem, client: client, downloads: downloads, startAt: startAt)
        if player.errorMessage == nil {
            player.play()
            startSyncTimer()
        }
    }

    /// Convenience for podcast episodes.
    func play(podcast: LibraryItem, episode: PodcastEpisode) async {
        var synthetic = LibraryItem(
            id: podcast.id,
            title: episode.title,
            author: podcast.title,
            durationSeconds: episode.durationSeconds > 0 ? episode.durationSeconds : episode.audioFile.durationSeconds,
            audioFiles: [episode.audioFile]
        )
        synthetic.mediaType = "podcast"
        synthetic.episodeId = episode.id
        await play(item: synthetic)
    }

    func stopPlayback(reportFinal: Bool = true) {
        if reportFinal { reportProgress(force: true) }
        syncTimer?.invalidate()
        syncTimer = nil
        player.teardown()
        currentItem = nil
        lastReportedSecond = -10
    }

    func togglePlay() {
        guard currentItem != nil else { return }
        player.togglePlay()
        if !player.isPlaying { reportProgress(force: true) }
    }

    func skip(by seconds: Double) {
        guard currentItem != nil else { return }
        player.skip(by: seconds)
        reportProgress(force: true)
    }

    func seekAbsolute(_ target: Double) {
        guard currentItem != nil else { return }
        player.seekAbsolute(target)
        reportProgress(force: true)
    }

    func setRate(_ newRate: Float) {
        player.setRate(newRate)
    }

    private func startSyncTimer() {
        syncTimer?.invalidate()
        let timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in
            Task { @MainActor [weak self] in
                self?.reportProgress(force: false)
            }
        }
        RunLoop.main.add(timer, forMode: .common)
        syncTimer = timer
    }

    private func reportProgress(force: Bool) {
        guard let item = currentItem else { return }
        let t = player.absoluteCurrentTime
        let d = player.totalDuration
        guard d > 0 else { return }
        if !force && abs(t - lastReportedSecond) < 3 { return }
        lastReportedSecond = t
        let finished = (d - t) < 30

        cacheProgress(
            itemId: item.id,
            episodeId: item.episodeId,
            currentTime: t,
            duration: d,
            isFinished: finished
        )

        Task {
            await sync.report(
                itemId: item.id,
                episodeId: item.episodeId,
                currentTime: t,
                duration: d,
                isFinished: finished,
                isOnline: network.isOnline
            )
        }
    }
}

extension LibraryItem {
    /// Matches PlaybackProgress.syncKey for cache lookups.
    var syncKey: String {
        if let episodeId { return "\(id)|\(episodeId)" }
        return id
    }

    /// The DownloadManager keys downloads by this composite identifier,
    /// allowing the same podcast item to host multiple per-episode downloads.
    var downloadKey: String { syncKey }
}
