package com.rosetta.messenger.network

import android.content.Context
import android.media.AudioManager
import android.util.Log
import com.rosetta.messenger.data.MessageRepository
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.IdentityHashMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.suspendCancellableCoroutine
import org.bouncycastle.math.ec.rfc7748.X25519
import org.json.JSONObject
import org.webrtc.AudioSource
import org.webrtc.AudioTrack
import org.webrtc.IceCandidate
import org.webrtc.MediaConstraints
import org.webrtc.PeerConnection
import org.webrtc.PeerConnectionFactory
import org.webrtc.RtpReceiver
import org.webrtc.RtpSender
import org.webrtc.RtpTransceiver
import org.webrtc.SdpObserver
import org.webrtc.SessionDescription
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

enum class CallPhase {
    IDLE,
    INCOMING,
    OUTGOING,
    CONNECTING,
    ACTIVE
}

enum class CallActionResult {
    STARTED,
    ALREADY_IN_CALL,
    NOT_AUTHENTICATED,
    ACCOUNT_NOT_BOUND,
    INVALID_TARGET,
    NOT_INCOMING
}

data class CallUiState(
    val phase: CallPhase = CallPhase.IDLE,
    val peerPublicKey: String = "",
    val peerTitle: String = "",
    val peerUsername: String = "",
    val statusText: String = "",
    val durationSec: Int = 0,
    val isMuted: Boolean = false,
    val isSpeakerOn: Boolean = false,
    val keyCast: String = ""
) {
    val isVisible: Boolean
        get() = phase != CallPhase.IDLE

    val displayName: String
        get() = when {
            peerTitle.isNotBlank() -> peerTitle
            peerUsername.isNotBlank() -> peerUsername
            peerPublicKey.isNotBlank() -> peerPublicKey.take(12)
            else -> "Unknown"
        }
}

private enum class CallRole {
    CALLER,
    CALLEE
}

/**
 * Android call signaling + WebRTC manager.
 * Mirrors desktop signaling flow on packets 0x1A/0x1B/0x1C.
 */
object CallManager {
    private const val TAG = "CallManager"
    private const val LOCAL_AUDIO_TRACK_ID = "rosetta_audio_track"
    private const val LOCAL_MEDIA_STREAM_ID = "rosetta_audio_stream"
    private const val BREADCRUMB_FILE_NAME = "e2ee_breadcrumb.txt"
    private const val DIAG_FILE_NAME = "e2ee_diag.txt"
    private const val NATIVE_CRASH_FILE_NAME = "native_crash.txt"
    private const val TAIL_LINES = 300
    private const val PROTOCOL_LOG_TAIL_LINES = 180
    private const val MAX_LOG_PREFIX = 180

    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    private val secureRandom = SecureRandom()

    private val _state = MutableStateFlow(CallUiState())
    val state: StateFlow<CallUiState> = _state.asStateFlow()

    @Volatile
    private var initialized = false
    private var appContext: Context? = null
    private var ownPublicKey: String = ""

    private var role: CallRole? = null
    private var roomId: String = ""
    private var offerSent = false
    private var remoteDescriptionSet = false
    private var callSessionId: String = ""
    private var callStartedAtMs: Long = 0L
    private var keyExchangeSent = false
    private var createRoomSent = false
    private var lastPeerSharedPublicHex: String = ""

    private var localPrivateKey: ByteArray? = null
    private var localPublicKey: ByteArray? = null

    private var durationJob: Job? = null
    private var protocolStateJob: Job? = null
    private var disconnectResetJob: Job? = null

    private var signalWaiter: ((Packet) -> Unit)? = null
    private var webRtcWaiter: ((Packet) -> Unit)? = null
    private var iceWaiter: ((Packet) -> Unit)? = null
    private val webRtcSignalMutex = Mutex()

    private var peerConnectionFactory: PeerConnectionFactory? = null
    private var peerConnection: PeerConnection? = null
    private var audioSource: AudioSource? = null
    private var localAudioTrack: AudioTrack? = null
    private val bufferedRemoteCandidates = mutableListOf<IceCandidate>()

    // E2EE (XChaCha20 — compatible with Desktop)
    private var sharedKeyBytes: ByteArray? = null
    private val senderEncryptors = IdentityHashMap<RtpSender, XChaCha20E2EE.Encryptor>()
    private val receiverDecryptors = IdentityHashMap<RtpReceiver, XChaCha20E2EE.Decryptor>()
    private var pendingAudioSenderForE2ee: RtpSender? = null
    private var lastRemoteOfferFingerprint: String = ""
    private var lastLocalOfferFingerprint: String = ""
    private var e2eeRebindJob: Job? = null

    private var iceServers: List<PeerConnection.IceServer> = emptyList()

    fun initialize(context: Context) {
        if (initialized) return
        initialized = true
        appContext = context.applicationContext
        CallSoundManager.initialize(context)
        XChaCha20E2EE.initWithContext(context)

        signalWaiter = ProtocolManager.waitCallSignal { packet ->
            scope.launch { handleSignalPacket(packet) }
        }
        webRtcWaiter = ProtocolManager.waitWebRtcSignal { packet ->
            scope.launch { handleWebRtcPacket(packet) }
        }
        iceWaiter = ProtocolManager.waitIceServers { packet ->
            handleIceServersPacket(packet)
        }

        protocolStateJob =
            scope.launch {
                ProtocolManager.state.collect { protocolState ->
                    when (protocolState) {
                        ProtocolState.AUTHENTICATED -> {
                            ProtocolManager.requestIceServers()
                        }
                        ProtocolState.DISCONNECTED -> {
                            resetSession(reason = "Disconnected", notifyPeer = false)
                        }
                        else -> Unit
                    }
                }
            }

        ProtocolManager.requestIceServers()
    }

    fun bindAccount(publicKey: String) {
        ownPublicKey = publicKey.trim()
    }

    fun startOutgoingCall(user: SearchUser): CallActionResult {
        val targetKey = user.publicKey.trim()
        if (targetKey.isBlank()) return CallActionResult.INVALID_TARGET
        if (!canStartNewCall()) return CallActionResult.ALREADY_IN_CALL
        if (ownPublicKey.isBlank()) return CallActionResult.ACCOUNT_NOT_BOUND
        if (!ProtocolManager.isAuthenticated()) return CallActionResult.NOT_AUTHENTICATED

        resetSession(reason = null, notifyPeer = false)
        beginCallSession("outgoing:${targetKey.take(8)}")
        role = CallRole.CALLER
        generateSessionKeys()
        setPeer(targetKey, user.title, user.username)
        updateState {
            it.copy(
                phase = CallPhase.OUTGOING,
                statusText = "Calling..."
            )
        }

        ProtocolManager.sendCallSignal(
            signalType = SignalType.CALL,
            src = ownPublicKey,
            dst = targetKey
        )
        breadcrumbState("startOutgoingCall")
        appContext?.let { CallSoundManager.play(it, CallSoundManager.CallSound.CALLING) }
        return CallActionResult.STARTED
    }

