import Foundation
@preconcurrency import React
@preconcurrency import BBNativePlayerKit
#if canImport(GoogleCast)
import GoogleCast
#endif


/**
 * Global registry for BBPlayerView instances.
 * Needed because bridge.uiManager.view(forReactTag:) doesn't work
 * with Fabric. Views register themselves when added to the window.
 */
final class BBPlayerViewRegistry: NSObject, @unchecked Sendable {
    static let shared = BBPlayerViewRegistry()

    private var views: [Int: BBPlayerView] = [:]
    private let lock = NSLock()

    private override init() {
        super.init()
    }

    func register(_ view: BBPlayerView, tag: Int) {
        lock.lock()
        defer { lock.unlock() }
        views[tag] = view
        NSLog("BBPlayerViewRegistry: Registered view with tag %d (total: %d)", tag, views.count)
    }

    func unregister(tag: Int) {
        lock.lock()
        defer { lock.unlock() }
        views.removeValue(forKey: tag)
        NSLog("BBPlayerViewRegistry: Unregistered view with tag %d (total: %d)", tag, views.count)
    }

    func getView(tag: Int) -> BBPlayerView? {
        lock.lock()
        defer { lock.unlock() }
        return views[tag]
    }
}

/**
 * Native Module for BBPlayer commands.
 * Extends RCTEventEmitter to support module-level events (modal player).
 * Looks up BBPlayerView instances by React tag and dispatches commands.
 */
@objc(BBPlayerModule)
class BBPlayerModule: RCTEventEmitter, @unchecked Sendable {

    private var modalPlayerView: BBNativePlayerView?
    private var modalDelegate: ModalPlayerDelegate?
    private var pendingModalLoad: (() -> Void)?
    private var hasListeners = false

    @objc override static func requiresMainQueueSetup() -> Bool {
        return true
    }

    @objc override static func moduleName() -> String! {
        return "BBPlayerModule"
    }

    override func supportedEvents() -> [String]! {
        return [
            "modalPlayerDismissed",
            "modalPlayerPlay",
            "modalPlayerPause",
            "modalPlayerEnded",
            "modalPlayerError",
            "modalPlayerApiReady",
            "modalPlayerCanPlay",
            "castSessionChanged",
        ]
    }

    override func startObserving() {
        hasListeners = true
        registerCastSessionListener()
    }

    override func stopObserving() {
        hasListeners = false
    }

    // MARK: - Cast Session Events

    private var castSessionListenerRegistered = false

