import AVKit
import ComposableArchitecture
import Utilities
import SwiftUI

/**
 Almost a carbon copy of `LoopingVideoPlayer` with the exception that this purposefully sets  AVAudioSession to mix with others when initializing `AVQueuePlayer`
 */

public class WelcomeLoopingVideoPlayerUIView: UIView {
    override public class var layerClass: AnyClass {
        AVPlayerLayer.self
    }

    private let log = Logger(category: "WelcomeLoopingVideoPlayerUIView")
    private var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
    private var playerLooper: AVPlayerLooper?
    private var hostingController: UIHostingController<AnyView>?
    private var player: AVQueuePlayer?
    private var hasSetupPlayer = false
    private var autoPlay = true
    private var videoGravity: AVLayerVideoGravity = .resizeAspectFill
    private var fadeIn: Bool = false

    public init(
        asset: AVAsset?,
        playableUrl: URL?,
        fallbackUrl: String?,
        autoPlay: Bool,
        fallbackView: AnyView,
        videoGravity: AVLayerVideoGravity = .resizeAspectFill,
        fadeIn: Bool
    ) {
        super.init(frame: .zero)
        self.videoGravity = videoGravity

        if let asset = asset {
            setupPlayer(with: asset)
        } else if let playableUrl = playableUrl {
            setupPlayer(with: AVAsset(url: playableUrl))
        } else if let fallbackUrl = fallbackUrl,
                  let url = URL(string: fallbackUrl)
        {
            if let cachedUrl = getCachedVideoUrl(for: url.lastPathComponent) {
                setupPlayer(with: AVAsset(url: cachedUrl))
            } else {
                setupFallbackView(fallbackView)
                downloadAndSetupVideo(from: url)
            }
        } else {
            setupFallbackView(fallbackView)
        }

        self.autoPlay = autoPlay
        self.fadeIn = fadeIn
    }

    override public func observeValue(forKeyPath keyPath: String?, of _: Any?, change _: [NSKeyValueChangeKey: Any]?, context _: UnsafeMutableRawPointer?) {
        if keyPath == #keyPath(AVPlayer.currentItem.isPlaybackLikelyToKeepUp) {
            if let player = player, player.currentItem?.isPlaybackLikelyToKeepUp == true, !hasSetupPlayer {
                DispatchQueue.main.async {
                    self.playerLayer.player = player
                    self.hasSetupPlayer = true
                    if self.autoPlay {
                        self.player?.play()
                    }

                    if self.fadeIn {
                        // Fade in the video layer
                        self.playerLayer.opacity = 0
                        CATransaction.begin()
                        CATransaction.setAnimationDuration(0.5) // Adjust duration as needed

                        let fadeAnimation = CABasicAnimation(keyPath: "opacity")
                        fadeAnimation.fromValue = 0.0
                        fadeAnimation.toValue = 1.0
                        fadeAnimation.duration = 0.5 // Match the transaction duration

                        self.playerLayer.add(fadeAnimation, forKey: "fadeIn")
                        self.playerLayer.opacity = 1.0

                        CATransaction.commit()
                    }
                }
            }
        }
    }

    override public func layoutSubviews() {
        super.layoutSubviews()
        hostingController?.view.frame = bounds
        playerLayer.frame = bounds
        playerLayer.videoGravity = videoGravity
    }

    public func play() {
        guard player?.currentItem?.isPlaybackLikelyToKeepUp == true else { return }
        player?.play()
    }

    public func pause() {
        player?.pause()
    }

    public func restartVideo() {
        player?.isMuted = true
        player?.seek(to: .zero)
        play()
    }

    public func seekTo(_ time: CMTime) {
        guard let player = player,
              let currentItem = player.currentItem else { return }

        // Get the asset duration and apply modulus
        let duration = currentItem.duration
        if duration.isValid && duration.seconds > 0 {
            let currentTime = currentItem.currentTime()
            let normalizedSeconds = time.seconds.truncatingRemainder(dividingBy: duration.seconds)

            // Only seek if we're more than 0.25 second away from target time
            if abs(currentTime.seconds - normalizedSeconds) > 0.25 {
                let normalizedTime = CMTime(seconds: normalizedSeconds, preferredTimescale: time.timescale)
                player.seek(to: normalizedTime)
            }
        }
    }

    public func setVideoGravity(_ videoGravity: AVLayerVideoGravity) {
        self.videoGravity = videoGravity
        setNeedsLayout()
    }

    deinit {
        player?.removeObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackLikelyToKeepUp))
    }

    @available(*, unavailable)
    required init?(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

private extension WelcomeLoopingVideoPlayerUIView {
    func setupPlayer(with asset: AVAsset) {
        @Dependency(WelcomeLoopingVideoPlayerClient.self) var welcomeLoopingVideoPlayerClient
        DispatchQueue.main.async {
            let playerItem = AVPlayerItem(asset: asset)

            // MARK: Deviation from `LoopingVideoPlayer`

            welcomeLoopingVideoPlayerClient.setAVAudioSessionToMixWithOthers()

            self.player = AVQueuePlayer(playerItem: playerItem)
            self.player?.isMuted = true

            self.player?.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackLikelyToKeepUp), options: .new, context: nil)

            guard let player = self.player else { return }
            self.playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)

            self.hostingController?.view.removeFromSuperview()
            self.hostingController = nil
        }
    }

    func setupFallbackView(_ fallbackView: AnyView) {
        hostingController = UIHostingController(rootView: fallbackView)
        hostingController?.view.backgroundColor = .clear
        guard let fallbackView = hostingController?.view else { return }
        addSubview(fallbackView)
        fallbackView.frame = bounds
    }

    func downloadAndSetupVideo(from url: URL) {
        let downloadTask = URLSession.shared.downloadTask(with: url) { [weak self] tempLocalUrl, _, error in
            guard let self, let tempLocalUrl, error == nil else { return }

            do {
                let downloadedData = try Data(contentsOf: tempLocalUrl)

                DispatchQueue.main.async {
                    let fileName = url.lastPathComponent
                    guard let permanentUrl = self.saveVideoFile(data: downloadedData, fileName: fileName),
                          self.isValidVideoFormat(url: permanentUrl)
                    else {
                        return
                    }

                    let asset = AVAsset(url: permanentUrl)
                    self.setupPlayer(with: asset)
                }
            } catch {
                log.telemetry.error(error, message: "Error processing downloaded file.")
            }
        }
        downloadTask.resume()
    }

    func saveVideoFile(data: Data, fileName: String) -> URL? {
        guard let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
            return nil
        }

        let fileUrl = cachesDirectory.appendingPathComponent(fileName)

        do {
            try data.write(to: fileUrl)
            return fileUrl
        } catch {
            return nil
        }
    }

    func isValidVideoFormat(url: URL) -> Bool {
        let validExtensions = ["mp4", "mov"]
        let fileExtension = url.pathExtension.lowercased()

        return validExtensions.contains(fileExtension)
    }

    func getCachedVideoUrl(for fileName: String) -> URL? {
        guard let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
            return nil
        }

        let cachedFilePath = cachesDirectory.appendingPathComponent(fileName)

        guard FileManager.default.fileExists(atPath: cachedFilePath.path) else { return nil }
        return cachedFilePath
    }
}

