import React
import BlazeSDK

@objc(RTNBlazeSdk)
class RTNBlazeSdkModule: RCTEventEmitter {
    
    var appOverridesCTAHandling: Bool = false
    private static let playbackModificationMethodName = "Blaze.GlobalDelegate.playbackModificationHandler"
    
    private struct PlaybackModificationJSRequest: Encodable {
        let originalURL: String
    }
    
    private struct PlaybackModificationJSResponse: Decodable {
        let modifiedURL: String
    }
    
    override init() {
        super.init()
        
        let reactSDKHelper = BlazeReactSDKHelper()
        BlazeExternalModulesBinder.shared.registerReactNativeSDKHelper(reactSDKHelper)
    }
    
    override static func moduleName() -> String {
        return "RTNBlazeSdk"
    }
    
    @objc override static func requiresMainQueueSetup() -> Bool {
        return false
    }
    
    lazy var delegate: BlazeSDKDelegate = .init(
        
        onEventTriggered: { [weak self] eventData in
            self?.onEventTriggered(eventData: eventData)
        },
        
        onErrorThrown: { [weak self] error in
            self?.onErrorThrown(error)
        },

        playbackModificationHandler: { [weak self] request in
            return await self?.onPlaybackModificationRequested(request: request) ?? request.response()
        }
    )
    
    lazy var entryPointDelegate: BlazePlayerEntryPointDelegate = .init(
        
        onDataLoadStarted: { [weak self] params in
            self?.onDataLoadStarted(playerType: params.playerType,
                                    sourceId: params.sourceId)
        },
        
        onDataLoadComplete: { [weak self] params in
            self?.onDataLoadComplete(playerType: params.playerType,
                                     sourceId: params.sourceId,
                                     itemsCount: params.itemsCount,
                                     result: params.result)
        },
        
        onPlayerDidAppear: { [weak self] params in
            self?.onPlayerDidAppear(playerType: params.playerType,
                                    sourceId: params.sourceId)
        },
        
        onPlayerDidDismiss: { [weak self] params in
            self?.onPlayerDidDismiss(playerType: params.playerType,
                                     sourceId: params.sourceId)
        },
        
        onTriggerCTA: { [weak self] params in
            self?.onTriggerCTA(playerType: params.playerType,
                               sourceId: params.sourceId,
                               actionType: params.actionType,
                               actionParam: params.actionParam) ?? false
        },
        
        onTriggerPlayerBodyTextLink: { [weak self] params in
            self?.onTriggerPlayerBodyTextLink(playerType: params.playerType,
                                              sourceId: params.sourceId,
                                              actionParam: params.actionParam) ?? .deeplink
        },
        
        onPlayerEventTriggered: { [weak self] params in
            self?.onPlayerEventTriggered(playerType: params.playerType,
                                         sourceId: params.sourceId,
                                         event: params.event)
        },
        
        onTriggerCustomActionButton: { [weak self] params in
            self?.onTriggerCustomActionButton(playerType: params.playerType,
                                              sourceId: params.sourceId,
                                              buttonParams: params.customActionParams)
        },
        
        onReadStatusChanged: { [weak self] params in
            self?.onReadStatusChanged(playerType: params.playerType,
                                      sourceId: params.sourceId,
                                      dataSourceStringRepresentation: params.dataSourceStringRepresentation,
                                      isEntireContentRead: params.isEntireContentRead,
                                      itemReadStatus: params.itemReadStatus)
        }
        
    )
    
    //------------------- Bridge methods ----------------------//
    
