package com.rosetta.messenger.network

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import okhttp3.*
import okio.ByteString
import java.util.concurrent.TimeUnit

/**
 * 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 = {}
) {
    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 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 HEARTBEAT_OK_LOG_THROTTLE_MS = 30_000L
    }
    
    private fun log(message: String) {
        // TEMPORARY: Enable logging for debugging PacketUserInfo
        logger(message)
    }
    
    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 isConnecting = false // Флаг для защиты от одновременных подключений
    
    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    
    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 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, MutableList<(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 ProtocolManager 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 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 {
        // Register handshake response handler
        waitPacket(0x00) { packet ->
            if (packet is 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)
            }
        }
    }
    
    /**
     * Start heartbeat to keep connection alive
     * Как в Архиве - отправляем text "heartbeat" СРАЗУ и потом с интервалом
     */
    private fun startHeartbeat(intervalSeconds: Int) {
        val normalizedServerIntervalSec =
            if (intervalSeconds > 0) intervalSeconds else DEFAULT_HEARTBEAT_INTERVAL_SECONDS
        // Отправляем чаще - каждые 1/3 интервала, но с нижним лимитом чтобы исключить tight-loop.
        val intervalMs =
            ((normalizedServerIntervalSec * 1000L) / 3).coerceAtLeast(MIN_HEARTBEAT_SEND_INTERVAL_MS)

        if (heartbeatJob?.isActive == true && heartbeatPeriodMs == intervalMs) {
            return
        }

        heartbeatJob?.cancel()
        heartbeatPeriodMs = intervalMs
        lastHeartbeatOkLogAtMs = 0L
        heartbeatOkSuppressedCount = 0
        log(
            "💓 HEARTBEAT START: server=${intervalSeconds}s(normalized=${normalizedServerIntervalSec}s), " +
                "sending=${intervalMs / 1000}s, state=${_state.value}"
        )
        
        heartbeatJob = scope.launch {
            // ⚡ СРАЗУ отправляем первый heartbeat (как в Архиве)
            sendHeartbeat()
            
            while (isActive) {
                delay(intervalMs)
                sendHeartbeat()
            }
        }
    }
    
    /**
     * Отправка 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()
                    }
                }
            } 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() {
        val currentState = _state.value
        log("🔌 CONNECT CALLED: currentState=$currentState, reconnectAttempts=$reconnectAttempts, isConnecting=$isConnecting")
        
        // КРИТИЧНО: Если уже подключены и аутентифицированы - не переподключаемся!
        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, а не только состояние
        if (isConnecting || currentState == ProtocolState.CONNECTING) {
            log("⚠️ Already connecting, skipping... (preventing duplicate connect)")
            return
        }
        
        // Отменяем любые запланированные переподключения
        reconnectJob?.cancel()
        reconnectJob = null
        log("🔄 Cancelled any pending reconnect jobs")
        
        // Устанавливаем флаг ПЕРЕД любыми операциями
        isConnecting = true
        
        reconnectAttempts++
        log("📊 RECONNECT 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
        
        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) {
                log("✅ WebSocket OPEN: response=${response.code}, hasCredentials=${lastPublicKey != null}")
                
                // Сбрасываем флаг подключения
                isConnecting = false
                
                setState(ProtocolState.CONNECTED, "WebSocket onOpen callback")
                
                // КРИТИЧНО: проверяем что не идет уже handshake
                if (_state.value != ProtocolState.HANDSHAKING && _state.value != ProtocolState.AUTHENTICATED) {
                    // If we have saved credentials, start handshake automatically
                    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}")
                }
            }
            
            override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
                log("📥 onMessage called - ${bytes.size} bytes")
                handleMessage(bytes.toByteArray())
            }
            
            override fun onMessage(webSocket: WebSocket, text: String) {
                log("Received text message (unexpected): $text")
            }
            
            override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
                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) {
                log("❌ WebSocket CLOSED: code=$code reason='$reason' state=${_state.value} manuallyClosed=$isManuallyClosed")
                isConnecting = false // Сбрасываем флаг
                handleDisconnect()
            }
            
            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                log("❌ WebSocket FAILURE: ${t.message}")
                log("   Response: ${response?.code} ${response?.message}")
                log("   State: ${_state.value}")
                log("   Manually closed: $isManuallyClosed")
                log("   Reconnect attempts: $reconnectAttempts")
                t.printStackTrace()
                isConnecting = false // Сбрасываем флаг
                _lastError.value = t.message
                handleDisconnect()
            }
        })
    }
    
    /**
     * 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")
            disconnect()
            connect() // Will auto-handshake with saved credentials (publicKey, privateHash) on connect
            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 socket = webSocket
        val isConnected = _state.value == ProtocolState.AUTHENTICATED
        
        // Добавляем в очередь если:
        // 1. Handshake не завершён (кроме самого пакета handshake)
        // 2. WebSocket не подключен или null
        // 3. Не authenticated
        if ((!handshakeComplete && packet !is PacketHandshake) || socket == null || !isConnected) {
            log("📦 Queueing packet: ${packet.getPacketId()} (handshake=$handshakeComplete, socket=${socket != null}, state=${_state.value})")
            packetQueue.add(packet)
            return
        }
        
        sendPacketDirect(packet)
    }
    
    private fun sendPacketDirect(packet: Packet) {
        val stream = packet.send()
        val data = stream.getStream()
        
        log("📤 Sending packet: ${packet.getPacketId()} (${data.size} bytes)")
        
        // Debug: log first 50 bytes as hex
        val hexDump = data.take(50).joinToString(" ") { String.format("%02X", it) }
        log("   Hex: $hexDump${if (data.size > 50) "..." else ""}")
        
        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("❌ WebSocket rejected packet ${packet.getPacketId()}, re-queueing")
                packetQueue.add(packet)
                return
            }
            log("✅ Packet ${packet.getPacketId()} sent successfully")
        } catch (e: Exception) {
            log("❌ Exception sending packet ${packet.getPacketId()}: ${e.message}")
            e.printStackTrace()
            // Как в Архиве - возвращаем пакет в очередь при ошибке отправки
            log("📦 Re-queueing packet ${packet.getPacketId()} 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 { sendPacketDirect(it) }
    }
    
    private fun handleMessage(data: ByteArray) {
        try {
            // Debug: log first 50 bytes as hex
            val hexDump = data.take(50).joinToString(" ") { String.format("%02X", it.toInt() and 0xFF) }
            log("📥 Received ${data.size} bytes: $hexDump${if (data.size > 50) "..." else ""}")

            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
            }

            // Notify waiters
            val waitersCount = packetWaiters[packetId]?.size ?: 0
            log("📥 Notifying $waitersCount waiter(s) for packet $packetId")

            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() {
        val previousState = _state.value
        log("🔌 DISCONNECT HANDLER: previousState=$previousState, manuallyClosed=$isManuallyClosed, reconnectAttempts=$reconnectAttempts, isConnecting=$isConnecting")
        
        // КРИТИЧНО: если уже идет подключение, не делаем ничего
        if (isConnecting) {
            log("⚠️ DISCONNECT IGNORED: connection already in progress")
            return
        }
        
        setState(ProtocolState.DISCONNECTED, "Disconnect handler called from $previousState")
        handshakeComplete = false
        handshakeJob?.cancel()
        heartbeatJob?.cancel()
        heartbeatPeriodMs = 0L
        
        // Автоматический reconnect с защитой от бесконечных попыток
        if (!isManuallyClosed) {
            // КРИТИЧНО: отменяем предыдущий reconnect job если есть
            reconnectJob?.cancel()
            
            // Экспоненциальная задержка: 1s, 2s, 4s, 8s, максимум 30s
            val delayMs = minOf(1000L * (1 shl minOf(reconnectAttempts - 1, 4)), 30000L)
            log("🔄 SCHEDULING RECONNECT: attempt #$reconnectAttempts, delay=${delayMs}ms")
            
            if (reconnectAttempts > 20) {
                log("⚠️ WARNING: Too many reconnect attempts ($reconnectAttempts), may be stuck in loop")
            }
            
            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) {
        packetWaiters.getOrPut(packetId) { mutableListOf() }.add(callback)
        val count = packetWaiters[packetId]?.size ?: 0
        log("📝 waitPacket(0x${Integer.toHexString(packetId)}) registered. Total handlers for 0x${Integer.toHexString(packetId)}: $count")
    }
    
    /**
     * Unregister callback for specific packet type
     */
    fun unwaitPacket(packetId: Int, callback: (Packet) -> Unit) {
        packetWaiters[packetId]?.remove(callback)
    }
    
    /**
     * Disconnect from server
     */
    fun disconnect() {
        log("🔌 Manual disconnect requested")
        isManuallyClosed = true
        isConnecting = false // Сбрасываем флаг
        reconnectJob?.cancel() // Отменяем запланированные переподключения
        reconnectJob = null
        handshakeJob?.cancel()
        heartbeatJob?.cancel()
        heartbeatPeriodMs = 0L
        webSocket?.close(1000, "User disconnected")
        webSocket = null
        _state.value = ProtocolState.DISCONNECTED
    }
    
    /**
     * 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") {
        val currentState = _state.value
        val hasCredentials = !lastPublicKey.isNullOrBlank() && !lastPrivateHash.isNullOrBlank()

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

        if (!hasCredentials) return

        if (
            currentState == ProtocolState.AUTHENTICATED ||
                currentState == ProtocolState.HANDSHAKING ||
                currentState == ProtocolState.DEVICE_VERIFICATION_REQUIRED ||
                currentState == ProtocolState.CONNECTED ||
                (currentState == ProtocolState.CONNECTING && isConnecting)
        ) {
            return
        }

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

    /**
     * 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() {
        disconnect()
        heartbeatJob?.cancel()
        scope.cancel()
    }
}