    fun acceptIncomingCall(): CallActionResult {
        val snapshot = _state.value
        if (snapshot.phase != CallPhase.INCOMING) return CallActionResult.NOT_INCOMING
        if (snapshot.peerPublicKey.isBlank()) return CallActionResult.INVALID_TARGET
        if (ownPublicKey.isBlank()) return CallActionResult.ACCOUNT_NOT_BOUND

        role = CallRole.CALLEE
        generateSessionKeys()
        val localPublic = localPublicKey ?: return CallActionResult.INVALID_TARGET

        ProtocolManager.sendCallSignal(
            signalType = SignalType.KEY_EXCHANGE,
            src = ownPublicKey,
            dst = snapshot.peerPublicKey,
            sharedPublic = localPublic.toHex()
        )
        keyExchangeSent = true

        updateState {
            it.copy(
                phase = CallPhase.CONNECTING,
                statusText = "Exchanging keys..."
            )
        }
        breadcrumbState("acceptIncomingCall")
        return CallActionResult.STARTED
    }

    fun declineIncomingCall() {
        val snapshot = _state.value
        if (snapshot.phase != CallPhase.INCOMING) return
        if (ownPublicKey.isNotBlank() && snapshot.peerPublicKey.isNotBlank()) {
            ProtocolManager.sendCallSignal(
                signalType = SignalType.END_CALL,
                src = ownPublicKey,
                dst = snapshot.peerPublicKey
            )
        }
        resetSession(reason = null, notifyPeer = false)
    }

    fun endCall() {
        val callerTrace =
            Throwable()
                .stackTrace
                .take(6)
                .joinToString(" | ") { "${it.className}.${it.methodName}:${it.lineNumber}" }
        breadcrumb("UI: endCall requested by $callerTrace")
        resetSession(reason = null, notifyPeer = true)
    }

    fun toggleMute() {
        updateState { current ->
            val nextMuted = !current.isMuted
            localAudioTrack?.setEnabled(!nextMuted)
            current.copy(isMuted = nextMuted)
        }
    }

    fun toggleSpeaker() {
        updateState { current ->
            val nextSpeaker = !current.isSpeakerOn
            setSpeakerphone(nextSpeaker)
            current.copy(isSpeakerOn = nextSpeaker)
        }
    }

    private fun canStartNewCall(): Boolean {
        return _state.value.phase == CallPhase.IDLE
    }

    private suspend fun handleSignalPacket(packet: PacketSignalPeer) {
        breadcrumb("SIG: ${packet.signalType} from=${packet.src.take(8)}… phase=${_state.value.phase}")

        when (packet.signalType) {
            SignalType.END_CALL_BECAUSE_BUSY -> {
                breadcrumb("SIG: peer busy → reset")
                resetSession(reason = "User is busy", notifyPeer = false)
                return
            }
            SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED -> {
                breadcrumb("SIG: peer disconnected → reset")
                resetSession(reason = "Peer disconnected", notifyPeer = false)
                return
            }
            SignalType.END_CALL -> {
                breadcrumb("SIG: END_CALL → reset")
                resetSession(reason = "Call ended", notifyPeer = false)
                return
            }
            else -> Unit
        }

        val currentPeer = _state.value.peerPublicKey
        val src = packet.src.trim()
        if (currentPeer.isNotBlank() && src.isNotBlank() && src != currentPeer && src != ownPublicKey) {
            breadcrumb("SIG: IGNORED (src mismatch: expected=${currentPeer.take(8)}… got=${src.take(8)}…)")
            return
        }

        when (packet.signalType) {
            SignalType.CALL -> {
                if (_state.value.phase != CallPhase.IDLE) {
                    breadcrumb("SIG: CALL but busy → sending END_CALL_BECAUSE_BUSY")
                    val callerKey = packet.src.trim()
                    if (callerKey.isNotBlank() && ownPublicKey.isNotBlank()) {
                        ProtocolManager.sendCallSignal(
                            signalType = SignalType.END_CALL_BECAUSE_BUSY,
                            src = ownPublicKey,
                            dst = callerKey
                        )
                    }
                    return
                }
                val incomingPeer = packet.src.trim()
                if (incomingPeer.isBlank()) return
                beginCallSession("incoming:${incomingPeer.take(8)}")
                breadcrumb("SIG: CALL from ${incomingPeer.take(8)}… → INCOMING")
                role = CallRole.CALLEE
                resetRtcObjects()
                setPeer(incomingPeer, "", "")
                updateState {
                    it.copy(
                        phase = CallPhase.INCOMING,
                        statusText = "Incoming call..."
                    )
                }
                appContext?.let { CallSoundManager.play(it, CallSoundManager.CallSound.RINGTONE) }
                resolvePeerIdentity(incomingPeer)
            }
            SignalType.KEY_EXCHANGE -> {
                breadcrumb("SIG: KEY_EXCHANGE → handleKeyExchange")
                handleKeyExchange(packet)
            }
            SignalType.CREATE_ROOM -> {
                val incomingRoomId = packet.roomId.trim()
                breadcrumb("SIG: CREATE_ROOM roomId=${incomingRoomId.take(16)}…")
                if (incomingRoomId.isBlank()) {
                    breadcrumb("SIG: CREATE_ROOM IGNORED — empty roomId!")
                    return
                }
                roomId = incomingRoomId
                updateState {
                    it.copy(
                        phase = CallPhase.CONNECTING,
                        statusText = "Connecting..."
                    )
                }
                ensurePeerConnectionAndOffer()
            }
            SignalType.ACTIVE_CALL -> Unit
            else -> breadcrumb("SIG: unhandled ${packet.signalType}")
        }
    }