    private func registerCastSessionListener() {
        guard !castSessionListenerRegistered else { return }
        castSessionListenerRegistered = true // set early to prevent re-entry
        #if canImport(GoogleCast)
        guard GCKCastContext.isSharedInstanceInitialized() else {
            castSessionListenerRegistered = false
            return
        }
        // GCK session manager must be accessed from main thread
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            let sessionManager = GCKCastContext.sharedInstance().sessionManager
            sessionManager.add(self)
            // Emit existing session state with a short delay to ensure JS listener
            // is wired up (addListener and this dispatch race on different threads)
            if let session = sessionManager.currentCastSession {
                let name = session.device.friendlyName ?? "Chromecast"
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
                    self?.emitEvent("castSessionChanged", body: ["connected": true, "deviceName": name])
                }
            }
        }
        #else
        castSessionListenerRegistered = false
        #endif
    }

    // MARK: - Helpers

    private func getView(_ reactTag: NSNumber) -> BBPlayerView? {
        if let view = BBPlayerViewRegistry.shared.getView(tag: reactTag.intValue) {
            return view
        }
        if let bridge = self.bridge {
            if let view = bridge.uiManager.view(forReactTag: reactTag) as? BBPlayerView {
                return view
            }
        }
        NSLog("BBPlayerModule: Could not find view with tag %@", reactTag)
        return nil
    }

    /// Dispatch to main queue and enter @MainActor context.
    private func onMainActor(_ work: @MainActor @Sendable @escaping () -> Void) {
        DispatchQueue.main.async {
            MainActor.assumeIsolated(work)
        }
    }

    // MARK: - Commands

    @objc func play(_ viewTag: NSNumber) {
        onMainActor { self.getView(viewTag)?.play() }
    }

    @objc func pause(_ viewTag: NSNumber) {
        onMainActor { self.getView(viewTag)?.pause() }
    }

    @objc func seek(_ viewTag: NSNumber, position: NSNumber) {
        onMainActor { self.getView(viewTag)?.seek(position.intValue) }
    }

    @objc func seekRelative(_ viewTag: NSNumber, offsetSeconds: NSNumber) {
        onMainActor { self.getView(viewTag)?.seekRelative(offsetSeconds.doubleValue) }
    }

    @objc func setMuted(_ viewTag: NSNumber, muted: Bool) {
        onMainActor { self.getView(viewTag)?.setMuted(muted) }
    }

    @objc func setVolume(_ viewTag: NSNumber, volume: NSNumber) {
        onMainActor { self.getView(viewTag)?.setVolume(volume.doubleValue) }
    }

    @objc func enterFullscreen(_ viewTag: NSNumber) {
        onMainActor { self.getView(viewTag)?.enterFullscreen() }
    }

    @objc func enterFullscreenLandscape(_ viewTag: NSNumber) {
        onMainActor { self.getView(viewTag)?.enterFullscreenLandscape() }
    }

    @objc func exitFullscreen(_ viewTag: NSNumber) {
        onMainActor { self.getView(viewTag)?.exitFullscreen() }
    }

    @objc func collapse(_ viewTag: NSNumber) {
        onMainActor { self.getView(viewTag)?.collapse() }
    }

    @objc func expand(_ viewTag: NSNumber) {
        onMainActor { self.getView(viewTag)?.expand() }
    }

    @objc func autoPlayNextCancel(_ viewTag: NSNumber) {
        onMainActor { self.getView(viewTag)?.autoPlayNextCancel() }
    }

    @objc func destroy(_ viewTag: NSNumber) {
        onMainActor { self.getView(viewTag)?.destroy() }
    }

    @objc func unload(_ viewTag: NSNumber) {
        onMainActor { self.getView(viewTag)?.unload() }
    }

    @objc func showCastPicker(_ viewTag: NSNumber) {
        onMainActor { self.getView(viewTag)?.showCastPicker() }
    }


    @objc func updatePlayout(_ viewTag: NSNumber, playoutJson: String) {
        DispatchQueue.main.async {
            self.getView(viewTag)?.updatePlayout(playoutJson)
        }
    }

    @objc func loadWithClipId(_ viewTag: NSNumber, clipId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
        onMainActor { self.getView(viewTag)?.loadWithClipId(clipId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson) }
    }

    @objc func loadWithClipListId(_ viewTag: NSNumber, clipListId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
        onMainActor { self.getView(viewTag)?.loadWithClipListId(clipListId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson) }
    }

    @objc func loadWithProjectId(_ viewTag: NSNumber, projectId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
        onMainActor { self.getView(viewTag)?.loadWithProjectId(projectId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson) }
    }

    @objc func loadWithClipJson(_ viewTag: NSNumber, clipJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
        onMainActor { self.getView(viewTag)?.loadWithClipJson(clipJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson) }
    }

    @objc func loadWithClipListJson(_ viewTag: NSNumber, clipListJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
        onMainActor { self.getView(viewTag)?.loadWithClipListJson(clipListJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson) }
    }

    @objc func loadWithProjectJson(_ viewTag: NSNumber, projectJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
        onMainActor { self.getView(viewTag)?.loadWithProjectJson(projectJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson) }
    }

    @objc func loadWithJsonUrl(_ viewTag: NSNumber, jsonUrl: String?, autoPlay: Bool, contextJson: String?) {
        NSLog("BBPlayerModule.loadWithJsonUrl called - viewTag: %@, jsonUrl: %@, autoPlay: %d, context: %@", viewTag, jsonUrl ?? "nil", autoPlay, contextJson ?? "nil")
        onMainActor {
            let view = self.getView(viewTag)
            NSLog("BBPlayerModule.loadWithJsonUrl - view found: %@", view != nil ? "YES" : "NO")
            if let view = view, let url = jsonUrl {
                NSLog("BBPlayerModule.loadWithJsonUrl - calling view.loadWithJsonUrl with url: %@", url)
                view.loadWithJsonUrl(url, autoPlay: autoPlay, contextJson: contextJson)
            } else {
                NSLog("BBPlayerModule.loadWithJsonUrl - FAILED: view=%@, url=%@", view != nil ? "found" : "nil", jsonUrl ?? "nil")
            }
        }
    }

    @objc func loadWithContentIdAndType(_ viewTag: NSNumber, contentId: String?, contentType: String?, autoPlay: Bool, contextJson: String?) {
        onMainActor {
            let view = self.getView(viewTag)
            guard let view = view, let id = contentId, let type = contentType else {
                NSLog("BBPlayerModule.loadWithContentIdAndType - FAILED: view=%@, id=%@, type=%@",
                      view != nil ? "found" : "nil", contentId ?? "nil", contentType ?? "nil")
                return
            }
            view.loadWithContentIdAndType(id, contentType: type, autoPlay: autoPlay, contextJson: contextJson)
        }
    }

    // MARK: - Getter methods with Promise support
    // Note: nonisolated(unsafe) on resolver because RCTPromiseResolveBlock is a
    // non-Sendable ObjC block, but RN guarantees safe cross-thread usage.

    @objc func getDuration(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
        nonisolated(unsafe) let resolve = resolver
        onMainActor { resolve(self.getView(viewTag)?.duration()) }
    }

    @objc func getMuted(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
        nonisolated(unsafe) let resolve = resolver
        onMainActor { resolve(self.getView(viewTag)?.muted()) }
    }

    @objc func getVolume(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
        nonisolated(unsafe) let resolve = resolver
        onMainActor { resolve(self.getView(viewTag)?.volume()) }
    }

    @objc func getPhase(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
        nonisolated(unsafe) let resolve = resolver
        onMainActor { resolve(self.getView(viewTag)?.phase()) }
    }

    @objc func getState(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
        nonisolated(unsafe) let resolve = resolver
        onMainActor { resolve(self.getView(viewTag)?.state()) }
    }

    @objc func getMode(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
        nonisolated(unsafe) let resolve = resolver
        onMainActor { resolve(self.getView(viewTag)?.mode()) }
    }

    @objc func getClipData(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
        nonisolated(unsafe) let resolve = resolver
        onMainActor { resolve(self.getView(viewTag)?.clipData()) }
    }

    @objc func getProjectData(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
        nonisolated(unsafe) let resolve = resolver
        onMainActor { resolve(self.getView(viewTag)?.projectData()) }
    }

    @objc func getPlayoutData(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
        nonisolated(unsafe) let resolve = resolver
        onMainActor { resolve(self.getView(viewTag)?.playoutData()) }
    }

    @objc func setInView(_ viewTag: NSNumber, inView: Bool) {
        onMainActor { self.getView(viewTag)?.setInView(inView) }
    }

    @objc func getInView(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
        nonisolated(unsafe) let resolve = resolver
        onMainActor { resolve(self.getView(viewTag)?.inView()) }
    }

    @objc func getAdMediaWidth(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
        nonisolated(unsafe) let resolve = resolver
        onMainActor { resolve(self.getView(viewTag)?.adMediaWidth()) }
    }

    @objc func getAdMediaHeight(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
        nonisolated(unsafe) let resolve = resolver
        onMainActor { resolve(self.getView(viewTag)?.adMediaHeight()) }
    }

    // MARK: - Cast State

    @objc func getIsCasting(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
        nonisolated(unsafe) let resolve = resolver
        onMainActor { resolve(self.getView(viewTag)?.isCastingValue()) }
    }

    @objc func getCastDeviceName(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
        nonisolated(unsafe) let resolve = resolver
        onMainActor { resolve(self.getView(viewTag)?.castDeviceName()) }
    }

    @objc func endCastSession(_ viewTag: NSNumber) {
        onMainActor { self.getView(viewTag)?.endCastSession() }
    }

    // MARK: - Modal Player API

    @objc func presentModalPlayer(_ jsonUrl: String, optionsJson: String?, contextJson: String?) {
        onMainActor {
            // RCTPresentedViewController() can return nil with RCTReactNativeFactory (RN 0.83+).
            // Fallback to UIScene-based lookup to find the root view controller.
            guard let rootVC = RCTPresentedViewController()
                    ?? UIApplication.shared.connectedScenes
                        .compactMap({ ($0 as? UIWindowScene)?.windows.first(where: { $0.isKeyWindow })?.rootViewController })
                        .first
            else {
                NSLog("BBPlayerModule: No root view controller found")
                return
            }

            // Parse options from JSON string
            var options: [String: Any]? = nil
            if let json = optionsJson, let data = json.data(using: .utf8) {
                options = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
            }

            // Parse context for playlist auto-advance
            var context: [String: Any]? = nil
            if let ctxJson = contextJson, let data = ctxJson.data(using: .utf8) {
                context = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
            }

            var loadOptions: [String: Any] = options ?? [:]
            if loadOptions["autoPlay"] == nil {
                loadOptions["autoPlay"] = true
            }
            loadOptions["showBackArrow"] = options?["showBackArrow"] as? Bool ?? false

            let collectionId = context?["contextCollectionId"] as? String
            let clipId = context?["contextEntityId"] as? String

            let playerView = BBNativePlayer.createModalPlayerView(uiViewContoller: rootVC, jsonUrl: jsonUrl, options: loadOptions)

            if let collectionId = collectionId, let clipId = clipId {
                let clipContext: [String: Any] = [
                    "contextCollectionType": context?["contextCollectionType"] as? String ?? "MediaClipList",
                    "contextCollectionId": collectionId,
                ]
                self.pendingModalLoad = {
                    playerView.player.loadWithClipId(clipId: clipId, initiator: "external", autoPlay: true, seekTo: nil, context: clipContext)
                }
            }

            let delegate = ModalPlayerDelegate(module: self)
            playerView.delegate = delegate

            self.modalPlayerView = playerView
            self.modalDelegate = delegate
        }
    }

    @objc func dismissModalPlayer() {
        onMainActor {
            self.modalPlayerView?.player.closeModalPlayer()
            self.modalPlayerView = nil
            self.modalDelegate = nil
            self.pendingModalLoad = nil
        }
    }

    // MARK: - Continue Watching (ADO #19144)

    /// Resolves the persisted "last known position" (seconds) for the given clip id, or null.
    ///
    /// The iOS SDK's storage-only static accessor for resume-position lookups landed in #19099 as
    /// `BBNativePlayerView.lastKnownPosition(forClipId:) -> Double?` (seconds). The consumer's pinned
    /// `BBNativePlayerKit.xcframework` still predates that symbol, so this bridge resolves to `NSNull`
    /// to keep the JS surface contract (`Promise<number | null>`) holding. Once the vendored framework
    /// is bumped to a release that includes #19099, replace the `resolver(...)` call below with the
    /// real SDK call: `resolver(BBNativePlayerView.lastKnownPosition(forClipId: clipId))`
    /// (Swift maps the `Double?` to a number, or NSNull when nil).
    @objc func getLastKnownPosition(_ clipId: String,
                                    resolver: @escaping RCTPromiseResolveBlock,
                                    rejecter _: @escaping RCTPromiseRejectBlock) {
        // No throw / no reject — Promise<number | null> always resolves; null means "no record / not yet supported".
        resolver(NSNull())
    }

    @objc override func addListener(_ eventName: String) {
        hasListeners = true
        registerCastSessionListener()
    }

    @objc override func removeListeners(_ count: Double) {
        // Required for RCTEventEmitter
    }

    private func emitEvent(_ name: String, body: Any? = nil) {
        if hasListeners {
            sendEvent(withName: name, body: body)
        }
    }

    // MARK: - Modal Player Delegate

    @MainActor
    private class ModalPlayerDelegate: NSObject, BBNativePlayerViewDelegate {
        // Module ref is @unchecked Sendable, safe to access from @MainActor
        weak var module: BBPlayerModule?

        init(module: BBPlayerModule) {
            self.module = module
        }

        func bbNativePlayerView(didTriggerPlay playerView: BBNativePlayerView) {
            module?.emitEvent("modalPlayerPlay")
        }

        func bbNativePlayerView(didTriggerPause playerView: BBNativePlayerView) {
            module?.emitEvent("modalPlayerPause")
        }

        func bbNativePlayerView(didTriggerEnded playerView: BBNativePlayerView) {
            module?.emitEvent("modalPlayerEnded")
        }

        func bbNativePlayerView(playerView: BBNativePlayerView, didFailWithError error: String?) {
            module?.emitEvent("modalPlayerError", body: ["error": error ?? "Unknown error"])
        }

        func bbNativePlayerView(didTriggerApiReady playerView: BBNativePlayerView) {
            module?.pendingModalLoad?()
            module?.pendingModalLoad = nil
            module?.emitEvent("modalPlayerApiReady")
        }

        func bbNativePlayerView(didTriggerCanPlay playerView: BBNativePlayerView) {
            module?.emitEvent("modalPlayerCanPlay")
        }

        func bbNativePlayerView(didCloseModalPlayer playerView: BBNativePlayerView) {
            module?.emitEvent("modalPlayerDismissed")
            module?.modalPlayerView = nil
            module?.modalDelegate = nil
            module?.pendingModalLoad = nil
        }
    }
}

