import AVKit
import Utilities
import SwiftUI

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

    private let log = Logger(category: "LoopingVideoPlayerUIView")
    private let player = AVPlayer()
    private var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
    private var hostingController: UIHostingController<AnyView>?
    private var autoPlay = true
    private var videoGravity: AVLayerVideoGravity = .resizeAspectFill
    private var fadeIn: Bool = false
    private var endTimeObserver: NSObjectProtocol?
    private var onVideoEnd: (() -> Void)?
    var lastRestartTrigger: Int = 0

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

        // Set player on layer immediately
        self.playerLayer.player = self.player

        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)
        }
    }

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

    public func play() {
        player.play()
    }

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

    public var isMuted: Bool {
        get { player.isMuted }
        set { player.isMuted = newValue }
    }

    public var volume: Float {
        get { player.volume }
        set { player.volume = newValue }
    }

    public func restartVideo() {
        player.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero)
        player.play()
    }

    public func seekTo(_ time: CMTime) {
        Task { @MainActor in
            guard let currentItem = player.currentItem,
                  currentItem.duration.isValid && currentItem.duration.seconds > 0
            else { return }

            let normalizedSeconds = time.seconds.truncatingRemainder(dividingBy: currentItem.duration.seconds)
            let normalizedTime = CMTime(seconds: normalizedSeconds, preferredTimescale: time.timescale)
            await player.seek(to: normalizedTime, toleranceBefore: .zero, toleranceAfter: .zero)
        }
    }

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

    public func setOnVideoEnd(_ callback: (() -> Void)?) {
        self.onVideoEnd = callback
    }

    deinit {
        log.debug("LoopingVideoPlayerUIView is being deinitialized")
        removeEndTimeObserver()
        player.pause()
        player.replaceCurrentItem(with: nil)
    }

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

private extension LoopingVideoPlayerUIView {
    func setupPlayer(with asset: AVAsset) {
        log.debug("Setting up player with asset: \(asset)")
        Task { @MainActor in
            // Create the player item
            let playerItem = AVPlayerItem(asset: asset)

            // Configure the player
            player.replaceCurrentItem(with: playerItem)
            player.isMuted = true
            player.actionAtItemEnd = .none // Required for proper looping

            // Configure player layer
            playerLayer.player = player

            // Remove fallback view if present
            hostingController?.view.removeFromSuperview()
            hostingController = nil

            // Setup looping functionality
            setupLooping()

            if fadeIn {
                applyFadeInAnimation()
            }

            if autoPlay {
                player.play()
            }
        }
    }

    func setupLooping() {
        // Remove any existing observation
        removeEndTimeObserver()

        // Add new notification observer for when the item reaches the end
        endTimeObserver = NotificationCenter.default.addObserver(
            forName: .AVPlayerItemDidPlayToEndTime,
            object: player.currentItem,
            queue: .main
        ) { [weak self] _ in
            guard let self = self else { return }

            // Seek synchronously for immediate restart
            self.player.seek(to: .zero)

            // Notify audio to sync, then immediately restart
            self.onVideoEnd?()
            self.player.play()
        }
    }

    func removeEndTimeObserver() {
        if let observer = endTimeObserver {
            NotificationCenter.default.removeObserver(observer)
            endTimeObserver = nil
        }
    }

    func applyFadeInAnimation() {
        playerLayer.opacity = 0
        UIView.animate(withDuration: 0.5) {
            self.playerLayer.opacity = 1.0
        }
    }

    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) {
        Task { @MainActor in
            do {
                let (tempLocalUrl, _) = try await URLSession.shared.download(from: url)
                let downloadedData = try Data(contentsOf: tempLocalUrl)

                let fileName = url.lastPathComponent
                guard let permanentUrl = saveVideoFile(data: downloadedData, fileName: fileName),
                      isValidVideoFormat(url: permanentUrl)
                else {
                    log.telemetry.assertionFailure("Failed to save or validate downloaded video file")
                    return
                }

                let asset = AVAsset(url: permanentUrl)
                setupPlayer(with: asset)
            } catch {
                log.telemetry.error(error, message: "Error downloading or processing video.")
            }
        }
    }

    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 LoopingVideoPlayer: 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
    let isMuted: Bool
    let volume: Float
    let onVideoEnd: (() -> Void)?
    let restartTrigger: Int

    public init(
        asset: AVAsset? = nil,
        playableUrl: URL? = nil,
        fallbackUrl: String? = nil,
        restartBeforePlaying: Bool = false,
        autoPlay: Bool = true,
        isPlaying: Binding<Bool>,
        isMuted: Bool = true,
        volume: Float = 1.0,
        overrideTime: CMTime? = nil,
        videoGravity: AVLayerVideoGravity = .resizeAspectFill,
        @ViewBuilder fallbackView: @escaping () -> some View,
        fadeIn: Bool = false,
        onVideoEnd: (() -> Void)? = nil,
        restartTrigger: Int = 0
    ) {
        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
        self.isMuted = isMuted
        self.volume = volume
        self.onVideoEnd = onVideoEnd
        self.restartTrigger = restartTrigger
    }

    public func makeUIView(context _: Context) -> LoopingVideoPlayerUIView {
        let view = LoopingVideoPlayerUIView(
            asset: asset,
            playableUrl: playableUrl,
            fallbackUrl: fallbackUrl,
            autoPlay: autoPlay,
            fallbackView: fallbackView,
            videoGravity: videoGravity,
            fadeIn: fadeIn,
            onVideoEnd: onVideoEnd
        )
        view.isMuted = isMuted
        view.volume = volume
        view.lastRestartTrigger = restartTrigger
        return view
    }

    public func updateUIView(_ uiView: LoopingVideoPlayerUIView, context _: Context) {
        // Check if restart trigger has changed - do this first and synchronously
        if restartTrigger != uiView.lastRestartTrigger {
            uiView.lastRestartTrigger = restartTrigger
            uiView.restartVideo()
        }

        if let overrideTime = overrideTime, !overrideTime.isInvalid {
            uiView.seekTo(overrideTime)
        }

        if restartBeforePlaying {
            uiView.restartVideo()
        }

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

        uiView.isMuted = isMuted
        uiView.volume = volume
    }
}
