@preconcurrency import AVFoundation
import Combine
import ComposableArchitecture
import SwiftUI

public enum VideoPreviewClientError: Error {
    case loadVideoTimeout
    case failedToGenerateThumbnails
}

public struct VideoPreviewClient {
    public var loadVideo: (URL, _ isMuted: Bool, _ cachedFileName: String?) async throws -> (AVAsset, AVPlayer)
    public var generateThumbnails: (AVAsset, Int) async throws -> [UIImage]
    public var seek: (CMTime) -> Void
    public var play: () -> Void
    public var pause: () -> Void
    public var periodicTime: () -> AnyPublisher<CMTime, Never>
    public var teardown: () -> Void
}

extension VideoPreviewClient: DependencyKey {
    public static let liveValue: Self = {
        var avPlayer: AVPlayer?
        var periodicTimeObserver: Any?

        return Self(
            loadVideo: { url, isMuted, cachedFileName in
                let asset = AVAsset(url: url)
                if let cachedFileName = cachedFileName,
                   let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
                {
                    let cachedFilePath = cachesDirectory.appendingPathComponent(cachedFileName)
                    if FileManager.default.fileExists(atPath: cachedFilePath.path) {
                        return try await loadVideoFromAsset(AVAsset(url: cachedFilePath), isMuted: isMuted, currentPlayer: avPlayer) { newPlayer in
                            avPlayer = newPlayer
                        }
                    }
                }

                return try await loadVideoFromAsset(asset, isMuted: isMuted, currentPlayer: avPlayer) { newPlayer in
                    avPlayer = newPlayer
                }
            },
            generateThumbnails: { asset, count in
                let imageGenerator = AVAssetImageGenerator(asset: asset)
                imageGenerator.appliesPreferredTrackTransform = true

                do {
                    let duration = try await asset.load(.duration)
                    let step = CMTimeGetSeconds(duration) / Double(count)

                    var thumbnails: [UIImage] = []
                    for i in 0 ..< count {
                        let time = CMTime(seconds: Double(i) * step, preferredTimescale: 600)
                        if let cgImage = try? imageGenerator.copyCGImage(at: time, actualTime: nil) {
                            thumbnails.append(UIImage(cgImage: cgImage))
                        }
                    }
                    return thumbnails
                } catch {
                    throw VideoPreviewClientError.failedToGenerateThumbnails
                }
            },
            seek: { time in
                avPlayer?.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
            },
            play: {
                avPlayer?.play()
            },
            pause: {
                avPlayer?.pause()
            },
            periodicTime: {
                let publisher = CurrentValueSubject<CMTime, Never>(.zero)
                if let existingObserver = periodicTimeObserver {
                    avPlayer?.removeTimeObserver(existingObserver)
                    periodicTimeObserver = nil
                }
                let timeScale = CMTimeScale(1000)
                let interval = CMTime(seconds: 0.01, preferredTimescale: timeScale)
                periodicTimeObserver = avPlayer?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
                    publisher.send(time)
                }
                return publisher
                    .eraseToAnyPublisher()
            },
            teardown: {
                if let existingObserver = periodicTimeObserver {
                    avPlayer?.removeTimeObserver(existingObserver)
                    periodicTimeObserver = nil
                }

                avPlayer?.pause()
                avPlayer?.replaceCurrentItem(with: nil)
                avPlayer = nil
            }
        )
    }()
}

private extension VideoPreviewClient {
    static func loadVideoFromAsset(
        _ asset: AVAsset,
        isMuted: Bool,
        currentPlayer: AVPlayer?,
        updatePlayer: @escaping (AVPlayer) -> Void
    ) async throws -> (AVAsset, AVPlayer) {
        _ = try await asset.load(.isPlayable)

        let playerItem = await AVPlayerItem(asset: asset)

        return try await withCheckedThrowingContinuation { continuation in
            Task { @MainActor in
                let player: AVPlayer
                if let existingPlayer = currentPlayer {
                    existingPlayer.replaceCurrentItem(with: playerItem)
                    player = existingPlayer
                } else {
                    player = AVPlayer(playerItem: playerItem)
                    updatePlayer(player)
                }

                var observation: NSKeyValueObservation?
                var hasResumed = false

                observation = playerItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new, .initial]) { item, _ in
                    Task { @MainActor in
                        if item.isPlaybackLikelyToKeepUp, !hasResumed {
                            hasResumed = true
                            player.isMuted = isMuted
                            observation?.invalidate()
                            continuation.resume(returning: (asset, player))
                        }
                    }
                }

                Task {
                    try await Task.sleep(for: .seconds(5))
                    if !hasResumed {
                        hasResumed = true
                        observation?.invalidate()
                        continuation.resume(throwing: VideoPreviewClientError.loadVideoTimeout)
                    }
                }
            }
        }
    }
}
