package com.rosetta.messenger.network

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.channels.Channel
import okhttp3.*
import okio.ByteString
import java.util.Locale
import java.util.concurrent.TimeUnit
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong

/**
 * Protocol connection states
 */
enum class ProtocolState {
    DISCONNECTED,
    CONNECTING,
    CONNECTED,
    HANDSHAKING,
    DEVICE_VERIFICATION_REQUIRED,
    AUTHENTICATED
}

/**
 * Protocol client for Rosetta Messenger
 * Handles WebSocket connection and packet exchange with server
 */
class Protocol(
    private val serverAddress: String,
    private val logger: (String) -> Unit = {},
    private val isNetworkAvailable: (() -> Boolean)? = null,
    private val onNetworkUnavailable: (() -> Unit)? = null
) {
    companion object {
        private const val TAG = "RosettaProtocol"
        private const val RECONNECT_INTERVAL = 5000L // 5 seconds (как в Архиве)
        private const val HANDSHAKE_TIMEOUT = 10000L // 10 seconds
        private const val CONNECTING_STUCK_TIMEOUT_MS = 15_000L
        private const val MIN_PACKET_ID_BITS = 18 // Stream.readInt16() = 2 * readInt8() (9 bits each)
        private const val DEFAULT_HEARTBEAT_INTERVAL_SECONDS = 15
        private const val MIN_HEARTBEAT_SEND_INTERVAL_MS = 2_000L
        private const val BACKGROUND_HEARTBEAT_INTERVAL_MS = 30_000L
        private const val MAX_RECONNECT_ATTEMPTS = 10
        private const val HEARTBEAT_OK_LOG_THROTTLE_MS = 30_000L
        private const val HEX_PREVIEW_BYTES = 64
        private const val TEXT_PREVIEW_CHARS = 80
        private val INSTANCE_COUNTER = AtomicInteger(0)
    }
    
    private fun log(message: String) {
        // TEMPORARY: Enable logging for debugging PacketUserInfo
        logger(message)
    }

    private fun packetName(packetId: Int): String =
        when (packetId) {
            0x00 -> "HANDSHAKE"
            0x01 -> "USER_INFO"
            0x02 -> "RESULT"
            0x03 -> "SEARCH"
            0x04 -> "ONLINE_SUBSCRIBE"
            0x05 -> "ONLINE_STATE"
            0x06 -> "MESSAGE"
            0x07 -> "READ"
            0x08 -> "DELIVERY"
            0x09 -> "DEVICE_NEW"
            0x0A -> "REQUEST_UPDATE"
            0x0B -> "TYPING"
            0x0F -> "REQUEST_TRANSPORT"
            0x10 -> "PUSH_NOTIFICATION"
            0x11 -> "GROUP_CREATE"
            0x12 -> "GROUP_INFO"
            0x13 -> "GROUP_INVITE_INFO"
            0x14 -> "GROUP_JOIN"
            0x15 -> "GROUP_LEAVE"
            0x16 -> "GROUP_BAN"
            0x17 -> "DEVICE_LIST"
            0x18 -> "DEVICE_RESOLVE"
            0x19 -> "SYNC"
            0x1A -> "SIGNAL_PEER"
            0x1B -> "WEBRTC"
            0x1C -> "ICE_SERVERS"
            else -> "UNKNOWN"
        }

    private fun shortKey(value: String, visible: Int = 8): String {
        val raw = value.trim()
        if (raw.isBlank()) return "<empty>"
        if (raw.length <= visible) return raw
        return "${raw.take(visible)}…"
    }

    private fun shortText(value: String, limit: Int = TEXT_PREVIEW_CHARS): String {
        val normalized = value.replace('\n', ' ').replace('\r', ' ').trim()
        if (normalized.isBlank()) return "<empty>"
        return if (normalized.length <= limit) normalized else "${normalized.take(limit)}…"
    }

    private fun hexPreview(bytes: ByteArray, limit: Int = HEX_PREVIEW_BYTES): String {
        return bytes
            .take(limit)
            .joinToString(" ") { b -> String.format(Locale.US, "%02X", b.toInt() and 0xFF) } +
            if (bytes.size > limit) " …" else ""
    }

    private fun packetSummary(packet: Packet): String {
        return when (packet) {
            is PacketHandshake ->
                "state=${packet.handshakeState} proto=${packet.protocolVersion} hb=${packet.heartbeatInterval}s " +
                    "pub=${shortKey(packet.publicKey, 12)} privLen=${packet.privateKey.length} " +
                    "deviceId=${shortKey(packet.device.deviceId, 12)} device='${shortText(packet.device.deviceName, 40)}' " +
                    "os='${shortText(packet.device.deviceOs, 32)}'"
            is PacketResult ->
                "code=${packet.resultCode}"
            is PacketSearch ->
                "query='${shortText(packet.search, 48)}' users=${packet.users.size} privLen=${packet.privateKey.length}"
            is PacketOnlineSubscribe ->
                "keys=${packet.publicKeys.size} first=${packet.publicKeys.firstOrNull()?.let { shortKey(it) } ?: "<none>"} " +
                    "privLen=${packet.privateKey.length}"
            is PacketOnlineState ->
                "entries=${packet.publicKeysState.size} first=${packet.publicKeysState.firstOrNull()?.let { "${shortKey(it.publicKey)}:${it.state}" } ?: "<none>"}"
            is PacketMessage ->
                "id=${shortKey(packet.messageId, 12)} from=${shortKey(packet.fromPublicKey)} to=${shortKey(packet.toPublicKey)} " +
                    "ts=${packet.timestamp} contentLen=${packet.content.length} chachaLen=${packet.chachaKey.length} aesLen=${packet.aesChachaKey.length} " +
                    "att=${packet.attachments.size}" +
                    if (packet.attachments.isNotEmpty()) {
                        " attTypes=${packet.attachments.joinToString(",") { it.type.name }}"
                    } else {
                        ""
                    }
            is PacketRead ->
                "from=${shortKey(packet.fromPublicKey)} to=${shortKey(packet.toPublicKey)} privLen=${packet.privateKey.length}"
            is PacketDelivery ->
                "id=${shortKey(packet.messageId, 12)} to=${shortKey(packet.toPublicKey)}"
            is PacketTyping ->
                "from=${shortKey(packet.fromPublicKey)} to=${shortKey(packet.toPublicKey)} privLen=${packet.privateKey.length}"
            is PacketPushNotification ->
                "action=${packet.action} tokenType=${packet.tokenType} tokenLen=${packet.notificationsToken.length} " +
                    "deviceId=${shortKey(packet.deviceId, 12)}"
            is PacketSync ->
                "status=${packet.status} ts=${packet.timestamp}"
            is PacketSignalPeer ->
                "type=${packet.signalType} src=${shortKey(packet.src)} dst=${shortKey(packet.dst)} " +
                    "sharedLen=${packet.sharedPublic.length} callId=${shortKey(packet.callId, 12)} join=${shortKey(packet.joinToken, 12)}"
            is PacketWebRTC ->
                "type=${packet.signalType} sdpLen=${packet.sdpOrCandidate.length} " +
                    "preview='${shortText(packet.sdpOrCandidate, 64)}'"
            is PacketIceServers ->
                "count=${packet.iceServers.size} firstUrl='${packet.iceServers.firstOrNull()?.url?.let { shortText(it, 40) } ?: "<none>"}'"
            is PacketRequestTransport ->
                "requestTransport"
            is PacketDeviceResolve ->
                "deviceId=${shortKey(packet.deviceId, 12)} solution=${packet.solution}"
            is PacketDeviceList ->
                "devices=${packet.devices.size}"
            is PacketDeviceNew ->
                "ip='${shortText(packet.ipAddress, 32)}' deviceId=${shortKey(packet.device.deviceId, 12)} " +
                    "device='${shortText(packet.device.deviceName, 32)}' os='${shortText(packet.device.deviceOs, 24)}'"
            is PacketUserInfo ->
                "username='${shortText(packet.username, 20)}' title='${shortText(packet.title, 24)}' privLen=${packet.privateKey.length}"
            else ->
                packet::class.java.simpleName
        }
    }

    private fun describePacket(packet: Packet, bytes: Int): String {
        val packetId = packet.getPacketId()
        return "id=0x${packetId.toString(16).uppercase(Locale.ROOT)}(${packetId}) name=${packetName(packetId)} bytes=$bytes ${packetSummary(packet)}"
    }
    
    private val client = OkHttpClient.Builder()
        .readTimeout(0, TimeUnit.MILLISECONDS)
        .connectTimeout(10, TimeUnit.SECONDS)
        // Отключаем встроенный ping/pong - используем свой heartbeat
        // .pingInterval(30, TimeUnit.SECONDS)
        .build()
    
    private var webSocket: WebSocket? = null
    private var isManuallyClosed = false
    private var handshakeComplete = false
    private var handshakeJob: Job? = null
    
    // 🔍 Диагностика соединения
    private var reconnectAttempts = 0
    private var lastStateChangeTime = System.currentTimeMillis()
    private var lastSuccessfulConnection = 0L
    private var reconnectJob: Job? = null // Для отмены запланированных переподключений
    private var connectingTimeoutJob: Job? = null
    private var isConnecting = false // Флаг для защиты от одновременных подключений
    private var connectingSinceMs = 0L
    
    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    private val connectionGeneration = AtomicLong(0L)
    @Volatile private var activeConnectionGeneration: Long = 0L
    private val instanceId = INSTANCE_COUNTER.incrementAndGet()

    /**
     * Single-writer session loop for all lifecycle mutations.
     * Replaces ad-hoc Mutex locking and guarantees strict FIFO ordering.
     */
    private sealed interface SessionEvent {
        data class Connect(val trigger: String = "api_connect") : SessionEvent
        data class HandleDisconnect(val source: String) : SessionEvent
        data class Disconnect(val manual: Boolean, val reason: String) : SessionEvent
        data class FastReconnect(val reason: String) : SessionEvent
        data class AccountSwitchReconnect(val reason: String = "Account switch reconnect") : SessionEvent
        data class HandshakeResponse(val packet: PacketHandshake) : SessionEvent
        data class DeviceVerificationAccepted(val deviceId: String) : SessionEvent
        data class DeviceVerificationDeclined(
            val deviceId: String,
            val observedState: ProtocolState
        ) : SessionEvent
        data class SocketOpened(
            val generation: Long,
            val socket: WebSocket,
            val responseCode: Int
        ) : SessionEvent
        data class SocketClosed(
            val generation: Long,
            val socket: WebSocket,
            val code: Int,
            val reason: String
        ) : SessionEvent
        data class SocketFailure(
            val generation: Long,
            val socket: WebSocket,
            val throwable: Throwable,
            val responseCode: Int?,
            val responseMessage: String?
        ) : SessionEvent
        data class ConnectingTimeout(val generation: Long) : SessionEvent
    }

    private val sessionEvents = Channel<SessionEvent>(Channel.UNLIMITED)
    private val sessionLoopJob =
        scope.launch {
            for (event in sessionEvents) {
                try {
                    when (event) {
                        is SessionEvent.Connect -> connectLocked()
                        is SessionEvent.HandleDisconnect -> handleDisconnectLocked(event.source)
                        is SessionEvent.Disconnect ->
                            disconnectLocked(manual = event.manual, reason = event.reason)
                        is SessionEvent.FastReconnect -> reconnectNowIfNeededLocked(event.reason)
                        is SessionEvent.AccountSwitchReconnect -> {
                            disconnectLocked(manual = false, reason = event.reason)
                            connectLocked()
                        }
                        is SessionEvent.HandshakeResponse -> handleHandshakeResponse(event.packet)
                        is SessionEvent.DeviceVerificationAccepted ->
                            handleDeviceVerificationAccepted(event.deviceId)
                        is SessionEvent.DeviceVerificationDeclined -> {
                            handshakeComplete = false
                            handshakeJob?.cancel()
                            packetQueue.clear()
                            if (webSocket != null) {
                                setState(
                                    ProtocolState.CONNECTED,
                                    "Device verification declined, waiting for retry"
                                )
                            } else {
                                setState(
                                    ProtocolState.DISCONNECTED,
                                    "Device verification declined without active socket"
                                )
                            }
                            log(
                                "⛔ DEVICE DECLINE APPLIED: deviceId=${shortKey(event.deviceId, 12)} " +
                                    "observed=${event.observedState} current=${_state.value}"
                            )
                        }
                        is SessionEvent.SocketOpened -> handleSocketOpened(event)
                        is SessionEvent.SocketClosed -> handleSocketClosed(event)
                        is SessionEvent.SocketFailure -> handleSocketFailure(event)
                        is SessionEvent.ConnectingTimeout -> handleConnectingTimeout(event.generation)
                    }
                } catch (e: CancellationException) {
                    throw e
                } catch (e: Exception) {
                    log("❌ Session event failed: ${event::class.java.simpleName} ${e.message}")
                    e.printStackTrace()
                }
            }
        }
    
    private val _state = MutableStateFlow(ProtocolState.DISCONNECTED)
    val state: StateFlow<ProtocolState> = _state.asStateFlow()
    
    /**
     * 🔍 Безопасное изменение состояния с подробным логированием
     */
    private fun setState(newState: ProtocolState, reason: String = "") {
        val oldState = _state.value
        val now = System.currentTimeMillis()
        val timeSinceLastChange = now - lastStateChangeTime
        
        if (oldState != newState) {
            log("🔄 STATE CHANGE: $oldState -> $newState (${timeSinceLastChange}ms since last change)")
            if (reason.isNotEmpty()) {
                log("   Reason: $reason")
            }
            log("   Reconnect attempts: $reconnectAttempts")
            log("   Last successful: ${if (lastSuccessfulConnection > 0) "${(now - lastSuccessfulConnection)/1000}s ago" else "never"}")
            
            _state.value = newState
            lastStateChangeTime = now
            
            // Сброс счетчика при успешном подключении
            if (newState == ProtocolState.AUTHENTICATED) {
                lastSuccessfulConnection = now
                reconnectAttempts = 0
                log("✅ CONNECTION FULLY ESTABLISHED")
            }
        }
    }

    private fun enqueueSessionEvent(event: SessionEvent) {
        val result = sessionEvents.trySend(event)
        if (result.isFailure) {
            log(
                "⚠️ Session event dropped: ${event::class.java.simpleName} " +
                    "reason=${result.exceptionOrNull()?.message ?: "channel_closed"}"
            )
        }
    }

    private fun cancelConnectingTimeout(reason: String) {
        if (connectingTimeoutJob != null) {
            log("⏱️ CONNECTING watchdog disarmed ($reason)")
        }
        connectingTimeoutJob?.cancel()
        connectingTimeoutJob = null
    }

    private fun armConnectingTimeout(generation: Long) {
        cancelConnectingTimeout(reason = "re-arm")
        connectingTimeoutJob = scope.launch {
            delay(CONNECTING_STUCK_TIMEOUT_MS)
            enqueueSessionEvent(SessionEvent.ConnectingTimeout(generation))
        }
        log("⏱️ CONNECTING watchdog armed gen=$generation timeout=${CONNECTING_STUCK_TIMEOUT_MS}ms")
    }

    private fun handleSocketOpened(event: SessionEvent.SocketOpened) {
        if (isStaleSocketEvent("onOpen", event.generation, event.socket)) return
        log(
            "✅ WebSocket OPEN: response=${event.responseCode}, " +
                "hasCredentials=${lastPublicKey != null}, gen=${event.generation}"
        )

        cancelConnectingTimeout(reason = "socket_opened")
        isConnecting = false
        connectingSinceMs = 0L

        setState(ProtocolState.CONNECTED, "WebSocket onOpen callback")
        // Flush queue as soon as socket is open.
        // Auth-required packets will remain queued until handshake completes.
        flushPacketQueue()

        if (_state.value != ProtocolState.HANDSHAKING && _state.value != ProtocolState.AUTHENTICATED) {
            lastPublicKey?.let { publicKey ->
                lastPrivateHash?.let { privateHash ->
                    log("🤝 Auto-starting handshake with saved credentials")
                    startHandshake(publicKey, privateHash, lastDevice)
                }
            } ?: log("⚠️ No saved credentials, waiting for manual handshake")
        } else {
            log("⚠️ Skipping auto-handshake: already in state ${_state.value}")
        }
    }

    private fun handleSocketClosed(event: SessionEvent.SocketClosed) {
        if (isStaleSocketEvent("onClosed", event.generation, event.socket)) return
        log(
            "❌ WebSocket CLOSED: code=${event.code} reason='${event.reason}' state=${_state.value} " +
                "manuallyClosed=$isManuallyClosed gen=${event.generation}"
        )
        cancelConnectingTimeout(reason = "socket_closed")
        isConnecting = false
        connectingSinceMs = 0L
        handleDisconnectLocked("onClosed")
    }

    private fun handleSocketFailure(event: SessionEvent.SocketFailure) {
        if (isStaleSocketEvent("onFailure", event.generation, event.socket)) return
        log("❌ WebSocket FAILURE: ${event.throwable.message}")
        log("   Response: ${event.responseCode} ${event.responseMessage}")
        log("   State: ${_state.value}")
        log("   Manually closed: $isManuallyClosed")
        log("   Reconnect attempts: $reconnectAttempts")
        log("   Generation: ${event.generation}")
        event.throwable.printStackTrace()
        cancelConnectingTimeout(reason = "socket_failure")
        isConnecting = false
        connectingSinceMs = 0L
        _lastError.value = event.throwable.message
        handleDisconnectLocked("onFailure")
    }

    private fun handleConnectingTimeout(generation: Long) {
        val currentState = _state.value
        if (generation != activeConnectionGeneration) {
            log(
                "⏱️ CONNECTING watchdog ignored for stale generation " +
                    "(event=$generation active=$activeConnectionGeneration)"
            )
            return
        }
        if (!isConnecting || currentState != ProtocolState.CONNECTING) {
            return
        }

        val elapsed = if (connectingSinceMs > 0L) {
            System.currentTimeMillis() - connectingSinceMs
        } else {
            CONNECTING_STUCK_TIMEOUT_MS
        }
        log("🧯 CONNECTING TIMEOUT fired (elapsed=${elapsed}ms) -> forcing disconnect/reconnect")

        cancelConnectingTimeout(reason = "timeout_fired")
        isConnecting = false
        connectingSinceMs = 0L
        runCatching { webSocket?.cancel() }
        webSocket = null
        handleDisconnectLocked("connecting_timeout")
    }

    private fun handleHandshakeResponse(packet: PacketHandshake) {
        handshakeJob?.cancel()

        when (packet.handshakeState) {
            HandshakeState.COMPLETED -> {
                log("✅ HANDSHAKE COMPLETE: protocol=${packet.protocolVersion}, heartbeat=${packet.heartbeatInterval}s")
                handshakeComplete = true
                setState(ProtocolState.AUTHENTICATED, "Handshake completed")
                flushPacketQueue()
            }

            HandshakeState.NEED_DEVICE_VERIFICATION -> {
                log("🔐 HANDSHAKE NEEDS DEVICE VERIFICATION")
                handshakeComplete = false
                setState(
                    ProtocolState.DEVICE_VERIFICATION_REQUIRED,
                    "Handshake requires device verification"
                )
                packetQueue.clear()
            }
        }

        // Keep heartbeat in both handshake states to maintain server session.
        startHeartbeat(packet.heartbeatInterval)
    }

    private fun handleDeviceVerificationAccepted(deviceId: String) {
        log("✅ DEVICE VERIFICATION ACCEPTED (deviceId=${shortKey(deviceId, 12)})")
        val stateAtAccept = _state.value
        if (stateAtAccept == ProtocolState.AUTHENTICATED) {
            log("✅ ACCEPT ignored: already authenticated")
            return
        }

        if (stateAtAccept == ProtocolState.DEVICE_VERIFICATION_REQUIRED) {
            setState(ProtocolState.CONNECTED, "Device verification accepted")
        }

        val publicKey = lastPublicKey
        val privateHash = lastPrivateHash
        if (publicKey.isNullOrBlank() || privateHash.isNullOrBlank()) {
            log("⚠️ ACCEPT received but credentials are missing, waiting for reconnect")
            return
        }

        when (_state.value) {
            ProtocolState.DISCONNECTED -> {
                log("🔄 ACCEPT while disconnected -> reconnecting")
                connectLocked()
            }

            ProtocolState.CONNECTING -> {
                log("⏳ ACCEPT while connecting -> waiting for onOpen auto-handshake")
            }

            else -> {
                startHandshake(publicKey, privateHash, lastDevice)
            }
        }
    }

    private fun rotateConnectionGeneration(reason: String): Long {
        val generation = connectionGeneration.incrementAndGet()
        activeConnectionGeneration = generation
        log("🧬 CONNECTION GENERATION: #$generation ($reason, instance=$instanceId)")
        return generation
    }

    private fun isStaleSocketEvent(event: String, generation: Long, socket: WebSocket): Boolean {
        val currentGeneration = activeConnectionGeneration
        val activeSocket = webSocket
        val staleByGeneration = generation != currentGeneration
        val staleBySocket = activeSocket != null && activeSocket !== socket
        if (!staleByGeneration && !staleBySocket) {
            return false
        }

        log(
            "🧊 STALE SOCKET EVENT ignored: event=$event gen=$generation activeGen=$currentGeneration " +
                "sameSocket=${activeSocket === socket} instance=$instanceId"
        )
        runCatching { socket.close(1000, "Stale socket event") }
        return true
    }
    
    private val _lastError = MutableStateFlow<String?>(null)
    val lastError: StateFlow<String?> = _lastError.asStateFlow()
    
    // Packet waiters - callbacks for specific packet types (thread-safe)
    private val packetWaiters =
        java.util.concurrent.ConcurrentHashMap<Int, CopyOnWriteArrayList<(Packet) -> Unit>>()
    
    // Packet queue for packets sent before handshake complete (thread-safe)
    private val packetQueue = java.util.Collections.synchronizedList(mutableListOf<Packet>())
    
    // Last used credentials for reconnection
    private var lastPublicKey: String? = null
    private var lastPrivateHash: String? = null
    private var lastDevice: HandshakeDevice = HandshakeDevice()
    
    // Getters for runtime layers to fetch own profile
    fun getPublicKey(): String? = lastPublicKey
    fun getPrivateHash(): String? = lastPrivateHash
    
    // Heartbeat
    private var heartbeatJob: Job? = null
    @Volatile private var heartbeatPeriodMs: Long = 0L
    @Volatile private var isAppInForeground: Boolean = true
    private var serverHeartbeatIntervalSec: Int = DEFAULT_HEARTBEAT_INTERVAL_SECONDS
    @Volatile private var lastHeartbeatOkLogAtMs: Long = 0L
    @Volatile private var heartbeatOkSuppressedCount: Int = 0
    
    // Supported packets
    private val supportedPackets = mapOf(
        0x00 to { PacketHandshake() },
        0x01 to { PacketUserInfo() },
        0x02 to { PacketResult() },
        0x03 to { PacketSearch() },
        0x04 to { PacketOnlineSubscribe() },
        0x05 to { PacketOnlineState() },
        0x06 to { PacketMessage() },
        0x07 to { PacketRead() },
        0x08 to { PacketDelivery() },
        0x09 to { PacketDeviceNew() },
        0x0A to { PacketRequestUpdate() },
        0x0B to { PacketTyping() },
        0x10 to { PacketPushNotification() },
        0x11 to { PacketCreateGroup() },
        0x12 to { PacketGroupInfo() },
        0x13 to { PacketGroupInviteInfo() },
        0x14 to { PacketGroupJoin() },
        0x15 to { PacketGroupLeave() },
        0x16 to { PacketGroupBan() },
        0x0F to { PacketRequestTransport() },
        0x17 to { PacketDeviceList() },
        0x18 to { PacketDeviceResolve() },
        0x19 to { PacketSync() },
        0x1A to { PacketSignalPeer() },
        0x1B to { PacketWebRTC() },
        0x1C to { PacketIceServers() }
    )
    
    init {
        log("🧩 Protocol init: instance=$instanceId")

        // Register handshake response handler
        waitPacket(0x00) { packet ->
            if (packet is PacketHandshake) {
                enqueueSessionEvent(SessionEvent.HandshakeResponse(packet))
            }
        }

        // Device verification resolution from primary device.
        // Desktop typically continues after next handshake response; here we also
        // add a safety re-handshake trigger on ACCEPT to avoid being stuck in
        // DEVICE_VERIFICATION_REQUIRED if server doesn't immediately push 0x00.
        waitPacket(0x18) { packet ->
            val resolve = packet as? PacketDeviceResolve ?: return@waitPacket
            when (resolve.solution) {
                DeviceResolveSolution.ACCEPT -> {
                    enqueueSessionEvent(
                        SessionEvent.DeviceVerificationAccepted(deviceId = resolve.deviceId)
                    )
                }
                DeviceResolveSolution.DECLINE -> {
                    val stateAtDecline = _state.value
                    log(
                        "⛔ DEVICE VERIFICATION DECLINED (deviceId=${shortKey(resolve.deviceId, 12)}, state=$stateAtDecline)"
                    )

                    // Critical recovery: after DECLINE user may retry login without app restart.
                    // Keep socket session alive when possible, but leave DEVICE_VERIFICATION_REQUIRED
                    // state so next authenticate() is not ignored by startHandshake guards.
                    if (
                        stateAtDecline == ProtocolState.DEVICE_VERIFICATION_REQUIRED ||
                            stateAtDecline == ProtocolState.HANDSHAKING
                    ) {
                        enqueueSessionEvent(
                            SessionEvent.DeviceVerificationDeclined(
                                deviceId = resolve.deviceId,
                                observedState = stateAtDecline
                            )
                        )
                    }
                }
            }
        }
    }
    
    /**
     * Start adaptive heartbeat to keep connection alive.
     * Foreground: serverInterval / 2 (like desktop).
     * Background: 30s to save battery.
     */
    private fun startHeartbeat(intervalSeconds: Int) {
        serverHeartbeatIntervalSec =
            if (intervalSeconds > 0) intervalSeconds else DEFAULT_HEARTBEAT_INTERVAL_SECONDS

        heartbeatJob?.cancel()
        lastHeartbeatOkLogAtMs = 0L
        heartbeatOkSuppressedCount = 0

        heartbeatJob = scope.launch {
            sendHeartbeat()

            while (isActive) {
                val intervalMs = if (isAppInForeground) {
                    ((serverHeartbeatIntervalSec * 1000L) / 2).coerceAtLeast(MIN_HEARTBEAT_SEND_INTERVAL_MS)
                } else {
                    BACKGROUND_HEARTBEAT_INTERVAL_MS
                }
                heartbeatPeriodMs = intervalMs
                delay(intervalMs)
                sendHeartbeat()
            }
        }

        val fgMs = ((serverHeartbeatIntervalSec * 1000L) / 2).coerceAtLeast(MIN_HEARTBEAT_SEND_INTERVAL_MS)
        log(
            "💓 HEARTBEAT START: server=${intervalSeconds}s, " +
                "foreground=${fgMs / 1000}s, background=${BACKGROUND_HEARTBEAT_INTERVAL_MS / 1000}s, " +
                "appForeground=$isAppInForeground, state=${_state.value}"
        )
    }

    /**
     * Notify protocol about app foreground/background state.
     * Adjusts heartbeat interval to save battery in background.
     */
    fun setAppInForeground(foreground: Boolean) {
        if (isAppInForeground == foreground) return
        isAppInForeground = foreground
        log("💓 App foreground=$foreground, heartbeat will adjust on next tick")
    }
    
    /**
     * Отправка heartbeat с проверкой состояния
     */
    private fun sendHeartbeat() {
        try {
            val currentState = _state.value
            val socketAlive = webSocket != null
            
            if (
                currentState == ProtocolState.AUTHENTICATED ||
                    currentState == ProtocolState.DEVICE_VERIFICATION_REQUIRED
            ) {
                val sent = webSocket?.send("heartbeat") ?: false
                if (sent) {
                    val now = System.currentTimeMillis()
                    if (now - lastHeartbeatOkLogAtMs >= HEARTBEAT_OK_LOG_THROTTLE_MS) {
                        val suppressed = heartbeatOkSuppressedCount
                        heartbeatOkSuppressedCount = 0
                        lastHeartbeatOkLogAtMs = now
                        val suffix =
                            if (suppressed > 0) ", +$suppressed suppressed" else ""
                        log("💓 Heartbeat OK (socket=$socketAlive, state=$currentState$suffix)")
                    } else {
                        heartbeatOkSuppressedCount++
                    }
                } else {
                    log("💔 HEARTBEAT FAILED: socket=$socketAlive, state=$currentState, manuallyClosed=$isManuallyClosed")
                    // Триггерим reconnect если heartbeat не прошёл
                    if (!isManuallyClosed) {
                        log("🔄 TRIGGERING RECONNECT due to failed heartbeat")
                        handleDisconnect("heartbeat_failed")
                    }
                }
            } else {
                log(
                    "💔 HEARTBEAT SKIPPED: state=$currentState (not ready), socket=$socketAlive"
                )
            }
        } catch (e: Exception) {
            log("💔 HEARTBEAT EXCEPTION: ${e.message}")
            e.printStackTrace()
        }
    }
    
    /**
     * Initialize connection to server
     */
    fun connect() {
        enqueueSessionEvent(SessionEvent.Connect())
    }

    private fun connectLocked() {
        val currentState = _state.value
        val now = System.currentTimeMillis()
        log("🔌 CONNECT CALLED: currentState=$currentState, reconnectAttempts=$reconnectAttempts, isConnecting=$isConnecting, instance=$instanceId")
        
        // КРИТИЧНО: Если уже подключены и аутентифицированы - не переподключаемся!
        if (
            currentState == ProtocolState.AUTHENTICATED ||
                currentState == ProtocolState.HANDSHAKING ||
                currentState == ProtocolState.DEVICE_VERIFICATION_REQUIRED
        ) {
            log("✅ Already authenticated or handshaking, skipping connect()")
            return
        }
        
        // КРИТИЧНО: Если уже CONNECTED и есть credentials - тоже пропускаем
        if (currentState == ProtocolState.CONNECTED && webSocket != null) {
            log("✅ Already connected with valid socket, skipping connect()")
            return
        }
        
        // КРИТИЧНО: проверяем флаг isConnecting, а не только состояние.
        // Дополнительно защищаемся от "залипшего CONNECTING", который ранее снимался только рестартом приложения.
        if (isConnecting || currentState == ProtocolState.CONNECTING) {
            val elapsed = if (connectingSinceMs > 0L) now - connectingSinceMs else 0L
            if (elapsed in 1 until CONNECTING_STUCK_TIMEOUT_MS) {
                log("⚠️ Already connecting, skipping... (elapsed=${elapsed}ms)")
                return
            }
            log("🧯 CONNECTING STUCK detected (elapsed=${elapsed}ms) -> forcing reconnect reset")
            cancelConnectingTimeout(reason = "connect_stuck_reset")
            isConnecting = false
            connectingSinceMs = 0L
            runCatching { webSocket?.cancel() }
            webSocket = null
            setState(ProtocolState.DISCONNECTED, "Reset stuck CONNECTING (${elapsed}ms)")
        }

        val networkReady = isNetworkAvailable?.invoke() ?: true
        if (!networkReady) {
            log("📡 CONNECT DEFERRED: no active internet network")
            _lastError.value = "No active internet network"
            onNetworkUnavailable?.invoke()
            return
        }
        
        // Отменяем любые запланированные переподключения
        reconnectJob?.cancel()
        reconnectJob = null
        log("🔄 Cancelled any pending reconnect jobs")
        
        // Устанавливаем флаг ПЕРЕД любыми операциями
        isConnecting = true
        connectingSinceMs = now
        
        reconnectAttempts++
        log("📊 RECONNECT ATTEMPT #$reconnectAttempts")
        val generation = rotateConnectionGeneration("connect_attempt_$reconnectAttempts")
        
        // Закрываем старый сокет если есть (как в Архиве)
        webSocket?.let { oldSocket ->
            try {
                oldSocket.close(1000, "Reconnecting")
                log("🔄 Closed old WebSocket before reconnect")
            } catch (e: Exception) {
                log("⚠️ Error closing old socket: ${e.message}")
            }
        }
        webSocket = null
        
        isManuallyClosed = false
        setState(ProtocolState.CONNECTING, "Starting new connection attempt #$reconnectAttempts")
        _lastError.value = null
        armConnectingTimeout(generation)
        
        log("🔌 Connecting to: $serverAddress (attempt #$reconnectAttempts)")
        
        val request = Request.Builder()
            .url(serverAddress)
            .build()
        
        webSocket = client.newWebSocket(request, object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                enqueueSessionEvent(
                    SessionEvent.SocketOpened(
                        generation = generation,
                        socket = webSocket,
                        responseCode = response.code
                    )
                )
            }
            
            override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
                if (isStaleSocketEvent("onMessage(bytes)", generation, webSocket)) return
                log("📥 onMessage called - ${bytes.size} bytes")
                handleMessage(bytes.toByteArray())
            }
            
            override fun onMessage(webSocket: WebSocket, text: String) {
                if (isStaleSocketEvent("onMessage(text)", generation, webSocket)) return
                log("Received text message (unexpected): $text")
            }
            
            override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
                if (isStaleSocketEvent("onClosing", generation, webSocket)) return
                log("⚠️ WebSocket CLOSING: code=$code reason='$reason' state=${_state.value}")
                // Must respond with close() so OkHttp transitions to onClosed.
                // Without this, the socket stays in a half-closed "zombie" state —
                // heartbeat keeps running but no data flows, and handleDisconnect
                // is never called (it only fires from onClosed/onFailure).
                try {
                    webSocket.close(code, reason)
                } catch (e: Exception) {
                    log("⚠️ Error responding to CLOSING: ${e.message}")
                }
            }
            
            override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                enqueueSessionEvent(
                    SessionEvent.SocketClosed(
                        generation = generation,
                        socket = webSocket,
                        code = code,
                        reason = reason
                    )
                )
            }
            
            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                enqueueSessionEvent(
                    SessionEvent.SocketFailure(
                        generation = generation,
                        socket = webSocket,
                        throwable = t,
                        responseCode = response?.code,
                        responseMessage = response?.message
                    )
                )
            }
        })
    }
    
    /**
     * Start handshake with server
     */
    fun startHandshake(publicKey: String, privateHash: String, device: HandshakeDevice) {
        log("🤝 Starting handshake...")
        log("   Public key: ${publicKey.take(20)}...")
        log("   Private hash: ${privateHash.take(20)}...")
        log("   Current state: ${_state.value}")

        // Detect account switch: already authenticated but with different credentials
        val switchingAccount = (
            _state.value == ProtocolState.AUTHENTICATED ||
                _state.value == ProtocolState.HANDSHAKING ||
                _state.value == ProtocolState.DEVICE_VERIFICATION_REQUIRED
            ) &&
                               lastPublicKey != null && lastPublicKey != publicKey

        // Save credentials for reconnection
        lastPublicKey = publicKey
        lastPrivateHash = privateHash
        lastDevice = device

        // If switching accounts, force disconnect and reconnect with new credentials
        if (switchingAccount) {
            log("🔄 Account switch detected, forcing reconnect with new credentials")
            enqueueSessionEvent(
                SessionEvent.AccountSwitchReconnect(reason = "Account switch reconnect")
            )
            return
        }

        // КРИТИЧНО: если уже в handshake или authenticated, не начинаем заново
        if (_state.value == ProtocolState.HANDSHAKING) {
            log("⚠️ HANDSHAKE IGNORED: Already handshaking")
            return
        }

        if (_state.value == ProtocolState.AUTHENTICATED) {
            log("⚠️ HANDSHAKE IGNORED: Already authenticated")
            return
        }

        if (_state.value == ProtocolState.DEVICE_VERIFICATION_REQUIRED) {
            log("⚠️ HANDSHAKE IGNORED: Waiting for device verification")
            return
        }
        
        if (_state.value != ProtocolState.CONNECTED) {
            log("⚠️ HANDSHAKE DEFERRED: Not connected (state=${_state.value}), will handshake after connection")
            connect()
            return
        }
        
        setState(ProtocolState.HANDSHAKING, "Starting handshake")
        handshakeComplete = false
        
        val handshake = PacketHandshake().apply {
            this.publicKey = publicKey
            this.privateKey = privateHash
            this.device = device
        }
        
        sendPacketDirect(handshake)
        
        // Set handshake timeout
        handshakeJob?.cancel()
        handshakeJob = scope.launch {
            delay(HANDSHAKE_TIMEOUT)
            if (!handshakeComplete) {
                log("❌ Handshake timeout")
                _lastError.value = "Handshake timeout"
                disconnect()
            }
        }
    }
    
    /**
     * Send packet to server
     * Packets are queued if handshake is not complete or if connection is down
     * (как в Архиве - сохраняем пакеты при любых проблемах с соединением)
     */
    fun sendPacket(packet: Packet) {
        val currentState = _state.value
        val socket = webSocket
        val socketReady = socket != null
        val authReady = currentState == ProtocolState.AUTHENTICATED
        if (authReady && !handshakeComplete) {
            // Defensive self-heal:
            // AUTHENTICATED state must imply completed handshake.
            // If these flags diverge, message sending can be stuck in queue forever.
            log("⚠️ AUTHENTICATED with handshakeComplete=false -> self-heal handshakeComplete=true")
            handshakeComplete = true
        }
        val preAuthAllowedPacket =
            packet is PacketSignalPeer || packet is PacketWebRTC || packet is PacketIceServers
        val preAuthReady =
            preAuthAllowedPacket &&
                socketReady &&
                (
                    currentState == ProtocolState.CONNECTED ||
                        currentState == ProtocolState.HANDSHAKING ||
                        currentState == ProtocolState.DEVICE_VERIFICATION_REQUIRED ||
                        currentState == ProtocolState.AUTHENTICATED
                )
        val canSendNow =
            packet is PacketHandshake ||
                authReady ||
                preAuthReady

        if (!canSendNow) {
            log(
                "📦 Queueing packet: ${packet.getPacketId()} " +
                    "(handshake=$handshakeComplete, socket=$socketReady, state=$currentState, preAuthAllowed=$preAuthAllowedPacket)"
            )
            packetQueue.add(packet)
            return
        }

        sendPacketDirect(packet)
    }
    
    private fun sendPacketDirect(packet: Packet) {
        val stream = packet.send()
        val data = stream.getStream()

        log("➡️ CLIENT -> SERVER ${describePacket(packet, data.size)}")
        log("   TX_HEX: ${hexPreview(data)}")
        
        val socket = webSocket
        if (socket == null) {
            log("❌ WebSocket is null, re-queueing packet ${packet.getPacketId()}")
            // Как в Архиве - возвращаем пакет в очередь при ошибке
            packetQueue.add(packet)
            return
        }
        
        try {
            val sent = socket.send(ByteString.of(*data))
            if (!sent) {
                log("❌ TX rejected by WebSocket for id=0x${packet.getPacketId().toString(16).uppercase(Locale.ROOT)}")
                log("📦 TX re-queue packet id=0x${packet.getPacketId().toString(16).uppercase(Locale.ROOT)}")
                packetQueue.add(packet)
                return
            }
            log("✅ TX delivered id=0x${packet.getPacketId().toString(16).uppercase(Locale.ROOT)}")
        } catch (e: Exception) {
            log("❌ TX exception id=0x${packet.getPacketId().toString(16).uppercase(Locale.ROOT)} err=${e.message}")
            e.printStackTrace()
            // Как в Архиве - возвращаем пакет в очередь при ошибке отправки
            log("📦 TX re-queue packet id=0x${packet.getPacketId().toString(16).uppercase(Locale.ROOT)} due to send error")
            packetQueue.add(packet)
        }
    }
    
    private fun flushPacketQueue() {
        val packets: List<Packet>
        synchronized(packetQueue) {
            packets = packetQueue.toList()
            packetQueue.clear()
        }
        log("📬 Flushing ${packets.size} queued packets")
        packets.forEach { sendPacket(it) }
    }
    
    private fun handleMessage(data: ByteArray) {
        try {
            log("⬅️ SERVER -> CLIENT rawBytes=${data.size}")
            log("   RX_HEX: ${hexPreview(data)}")

            val stream = Stream(data)
            if (stream.getRemainingBits() < MIN_PACKET_ID_BITS) {
                log("⚠️ Frame too short to contain packet ID (${stream.getRemainingBits()} bits)")
                return
            }
            // Desktop/server parity: one WebSocket frame contains one packet.
            val packetId = stream.readInt16()
            log("📥 Packet ID: $packetId")

            val packetFactory = supportedPackets[packetId]
            if (packetFactory == null) {
                log("⚠️ Unknown packet ID: $packetId")
                return
            }

            val packet = packetFactory()
            try {
                packet.receive(stream)
            } catch (e: Exception) {
                log("❌ Error parsing packet $packetId: ${e.message}")
                e.printStackTrace()
                return
            }

            log("⬅️ SERVER -> CLIENT ${describePacket(packet, data.size)}")
            val remainingBits = stream.getRemainingBits()
            if (remainingBits > 0) {
                log("⚠️ RX parser leftover bits for packet id=0x${packetId.toString(16).uppercase(Locale.ROOT)}: $remainingBits")
            }

            // Notify waiters
            val waitersCount = packetWaiters[packetId]?.size ?: 0
            log("📥 RX dispatch packet id=0x${packetId.toString(16).uppercase(Locale.ROOT)} waiters=$waitersCount")

            packetWaiters[packetId]?.forEach { callback ->
                try {
                    callback(packet)
                } catch (e: Exception) {
                    log("❌ Error in packet handler: ${e.message}")
                    e.printStackTrace()
                }
            }
        } catch (e: Exception) {
            log("❌ Error parsing packet: ${e.message}")
            e.printStackTrace()
        }
    }
    
    private fun handleDisconnect(source: String = "unknown") {
        enqueueSessionEvent(SessionEvent.HandleDisconnect(source))
    }

    private fun handleDisconnectLocked(source: String) {
        val previousState = _state.value
        log(
            "🔌 DISCONNECT HANDLER: source=$source previousState=$previousState, manuallyClosed=$isManuallyClosed, " +
                "reconnectAttempts=$reconnectAttempts, isConnecting=$isConnecting, instance=$instanceId"
        )
        cancelConnectingTimeout(reason = "handle_disconnect:$source")

        // Duplicate callbacks are possible (e.g. heartbeat failure + onFailure/onClosed).
        // If we are already disconnected and a reconnect is pending, avoid scheduling another one.
        if (previousState == ProtocolState.DISCONNECTED && reconnectJob?.isActive == true) {
            log("⚠️ DISCONNECT DUPLICATE: reconnect already scheduled, skipping")
            return
        }
        
        // КРИТИЧНО: если уже идет подключение, не делаем ничего
        if (isConnecting) {
            log("⚠️ DISCONNECT IGNORED: connection already in progress")
            return
        }

        rotateConnectionGeneration("disconnect:$source")
        
        setState(ProtocolState.DISCONNECTED, "Disconnect handler called from $previousState")
        handshakeComplete = false
        handshakeJob?.cancel()
        heartbeatJob?.cancel()
        heartbeatPeriodMs = 0L
        
        if (!isManuallyClosed) {
            val networkReady = isNetworkAvailable?.invoke() ?: true
            if (!networkReady) {
                log("📡 RECONNECT DEFERRED: no active internet network, waiting for callback")
                onNetworkUnavailable?.invoke()
                return
            }
        }

        // Автоматический reconnect с лимитом попыток
        if (!isManuallyClosed) {
            reconnectJob?.cancel()

            val nextAttemptNumber = (reconnectAttempts + 1).coerceAtLeast(1)

            // После 10 попыток — останавливаемся, ждём NetworkCallback или foreground resume
            if (nextAttemptNumber > MAX_RECONNECT_ATTEMPTS) {
                log("🛑 RECONNECT STOPPED: $nextAttemptNumber attempts exhausted, waiting for network change or foreground resume")
                onNetworkUnavailable?.invoke()
                return
            }

            val exponent = (nextAttemptNumber - 1).coerceIn(0, 4)
            val delayMs = minOf(1000L * (1L shl exponent), 30000L)
            log("🔄 SCHEDULING RECONNECT: attempt #$nextAttemptNumber/$MAX_RECONNECT_ATTEMPTS, delay=${delayMs}ms")
            
            reconnectJob = scope.launch {
                delay(delayMs)
                if (!isManuallyClosed && !isConnecting) {
                    log("🔄 EXECUTING RECONNECT after ${delayMs}ms delay")
                    connect()
                } else {
                    log("🔄 RECONNECT CANCELLED: manuallyClosed=$isManuallyClosed, isConnecting=$isConnecting")
                }
            }
        } else {
            log("🔌 No reconnect: connection was manually closed")
        }
    }
    
    /**
     * Register callback for specific packet type
     */
    fun waitPacket(packetId: Int, callback: (Packet) -> Unit) {
        val waiters = packetWaiters.computeIfAbsent(packetId) { CopyOnWriteArrayList() }
        if (waiters.contains(callback)) {
            log(
                "📝 waitPacket(0x${Integer.toHexString(packetId)}) skipped duplicate callback. " +
                    "Total handlers for 0x${Integer.toHexString(packetId)}: ${waiters.size}"
            )
            return
        }
        waiters.add(callback)
        log(
            "📝 waitPacket(0x${Integer.toHexString(packetId)}) registered. " +
                "Total handlers for 0x${Integer.toHexString(packetId)}: ${waiters.size}"
        )
    }
    
    /**
     * Unregister callback for specific packet type
     */
    fun unwaitPacket(packetId: Int, callback: (Packet) -> Unit) {
        val waiters = packetWaiters[packetId] ?: return
        waiters.remove(callback)
        if (waiters.isEmpty()) {
            packetWaiters.remove(packetId, waiters)
        }
    }
    
    /**
     * Disconnect from server
     */
    fun disconnect() {
        enqueueSessionEvent(
            SessionEvent.Disconnect(manual = true, reason = "User disconnected")
        )
    }

    private fun disconnectLocked(manual: Boolean, reason: String) {
        log("🔌 Disconnect requested: manual=$manual reason='$reason' instance=$instanceId")
        isManuallyClosed = manual
        cancelConnectingTimeout(reason = "disconnect_locked")
        isConnecting = false // Сбрасываем флаг
        connectingSinceMs = 0L
        reconnectJob?.cancel() // Отменяем запланированные переподключения
        reconnectJob = null
        handshakeJob?.cancel()
        heartbeatJob?.cancel()
        heartbeatPeriodMs = 0L
        rotateConnectionGeneration("disconnect_locked:${if (manual) "manual" else "internal"}")

        val socket = webSocket
        webSocket = null
        runCatching { socket?.close(1000, reason) }
        setState(ProtocolState.DISCONNECTED, "disconnectLocked(manual=$manual, reason=$reason)")
    }
    
    /**
     * Check if connected and authenticated
     */
    fun isAuthenticated(): Boolean = _state.value == ProtocolState.AUTHENTICATED

    /**
     * Foreground fast reconnect:
     * on app resume we should not wait scheduled exponential backoff.
     */
    fun reconnectNowIfNeeded(reason: String = "foreground") {
        enqueueSessionEvent(SessionEvent.FastReconnect(reason))
    }

    private fun reconnectNowIfNeededLocked(reason: String) {
        val currentState = _state.value
        val hasCredentials = !lastPublicKey.isNullOrBlank() && !lastPrivateHash.isNullOrBlank()
        val now = System.currentTimeMillis()

        log(
            "⚡ FAST RECONNECT CHECK: state=$currentState, hasCredentials=$hasCredentials, isConnecting=$isConnecting, attempts=$reconnectAttempts, reason=$reason"
        )

        // Reset attempt counter — network changed or user returned to app
        reconnectAttempts = 0

        if (isManuallyClosed) {
            log("⚡ FAST RECONNECT SKIP: manually closed, reason=$reason")
            return
        }

        if (!hasCredentials) return

        if (currentState == ProtocolState.CONNECTING && isConnecting) {
            val elapsed = if (connectingSinceMs > 0L) now - connectingSinceMs else 0L
            if (elapsed in 1 until CONNECTING_STUCK_TIMEOUT_MS) {
                return
            }
            log("🧯 FAST RECONNECT: stuck CONNECTING (${elapsed}ms) -> reset and reconnect")
            cancelConnectingTimeout(reason = "fast_reconnect_reset")
            isConnecting = false
            connectingSinceMs = 0L
            runCatching { webSocket?.cancel() }
            webSocket = null
            rotateConnectionGeneration("fast_reconnect_reset:$reason")
            setState(ProtocolState.DISCONNECTED, "Fast reconnect reset stuck CONNECTING")
        } else if (
            currentState == ProtocolState.AUTHENTICATED ||
                currentState == ProtocolState.HANDSHAKING ||
                currentState == ProtocolState.DEVICE_VERIFICATION_REQUIRED ||
                currentState == ProtocolState.CONNECTED
        ) {
            return
        }

        // Reset backoff and connect immediately.
        reconnectAttempts = 0
        reconnectJob?.cancel()
        reconnectJob = null
        connectLocked()
    }

    /**
     * Check if connected (may not be authenticated yet)
     */
    fun isConnected(): Boolean = _state.value == ProtocolState.CONNECTED || 
                                 _state.value == ProtocolState.HANDSHAKING ||
                                 _state.value == ProtocolState.DEVICE_VERIFICATION_REQUIRED ||
                                 _state.value == ProtocolState.AUTHENTICATED
    
    /**
     * Clear saved credentials
     */
    fun clearCredentials() {
        lastPublicKey = null
        lastPrivateHash = null
    }
    
    /**
     * Release resources
     */
    fun destroy() {
        enqueueSessionEvent(
            SessionEvent.Disconnect(manual = true, reason = "Destroy protocol")
        )
        runCatching { sessionEvents.close() }
        runBlocking {
            val drained = withTimeoutOrNull(2_000L) {
                sessionLoopJob.join()
                true
            } ?: false
            if (!drained) {
                sessionLoopJob.cancelAndJoin()
            }
        }
        connectingTimeoutJob?.cancel()
        heartbeatJob?.cancel()
        scope.cancel()
    }
}