    private suspend fun handleKeyExchange(packet: PacketSignalPeer) {
        val peerKey = packet.src.trim().ifBlank { _state.value.peerPublicKey }
        if (peerKey.isBlank()) {
            breadcrumb("KE: ABORT — peerKey blank")
            return
        }
        setPeer(peerKey, _state.value.peerTitle, _state.value.peerUsername)

        val peerPublicHex = packet.sharedPublic.trim()
        if (peerPublicHex.isBlank()) {
            breadcrumb("KE: ABORT — sharedPublic blank")
            return
        }
        val duplicatePeerKey = lastPeerSharedPublicHex.equals(peerPublicHex, ignoreCase = true)
        if (duplicatePeerKey && sharedKeyBytes != null) {
            breadcrumb("KE: duplicate peer key ignored")
            return
        }
        breadcrumb("KE: role=$role peerPub=${peerPublicHex.take(16)}…")
        lastPeerSharedPublicHex = peerPublicHex

        if (role == CallRole.CALLER) {
            if (localPrivateKey == null || localPublicKey == null) {
                breadcrumb("KE: CALLER — generating session keys (were null)")
                generateSessionKeys()
            }
            val sharedKey = computeSharedSecretHex(peerPublicHex)
            if (sharedKey == null) {
                breadcrumb("KE: CALLER — computeSharedSecret FAILED")
                return
            }
            setupE2EE(sharedKey)
            breadcrumb("KE: CALLER — E2EE ready, sending missing signaling packets")
            updateState { it.copy(keyCast = sharedKey, statusText = "Creating room...") }
            val localPublic = localPublicKey ?: return
            if (!keyExchangeSent) {
                ProtocolManager.sendCallSignal(
                    signalType = SignalType.KEY_EXCHANGE,
                    src = ownPublicKey,
                    dst = peerKey,
                    sharedPublic = localPublic.toHex()
                )
                keyExchangeSent = true
            }
            if (!createRoomSent) {
                ProtocolManager.sendCallSignal(
                    signalType = SignalType.CREATE_ROOM,
                    src = ownPublicKey,
                    dst = peerKey
                )
                createRoomSent = true
            }
            updateState { it.copy(phase = CallPhase.CONNECTING) }
            return
        }

        if (role == CallRole.CALLEE) {
            if (localPrivateKey == null || localPublicKey == null) {
                breadcrumb("KE: CALLEE — regenerating session keys (were null)")
                generateSessionKeys()
            }
            val sharedKey = computeSharedSecretHex(peerPublicHex)
            if (sharedKey == null) {
                breadcrumb("KE: CALLEE — computeSharedSecret FAILED")
                return
            }
            setupE2EE(sharedKey)
            breadcrumb("KE: CALLEE — E2EE ready, waiting for CREATE_ROOM")
            updateState { it.copy(keyCast = sharedKey, phase = CallPhase.CONNECTING) }
        }
    }

    private suspend fun handleWebRtcPacket(packet: PacketWebRTC) {
        webRtcSignalMutex.withLock {
            val phase = _state.value.phase
            breadcrumb("RTC: packet=${packet.signalType} payloadLen=${packet.sdpOrCandidate.length} phase=$phase")
            if (phase != CallPhase.CONNECTING && phase != CallPhase.ACTIVE) {
                breadcrumb("RTC: IGNORED ${packet.signalType} — phase=$phase")
                return@withLock
            }
            val pc = peerConnection
            if (pc == null) {
                breadcrumb("RTC: IGNORED ${packet.signalType} — peerConnection=null!")
                return@withLock
            }

            when (packet.signalType) {
                WebRTCSignalType.ANSWER -> {
                    val answer = parseSessionDescription(packet.sdpOrCandidate) ?: return@withLock
                    if (answer.type != SessionDescription.Type.ANSWER) {
                        breadcrumb("RTC: ANSWER packet with type=${answer.type} ignored")
                        return@withLock
                    }

                    val stateBefore = pc.signalingState()
                    breadcrumb("RTC: ANSWER received state=$stateBefore")
                    if (stateBefore == PeerConnection.SignalingState.STABLE && remoteDescriptionSet) {
                        breadcrumb("RTC: ANSWER duplicate ignored (already stable)")
                        return@withLock
                    }

                    try {
                        pc.setRemoteDescriptionAwait(answer)
                        remoteDescriptionSet = true
                        flushBufferedRemoteCandidates()
                        attachReceiverE2EEFromPeerConnection()
                        breadcrumb("RTC: ANSWER applied OK, state=${pc.signalingState()}")
                    } catch (e: Exception) {
                        breadcrumb("RTC: ANSWER FAILED — ${e.message}")
                        saveCrashReport("setRemoteDescription(answer) failed", e)
                    }
                }
                WebRTCSignalType.ICE_CANDIDATE -> {
                    val candidate = parseIceCandidate(packet.sdpOrCandidate) ?: return@withLock
                    if (!remoteDescriptionSet) {
                        breadcrumb("RTC: ICE buffered (remoteDesc not set yet)")
                        bufferedRemoteCandidates.add(candidate)
                        return@withLock
                    }
                    breadcrumb("RTC: ICE added: ${candidate.sdp.take(40)}…")
                    runCatching { pc.addIceCandidate(candidate) }
                }
                WebRTCSignalType.OFFER -> {
                    val remoteOffer = parseSessionDescription(packet.sdpOrCandidate) ?: return@withLock
                    if (remoteOffer.type != SessionDescription.Type.OFFER) {
                        breadcrumb("RTC: OFFER packet with type=${remoteOffer.type} ignored")
                        return@withLock
                    }
                    val offerFingerprint = remoteOffer.description.shortFingerprintHex(10)
                    val phaseNow = _state.value.phase
                    if (offerFingerprint == lastLocalOfferFingerprint) {
                        breadcrumb("RTC: OFFER loopback ignored fp=$offerFingerprint")
                        return@withLock
                    }
                    if (phaseNow == CallPhase.ACTIVE && offerFingerprint == lastRemoteOfferFingerprint) {
                        breadcrumb("RTC: OFFER duplicate in ACTIVE ignored fp=$offerFingerprint")
                        return@withLock
                    }

                    breadcrumb("RTC: OFFER received (offerSent=$offerSent state=${pc.signalingState()})")
                    try {
                        pc.setRemoteDescriptionAwait(remoteOffer)
                        remoteDescriptionSet = true
                        flushBufferedRemoteCandidates()
                        attachReceiverE2EEFromPeerConnection()

                        val stateAfterRemote = pc.signalingState()
                        if (stateAfterRemote != PeerConnection.SignalingState.HAVE_REMOTE_OFFER &&
                            stateAfterRemote != PeerConnection.SignalingState.HAVE_LOCAL_PRANSWER
                        ) {
                            breadcrumb("RTC: OFFER skip createAnswer, bad state=$stateAfterRemote")
                            return@withLock
                        }

                        val answer = pc.createAnswerAwait()
                        pc.setLocalDescriptionAwait(answer)
                        ProtocolManager.sendWebRtcSignal(
                            signalType = WebRTCSignalType.ANSWER,
                            sdpOrCandidate = serializeSessionDescription(answer)
                        )
                        attachReceiverE2EEFromPeerConnection()
                        lastRemoteOfferFingerprint = offerFingerprint
                        breadcrumb("RTC: OFFER handled → ANSWER sent")
                    } catch (e: Exception) {
                        breadcrumb("RTC: OFFER FAILED — ${e.message}")
                        saveCrashReport("handleOffer failed", e)
                    }
                }
            }
        }
    }