    @objc func `init`(_ options: [String : AnyHashable],
                      resolver: @escaping RCTPromiseResolveBlock,
                      rejecter: @escaping RCTPromiseRejectBlock) {
        // Initialize the sdk.
        guard let apiKey = options["apiKey"] as? String,
              let cachingSize = options["cachingSize"] as? Int else {
            handleError(rejecter, errMessage: "Missing params in options for `init`")
            return
        }
        let externalUserId = options["externalUserId"] as? String
        let geoLocation = options["geoLocation"] as? String
        let cachingLevelRaw = options["cachingLevel"] as? String

        if let appOverridesCTAHandling = options["appOverridesCTAHandling"] as? Bool {
            self.appOverridesCTAHandling = appOverridesCTAHandling
        }
        
        let cachingLevel = cachingLevelRaw?.asCachingLevel ?? .Default

        Blaze.shared.initialize(apiKey: apiKey,
                                externalUserId: externalUserId,
                                cachingSize: cachingSize,
                                prefetchingPolicy: cachingLevel,
                                geo: geoLocation,
                                delegate: delegate) { result in
            result.handleResult(resolver: resolver,
                                rejecter: rejecter)
        }
        
        Blaze.shared.playerEntryPointDelegate = entryPointDelegate
        Blaze.shared.followEntitiesManager.delegate = self
        
        Blaze.shared.castingManager.delegate = BlazeCastingDelegate(
            onCastingStateChanged: { [weak self] params in
                self?.onCastingStateChanged(
                    playerType: params.playerType,
                    sourceId: params.sourceId,
                    newState: params.newState
                )
            }
        )
        
        Blaze.shared.pipManager.delegate = BlazePipDelegate(
            onPiPStateChanged: { [weak self] params in
                self?.onPiPStateChanged(
                    playerType: params.playerType,
                    sourceId: params.sourceId,
                    newState: params.newState
                )
            }
        )

        if let defaultStoryPlayerStyle = (options["defaultStoryPlayerStyle"] as? [String: AnyHashable]).extractPlayerStoryStyle() {
            Blaze.shared.setDefaultStoryPlayerStyle(defaultStoryPlayerStyle)
        }
        
        if let defaultMomentsPlayerStyle = (options["defaultMomentsPlayerStyle"] as? [String: AnyHashable]).extractPlayerMomentsStyle() {
            Blaze.shared.setDefaultMomentsPlayerStyle(defaultMomentsPlayerStyle)
        }
        
        if let defaultVideosPlayerStyle = (options["defaultVideosPlayerStyle"] as? [String: AnyHashable]).extractPlayerVideosStyle() {
            Blaze.shared.setDefaultVideosPlayerStyle(defaultVideosPlayerStyle)
        }

    }
    
    @objc func isInitialized() -> NSNumber {
        return NSNumber(booleanLiteral: Blaze.shared.isInitialized)
    }
    
    @objc func playStory(_ options: [String : AnyHashable],
                         resolver: @escaping RCTPromiseResolveBlock,
                         rejecter: @escaping RCTPromiseRejectBlock) {
        guard let storyId = options["storyId"] as? String else {
            handleError(rejecter, errMessage: "Missing storyId in playStory options")
            return
        }
        
        let pageId = options["pageId"] as? String
        
        let playerStyle = options.extractEntryPointRawPlayerStyle().extractPlayerStoryStyle()
        let triggerSource = options["triggerSource"]?.asEntryPointTriggerSource ?? .entryPoint

        Blaze.shared.playStory(storyId,
                               pageId: pageId,
                               style: playerStyle,
                               triggerSource: triggerSource) { result in
            result.handleResult(resolver: resolver,
                                rejecter: rejecter)
        }
    }
    
    @objc func playStories(_ options: [String : AnyHashable],
                           resolver: @escaping RCTPromiseResolveBlock,
                           rejecter: @escaping RCTPromiseRejectBlock) {
        guard let dataSourceType = options.extractEntryPointDataSource() else {
            handleError(rejecter, errMessage: "Missing stories datasource in playStories options")
            return
        }
        
        let entryContentId = options.extractEntryPointContentId()
        let playerStyle = options.extractEntryPointRawPlayerStyle().extractPlayerStoryStyle()
        let triggerSource = options["triggerSource"]?.asEntryPointTriggerSource ?? .entryPoint
        let shouldOrderContentByReadStatus = options["shouldOrderContentByReadStatus"] as? Bool ?? true

        Blaze.shared.playStories(dataSourceType: dataSourceType,
                                 entryContentId: entryContentId,
                                 style: playerStyle,
                                 shouldOrderContentByReadStatus: shouldOrderContentByReadStatus,
                                 triggerSource: triggerSource) { result in
            result.handleResult(resolver: resolver,
                                rejecter: rejecter)
        }
    }
    
