package chat.rocket.reactnative.voip

import android.content.Context
import android.media.AudioAttributes
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.MediaPlayer
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import chat.rocket.reactnative.BuildConfig
import chat.rocket.reactnative.R
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.Arguments
import com.facebook.react.modules.core.DeviceEventManagerModule
import chat.rocket.reactnative.networking.NativeVoipSpec
import java.lang.ref.WeakReference
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicReference

/**
 * Native module to expose VoIP call data to JavaScript.
 * Used to retrieve pending VoIP call data when the app opens from a VoIP notification.
 */
class VoipModule(reactContext: ReactApplicationContext) : NativeVoipSpec(reactContext) {

    companion object {
        private const val TAG = "RocketChat.VoipModule"
        private const val EVENT_VOIP_ACCEPT_SUCCEEDED = "VoipAcceptSucceeded"
        private const val EVENT_VOIP_ACCEPT_FAILED = "VoipAcceptFailed"
        private const val EVENT_VOIP_COMMUNICATION_DEVICE_CHANGED = "VoipCommunicationDeviceChanged"

        private var reactContextRef: WeakReference<ReactApplicationContext>? = null

        private val initialEventsData = AtomicReference<VoipPayload?>(null)

        /**
         * Sets the React context reference for event emission.
         */
        @JvmStatic
        fun setReactContext(context: ReactApplicationContext) {
            reactContextRef = WeakReference(context)
        }

        /**
         * Emits a VoIP call event to JavaScript when the app is running.
         */
        @JvmStatic
        fun emitInitialEventsEvent(voipPayload: VoipPayload) {
            try {
                reactContextRef?.get()?.let { context ->
                    if (context.hasActiveReactInstance()) {
                        context
                            .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
                            .emit(EVENT_VOIP_ACCEPT_SUCCEEDED, voipPayload.toWritableMap())
                    }
                }
            } catch (e: Exception) {
                Log.e(TAG, "Failed to emit VoIP call event", e)
            }
        }

        /**
         * Stores VoIP call data for JS to retrieve.
         * Also emits an event if the app is running.
         */
        @JvmStatic
        fun storeInitialEvents(voipPayload: VoipPayload) {
            initialEventsData.set(voipPayload)
            emitInitialEventsEvent(voipPayload)
        }

        /**
         * Stash native accept failure for cold start [getInitialEvents] and emit [EVENT_VOIP_ACCEPT_FAILED] when JS is running.
         */
        @JvmStatic
        fun storeAcceptFailureForJs(payload: VoipPayload) {
            val failed = payload.copy(voipAcceptFailed = true)
            initialEventsData.set(failed)
            emitVoipAcceptFailedEvent(failed)
        }

        private fun emitVoipAcceptFailedEvent(voipPayload: VoipPayload) {
            try {
                reactContextRef?.get()?.let { context ->
                    if (context.hasActiveReactInstance()) {
                        context
                            .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
                            .emit(EVENT_VOIP_ACCEPT_FAILED, voipPayload.toWritableMap())
                    }
                }
            } catch (e: Exception) {
                Log.e(TAG, "Failed to emit VoipAcceptFailed", e)
            }
        }

        @JvmStatic
        fun clearInitialEventsInternal() {
            try {
                initialEventsData.set(null)
                Log.d(TAG, "Cleared initial events")
            } catch (e: Exception) {
                Log.e(TAG, "Error clearing initial events", e)
            }
        }

        // Outstanding FGS-start promises keyed by callId. VoipCallService.onStartCommand resolves or
        // rejects these via notifyFgsStarted / notifyFgsFailed once startForeground actually returns,
        // so JS sees async failures (ForegroundServiceDidNotStartInTimeException,
        // ForegroundServiceTypeNotAllowedException) instead of a misleading resolve.
        private const val FGS_START_TIMEOUT_MS = 7_000L
        private val pendingFgsStarts = ConcurrentHashMap<String, FgsStartPending>()
        private val fgsHandler = Handler(Looper.getMainLooper())

        private class FgsStartPending(val promise: Promise, val timeoutRunnable: Runnable)

        @JvmStatic
        fun notifyFgsStarted(callId: String) {
            val pending = pendingFgsStarts.remove(callId) ?: return
            fgsHandler.removeCallbacks(pending.timeoutRunnable)
            pending.promise.resolve(null)
        }

        @JvmStatic
        fun notifyFgsFailed(callId: String, throwable: Throwable) {
            val pending = pendingFgsStarts.remove(callId) ?: return
            fgsHandler.removeCallbacks(pending.timeoutRunnable)
            val cause = throwable as? Exception ?: Exception(throwable)
            pending.promise.reject("E_VOIP_FGS_START", throwable.message, cause)
        }
    }

