package com.humand.demo.modules.CallService

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.media.AudioAttributes
import android.media.AudioDeviceInfo
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.media.MediaPlayer
import android.net.wifi.WifiManager
import android.os.Build
import android.os.PowerManager
import androidx.core.content.ContextCompat
import com.facebook.react.bridge.ReactApplicationContext
import com.humand.demo.modules.Audio.AudioRouteCoordinator
import com.humand.demo.modules.Audio.AudioRouteMonitor
import com.humand.demo.modules.LivestreamService.LivestreamManager
import io.getstream.android.video.generated.models.CallRejectedEvent
import io.getstream.result.Result as StreamResult
import io.getstream.video.android.core.Call
import io.getstream.video.android.core.EventSubscription
import io.getstream.video.android.core.notifications.internal.service.CallServiceConfigRegistry
import kotlinx.coroutines.*

/**
 * Main call coordinator for 1-on-1 and group calls.
 */
class CallManager private constructor(
    appContext: Context,
    initialReactContext: ReactApplicationContext?,
) : StreamVideoManager(appContext) {

    companion object {
        private const val DEFAULT_SERVICE_CLASS =
            "io.getstream.video.android.core.notifications.internal.service.CallService"
        private const val AUDIO_SERVICE_CLASS =
            "io.getstream.video.android.core.notifications.internal.service.AudioCallService"
        private const val ANY_CALL_TYPE = "ALL_CALL_TYPES"

        @Volatile
        private var instance: CallManager? = null

        @Volatile
        var isPipEnabled: Boolean = false

        fun getInstance(context: Context): CallManager {
            val reactContext = context as? ReactApplicationContext
            val current = instance
            if (current != null) {
                reactContext?.let { current.setReactContext(it) }
                return current
            }

            return synchronized(this) {
                instance ?: CallManager(context.applicationContext, reactContext).also { instance = it }
            }
        }

        fun getInstanceOrNull(): CallManager? = instance
    }
    @Volatile
    private var reactContext: ReactApplicationContext? = initialReactContext

    private fun setReactContext(newContext: ReactApplicationContext) {
        reactContext = newContext
    }

    private var humandCall: HumandCall? = null
    private var isCallAccepted = false
    private var isLeavingCall = false
    private var isCaller: Boolean = false
    private var participantUserIds: List<Int> = emptyList()
    private var callTimeoutJob: Job? = null
    private var audioFocusRequest: AudioFocusRequest? = null
    private var ringbackPlayer: MediaPlayer? = null
    private var callEventSubscription: EventSubscription? = null
    private val audioManager: AudioManager =
        context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
    private val audioRouteMonitor = AudioRouteMonitor(context) { route, hasBluetooth ->
        emitAudioRouteUpdate(route, hasBluetooth)
    }
    private val videoSessionController = VideoSessionController(context) { reactContext }
    private val wifiManager: WifiManager? = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
    private var wifiLock: WifiManager.WifiLock? = null
    private val powerManager: PowerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
    private var powerWakeLock: PowerManager.WakeLock? = null
    private var lastAudioRoute: String? = null
    private var lastHasBluetoothDevice: Boolean = false
    private var localVideoActive = false
    private var remoteVideoActive = false
    private var callHasVideo = false
    private var hasEmittedRingingEvent = false
    private var pendingEndSource: String = CallConstants.ObserverEndSource.UNKNOWN
    private var pendingEndReason: String = CallConstants.ObserverEndReason.UNKNOWN
    private var pendingEndedByUserId: Any? = null

    /**
     * Always run the Stream foreground service that matches the user’s permission state.
     * If camera permission is denied we force the SDK to use AudioCallService so Android
     * does not try to start the camera foreground service.
     */
    override fun buildCallServiceConfigRegistry(): CallServiceConfigRegistry {
        val registry = CallServiceConfigRegistry()
        val serviceClass = resolveCallServiceClass()
        if (serviceClass == null) {
            callLog("buildCallServiceConfigRegistry: Falling back to SDK defaults (service class missing)")
            return registry
        }

        registry.update(CallConstants.CallType.DEFAULT) {
            setServiceClass(serviceClass)
        }
        registry.update(ANY_CALL_TYPE) {
            setServiceClass(serviceClass)
        }
        return registry
    }

    private fun resolveCallServiceClass(): Class<*>? {
        val hasCameraPermission =
            ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
        val preferredClassName = if (hasCameraPermission) {
            DEFAULT_SERVICE_CLASS
        } else {
            AUDIO_SERVICE_CLASS
        }

        return loadServiceClass(preferredClassName) ?: loadServiceClass(DEFAULT_SERVICE_CLASS)
    }

    private fun loadServiceClass(className: String): Class<*>? {
        return try {
            Class.forName(className)
        } catch (error: ClassNotFoundException) {
            callLog("Service class not found: $className (${error.message})")
            null
        }
    }

    private fun isGroupCall(): Boolean = humandCall?.isGroup ?: false

    private fun hasRemoteParticipants(): Boolean {
        return streamCall?.state?.participants?.value?.any {
            it.userId.value != localUserId
        } ?: false
    }

    private fun shouldNotifyBackendOfCallEnd(callId: String, requireJoinedState: Boolean = true): Boolean {
        if (isGroupCall()) {
            return !hasRemoteParticipants()
        }
        return if (requireJoinedState) hasJoinedCallState() else true
    }

    /**
     * Logging implementation.
     */
    override fun logMessage(message: String) {
        callLog(message)
    }

    /**
     * Emit state change.
     */
    override fun emitStateChanged(state: String) {
        RNCallManager.emitCallStateChanged(state)
    }

    /**
     * Emit participants change.
     */
    override fun emitParticipantsChanged(participants: List<NativeParticipant>) {
        updateRemoteVideoState(participants)
        RNCallManager.emitParticipantsChanged(participants)
    }

    /**
     * Emit media state change.
     */
    override fun emitMediaStateChanged(microphoneEnabled: Boolean, cameraEnabled: Boolean, speakerEnabled: Boolean) {
        RNCallManager.emitMediaStateChanged(microphoneEnabled, cameraEnabled, speakerEnabled)
    }

    override fun onSpeakerStateChanged(isEnabled: Boolean) {
        AudioRouteCoordinator.updateCallSpeakerPreference(isEnabled)
    }

    override fun onCameraStateChanged(isEnabled: Boolean) {
        updateLocalVideoState(isEnabled)
    }

    /**
     * Emit audio route change (mirrors iOS behavior).
     */
    override fun emitAudioRouteChanged(audioRoute: String) {
        emitAudioRouteUpdate(audioRoute)
    }

    private fun emitAudioRouteUpdate(audioRoute: String, hasBluetoothDevice: Boolean? = null) {
        val bluetoothAvailable = hasBluetoothDevice ?: audioRouteMonitor.hasBluetoothDevice()
        val routeChanged = lastAudioRoute != audioRoute
        val bluetoothChanged = lastHasBluetoothDevice != bluetoothAvailable

        if (!routeChanged && !bluetoothChanged) {
            return
        }

        lastAudioRoute = audioRoute
        lastHasBluetoothDevice = bluetoothAvailable
        val isEarpieceRoute = audioRoute.equals("EARPIECE", ignoreCase = true)
        videoSessionController.setEarpieceRouteActive(isEarpieceRoute)
        RNCallManager.emitAudioRouteChanged(audioRoute, bluetoothAvailable)
    }

    private fun updateLocalVideoState(isEnabled: Boolean) {
        if (localVideoActive == isEnabled) {
            return
        }
        localVideoActive = isEnabled
        updateVideoSessionState()
    }

    private fun updateRemoteVideoState(participants: List<NativeParticipant>) {
        val remoteHasVideo = participants.any { !it.isLocalParticipant && it.hasVideo }
        if (remoteVideoActive == remoteHasVideo) {
            return
        }
        remoteVideoActive = remoteHasVideo
        updateVideoSessionState()
    }

    private fun updateVideoSessionState() {
        val anyVideoActive = localVideoActive || remoteVideoActive
        videoSessionController.setVideoActive(anyVideoActive)
    }

    private fun acquireWifiLock() {
        if (wifiLock?.isHeld == true) {
            return
        }
        val manager = wifiManager ?: return
        try {
            val lock = manager.createWifiLock(
                WifiManager.WIFI_MODE_FULL_HIGH_PERF,
                "HumandCall:WifiLock"
            ).apply {
                setReferenceCounted(false)
                acquire()
            }
            wifiLock = lock
            logMessage("Wi-Fi lock acquired")
        } catch (e: Exception) {
            logMessage("Failed to acquire Wi-Fi lock: ${e.message}")
        }
    }

    private fun releaseWifiLock() {
        val lock = wifiLock ?: return
        try {
            if (lock.isHeld) {
                lock.release()
                logMessage("Wi-Fi lock released")
            }
        } catch (e: Exception) {
            logMessage("Failed to release Wi-Fi lock: ${e.message}")
        } finally {
            wifiLock = null
        }
    }

    private fun acquirePowerWakeLock() {
        if (powerWakeLock?.isHeld == true) {
            return
        }
        try {
            val lock = powerManager.newWakeLock(
                PowerManager.PARTIAL_WAKE_LOCK,
                "HumandCall:CpuLock"
            ).apply {
                setReferenceCounted(false)
                acquire()
            }
            powerWakeLock = lock
            logMessage("CPU wake lock acquired")
        } catch (e: Exception) {
            logMessage("Failed to acquire CPU wake lock: ${e.message}")
        }
    }

    private fun releasePowerWakeLock() {
        val lock = powerWakeLock ?: return
        try {
            if (lock.isHeld) {
                lock.release()
                logMessage("CPU wake lock released")
            }
        } catch (e: Exception) {
            logMessage("Failed to release CPU wake lock: ${e.message}")
        } finally {
            powerWakeLock = null
        }
    }

    private fun hasCameraPermission(): Boolean {
        return ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
            PackageManager.PERMISSION_GRANTED
    }

    /**
     * Handle call ended by remote participant.
     */
    override fun onCallEndedByRemote(endReason: String) {
        capturePendingEndContext(
            endSource = if (endReason == CallConstants.ObserverEndReason.TIMEOUT) {
                CallConstants.ObserverEndSource.SYSTEM
            } else {
                CallConstants.ObserverEndSource.REMOTE
            },
            endReason = endReason,
            endedByUserId = if (endReason == CallConstants.ObserverEndReason.TIMEOUT) {
                null
            } else {
                remoteEndedByUserId()
            }
        )
        logMessage("Call ended by remote (reason=$endReason)")
        stopRingback()
        managerScope.launch {
            emitObservedCallEnded()
            cleanup()
            RNCallManager.emitCallEnd()
            configureAudioForSpeakerAfterCall()
        }
    }

    /**
     * Handle permanent connection failure (e.g., "call not found" from server).
     * This stops reconnection attempts and cleans up resources.
     */
    override fun onConnectionFailed() {
        logMessage("Connection failed permanently, cleaning up")
        capturePendingEndContext(
            endSource = CallConstants.ObserverEndSource.SYSTEM,
            endReason = CallConstants.ObserverEndReason.CONNECTION_FAILED,
            endedByUserId = null
        )
        stopRingback()
        managerScope.launch {
            emitObservedCallEnded()
            cleanup()
            RNCallManager.emitCallEnd()
            configureAudioForSpeakerAfterCall()
        }
    }

    /**
     * Start playing ringback tone.
     */
    private fun startRingback() {
        try {
            val afd = context.resources.openRawResourceFd(com.humand.demo.R.raw.incallmanager_ringback)
            if (afd == null) {
                logMessage("startRingback: sound file not found")
                return
            }
            val player = MediaPlayer()
            player.setAudioAttributes(
                AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
                    .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                    .build()
            )
            player.setDataSource(afd.fileDescriptor, afd.startOffset, afd.length)
            afd.close()
            player.isLooping = true
            player.prepare()
            player.start()
            ringbackPlayer = player
            emitObservedCallRinging()
            logMessage("startRingback: playing")
        } catch (e: Exception) {
            logMessage("startRingback error: ${e.message}")
        }
    }

    /**
     * Stop playing ringback tone and cancel call timeouts.
     * Called by StreamVideoManager when remote participants join.
     */
    override fun stopRingback() {
        cancelCallTimeouts()
        callEventSubscription?.dispose()
        callEventSubscription = null
        ringbackPlayer?.let { player ->
            try {
                player.stop()
                player.release()
            } catch (e: Exception) {
                logMessage("stopRingback error: ${e.message}")
            }
            ringbackPlayer = null
            logMessage("stopRingback: stopped")
        }
    }

    /**
     * Subscribe to CallRejectedEvent for 1:1 outgoing calls.
     * Since we use call.join(create=true) instead of call.ring(), the Stream SDK's
     * RingingState doesn't track rejections. This listens for the callee's rejection
     * event directly and ends the call (stopping ringback).
     */
    private fun startCalleeRejectionObservation() {
        callEventSubscription?.dispose()
        val video = streamVideo ?: return
        val currentCallId = streamCall?.id ?: return

        callEventSubscription = video.subscribeFor(
            CallRejectedEvent::class.java
        ) { event ->
            val rejectedEvent = event as? CallRejectedEvent ?: return@subscribeFor
            if (rejectedEvent.call.id == currentCallId && rejectedEvent.user.id != localUserId) {
                logMessage("Callee rejected 1:1 call, ending")
                onCallEndedByRemote(CallConstants.ObserverEndReason.REJECTED)
            }
        }
    }

    override fun onRemoteParticipantsChanged(remoteCount: Int) {
        if (remoteCount >= 2 && !isGroupCall()) {
            setIsGroup(true)
            logMessage("Auto-promoted to group call (remote participants: $remoteCount)")
        }
    }

    override fun onRemoteParticipantsJoined(joinedParticipants: List<io.getstream.video.android.core.ParticipantState>) {
        val call = humandCall ?: return
        joinedParticipants.forEach { participant ->
            NativeCallsEventObserver.observeParticipantJoined(
                callId = call.id,
                providerCallId = call.providerCallId,
                cameraEnabled = callHasVideo,
                isGroup = call.isGroup,
                participant = normalizeParticipantIdentity(participant.userId.value)
            )
        }
    }

    /**
     * Start a call (outgoing or incoming).
     */
    fun startCall(
        id: String,
        providerCallId: String,
        cameraEnabled: Boolean,
        callerName: String,
        isGroup: Boolean,
        isCaller: Boolean,
        participantUserIds: List<Int> = emptyList()
    ) {
        if (streamVideo != null || streamCall != null) {
            logMessage("Cleaning up previous call state before starting new call")
            managerScope.launch {
                cleanup()
                startCallInternal(id, providerCallId, cameraEnabled, callerName, isGroup, isCaller, participantUserIds)
            }
        } else {
            startCallInternal(id, providerCallId, cameraEnabled, callerName, isGroup, isCaller, participantUserIds)
        }
    }

    private fun startCallInternal(
        id: String,
        providerCallId: String,
        cameraEnabled: Boolean,
        callerName: String,
        isGroup: Boolean,
        isCaller: Boolean,
        participantUserIds: List<Int> = emptyList()
    ) {
        logMessage("Starting call: $id, isGroup=$isGroup, isCaller=$isCaller")
        audioRouteMonitor.start()

        this.isCaller = isCaller
        this.participantUserIds = participantUserIds
        this.callHasVideo = cameraEnabled
        this.hasEmittedRingingEvent = false

        currentCallingState = CallConstants.CallingState.JOINING
        emitStateChanged(currentCallingState)

        humandCall = HumandCall(
            id = id,
            providerCallId = providerCallId,
            callerName = callerName,
            callerAvatar = null,
            cameraEnabled = cameraEnabled,
            isGroup = isGroup,
            initializationConfig = null
        )

        managerScope.launch {
            // End any active livestream before joining call (must await to prevent audio conflicts)
            LivestreamManager.getInstanceOrNull()?.let { livestream ->
                if (livestream.getCall() != null) {
                    logMessage("Ending active livestream before starting call")
                    livestream.leaveLivestreamAsync()
                }
            }

            if (isCaller) {
                joinStreamCallAsOutgoing(cameraEnabled)
            } else {
                joinStreamCallAsIncoming(cameraEnabled)
            }
        }
    }

    /**
     * Handle incoming call acceptance.
     * Called when user accepts an incoming call via notification.
     */
    fun handleIncomingCallAccepted(call: HumandCall) {
        logMessage("Handling incoming call accepted: ${call.id}")

        // If already in a call, end current call first then accept the new one
        if (currentCallingState != CallConstants.CallingState.IDLE) {
            logMessage("handleIncomingCallAccepted: Already in a call, will end current and accept new")
            managerScope.launch {
                endActiveCallForCallSwitch()
                handleIncomingCallAcceptedInternal(call)
            }
            return
        }

        handleIncomingCallAcceptedInternal(call)
    }

    private fun handleIncomingCallAcceptedInternal(call: HumandCall) {
        currentCallingState = CallConstants.CallingState.JOINING
        emitStateChanged(currentCallingState)
        audioRouteMonitor.start()

        humandCall = call
        isCallAccepted = true
        callHasVideo = call.cameraEnabled

        RNCallManager.emitAcceptedCall(
            mapOf(
                "id" to call.id,
                "providerCallId" to call.providerCallId,
                "isGroup" to call.isGroup,
                "initializationConfig" to call.initializationConfig
            )
        )

        managerScope.launch {
            // End any active livestream before joining call (must await to prevent audio conflicts)
            LivestreamManager.getInstanceOrNull()?.let { livestream ->
                if (livestream.getCall() != null) {
                    logMessage("handleIncomingCallAcceptedInternal: Ending active livestream before accepting call")
                    livestream.leaveLivestreamAsync()
                }
            }

            joinStreamCallAsIncoming(call.cameraEnabled)
        }
    }

    /**
     * End the active call when switching to a new call.
     * This properly cleans up the current call before joining another.
     */
    private suspend fun endActiveCallForCallSwitch() {
        logMessage("endActiveCallForCallSwitch: Ending current call to switch to new one")

        val prevCallId = humandCall?.id
        capturePendingEndContext(
            endSource = CallConstants.ObserverEndSource.LOCAL,
            endReason = CallConstants.ObserverEndReason.LOCAL_HANGUP,
            endedByUserId = localUserId?.let(::normalizeParticipantIdentity)
        )

        emitObservedCallEnded()
        RNCallManager.emitCallEnd()

        // Leave the Stream call and cleanup
        cleanupStreamClient()
        releaseAudioSession()
        releaseWifiLock()
        releasePowerWakeLock()

        // End/leave the call on backend
        if (prevCallId != null) {
            if (shouldNotifyBackendOfCallEnd(prevCallId)) {
                try {
                    CallApiService.end(prevCallId)
                } catch (e: Exception) {
                    logMessage("endActiveCallForCallSwitch: Failed to end call on backend: ${e.message}")
                }
            }
        }

        // Reset state
        humandCall = null
        isCallAccepted = false
        currentCallingState = CallConstants.CallingState.IDLE
        callHasVideo = false
        hasEmittedRingingEvent = false

        logMessage("endActiveCallForCallSwitch: Active call ended, ready for new call")
    }

    /**
     * Join helper with retry while preserving current soft-failure behavior.
     */
    private suspend fun joinCallWithRetry(call: Call, create: Boolean) {
        val joinResult = withRetry {
            when (val result = call.join(create = create)) {
                is StreamResult.Success -> result
                is StreamResult.Failure -> {
                    val message = result.value.message
                    if (message.contains("timed out") || message.contains("already been joined")) {
                        logMessage("Join had issue but continuing: $message")
                        result
                    } else {
                        throw Exception("Join failed: $message")
                    }
                }
            }
        }

        if (joinResult is StreamResult.Success) {
            logMessage("Joined call successfully")
        }
    }

    /**
     * Join stream call as outgoing caller.
     */
    private suspend fun joinStreamCallAsOutgoing(cameraEnabled: Boolean) {
        try {
            currentCallingState = CallConstants.CallingState.JOINING
            emitStateChanged(currentCallingState)

            val tokenResponse = CallApiService.getCallToken()
            if (tokenResponse == null) {
                logMessage("Failed to get call token")
                cleanup()
                return
            }

            val apiKey = CallApiService.streamApiKey ?: ""
            val video = ensureStreamClient(
                apiKey = apiKey,
                userId = tokenResponse.participantId,
                token = tokenResponse.token,
                tokenRefreshHandler = { CallApiService.getCallToken()?.token }
            )

            // Create and join the call
            val callId = humandCall?.providerCallId ?: return
            val call = video.call(CallConstants.CallType.DEFAULT, callId)
            streamCall = call

            // Get call info
            withRetry { call.get() }

            setupAudioSession()

            val humandCallId = humandCall?.id

            if (isGroupCall() && humandCallId != null) {
                val preJoinAccept = CallApiService.accept(humandCallId)
                logMessage("joinStreamCallAsOutgoing: Backend accept (pre-join, group) ${if (preJoinAccept) "succeeded" else "failed"}")
            }

            // Join the call
            joinCallWithRetry(call, create = true)

            // Set flag BEFORE starting observation to prevent race condition
            isConfiguringMedia = true
            startStateObservation(observeRingingState = !isGroupCall(), observeCallEnd = !isGroupCall())

            if (!isGroupCall() && humandCallId != null) {
                val acceptResponse = CallApiService.accept(humandCallId)

                if (!acceptResponse) {
                    logMessage("joinStreamCallAsOutgoing: Backend accept failed, call may have ended")
                    if (hasCallEnded()) {
                        logMessage("joinStreamCallAsOutgoing: Call confirmed ended, cleaning up")
                        isConfiguringMedia = false
                        capturePendingEndContext(
                            endSource = CallConstants.ObserverEndSource.REMOTE,
                            endReason = CallConstants.ObserverEndReason.REMOTE_HANGUP,
                            endedByUserId = remoteEndedByUserId()
                        )
                        emitObservedCallEnded()
                        cleanup()
                        RNCallManager.emitCallEnd()
                        configureAudioForSpeakerAfterCall()
                        return
                    }

                    logMessage("joinStreamCallAsOutgoing: Proceeding, sdk will notify if call ended")
                }
            }

            currentCallingState = CallConstants.CallingState.JOINED
            emitStateChanged(currentCallingState)
            emitObservedCallStarted()

            enableCallMedia(cameraEnabled)

            if (!isGroupCall()) {
                val remoteParticipantAlreadyJoined = streamCall?.state?.participants?.value?.any {
                    it.userId.value != localUserId
                } ?: false

                if (remoteParticipantAlreadyJoined) {
                    logMessage("joinStreamCallAsOutgoing: remote participant already joined, skipping ringback and timeout")
                } else {
                    startRingback()
                    startOutgoingCallTimeout()
                }
                startCalleeRejectionObservation()
            }
        } catch (e: Exception) {
            logMessage("Failed to join outgoing call: ${e.message}")
            capturePendingEndContext(
                endSource = CallConstants.ObserverEndSource.UNKNOWN,
                endReason = CallConstants.ObserverEndReason.UNKNOWN,
                endedByUserId = null
            )
            emitObservedCallEnded()
            cleanup()
            RNCallManager.emitCallEnd()
            configureAudioForSpeakerAfterCall()
        }
    }

    /**
     * Join stream call as incoming callee.
     */
    private suspend fun joinStreamCallAsIncoming(cameraEnabled: Boolean) {
        try {
            currentCallingState = CallConstants.CallingState.JOINING
            emitStateChanged(currentCallingState)

            val tokenResponse = CallApiService.getCallToken()
            if (tokenResponse == null) {
                logMessage("Failed to get call token")
                cleanup()
                return
            }

            val apiKey = CallApiService.streamApiKey ?: ""
            val video = ensureStreamClient(
                apiKey = apiKey,
                userId = tokenResponse.participantId,
                token = tokenResponse.token,
                tokenRefreshHandler = { CallApiService.getCallToken()?.token }
            )

            val callId = humandCall?.providerCallId ?: return
            val call = video.call(CallConstants.CallType.DEFAULT, callId)
            streamCall = call

            // For group calls, skip call.get() — it triggers RejectedByAll in the SDK
            // (from stale 1:1 rejection state) which cleans up the scope and prevents joining.
            // call.join() fetches the call state internally.
            if (!isGroupCall()) {
                withRetry { call.get() }
            }

            setupAudioSession()

            joinCallWithRetry(call, create = false)

            // Set flag BEFORE starting observation to prevent race condition
            isConfiguringMedia = true
            startStateObservation(observeRingingState = !isGroupCall(), observeCallEnd = !isGroupCall())

            val humandCallId = humandCall?.id
            val acceptResponse = if (humandCallId != null) CallApiService.accept(humandCallId) else false

            // If backend accept fails (e.g., 400 = call already ended), clean up immediately
            // This handles the race condition where caller hung up before we could accept.
            // For group calls, skip this check — accept failures are expected when joining
            // (vs answering a ring), and the SDK state observation will handle actual call endings.
            if (!acceptResponse && humandCallId != null && !isGroupCall()) {
                logMessage("joinStreamCallAsIncoming: Backend accept failed, call may have ended")

                if (hasCallEnded()) {
                    logMessage("joinStreamCallAsIncoming: Call confirmed ended, cleaning up")
                    isConfiguringMedia = false
                    capturePendingEndContext(
                        endSource = CallConstants.ObserverEndSource.REMOTE,
                        endReason = CallConstants.ObserverEndReason.REMOTE_HANGUP,
                        endedByUserId = remoteEndedByUserId()
                    )
                    emitObservedCallEnded()
                    cleanup()
                    RNCallManager.emitCallEnd()
                    configureAudioForSpeakerAfterCall()
                    return
                }

                logMessage("joinStreamCallAsIncoming: Proceeding, sdk will notify if call ended")
            }

            currentCallingState = CallConstants.CallingState.JOINED
            emitStateChanged(currentCallingState)
            emitObservedCallAccepted()

            enableCallMedia(cameraEnabled)
        } catch (e: Exception) {
            logMessage("Failed to join incoming call: ${e.message}")
            capturePendingEndContext(
                endSource = CallConstants.ObserverEndSource.UNKNOWN,
                endReason = CallConstants.ObserverEndReason.UNKNOWN,
                endedByUserId = null
            )
            emitObservedCallEnded()
            cleanup()
            RNCallManager.emitCallEnd()
            configureAudioForSpeakerAfterCall()
        }
    }

    /**
     * Enable camera and microphone after joining.
     */
    private suspend fun enableCallMedia(cameraEnabled: Boolean) {
        // Check if call has already ended (race condition protection)
        if (hasCallEnded()) {
            logMessage("enableCallMedia: Call already ended, skipping")
            isConfiguringMedia = false
            return
        }

        logMessage("enableCallMedia: starting, camera=$cameraEnabled")
        acquireWifiLock()
        if (hasCameraPermission()) {
            releasePowerWakeLock()
        } else {
            acquirePowerWakeLock()
        }

        try {
            // Use participantUserIds (all invited members) rather than live participants
            // which may not be fully populated immediately after join
            val memberCount = participantUserIds.size
            val shouldMuteMic = memberCount >= CallConstants.MIC_DISABLED_PARTICIPANT_THRESHOLD
            val micEnabled = !shouldMuteMic
            logMessage("enableCallMedia: members=$memberCount, micEnabled=$micEnabled")

            setMicrophoneEnabled(micEnabled, emitState = false)

            // Camera preference comes from initialization config
            setCameraEnabled(cameraEnabled, emitState = false)

            val isVideoCall = cameraEnabled || (streamCall?.camera?.isEnabled?.value == true)
            val hasHeadset = isBluetoothOrHeadsetConnected()
            val shouldEnableSpeaker = isVideoCall && !hasHeadset

            setSpeakerEnabled(shouldEnableSpeaker, emitState = false)
            if (hasHeadset) {
                logMessage("enableCallMedia: headset connected, speaker=$shouldEnableSpeaker")
            } else {
                logMessage("enableCallMedia: speaker set to $shouldEnableSpeaker (no headset)")
            }

            RNCallManager.emitMediaStateChanged(micEnabled, cameraEnabled, shouldEnableSpeaker)
            logMessage("enableCallMedia: emitted state micEnabled=$micEnabled, camEnabled=$cameraEnabled, speakerEnabled=$shouldEnableSpeaker")

            // Clear the configuring flag after setup is complete
            isConfiguringMedia = false

            AudioRouteCoordinator.setCallRoute(context, active = true, prefersSpeaker = shouldEnableSpeaker)

            // Emit audio activated event (Android equivalent)
            RNCallManager.emitAudioActivated()

        } catch (e: Exception) {
            isConfiguringMedia = false
            logMessage("Error enabling call media: ${e.message}")
        }
    }

    // MARK: - Call Timeouts

    private fun startOutgoingCallTimeout() {
        cancelCallTimeouts()
        logMessage("startOutgoingCallTimeout: ${CallConstants.Timeout.OUTGOING_CALL_MS}ms")
        callTimeoutJob = managerScope.launch {
            delay(CallConstants.Timeout.OUTGOING_CALL_MS)

            // Safety check: if a remote participant has already joined, the timeout
            // should have been cancelled by the participants observer. If it wasn't
            // (e.g. due to a Flow collection race), skip the timeout action.
            val remoteParticipantAlreadyJoined = streamCall?.state?.participants?.value?.any {
                it.userId.value != localUserId
            } ?: false
            if (remoteParticipantAlreadyJoined) {
                logMessage("startOutgoingCallTimeout: timed out but remote participant present, cancelling instead of leaving")
                cancelCallTimeouts()
                return@launch
            }

            logMessage("startOutgoingCallTimeout: timed out, calling miss API then leaving")
            val callId = humandCall?.id
            val userIds = participantUserIds
            if (callId != null && userIds.isNotEmpty()) {
                try {
                    CallApiService.miss(callId, userIds)
                } catch (e: Exception) {
                    logMessage("startOutgoingCallTimeout: miss API error: ${e.message}")
                }
            }
            leaveCall(endReason = CallConstants.ObserverEndReason.TIMEOUT)
        }
    }

    private fun cancelCallTimeouts() {
        callTimeoutJob?.let {
            it.cancel()
            callTimeoutJob = null
            logMessage("cancelCallTimeouts: timer cancelled")
        }
    }

    /**
     * Leave the current call.
     */
    fun leaveCall(endReason: String = CallConstants.ObserverEndReason.LOCAL_HANGUP) {
        if (isLeavingCall || currentCallingState == CallConstants.CallingState.IDLE) {
            logMessage("leaveCall: already leaving or idle, skipping")
            return
        }
        isLeavingCall = true
        capturePendingEndContext(
            endSource = when (endReason) {
                CallConstants.ObserverEndReason.LOCAL_HANGUP -> CallConstants.ObserverEndSource.LOCAL
                CallConstants.ObserverEndReason.TIMEOUT,
                CallConstants.ObserverEndReason.CONNECTION_FAILED -> CallConstants.ObserverEndSource.SYSTEM
                else -> CallConstants.ObserverEndSource.UNKNOWN
            },
            endReason = endReason,
            endedByUserId = if (endReason == CallConstants.ObserverEndReason.LOCAL_HANGUP) {
                localUserId?.let(::normalizeParticipantIdentity)
            } else {
                null
            }
        )
        logMessage("Leaving call")
        stopRingback()

        val callId = humandCall?.id

        managerScope.launch {
            try {
                if (callId != null && shouldNotifyBackendOfCallEnd(callId, requireJoinedState = false)) {
                    logMessage("Notifying API: call ended (last participant)")
                    val success = CallApiService.end(callId)
                    if (!success) {
                        logMessage("Call end API returned false for callId=$callId")
                    }
                }
            } catch (e: Exception) {
                logMessage("Error notifying backend about call end: ${e.message}")
            } finally {
                emitObservedCallEnded()
                cleanup()
                RNCallManager.emitCallEnd()
                configureAudioForSpeakerAfterCall()
            }
        }
    }

    /**
     * Cleanup all call state.
     */
    private suspend fun cleanup() {
        logMessage("Cleaning up call")

        cancelCallTimeouts()
        stopRingback()
        callEventSubscription?.dispose()
        callEventSubscription = null
        audioRouteMonitor.stop()
        videoSessionController.reset()
        lastAudioRoute = null
        lastHasBluetoothDevice = false
        localVideoActive = false
        remoteVideoActive = false
        cleanupStreamClient()
        releaseAudioSession()
        releaseWifiLock()
        releasePowerWakeLock()
        resetLockScreenFlags()

        humandCall = null
        isCallAccepted = false
        isLeavingCall = false
        isCaller = false
        participantUserIds = emptyList()
        callHasVideo = false
        hasEmittedRingingEvent = false
        pendingEndSource = CallConstants.ObserverEndSource.UNKNOWN
        pendingEndReason = CallConstants.ObserverEndReason.UNKNOWN
        pendingEndedByUserId = null

        currentCallingState = CallConstants.CallingState.IDLE
        emitStateChanged(currentCallingState)
    }

    /**
     * Update the isGroup flag for the current call.
     * Called when participants are added to a 1-to-1 call, making it a group call.
     */
    fun setIsGroup(isGroup: Boolean) {
        logMessage("setIsGroup: $isGroup")
        humandCall?.let { call ->
            humandCall = call.copy(isGroup = isGroup)
        }
    }

    /**
     * Set microphone enabled state (called from RN).
     */
    fun setMicrophoneEnabledFromRN(enabled: Boolean) {
        managerScope.launch {
            setMicrophoneEnabled(enabled)
        }
    }

    /**
     * Set camera enabled state (called from RN).
     */
    fun setCameraEnabledFromRN(enabled: Boolean) {
        managerScope.launch {
            setCameraEnabled(enabled)
        }
    }

    /**
     * Switch camera (called from RN).
     */
    fun switchCameraFromRN() {
        managerScope.launch {
            switchCamera()
        }
    }

    /**
     * Set speaker enabled (called from RN).
     */
    fun setSpeakerEnabledFromRN(enabled: Boolean) {
        logMessage("setSpeakerEnabledFromRN: $enabled")
        managerScope.launch {
            setSpeakerEnabled(enabled)
        }
    }

    /**
     * Check if a call is currently active.
     */
    fun hasActiveCall(): Boolean {
        return streamCall != null && hasJoinedCallState()
    }

    /**
     * Get the current active call ID.
     * Returns null if no call is active.
     */
    fun getCurrentCallId(): String? {
        return if (hasActiveCall()) humandCall?.id else null
    }

    fun getCurrentAudioRoute(): String {
        return audioRouteMonitor.getCurrentRoute()
    }

    private fun startedCallIdentity(
        callerName: String,
        isGroup: Boolean,
        participantUserIds: List<Int>
    ): Any? {
        if (isGroup) {
            return callerName.takeIf { it.isNotBlank() } ?: participantUserIds.takeIf { it.isNotEmpty() }
        }

        return participantUserIds.firstOrNull() ?: callerName.takeIf { it.isNotBlank() }
    }

    private fun emitObservedCallStarted() {
        val call = humandCall ?: return
        NativeCallsEventObserver.observeCallStarted(
            callId = call.id,
            providerCallId = call.providerCallId,
            cameraEnabled = callHasVideo,
            isGroup = call.isGroup,
            callee = startedCallIdentity(call.callerName, call.isGroup, participantUserIds),
            participantUserIds = participantUserIds
        )
    }

    private fun emitObservedCallAccepted() {
        val call = humandCall ?: return
        NativeCallsEventObserver.observeCallAccepted(
            callId = call.id,
            providerCallId = call.providerCallId,
            cameraEnabled = callHasVideo,
            isGroup = call.isGroup,
            caller = acceptedCallIdentity()
        )
    }

    private fun acceptedCallIdentity(): Any? {
        val remoteMemberIds = streamCall?.state?.members?.value
            ?.mapNotNull { member ->
                val userId = member.user.id
                if (userId == localUserId) {
                    return@mapNotNull null
                }

                normalizeParticipantIdentity(userId)
            }
            .orEmpty()

        if (humandCall?.isGroup == true) {
            return humandCall?.callerName?.takeIf { it.isNotBlank() }
                ?: remoteMemberIds.takeIf { it.isNotEmpty() }
        }

        return remoteMemberIds.firstOrNull()
            ?: humandCall?.callerName?.takeIf { it.isNotBlank() }
    }

    private fun normalizeParticipantIdentity(userId: String): Any {
        return userId.toIntOrNull() ?: userId
    }

    private fun emitObservedCallEnded() {
        val call = humandCall ?: return
        NativeCallsEventObserver.observeCallEnded(
            callId = call.id,
            providerCallId = call.providerCallId,
            cameraEnabled = callHasVideo,
            isGroup = call.isGroup,
            endSource = pendingEndSource,
            endReason = pendingEndReason,
            endedByUserId = pendingEndedByUserId
        )
    }

    private fun capturePendingEndContext(
        endSource: String,
        endReason: String,
        endedByUserId: Any?
    ) {
        pendingEndSource = endSource
        pendingEndReason = endReason
        pendingEndedByUserId = endedByUserId
    }

    private fun remoteEndedByUserId(): Any? {
        val remoteParticipantIds = streamCall?.state?.participants?.value
            ?.mapNotNull { participant ->
                val userId = participant.userId.value
                if (userId == localUserId) {
                    return@mapNotNull null
                }

                normalizeParticipantIdentity(userId)
            }
            .orEmpty()

        return if (humandCall?.isGroup == true) {
            remoteParticipantIds.takeIf { it.isNotEmpty() }
        } else {
            remoteParticipantIds.firstOrNull()
        }
    }

    private fun emitObservedCallRinging() {
        if (hasEmittedRingingEvent) {
            return
        }

        val call = humandCall ?: return
        hasEmittedRingingEvent = true
        NativeCallsEventObserver.observeCallRinging(
            callId = call.id,
            providerCallId = call.providerCallId,
            cameraEnabled = callHasVideo,
            isGroup = call.isGroup,
            direction = if (isCaller) "outgoing" else "incoming"
        )
    }

    /**
     * Check if Bluetooth or wired headset is connected.
     */
    private fun isBluetoothOrHeadsetConnected(): Boolean {
        val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
        for (device in devices) {
            when (device.type) {
                AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
                AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
                AudioDeviceInfo.TYPE_WIRED_HEADSET,
                AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
                AudioDeviceInfo.TYPE_USB_HEADSET -> {
                    logMessage("isBluetoothOrHeadsetConnected: Found headset - type ${device.type}")
                    return true
                }
            }
        }
        return false
    }

    private fun setupAudioSession() {
        logMessage("setupAudioSession")
        if (audioManager.mode != AudioManager.MODE_IN_COMMUNICATION) {
            audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
        }

        val audioAttributes = AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
            .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
            .build()

        audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
            .setAudioAttributes(audioAttributes)
            .setAcceptsDelayedFocusGain(true)
            .setOnAudioFocusChangeListener { focusChange ->
                logMessage("Audio focus changed: $focusChange")
                when (focusChange) {
                    AudioManager.AUDIOFOCUS_LOSS_TRANSIENT,
                    AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
                        // Transient loss - re-request focus after a short delay
                        managerScope.launch {
                            delay(500)
                            if (currentCallingState != CallConstants.CallingState.IDLE) {
                                audioFocusRequest?.let { request ->
                                    val result = audioManager.requestAudioFocus(request)
                                    logMessage("Re-requested audio focus after transient loss: $result")
                                }
                            }
                        }
                    }
                    AudioManager.AUDIOFOCUS_LOSS -> {
                        // Permanent loss - another app took over, may need to end call
                        logMessage("Audio focus permanently lost")
                    }
                    AudioManager.AUDIOFOCUS_GAIN -> {
                        logMessage("Audio focus regained")
                    }
                }
            }
            .build()

        val focusResult = audioManager.requestAudioFocus(audioFocusRequest!!)
        logMessage("Audio focus result: $focusResult")
    }

    private fun releaseAudioSession() {
        logMessage("releaseAudioSession")
        audioFocusRequest?.let {
            audioManager.abandonAudioFocusRequest(it)
        }
        audioFocusRequest = null
        audioManager.mode = AudioManager.MODE_NORMAL
    }

    private fun resetLockScreenFlags() {
        val activity = reactContext?.currentActivity ?: return
        activity.runOnUiThread {
            activity.setShowWhenLocked(false)
            activity.setTurnScreenOn(false)
            activity.window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
        }
    }

    /**
     * Configure audio for speaker output after call truly ends.
     * Called only when emitting end call event, not during call switching.
     */
    private fun configureAudioForSpeakerAfterCall() {
        logMessage("Configuring audio for speaker after call end")
        AudioRouteCoordinator.handleCallEnded(context)
    }

    /**
     * Cleanup call state on logout/account switch.
     * Disconnects the Stream client and clears any active call state.
     */
    fun cleanupOnLogout() {
        logMessage("cleanupOnLogout: Starting cleanup")

        cancelCallTimeouts()
        stopRingback()
        callEventSubscription?.dispose()
        callEventSubscription = null
        audioRouteMonitor.stop()
        videoSessionController.reset()
        lastAudioRoute = null
        lastHasBluetoothDevice = false
        localVideoActive = false
        remoteVideoActive = false

        managerScope.launch {
            // Leave the Stream call if active
            if (streamCall != null) {
                logMessage("cleanupOnLogout: Leaving Stream call")
                cleanupStreamClient()
            }

            releaseWifiLock()
            releasePowerWakeLock()

            // Disconnect the StreamVideo client
            disconnectStreamVideo()

            // Release audio session
            releaseAudioSession()
            resetLockScreenFlags()

            // Reset state
            humandCall = null
            isCallAccepted = false
            isLeavingCall = false
            isCaller = false
            participantUserIds = emptyList()
            currentCallingState = CallConstants.CallingState.IDLE
            emitStateChanged(currentCallingState)

            // Configure audio for speaker output
            configureAudioForSpeakerAfterCall()

            logMessage("cleanupOnLogout: Cleanup complete")
        }
    }

}
