import AVFoundation
import Combine
import ComposableArchitecture
import Foundation
import MediaPlayer
import PlayerUtilities
import Utilities

public struct AVPlayerClient: PlayerEngine, Sendable {
    public let setup: @Sendable () async throws -> Void
    public let teardown: @Sendable () -> Void
    public let play: @Sendable () -> Void
    public let pause: @Sendable () -> Void
    public let seek: @Sendable (CMTime) async throws -> Void
    public let setVolume: @Sendable (Float) -> Void
    public let getVolume: @Sendable () -> Float
    public let replaceCurrentItem: @Sendable (String?) async throws -> CMTime
    public let setupNowPlayingInfo: @Sendable (String, String, String?, CMTime, CMTime) -> Void
    public let timeControlStatus: @Sendable () -> AsyncStream<AVPlayer.TimeControlStatus>
    public let periodicTime: @Sendable () -> AsyncStream<CMTime>
    public let didPlayToEndTime: @Sendable () -> AsyncStream<AVPlayerItem>
    public let getCurrentItem: @Sendable () async -> AVPlayerItem?
    public let isPlaybackLikelyToKeepUp: @Sendable () async -> Bool
    public let getUnderlyingPlayer: @Sendable () -> Any
    public var preloadAssets: @Sendable ([URL]) async -> Void
    public let cancelPreload: @Sendable () async -> Void
    public let setTimeUpdateInterval: @Sendable (TimeInterval) -> Void
    public let setOwner: @Sendable (PlayerOwner) async -> Void
    public let getOwner: @Sendable () async -> PlayerOwner