    private fun handleIceServersPacket(packet: PacketIceServers) {
        iceServers =
            packet.iceServers.mapNotNull { server ->
                val url = server.url.trim()
                val transport = server.transport.trim()
                if (url.isBlank() || transport.isBlank()) return@mapNotNull null
                PeerConnection.IceServer
                    .builder("turn:$url?transport=$transport")
                    .setUsername(server.username)
                    .setPassword(server.credential)
                    .createIceServer()
            }
    }

    private suspend fun ensurePeerConnectionAndOffer() {
        val peerKey = _state.value.peerPublicKey
        if (peerKey.isBlank() || roomId.isBlank()) {
            breadcrumb("PC: ensurePCAndOffer SKIP — peer=${peerKey.take(8)}… room=${roomId.take(8)}…")
            return
        }
        if (offerSent) {
            breadcrumb("PC: ensurePCAndOffer SKIP — offerSent=true")
            return
        }

        breadcrumb("PC: ensurePCAndOffer START role=$role room=${roomId.take(8)}…")
        ensurePeerFactory()
        val factory = peerConnectionFactory
        if (factory == null) {
            breadcrumb("PC: ABORT — factory=null")
            return
        }
        val pc = peerConnection ?: createPeerConnection(factory)
        if (pc == null) {
            breadcrumb("PC: ABORT — createPeerConnection returned null")
            return
        }

        if (audioSource == null) {
            audioSource = factory.createAudioSource(MediaConstraints())
        }
        var senderToAttach: RtpSender? = null
        if (localAudioTrack == null) {
            localAudioTrack = factory.createAudioTrack(LOCAL_AUDIO_TRACK_ID, audioSource)
            localAudioTrack?.setEnabled(!_state.value.isMuted)
            val txInit =
                RtpTransceiver.RtpTransceiverInit(
                    RtpTransceiver.RtpTransceiverDirection.SEND_RECV,
                    listOf(LOCAL_MEDIA_STREAM_ID)
                )
            val transceiver = pc.addTransceiver(localAudioTrack, txInit)
            senderToAttach = transceiver?.sender
            pendingAudioSenderForE2ee = senderToAttach
            breadcrumb("PC: audio transceiver added (E2EE attach deferred)")
        } else {
            senderToAttach =
                runCatching {
                    pc.senders.firstOrNull { sender ->
                        sender.track()?.kind() == "audio"
                    }
                }.getOrNull()
            if (senderToAttach != null) {
                pendingAudioSenderForE2ee = senderToAttach
            }
        }
        attachSenderE2EE(pendingAudioSenderForE2ee ?: senderToAttach)

        try {
            val offer = pc.createOfferAwait()
            pc.setLocalDescriptionAwait(offer)
            lastLocalOfferFingerprint = offer.description.shortFingerprintHex(10)
            breadcrumb("RTC: local OFFER fp=$lastLocalOfferFingerprint")
            ProtocolManager.sendWebRtcSignal(
                signalType = WebRTCSignalType.OFFER,
                sdpOrCandidate = serializeSessionDescription(offer)
            )
            offerSent = true
            breadcrumb("PC: OFFER sent OK")
        } catch (e: Exception) {
            breadcrumb("PC: OFFER FAILED — ${e.message}")
            saveCrashReport("createOffer failed", e)
        }
    }