    private var communicationDeviceListener: AudioManager.OnCommunicationDeviceChangedListener? = null
    private var ringbackPlayer: MediaPlayer? = null

    init {
        // Store reference for event emission
        setReactContext(reactApplicationContext)
    }

    /**
     * Gets any initial events.
     * Returns null if no initial events.
     */
    override fun getInitialEvents(): WritableMap? {
        while (true) {
            val data = initialEventsData.get() ?: return null

            if (data.isExpired()) {
                if (BuildConfig.DEBUG) {
                    Log.d(TAG, "Discarding expired VoIP initial event: ${data.callId}")
                }
                if (initialEventsData.compareAndSet(data, null)) {
                    return null
                }
                continue
            }

            val result = data.toWritableMap()
            if (initialEventsData.compareAndSet(data, null)) {
                return result
            }
        }
    }

    /**
     * Clears any initial events.
     */
    override fun clearInitialEvents() {
        clearInitialEventsInternal()
    }

    // No-op on Android - FCM handles push notifications
    override fun getLastVoipToken(): String = ""

    /**
     * Registers for VoIP push token.
     * No-op on Android - uses FCM for push notifications.
     */
    override fun registerVoipToken() {
        // No-op on Android - FCM handles push notifications
        Log.d(TAG, "registerVoipToken called (no-op on Android)")
    }

    override fun stopNativeDDPClient() {
        Log.d(TAG, "stopNativeDDPClient called, stopping native DDP client")
        VoipNotification.stopDDPClient()
    }

    override fun stopVoipCallService() {
        try {
            VoipCallService.stopService(reactApplicationContext)
            Log.d(TAG, "stopVoipCallService: service stopped")
        } catch (e: Exception) {
            Log.e(TAG, "stopVoipCallService: failed to stop service", e)
        }
    }

    override fun startVoipCallService(callId: String, promise: Promise) {
        // Only valid for outgoing calls initiated from a visible activity. The incoming-accept
        // path starts the service from native (VoipNotification.handleAcceptAction) after Telecom
        // is active, and must NOT call this — it would race the JS bridge.
        //
        // The promise is held until VoipCallService.onStartCommand reports back via
        // notifyFgsStarted / notifyFgsFailed; resolving on the synchronous return of
        // startForegroundService would hide the async ForegroundServiceDidNotStartInTimeException
        // and ForegroundServiceTypeNotAllowedException paths that NATIVE-1178 also produces.
        pendingFgsStarts.remove(callId)?.let { stale ->
            fgsHandler.removeCallbacks(stale.timeoutRunnable)
            stale.promise.reject("E_VOIP_FGS_SUPERSEDED", "Another start was requested for $callId")
        }

        val timeoutRunnable = Runnable {
            val pending = pendingFgsStarts.remove(callId) ?: return@Runnable
            Log.e(TAG, "startVoipCallService: no foreground signal within ${FGS_START_TIMEOUT_MS}ms for callId=$callId")
            pending.promise.reject(
                "E_VOIP_FGS_TIMEOUT",
                "VoipCallService did not signal foreground within ${FGS_START_TIMEOUT_MS}ms"
            )
        }
        pendingFgsStarts[callId] = FgsStartPending(promise, timeoutRunnable)
        fgsHandler.postDelayed(timeoutRunnable, FGS_START_TIMEOUT_MS)

        try {
            VoipCallService.startService(reactApplicationContext, callId)
            if (BuildConfig.DEBUG) {
                Log.d(TAG, "startVoipCallService: dispatched for callId=$callId; awaiting foreground signal")
            }
        } catch (e: Exception) {
            // Synchronous failure from startForegroundService (e.g. ForegroundServiceStartNotAllowedException
            // on Android 12+). The service never received the start, so it can't signal back.
            Log.e(TAG, "startVoipCallService: failed to dispatch start for callId=$callId", e)
            pendingFgsStarts.remove(callId)?.let { fgsHandler.removeCallbacks(it.timeoutRunnable) }
            promise.reject("E_VOIP_FGS_START", e.message, e)
        }
    }

