//
//  RNMobileMessaging.swift
//  MobileMessagingReactNative
//
//  Copyright (c) 2016-2025 Infobip Limited
//  Licensed under the Apache License, Version 2.0
//

import Foundation
import MobileMessaging

// It may happen that RN call module initialization more than once,
// Not needed to perform early start more than once per application session.
var isEarlyStartPerformed = false;

@objc(ReactNativeMobileMessaging)
class ReactNativeMobileMessaging: RCTEventEmitter  {
    private(set) static weak var shared: ReactNativeMobileMessaging?
    private var messageStorageAdapter: MessageStorageAdapter?
    private var eventsManager: RNMobileMessagingEventsManager?
    private var isStarted: Bool = false
    
    @objc
    override func supportedEvents() -> [String]! {
        return [
            EventName.tokenReceived,
            EventName.registrationUpdated,
            EventName.installationUpdated,
            EventName.userUpdated,
            EventName.personalized,
            EventName.depersonalized,
            EventName.actionTapped,
            EventName.notificationTapped,
            EventName.messageReceived,
            EventName.messageStorage_start,
            EventName.messageStorage_stop,
            EventName.messageStorage_save,
            EventName.messageStorage_find,
            EventName.messageStorage_findAll,
            EventName.debugLoggerMessageReceived,
            EventName.inAppChat_availabilityUpdated,
            EventName.inAppChat_unreadMessageCounterUpdated,
            EventName.inAppChat_viewStateChanged,
            EventName.inAppChat_configurationSynced,
            EventName.inAppChat_registrationIdUpdated,
            EventName.inAppChat_jwtRequested,
            EventName.inAppChat_exceptionReceived
        ]
    }
    
    override func startObserving() {
        eventsManager?.startObserving()
        super.startObserving()
    }
    
    override func stopObserving() {
        eventsManager?.stopObserving()
        super.stopObserving()
    }
    
    override func sendEvent(withName name: String!, body: Any!) {
        guard let _eventsManager = eventsManager, _eventsManager.hasEventListeners == true else {
            return
        }
        super.sendEvent(withName: name, body: body)
    }
    
    func sendDirectEvent(withName name: String!, body: Any!) {
        guard let _eventsManager = eventsManager, _eventsManager.hasEventListeners == true else {
            // JS listener persists on the mobileMessaging singleton but RCTEventEmitter's _listenerCount may be zero during a React Navigation transition (reproducible with custom chat navigations and JWT async callback/authentication flow). Bypass that guard by invoking RCTDeviceEventEmitter directly via the public callableJSModules API (bridgeless-safe; the same path super.sendEvent uses internally).
            MMLogDebug("[RNMobileMessaging] sendDirectEvent: invoking RCTDeviceEventEmitter directly")
            self.callableJSModules?.invokeModule(
                "RCTDeviceEventEmitter",
                method: "emit",
                withArgs: [name!, body ?? NSNull()]
            )
            return
        }
        super.sendEvent(withName: name, body: body)
    }
        
    @objc
    override static func requiresMainQueueSetup() -> Bool {
        return true
    }
    
    override init() {
        super.init()
        ReactNativeMobileMessaging.shared = self
        self.eventsManager = RNMobileMessagingEventsManager(eventEmitter: self)
        self.messageStorageAdapter = MessageStorageAdapter(eventEmitter: self)
        performEarlyStartIfPossible()
    }
    
    deinit {
        self.eventsManager?.stop(stopObservations: true)
    }
    
    @objc(init:onSuccess:onError:)
    func start(config: NSDictionary, onSuccess: @escaping RCTResponseSenderBlock, onError: @escaping RCTResponseSenderBlock) {
        guard var userConfigDict = config as? [String : AnyObject],
              let applicationCode = userConfigDict.removeValue(forKey: RNMobileMessagingConfiguration.Keys.applicationCode) as? String,
              let configuration = RNMobileMessagingConfiguration(rawConfig: userConfigDict)
        else {
            onError([NSError(type: .InvalidArguments).reactNativeObject])
            return
        }
        
        let successCallback: RCTResponseSenderBlock = { [weak self] response in
            RNMobileMessagingConfiguration.saveConfigToDefaults(rawConfig: userConfigDict)
            self?.isStarted = true
            onSuccess(response)
        }

        let cachedConfigDict = RNMobileMessagingConfiguration.getRawConfigFromDefaults()
        let keychainApplicationCode = MobileMessaging.getKeychainApplicationCode()
        let shouldRestart = needsRestart(userConfigDict: userConfigDict, applicationCode: applicationCode)
        let shouldStart = cachedConfigDict == nil || keychainApplicationCode == nil

        if shouldRestart {
            stop {
                self.startWithApplicationCode(
                    configuration: configuration,
                    applicationCode: applicationCode,
                    onSuccess: successCallback,
                    onError: onError
                )
            }
        } else if shouldStart {
            startWithApplicationCode(
                configuration: configuration,
                applicationCode: applicationCode,
                onSuccess: successCallback,
                onError: onError
            )
        } else {
            successCallback(nil)
        }
    }
    