// MARK: - GCKSessionManagerListener (Cast Session Events)
// GCK callbacks may fire on any thread — dispatch to main for RCTEventEmitter safety.
#if canImport(GoogleCast)
extension BBPlayerModule: GCKSessionManagerListener {
    func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKCastSession) {
        let name = session.device.friendlyName ?? "Chromecast"
        DispatchQueue.main.async { [weak self] in
            self?.emitEvent("castSessionChanged", body: ["connected": true, "deviceName": name])
        }
    }

    func sessionManager(_ sessionManager: GCKSessionManager, didResumeCastSession session: GCKCastSession) {
        let name = session.device.friendlyName ?? "Chromecast"
        DispatchQueue.main.async { [weak self] in
            self?.emitEvent("castSessionChanged", body: ["connected": true, "deviceName": name])
        }
    }

    func sessionManager(_ sessionManager: GCKSessionManager, didEnd session: GCKSession, withError error: Error?) {
        DispatchQueue.main.async { [weak self] in
            var body: [String: Any] = ["connected": false]
            if let error = error {
                body["error"] = error.localizedDescription
            }
            self?.emitEvent("castSessionChanged", body: body)
        }
    }

    func sessionManager(_ sessionManager: GCKSessionManager, didSuspend session: GCKCastSession, with reason: GCKConnectionSuspendReason) {
        DispatchQueue.main.async { [weak self] in
            self?.emitEvent("castSessionChanged", body: ["connected": false])
        }
    }
}
#endif
