import AVFoundation
import Combine
import ComposableArchitecture
import Foundation
import MediaPlayer

public struct PlayerClient {
    public var setup: () throws -> Void
    public var teardown: () -> Void
    public var replaceCurrentItem: (String?) async -> (CMTime)
    public var replaceCurrentItemAndPlay: (String?) async -> (CMTime)
    public var play: () -> Void
    public var pause: () -> Void
    public var seek: (CMTime) -> Void
    public var setupNowPlayingInfo: (_ title: String, _ artist: String, _ artworkURL: String?, _ elapsedTime: CMTime, _ totalTime: CMTime) -> Void
    public var timeControlStatus: () -> AnyPublisher<AVPlayer.TimeControlStatus, Never>
    public var periodicTime: () -> AnyPublisher<CMTime, Never>
    public var getCurrentItem: () -> AVPlayerItem?
    public var isPlaybackLikelyToKeepUp: () -> Bool
    public var getAVPlayer: () -> AVPlayer
}

extension PlayerClient: DependencyKey {
    public static let liveValue: Self = {
        let avPlayer = AVPlayer()
        var periodicTimeObserver: Any?
        var totalTimeObserver: Any?

        let timeScale = CMTimeScale(1000)
        let time = CMTime(seconds: 0.5, preferredTimescale: timeScale)

        @Dependency(\.mainQueue) var mainQueue

        func replace(_ url: String?) {
            let url = URL(string: url!)!
            let playerItem = AVPlayerItem(url: url)
            avPlayer.replaceCurrentItem(with: playerItem)
        }

        @Sendable
        func updateNowPlayingInfo(isPlaying: Bool, elapsedTime: CMTime) {
            Task { @MainActor in
                var nowPlayingInfo: [String: Any] = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
                // The order of these two lines is important
                // If you set elapsed time, the playback rate gets reset
                // and you'll see the wrong play/pause icon on the lock screen
                nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = elapsedTime.seconds
                nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0
                MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
            }
        }

        @Sendable
        func setupNowPlayingInfo(title: String, artist: String, artworkURL: String?, elapsedTime _: CMTime, totalTime: CMTime) {
            Task { @MainActor in
                var nowPlayingInfo: [String: Any] = [
                    MPMediaItemPropertyTitle: title,
                    MPMediaItemPropertyArtist: artist,
                    MPNowPlayingInfoPropertyElapsedPlaybackTime: 0,
                ]

                if !totalTime.seconds.isNaN {
                    nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = totalTime.seconds
                }

                MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo

                if let artworkURL, let url = URL(string: artworkURL),
                   let (data, _) = try? await URLSession.shared.data(from: url),
                   let artworkImage = UIImage(data: data)
                {
                    nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: artworkImage.size) { _ in
                        artworkImage
                    }
                    MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
                }
            }
        }

        return Self(
            setup: {
                let audioSession = AVAudioSession.sharedInstance()
                try audioSession.setCategory(.ambient) // Clears out any other `playback` options in `VideoCoverClient`, like `.mixWithOthers`
                try audioSession.setCategory(.playback)
                try audioSession.setActive(true)
            },
            teardown: {
                // TODO: Where to call this
            },
            replaceCurrentItem: { @MainActor newUrl in
                if let currentItem = avPlayer.currentItem {
                    if let currentURL = (avPlayer.currentItem?.asset as? AVURLAsset)?.url {
                        if currentURL.absoluteString != newUrl {
                            replace(newUrl)
                        } else {
                            // Do nothing if the URL is the same
                            await avPlayer.seek(to: .zero)
                        }
                    } else {
                        replace(newUrl)
                    }
                } else {
                    replace(newUrl)
                }
                let duration = try? await avPlayer.currentItem?.asset.load(.duration)
                return duration ?? .zero
            },
            replaceCurrentItemAndPlay: { @MainActor newUrl in
                if let currentItem = avPlayer.currentItem {
                    if let currentURL = (avPlayer.currentItem?.asset as? AVURLAsset)?.url {
                        if currentURL.absoluteString != newUrl {
                            replace(newUrl)
                        } else {
                            // Do nothing if the URL is the same
                            await avPlayer.seek(to: .zero)
                        }
                    } else {
                        replace(newUrl)
                    }
                } else {
                    replace(newUrl)
                }
                let duration = try? await avPlayer.currentItem?.asset.load(.duration)
                avPlayer.play() // Always play the item
                return duration ?? .zero
            },
            play: {
                avPlayer.play()
                updateNowPlayingInfo(isPlaying: true, elapsedTime: avPlayer.currentTime())
            },
            pause: {
                avPlayer.pause()
                updateNowPlayingInfo(isPlaying: false, elapsedTime: avPlayer.currentTime())
            },
            seek: { time in
                avPlayer.seek(to: time)
                updateNowPlayingInfo(isPlaying: avPlayer.timeControlStatus == .playing, elapsedTime: time)
            },
            setupNowPlayingInfo: { title, artist, artworkURL, _, totalTime in
                setupNowPlayingInfo(title: title, artist: artist, artworkURL: artworkURL, elapsedTime: .zero, totalTime: totalTime)
            },
            timeControlStatus: {
                avPlayer.publisher(for: \.timeControlStatus)
                    .receive(on: mainQueue)
                    .eraseToAnyPublisher()
            },
            periodicTime: {
                let currentTime = avPlayer.currentTime()
                let publisher = CurrentValueSubject<CMTime, Never>(currentTime)
                if let periodicTimeObserver {
                    avPlayer.removeTimeObserver(periodicTimeObserver)
                }
                periodicTimeObserver = avPlayer.addPeriodicTimeObserver(forInterval: time, queue: .main) { time in
                    publisher.send(time)
                }
                return publisher
                    .receive(on: mainQueue)
                    .eraseToAnyPublisher()
            },
            getCurrentItem: {
                avPlayer.currentItem
            },
            isPlaybackLikelyToKeepUp: {
                avPlayer.currentItem?.isPlaybackLikelyToKeepUp ?? false
            },
            getAVPlayer: { avPlayer }
        )
    }()
}