    private func performEarlyStartIfPossible() {
        let cachedConfigDict = RNMobileMessagingConfiguration.getRawConfigFromDefaults()
        let configuration = cachedConfigDict.flatMap(RNMobileMessagingConfiguration.init)
        let applicationCode = MobileMessaging.getKeychainApplicationCode()

        guard let configuration = configuration,
              let applicationCode = applicationCode,
              !self.isStarted,
              !isEarlyStartPerformed else
        {
            return
        }

        isEarlyStartPerformed = true
        startWithApplicationCode(configuration: configuration, applicationCode: applicationCode) { response in }
    }
    
    private func startWithApplicationCode(
        configuration: RNMobileMessagingConfiguration,
        applicationCode: String,
        onSuccess: @escaping RCTResponseSenderBlock,
        onError: RCTResponseSenderBlock? = nil
    ) {
        guard let mobileMessaging = createMobileMessagingInstance(
            configuration: configuration,
            applicationCode: applicationCode
        ) else {
            MMLogError("[RNMobileMessaging] Failed to initialize MobileMessaging instance, SDK can't start.")
            onError?([NSError(type: .InitializationFailed).reactNativeObject])
            return
        }

        applyConfigurationAndStart(
            mobileMessaging,
            configuration: configuration,
            onSuccess: onSuccess
        )
    }

    private func createMobileMessagingInstance(configuration: RNMobileMessagingConfiguration, applicationCode: String) -> MobileMessaging? {
        let backendBaseURL = configuration.backendBaseURL ?? MMConsts.APIValues.prodDynamicBaseURLString
        return MobileMessaging.withApplicationCode(
            applicationCode,
            notificationType: configuration.notificationType,
            backendBaseURL: backendBaseURL
        )
    }

    private func applyConfigurationAndStart(_ mobileMessaging: MobileMessaging, configuration: RNMobileMessagingConfiguration, onSuccess: @escaping RCTResponseSenderBlock) {
        self.eventsManager?.startObserving()
        MobileMessaging.privacySettings.systemInfoSendingDisabled = configuration.privacySettings[RNMobileMessagingConfiguration.Keys.systemInfoSendingDisabled].unwrap(orDefault: false)
        MobileMessaging.privacySettings.carrierInfoSendingDisabled = configuration.privacySettings[RNMobileMessagingConfiguration.Keys.carrierInfoSendingDisabled].unwrap(orDefault: false)
        MobileMessaging.privacySettings.userDataPersistingDisabled = configuration.privacySettings[RNMobileMessagingConfiguration.Keys.userDataPersistingDisabled].unwrap(orDefault: false)
        
        var mobileMessaging = mobileMessaging
        
        if let storageAdapter = messageStorageAdapter, configuration.messageStorageEnabled {
            mobileMessaging = mobileMessaging.withMessageStorage(storageAdapter)
        } else if configuration.defaultMessageStorage {
            mobileMessaging = mobileMessaging.withDefaultMessageStorage()
        }
        if let categories = configuration.categories {
            mobileMessaging = mobileMessaging.withInteractiveNotificationCategories(Set(categories))
        }
        MobileMessaging.userAgent.pluginVersion = "reactNative \(configuration.reactNativePluginVersion)"
        if configuration.logging {
            MobileMessaging.logger = RNMMLogger()
        }

        if configuration.inAppChatEnabled {
            mobileMessaging = mobileMessaging.withInAppChat()
        }

        if configuration.fullFeaturedInAppsEnabled {
            mobileMessaging = mobileMessaging.withFullFeaturedInApps()
        }
        
        if let webViewSettings = configuration.webViewSettings {
            mobileMessaging.webViewSettings.configureWith(rawConfig: webViewSettings)
        }

        if let jwt = configuration.userDataJwt, !jwt.isEmpty {
            mobileMessaging = mobileMessaging.withJwtSupplier(VariableJwtSupplier(jwt: jwt))
        }

        mobileMessaging.start({
            onSuccess(nil)
        })
    }
    
    private func stop(stopObservations: Bool = false, completion: @escaping () -> Void) {
        self.eventsManager?.stop(stopObservations: stopObservations)
        MobileMessaging.stop(false, completion: completion)
    }

