// PulseUpdates React Native Module
// Bridges PulseController to React Native

import Foundation
import React

@objc(PulseUpdates)
public class PulseUpdates: RCTEventEmitter {

    // MARK: - RCTEventEmitter

    public override func supportedEvents() -> [String]! {
        return ["PulseUpdatesEvent"]
    }

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

    // MARK: - Module Constants

    public override func constantsToExport() -> [AnyHashable: Any]! {
        return PulseUpdates.buildState()
    }

    // MARK: - Configuration

    @objc
    func configure(
        _ enabled: Bool,
        updateUrl: String,
        runtimeVersion: String,
        checkOnLaunch: String,
        launchWaitMs: Int,
        channel: String?,
        signingKeyId: String?,
        signingPublicKey: String?
    ) {
        let config = PulseUpdatesConfig(
            enabled: enabled,
            updateUrl: updateUrl,
            runtimeVersion: runtimeVersion.isEmpty ? Self.nativeRuntimeVersion() : runtimeVersion,
            checkOnLaunch: checkOnLaunch,
            launchWaitMs: launchWaitMs,
            channel: channel,
            signingKeyId: signingKeyId,
            signingPublicKey: signingPublicKey
        )

        PulseController.shared.configure(config)
        PulseController.shared.start()

        pulseLog("Configured enabled=\(enabled) url=\(updateUrl) runtime=\(config.runtimeVersion)")
    }

    // MARK: - Get State

    @objc
    func getStateAsync(_ resolve: @escaping RCTPromiseResolveBlock,
                       reject: @escaping RCTPromiseRejectBlock) {
        resolve(PulseUpdates.buildState())
    }

    private static func buildState() -> [String: Any] {
        let controller = PulseController.shared

        // Match expo-updates constants structure
        var state: [String: Any] = [
            "isEnabled": controller.config?.enabled ?? false,
            "isEmbeddedLaunch": controller.isEmbeddedLaunch,
            // expo-updates calls this isUsingEmbeddedAssets
            "isUsingEmbeddedAssets": controller.isEmbeddedLaunch,
            "runtimeVersion": controller.config?.runtimeVersion ?? nativeRuntimeVersion()
        ]

        if let updateId = controller.launchedUpdate?.updateId {
            state["updateId"] = updateId
        } else {
            state["updateId"] = NSNull()
        }

        state["channel"] = controller.config?.channel ?? NSNull()

        // Return manifest JSON string for metadata (build number, etc.)
        // expo-updates returns this as 'manifest' in native constants
        if let manifestJson = controller.launchedManifestJson {
            state["manifestString"] = manifestJson
        } else {
            state["manifestString"] = NSNull()
        }

        if let commitTime = controller.launchedUpdate?.commitTime {
            state["commitTime"] = commitTime.timeIntervalSince1970 * 1000
        } else {
            state["commitTime"] = NSNull()
        }

        // Local assets map for expo-asset compatibility
        // Key: asset key from manifest, Value: local file:// URL
        // expo-updates exposes this as localAssets
        let assets = controller.localAssets
        state["localAssets"] = assets.isEmpty ? [:] : assets

        return state
    }

    // MARK: - Check for Update

    @objc
    func checkForUpdateAsync(_ resolve: @escaping RCTPromiseResolveBlock,
                             reject: @escaping RCTPromiseRejectBlock) {
        PulseController.shared.checkForUpdate { result in
            switch result {
            case .success(let checkResult):
                var response: [String: Any] = [
                    "isAvailable": checkResult.isAvailable
                ]

                if let manifest = checkResult.manifest,
                   let data = try? JSONEncoder().encode(manifest),
                   let string = String(data: data, encoding: .utf8) {
                    response["manifestString"] = string
                }

                if checkResult.isRollback {
                    response["isRollback"] = true
                    response["failedReason"] = checkResult.failedReason ?? "Unknown"
                }

                resolve(response)

            case .failure(let error):
                reject("E_CHECK_UPDATE", error.localizedDescription, error)
            }
        }
    }

    // MARK: - Fetch Update

    @objc
    func fetchUpdateAsync(_ resolve: @escaping RCTPromiseResolveBlock,
                          reject: @escaping RCTPromiseRejectBlock) {
        PulseController.shared.fetchUpdate { result in
            switch result {
            case .success(let fetchResult):
                var response: [String: Any] = [
                    "isNew": fetchResult.isNew
                ]

                if let manifest = fetchResult.manifest,
                   let data = try? JSONEncoder().encode(manifest),
                   let string = String(data: data, encoding: .utf8) {
                    response["manifestString"] = string
                }

                resolve(response)

            case .failure(let error):
                reject("E_FETCH_UPDATE", error.localizedDescription, error)
            }
        }
    }

    // MARK: - Reload

    @objc
    func reloadAsync(_ resolve: @escaping RCTPromiseResolveBlock,
                     reject: @escaping RCTPromiseRejectBlock) {
        pulseLog("reloadAsync BEFORE launchAssetUrl=\(PulseController.shared.launchAssetUrl?.lastPathComponent ?? "nil")")

        // Re-select the best update, then trigger reload
        PulseController.shared.reload { success in
            let newBundleUrl = PulseController.shared.launchAssetUrl
            pulseLog("reloadAsync AFTER success=\(success) launchAssetUrl=\(newBundleUrl?.lastPathComponent ?? "nil")")

            DispatchQueue.main.async {
                // Set new bundle URL before triggering reload - like expo-updates does
                if let bundleUrl = newBundleUrl {
                    RCTReloadCommandSetBundleURL(bundleUrl)
                }
                RCTTriggerReloadCommandListeners("PulseUpdates reload")
                resolve(nil)
            }
        }
    }

    // MARK: - Health

    @objc
    func markAppReady(_ resolve: @escaping RCTPromiseResolveBlock,
                      reject: @escaping RCTPromiseRejectBlock) {
        PulseController.shared.markAppReady()
        resolve(nil)
    }

    @objc
    func reportLaunchFailure(_ reason: String,
                             resolve: @escaping RCTPromiseResolveBlock,
                             reject: @escaping RCTPromiseRejectBlock) {
        let error = NSError(domain: "PulseUpdates", code: -1, userInfo: [NSLocalizedDescriptionKey: reason])
        PulseController.shared.reportError(error)
        resolve(nil)
    }

    // MARK: - Get Bundle URL

    @objc
    public static func getBundleURL() -> URL? {
        // If controller hasn't started yet, start it now
        if !PulseController.shared.isStarted {
            PulseController.shared.initializeWithoutStarting()
        }

        let url = PulseController.shared.launchAssetURL()
        pulseLog("getBundleURL isStarted=\(PulseController.shared.isStarted) url=\(url?.lastPathComponent ?? "nil")")
        return url
    }

    // MARK: - Helpers

    private static func nativeRuntimeVersion() -> String {
        let info = Bundle.main.infoDictionary
        return (info?["CFBundleShortVersionString"] as? String)
            ?? (info?["CFBundleVersion"] as? String)
            ?? "unknown"
    }

    // MARK: - Event Emission

    private func sendEvent(type: PulseUpdatesEventType, body: [String: Any]? = nil) {
        var event: [String: Any] = ["type": type.rawValue]
        if let body = body {
            event["body"] = body
        }
        sendEvent(withName: "PulseUpdatesEvent", body: event)
    }
}
