import Foundation
import UIKit
import React
import FreshdeskSDK

// MARK: - Constants
private let kEventUnreadCountChanged = "unreadCountChanged"
private let kEventUserStateChanged = "userStateChanged"
private let kEventUserCreated = "userCreated"
private let kEventOnLinkPressed = "onLinkPressed"

// MARK: - Bridge Class
@objc public class FreshdeskNativeModuleBridge: NSObject {
    
    // MARK: - Properties
    private static var isInitialized = false
    private static var linkHandlerEnabled = false
    fileprivate weak static var eventEmitter: RCTEventEmitter?

    /// `Freshdesk.initialize(with:)` (the vendored native SDK) exposes no completion
    /// callback, `async` variant, notification, or published readiness property --
    /// confirmed by inspecting both the public and private `.swiftinterface` files in
    /// `ios/Frameworks/FreshdeskSDK.xcframework`. The SDK does its own async internal
    /// loading after `initialize()` returns, and calls made before that finishes are
    /// silently queued/dropped by the native SDK itself, logging
    /// "Tasks will be executed once the SDK is loaded" -- this is the root cause behind
    /// intermittent `openSupport()` failures and near-total `trackEvent()`/
    /// `setUserProperties()`/`setTicketProperties()` failure rates reported by
    /// customers calling immediately after `initialize()` resolves. See
    /// PLATFORM_DIFFERENCES.md.
    ///
    /// There is no real signal to poll, so -- mirroring Android's
    /// `FreshdeskInitCoordinator`, which works around the same class of problem for the
    /// same underlying reason (that native SDK also doesn't reliably confirm
    /// readiness) -- `initialize()` below resolves only after this settle delay, not
    /// the instant the native call returns. This is a heuristic, not a guarantee: there
    /// is no telemetry on the native SDK's real load time behind this number. If
    /// customers still hit the race at this value, or if it's needlessly slowing down
    /// app startup, tune it based on real-world reports.
    private static let nativeSdkSettleDelay: TimeInterval = 2.0