    private func needsRestart(userConfigDict: [String: AnyObject], applicationCode: String) -> Bool {
        let configurationChanged = RNMobileMessagingConfiguration.didConfigurationChange(userConfigDict: userConfigDict)
        let applicationCodeChanged = MobileMessaging.didApplicationCodeChange(applicationCode: applicationCode)

        return configurationChanged || applicationCodeChanged
    }
    
    /*User Profile Management*/
    
    @objc(saveUser:onSuccess:onError:)
    func saveUser(userData: NSDictionary, onSuccess: @escaping RCTResponseSenderBlock, onError: @escaping RCTResponseSenderBlock) {
        guard let userDataDictionary = userData as? [String: Any], let user = MMUser(dictRepresentation: userDataDictionary) else
        {
            onError([NSError(type: .InvalidArguments).reactNativeObject])
            return
        }
        
        MobileMessaging.saveUser(user, completion: { (error) in
            if let error = error {
                onError([error.reactNativeObject])
            } else {
                onSuccess([MobileMessaging.getUser()?.dictionaryRepresentation ?? [:]])
            }
        })
    }
    
    @objc(fetchInboxMessages:externalUserId:inboxFilterOptions:onSuccess:onError:)
    func fetchInboxMessages(token: String, externalUserId: String, inboxFilterOptions: NSDictionary, onSuccess: @escaping RCTResponseSenderBlock, onError: @escaping RCTResponseSenderBlock) {
        var filteringOptions: MMInboxFilterOptions? = nil
        if let inboxFilterOptionsDictionary = inboxFilterOptions as? [String: Any] {
            let inboxFilterOptions = MMInboxFilterOptions(dictRepresentation: inboxFilterOptionsDictionary)
            filteringOptions = inboxFilterOptions
        }
        MobileMessaging.inbox?.fetchInbox(token: token, externalUserId: externalUserId, options: filteringOptions, completion: { (inbox, error) in
            if let error = error {
                onError([error.reactNativeObject])
            } else {
                onSuccess([inbox?.dictionary() ?? [:]])
            }
        })
    }
    
    @objc(fetchInboxMessagesWithoutToken:inboxFilterOptions:onSuccess:onError:)
    func fetchInboxMessagesWithoutToken(externalUserId: String, inboxFilterOptions: NSDictionary, onSuccess: @escaping RCTResponseSenderBlock, onError: @escaping RCTResponseSenderBlock) {
        var filteringOptions: MMInboxFilterOptions? = nil
        if let inboxFilterOptionsDictionary = inboxFilterOptions as? [String: Any] {
            let inboxFilterOptions = MMInboxFilterOptions(dictRepresentation: inboxFilterOptionsDictionary)
            filteringOptions = inboxFilterOptions
        }
        MobileMessaging.inbox?.fetchInbox(externalUserId: externalUserId, options: filteringOptions, completion: { (inbox, error) in
            if let error = error {
                onError([error.reactNativeObject])
            } else {
                onSuccess([inbox?.dictionary() ?? [:]])
            }
        })
    }
    
    @objc(setInboxMessagesSeen:messages:onSuccess:onError:)
    func setInboxMessagesSeen(externalUserId: String, messages: [String]?, onSuccess: @escaping RCTResponseSenderBlock, onError: @escaping RCTResponseSenderBlock) {
        guard let messagesIDs = messages else
        {
            onError([NSError(type: .InvalidArguments).reactNativeObject])
            return
        }
        MobileMessaging.inbox?.setSeen(externalUserId: externalUserId, messageIds: messagesIDs, completion: { (error) in
            if let error = error {
                onError([error.reactNativeObject])
            } else {
                onSuccess(messagesIDs)
            }
        })
    }
    
    
    @objc(fetchUser:onError:)
    func fetchUser(onSuccess: @escaping RCTResponseSenderBlock, onError: @escaping RCTResponseSenderBlock) {
        MobileMessaging.fetchUser(completion: { (user, error) in
            if let error = error {
                onError([error.reactNativeObject])
            } else {
                onSuccess([MobileMessaging.getUser()?.dictionaryRepresentation ?? [:]])
            }
        })
    }
    
    @objc(getUser:)
    func getUser(onSuccess: RCTResponseSenderBlock) {
        onSuccess([MobileMessaging.getUser()?.dictionaryRepresentation ?? [:]])
    }
    