    private fun createPeerConnection(factory: PeerConnectionFactory): PeerConnection? {
        breadcrumb("PC: createPeerConnection iceServers=${iceServers.size}")
        val rtcIceServers =
            if (iceServers.isNotEmpty()) {
                iceServers
            } else {
                breadcrumb("PC: no TURN servers — using Google STUN fallback")
                listOf(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer())
            }

        val configuration = PeerConnection.RTCConfiguration(rtcIceServers).apply {
            sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
        }

        val observer =
            object : PeerConnection.Observer {
                override fun onSignalingChange(newState: PeerConnection.SignalingState?) {
                    breadcrumb("PC: signalingState=$newState")
                }
                override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) {
                    breadcrumb("PC: iceConnState=$newState")
                }
                override fun onIceConnectionReceivingChange(receiving: Boolean) = Unit
                override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState?) {
                    breadcrumb("PC: iceGathering=$newState")
                }
                override fun onIceCandidate(candidate: IceCandidate?) {
                    if (candidate == null) return
                    breadcrumb("PC: local ICE: ${candidate.sdp.take(30)}…")
                    ProtocolManager.sendWebRtcSignal(
                        signalType = WebRTCSignalType.ICE_CANDIDATE,
                        sdpOrCandidate = serializeIceCandidate(candidate)
                    )
                }
                override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>?) = Unit
                override fun onAddStream(stream: org.webrtc.MediaStream?) = Unit
                override fun onRemoveStream(stream: org.webrtc.MediaStream?) = Unit
                override fun onDataChannel(dataChannel: org.webrtc.DataChannel?) = Unit
                override fun onRenegotiationNeeded() = Unit
                override fun onAddTrack(receiver: RtpReceiver?, mediaStreams: Array<out org.webrtc.MediaStream>?) = Unit
                override fun onTrack(transceiver: RtpTransceiver?) {
                    breadcrumb("PC: onTrack → attachReceiverE2EE")
                    attachReceiverE2EE(transceiver?.receiver)
                    attachReceiverE2EEFromPeerConnection()
                }
                override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) {
                    breadcrumb("PC: connState=$newState")
                    breadcrumbState("onConnectionChange:$newState")
                    when (newState) {
                        PeerConnection.PeerConnectionState.CONNECTED -> {
                            disconnectResetJob?.cancel()
                            disconnectResetJob = null
                            onCallConnected()
                        }
                        PeerConnection.PeerConnectionState.FAILED,
                        PeerConnection.PeerConnectionState.CLOSED,
                        PeerConnection.PeerConnectionState.DISCONNECTED -> {
                            // Desktop flow is signal-driven. Keep call alive across short
                            // transport glitches and only fallback-reset after timeout.
                            disconnectResetJob?.cancel()
                            disconnectResetJob =
                                scope.launch {
                                    val timeoutMs =
                                        when (newState) {
                                            PeerConnection.PeerConnectionState.DISCONNECTED -> 15_000L
                                            PeerConnection.PeerConnectionState.FAILED -> 12_000L
                                            PeerConnection.PeerConnectionState.CLOSED -> 12_000L
                                            else -> 12_000L
                                        }
                                    delay(timeoutMs)
                                    val pcState = peerConnection?.connectionState()
                                    if (pcState == PeerConnection.PeerConnectionState.DISCONNECTED ||
                                        pcState == PeerConnection.PeerConnectionState.FAILED ||
                                        pcState == PeerConnection.PeerConnectionState.CLOSED
                                    ) {
                                        breadcrumb("PC: $newState timeout ($timeoutMs ms) → reset")
                                        resetSession(reason = "Connection lost", notifyPeer = false)
                                    } else {
                                        breadcrumb("PC: $newState recovered (state=$pcState)")
                                    }
                                }
                        }
                        else -> Unit
                    }
                }
            }

        peerConnection = factory.createPeerConnection(configuration, observer)
        return peerConnection
    }

    private fun onCallConnected() {
        appContext?.let { CallSoundManager.play(it, CallSoundManager.CallSound.CONNECTED) }
        updateState { it.copy(phase = CallPhase.ACTIVE, statusText = "Call active") }
        durationJob?.cancel()
        durationJob =
            scope.launch {
                while (true) {
                    delay(1_000L)
                    _state.update { current ->
                        if (current.phase != CallPhase.ACTIVE) return@update current
                        current.copy(durationSec = current.durationSec + 1)
                    }
                }
            }
    }

    private fun setPeer(publicKey: String, title: String, username: String) {
        updateState {
            it.copy(
                peerPublicKey = publicKey.trim(),
                peerTitle = title.trim(),
                peerUsername = username.trim()
            )
        }
    }

    private fun resolvePeerIdentity(publicKey: String) {
        scope.launch {
            val resolved = ProtocolManager.resolveUserInfo(publicKey)
            if (resolved != null && _state.value.peerPublicKey == publicKey) {
                setPeer(publicKey, resolved.title, resolved.username)
            }
        }
    }

    private fun ensurePeerFactory() {
        if (peerConnectionFactory != null) return
        val context = appContext ?: return
        PeerConnectionFactory.initialize(
            PeerConnectionFactory.InitializationOptions.builder(context)
                .createInitializationOptions()
        )
        peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory()
    }

    private fun emitCallAttachmentIfNeeded(snapshot: CallUiState) {
        if (role != CallRole.CALLER) return
        val peerPublicKey = snapshot.peerPublicKey.trim()
        val context = appContext ?: return
        if (peerPublicKey.isBlank()) return

        val durationSec = snapshot.durationSec.coerceAtLeast(0)
        val callAttachment =
            MessageAttachment(
                id = java.util.UUID.randomUUID().toString().replace("-", "").take(16),
                blob = "",
                type = AttachmentType.CALL,
                preview = durationSec.toString()
            )

        scope.launch {
            runCatching {
                MessageRepository.getInstance(context).sendMessage(
                    toPublicKey = peerPublicKey,
                    text = "",
                    attachments = listOf(callAttachment)
                )
            }.onFailure { error ->
                Log.w(TAG, "Failed to send call attachment", error)
            }
        }
    }

    private fun resetSession(reason: String?, notifyPeer: Boolean) {
        breadcrumb("RESET: reason=$reason notifyPeer=$notifyPeer phase=${_state.value.phase}")
        breadcrumbState("resetSession")
        val snapshot = _state.value
        val wasActive = snapshot.phase != CallPhase.IDLE
        val peerToNotify = snapshot.peerPublicKey
        if (notifyPeer && ownPublicKey.isNotBlank() && peerToNotify.isNotBlank()) {
            ProtocolManager.sendCallSignal(
                signalType = SignalType.END_CALL,
                src = ownPublicKey,
                dst = peerToNotify
            )
        }
        // Play end call sound, then stop all
        if (wasActive) {
            appContext?.let { CallSoundManager.play(it, CallSoundManager.CallSound.END_CALL) }
        } else {
            CallSoundManager.stop()
        }
        if (!reason.isNullOrBlank()) {
            Log.d(TAG, reason)
        }
        emitCallAttachmentIfNeeded(snapshot)
        resetRtcObjects()
        e2eeAvailable = true
        role = null
        roomId = ""
        offerSent = false
        remoteDescriptionSet = false
        keyExchangeSent = false
        createRoomSent = false
        lastPeerSharedPublicHex = ""
        lastRemoteOfferFingerprint = ""
        lastLocalOfferFingerprint = ""
        e2eeRebindJob?.cancel()
        e2eeRebindJob = null
        localPrivateKey = null
        localPublicKey = null
        callSessionId = ""
        callStartedAtMs = 0L
        durationJob?.cancel()
        durationJob = null
        disconnectResetJob?.cancel()
        disconnectResetJob = null
        setSpeakerphone(false)
        _state.value = CallUiState()
    }

    private fun resetRtcObjects() {
        bufferedRemoteCandidates.clear()
        // Teardown E2EE BEFORE closing PeerConnection — WebRTC may access
        // encryptor/decryptor during close(), causing SIGSEGV if done after.
        teardownE2EE()
        runCatching { localAudioTrack?.setEnabled(false) }
        runCatching { localAudioTrack?.dispose() }
        runCatching { audioSource?.dispose() }
        runCatching { peerConnection?.close() }
        localAudioTrack = null
        audioSource = null
        peerConnection = null
    }

    private fun flushBufferedRemoteCandidates() {
        val pc = peerConnection ?: return
        if (bufferedRemoteCandidates.isEmpty()) return
        bufferedRemoteCandidates.forEach { candidate ->
            runCatching { pc.addIceCandidate(candidate) }
        }
        bufferedRemoteCandidates.clear()
    }

    // ── E2EE (XChaCha20 — compatible with Desktop) ──────────────────

    @Volatile
    private var e2eeAvailable = true

    private fun setupE2EE(sharedKeyHex: String) {
        val keyBytes = sharedKeyHex.hexToBytes()
        if (keyBytes == null || keyBytes.size < 32) {
            Log.e(TAG, "E2EE: invalid key (${keyBytes?.size ?: 0} bytes)")
            return
        }
        sharedKeyBytes = keyBytes.copyOf(32)
        breadcrumb("KE: shared key fp=${sharedKeyBytes?.fingerprintHex(8)}")
        // Open native diagnostics file for frame-level logging
        try {
            val dir = java.io.File(appContext!!.filesDir, "crash_reports")
            if (!dir.exists()) dir.mkdirs()
            val diagPath = java.io.File(dir, "e2ee_diag.txt").absolutePath
            XChaCha20E2EE.nativeOpenDiagFile(diagPath)
        } catch (_: Throwable) {}
        // If sender track already exists, bind encryptor now.
        val existingSender =
            pendingAudioSenderForE2ee
                ?: runCatching {
                    peerConnection?.senders?.firstOrNull { sender -> sender.track()?.kind() == "audio" }
                }.getOrNull()
        if (existingSender != null) {
            attachSenderE2EE(existingSender)
        }
        attachReceiverE2EEFromPeerConnection()
        startE2EERebindLoopIfNeeded()
        Log.i(TAG, "E2EE key ready (XChaCha20)")
    }

    private fun startE2EERebindLoopIfNeeded() {
        if (e2eeRebindJob?.isActive == true) return
        e2eeRebindJob =
            scope.launch {
                while (true) {
                    delay(1500L)
                    if (!e2eeAvailable || sharedKeyBytes == null) continue
                    val phaseNow = _state.value.phase
                    if (phaseNow != CallPhase.CONNECTING && phaseNow != CallPhase.ACTIVE) continue
                    val pc = peerConnection ?: continue
                    val sender =
                        runCatching {
                            pc.senders.firstOrNull { it.track()?.kind() == "audio" }
                        }.getOrNull()
                    if (sender != null) {
                        attachSenderE2EE(sender)
                    }
                    attachReceiverE2EEFromPeerConnection()
                }
            }
    }

    private fun attachReceiverE2EEFromPeerConnection() {
        val pc = peerConnection ?: return
        runCatching {
            var fromReceivers = 0
            var fromTransceivers = 0
            pc.receivers.forEach { receiver ->
                if (isAudioReceiver(receiver)) {
                    attachReceiverE2EE(receiver)
                    fromReceivers++
                }
            }
            pc.transceivers.forEach { transceiver ->
                val receiver = transceiver.receiver ?: return@forEach
                if (isAudioReceiver(receiver)) {
                    attachReceiverE2EE(receiver)
                    fromTransceivers++
                }
            }
            breadcrumb("E2EE: scan receivers attached recv=$fromReceivers tx=$fromTransceivers totalMap=${receiverDecryptors.size}")
        }.onFailure {
            breadcrumb("E2EE: attachReceiverE2EEFromPeerConnection failed: ${it.message}")
        }
    }

    /** Write a breadcrumb to crash_reports/e2ee_breadcrumb.txt — survives SIGSEGV */
    private fun breadcrumb(step: String) {
        try {
            val dir = ensureCrashReportsDir() ?: return
            if (!dir.exists()) dir.mkdirs()
            val f = java.io.File(dir, BREADCRUMB_FILE_NAME)
            // Reset file at start of key exchange
            if (step.startsWith("KE:") && step.contains("agreement")) {
                f.writeText("")
            }
            val sidPrefix = if (callSessionId.isNotBlank()) "[sid=$callSessionId] " else ""
            f.appendText("${System.currentTimeMillis()} $sidPrefix$step\n")
        } catch (_: Throwable) {}
    }

    /** Save a full crash report to crash_reports/ */
    private fun saveCrashReport(title: String, error: Throwable) {
        try {
            val dir = ensureCrashReportsDir() ?: return
            if (!dir.exists()) dir.mkdirs()
            val ts = java.text.SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", java.util.Locale.getDefault()).format(java.util.Date())
            val f = java.io.File(dir, "crash_e2ee_$ts.txt")
            val sw = java.io.StringWriter()
            error.printStackTrace(java.io.PrintWriter(sw))
            val breadcrumbTail = readFileTail(java.io.File(dir, BREADCRUMB_FILE_NAME), TAIL_LINES)
            val diagTail = readFileTail(java.io.File(dir, DIAG_FILE_NAME), TAIL_LINES)
            val nativeCrash = readFileTail(java.io.File(dir, NATIVE_CRASH_FILE_NAME), TAIL_LINES)
            val protocolTail =
                ProtocolManager.debugLogs.value
                    .takeLast(PROTOCOL_LOG_TAIL_LINES)
                    .joinToString("\n")
            f.writeText(
                buildString {
                    appendLine("=== E2EE CRASH REPORT ===")
                    appendLine(title)
                    appendLine()
                    appendLine("Time: $ts")
                    appendLine("Type: ${error.javaClass.name}")
                    appendLine("Message: ${error.message}")
                    appendLine()
                    appendLine("--- CALL SNAPSHOT ---")
                    appendLine(buildStateSnapshot())
                    appendLine()
                    appendLine("--- STACKTRACE ---")
                    appendLine(sw.toString())
                    appendLine()
                    appendLine("--- NATIVE CRASH (tail) ---")
                    appendLine(nativeCrash)
                    appendLine()
                    appendLine("--- E2EE DIAG (tail) ---")
                    appendLine(diagTail)
                    appendLine()
                    appendLine("--- E2EE BREADCRUMB (tail) ---")
                    appendLine(breadcrumbTail)
                    appendLine()
                    appendLine("--- PROTOCOL LOGS (tail) ---")
                    appendLine(if (protocolTail.isBlank()) "<empty>" else protocolTail)
                }
            )
        } catch (_: Throwable) {}
    }

    private fun beginCallSession(seed: String) {
        val bytes = ByteArray(4)
        secureRandom.nextBytes(bytes)
        val random = bytes.joinToString("") { "%02x".format(it) }
        callSessionId = "${seed.take(8)}-$random"
        callStartedAtMs = System.currentTimeMillis()
        breadcrumb("SESSION: begin seed=$seed")
    }

    private fun ensureCrashReportsDir(): java.io.File? {
        val context = appContext ?: return null
        return java.io.File(context.filesDir, "crash_reports").apply { if (!exists()) mkdirs() }
    }

    private fun readFileTail(file: java.io.File, maxLines: Int): String {
        if (!file.exists()) return "<missing: ${file.name}>"
        return runCatching {
            val lines = file.readLines()
            val tail = if (lines.size <= maxLines) lines else lines.takeLast(maxLines)
            if (tail.isEmpty()) "<empty: ${file.name}>" else tail.joinToString("\n")
        }.getOrElse { "<read-failed: ${file.name}: ${it.message}>" }
    }

    private fun buildStateSnapshot(): String {
        val st = _state.value
        val now = System.currentTimeMillis()
        val age = if (callStartedAtMs > 0L) now - callStartedAtMs else -1L
        val pc = peerConnection
        val pcSig = runCatching { pc?.signalingState() }.getOrNull()
        val pcIce = runCatching { pc?.iceConnectionState() }.getOrNull()
        val pcConn = runCatching { pc?.connectionState() }.getOrNull()
        val pcLocal = runCatching { pc?.localDescription?.type?.canonicalForm() }.getOrDefault("-")
        val pcRemote = runCatching { pc?.remoteDescription?.type?.canonicalForm() }.getOrDefault("-")
        val senders = runCatching { pc?.senders?.size ?: 0 }.getOrDefault(-1)
        val receivers = runCatching { pc?.receivers?.size ?: 0 }.getOrDefault(-1)
        return buildString {
            append("sid=").append(if (callSessionId.isBlank()) "<none>" else callSessionId)
            append(" ageMs=").append(age)
            append(" phase=").append(st.phase)
            append(" role=").append(role)
            append(" peer=").append(st.peerPublicKey.take(12))
            append(" room=").append(roomId.take(16))
            append(" offerSent=").append(offerSent)
            append(" remoteDescSet=").append(remoteDescriptionSet)
            append(" e2eeAvail=").append(e2eeAvailable)
            append(" keyBytes=").append(sharedKeyBytes?.size ?: 0)
            append(" pc(sig=").append(pcSig)
            append(",ice=").append(pcIce)
            append(",conn=").append(pcConn)
            append(",local=").append(pcLocal)
            append(",remote=").append(pcRemote)
            append(",senders=").append(senders)
            append(",receivers=").append(receivers)
            append(")")
        }
    }

    private fun breadcrumbState(marker: String) {
        breadcrumb("STATE[$marker] ${buildStateSnapshot()}")
    }

    private fun attachSenderE2EE(sender: RtpSender?) {
        if (!e2eeAvailable) return
        val key = sharedKeyBytes ?: return
        if (sender == null) return
        val existing = senderEncryptors[sender]
        if (existing != null) {
            runCatching { sender.setFrameEncryptor(existing) }
            return
        }

        try {
            breadcrumb("1. encryptor: nativeLoaded=${XChaCha20E2EE.nativeLoaded}")
            val enc = XChaCha20E2EE.Encryptor(key)
            breadcrumb("2. encryptor created")
            val ptr = enc.getNativeFrameEncryptor()
            breadcrumb("3. encryptor ptr=0x${ptr.toString(16)}")
            if (ptr == 0L) {
                saveCrashReport("Encryptor native ptr is 0", RuntimeException("null native ptr"))
                return
            }
            breadcrumb("4. calling sender.setFrameEncryptor…")
            sender.setFrameEncryptor(enc)
            breadcrumb("5. setFrameEncryptor OK!")
            senderEncryptors[sender] = enc
            pendingAudioSenderForE2ee = null
        } catch (e: Throwable) {
            saveCrashReport("attachSenderE2EE failed", e)
            Log.e(TAG, "E2EE: sender encryptor failed", e)
            e2eeAvailable = false
        }
    }

    private fun isAudioReceiver(receiver: RtpReceiver?): Boolean {
        if (receiver == null) return false
        return runCatching { receiver.track()?.kind() == "audio" }.getOrDefault(false)
    }

    private fun attachReceiverE2EE(receiver: RtpReceiver?) {
        if (!e2eeAvailable) return
        val key = sharedKeyBytes ?: return
        if (receiver == null) return
        val existing = receiverDecryptors[receiver]
        if (existing != null) {
            runCatching { receiver.setFrameDecryptor(existing) }
            return
        }

        try {
            breadcrumb("6. decryptor: creating…")
            val dec = XChaCha20E2EE.Decryptor(key)
            breadcrumb("7. decryptor created")
            val ptr = dec.getNativeFrameDecryptor()
            breadcrumb("8. decryptor ptr=0x${ptr.toString(16)}")
            if (ptr == 0L) {
                saveCrashReport("Decryptor native ptr is 0", RuntimeException("null native ptr"))
                return
            }
            breadcrumb("9. calling receiver.setFrameDecryptor…")
            receiver.setFrameDecryptor(dec)
            breadcrumb("10. setFrameDecryptor OK!")
            receiverDecryptors[receiver] = dec
        } catch (e: Throwable) {
            saveCrashReport("attachReceiverE2EE failed", e)
            Log.e(TAG, "E2EE: receiver decryptor failed", e)
            e2eeAvailable = false
        }
    }

    private fun teardownE2EE() {
        // Release our ref. WebRTC holds its own ref via scoped_refptr.
        // After our Release: WebRTC ref remains. On peerConnection.close()
        // WebRTC releases its ref → ref=0 → native object deleted.
        senderEncryptors.values.forEach { enc ->
            runCatching { enc.dispose() }
        }
        receiverDecryptors.values.forEach { dec ->
            runCatching { dec.dispose() }
        }
        senderEncryptors.clear()
        receiverDecryptors.clear()
        pendingAudioSenderForE2ee = null
        sharedKeyBytes?.let { it.fill(0) }
        sharedKeyBytes = null
        runCatching { XChaCha20E2EE.nativeCloseDiagFile() }
    }

    private fun generateSessionKeys() {
        val privateKey = ByteArray(32)
        X25519.generatePrivateKey(secureRandom, privateKey)
        val publicKey = ByteArray(32)
        X25519.generatePublicKey(privateKey, 0, publicKey, 0)
        localPrivateKey = privateKey
        localPublicKey = publicKey
        breadcrumb("KE: local keypair pub=${publicKey.shortHex()} privFp=${privateKey.fingerprintHex(6)}")
    }

    private fun computeSharedSecretHex(peerPublicHex: String): String? {
        val privateKey = localPrivateKey ?: return null
        val peerPublic = peerPublicHex.hexToBytes() ?: return null
        if (peerPublic.size != 32) return null
        val rawDh = ByteArray(32)
        breadcrumb("KE: X25519 agreement with peerPub=${peerPublic.shortHex()}…")
        val ok = X25519.calculateAgreement(privateKey, 0, peerPublic, 0, rawDh, 0)
        if (!ok) {
            breadcrumb("KE: X25519 FAILED")
            return null
        }
        breadcrumb("KE: X25519 OK rawDhFp=${rawDh.fingerprintHex(8)}, calling HSalsa20…")
        return try {
            val naclShared = XChaCha20E2EE.hsalsa20(rawDh)
            rawDh.fill(0)
            breadcrumb("KE: HSalsa20 OK keyFp=${naclShared.fingerprintHex(8)}")
            naclShared.toHex()
        } catch (e: Throwable) {
            saveCrashReport("HSalsa20 failed", e)
            breadcrumb("KE: HSalsa20 FAILED: ${e.message}")
            e2eeAvailable = false
            val hex = rawDh.toHex()
            rawDh.fill(0)
            hex
        }
    }

    private fun serializeSessionDescription(description: SessionDescription): String {
        return JSONObject()
            .put("type", description.type.canonicalForm())
            .put("sdp", description.description)
            .toString()
    }

    private fun parseSessionDescription(raw: String): SessionDescription? {
        return runCatching {
            val json = JSONObject(raw)
            val type = SessionDescription.Type.fromCanonicalForm(json.getString("type"))
            val sdp = json.getString("sdp")
            SessionDescription(type, sdp)
        }.onFailure { error ->
            val preview = raw.replace('\n', ' ').replace('\r', ' ')
            breadcrumb(
                "RTC: parseSessionDescription FAILED len=${raw.length} " +
                    "preview=${preview.take(MAX_LOG_PREFIX)} err=${error.message}"
            )
        }.getOrNull()
    }

    private fun serializeIceCandidate(candidate: IceCandidate): String {
        return JSONObject()
            .put("candidate", candidate.sdp)
            .put("sdpMid", candidate.sdpMid)
            .put("sdpMLineIndex", candidate.sdpMLineIndex)
            .toString()
    }

    private fun parseIceCandidate(raw: String): IceCandidate? {
        return runCatching {
            val json = JSONObject(raw)
            val candidate = json.getString("candidate")
            val sdpMid = if (json.has("sdpMid") && !json.isNull("sdpMid")) json.getString("sdpMid") else null
            val sdpMLineIndex = json.optInt("sdpMLineIndex", 0)
            IceCandidate(sdpMid, sdpMLineIndex, candidate)
        }.onFailure { error ->
            val preview = raw.replace('\n', ' ').replace('\r', ' ')
            breadcrumb(
                "RTC: parseIceCandidate FAILED len=${raw.length} " +
                    "preview=${preview.take(MAX_LOG_PREFIX)} err=${error.message}"
            )
        }.getOrNull()
    }

    private fun setSpeakerphone(enabled: Boolean) {
        val context = appContext ?: return
        val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: return
        audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
        audioManager.isSpeakerphoneOn = enabled
    }

    private fun updateState(reducer: (CallUiState) -> CallUiState) {
        _state.update(reducer)
    }

    private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
    private fun ByteArray.shortHex(bytes: Int = 6): String =
        take(bytes.coerceAtMost(size)).joinToString("") { "%02x".format(it) }
    private fun ByteArray.fingerprintHex(bytes: Int = 8): String {
        val digest = MessageDigest.getInstance("SHA-256").digest(this)
        return digest.take(bytes.coerceAtMost(digest.size)).joinToString("") { "%02x".format(it) }
    }
    private fun String.shortFingerprintHex(bytes: Int = 8): String {
        val digest = MessageDigest.getInstance("SHA-256").digest(toByteArray(Charsets.UTF_8))
        return digest.take(bytes.coerceAtMost(digest.size)).joinToString("") { "%02x".format(it) }
    }

    private fun String.hexToBytes(): ByteArray? {
        val clean = trim().lowercase()
        if (clean.length % 2 != 0) return null
        return runCatching {
            ByteArray(clean.length / 2) { idx ->
                clean.substring(idx * 2, idx * 2 + 2).toInt(16).toByte()
            }
        }.getOrNull()
    }

    private suspend fun PeerConnection.createOfferAwait(): SessionDescription =
        suspendCancellableCoroutine { continuation ->
            createOffer(
                object : SdpObserver {
                    override fun onCreateSuccess(sdp: SessionDescription?) {
                        if (sdp == null) {
                            continuation.resumeWithException(IllegalStateException("Offer is null"))
                            return
                        }
                        continuation.resume(sdp)
                    }

                    override fun onSetSuccess() = Unit
                    override fun onCreateFailure(error: String?) {
                        continuation.resumeWithException(
                            IllegalStateException(error ?: "createOffer failed")
                        )
                    }

                    override fun onSetFailure(error: String?) = Unit
                },
                MediaConstraints().apply {
                    mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
                    mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"))
                }
            )
        }

    private suspend fun PeerConnection.createAnswerAwait(): SessionDescription =
        suspendCancellableCoroutine { continuation ->
            createAnswer(
                object : SdpObserver {
                    override fun onCreateSuccess(sdp: SessionDescription?) {
                        if (sdp == null) {
                            continuation.resumeWithException(IllegalStateException("Answer is null"))
                            return
                        }
                        continuation.resume(sdp)
                    }

                    override fun onSetSuccess() = Unit
                    override fun onCreateFailure(error: String?) {
                        continuation.resumeWithException(
                            IllegalStateException(error ?: "createAnswer failed")
                        )
                    }

                    override fun onSetFailure(error: String?) = Unit
                },
                MediaConstraints()
            )
        }

    private suspend fun PeerConnection.setLocalDescriptionAwait(description: SessionDescription) {
        suspendCancellableCoroutine<Unit> { continuation ->
            setLocalDescription(
                object : SdpObserver {
                    override fun onCreateSuccess(sdp: SessionDescription?) = Unit
                    override fun onSetSuccess() {
                        continuation.resume(Unit)
                    }
                    override fun onCreateFailure(error: String?) = Unit
                    override fun onSetFailure(error: String?) {
                        continuation.resumeWithException(
                            IllegalStateException(error ?: "setLocalDescription failed")
                        )
                    }
                },
                description
            )
        }
    }

    private suspend fun PeerConnection.setRemoteDescriptionAwait(description: SessionDescription) {
        suspendCancellableCoroutine<Unit> { continuation ->
            setRemoteDescription(
                object : SdpObserver {
                    override fun onCreateSuccess(sdp: SessionDescription?) = Unit
                    override fun onSetSuccess() {
                        continuation.resume(Unit)
                    }
                    override fun onCreateFailure(error: String?) = Unit
                    override fun onSetFailure(error: String?) {
                        continuation.resumeWithException(
                            IllegalStateException(error ?: "setRemoteDescription failed")
                        )
                    }
                },
                description
            )
        }
    }
}
