import React
import BlazeSDK

@objc(RTNBlazeSdk)
class RTNBlazeSdkModule: RCTEventEmitter {
    
    let reactSDKHelper = BlazeReactSDKHelper()
    
    var appOverridesCTAHandling: Bool = false
    
    override init() {
        super.init()
        
        BlazeExternalModulesBinder.shared.registerReactNativeSDKHelper(reactSDKHelper)
    }
    
    override static func moduleName() -> String {
        return "RTNBlazeSdk"
    }
    
    @objc override static func requiresMainQueueSetup() -> Bool {
        return true
    }
    
    lazy var delegate: BlazeSDKDelegate = .init(
        
        onEventTriggered: { [weak self] eventData in
            self?.onEventTriggered(eventData: eventData)
        },
        
        onErrorThrown: { [weak self] error in
            self?.onErrorThrown(error)
        }
        
    )
    
    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)
        }
        
    )
    
    //------------------- 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 {
            rejecter("0", "Missing params in options for `init`", nil)
            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
        
        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)
        }
    }
    
    @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 {
            rejecter("0", "Missing storyId in playStory options", nil)
            return
        }
        
        let pageId = options["pageId"] as? String
        
        let playerStyle = options.extractEntryPointRawPlayerStyle().extractPlayerStoryStyle()
        Blaze.shared.playStory(storyId,
                               pageId: pageId,
                               style: playerStyle) { 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 {
            rejecter("0", "Missing stories datasource in playStories options", nil)
            return
        }
        
        let playerStyle = options.extractEntryPointRawPlayerStyle().extractPlayerStoryStyle()
        Blaze.shared.playStories(dataSourceType: dataSourceType,
                                 style: playerStyle) { 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 {
            rejecter("0", "Missing stories datasource in prepareStories options", nil)
            return
        }
        
        Blaze.shared.prepareStories(dataSourceType: dataSourceType) { 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 {
            rejecter("0", "Missing momentId in playMoment options", nil)
            return
        }
        
        let playerStyle = options.extractEntryPointRawPlayerStyle().extractPlayerMomentsStyle()
        Blaze.shared.playMoment(for: momentId,
                                style: playerStyle) { 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 {
            rejecter("0", "Missing moments datasource in playMoments options", nil)
            return
        }
        
        let playerStyle = options.extractEntryPointRawPlayerStyle().extractPlayerMomentsStyle()
        Blaze.shared.playMoments(dataSourceType: dataSourceType,
                                 style: playerStyle) { 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 {
            rejecter("0", "Missing moments datasource in prepareMoments options", nil)
            return
        }
        
        Blaze.shared.prepareMoments(dataSourceType: dataSourceType) { 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 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 {
            rejecter("\((error as NSError).code)",
                     (error as NSError).localizedDescription,
                     error)
        }
    }
    
    @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)
        }
    }
    
    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)

    }
    
}

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