    @objc(saveInstallation:onSuccess:onError:)
    func saveInstallation(installation: NSDictionary, onSuccess: @escaping RCTResponseSenderBlock, onError: @escaping RCTResponseSenderBlock) {
        guard let installationDictionary = installation as? [String: Any], let installation = MMInstallation(dictRepresentation: installationDictionary) else
        {
            onError([NSError(type: .InvalidArguments).reactNativeObject])
            return
        }
        
        MobileMessaging.saveInstallation(installation, completion: { (error) in
            if let error = error {
                onError([error.reactNativeObject])
            } else {
                onSuccess([MobileMessaging.getInstallation()?.dictionaryRepresentation ?? [:]])
            }
        })
    }
    
    @objc(fetchInstallation:onError:)
    func fetchInstallation(onSuccess: @escaping RCTResponseSenderBlock, onError: @escaping RCTResponseSenderBlock) {
        MobileMessaging.fetchInstallation(completion: { (installation, error) in
            if let error = error {
                onError([error.reactNativeObject])
            } else {
                onSuccess([MobileMessaging.getInstallation()?.dictionaryRepresentation ?? [:]])
            }
        })
    }
    
    @objc(getInstallation:)
    func getInstallation(onSuccess: RCTResponseSenderBlock) {
        onSuccess([MobileMessaging.getInstallation()?.dictionaryRepresentation ?? [:]])
    }
    
    @objc(setInstallationAsPrimary:primary:onSuccess:onError:)
    func setInstallationAsPrimary(pushRegistrationId: NSString, primary: Bool, onSuccess: @escaping RCTResponseSenderBlock, onError: @escaping RCTResponseSenderBlock) {
        MobileMessaging.setInstallation(withPushRegistrationId: pushRegistrationId as String, asPrimary: primary, completion: { (installations, error) in
            if let error = error {
                onError([error.reactNativeObject])
            } else {
                onSuccess(installations?.map({ $0.dictionaryRepresentation }) ?? [])
            }
        })
    }
    
    @objc(personalize:onSuccess:onError:)
    func personalize(context: NSDictionary, onSuccess: @escaping RCTResponseSenderBlock, onError: @escaping RCTResponseSenderBlock) {
        guard let context = context as? [String: Any], let uiDict = context["userIdentity"] as? [String: Any] else
        {
            onError([NSError(type: .InvalidArguments).reactNativeObject])
            return
        }
        guard let ui = MMUserIdentity(phones: uiDict["phones"] as? [String], emails: uiDict["emails"] as? [String], externalUserId: uiDict["externalUserId"] as? String) else
        {
            onError([NSError(type: .InvalidUserIdentity).reactNativeObject])
            return
        }
        let uaDict = context["userAttributes"] as? [String: Any]
        let ua = uaDict == nil ? nil : MMUserAttributes(dictRepresentation: uaDict!)
        let keepAsLead = (context["keepAsLead"] as? Bool) ?? false
        let forceDepersonalize = context["forceDepersonalize"] as? Bool ?? false
        MobileMessaging.personalize(forceDepersonalize: forceDepersonalize, keepAsLead: keepAsLead, userIdentity: ui, userAttributes: ua) { (error) in
            if let error = error {
                onError([error.reactNativeObject])
            } else {
                onSuccess([MobileMessaging.getUser()?.dictionaryRepresentation ?? [:]])
            }
        }
    }
    
    @objc(depersonalize:onError:)
    func depersonalize(onSuccess: @escaping RCTResponseSenderBlock, onError: @escaping RCTResponseSenderBlock) {
        MobileMessaging.depersonalize(completion: { (status, error) in
            if (status == MMSuccessPending.pending) {
                onSuccess(["pending"])
            } else if let error = error {
                onError([error.reactNativeObject])
            } else {
                onSuccess(["success"])
            }
        })
    }
    
    @objc(depersonalizeInstallation:onSuccess:onError:)
    func depersonalizeInstallation(pushRegistrationId: NSString, onSuccess: @escaping RCTResponseSenderBlock, onError: @escaping RCTResponseSenderBlock) {
        MobileMessaging.depersonalizeInstallation(withPushRegistrationId: pushRegistrationId as String, completion: { (installations, error) in
            if let error = error {
                onError([error.reactNativeObject])
            } else {
                onSuccess(installations?.map({ $0.dictionaryRepresentation }) ?? [])
            }
        })
    }
    
    /*Messages and Notifications*/
    
    @objc(markMessagesSeen:onSuccess:onError:)
    func markMessagesSeen(messageIds: [String], onSuccess: @escaping RCTResponseSenderBlock, onError: RCTResponseSenderBlock) {
        guard !messageIds.isEmpty else {
            onError([NSError(type: .InvalidArguments).reactNativeObject])
            return
        }
        MobileMessaging.setSeen(messageIds: messageIds, completion: {
            onSuccess(messageIds);
        })
    }
    
