import Foundation
import Hackle
import os.log
import React

@objc(HackleReactNativeSdk)
public final class HackleReactNativeSdk: RCTEventEmitter {

    internal var hasEventListener = false
    internal var inAppMessageOnClickResolveBlock: ((Bool) -> Void)?
    internal var inAppMessageView: HackleInAppMessageView? = nil
    internal var inAppMessageClickLock = NSRecursiveLock()

    public override func supportedEvents() -> [String]! {
        return [HackleInAppMessageEvents.beforeOpen.rawValue,
                HackleInAppMessageEvents.afterOpen.rawValue,
                HackleInAppMessageEvents.beforeClose.rawValue,
                HackleInAppMessageEvents.afterClose.rawValue,
                HackleInAppMessageEvents.onClick.rawValue]
    }

    @objc
    func initialize(_ sdkKey: String, config: NSDictionary?, user: NSDictionary?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        let hackleConfig = config?.toHackleConfig() ?? HackleConfig.DEFAULT
        let hackleUser = user?.toUser()
        DispatchQueue.main.async {
            if let hackleUser = hackleUser {
                Hackle.initialize(sdkKey: sdkKey, user: hackleUser, config: hackleConfig) {
                    resolve(nil)
                }
            } else {
                Hackle.initialize(sdkKey: sdkKey, config: hackleConfig) {
                    resolve(nil)
                }
            }
            self.internalSetInAppMessageDelegate()
        }
    }

    private func internalSetInAppMessageDelegate() {
        do {
            let hackleApp = try getHackleApp()
            hackleApp.inAppMessageDelegate = self
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
        }
    }

    private func getHackleApp() throws -> HackleApp {
        guard let hackleApp = Hackle.app() else {
            throw HackleError.error("HackleApp is not initialized. Make sure to call initialize first")
        }
        return hackleApp
    }

    @objc
    func variationDetailSync(_ experimentKey: NSNumber) -> [String: Any] {
        do {
            let hackleApp = try getHackleApp()
            let decision = hackleApp.variationDetail(experimentKey: Int(truncating: experimentKey))
            return decision.toMap()
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            return getDefaultDecisionMap()
        }
    }

    @objc
    func variationDetail(_ experimentKey: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        resolve(variationDetailSync(experimentKey))
    }

    private func getDefaultDecisionMap() -> [String: Any] {
        return ["variation": "A", "reason": "EXCEPTION", "parameters": []]
    }

    @objc
    func featureFlagDetailSync(_ featureKey: NSNumber) -> [String: Any] {
        do {
            let hackleApp = try getHackleApp()
            let decision = hackleApp.featureFlagDetail(featureKey: Int(truncating: featureKey))
            return decision.toMap()
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            return getDefaultFeatureFlagDecisionMap()
        }
    }

    private func getDefaultFeatureFlagDecisionMap() -> [String: Any] {
        return ["variation": "A", "reason": "EXCEPTION", "parameters": []]
    }

    @objc
    func featureFlagDetail(_ featureKey: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        resolve(featureFlagDetailSync(featureKey))
    }

    @objc
    func track(_ event: NSDictionary) {
        do {
            let hackleApp = try getHackleApp()
            let event = try event.toEvent()
            hackleApp.track(event: event)
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
        }
    }

    @objc
    func getRemoteConfigStringSync(_ parameterKey: String, defaultValue: String) -> String {
        do {
            let hackleApp = try getHackleApp()
            return hackleApp.remoteConfig().getString(forKey: parameterKey, defaultValue: defaultValue)
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            return defaultValue
        }
    }

    @objc
    func getRemoteConfigString(_ parameterKey: String, defaultValue: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        resolve(getRemoteConfigStringSync(parameterKey, defaultValue: defaultValue))
    }

    @objc
    func getRemoteConfigDoubleSync(_ parameterKey: String, defaultValue: NSNumber) -> NSNumber {
        do {
            let hackleApp = try getHackleApp()
            let value = hackleApp.remoteConfig().getDouble(forKey: parameterKey, defaultValue: Double(truncating: defaultValue))
            return NSNumber(value: value)
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            return defaultValue
        }
    }

    @objc
    func getRemoteConfigDouble(_ parameterKey: String, defaultValue: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        resolve(getRemoteConfigDoubleSync(parameterKey, defaultValue: defaultValue))
    }

