// Copyright (C) 2025 Acoustic, L.P. All rights reserved.
//
// NOTICE: This file contains material that is confidential and proprietary to
// Acoustic, L.P. and/or other developers. No license is granted under any intellectual or
// industrial property rights of Acoustic, L.P. except as may be provided in an agreement with
// Acoustic, L.P. Any unauthorized copying or distribution of content from this file is
// prohibited.
//
//
//  Created on 5/9/25.
//

import Foundation
import Connect
import NitroModules
import OSLog

// Two loggers under one subsystem so developers can filter Console.app / Xcode
// output by category. `config` covers anything about reading and validating
// `AcousticConnectRNConfig.bundle`; `bridge` covers SDK lifecycle calls
// (`enable`, `disable`, the resolved-config summary line).
private let configLog = Logger(subsystem: "com.acoustic.AcousticConnectRN", category: "config")
private let bridgeLog = Logger(subsystem: "com.acoustic.AcousticConnectRN", category: "bridge")

// Small descriptor on `ConnectPushConfig` so the bridge can log "off" /
// "automatic" / "manual" without round-tripping through the JS-side enum.
private extension ConnectPushConfig {
    var modeDescription: String {
        switch self.mode {
        case .off:       return "off"
        case .automatic: return "automatic"
        case .manual:    return "manual"
        @unknown default: return "unknown"
        }
    }
}

// Single compat entry point for the config-item store. Works against every
// shipping iOS SDK variant (Release with separate EOCore/Tealeaf, current merged
// Debug 2.1.2, future merged-only) because `ConnectApplicationHelper` ships with
// each of them. Not importing `EOCore`/`Tealeaf` avoids a link dependency on
// frameworks that aren't always packaged into the app bundle.
private enum ConnectConfigStore {
    @discardableResult
    static func set(_ key: String, value: Any) -> Bool {
        return ConnectApplicationHelper.sharedInstance().setConfigurableItem(key, value: value)
    }

    static func bool(forKey key: String, default def: Bool) -> Bool {
        let raw = ConnectApplicationHelper.sharedInstance().value(forConfigurableItem: key)
        if let n = raw as? NSNumber { return n.boolValue }
        if let s = raw as? String   { return (s as NSString).boolValue }
        return def
    }

    static func string(forKey key: String, default def: String) -> String? {
        return (ConnectApplicationHelper.sharedInstance().value(forConfigurableItem: key) as? String) ?? def
    }

    static func number(forKey key: String, default def: Double) -> Double {
        return (ConnectApplicationHelper.sharedInstance().value(forConfigurableItem: key) as? NSNumber)?.doubleValue ?? def
    }
}

// MARK: - Push adapter wrappers

/// Bridges a JS-supplied `userInfo` dictionary to the SDK's `ConnectNotification`
/// protocol (a single `userInfo` requirement).
private struct HybridAcousticConnectNotification: ConnectNotification {
    let userInfo: [AnyHashable: Any]

    init(userInfo: [String: Any]) {
        self.userInfo = userInfo.reduce(into: [AnyHashable: Any]()) { $0[$1.key] = $1.value }
    }
}

/// Bridges a JS-supplied response to the SDK's `ConnectNotificationResponse`
/// protocol (`actionIdentifier` + `userInfo`).
private struct HybridAcousticConnectNotificationResponse: ConnectNotificationResponse {
    let actionIdentifier: String
    let userInfo: [AnyHashable: Any]

    init(actionIdentifier: String, userInfo: [String: Any]) {
        self.actionIdentifier = actionIdentifier
        self.userInfo = userInfo.reduce(into: [AnyHashable: Any]()) { $0[$1.key] = $1.value }
    }
}

class HybridAcousticConnectRN: HybridAcousticConnectRNSpec {

    /// Push mode resolved from `ConnectConfig.json` at `load()` time. The push
    /// bridge methods branch on this synchronously rather than calling into the
    /// `@MainActor` SDK to detect mode (mirrors the Android bridge): manual mode
    /// forwards events to the SDK; automatic/off surface `EAC-RN-007` (`false`).
    ///
    // Constructor — auto-inits the SDK from `ConnectConfig.json`. Matches the
    // pre-existing auto-init behaviour; consumers that need consent-gated init can call
    // `disable()` immediately and `enable()` once they have permission.
    override init() {
        super.init()

        Task { @MainActor [weak self] in
            self?.load()
        }

        bridgeLog.info("HybridAcousticConnectRN constructed; load() dispatched on main actor.")
    }