    @objc func prepareStories(_ options: [String : AnyHashable],
                           resolver: @escaping RCTPromiseResolveBlock,
                           rejecter: @escaping RCTPromiseRejectBlock) {
        guard let dataSourceType = options.extractEntryPointDataSource() else {
            handleError(rejecter, errMessage: "Missing stories datasource in prepareStories options")
            return
        }

        let entryContentId = options.extractEntryPointContentId()
        
        Blaze.shared.prepareStories(dataSourceType: dataSourceType, entryContentId: entryContentId) { result in
            result.handleResult(resolver: resolver,
                                rejecter: rejecter)
        }
    }
    
    @objc func playMoment(_ options: [String : AnyHashable],
                          resolver: @escaping RCTPromiseResolveBlock,
                          rejecter: @escaping RCTPromiseRejectBlock) {
        guard let momentId = options["momentId"] as? String else {
            handleError(rejecter, errMessage: "Missing momentId in playMoment options")
            return
        }
        
        let playerStyle = options.extractEntryPointRawPlayerStyle().extractPlayerMomentsStyle()
        let triggerSource = options["triggerSource"]?.asEntryPointTriggerSource ?? .entryPoint
        Blaze.shared.playMoment(for: momentId,
                                style: playerStyle,
                                triggerSource: triggerSource) { result in
            result.handleResult(resolver: resolver,
                                rejecter: rejecter)
        }
    }
    
    @objc func playMoments(_ options: [String : AnyHashable],
                           resolver: @escaping RCTPromiseResolveBlock,
                           rejecter: @escaping RCTPromiseRejectBlock) {
        guard let dataSourceType = options.extractEntryPointDataSource() else {
            handleError(rejecter, errMessage: "Missing moments datasource in playMoments options")
            return
        }
    
        let entryContentId = options.extractEntryPointContentId()
        let playerStyle = options.extractEntryPointRawPlayerStyle().extractPlayerMomentsStyle()
        let triggerSource = options["triggerSource"]?.asEntryPointTriggerSource ?? .entryPoint
        let shouldOrderContentByReadStatus = options["shouldOrderContentByReadStatus"] as? Bool ?? true
        Blaze.shared.playMoments(dataSourceType: dataSourceType,
                                 entryContentId: entryContentId,
                                 style: playerStyle,
                                 shouldOrderContentByReadStatus: shouldOrderContentByReadStatus,
                                 triggerSource: triggerSource) { result in
            result.handleResult(resolver: resolver,
                                rejecter: rejecter)
        }
    }
    
    @objc func prepareMoments(_ options: [String : AnyHashable],
                           resolver: @escaping RCTPromiseResolveBlock,
                           rejecter: @escaping RCTPromiseRejectBlock) {
        guard let dataSourceType = options.extractEntryPointDataSource() else {
            handleError(rejecter, errMessage: "Missing moments datasource in prepareMoments options")
            return
        }

        let entryContentId = options.extractEntryPointContentId()
        
        Blaze.shared.prepareMoments(dataSourceType: dataSourceType, entryContentId: entryContentId) { result in
            result.handleResult(resolver: resolver,
                                rejecter: rejecter)
        }
    }

