package com.humand.demo.modules.CallService

import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.humand.demo.NativeRNCallManagerSpec

/**
 * React Native bridge module for CallService.
 */
class RNCallManager(private val reactContext: ReactApplicationContext) :
    NativeRNCallManagerSpec(reactContext) {

    companion object {
        const val NAME = "RNCallManager"
        private var instance: RNCallManager? = null
        private var isReady: Boolean = false
        private var savedData: Map<String, Any?>? = null
        private val pendingObserverEvents = mutableListOf<Pair<String, Map<String, Any?>>>()

        fun setInstance(manager: RNCallManager) {
            instance = manager
        }

        /**
         * Emit accepted call event to React Native.
         */
        fun emitAcceptedCall(data: Map<String, Any?>) {
            if (!isReady) {
                savedData = data
                return
            }
            instance?.sendEvent(CallConstants.RNEvents.CALL_ACCEPT, data.toWritableMap())
        }

        fun emitObservedEvent(eventName: String, data: Map<String, Any?>) {
            synchronized(pendingObserverEvents) {
                if (!isReady) {
                    pendingObserverEvents.add(eventName to data)
                    return
                }
            }
            instance?.sendEvent(eventName, data.toWritableMap())
        }

        /**
         * Emit call state changed event.
         */
        fun emitCallStateChanged(state: String) {
            val params = Arguments.createMap().apply {
                putString("state", state)
            }
            instance?.sendEvent(CallConstants.RNEvents.CALL_STATE_CHANGED, params)
        }

        /**
         * Emit participants changed event.
         */
        fun emitParticipantsChanged(participants: List<NativeParticipant>) {
            val participantsArray = Arguments.createArray()
            participants.forEach { participant ->
                participantsArray.pushMap(participant.toMap().toWritableMap())
            }

            val params = Arguments.createMap().apply {
                putArray("participants", participantsArray)
            }
            instance?.sendEvent(CallConstants.RNEvents.CALL_PARTICIPANTS_CHANGED, params)
        }

        /**
         * Emit media state changed event.
         */
        fun emitMediaStateChanged(microphoneEnabled: Boolean, cameraEnabled: Boolean, speakerEnabled: Boolean) {
            val params = Arguments.createMap().apply {
                putBoolean("microphoneEnabled", microphoneEnabled)
                putBoolean("cameraEnabled", cameraEnabled)
                putBoolean("speakerEnabled", speakerEnabled)
            }
            instance?.sendEvent(CallConstants.RNEvents.CALL_MEDIA_STATE_CHANGED, params)
        }

        /**
         * Emit call end event.
         */
        fun emitCallEnd() {
            val params = Arguments.createMap()
            instance?.sendEvent(CallConstants.RNEvents.CALL_END, params)
        }

        /**
         * Emit audio activated event.
         */
        fun emitAudioActivated() {
            val params = Arguments.createMap()
            instance?.sendEvent(CallConstants.RNEvents.CALL_AUDIO_ACTIVATED, params)
        }

        /**
         * Emit PiP mode changed event.
         */
        fun emitPipModeChanged(isInPipMode: Boolean) {
            val params = Arguments.createMap().apply {
                putBoolean("isInPipMode", isInPipMode)
            }
            instance?.sendEvent(CallConstants.RNEvents.CALL_PIP_MODE_CHANGED, params)
        }

        /**
         * Emit audio route changed event.
         */
        fun emitAudioRouteChanged(audioRoute: String, hasBluetoothDevice: Boolean = false) {
            callLog("emitAudioRouteChanged: audioRoute=$audioRoute, hasBluetoothDevice=$hasBluetoothDevice")
            val params = Arguments.createMap().apply {
                putString("audioRoute", audioRoute)
                putBoolean("hasBluetoothDevice", hasBluetoothDevice)
            }

            instance?.sendEvent(CallConstants.RNEvents.CALL_AUDIO_ROUTE_CHANGED, params)
        }
    }

    private val callManager: CallManager by lazy {
        CallManager.getInstance(reactContext)
    }

    init {
        setInstance(this)
        CallApiService.init(reactContext)
        NativeCallsEventObserver.addObserver("RNCallManager") { eventName, properties ->
            emitObservedEvent(eventName, properties)
        }
    }

    override fun getName(): String = NAME

    override fun getConstants(): Map<String, Any> = mapOf(
        "CALL_ACCEPT_EVENT" to CallConstants.RNEvents.CALL_ACCEPT,
        "CALL_MUTE_EVENT" to CallConstants.RNEvents.CALL_MUTE,
        "CALL_END_EVENT" to CallConstants.RNEvents.CALL_END,
        "CALL_AUDIO_ACTIVATED_EVENT" to CallConstants.RNEvents.CALL_AUDIO_ACTIVATED,
        "CALL_AUDIO_ROUTE_CHANGED_EVENT" to CallConstants.RNEvents.CALL_AUDIO_ROUTE_CHANGED,
        "CALL_STATE_CHANGED_EVENT" to CallConstants.RNEvents.CALL_STATE_CHANGED,
        "CALL_PARTICIPANTS_CHANGED_EVENT" to CallConstants.RNEvents.CALL_PARTICIPANTS_CHANGED,
        "CALL_MEDIA_STATE_CHANGED_EVENT" to CallConstants.RNEvents.CALL_MEDIA_STATE_CHANGED,
        "CALL_PIP_MODE_CHANGED_EVENT" to CallConstants.RNEvents.CALL_PIP_MODE_CHANGED,
        "CALLS_MOBILE_CALL_STARTED" to CallConstants.RNEvents.CALLS_MOBILE_CALL_STARTED,
        "CALLS_MOBILE_CALL_ACCEPTED" to CallConstants.RNEvents.CALLS_MOBILE_CALL_ACCEPTED,
        "CALLS_MOBILE_CALL_REJECTED" to CallConstants.RNEvents.CALLS_MOBILE_CALL_REJECTED,
        "CALLS_MOBILE_CALL_PARTICIPANT_JOINED" to CallConstants.RNEvents.CALLS_MOBILE_CALL_PARTICIPANT_JOINED,
        "CALLS_MOBILE_CALL_ENDED" to CallConstants.RNEvents.CALLS_MOBILE_CALL_ENDED,
        "CALLS_MOBILE_CALL_RINGING" to CallConstants.RNEvents.CALLS_MOBILE_CALL_RINGING
    )

    /**
     * Mark React Native listeners as ready.
     */
    @ReactMethod
    override fun setListenersReady() {
        callLog("setListenersReady called")
        isReady = true

        // Emit any queued events
        savedData?.let {
            emitAcceptedCall(it)
            savedData = null
        }

        val eventsToEmit = synchronized(pendingObserverEvents) {
            val snapshot = pendingObserverEvents.toList()
            pendingObserverEvents.clear()
            snapshot
        }
        eventsToEmit.forEach { (eventName, payload) ->
            emitObservedEvent(eventName, payload)
        }
    }

    /**
     * Set auth tokens from React Native.
     */
    @ReactMethod
    override fun setAuthTokens(accessToken: String, refreshToken: String) {
        callLog("setAuthTokens called")
        if (accessToken.isEmpty() || refreshToken.isEmpty()) {
            CallApiService.setAuthTokens(null)
            return
        }

        val tokensMap: Map<String, Any> = mapOf(
            "accessToken" to accessToken,
            "refreshToken" to refreshToken
        )
        CallApiService.setAuthTokens(tokensMap)
    }

    /**
     * Start a call (outgoing or incoming).
     */
    @ReactMethod
    override fun startCall(id: String, providerCallId: String, cameraEnabled: Boolean, callerName: String, isGroup: Boolean, isCaller: Boolean, participantUserIds: ReadableArray?) {
        callLog("startCall: id=$id, providerCallId=$providerCallId, camera=$cameraEnabled, isGroup=$isGroup, isCaller=$isCaller")
        val userIds = mutableListOf<Int>()
        participantUserIds?.let { arr ->
            for (i in 0 until arr.size()) {
                userIds.add(arr.getInt(i))
            }
        }
        callManager.startCall(id, providerCallId, cameraEnabled, callerName, isGroup, isCaller, userIds)
    }

    /**
     * Leave the current call.
     */
    @ReactMethod
    override fun leaveCall() {
        callLog("leaveCall called")
        callManager.leaveCall()
    }

    /**
     * Set microphone enabled state.
     */
    @ReactMethod
    override fun setMicrophoneEnabled(enabled: Boolean) {
        callLog("setMicrophoneEnabled: $enabled")
        callManager.setMicrophoneEnabledFromRN(enabled)
    }

    /**
     * Set camera enabled state.
     */
    @ReactMethod
    override fun setCameraEnabled(enabled: Boolean) {
        callLog("setCameraEnabled: $enabled")
        callManager.setCameraEnabledFromRN(enabled)
    }

    /**
     * Switch camera (front/back).
     */
    @ReactMethod
    override fun switchCamera() {
        callLog("switchCamera called")
        callManager.switchCameraFromRN()
    }

    /**
     * Set speaker enabled state during a call.
     */
    @ReactMethod
    override fun setSpeakerEnabled(enabled: Boolean) {
        callLog("setSpeakerEnabled: $enabled")
        callManager.setSpeakerEnabledFromRN(enabled)
    }

    /**
     * Get current audio route for initial UI sync.
     */
    @ReactMethod
    override fun getCurrentAudioRoute(promise: Promise) {
        promise.resolve(callManager.getCurrentAudioRoute())
    }

    /**
     * Get current VoIP token (iOS only - no-op on Android).
     */
    @ReactMethod
    override fun getVoipToken(promise: Promise) {
        promise.resolve(null)
    }

    /**
     * Set PiP enabled state from feature flag.
     */
    @ReactMethod
    override fun setPipEnabled(enabled: Boolean) {
        callLog("setPipEnabled: $enabled")
        CallManager.isPipEnabled = enabled
    }

    /**
     * Update the isGroup flag for the current call.
     * Called when participants are added to a 1-to-1 call, making it a group call.
     */
    @ReactMethod
    override fun setIsGroup(isGroup: Boolean) {
        callLog("setIsGroup: $isGroup")
        callManager.setIsGroup(isGroup)
    }

    /**
     * Set system microphone enabled (for CallKit UI on iOS).
     * On Android, this is a no-op as there's no system call UI.
     */
    @ReactMethod
    override fun setSystemMicrophoneEnabled(enabled: Boolean) {
        callLog("setSystemMicrophoneEnabled: $enabled (no-op on Android)")
        // No-op on Android - no equivalent system call UI
    }

    /**
     * Cleanup call state on logout/account switch.
     * Disconnects the Stream client and clears any active call state.
     */
    @ReactMethod
    override fun cleanup() {
        callLog("cleanup called")
        callManager.cleanupOnLogout()
    }

    /**
     * Get current call state.
     */
    @ReactMethod
    override fun getCallState(promise: Promise) {
        try {
            val state = callManager.getCurrentState()
            promise.resolve(state.toMap().toWritableMap())
        } catch (e: Exception) {
            promise.reject("GET_CALL_STATE_ERROR", e.message)
        }
    }

    /**
     * Required for RN Event Emitter.
     */
    @ReactMethod
    override fun addListener(eventName: String) {
        // Required for RN event emitter
    }

    /**
     * Required for RN Event Emitter.
     */
    @ReactMethod
    override fun removeListeners(count: Double) {
        // Required for RN event emitter
    }

    /**
     * Send event to React Native.
     */
    private fun sendEvent(eventName: String, params: WritableMap?) {
        if (reactContext.hasActiveReactInstance()) {
            reactContext
                .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
                .emit(eventName, params)
        }
    }
}

/**
 * Extension function to convert Map to WritableMap.
 */
fun Map<String, Any?>.toWritableMap(): WritableMap {
    val map = Arguments.createMap()
    forEach { (key, value) ->
        when (value) {
            null -> map.putNull(key)
            is String -> map.putString(key, value)
            is Boolean -> map.putBoolean(key, value)
            is Int -> map.putInt(key, value)
            is Double -> map.putDouble(key, value)
            is Float -> map.putDouble(key, value.toDouble())
            is Map<*, *> -> {
                @Suppress("UNCHECKED_CAST")
                map.putMap(key, (value as Map<String, Any?>).toWritableMap())
            }
            is List<*> -> {
                val array = Arguments.createArray()
                value.forEach { item ->
                    when (item) {
                        is String -> array.pushString(item)
                        is Boolean -> array.pushBoolean(item)
                        is Int -> array.pushInt(item)
                        is Double -> array.pushDouble(item)
                        is Map<*, *> -> {
                            @Suppress("UNCHECKED_CAST")
                            array.pushMap((item as Map<String, Any?>).toWritableMap())
                        }
                        else -> {}
                    }
                }
                map.putArray(key, array)
            }
            else -> {}
        }
    }
    return map
}