public struct WelcomeLoopingVideoPlayer: UIViewRepresentable {
    let asset: AVAsset?
    let playableUrl: URL?
    let fallbackUrl: String?
    let restartBeforePlaying: Bool
    let autoPlay: Bool
    let fallbackView: AnyView
    let overrideTime: CMTime?
    let videoGravity: AVLayerVideoGravity
    let fadeIn: Bool
    @Binding var isPlaying: Bool

    public init(
        asset: AVAsset? = nil,
        playableUrl: URL? = nil,
        fallbackUrl: String? = nil,
        restartBeforePlaying: Bool = false,
        autoPlay: Bool = true,
        isPlaying: Binding<Bool>,
        overrideTime: CMTime? = nil,
        videoGravity: AVLayerVideoGravity = .resizeAspectFill,
        @ViewBuilder fallbackView: @escaping () -> some View,
        fadeIn: Bool = false
    ) {
        self.asset = asset
        self.playableUrl = playableUrl
        self.fallbackUrl = fallbackUrl
        self.restartBeforePlaying = restartBeforePlaying
        self.autoPlay = autoPlay
        self.fallbackView = AnyView(fallbackView())
        self._isPlaying = isPlaying
        self.overrideTime = overrideTime
        self.videoGravity = videoGravity
        self.fadeIn = fadeIn
    }

    public func makeUIView(context _: Context) -> WelcomeLoopingVideoPlayerUIView {
        let view = WelcomeLoopingVideoPlayerUIView(
            asset: asset,
            playableUrl: playableUrl,
            fallbackUrl: fallbackUrl,
            autoPlay: autoPlay,
            fallbackView: fallbackView,
            videoGravity: videoGravity,
            fadeIn: fadeIn
        )
        return view
    }

    public func updateUIView(_ uiView: WelcomeLoopingVideoPlayerUIView, context _: Context) {
        if let overrideTime = overrideTime, !overrideTime.isInvalid {
            uiView.seekTo(overrideTime)
        }

        if restartBeforePlaying {
            uiView.restartVideo()
        }

        if isPlaying {
            uiView.play()
        } else {
            uiView.pause()
        }
    }
}