    @objc func appendMomentsToPlayer(_ options: [String : AnyHashable],
                                     resolver: @escaping RCTPromiseResolveBlock,
                                     rejecter: @escaping RCTPromiseRejectBlock) {
        guard let sourceId = options["sourceId"] as? String else {
            handleError(rejecter, errMessage: "Missing sourceId in appendMomentsToPlayer options")
            return
        }

        guard let dataSourceType = options.extractEntryPointDataSource() else {
            handleError(rejecter, errMessage: "Missing moments datasource in appendMomentsToPlayer options")
            return
        }
        
        let shouldOrderContentByReadStatus = options["shouldOrderContentByReadStatus"] as? Bool ?? false
    
        Blaze.shared.appendMomentsToPlayer(sourceId: sourceId,
                                           dataSourceType: dataSourceType,
                                           shouldOrderContentByReadStatus: shouldOrderContentByReadStatus) { result in
            result.handleResult(resolver: resolver,
                                rejecter: rejecter)
        }
    }
    
    @objc func playVideo(_ options: [String : AnyHashable],
                          resolver: @escaping RCTPromiseResolveBlock,
                          rejecter: @escaping RCTPromiseRejectBlock) {
        guard let videoId = options["videoId"] as? String else {
            handleError(rejecter, errMessage: "Missing videoId in playVideo options")
            return
        }
        
        let playerStyle = options.extractEntryPointRawPlayerStyle().extractPlayerVideosStyle()
        let playbackConfiguration = options.extractEntryPointPlaybackConfiguration().extractVideosPlaybackConfiguration()
        let triggerSource = options["triggerSource"]?.asEntryPointTriggerSource ?? .entryPoint

        Blaze.shared.playVideo(for: videoId,
                               style: playerStyle,
                               playbackConfiguration: playbackConfiguration,
                               triggerSource: triggerSource) { result in
            result.handleResult(resolver: resolver,
                                rejecter: rejecter)
        }
    }
    
    @objc func playVideos(_ options: [String : AnyHashable],
                           resolver: @escaping RCTPromiseResolveBlock,
                           rejecter: @escaping RCTPromiseRejectBlock) {
        guard let dataSourceType = options.extractEntryPointDataSource() else {
            handleError(rejecter, errMessage: "Missing videos datasource in playVideos options")
            return
        }
    
        let entryContentId = options.extractEntryPointContentId()
        let playerStyle = options.extractEntryPointRawPlayerStyle().extractPlayerVideosStyle()
        let shouldOrderContentByReadStatus = options["shouldOrderContentByReadStatus"] as? Bool ?? true
        let playbackConfiguration = options.extractEntryPointPlaybackConfiguration().extractVideosPlaybackConfiguration()
        let triggerSource = options["triggerSource"]?.asEntryPointTriggerSource ?? .entryPoint

        Blaze.shared.playVideos(dataSourceType: dataSourceType,
                                entryContentId: entryContentId,
                                style: playerStyle,
                                playbackConfiguration: playbackConfiguration,
                                shouldOrderContentByReadStatus: shouldOrderContentByReadStatus,
                                triggerSource: triggerSource) { result in
            result.handleResult(resolver: resolver,
                                rejecter: rejecter)
        }
    }
    
    @objc func prepareVideos(_ options: [String : AnyHashable],
                           resolver: @escaping RCTPromiseResolveBlock,
                           rejecter: @escaping RCTPromiseRejectBlock) {
        guard let dataSourceType = options.extractEntryPointDataSource() else {
            handleError(rejecter, errMessage: "Missing videos datasource in prepareVideos options")
            return
        }

        let entryContentId = options.extractEntryPointContentId()
        
        Blaze.shared.prepareVideos(dataSourceType: dataSourceType, entryContentId: entryContentId) { result in
            result.handleResult(resolver: resolver,
                                rejecter: rejecter)
        }
    }
    
    @objc(dismissPlayer:rejecter:)
    func dismissPlayer(resolver: @escaping RCTPromiseResolveBlock,
                       rejecter: @escaping RCTPromiseRejectBlock) {
        DispatchQueue.main.async {
            Blaze.shared.dismissCurrentPlayer() {
                resolver(nil)
            }
        }
    }
    