    @objc(defaultMessageStorage_find:onSuccess:onError:)
    func defaultMessageStorage_find(messageId: NSString, onSuccess: @escaping RCTResponseSenderBlock, onError: RCTResponseSenderBlock) {
        guard let storage = MobileMessaging.defaultMessageStorage else {
            onError([NSError(type: .DefaultStorageNotInitialized).reactNativeObject])
            return
        }
        
        storage.findMessages(withIds: [(messageId as MessageId)], completion: { messages in
            onSuccess([messages?[0].dictionary() ?? [:]])
        })
    }
    
    @objc(defaultMessageStorage_findAll:onError:)
    func defaultMessageStorage_findAll(onSuccess: @escaping RCTResponseSenderBlock, onError: RCTResponseSenderBlock) {
        guard let storage = MobileMessaging.defaultMessageStorage else {
            onError([NSError(type: .DefaultStorageNotInitialized).reactNativeObject])
            return
        }
        
        storage.findAllMessages(completion: { messages in
            let result = messages?.map({$0.dictionary()})
            onSuccess([result ?? []])
        })
    }
    
    @objc(defaultMessageStorage_delete:onSuccess:onError:)
    func defaultMessageStorage_delete(messageId: NSString, onSuccess: @escaping RCTResponseSenderBlock, onError: RCTResponseSenderBlock) {
        guard let storage = MobileMessaging.defaultMessageStorage else {
            onError([NSError(type: .DefaultStorageNotInitialized).reactNativeObject])
            return
        }
        
        storage.remove(withIds: [(messageId as MessageId)]) { _ in
            onSuccess(nil)
        }
    }
    
    @objc(defaultMessageStorage_deleteAll:onError:)
    func defaultMessageStorage_deleteAll(onSuccess: @escaping RCTResponseSenderBlock, onError: RCTResponseSenderBlock) {
        MobileMessaging.defaultMessageStorage?.removeAllMessages() { _ in
            onSuccess(nil)
        }
    }
    
    /*
     Custom message storage:
     methods to provide results to Native Bridge.
     Need to be called from JS part.
     */
    
    @objc(messageStorage_provideFindResult:)
    func messageStorage_provideFindResult(messageDict: [String: Any]?) {
        self.messageStorageAdapter?.findResult(messageDict: messageDict)
    }
    
    @objc(messageStorage_provideFindAllResult:)
    func messageStorage_provideFindAllResult(messages: [Any]?) {
        //not needed for iOS SDK
    }
    
    /* Events */
    
    @objc(submitEvent:onError:)
    func submitEvent(eventData: NSDictionary, onError: @escaping RCTResponseSenderBlock) {
        guard let eventDataDictionary = eventData as? [String: Any], let event = MMCustomEvent(dictRepresentation: eventDataDictionary) else
        {
            onError([NSError(type: .InvalidArguments).reactNativeObject])
            return
        }
        
        MobileMessaging.submitEvent(event)
    }
    
    @objc(submitEventImmediately:onSuccess:onError:)
    func submitEventImmediately(eventData: NSDictionary, onSuccess: @escaping RCTResponseSenderBlock, onError: @escaping RCTResponseSenderBlock) {
        guard let eventDataDictionary = eventData as? [String: Any], let event = MMCustomEvent(dictRepresentation: eventDataDictionary) else
        {
            onError([NSError(type: .InvalidArguments).reactNativeObject])
            return
        }
        
        MobileMessaging.submitEvent(event) { (error) in
            if let error = error {
                onError([error.reactNativeObject])
            } else {
                onSuccess(nil)
            }
        }
    }
    
    /*
     It's not supported for iOS, method created for compatibility
     */
    @objc(showDialogForError:onSuccess:onError:)
    func showDialogForError(errorCode: Int, onSuccess: RCTResponseSenderBlock, onError: RCTResponseSenderBlock) {
        onError([NSError(type: .NotSupported).reactNativeObject])
    }

    @objc(setUserDataJwt:onSuccess:onError:)
    func setUserDataJwt(jwt: String?, onSuccess: @escaping RCTResponseSenderBlock, onError: @escaping RCTResponseSenderBlock) {
      MobileMessaging.jwtSupplier = VariableJwtSupplier(jwt: jwt)
      onSuccess(nil)
    }

    class VariableJwtSupplier: NSObject, MMJwtSupplier {
      private let jwt: String?

      init(jwt: String?) {
          self.jwt = jwt
          super.init()
      }

      func getJwt() -> String? {
          return jwt
      }
    }
    
    var isChatAvailable: Bool {
        return RNMobileMessagingEventsManager.isChatAvailable
    }

}