    // `load()` calls into the MainActor-isolated `ConnectSDK.shared` API.
    // Marking the method `@MainActor` means Swift can verify the SDK calls
    // without an inner dispatch, and it makes the contract explicit for any
    // future caller.
    @MainActor
    func load() {
        let connectData = self.parseConnectConfigJSON()

        guard let connectData = connectData else {
            configLog.error("AcousticConnectRNConfig.bundle missing or unreadable. SDK not initialised. Verify ConnectConfig.json at your project root and re-run `pod install`.")
            return
        }
        configLog.info("Read AcousticConnectRNConfig.bundle (\(connectData.count) top-level keys).")

        let appKey  = (connectData["AppKey"]         as? String) ?? ""
        let postURL = (connectData["PostMessageUrl"] as? String) ?? ""

        // Skip enable entirely if the consumer's config didn't reach us —
        // proceeding with empty strings would let the SDK silently fall back
        // to its bundled demo collector, which is exactly the bug this flow
        // is designed to prevent. Most likely cause: missing or malformed
        // ConnectConfig.json at the consumer's project root, or `pod install`
        // didn't run after the file was added.
        guard !appKey.isEmpty, !postURL.isEmpty else {
            configLog.error("SDK NOT ENABLED — empty AppKey or PostMessageUrl. Verify ConnectConfig.json at your project root contains both fields, then re-run `pod install`.")
            return
        }

        // Log every field the bridge consumes so the developer can see in
        // Xcode / Console.app exactly what's in effect, without cross-checking
        // ConnectConfig.json.
        configLog.info("AppKey: \(appKey, privacy: .public) (length=\(appKey.count))")
        configLog.info("PostMessageUrl: \(postURL, privacy: .public)")
        if let killSwitch = connectData["KillSwitchUrl"] as? String, !killSwitch.isEmpty {
            configLog.info("KillSwitchUrl: \(killSwitch, privacy: .public)")
        }

        let pushConfig = ConnectRNParsing.resolvePushConfig(from: connectData)

        // Non-release integrations (useRelease:false — the AcousticConnectDebug
        // SDK variant) turn on the Connect/Tealeaf/EOCore verbose native logging
        // so it surfaces in the Xcode console / Console.app for QA and support.
        // Must be set before enable() below: EOCore reads `EODebug` from the
        // process environment and caches it on the first log call. overwrite=0
        // leaves any value the host already set (e.g. an Xcode scheme) intact,
        // so a consumer can force it off with CONNECT_DEBUG=0. Release builds
        // (useRelease:true) stay quiet.
        let useRelease = (connectData["useRelease"] as? Bool) ?? false
        if !useRelease {
            setenv("CONNECT_DEBUG", "1", 0)
            setenv("TLF_DEBUG", "1", 0)
            setenv("EODebug", "1", 0)
            bridgeLog.info("useRelease=false — enabled verbose native SDK logging (CONNECT_DEBUG/TLF_DEBUG/EODebug)")
        }

        let sdk = ConnectSDK.shared
        bridgeLog.info("Calling ConnectSDK.shared.enable(with: ConnectConfig(push: \(pushConfig.modeDescription, privacy: .public)))")
        sdk.enable(with: ConnectConfig(appKey: appKey, postURL: postURL, push: pushConfig))
        _ = sdk.setReactNative(true, wrapNavigationContainer: true)

        // Apply remaining programmatic overrides (everything other than the
        // two enable parameters) AFTER enable — matches the order in the SDK's
        // own ConnectSDK.shared.enable(with:) implementation.
        self.applyConnectConfig(connectData)

        bridgeLog.info("SDK initialised: appKey length=\(appKey.count), push=\(pushConfig.modeDescription, privacy: .public), isReactNative=true, wrapNavigationContainer=true")
    }

    // Push-config resolution and the PushEnabled / iOSPushMode parsers moved
    // to ConnectRNParsing (behaviour-preserving extraction) so
    // they are unit-testable without constructing this hybrid.