    @objc func setDoNotTrack(_ doNotTrack: Bool,
                             resolver: @escaping RCTPromiseResolveBlock,
                             rejecter: @escaping RCTPromiseRejectBlock) {
        Blaze.shared.doNotTrackUser = doNotTrack
        resolver(nil)
    }

    @objc func setDisableAnalytics(_ disableAnalytics: Bool,
                                  resolver: @escaping RCTPromiseResolveBlock,
                                  rejecter: @escaping RCTPromiseRejectBlock) {
        Blaze.shared.disableAnalytics = disableAnalytics
        resolver(nil)
    }
    
    @objc func setExternalUserId(_ externalUserId: String?,
                                 resolver: @escaping RCTPromiseResolveBlock,
                                 rejecter: @escaping RCTPromiseRejectBlock) {
        Blaze.shared.setExternalUserId(externalUserId) { result in
            result.handleResult(resolver: resolver,
                                rejecter: rejecter)
        }
    }
    
    @objc func handleUniversalLink(_ link: String,
                                   resolver: @escaping RCTPromiseResolveBlock,
                                   rejecter: @escaping RCTPromiseRejectBlock) {
        
        Blaze.shared.handleUniversalLink(link) { result in
            result.handleResult(resolver: resolver,
                                rejecter: rejecter)
        }
    }
    
    @objc func canHandleUniversalLink(_ link: String,
                                   resolver: @escaping RCTPromiseResolveBlock,
                                   rejecter: @escaping RCTPromiseRejectBlock) {
        
        resolver(Blaze.shared.canHandleUniversalLink(link))
    }
    
    @objc func updateGeoRestriction(_ geoLocation: String,
                                    resolver: @escaping RCTPromiseResolveBlock,
                                    rejecter: @escaping RCTPromiseRejectBlock) {
        do {
            try Blaze.shared.updateGeo(geoLocation)
            resolver(true)
        } catch {
            handleError(rejecter,
                        errMessage: (error as NSError).localizedDescription
            )
        }
    }
    
    @objc func setDefaultVideosPlaybackConfiguration(_ config: [String : AnyHashable],
                                                      resolver: @escaping RCTPromiseResolveBlock,
                                                      rejecter: @escaping RCTPromiseRejectBlock) {
        guard let playbackConfig = (config as [String: AnyHashable]?).extractVideosPlaybackConfiguration() else {
            handleError(rejecter, errMessage: "Invalid playback configuration")
            return
        }
        Blaze.shared.setDefaultVideosPlaybackConfiguration(playbackConfig)
        resolver(nil)
    }
    
    @objc(getDefaultVideosPlaybackConfiguration:rejecter:)
    func getDefaultVideosPlaybackConfiguration(resolver: @escaping RCTPromiseResolveBlock,
                                                rejecter: @escaping RCTPromiseRejectBlock) {
        let config = Blaze.shared.getDefaultVideosPlaybackConfiguration()
        let result: [String: Any] = [
            "multiAspectRatio": config.multiAspectRatio,
            "shouldOpenInLandscape": config.shouldOpenInLandscape,
            "pipConfiguration": [
                "enterPipOnAppBackground": config.pipConfiguration.enterPipOnAppBackground
            ]
        ]
        resolver(result)
    }
    
    @objc func setDefaultMomentsPlaybackConfiguration(_ config: [String : AnyHashable],
                                                       resolver: @escaping RCTPromiseResolveBlock,
                                                       rejecter: @escaping RCTPromiseRejectBlock) {
        guard let playbackConfig = (config as [String: AnyHashable]?).extractMomentsPlaybackConfiguration() else {
            handleError(rejecter, errMessage: "Invalid moments playback configuration")
            return
        }
        Blaze.shared.setDefaultMomentsPlaybackConfiguration(playbackConfig)
        resolver(nil)
    }
    
