// Copyright (C) 2026 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.

import Foundation
import Connect
import OSLog

// Same subsystem/category as the bridge's config logger so the extraction
// does not change where these lines surface in Console.app.
private let configLog = Logger(subsystem: "com.acoustic.AcousticConnectRN", category: "config")

/// Pure parsing / mapping / conversion logic extracted from
/// `HybridAcousticConnectRN` (behaviour-preserving extraction) so it can be
/// unit-tested without constructing the hybrid — the hybrid's `init`
/// dispatches `load()`, which enables the real SDK with the bundled AppKey.
internal enum ConnectRNParsing {

    internal enum IOSPushMode: Equatable {
        case automatic, manual
        var descriptor: String { self == .automatic ? "automatic" : "manual" }
    }

    /// Lenient `PushEnabled` parser — accepts a `Bool` directly, or a string
    /// like `"true"`/`"false"`/`"yes"`/`"no"`. Anything else falls back to
    /// `false` with a warning so a typo in the JSON doesn't silently enable
    /// push when the developer thought they'd turned it off.
    static func parsePushEnabled(_ raw: Any?) -> Bool {
        if let b = raw as? Bool { return b }
        if let n = raw as? NSNumber { return n.boolValue }
        if let s = raw as? String {
            switch s.lowercased() {
            case "true", "yes", "1": return true
            case "false", "no", "0", "": return false
            default:
                configLog.warning("PushEnabled string \"\(s, privacy: .public)\" not recognised. Falling back to false.")
                return false
            }
        }
        if raw != nil {
            configLog.warning("PushEnabled is set but is neither a Bool nor a recognised string. Falling back to false.")
        }
        return false
    }

    static func parseIOSPushMode(_ raw: String?) -> IOSPushMode {
        switch raw?.lowercased() {
        case "automatic", "auto", nil, "":
            return .automatic
        case "manual":
            return .manual
        default:
            configLog.error("iOSPushMode \"\(raw ?? "", privacy: .public)\" not recognised. Allowed values: \"automatic\", \"manual\". Falling back to \"automatic\".")
            return .automatic
        }
    }

    /// Resolves `PushEnabled` + `iOSPushMode` + `iOSAppGroupIdentifier` from the
    /// bundled config into a `ConnectPushConfig`. Validation errors are logged
    /// at `.error`; soft mismatches at `.warning`. The SDK always falls back to
    /// a safe default (`.off`) so an invalid config never crashes the app.
    static func resolvePushConfig(from connectData: [String: Any]) -> ConnectPushConfig {
        let pushEnabled = parsePushEnabled(connectData["PushEnabled"])
        let iOSPushModeRaw = connectData["iOSPushMode"] as? String
        let groupId = connectData["iOSAppGroupIdentifier"] as? String

        configLog.info("PushEnabled: \(pushEnabled)")

        // Inconsistency: iOSPushMode is set but PushEnabled is false. The
        // mode value would never take effect; warn the developer so they
        // know to fix one or the other.
        if !pushEnabled, let raw = iOSPushModeRaw, !raw.isEmpty {
            configLog.error("iOSPushMode \"\(raw, privacy: .public)\" is set but PushEnabled is false. Ignoring iOSPushMode; SDK will run with push disabled.")
            return .off
        }

        guard pushEnabled else {
            configLog.info("iOSPushMode: (not used, PushEnabled is false)")
            configLog.info("iOSAppGroupIdentifier: (not used, PushEnabled is false)")
            return .off
        }

        let mode = parseIOSPushMode(iOSPushModeRaw)
        configLog.info("iOSPushMode: \(mode.descriptor, privacy: .public)")
        if let groupId = groupId, !groupId.isEmpty {
            configLog.info("iOSAppGroupIdentifier: \(groupId, privacy: .public)")
        } else {
            configLog.warning("iOSAppGroupIdentifier is not set. Required if your app uses a Notification Service or Notification Content extension to render rich push payloads.")
        }

        switch mode {
        case .automatic:
            return ConnectPushConfig(mode: .automatic, appGroupIdentifier: groupId)
        case .manual:
            return ConnectPushConfig(mode: .manual, appGroupIdentifier: groupId)
        }
    }

    // Note: nsError(from: PushErrorInfo) stays in HybridAcousticConnectRN —
    // PushErrorInfo is a C++-backed nitro type, and this file is also
    // compiled into the UnitTests bundle, which builds without C++ interop.

    /// Maps the JS-facing module name to the config store's module name
    /// ("Connect" → "TLFCoreModule").
    static func storeModuleName(for moduleName: String) -> String {
        if moduleName.caseInsensitiveCompare("Connect") == .orderedSame {
            return "TLFCoreModule"
        }
        return moduleName
    }

    static func convertToAnyDictionary(input: [String: Variant_Bool_String_Double]) -> [String: Any] {
        var result: [String: Any] = [:]

        for (key, value) in input {
            switch value {
            case .first(let boolValue):
                result[key] = boolValue
            case .second(let stringValue):
                result[key] = stringValue
            case .third(let doubleValue):
                result[key] = doubleValue
            }
        }

        return result
    }

    static func convertVariantToAny(_ variant: Variant_Bool_String_Double) -> Any {
        switch variant {
        case .first(let boolValue):
            return boolValue
        case .second(let stringValue):
            return stringValue
        case .third(let doubleValue):
            return doubleValue
        }
    }

    static func logLevel(from level: Double) -> kConnectMonitoringLevelType {
        let intValue: Int = Int(level)
        if intValue == 0 {
            return kConnectMonitoringLevelType.connectMonitoringLevelIgnore
        } else if intValue == 1 {
            return kConnectMonitoringLevelType.connectMonitoringLevelCellularAndWiFi
        } else {
            return kConnectMonitoringLevelType.connectMonitoringLevelWiFi
        }
    }
}