    /// Reads and returns the `Connect` dictionary from the bundled
    /// `AcousticConnectRNConfig.json`. The bundle is populated at pod install
    /// time by the podspec — see `AcousticConnectRN.podspec`. Returns nil when
    /// the bundle or file is missing; callers fall back to empty values.
    private func parseConnectConfigJSON() -> [String: Any]? {
        let bundle = Bundle(for: Self.self)
        let bundleURL = bundle.resourceURL?.appendingPathComponent("AcousticConnectRNConfig.bundle")
        let resourceBundle = bundleURL.flatMap { Bundle(url: $0) }
        let path = resourceBundle?.path(forResource: "AcousticConnectRNConfig", ofType: "json")
        let data = path.flatMap { try? Data(contentsOf: URL(fileURLWithPath: $0)) }
        let jsonData = data.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] }
        return jsonData?["Connect"] as? [String: Any]
    }
    
    /// Applies every recognised key from the parsed `Connect` config as a
    /// programmatic override. Called by `load()` AFTER enable so values land on
    /// a fully-initialised SDK. AppKey and PostMessageUrl are skipped here —
    /// they're passed to enable directly because the SDK's bundle-read
    /// short-circuits `tempConfigDict` for those two specifically.
    private func applyConnectConfig(_ connectData: [String: Any]) {
        let eocoreKeys = [
            "CachingLevel", "DoPostAppComesFromBackground", "DoPostAppGoesToBackground", "DoPostAppGoesToClose",
            "DoPostAppIsLaunched", "DoPostOnIntervals", "DynamicConfigurationEnabled", "HasToPersistLocalCache",
            "LoggingLevel", "ManualPostEnabled", "PostMessageLevelCellular", "PostMessageLevelWiFi",
            "PostMessageTimeIntervals", "CachedFileMaxBytesSize", "CompressPostMessage", "DefaultOrientation",
            "LibraryVersion", "MaxNumberOfFilesToCache", "MessageVersion", "PostMessageMaxBytesSize",
            "PostMessageTimeout", "TurnOffCorrectOrientationUpdates"
        ]

        let tealeafKeys = [
            "AppKey", "DisableAutoInstrumentation", "GetImageDataOnScreenLayout", "JavaScriptInjectionDelay",
            "KillSwitchEnabled", "KillSwitchMaxNumberOfTries", "KillSwitchTimeInterval", "KillSwitchTimeout",
            "KillSwitchUrl", "UseWhiteList", "WhiteListParam", "LogLocationEnabled", "MaxStringsLength",
            "PercentOfScreenshotsSize", "PercentToCompressImage", "ScreenShotPixelDensity", "PostMessageUrl",
            "DoPostOnScreenChange", "printScreen", "ScreenshotFormat", "SessionTimeout", "SessionizationCookieName",
            "CookieSecure", "disableTLTDID", "SetGestureDetector", "AddGestureRecognizerUIButton",
            "AddGestureRecognizerUIDatePicker", "AddGestureRecognizerUIPageControl", "AddGestureRecognizerUIPickerView",
            "AddGestureRecognizerUIScrollView", "AddGestureRecognizerUISegmentedControl", "AddGestureRecognizerUISwitch",
            "AddGestureRecognizerUITextView", "AddGestureRecognizerWKWebView", "AddMessageTypeHeader",
            "DisableAlertAutoCapture", "DisableAlertBackgroundForDisabledLogViewLayout", "DisableKeyboardCapture",
            "EnableWebViewInjectionForDisabledAutoCapture", "FilterMessageTypes", "InitialZIndex", "IpPlaceholder",
            "LibraryVersion", "LogFullRequestResponsePayloads", "LogViewLayoutOnScreenTransition", "MessageTypeHeader",
            "MessageTypes", "RemoveIp", "RemoveSwiftUIDuplicates", "SubViewArrayZIndexIncrementTrigger",
            "SwiftUICaptureNonVariadic", "TextFieldBeingEditedUseSender", "TreatJsonDictionariesAsString", "UICPayload",
            "UIKeyboardCaptureTouches", "UseJPGForReplayImagesExtension", "UseXpathId", "actionSheet:buttonIndex",
            "actionSheet:show", "alertView:buttonIndex", "alertView:show", "autolog:pageControl",
            "autolog:textBox:_searchFieldEndEditing", "button:click", "button:load", "canvas:click", "connection",
            "customEvent", "datePicker:dateChange", "exception", "gestures", "label:load", "label:textChange", "layout",
            "location", "mobileState", "orientation", "pageControl:valueChanged", "pickerView:valueChanged",
            "screenChangeLevel", "scroller:scrollChange", "selectList:UITableViewSelectionDidChangeNotification",
            "selectList:load", "selectList:valueChange", "slider:valueChange", "stepper:valueChange",
            "textBox:_searchFieldBeginChanged", "textBox:_searchFieldBeginEditing", "textBox:_searchFieldEditingChanged",
            "textBox:textChange", "textBox:textChanged", "textBox:textFieldDidChange", "toggleButton:click"
        ]

        for (key, value) in connectData {
            // Skip keys load() passes directly to enable — re-applying is harmless but redundant.
            if key == "AppKey" || key == "PostMessageUrl" { continue }

            if tealeafKeys.contains(key) || eocoreKeys.contains(key) {
                ConnectConfigStore.set(key, value: value)
            } else if key == "layoutConfig",
                      let layoutConfig = value as? [String: Any] {
                if let autoLayout = layoutConfig["AutoLayout"] {
                    ConnectConfigStore.set("AutoLayout", value: autoLayout)
                }
                if let appendMapIds = layoutConfig["AppendMapIds"] {
                    ConnectConfigStore.set("AppendMapIds", value: appendMapIds)
                }
            }
        }
    }

    // MARK: - Gate-keeper API

    /// Re-enables the Connect SDK after a prior `disable()`. All configuration
    /// comes from `ConnectConfig.json` — re-runs `load()` on the main actor,
    /// which the native SDK no-ops if it's already enabled.
    func enable() throws -> Bool {
        bridgeLog.info("enable() called from JS.")
        Task { @MainActor [weak self] in self?.load() }
        return true
    }

    /// Disables the Connect SDK. Idempotent — the native SDK handles repeat
    /// calls safely. After this call, sessions and push state are released;
    /// a subsequent `enable()` re-initialises from `ConnectConfig.json`.
    func disable() throws -> Bool {
        bridgeLog.info("disable() called from JS.")
        Task { @MainActor in
            ConnectSDK.shared.disable()
            bridgeLog.info("SDK disabled.")
        }
        return true
    }

    // MARK: - Push: APNs lifecycle

    /// Forwards the raw APNs device token to the SDK. Nitro hands us native
    /// `Data` via `ArrayBuffer` — no hex conversion, no validation. Idempotent
    /// in automatic mode (the SDK already captured the token via its swizzle).
    ///
    /// Resolves `true` once the SDK accepted the token, `false` if the call was
    /// rejected (e.g. push not enabled). Never rejects.
    func pushDidRegisterWithToken(deviceToken: ArrayBuffer) throws -> Promise<Bool> {
        let token = deviceToken.toData(copyIfNeeded: true)
        return Promise.async { @MainActor in
            do {
                try ConnectSDK.shared.push.didRegisterWithToken(token)
                return true
            } catch {
                bridgeLog.error("pushDidRegisterWithToken failed: \(error.localizedDescription, privacy: .public)")
                return false
            }
        }
    }

    /// Forwards an APNs registration failure to the SDK as an `NSError`.
    /// Resolves `true` once forwarded, `false` on failure. Never rejects.
    func pushDidFailToRegister(error: PushErrorInfo) throws -> Promise<Bool> {
        let nsError = Self.nsError(from: error)
        return Promise.async { @MainActor in
            do {
                try ConnectSDK.shared.push.didFailToRegisterWithError(nsError)
                return true
            } catch {
                bridgeLog.error("pushDidFailToRegister failed: \(error.localizedDescription, privacy: .public)")
                return false
            }
        }
    }

    // MARK: - Push: notification delivery — manual mode only

    /// Forwards a received notification so the SDK logs `pushReceived` (manual
    /// mode). Resolves `true` when processed. In automatic/off mode the SDK
    /// throws `pushModeNotManual` (its own delegate already handles delivery);
    /// the bridge catches it and resolves `false` — the `EAC-RN-007` surface.
    /// Never rejects.
    func pushDidReceiveNotification(userInfo: [String: Variant_Bool_String_Double]) throws -> Promise<Bool> {
        let notification = HybridAcousticConnectNotification(userInfo: convertToAnyDictionary(input: userInfo))
        return Promise.async { @MainActor in
            do {
                try ConnectSDK.shared.push.didReceiveNotification(notification)
                return true
            } catch {
                bridgeLog.error("pushDidReceiveNotification failed: \(error.localizedDescription, privacy: .public)")
                return false
            }
        }
    }

    /// Forwards a notification response (tap / action) so the SDK runs the
    /// built-in action and logs `pushAction` (manual mode). Resolves `true` when
    /// processed; `false` (EAC-RN-007) in automatic/off mode via the same
    /// caught `pushModeNotManual`. Never rejects.
    func pushDidReceiveResponse(actionIdentifier: String, userInfo: [String: Variant_Bool_String_Double]) throws -> Promise<Bool> {
        let response = HybridAcousticConnectNotificationResponse(
            actionIdentifier: actionIdentifier,
            userInfo: convertToAnyDictionary(input: userInfo)
        )
        return Promise.async { @MainActor in
            do {
                try ConnectSDK.shared.push.didReceive(response)
                return true
            } catch {
                bridgeLog.error("pushDidReceiveResponse failed: \(error.localizedDescription, privacy: .public)")
                return false
            }
        }
    }

    // MARK: - Push: permission management

    /// Forwards externally-obtained permission state to the SDK. Tri-state
    /// `granted`: `true`/`false` forward; `nil` (notDetermined) is recorded by
    /// the OS, not forwarded — the SDK has no notion of an "unknown" state.
    func pushDidReceiveAuthorization(granted: Variant_NullType_Bool?, error: PushErrorInfo?) throws -> Promise<Bool> {
        // `nil` (notDetermined) is intentionally not forwarded — the SDK has no
        // notion of an "unknown" authorization. This resolves `true` ("handled —
        // accepted, not forwarded"), not a failure.
        guard let triState = granted?.asType(Bool.self) else {
            return Promise.resolved(withResult: true)
        }
        // Not gated on push mode: the SDK's didReceiveAuthorization is safe in
        // both modes, so externally-obtained permission state is always forwarded.
        let nsError = error.map(Self.nsError(from:))
        return Promise.async { @MainActor in
            do {
                try ConnectSDK.shared.push.didReceiveAuthorization(granted: triState, error: nsError)
                return true
            } catch {
                bridgeLog.error("pushDidReceiveAuthorization failed: \(error.localizedDescription, privacy: .public)")
                return false
            }
        }
    }

    /// Requests notification permission via the SDK, presenting the system
    /// prompt when the status is undetermined, and resolves with the structured
    /// result. Never rejects — a system error (or push-not-enabled) surfaces in
    /// `error` with `granted: false`.
    func pushRequestPermission() throws -> Promise<PushPermissionResult> {
        return Promise.async { @MainActor in
            do {
                let result = try await ConnectSDK.shared.push.requestAuthorization()
                let error: Variant_NullType_String? = result.error.map { .second($0.localizedDescription) }
                return PushPermissionResult(granted: result.granted, error: error)
            } catch {
                bridgeLog.error("pushRequestPermission failed: \(error.localizedDescription, privacy: .public)")
                return PushPermissionResult(granted: false, error: .second(error.localizedDescription))
            }
        }
    }

    /// Reads the current notification permission state without prompting, mapped
    /// to a tri-state: `true` granted, `false` denied, `null` not determined.
    /// Never rejects — push-not-enabled resolves as `null`.
    func pushGetPermissionState() throws -> Promise<Variant_NullType_Bool> {
        return Promise.async { @MainActor in
            do {
                if let granted = try await ConnectSDK.shared.push.getCurrentAuthorization() {
                    return .second(granted)
                }
                return .first(.null)
            } catch {
                bridgeLog.error("pushGetPermissionState failed: \(error.localizedDescription, privacy: .public)")
                return .first(.null)
            }
        }
    }

    // MARK: - Push: helpers

    /// Builds an `NSError` from the structured bridge error object, shared by
    /// `pushDidFailToRegister` and `pushDidReceiveAuthorization`. Kept here
    /// (not in ConnectRNParsing) because `PushErrorInfo` is a C++-backed
    /// nitro type and ConnectRNParsing is also compiled into the UnitTests
    /// bundle, which builds without C++ interop.
    private static func nsError(from info: PushErrorInfo) -> NSError {
        NSError(
            domain: info.domain ?? "ConnectRNBridge",
            code: Int(info.code ?? -1),
            userInfo: [NSLocalizedDescriptionKey: info.message]
        )
    }

    /// Sets the module's configuration item from AdvancedConfig.json or BasicConfig.plist that matches the specified key as a BOOL value.
    /// - Parameters:
    ///   - key: Key to update value in configuration settings.
    ///   - value: Value to use.
    ///   - moduleName: The name of the module to be updated. For EOCore settings, please use 'EOCore' which can be found the following files EOCoreBasicConfig.plist, EOCoreBasicConfig.properties or EOCoreAdvancedConfig.json and 'Connect' for Connect which can be found the following files ConnectBasicConfig.plist, ConnectBasicConfig.properties or ConnectAdvancedConfig.json.
    /// - Returns: Whether it was able to set the value as Boolean value.
    func setBooleanConfigItemForKey(key: String, value: Bool, moduleName: String) throws -> Bool {
        return ConnectConfigStore.set(key, value: value)
    }
    
    /// Sets the module's configuration item from AdvancedConfig.json or BasicConfig.plist that matches the specified key as a NString value.
    /// - Parameters:
    ///   - key: Key to update value in configuration settings.
    ///   - value: Value to use.
    ///   - moduleName: The name of the module to be updated. For EOCore settings, please use 'EOCore' which can be found the following files EOCoreBasicConfig.plist, EOCoreBasicConfig.properties or EOCoreAdvancedConfig.json and 'Connect' for Connect which can be found the following files ConnectBasicConfig.plist, ConnectBasicConfig.properties or ConnectAdvancedConfig.json.
    /// - Returns: Whether it was able to set the value as Boolean value.
    func setStringItemForKey(key: String, value: String, moduleName: String) throws -> Bool {
        return ConnectConfigStore.set(key, value: value)
    }
    
    /// Sets the module's configuration item from AdvancedConfig.json or BasicConfig.plist that matches the specified key as a NSNumber value.
    /// - Parameters:
    ///   - key: Key to update value in configuration settings.
    ///   - value: Value to use.
    ///   - moduleName: The name of the module to be updated. For EOCore settings, please use 'EOCore' which can be found the following files EOCoreBasicConfig.plist, EOCoreBasicConfig.properties or EOCoreAdvancedConfig.json and 'Connect' for Connect which can be found the following files ConnectBasicConfig.plist, ConnectBasicConfig.properties or ConnectAdvancedConfig.json.
    /// - Returns: Whether it was able to set the value as Boolean value.
    func setNumberItemForKey(key: String, value: Double, moduleName: String) throws -> Bool {
        return ConnectConfigStore.set(key, value: value)
    }
  
  
    /// Sets the module's configuration item from AdvancedConfig.json or BasicConfig.properties that matches the specified key.
    /// - Parameters:
    ///   - key: Key to update value in configuration settings.
    ///   - value: Value to use.
    ///   - moduleName: The name of the module to be updated. For EOCore settings, please use 'EOCore' which can be found the following files EOCoreBasicConfig.plist, EOCoreBasicConfig.properties or EOCoreAdvancedConfig.json and 'Connect' for Connect which can be found the following files ConnectBasicConfig.plist, ConnectBasicConfig.properties or ConnectAdvancedConfig.json.
    /// - Returns: Whether it was able to set the value.
    func setConfigItemForKey(key: String, value: Variant_Bool_String_Double, moduleName: String) throws -> Bool {
        return ConnectConfigStore.set(key, value: convertVariantToAny(value))
    }
    
    /// Gets the module's configuration item from AdvancedConfig.json or BasicConfig.plist that matches the specified key as a BOOL value.
    /// - Parameters:
    ///   - key: Key to update value in configuration settings.
    ///   - theDefault: Default value if not found.
    ///   - moduleName: The name of the module to be updated. For EOCore settings, please use 'EOCore' which can be found the following files EOCoreBasicConfig.plist, EOCoreBasicConfig.properties or EOCoreAdvancedConfig.json and 'Connect' for Connect which can be found the following files ConnectBasicConfig.plist, ConnectBasicConfig.properties or ConnectAdvancedConfig.json.
    /// - Returns: The value of the configuration item key as a BOOL value.
    func getBooleanConfigItemForKey(theDefault: Bool, key: String, moduleName: String) throws -> Bool {
        return ConnectConfigStore.bool(forKey: key, default: theDefault)
    }
    
    /// Gets the module's configuration item from AdvancedConfig.json or BasicConfig.plist that matches the specified key as a NString value.
    /// - Parameters:
    ///   - key: Key to update value in configuration settings.
    ///   - theDefault: Default value if not found.
    ///   - moduleName: The name of the module to be updated. For EOCore settings, please use 'EOCore' which can be found the following files EOCoreBasicConfig.plist, EOCoreBasicConfig.properties or EOCoreAdvancedConfig.json and 'Connect' for Connect which can be found the following files ConnectBasicConfig.plist, ConnectBasicConfig.properties or ConnectAdvancedConfig.json.
    /// - Returns: The value of the configuration item key as a NString value.
    func getStringItemForKey(theDefault: String, key: String, moduleName: String) throws -> Variant_NullType_String? {
        let result = ConnectConfigStore.string(forKey: key, default: theDefault)
        return result.map { .second($0) }
    }
    
    /// Gets the module's configuration item from AdvancedConfig.json or BasicConfig.plist that matches the specified key as a NSNumber value.
    /// - Parameters:
    ///   - key: Key to update value in configuration settings.
    ///   - theDefault: Default value if not found.
    ///   - moduleName: The name of the module to be updated. For EOCore settings, please use 'EOCore' which can be found the following files EOCoreBasicConfig.plist, EOCoreBasicConfig.properties or EOCoreAdvancedConfig.json and 'Connect' for Connect which can be found the following files ConnectBasicConfig.plist, ConnectBasicConfig.properties or ConnectAdvancedConfig.json.
    /// - Returns: The value of the configuration item key as a NSNumber value.
    func getNumberItemForKey(theDefault: Double, key: String, moduleName: String) throws -> Double {
        return ConnectConfigStore.number(forKey: key, default: theDefault)
    }
    
    /// Log custom event.
    /// - Parameters:
    ///   - eventName:the name of the event to be logged this will appear in the posted json.
    ///   - values: additional key value pairs to be logged with the message.
    ///   - level: set a custom log level to the event.
    /// - Returns: Boolean value will return whether it was able to log the custom event.
    func logCustomEvent(eventName: String, values: Dictionary<String, Variant_Bool_String_Double>, level: Double) throws -> Bool {
        let logLevel = try getLogLevel(level: level)
        let result = ConnectCustomEvent().logEvent(eventName, values: convertToAnyDictionary(input: values), level: logLevel)
        return result
    }
    
    /// Log signal data.
    /// - Parameters:
    ///   - values: additional key value pairs to be logged with the signal message.
    ///   - level: set a custom log level to the event.
    /// - Returns: Boolean value will return whether it was able to log the signal message.
    func logSignal(values: Dictionary<String, Variant_Bool_String_Double>, level: Double) throws -> Bool {
        let logLevel = try getLogLevel(level: level)
        let result = ConnectCustomEvent().logSignal(convertToAnyDictionary(input: values), level: logLevel)
        return result
    }

    /// Logs a user identity so device activity can be associated with a known
    /// Connect contact. Wraps `ConnectSDK.shared.identity.log(...)`.
    ///
    /// `ConnectSDK.shared` is `@MainActor`-isolated, so — unlike the synchronous
    /// loggers above (which use the legacy non-actor `ConnectCustomEvent`) —
    /// this hops to the main actor and resolves with the SDK's real return
    /// value. The native API returns `false` (and emits no signal) when either
    /// identifier is blank, satisfying the bridge's blank-input contract without
    /// extra guards here. `url`, if supplied, rides inside `additionalParameters`
    /// (the iOS convention).
    /// - Parameters:
    ///   - identifierName: Identifier name, e.g. "Email".
    ///   - identifierValue: Identifier value, e.g. "user@example.com".
    ///   - signalType: Optional signal type; defaults to "loggedIn" when omitted.
    ///   - additionalParameters: Optional extra key/value pairs merged into the
    ///     signal. Defaults to `["registrationMethod": "email"]` only when `nil`
    ///     (omitted); an explicit map — including an empty one — is used as-is.
    /// - Returns: A promise resolving to `true` if the signal was dispatched,
    ///   `false` otherwise. Never rejects.
    func logIdentity(identifierName: String, identifierValue: String, signalType: String?, additionalParameters: [String: String]?) throws -> Promise<Bool> {
        return Promise.async { @MainActor in
            ConnectSDK.shared.identity.log(
                identifierName: identifierName,
                identifierValue: identifierValue,
                signalType: signalType ?? "loggedIn",
                additionalParameters: additionalParameters ?? ["registrationMethod": "email"]
            )
        }
    }

    /// Log exception.
    /// - Parameters:
    ///   - message: the message of the error/exception to be logged this will appear in the posted json.
    ///   - stackInfo: the stack trace to be logged with the message.
    ///   - unhandled: Whether exception is unhandled.
    /// - Returns: Boolean value will return whether it was able to log the exception event.
    func logExceptionEvent(message: String, stackInfo: String, unhandled: Bool) throws -> Bool {
        let exceptionDict: [String: Any] = [
            "type": "React Plugin",
            "message": message,
            "stacktrace": stackInfo
        ]
        let result = ConnectCustomEvent().logNSExceptionEvent(nil, dataDictionary: exceptionDict, isUnhandled: unhandled)
        return result
    }
    
    /// Requests that the framework logs a geographic location
    /// - Returns: Boolean value will return whether it was able to log the location event.
    func logLocation() throws -> Bool {
        let result = ConnectCustomEvent().logLocation(nil)
        return result
    }
    
    /// Requests that the framework logs the location information. This is not logged automatically to avoid making unnecessary location updates and to protect the privacy of your application's users by ensuring that location is reported only when the app has some other reason to request it. Your application must include the Core Location framework.
    /// - Parameters:
    ///   - latitude: The geographic latitude of the user.
    ///   - longitude: The geographic longitude of the user.
    ///   - level: lThe monitoring level of the event.
    /// - Returns: Boolean value will return whether it was able to log the location event.
    func logLocationWithLatitudeLongitude(latitude: Double, longitude: Double, level: Double) throws -> Bool {
        let logLevel = try getLogLevel(level: level)
        let result = ConnectCustomEvent().logLocationUpdate(withLatitude: latitude, longitude: longitude, level: logLevel)
        return result
    }
    
    /// Requests that the framework logs the click events on any UIControl or UIView. Click event is a normalized form of touch up inside event.
    /// - Parameters:
    ///   - target: Native node handle for a component from React Native.
    ///   - controlId: Control id a component from React Native.
    /// - Returns: Boolean value will return whether it was able to log the click event.
    func logClickEvent(target: Double, controlId: String) throws -> Bool {
      var result: Bool = false
      Task { @MainActor in
        let view: UIView? = nil
        let result = ConnectCustomEvent().logClick(view, controlId: controlId, data: nil)
        _ = result
      }
      return result
    }
    
    /// Requests that the framework logs the text change events.
    /// - Parameters:
    ///   - target: Native node handle for a component from React Native.
    ///   - controlId: Control id a component from React Native.
    ///   - text: The input string from txt control.
    /// - Returns: Boolean value will return whether it was able to log the text change event.
    func logTextChangeEvent(target: Double, controlId: String, text: Variant_NullType_String?) throws -> Bool {
        let view:UIView? = nil
        var data: [String: Any] = [:]

        if case .second(let str) = text {
            data["text"] = str
        }
        let result = ConnectCustomEvent().logTextChange(view, controlId: controlId, data: data)
        return result
    }
    
    /// Requests that the framework save the current  application page name.
    /// - Parameter logicalPageName: Page name or title e.g. "Login View Controller"; Must not be empty.
    /// - Returns: Boolean value will return whether it was able to log the screenview event.
    func setCurrentScreenName(logicalPageName: String) throws -> Bool {
        let result = ConnectApplicationHelper().setCurrentScreenName(logicalPageName)
        return result
    }
    
    /// Requests that the framework logs an application context for load.
    /// - Parameters:
    ///   - logicalPageName: Page name or title e.g. "Login View Controller"; Must not be empty.
    ///   - referrer: Page name or title that loads logicalPageName. Could be empty.
    /// - Returns: Boolean value will return whether it was able to log the screenview event.
    func logScreenViewContextLoad(logicalPageName: Variant_NullType_String?, referrer: Variant_NullType_String?) throws -> Bool {
        let pageName: String? = { if case .second(let s) = logicalPageName { return s }; return nil }()
        let ref: String? = { if case .second(let s) = referrer { return s }; return nil }()
        let cllasss = pageName == nil ? "ReactNative" : "ReactNative_\(pageName!)"
        let result = ConnectCustomEvent().logScreenViewContext(pageName, withClass: cllasss, applicationContext: ConnectScreenViewType.load, referrer: ref)
        return result
    }
    
    /// Requests that the framework logs an application context for unload.
    /// - Parameters:
    ///   - logicalPageName: Page name or title e.g. "Login View Controller"; Must not be empty.
    ///   - referrer: Page name or title that loads logicalPageName. Could be empty.
    /// - Returns: Boolean value will return whether it was able to log the screenview event.
    func logScreenViewContextUnload(logicalPageName: Variant_NullType_String?, referrer: Variant_NullType_String?) throws -> Bool {
        let pageName: String? = { if case .second(let s) = logicalPageName { return s }; return nil }()
        let ref: String? = { if case .second(let s) = referrer { return s }; return nil }()
        let cllasss = pageName == nil ? "ReactNative" : "ReactNative_\(pageName!)"
        let result = ConnectCustomEvent().logScreenViewContext(pageName, withClass: cllasss, applicationContext: ConnectScreenViewType.unload, referrer: ref)
        return result
    }
    
    /// Requests that the framework logs the layout of the screen
    /// - Parameters:
    ///   - name: Custom name to associate with the viewcontroller.
    ///   - delay: number of seconds to wait before logging the view.
    /// - Returns: Boolean value will return whether it was able to log the screen layout event.
    func logScreenLayout(name: String, delay: Double) throws -> Bool {
        // RCTLogInfo(@"logScreenLayout Name:%@ with Delay:%@", name, delay);
        let uvc: UIViewController! = nil
        var result: Bool = false
        if (delay <= 0) {
            result = ConnectCustomEvent().logScreenLayout(with: uvc, andName: name)
        } else {
            result = ConnectCustomEvent().logScreenLayout(with: uvc, andDelay: delay, andName: name)
        }
        
        return result
    }
    
    /// Logs a dialog show event with the specified dialog information.
    /// - Parameters:
    ///   - dialogId: Unique identifier for the dialog.
    ///   - dialogTitle: The title of the dialog.
    ///   - dialogType: The type of dialog (alert, custom, modal).
    /// - Returns: Boolean value will return whether it was able to log the dialog show event.
    func logDialogShowEvent(dialogId: String, dialogTitle: String, dialogType: String) throws -> Bool {
        let values: [String: Any] = [
            "dialogId": dialogId,
            "dialogTitle": dialogTitle,
            "dialogType": dialogType,
            "eventType": "dialog_show",
            "timestamp": String(Int(Date().timeIntervalSince1970 * 1000))
        ]
        
        return true
    }
    
    /// Logs a dialog dismiss event with the specified dialog information.
    /// - Parameters:
    ///   - dialogId: Unique identifier for the dialog.
    ///   - dismissReason: The reason for dismissing the dialog.
    /// - Returns: Boolean value will return whether it was able to log the dialog dismiss event.
    func logDialogDismissEvent(dialogId: String, dismissReason: String) throws -> Bool {
        let values: [String: Any] = [
            "dialogId": dialogId,
            "dismissReason": dismissReason,
            "eventType": "dialog_dismiss",
            "timestamp": String(Int(Date().timeIntervalSince1970 * 1000))
        ]
        
        return true
    }
    
    /// Logs a dialog button click event with the specified button information.
    /// - Parameters:
    ///   - dialogId: Unique identifier for the dialog.
    ///   - buttonText: The text of the clicked button.
    ///   - buttonIndex: The index of the clicked button.
    /// - Returns: Boolean value will return whether it was able to log the dialog button click event.
    func logDialogButtonClickEvent(dialogId: String, buttonText: String, buttonIndex: Double) throws -> Bool {
        let values: [String: Any] = [
            "dialogId": dialogId,
            "buttonText": buttonText,
            "buttonIndex": String(Int(buttonIndex)),
            "eventType": "dialog_button_click",
            "timestamp": String(Int(Date().timeIntervalSince1970 * 1000))
        ]
        
        return true
    }
    
    /// Logs a custom dialog event with the specified event information.
    /// - Parameters:
    ///   - dialogId: Unique identifier for the dialog.
    ///   - eventName: The name of the custom event.
    ///   - values: A map of values associated with the event.
    /// - Returns: Boolean value will return whether it was able to log the dialog custom event.
    func logDialogCustomEvent(dialogId: String, eventName: String, values: Dictionary<String, Variant_Bool_String_Double>) throws -> Bool {
        var eventValues: [String: Any] = [
            "dialogId": dialogId,
            "customEventName": eventName,
            "eventType": "dialog_custom_event",
            "timestamp": String(Int(Date().timeIntervalSince1970 * 1000))
        ]
        
        // Add the custom values
        let convertedValues = convertToAnyDictionary(input: values)
        for (key, value) in convertedValues {
            eventValues[key] = value
        }
        
        return true
    }
    
    // The bodies below moved to ConnectRNParsing (behaviour-preserving
    // extraction); these thin delegates keep the class API stable.
    func testModuleName(moduleName: String) throws -> String {
        return ConnectRNParsing.storeModuleName(for: moduleName)
    }

    func convertToAnyDictionary(input: [String: Variant_Bool_String_Double]) -> [String: Any] {
        return ConnectRNParsing.convertToAnyDictionary(input: input)
    }

    func convertVariantToAny(_ variant: Variant_Bool_String_Double) -> Any {
        return ConnectRNParsing.convertVariantToAny(variant)
    }
    
//    func convertKeyValueMapToDictionary(_ keyValueMap: KeyValueMap) -> [AnyHashable: Any] {
//        var dictionary: [AnyHashable: Any] = [:]
//        // Assuming KeyValueMap provides a method to access keys and values
//        for key in keyValueMap.keys {
//            if let value = keyValueMap[key] {
//                dictionary[key] = value
//            }
//        }
//        return dictionary
//    }
    
    func getLogLevelHelper(level: Double) throws -> kConnectMonitoringLevelType {
        return ConnectRNParsing.logLevel(from: level)
    }
    
    func getLogLevel(level: Double) throws -> kConnectMonitoringLevelType {
        do {
            // Try to get the log level
            return try getLogLevelHelper(level: level)
        } catch {
            // Return a default value if an error occurs
            return kConnectMonitoringLevelType.connectMonitoringLevelIgnore
        }
    }
    
    deinit {
        // Perform any necessary cleanup here
        print("HybridAcousticConnectRN deinitialized")
    }
}