    private static func normalizeHost(_ host: String) -> String {
        let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines)
        if trimmed.isEmpty { return trimmed }
        let lower = trimmed.lowercased()
        if lower.hasPrefix("http://") || lower.hasPrefix("https://") {
            return trimmed
        }
        return "https://\(trimmed)"
    }
    
    // MARK: - Setup
    @objc public static func setEventEmitter(_ emitter: RCTEventEmitter) {
        self.eventEmitter = emitter
    }
    
    // MARK: - Initialize
    @objc public static func initialize(
        config: [String: Any],
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            guard let token = config["token"] as? String,
                  let host = config["host"] as? String,
                  let sdkId = config["sdkId"] as? String else {
                rejecter("FRESHDESK_INVALID_CONFIG", "Missing required config: token, host, or sdkId", nil)
                return
            }
            
            let jwtToken = config["jwt"] as? String
            let locale = config["locale"] as? String ?? "en"
            
            // `host` is already normalized by src/utils/normalizeHost.ts before it
            // crosses the bridge (see initialize() in src/index.ts) — this is the
            // JS-bridge entry point, so do not re-normalize here. `initializeSDK`
            // below is the separate native-direct entry point and still normalizes,
            // since it has no JS layer in front of it.
            let sdkConfig = FreshdeskSDKConfig(
                token: token,
                host: host,
                sdkId: sdkId,
                jwtToken: jwtToken,
                locale: locale,
                hostPlatform: .reactNative
            )
            
            Freshdesk.initialize(with: sdkConfig)

            // Setup delegates and observers
            setupNotificationObservers()
            setupJWTDelegate()

            // See nativeSdkSettleDelay's doc above: the native SDK gives no completion
            // signal, so wait out a heuristic settle delay before treating init as done
            // and resolving. Calling openSupport()/trackEvent()/setUserProperties()/etc.
            // immediately after resolve is exactly the customer-reported race this
            // guards against -- isInitialized flipping only here means every other
            // method's existing `guard isInitialized` check is naturally covered too,
            // with no per-method changes needed.
            DispatchQueue.main.asyncAfter(deadline: .now() + nativeSdkSettleDelay) {
                isInitialized = true
                resolver(nil)
            }
        }
    }

    /// Native initialization entry point (no React promise types) for the host
    /// AppDelegate, so push token registration works before the JS layer loads.
    ///
    /// This runs **synchronously** (on the main thread) rather than dispatching the
    /// init asynchronously. When the app is cold-launched in the background by a push,
    /// iOS invokes the background delivery handler
    /// (`application:didReceiveRemoteNotification:fetchCompletionHandler:`) right after
    /// `didFinishLaunchingWithOptions` returns. If init were dispatched async, that
    /// handler could run before the SDK finished initializing, so
    /// `handleRemoteNotification` would be a no-op and **no notification would be posted
    /// to the system tray** — the push would only ever surface in the foreground (where
    /// the SDK is already initialized). Initializing synchronously here guarantees the
    /// SDK is push-ready by the time `didFinishLaunchingWithOptions` returns.
    ///
    /// Deliberately does NOT apply `nativeSdkSettleDelay` (see the JS-bridge
    /// `initialize()` above): this path exists specifically so push delivery isn't
    /// blocked on the SDK's async load, so adding a wait here would defeat its purpose.
    /// A call to `openSupport()`/`trackEvent()`/etc. issued immediately after a
    /// native-triggered init can still race the SDK's internal loading -- that's a
    /// narrower, separate risk than the JS-driven flow this session's fix targets, and
    /// is not addressed here.
    @objc public static func initializeSDK(
        token: String,
        host: String,
        sdkId: String,
        jwt: String?,
        locale: String
    ) {
        let initBlock = {
            // Avoid re-initializing if the SDK is already set up in this process
            // (e.g. native init followed by a JS-driven initialize()).
            guard !isInitialized else { return }

            // Native-direct entry point (called from host app Swift/ObjC code, e.g.
            // AppDelegate) — no JS layer runs first, so this one DOES need to
            // normalize its own `host`. Keep normalizeHost() for this call only.
            let sdkConfig = FreshdeskSDKConfig(
                token: token,
                host: normalizeHost(host),
                sdkId: sdkId,
                jwtToken: jwt,
                locale: locale,
                hostPlatform: .reactNative
            )
            Freshdesk.initialize(with: sdkConfig)
            setupNotificationObservers()
            setupJWTDelegate()
            isInitialized = true
        }

        if Thread.isMainThread {
            initBlock()
        } else {
            DispatchQueue.main.sync(execute: initBlock)
        }
    }
    
    // MARK: - Open Support
    @objc public static func openSupport(
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            guard isInitialized else {
                rejecter("FRESHDESK_NOT_INITIALIZED", "SDK not initialized. Call initialize() first.", nil)
                return
            }
            
            guard let viewController = RCTPresentedViewController() else {
                rejecter("FRESHDESK_NO_VIEW_CONTROLLER", "Unable to get top view controller", nil)
                return
            }
            
            Freshdesk.openSupport(viewController)
            resolver(nil)
        }
    }
    
    // MARK: - Open Knowledge Base
    @objc public static func openKnowledgeBase(
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            guard isInitialized else {
                rejecter("FRESHDESK_NOT_INITIALIZED", "SDK not initialized. Call initialize() first.", nil)
                return
            }
            
            guard let viewController = RCTPresentedViewController() else {
                rejecter("FRESHDESK_NO_VIEW_CONTROLLER", "Unable to get top view controller", nil)
                return
            }
            
            Freshdesk.openKnowledgeBase(viewController)
            resolver(nil)
        }
    }
    
    // MARK: - Open Topic
    @objc public static func openTopic(
        topicName: String,
        topicId: String?,
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            guard isInitialized else {
                rejecter("FRESHDESK_NOT_INITIALIZED", "SDK not initialized. Call initialize() first.", nil)
                return
            }
            
            guard let viewController = RCTPresentedViewController() else {
                rejecter("FRESHDESK_NO_VIEW_CONTROLLER", "Unable to get top view controller", nil)
                return
            }
            
            // The native SDK expects an optional Int topic id; topic ids arrive from JS as strings.
            let parsedTopicId = topicId.flatMap { Int($0) }
            Freshdesk.openTopic(viewController, topicId: parsedTopicId, topicName: topicName)
            resolver(nil)
        }
    }
    
    // MARK: - Get Unread Count
    @objc public static func getUnreadCount(
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            guard isInitialized else {
                rejecter("FRESHDESK_NOT_INITIALIZED", "SDK not initialized. Call initialize() first.", nil)
                return
            }

            let count = Freshdesk.getUnreadCount()
            resolver(count)
        }
    }
    
    // MARK: - Track Event
    @objc public static func trackEvent(
        name: String,
        properties: [String: Any],
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            guard isInitialized else {
                rejecter("FRESHDESK_NOT_INITIALIZED", "SDK not initialized. Call initialize() first.", nil)
                return
            }

            // The native SDK expects string-valued event properties.
            let payload = properties.mapValues { String(describing: $0) }

            Freshdesk.trackUserEvents(name: name, payload: payload)
            resolver(nil)
        }
    }
    
    // MARK: - Set User Properties
    @objc public static func setUserProperties(
        properties: [String: Any],
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            guard isInitialized else {
                rejecter("FRESHDESK_NOT_INITIALIZED", "SDK not initialized. Call initialize() first.", nil)
                return
            }

            Freshdesk.setUserDetails(with: properties)
            resolver(nil)
        }
    }
    
    // MARK: - Set Ticket Properties
    @objc public static func setTicketProperties(
        properties: [String: Any],
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            guard isInitialized else {
                rejecter("FRESHDESK_NOT_INITIALIZED", "SDK not initialized. Call initialize() first.", nil)
                return
            }

            Freshdesk.setTicketProperties(with: properties)
            resolver(nil)
        }
    }
    
    // MARK: - Authenticate and Update
    @objc public static func authenticateAndUpdate(
        jwt: String,
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            guard isInitialized else {
                rejecter("FRESHDESK_NOT_INITIALIZED", "SDK not initialized. Call initialize() first.", nil)
                return
            }

            Freshdesk.authenticateAndUpdate(jwt: jwt)
            resolver(nil)
        }
    }
    
    // MARK: - Reset User
    @objc public static func resetUser(
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            guard isInitialized else {
                rejecter("FRESHDESK_NOT_INITIALIZED", "SDK not initialized. Call initialize() first.", nil)
                return
            }

            Freshdesk.resetUser()

            let result: [String: Any] = [
                "success": true,
                "message": "User reset successful",
                "error": ""
            ]
            resolver(result)
        }
    }
    
    // MARK: - Dismiss
    @objc public static func dismiss(
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            guard isInitialized else {
                rejecter("FRESHDESK_NOT_INITIALIZED", "SDK not initialized. Call initialize() first.", nil)
                return
            }

            Freshdesk.dismissFreshdeskSDKViews()
            resolver(nil)
        }
    }
    
    // MARK: - Set Link Handler
    @objc public static func setLinkHandlerEnabled(
        enabled: Bool,
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            linkHandlerEnabled = enabled

            if enabled {
                Freshdesk.setCustomLinkHandler { url in
                    let body: [String: String] = ["url": url.absoluteString]
                    eventEmitter?.sendEvent(withName: kEventOnLinkPressed, body: body)
                }
            } else {
                Freshdesk.setCustomLinkHandler { _ in
                    // No-op when disabled
                }
            }

            resolver(nil)
        }
    }

    // MARK: - Get SDK Version
    @objc public static func getSDKVersion(
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            guard isInitialized else {
                rejecter("FRESHDESK_NOT_INITIALIZED", "SDK not initialized. Call initialize() first.", nil)
                return
            }

            let version = Freshdesk.getSDKVersion()
            resolver(version)
        }
    }

    // MARK: - Get User
    @objc public static func getUser(
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            guard isInitialized else {
                rejecter("FRESHDESK_NOT_INITIALIZED", "SDK not initialized. Call initialize() first.", nil)
                return
            }

            Freshdesk.getUser(onFailure: { error in
                rejecter("FRESHDESK_USER_ERROR", error.localizedDescription, nil)
            }, onSuccess: { user in
                do {
                    let jsonData = try JSONSerialization.data(withJSONObject: user, options: [])
                    if let jsonString = String(data: jsonData, encoding: .utf8) {
                        resolver(jsonString)
                    } else {
                        rejecter("FRESHDESK_USER_PARSE_ERROR", "Failed to encode user data", nil)
                    }
                } catch {
                    rejecter("FRESHDESK_USER_PARSE_ERROR", "Failed to parse user data: \(error.localizedDescription)", nil)
                }
            })
        }
    }

    // MARK: - Diagnostics
    @objc public static func enableDebugLogs(
        enabled: Bool,
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            Freshdesk.enableDebugLogs(enabled)
            resolver(nil)
        }
    }

    @objc public static func runDiagnostics(
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            Freshdesk.runDiagnostics { report in
                resolver(serializeDiagnosticReport(report))
            }
        }
    }

    private static func serializeDiagnosticReport(_ report: FDDiagnosticReport) -> [String: Any] {
        var payload: [String: Any] = [
            "platform": "ios",
            "prettyPrinted": report.prettyPrinted(),
        ]

        let jsonValue = report.toJSON()
        if let jsonString = jsonValue as? String,
           let data = jsonString.data(using: .utf8),
           let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
            mergeDiagnosticPayload(&payload, with: object)
        } else if let object = jsonValue as? [String: Any] {
            mergeDiagnosticPayload(&payload, with: object)
        }

        if payload["overallStatus"] == nil {
            payload["overallStatus"] = deriveOverallStatus(from: payload["checks"])
        }

        #if RCT_NEW_ARCH_ENABLED
        payload["architecture"] = "new"
        payload["turboModule"] = true
        #else
        payload["architecture"] = "old"
        payload["turboModule"] = false
        #endif

        return payload
    }

    private static func mergeDiagnosticPayload(_ payload: inout [String: Any], with object: [String: Any]) {
        for (key, value) in object where key != "platform" && key != "prettyPrinted" {
            payload[key] = value
        }
    }

    private static func deriveOverallStatus(from checksValue: Any?) -> String {
        guard let checks = checksValue as? [[String: Any]] else {
            return "skipped"
        }

        if checks.contains(where: { ($0["status"] as? String) == "fail" }) {
            return "fail"
        }
        if checks.contains(where: { ($0["status"] as? String) == "warn" }) {
            return "warn"
        }
        if !checks.isEmpty && checks.allSatisfy({ ($0["status"] as? String) == "pass" }) {
            return "pass"
        }
        return "skipped"
    }

    // MARK: - Set Content Configuration
    @objc public static func setContentConfiguration(
        configJson: String,
        resolver: @escaping RCTPromiseResolveBlock,
        rejecter: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            guard isInitialized else {
                rejecter("FRESHDESK_NOT_INITIALIZED", "SDK not initialized. Call initialize() first.", nil)
                return
            }

            guard let data = configJson.data(using: .utf8) else {
                rejecter("FRESHDESK_INVALID_CONFIG", "Invalid configuration JSON", nil)
                return
            }

            do {
                let decoder = JSONDecoder()
                let config = try decoder.decode(ContentConfiguration.self, from: data)
                Freshdesk.setContentConfiguration(config)
                resolver(nil)
            } catch {
                rejecter("FRESHDESK_CONFIG_PARSE_ERROR", "Failed to parse content configuration: \(error.localizedDescription)", nil)
            }
        }
    }

    // MARK: - Push Notifications
    // These are invoked from the host app's AppDelegate (Objective-C) via the
    // generated `FreshdeskReactNative-Swift.h` interface.

    /// Forward the APNs device token to Freshdesk after registering for remote notifications.
    @objc public static func registerPushToken(_ deviceToken: Data) {
        Freshdesk.setPushRegistrationToken(deviceToken)
    }

    /// Returns true if the given notification payload originated from Freshdesk.
    @objc public static func isFreshdeskNotification(_ userInfo: [AnyHashable: Any]) -> Bool {
        return Freshdesk.isFreshdeskNotification(userInfo)
    }

    /// Hand a Freshdesk push payload to the SDK for handling/display.
    @objc public static func handleRemoteNotification(
        _ userInfo: [AnyHashable: Any],
        applicationState: UIApplication.State
    ) {
        Freshdesk.handleRemoteNotification(userInfo, appState: applicationState)
    }

    // MARK: - Notification Observers
    private static func setupNotificationObservers() {
        // Remove any existing observers first so repeated initialize() calls
        // (e.g. native init + JS init, or account switching via reinitialize)
        // do not stack duplicate observers and deliver events multiple times.
        NotificationCenter.default.removeObserver(self)

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(onUnreadCount(_:)),
            name: Notification.Name(FDEvents.unreadCount.rawValue),
            object: nil
        )
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(onUserCreated(_:)),
            name: Notification.Name(FDEvents.userCreated.rawValue),
            object: nil
        )
    }
    
    @objc private static func onUnreadCount(_ notification: Notification) {
        if let count = notification.object as? Int {
            let body: [String: Int] = ["count": count]
            eventEmitter?.sendEvent(withName: kEventUnreadCountChanged, body: body)
        }
    }
    
    @objc private static func onUserCreated(_ notification: Notification) {
        let body: [String: Any] = ["user": notification.object ?? [:]]
        eventEmitter?.sendEvent(withName: kEventUserCreated, body: body)
    }
    
    // MARK: - JWT Delegate
    private static func setupJWTDelegate() {
        Freshdesk.setJWTDelegate(FreshdeskJWTDelegateHandler.shared)
    }
}

// MARK: - JWT Delegate Handler
private class FreshdeskJWTDelegateHandler: NSObject, FreshdeskJWTDelegate {
    static let shared = FreshdeskJWTDelegateHandler()
    
    func userStateChanged(_ userState: UserState) {
        let stateString: String
        switch userState {
        case .authenticated:
            stateString = "authenticated"
        case .authExpired:
            stateString = "authExpired"
        case .notAuthenticated:
            stateString = "notAuthenticated"
        case .identifierUpdated:
            stateString = "identifierUpdated"
        case .jwtNotPresent:
            stateString = "jwtNotPresent"
        case .undefined:
            stateString = "undefined"
        @unknown default:
            stateString = "undefined"
        }
        
        let body: [String: String] = ["state": stateString]
        FreshdeskNativeModuleBridge.eventEmitter?.sendEvent(
            withName: kEventUserStateChanged,
            body: body
        )
    }
}