    @objc(getDefaultMomentsPlaybackConfiguration:rejecter:)
    func getDefaultMomentsPlaybackConfiguration(resolver: @escaping RCTPromiseResolveBlock,
                                                 rejecter: @escaping RCTPromiseRejectBlock) {
        let config = Blaze.shared.getDefaultMomentsPlaybackConfiguration()
        var result: [String: Any] = [:]
        switch config.loopBehavior {
        case .infiniteLoop:
            result["loopBehavior"] = ["type": "infiniteLoop"]
        case .loopAndAdvance(let numberOfPlays):
            result["loopBehavior"] = ["type": "loopAndAdvance", "numberOfPlays": numberOfPlays]
        }
        resolver(result)
    }
    
    @objc func canHandlePushNotification(_ payload: [String : AnyHashable],
                                         resolver: @escaping RCTPromiseResolveBlock,
                                         rejecter: @escaping RCTPromiseRejectBlock) {
        resolver(Blaze.shared.canHandlePushNotification(payload))
    }
    
    @objc func handleNotificationPayload(_ payload: [String : AnyHashable],
                                         resolver: @escaping RCTPromiseResolveBlock,
                                         rejecter: @escaping RCTPromiseRejectBlock) {
        Blaze.shared.handlePushNotificationPayload(payload) { result in
            result.handleResult(resolver: resolver,
                                rejecter: rejecter)
        }
    }
    
    @objc func setFollowedEntities(_ entityIds: [String],
                                    resolver: @escaping RCTPromiseResolveBlock,
                                    rejecter: @escaping RCTPromiseRejectBlock) {
        let entities = Set(entityIds.map { BlazeFollowEntity(id: $0) })
        Blaze.shared.followEntitiesManager.setFollowedEntities(entities)
        resolver(nil)
    }
    
    @objc func insertFollowedEntities(_ entityIds: [String],
                                       resolver: @escaping RCTPromiseResolveBlock,
                                       rejecter: @escaping RCTPromiseRejectBlock) {
        let entities = Set(entityIds.map { BlazeFollowEntity(id: $0) })
        Blaze.shared.followEntitiesManager.insertFollowedEntities(entities)
        resolver(nil)
    }
    
    @objc func removeFollowedEntities(_ entityIds: [String],
                                       resolver: @escaping RCTPromiseResolveBlock,
                                       rejecter: @escaping RCTPromiseRejectBlock) {
        let entities = Set(entityIds.map { BlazeFollowEntity(id: $0) })
        Blaze.shared.followEntitiesManager.removeFollowedEntities(entities)
        resolver(nil)
    }
    
    @objc func getFollowedEntities(_ resolver: @escaping RCTPromiseResolveBlock,
                                    rejecter: @escaping RCTPromiseRejectBlock) {
        let entities = Blaze.shared.followEntitiesManager.getFollowedEntities()
        let ids = entities.map { $0.id }
        resolver(ids)
    }
    
    @objc func setPreferredLanguage(_ language: String?,
                                    resolver: @escaping RCTPromiseResolveBlock,
                                    rejecter: @escaping RCTPromiseRejectBlock) {
        Blaze.shared.setPreferredLanguage(language)
        resolver(nil)
    }
    
    @objc func showSearchScreen(_ options: NSDictionary,
                                resolver: @escaping RCTPromiseResolveBlock,
                                rejecter: @escaping RCTPromiseRejectBlock) {
        let dict = options as? [String: AnyHashable]
        var searchParams: BlazeSearchScreenParams? = nil
        if let suggestionsDataSourceDict = dict?["suggestionsDataSource"] as? [String: AnyHashable],
           let dataSourceType = suggestionsDataSourceDict.toDataSourceType {
            searchParams = BlazeSearchScreenParams(suggestionsDataSource: dataSourceType)
        }
        DispatchQueue.main.async {
            Blaze.shared.showSearchScreen(searchParams: searchParams) { result in
                result.handleResult(resolver: resolver, rejecter: rejecter)
            }
        }
    }
    