    public init() {
        let avPlayer = AVPlayer()
        let state = AVPlayerClientState()
        let timeScale = CMTimeScale(1000)
        let log = Logger(category: "AVPlayerClient")

        @Sendable
        func updateNowPlayingInfo(isPlaying: Bool, elapsedTime: CMTime) {
            Task { @MainActor in
                var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
                nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = elapsedTime.seconds
                nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0
                MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
            }
        }

        @Sendable
        func makeSquareImage(_ image: UIImage) -> UIImage {
            let size = min(image.size.width, image.size.height)
            let x = (image.size.width - size) / 2
            let y = (image.size.height - size) / 2

            let format = UIGraphicsImageRendererFormat()
            format.scale = image.scale
            format.opaque = true

            let renderer = UIGraphicsImageRenderer(size: CGSize(width: size, height: size), format: format)
            return renderer.image { context in
                // Fill with black to handle transparency
                UIColor.black.setFill()
                context.fill(CGRect(origin: .zero, size: CGSize(width: size, height: size)))

                // Draw the image centered
                image.draw(in: CGRect(origin: CGPoint(x: -x, y: -y), size: image.size))
            }
        }

        @Sendable
        func setupNowPlayingInfoImpl(title: String, artist: String, artworkURL: String?, 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 = artworkURL {
                    // First try to get the image from cache
                    if let url = URL(string: artworkURL),
                       let cachedImage = try? await RemoteImagePrefetcher.shared.cachedUIImage(for: url)
                    {
                        let squareImage = makeSquareImage(cachedImage)
                        nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: squareImage.size) { _ in
                            squareImage
                        }
                        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
                    } else if let url = URL(string: artworkURL) {
                        // If not in cache, trigger a load which will cache it for next time
                        RemoteImagePrefetcher.shared.loadImages(urls: [url])

                        // Still need to download for this time since we need it now
                        if let (data, _) = try? await URLSession.shared.data(from: url),
                           let artworkImage = UIImage(data: data)
                        {
                            let squareImage = makeSquareImage(artworkImage)
                            nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: squareImage.size) { _ in
                                squareImage
                            }
                            MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
                        }
                    }
                }
            }
        }

        @Sendable
        func createAndLoadPlayerItem(_ url: String) async -> AVPlayerItem? {
            guard let url = URL(string: url) else { return nil }

            // Check if we have a preloaded asset for this URL
            if let preloaded = await state.getPreloadedAsset(for: url) {
                // Remove the preloaded asset since we're using it
                await state.removePreloadedAsset(for: url)

                // Create the player item from the preloaded asset
                let item = AVPlayerItem(asset: preloaded.asset)
                return item
            }

            let asset = AVURLAsset(url: url)

            // Load the essential properties before creating the player item
            do {
                // Check for cancellation before loading
                try Task.checkCancellation()

                // Only load duration - it's the minimum we need
                let duration = try await asset.load(.duration)

                // Check for cancellation after loading
                try Task.checkCancellation()

                // Basic validation
                guard duration.isValid,
                      duration.seconds > 0,
                      !duration.seconds.isNaN
                else {
                    return nil
                }

                // Create a player item with the validated asset
                let playerItem = AVPlayerItem(asset: asset)
                return playerItem
            } catch is CancellationError {
                return nil
            } catch {
                log.telemetry.error(error, message: "Error creating player item.")
                return nil
            }
        }

        // Initialize properties with closures
        self.setup = {
            let audioSession = AVAudioSession.sharedInstance()
            try audioSession.setCategory(.playback)
            try audioSession.setActive(true)
        }

        self.teardown = {
            Task {
                await state.clearAllObservers(from: avPlayer)
            }
        }

        self.play = {
            avPlayer.play()
            updateNowPlayingInfo(isPlaying: true, elapsedTime: avPlayer.currentTime())
        }

        self.pause = {
            avPlayer.pause()
            updateNowPlayingInfo(isPlaying: false, elapsedTime: avPlayer.currentTime())
        }

        self.seek = { time in
            let tolerance = CMTime(seconds: 0.5, preferredTimescale: 600)
            try await withCheckedThrowingContinuation { continuation in
                avPlayer.seek(to: time, toleranceBefore: tolerance, toleranceAfter: tolerance) { finished in
                    if finished {
                        continuation.resume()
                    } else {
                        continuation.resume(throwing: AVPlayerClientError.seekFailed)
                    }
                }
            }

            Task {
                await state.setLastSeekTime(time)
            }

            updateNowPlayingInfo(isPlaying: avPlayer.timeControlStatus == .playing, elapsedTime: time)
        }

        self.setVolume = { volume in
            avPlayer.volume = volume
        }

        self.getVolume = {
            avPlayer.volume
        }

        self.replaceCurrentItem = { newUrl async in
            guard let newUrl,
                  let url = URL(string: newUrl)
            else {
                // Clear current item and observers
                await state.clearPeriodicTimeObserver(from: avPlayer)
                await state.clearStatusObservations(from: avPlayer)
                avPlayer.replaceCurrentItem(with: nil)
                return .zero
            }

            let audioSource = AudioSource(url: url)

            // If the clip is streaming, we need to wait for it to be ready
            // Otherwise, we can load it immediately
            if case .streaming = audioSource {
                await state.clearStatusObservations(from: avPlayer)
                let asset = AVURLAsset(url: url)

                let item = AVPlayerItem(asset: asset)

                avPlayer.replaceCurrentItem(with: item)

                return await withCheckedContinuation { continuation in
                    var didComplete: Bool = false

                    @Sendable
                    func resumeSafely(_ result: CMTime) {
                        if !didComplete {
                            didComplete = true
                            continuation.resume(returning: result)
                        }
                    }

                    // Helper method that attempts to load the AVPlayerItem
                    // with a timeout of 10 seconds and up to 3 retries.
                    @Sendable
                    func attemptToLoadItem(timeout: TimeInterval) async -> Bool {
                        return await withCheckedContinuation { attemptContinuation in
                            var didAttemptComplete: Bool = false
                            var timeoutTask: Task<Void, Error>?
                            var observation: NSKeyValueObservation?

                            func completeLoadAttempt(success: Bool) {
                                // Only resume if we haven't already
                                guard !didAttemptComplete else { return }
                                didAttemptComplete = true
                                timeoutTask?.cancel()
                                observation?.invalidate()
                                if let observation {
                                    Task { await state.removeStatusObservation(observation) }
                                }
                                attemptContinuation.resume(returning: success)
                            }

                            timeoutTask = Task {
                                try await Task.sleep(for: .seconds(timeout))
                                completeLoadAttempt(success: false)
                            }

                            observation = item.observe(\.status) { item, _ in
                                if item.status == .readyToPlay {
                                    Task {
                                        let duration = try? await item.asset.load(.duration)
                                        let fallbackDuration = CMTime(seconds: 120, preferredTimescale: 1000)
                                        resumeSafely(duration ?? fallbackDuration)
                                    }
                                    completeLoadAttempt(success: true)
                                } else if item.status == .failed {
                                    completeLoadAttempt(success: false)
                                }
                            }

                            Task {
                                guard let observation else { return }
                                await state.addStatusObservation(observation)
                            }
                        }
                    }

                    Task {
                        let maxRetries = 3
                        let timeoutSeconds: TimeInterval = 10
                        let retryDelaySeconds: TimeInterval = 5

                        for attempt in 0 ..< maxRetries {
                            let attemptSucceeded = await attemptToLoadItem(timeout: timeoutSeconds)

                            if attemptSucceeded {
                                return
                            }

                            // Retry if we haven't exceeded max retries
                            if attempt < maxRetries {
                                try await Task.sleep(for: .seconds(retryDelaySeconds))
                            }
                        }
                        // Final failure after all retries
                        resumeSafely(.zero)
                    }
                }

            } else {
                guard let item = await createAndLoadPlayerItem(newUrl)
                else {
                    return .zero
                }

                avPlayer.replaceCurrentItem(with: item)

                do {
                    let duration = try await item.asset.load(.duration)
                    return duration
                } catch {
                    log.telemetry.error(error, message: "Error loading duration.")
                    return CMTime(seconds: 120, preferredTimescale: 1000)
                }
            }
        }

        self.setupNowPlayingInfo = { title, artist, artworkURL, _, totalTime in
            setupNowPlayingInfoImpl(title: title, artist: artist, artworkURL: artworkURL, totalTime: totalTime)
        }

        self.timeControlStatus = {
            AsyncStream { continuation in
                let observation = avPlayer.observe(\.timeControlStatus, options: [.initial, .new]) { player, _ in
                    continuation.yield(player.timeControlStatus)
                }

                continuation.onTermination = { _ in
                    observation.invalidate()
                }
            }
        }

        self.periodicTime = {
            AsyncStream<CMTime> { continuation in
                Task {
                    let interval = await state.getTimeUpdateInterval()
                    let timeInterval = CMTime(seconds: interval, preferredTimescale: timeScale)

                    let observer = avPlayer.addPeriodicTimeObserver(forInterval: timeInterval, queue: .main) { time in
                        continuation.yield(time)
                    }

                    // Store observer for potential recreation
                    await state.setPeriodicTimeObserver(observer)
                    await state.setPeriodicTimeContinuation(continuation)
                }

                continuation.onTermination = { @Sendable _ in
                    Task {
                        await state.setPeriodicTimeObserver(nil)
                        await state.setPeriodicTimeContinuation(nil)
                    }
                }
            }
        }

        self.didPlayToEndTime = {
            AsyncStream { continuation in
                let observer = NotificationCenter.default.addObserver(
                    forName: AVPlayerItem.didPlayToEndTimeNotification,
                    object: nil,
                    queue: .main
                ) { notification in
                    guard let playerItem = notification.object as? AVPlayerItem,
                          playerItem === avPlayer.currentItem else { return }

                    let duration = playerItem.duration
                    let currentTime = playerItem.currentTime()

                    guard duration.isValid,
                          duration.seconds > 0,
                          !duration.seconds.isNaN,
                          abs(duration.seconds - currentTime.seconds) <= 1.0
                    else { return }

                    continuation.yield(playerItem)
                }

                continuation.onTermination = { _ in
                    NotificationCenter.default.removeObserver(observer)
                }
            }
        }

        self.getCurrentItem = {
            avPlayer.currentItem
        }

        self.isPlaybackLikelyToKeepUp = {
            avPlayer.currentItem?.isPlaybackLikelyToKeepUp ?? false
        }

        self.getUnderlyingPlayer = { avPlayer }

        self.preloadAssets = { urls in
            for url in urls {
                // Start preloading in the background - use regular Task so it inherits cancellation
                let task = Task {
                    do {
                        // Add a small delay to allow for rapid queue changes
                        try await Task.sleep(for: .milliseconds(100))

                        // Check if task was cancelled during delay
                        try Task.checkCancellation()

                        let asset = AVURLAsset(url: url)
                        let duration = try await asset.load(.duration)
                        _ = try await asset.load(.isPlayable)

                        // Check again if task was cancelled during loading
                        try Task.checkCancellation()

                        // Store the preloaded asset if it's valid
                        if duration.isValid, duration.seconds > 0 {
                            await state.setPreloadedAsset(url: url, asset: asset)
                        }
                    } catch is CancellationError {
                        // Task was cancelled - this is expected
                        return
                    } catch {
                        log.telemetry.error(error, message: "Error preloading asset.")
                    }
                }

                // Store the task immediately so it can be cancelled
                Task {
                    await state.addPreloadTask(task)
                }
            }
        }

        self.cancelPreload = {
            await state.cancelAllPreloadTasks()
            await state.clearPreloadedAsset()
        }

        self.setTimeUpdateInterval = { interval in
            Task {
                await state.setTimeUpdateInterval(interval)
                // Note: New interval will be used next time periodicTime is called
                // We don't recreate existing observers to avoid complexity/crashes
            }
        }

        let replaceItemImpl = self.replaceCurrentItem
        let seekImpl = self.seek

        self.setOwner = { newOwner in
            let currentOwner = await state.getOwner()

            // Save state when transitioning away from a player that owns the AVPlayer
            if currentOwner != .none && newOwner != currentOwner {
                if let currentItem = avPlayer.currentItem,
                   let asset = await currentItem.asset as? AVURLAsset
                {
                    let url = asset.url.absoluteString
                    let currentTime = currentItem.currentTime()
                    let wasPlaying = avPlayer.timeControlStatus == .playing

                    let savedState = SavedPlayerState(
                        url: url,
                        time: currentTime,
                        wasPlaying: wasPlaying,
                        owner: currentOwner
                    )
                    await state.setSavedPlayerState(savedState)
                }
            }

            // Set finer time updates when editing Hooks and playing snippets
            // through SnippetPlayerClient
            let timeUpdateInterval: TimeInterval = newOwner.timeUpdateInterval
            await state.setTimeUpdateInterval(timeUpdateInterval)

            // Restore state if we have a saved state for the new owner
            if let savedState = await state.getSavedPlayerState(),
               savedState.owner == newOwner
            {
                _ = try? await replaceItemImpl(savedState.url)
                if savedState.time.seconds > 0 {
                    try? await seekImpl(savedState.time)
                }
                if savedState.wasPlaying {
                    avPlayer.play()
                }
                await state.clearSavedPlayerState()
            }

            await state.setOwner(newOwner)
        }

        self.getOwner = {
            await state.getOwner()
        }
    }

    // For testing
    public init(
        setup: @escaping @Sendable () async throws -> Void,
        teardown: @escaping @Sendable () -> Void,
        play: @escaping @Sendable () -> Void,
        pause: @escaping @Sendable () -> Void,
        seek: @escaping @Sendable (CMTime) async throws -> Void,
        setVolume: @escaping @Sendable (Float) -> Void,
        getVolume: @escaping @Sendable () -> Float,
        replaceCurrentItem: @escaping @Sendable (String?) async throws -> CMTime,
        setupNowPlayingInfo: @escaping @Sendable (String, String, String?, CMTime, CMTime) -> Void,
        timeControlStatus: @escaping @Sendable () -> AsyncStream<AVPlayer.TimeControlStatus>,
        periodicTime: @escaping @Sendable () -> AsyncStream<CMTime>,
        didPlayToEndTime: @escaping @Sendable () -> AsyncStream<AVPlayerItem>,
        getCurrentItem: @escaping @Sendable () async -> AVPlayerItem?,
        isPlaybackLikelyToKeepUp: @escaping @Sendable () async -> Bool,
        getUnderlyingPlayer: @escaping @Sendable () -> Any,
        preloadAssets: @escaping @Sendable ([URL]) async -> Void,
        cancelPreload: @escaping @Sendable () async -> Void,
        setTimeUpdateInterval: @escaping @Sendable (TimeInterval) -> Void,
        setOwner: @escaping @Sendable (PlayerOwner) async -> Void,
        getOwner: @escaping @Sendable () async -> PlayerOwner
    ) {
        self.setup = setup
        self.teardown = teardown
        self.play = play
        self.pause = pause
        self.seek = seek
        self.setVolume = setVolume
        self.getVolume = getVolume
        self.replaceCurrentItem = replaceCurrentItem
        self.setupNowPlayingInfo = setupNowPlayingInfo
        self.timeControlStatus = timeControlStatus
        self.periodicTime = periodicTime
        self.didPlayToEndTime = didPlayToEndTime
        self.getCurrentItem = getCurrentItem
        self.isPlaybackLikelyToKeepUp = isPlaybackLikelyToKeepUp
        self.getUnderlyingPlayer = getUnderlyingPlayer
        self.preloadAssets = preloadAssets
        self.cancelPreload = cancelPreload
        self.setTimeUpdateInterval = setTimeUpdateInterval
        self.setOwner = setOwner
        self.getOwner = getOwner
    }

    public enum AudioSource {
        case streaming(url: URL)
        case file(url: URL)

        init(url: URL) {
            if url.host?.contains("audiopipe") == true ||
                (!url.pathExtension.isEmpty && !["mp3", "wav", "flac", "mp4"].contains(url.pathExtension.lowercased()))
            {
                self = .streaming(url: url)
            } else {
                self = .file(url: url)
            }
        }
    }
}

public enum AVPlayerClientError: Error {
    case seekFailed
}