    @objc
    func getRemoteConfigBooleanSync(_ parameterKey: String, defaultValue: NSNumber) -> NSNumber {
        do {
            let hackleApp = try getHackleApp()
            let value = hackleApp.remoteConfig().getBool(forKey: parameterKey, defaultValue: defaultValue.boolValue)
            return NSNumber(value: value)
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            return defaultValue
        }
    }

    @objc
    func getRemoteConfigBoolean(_ parameterKey: String, defaultValue: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        resolve(getRemoteConfigBooleanSync(parameterKey, defaultValue: defaultValue))
    }

    @objc
    func getUser(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            resolve(hackleApp.user.toMap())
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve([:])
        }
    }

    @objc
    func setUser(_ user: NSDictionary?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            guard let user = user?.toUser() else {
                resolve(nil)
                return
            }
            hackleApp.setUser(user: user) { resolve(nil) }
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    func setUserId(_ userId: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            hackleApp.setUserId(userId: userId) { resolve(nil) }
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    func setDeviceId(_ deviceId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            hackleApp.setDeviceId(deviceId: deviceId) { resolve(nil) }
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    func updateUserProperties(_ operations: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            hackleApp.updateUserProperties(operations: operations.toPropertyOperations()) { resolve(nil) }
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    func updatePushSubscriptions(_ operations: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()

            hackleApp.updatePushSubscriptions(operations: operations.toHackleSubscriptionOperations())
            resolve(nil)
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    func updateSmsSubscriptions(_ operations: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            hackleApp.updateSmsSubscriptions(operations: operations.toHackleSubscriptionOperations())
            resolve(nil)
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    func updateKakaoSubscriptions(_ operations: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            hackleApp.updateKakaoSubscriptions(operations: operations.toHackleSubscriptionOperations())
            resolve(nil)
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    func resetUser(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            hackleApp.resetUser { resolve(nil) }
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    func setPhoneNumber(_ phoneNumber: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            hackleApp.setPhoneNumber(phoneNumber: phoneNumber) { resolve(nil) }
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    func unsetPhoneNumber(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            hackleApp.unsetPhoneNumber { resolve(nil) }
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    func showUserExplorer(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            hackleApp.showUserExplorer()
            resolve(nil)
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    func hideUserExplorer(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            hackleApp.hideUserExplorer()
            resolve(nil)
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    func fetch(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            hackleApp.fetch {
                resolve(nil)
            }
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    func setCurrentScreen(_ screen: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            let screenObject = screen.toScreen()
            hackleApp.setCurrentScreen(screen: screenObject)
            resolve(nil)
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    public func isInvocableString(_ command: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            let result = hackleApp.invocator().isInvocableString(string: command)
            resolve(result)
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    public func bridgeInvoke(_ command: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            let result = hackleApp.invocator().invoke(string: command)
            resolve(result)
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    func setOptOutTracking(_ optOut: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            hackleApp.setOptOutTracking(optOut: optOut)
            resolve(nil)
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(nil)
        }
    }

    @objc
    func isOptOutTracking(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        do {
            let hackleApp = try getHackleApp()
            resolve(hackleApp.isOptOutTracking)
        } catch {
            os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
            resolve(false)
        }
    }

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

    @objc
    func beginListening(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        self.hasEventListener = true
        resolve(nil)
    }

    @objc
    func endListening(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        self.hasEventListener = false
        resolve(nil)
    }

    public override func stopObserving() {
        self.hasEventListener = false
        self.internalResolveInAppMessageClick(result: false)
        self.internalCloseInAppMessageView()
    }

    @objc
    func resolveInAppMessageClick(_ result: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        self.internalResolveInAppMessageClick(result: result)
        resolve(nil)
    }

    func internalResolveInAppMessageClick(result: Bool) {
        if let resolveBlock = inAppMessageOnClickResolveBlock {
            resolveBlock(result)
            self.inAppMessageOnClickResolveBlock = nil
        }
    }

    @objc
    func closeInAppMessageView(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        self.internalCloseInAppMessageView()
        resolve(nil)
    }

    func internalCloseInAppMessageView() {
        DispatchQueue.main.async {
            self.inAppMessageView?.dismiss()
            self.inAppMessageView = nil
        }
    }

    @objc
    func dismissDisplayedInAppMessageView(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        DispatchQueue.main.async {
            do {
                let hackleApp = try self.getHackleApp()
                hackleApp.displayedInAppMessageView?.dismiss()
                resolve(nil)
            } catch {
                os_log("%@", log: .hackle, type: .error, "Unexpected error: \(error).")
                resolve(nil)
            }
        }
    }
}