    @objc(stopActiveCastingSession:rejecter:)
    func stopActiveCastingSession(resolver: @escaping RCTPromiseResolveBlock,
                                   rejecter: @escaping RCTPromiseRejectBlock) {
        Blaze.shared.castingManager.stopActiveCastingSession()
        resolver(nil)
    }
    
    @objc(stopActivePiPSession:rejecter:)
    func stopActivePiPSession(resolver: @escaping RCTPromiseResolveBlock,
                               rejecter: @escaping RCTPromiseRejectBlock) {
        Blaze.shared.pipManager.stopActivePiPSession()
        resolver(nil)
    }
    
    @objc(isPiPActive:rejecter:)
    func isPiPActive(resolver: @escaping RCTPromiseResolveBlock,
                      rejecter: @escaping RCTPromiseRejectBlock) {
        resolver(Blaze.shared.pipManager.isActive)
    }
    
    override func supportedEvents() -> [String] {
        return SupportedAppEvents.allCases.map({ $0.rawValue })
    }
}

extension RTNBlazeSdkModule {
    
    func onEventTriggered(eventData: BlazeSDK.BlazeAnalytics) {
        guard let event = createOnEventTriggeredEvent(eventData) else {
            return
        }

        bridge?.sendBlazeAppEvent(event)
    }
    
    func onErrorThrown(_ error: BlazeSDK.BlazeError) {
        bridge?.sendBlazeAppEvent(createOnErrorThrownEvent(error))
    }
    
}

extension RTNBlazeSdkModule {
    
    func onDataLoadStarted(playerType: BlazePlayerType, sourceId: String?) {
        let event = createOnDataLoadStartedEvent(playerType: playerType,
                                                 sourceId: sourceId)
        bridge?.sendBlazeAppEvent(event)
    }
    
    func onDataLoadComplete(playerType: BlazePlayerType, sourceId: String?, itemsCount: Int, result: BlazeResult) {
        let event = createOnDataLoadCompleteEvent(playerType: playerType,
                                                  sourceId: sourceId,
                                                  itemsCount: itemsCount,
                                                  result: result)
        bridge?.sendBlazeAppEvent(event)
    }
    
    func onPlayerDidAppear(playerType: BlazePlayerType, sourceId: String?) {
        let event = createOnPlayerDidAppearEvent(playerType: playerType,
                                                 sourceId: sourceId)
        bridge?.sendBlazeAppEvent(event)
    }
   
    func onPlayerDidDismiss(playerType: BlazePlayerType, sourceId: String?) {
        let event = createOnPlayerDidDismissEvent(playerType: playerType,
                                                  sourceId: sourceId)
        bridge?.sendBlazeAppEvent(event)
    }
    
    func onTriggerCTA(playerType: BlazePlayerType, sourceId: String?, actionType: BlazeCTAActionType, actionParam: String) -> Bool {
        let event = createOnTriggerCTAEvent(playerType: playerType,
                                            sourceId: sourceId,
                                            actionType: actionType,
                                            actionParam: actionParam)
        bridge?.sendBlazeAppEvent(event)

        return appOverridesCTAHandling
    }
    
    func onTriggerPlayerBodyTextLink(playerType: BlazePlayerType, sourceId: String?, actionParam: String) -> BlazeLinkActionHandleType {
        let event = createOnTriggerPlayerBodyTextLinkEvent(playerType: playerType,
                                                           sourceId: sourceId,
                                                           actionParam: actionParam)
        bridge?.sendBlazeAppEvent(event)
        
        // Currently not supported for bridging out to React side.
        return .deeplink
    }
    
    func onPlayerEventTriggered(playerType: BlazePlayerType, sourceId: String?, event: BlazePlayerEvent) {
        guard let event = createOnPlayerEventTriggeredEvent(playerType: playerType,
                                                            sourceId: sourceId,
                                                            event: event) else {
            return
        }
        bridge?.sendBlazeAppEvent(event)
    }
    