    override fun startAudioRouteSync(promise: Promise) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
            promise.resolve(null)
            return
        }
        if (communicationDeviceListener != null) {
            promise.resolve(null)
            return
        }
        val audioManager = reactApplicationContext.getSystemService(AudioManager::class.java)
        val listener = AudioManager.OnCommunicationDeviceChangedListener { device ->
            val isSpeaker = device?.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
            emitCommunicationDeviceChanged(isSpeaker)
        }
        try {
            audioManager.addOnCommunicationDeviceChangedListener(reactApplicationContext.mainExecutor, listener)
        } catch (e: Exception) {
            Log.e(TAG, "startAudioRouteSync: failed to register listener", e)
            promise.reject("E_AUDIO_ROUTE_SYNC_START", e.message, e)
            return
        }
        communicationDeviceListener = listener

        // addOnCommunicationDeviceChangedListener does not invoke the callback on registration,
        // so seed the JS-side state with the current device. Seed failure is non-fatal:
        // the listener is already active and will deliver subsequent route changes.
        try {
            val currentDevice = audioManager.communicationDevice
            if (currentDevice != null) {
                val isSpeaker = currentDevice.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
                emitCommunicationDeviceChanged(isSpeaker)
            }
        } catch (e: Exception) {
            Log.w(TAG, "startAudioRouteSync: failed to seed current device; listener remains active", e)
        }

        promise.resolve(null)
    }

    override fun stopAudioRouteSync(promise: Promise) {
        val listener = communicationDeviceListener
        if (listener == null) {
            promise.resolve(null)
            return
        }
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
            communicationDeviceListener = null
            promise.resolve(null)
            return
        }
        try {
            val audioManager = reactApplicationContext.getSystemService(AudioManager::class.java)
            audioManager.removeOnCommunicationDeviceChangedListener(listener)
        } catch (e: Exception) {
            // Leave the handle intact so the next stop attempt can retry removal
            // instead of leaking a still-registered native listener.
            Log.e(TAG, "stopAudioRouteSync: removal failed; keeping listener handle", e)
            promise.reject("E_AUDIO_ROUTE_SYNC_STOP", e.message, e)
            return
        }
        communicationDeviceListener = null
        promise.resolve(null)
    }

    private fun emitCommunicationDeviceChanged(isSpeaker: Boolean) {
        try {
            reactContextRef?.get()?.let { context ->
                if (context.hasActiveReactInstance()) {
                    val params = Arguments.createMap().apply { putBoolean("isSpeaker", isSpeaker) }
                    context
                        .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
                        .emit(EVENT_VOIP_COMMUNICATION_DEVICE_CHANGED, params)
                }
            }
        } catch (e: Exception) {
            Log.e(TAG, "Failed to emit VoipCommunicationDeviceChanged", e)
        }
    }

    /**
     * Routes call audio between the device speakerphone and the system-chosen output (earpiece/headset).
     *
     * The app's Telecom PhoneAccount is self-managed, so Connection.setAudioRoute() is a no-op —
     * the app owns audio routing and must drive AudioManager directly.
     *
     * API 31+: setCommunicationDevice(BUILTIN_SPEAKER) preempts WebRTC's AudioDeviceModule routing race.
     * Pre-31: legacy MODE_IN_COMMUNICATION + setSpeakerphoneOn fallback.
     */
    override fun setSpeakerOn(on: Boolean, promise: Promise) {
        try {
            val audioManager = reactApplicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                if (on) {
                    val speaker = audioManager.availableCommunicationDevices
                        .firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }
                    if (speaker == null) {
                        Log.w(TAG, "setSpeakerOn: no BUILTIN_SPEAKER in availableCommunicationDevices")
                        promise.resolve(false)
                        return
                    }
                    val ok = audioManager.setCommunicationDevice(speaker)
                    Log.d(TAG, "setSpeakerOn(true) via setCommunicationDevice -> $ok")
                    promise.resolve(ok)
                } else {
                    audioManager.clearCommunicationDevice()
                    Log.d(TAG, "setSpeakerOn(false) via clearCommunicationDevice")
                    promise.resolve(true)
                }
            } else {
                @Suppress("DEPRECATION")
                audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
                @Suppress("DEPRECATION")
                audioManager.isSpeakerphoneOn = on
                Log.d(TAG, "setSpeakerOn($on) via legacy setSpeakerphoneOn")
                promise.resolve(true)
            }
        } catch (e: Exception) {
            Log.e(TAG, "setSpeakerOn failed", e)
            promise.reject("E_AUDIO_ROUTE", e.message, e)
        }
    }

    /**
     * Plays the outgoing-call dialtone on the voice-communication audio path.
     *
     * expo-av's Audio.Sound on Android plays through STREAM_MUSIC, which routes to the
     * loudspeaker by default and is unaffected by setCommunicationDevice — so the dialtone
     * was loud regardless of isSpeakerOn, and toggleSpeaker had no audible effect during
     * outgoing ring. AudioAttributes USAGE_VOICE_COMMUNICATION ties this MediaPlayer to the
     * comm-device routing, so dialtone follows earpiece/speaker/BT like the call audio.
     */
    override fun startRingback(promise: Promise) {
        try {
            if (ringbackPlayer != null) {
                promise.resolve(null)
                return
            }
            val attrs = AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                .build()
            val player = MediaPlayer().apply {
                setAudioAttributes(attrs)
                isLooping = true
            }
            val afd = reactApplicationContext.resources.openRawResourceFd(R.raw.dialtone)
                ?: run {
                    player.release()
                    promise.reject("E_RINGBACK_START", "dialtone raw resource missing")
                    return
                }
            try {
                player.setDataSource(afd.fileDescriptor, afd.startOffset, afd.length)
            } finally {
                afd.close()
            }
            player.prepare()
            player.start()
            ringbackPlayer = player
            Log.d(TAG, "startRingback: dialtone playing on USAGE_VOICE_COMMUNICATION")
            promise.resolve(null)
        } catch (e: Exception) {
            Log.e(TAG, "startRingback failed", e)
            ringbackPlayer?.let {
                try { it.release() } catch (_: Exception) {}
            }
            ringbackPlayer = null
            promise.reject("E_RINGBACK_START", e.message, e)
        }
    }

    override fun stopRingback(promise: Promise) {
        val player = ringbackPlayer
        ringbackPlayer = null
        if (player == null) {
            promise.resolve(null)
            return
        }
        try {
            try {
                if (player.isPlaying) player.stop()
            } catch (_: IllegalStateException) {
                // player not in a state where isPlaying/stop is valid; release anyway
            }
            player.release()
            Log.d(TAG, "stopRingback: dialtone stopped")
            promise.resolve(null)
        } catch (e: Exception) {
            Log.e(TAG, "stopRingback failed", e)
            promise.reject("E_RINGBACK_STOP", e.message, e)
        }
    }

    /**
     * Required for NativeEventEmitter in TurboModules.
     * Called when JS starts listening to events.
     */
    override fun addListener(eventName: String) {
        // Keep track of listeners if needed
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "addListener: $eventName")
        }
    }

    /**
     * Required for NativeEventEmitter in TurboModules.
     * Called when JS stops listening to events.
     */
    override fun removeListeners(count: Double) {
        // Remove listeners if needed
        Log.d(TAG, "removeListeners: $count")
    }
}