    func onTriggerCustomActionButton(playerType: BlazePlayerType, sourceId: String?, buttonParams: BlazePlayerCustomActionButtonParams) {
        let event = createOnTriggerCustomActionButtonEvent(playerType: playerType, sourceId: sourceId, buttonParams: buttonParams)
        bridge?.sendBlazeAppEvent(event)
    }
    
    func onReadStatusChanged(playerType: BlazePlayerType, sourceId: String?, dataSourceStringRepresentation: String, isEntireContentRead: Bool, itemReadStatus: [String: Bool]) {
        let event = createOnReadStatusChangedEvent(playerType: playerType, sourceId: sourceId, dataSourceStringRepresentation: dataSourceStringRepresentation, isEntireContentRead: isEntireContentRead, itemReadStatus: itemReadStatus)
        bridge?.sendBlazeAppEvent(event)
    }

    private func onPlaybackModificationRequested(
        request: BlazePlaybackModificationRequest
    ) async -> BlazePlaybackModificationResponse {
        guard let asyncBridge = blazeAsyncBridge else {
            return request.response()
        }
        
        do {
            let jsRequest = PlaybackModificationJSRequest(originalURL: request.originalURL.absoluteString)
            let jsResponse: PlaybackModificationJSResponse = try await asyncBridge.callJSMethod(
                Self.playbackModificationMethodName,
                params: jsRequest
            )
            
            let modifiedURL = URL(string: jsResponse.modifiedURL)
            return request.response(with: modifiedURL)
        } catch {
            blazeLogDebug("Error playbackModificationHandler: \(error)")
            return request.response()
        }
    }
}

extension RTNBlazeSdkModule {
    func onCastingStateChanged(playerType: BlazePlayerType, sourceId: String?, newState: BlazeCastingState) {
        let eventData: [String: AnyHashable] = [
            "playerType": playerType.toReactValue,
            "sourceId": sourceId,
            "state": newState.rawValue
        ]
        bridge?.sendBlazeAppEvent(.init(name: SupportedAppEvents.onCastingStateChanged.rawValue, body: eventData))
    }
}

extension RTNBlazeSdkModule {
    func onPiPStateChanged(playerType: BlazePlayerType, sourceId: String?, newState: BlazePipState) {
        let eventData: [String: AnyHashable] = [
            "playerType": playerType.toReactValue,
            "sourceId": sourceId,
            "state": newState.rawValue
        ]
        bridge?.sendBlazeAppEvent(.init(name: SupportedAppEvents.onPiPStateChanged.rawValue, body: eventData))
    }
}

extension RTNBlazeSdkModule: BlazeFollowEntitiesDelegate {
    func onFollowEntityClicked(_ params: BlazeFollowEntityClickedParams) {
        let event = createOnFollowEntityClickedEvent(
            playerType: params.playerType,
            sourceId: params.sourceId,
            newFollowingState: params.newFollowingState,
            followEntityId: params.followEntity.id
        )
        bridge?.sendBlazeAppEvent(event)
    }
}

extension [String : AnyHashable] {
    
    func extractEntryPointDataSource() -> BlazeDataSourceType? {
        return (self["dataSource"] as? [String: AnyHashable])?.toDataSourceType
    }

    func extractEntryPointContentId() -> String? {
        return self["entryContentId"] as? String
    }
    
    func extractEntryPointRawPlayerStyle() -> [String: AnyHashable]? {
        return self["playerStyle"] as? [String: AnyHashable]
    }
    
    func extractEntryPointPlaybackConfiguration() -> [String: AnyHashable]? {
         return self["playbackConfiguration"] as? [String: AnyHashable]
    }
}

extension AnyHashable {
    
    var asEntryPointTriggerSource: BlazeEntryPointTriggerSource? {
        guard let self = self as? String else {
            return nil
        }
        
        switch self {
        case "notification":
            return .notification
        case "deepLink":
            return .deepLink
        case "entryPoint":
            return .entryPoint
        default:
            return nil
        }
    }
    
}
