package com.durian.tssparty.data.repository

import android.util.Base64
import com.durian.tssparty.data.local.AppSettingDao
import com.durian.tssparty.data.local.AppSettingEntity
import com.durian.tssparty.data.local.ShareRecordDao
import com.durian.tssparty.data.local.ShareRecordEntity
import com.durian.tssparty.data.local.TransactionRecordDao
import com.durian.tssparty.data.local.TransactionRecordEntity
import com.durian.tssparty.data.local.TssNativeBridge
import com.durian.tssparty.data.remote.GrpcClient
import com.durian.tssparty.data.remote.GrpcConnectionEvent
import com.durian.tssparty.data.remote.GrpcConnectionState
import com.durian.tssparty.data.remote.IncomingMessage
import com.durian.tssparty.data.remote.JoinSessionData
import com.durian.tssparty.data.remote.SessionEventData
import com.durian.tssparty.domain.model.*
import com.durian.tssparty.util.AddressUtils
import com.durian.tssparty.util.TransactionUtils
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Repository for TSS operations
 */
@Singleton
class TssRepository @Inject constructor(
    private val grpcClient: GrpcClient,
    private val tssNativeBridge: TssNativeBridge,
    private val shareRecordDao: ShareRecordDao,
    private val appSettingDao: AppSettingDao,
    private val transactionRecordDao: TransactionRecordDao
) {
    private val _currentSession = MutableStateFlow<TssSession?>(null)
    val currentSession: StateFlow<TssSession?> = _currentSession.asStateFlow()

    private val _sessionStatus = MutableStateFlow<SessionStatus>(SessionStatus.WAITING)
    val sessionStatus: StateFlow<SessionStatus> = _sessionStatus.asStateFlow()

    // Expose gRPC connection state for UI
    val grpcConnectionState: StateFlow<GrpcConnectionState> = grpcClient.connectionState

    // Expose gRPC connection events for UI notifications
    val grpcConnectionEvents: SharedFlow<GrpcConnectionEvent> = grpcClient.connectionEvents

    // partyId is loaded once from database in registerParty() and cached here
    // This matches Electron's getOrCreatePartyId() pattern
    private lateinit var partyId: String

    // currentSigningPartyId: The partyId to use for the current signing session
    // This may differ from partyId when signing with a restored wallet backup
    // CRITICAL: For backup/restore to work, signing must use the original partyId from keygen
    private var currentSigningPartyId: String? = null

    private var messageCollectionJob: Job? = null
    private var sessionEventJob: Job? = null

    // Pre-registered session ID for event matching (set before joinSession to avoid race condition)
    // This allows session_started events to be matched even if _currentSession is not yet set
    private var pendingSessionId: String? = null

    // Fallback polling job for session status (handles gRPC stream disconnection on Android)
    // Android gRPC streams can disconnect when app goes to background, so we poll as backup
    private var sessionStatusPollingJob: Job? = null

    // Keygen timeout callback (set by ViewModel)
    // Called when 5-minute polling timeout is reached without session starting
    private var keygenTimeoutCallback: ((String) -> Unit)? = null

    // Countdown tick callback (set by ViewModel)
    // Called every second with remaining seconds for UI countdown display
    private var countdownTickCallback: ((Long) -> Unit)? = null

    // Progress callback (set by ViewModel)
    // Called when TSS protocol progress updates (round/totalRounds)
    private var progressCallback: ((Int, Int) -> Unit)? = null

    // Job for collecting progress from native bridge
    private var progressCollectionJob: Job? = null

    // Repository-level CoroutineScope for background tasks
    // Uses SupervisorJob so individual task failures don't cancel other tasks
    private val repositoryScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    companion object {
        // Polling interval for session status check (matching Electron's 2-second interval)
        private const val POLL_INTERVAL_MS = 2000L
        // Maximum wait time for keygen to start after all parties joined (5 minutes, matching Electron)
        private const val MAX_WAIT_MS = 5 * 60 * 1000L
    }

    /**
     * Get the current party ID
     * Note: This will throw UninitializedPropertyAccessException if called before registerParty()
     */
    fun getPartyId(): String {
        if (!::partyId.isInitialized) {
            throw IllegalStateException("partyId not initialized. Call registerParty() first.")
        }
        return partyId
    }

    // Track current message routing params for reconnection recovery
    private var currentMessageRoutingSessionId: String? = null
    private var currentMessageRoutingPartyIndex: Int? = null
    private var currentMessageRoutingPartyId: String? = null  // The partyId used for message routing (may differ from device partyId for restored wallets)

    // Track current session event subscription partyId for reconnection recovery
    // This may differ from device partyId when signing with restored wallets
    private var currentSessionEventPartyId: String? = null

    // Account service URL (configurable via settings)
    private var accountServiceUrl: String = "https://rwaapi.szaiai.com"

    init {
        // Set up reconnection callback to restore streams
        grpcClient.setOnReconnectedCallback {
            android.util.Log.d("TssRepository", ">>> onReconnectedCallback triggered! <<<")
            restoreStreamsAfterReconnect()
        }
    }

    /**
     * Restore message and event streams after gRPC reconnection
     */
    private fun restoreStreamsAfterReconnect() {
        android.util.Log.d("TssRepository", "=== restoreStreamsAfterReconnect() called ===")

        val sessionId = currentMessageRoutingSessionId
        val partyIndex = currentMessageRoutingPartyIndex
        val routingPartyId = currentMessageRoutingPartyId

        android.util.Log.d("TssRepository", "  currentMessageRoutingSessionId: $sessionId")
        android.util.Log.d("TssRepository", "  currentMessageRoutingPartyIndex: $partyIndex")
        android.util.Log.d("TssRepository", "  currentSessionEventPartyId: $currentSessionEventPartyId")
        android.util.Log.d("TssRepository", "  grpcClient.wasEventStreamSubscribed(): ${grpcClient.wasEventStreamSubscribed()}")

        // Restore message routing if we had an active session
        if (sessionId != null && partyIndex != null) {
            android.util.Log.d("TssRepository", "  >>> Restoring message routing for session: $sessionId")
            startMessageRouting(sessionId, partyIndex, routingPartyId)
        } else {
            android.util.Log.d("TssRepository", "  No active message routing to restore")
        }

        // Restore session event subscription with the correct partyId
        if (grpcClient.wasEventStreamSubscribed()) {
            val eventPartyId = currentSessionEventPartyId
            android.util.Log.d("TssRepository", "  >>> Restoring session event subscription with partyId: $eventPartyId")
            startSessionEventSubscription(eventPartyId)
        } else {
            android.util.Log.d("TssRepository", "  No event stream subscription to restore")
        }

        android.util.Log.d("TssRepository", "=== restoreStreamsAfterReconnect() completed ===")
    }

    // HTTP client for API calls
    private val httpClient = okhttp3.OkHttpClient.Builder()
        .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
        .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
        .build()

    /**
     * Update account service URL from settings
     */
    fun setAccountServiceUrl(url: String) {
        accountServiceUrl = url.trimEnd('/')
    }

    /**
     * Connect to the Message Router
     */
    fun connect(host: String, port: Int) {
        grpcClient.connect(host, port)
    }

    /**
     * Disconnect from the server
     */
    fun disconnect() {
        messageCollectionJob?.cancel()
        sessionEventJob?.cancel()
        sessionStatusPollingJob?.cancel()
        grpcClient.disconnect()
    }

    /**
     * Cleanup all resources when the repository is destroyed.
     * Call this when the app is being destroyed to prevent memory leaks.
     */
    fun cleanup() {
        messageCollectionJob?.cancel()
        sessionEventJob?.cancel()
        sessionStatusPollingJob?.cancel()
        repositoryScope.cancel()
        grpcClient.disconnect()
    }

    /**
     * Check if gRPC is connected
     */
    fun isGrpcConnected(): Boolean = grpcClient.isConnected()

    /**
     * Force reconnect to gRPC server
     */
    fun forceReconnect() {
        grpcClient.forceReconnect()
    }

    /**
     * Get current gRPC connection info
     */
    fun getGrpcConnectionInfo(): Pair<String, Int>? = grpcClient.getConnectionInfo()

    // Session event callback (set by ViewModel)
    private var sessionEventCallback: ((SessionEventData) -> Unit)? = null

    // Pending sign info cache (to handle session_started race condition)
    // Stores sign session info before session_started event arrives
    // Pattern matches server-party-co-managed's PendingSessionCache
    private data class PendingSignInfo(
        val sessionId: String,
        val shareId: Long,
        val password: String,
        val messageHash: String
    )
    private var pendingSignInfo: PendingSignInfo? = null

    // Track if signing has already been triggered (prevent double execution)
    private var signingTriggered: Boolean = false

    /**
     * Register this party with the router
     * Also subscribes to session events (matching Electron behavior)
     *
     * This method loads or creates a persistent partyId from the database (matching Electron's
     * getOrCreatePartyId pattern). The partyId is loaded ONCE here and cached in memory.
     */
    suspend fun registerParty(): String {
        android.util.Log.d("TssRepository", "=== registerParty() called ===")

        // Load or create persistent partyId (matching Electron's getOrCreatePartyId)
        val existingPartyId = appSettingDao.getValue("party_id")
        partyId = if (existingPartyId != null) {
            android.util.Log.d("TssRepository", "  Loaded existing partyId: $existingPartyId")
            existingPartyId
        } else {
            val newPartyId = UUID.randomUUID().toString()
            appSettingDao.setValue(AppSettingEntity("party_id", newPartyId))
            android.util.Log.d("TssRepository", "  Generated new partyId: $newPartyId")
            newPartyId
        }

        android.util.Log.d("TssRepository", "  Calling grpcClient.registerParty()...")
        grpcClient.registerParty(partyId, "temporary", "1.0.0")
        android.util.Log.d("TssRepository", "  grpcClient.registerParty() completed")

        // Subscribe to session events immediately after registration (like Electron does)
        android.util.Log.d("TssRepository", "  Calling startSessionEventSubscription()...")
        startSessionEventSubscription()
        android.util.Log.d("TssRepository", "=== registerParty() completed ===")

        return partyId
    }

    /**
     * Start session event subscription (called after registration)
     *
     * @param subscriptionPartyId Optional partyId for subscription. If null, uses device partyId.
     *        CRITICAL: For signing with restored wallets, this should be the original partyId
     *        from keygen (shareEntity.partyId) so that session events are received correctly.
     */
    private fun startSessionEventSubscription(subscriptionPartyId: String? = null) {
        sessionEventJob?.cancel()
        val effectivePartyId = subscriptionPartyId ?: partyId
        // Save for reconnection recovery
        currentSessionEventPartyId = effectivePartyId
        android.util.Log.d("TssRepository", "Starting session event subscription for partyId: $effectivePartyId (device partyId: $partyId)")
        sessionEventJob = repositoryScope.launch {
            grpcClient.subscribeSessionEvents(effectivePartyId)
                .catch { e ->
                    // Log error but don't crash - connection will be restored by GrpcClient
                    android.util.Log.e("TssRepository", "Session event stream error: ${e.message}")
                }
                .collect { event ->
                android.util.Log.d("TssRepository", "=== Session event received ===")
                android.util.Log.d("TssRepository", "  eventType: ${event.eventType}")
                android.util.Log.d("TssRepository", "  sessionId: ${event.sessionId}")
                android.util.Log.d("TssRepository", "  selectedParties: ${event.selectedParties}")

                // Check if this event is for our active session OR pending session
                // (pendingSessionId handles race condition where session_started arrives before _currentSession is set)
                val activeSession = _currentSession.value
                val pendingId = pendingSessionId
                android.util.Log.d("TssRepository", "  activeSession: ${activeSession?.sessionId ?: "null"}")
                android.util.Log.d("TssRepository", "  pendingSessionId: ${pendingId ?: "null"}")

                val matchesActiveSession = activeSession != null && event.sessionId == activeSession.sessionId
                val matchesPendingSession = pendingId != null && event.sessionId == pendingId

                if (matchesActiveSession || matchesPendingSession) {
                    android.util.Log.d("TssRepository", "  → Event matches session! (active=$matchesActiveSession, pending=$matchesPendingSession)")
                    when (event.eventType) {
                        "session_started" -> {
                            android.util.Log.d("TssRepository", "  → Processing session_started event")

                            // CRITICAL: Auto-trigger signing if we have cached pendingSignInfo
                            // This handles race condition where session_started arrives before
                            // MainViewModel's result.fold completes (typically 4ms race window)
                            // Pattern matches server-party-co-managed's auto-participation
                            val signInfo = pendingSignInfo
                            if (signInfo != null && signInfo.sessionId == event.sessionId && !signingTriggered) {
                                android.util.Log.d("TssRepository", "[RACE-FIX] session_started arrived! Auto-triggering signing from TssRepository")
                                android.util.Log.d("TssRepository", "[RACE-FIX] sessionId=${signInfo.sessionId}, shareId=${signInfo.shareId}")

                                signingTriggered = true  // Mark as triggered BEFORE launching coroutine

                                // Launch signing in background (don't block event handler)
                                repositoryScope.launch {
                                    android.util.Log.d("TssRepository", "[RACE-FIX] Calling startSigning from TssRepository...")
                                    val startResult = startSigning(signInfo.sessionId, signInfo.shareId, signInfo.password)

                                    if (startResult.isSuccess) {
                                        android.util.Log.d("TssRepository", "[RACE-FIX] startSigning succeeded, calling waitForSignature...")
                                        val signResult = waitForSignature()
                                        android.util.Log.d("TssRepository", "[RACE-FIX] waitForSignature completed: isSuccess=${signResult.isSuccess}")

                                        // Note: Signature will be available via repository._signature flow
                                        // MainViewModel will pick it up automatically
                                    } else {
                                        android.util.Log.e("TssRepository", "[RACE-FIX] startSigning FAILED: ${startResult.exceptionOrNull()?.message}")
                                    }

                                    // Clean up after signing (whether success or failure)
                                    pendingSignInfo = null
                                }
                            } else {
                                if (signInfo == null) {
                                    android.util.Log.d("TssRepository", "[RACE-FIX] No pendingSignInfo cached, skipping auto-trigger")
                                } else if (signInfo.sessionId != event.sessionId) {
                                    android.util.Log.d("TssRepository", "[RACE-FIX] sessionId mismatch (cached=${signInfo.sessionId}, event=${event.sessionId})")
                                } else if (signingTriggered) {
                                    android.util.Log.d("TssRepository", "[RACE-FIX] Signing already triggered, skipping duplicate")
                                }
                            }

                            // Notify callback (MainViewModel may also try to trigger, but防重入检查 will prevent duplicate)
                            sessionEventCallback?.invoke(event)
                        }
                        "party_joined", "participant_joined" -> {
                            android.util.Log.d("TssRepository", "  → Processing participant_joined event")
                            sessionEventCallback?.invoke(event)
                        }
                        "all_joined" -> {
                            android.util.Log.d("TssRepository", "  → Processing all_joined event")
                            sessionEventCallback?.invoke(event)
                        }
                        else -> {
                            android.util.Log.d("TssRepository", "  → Unknown event type: ${event.eventType}")
                        }
                    }
                } else {
                    android.util.Log.d("TssRepository", "  → Event does NOT match any session (ignored)")
                    if (activeSession == null && pendingId == null) {
                        android.util.Log.d("TssRepository", "    Reason: both activeSession and pendingSessionId are null")
                    } else if (activeSession != null) {
                        android.util.Log.d("TssRepository", "    Reason: sessionId mismatch (event: ${event.sessionId}, active: ${activeSession.sessionId})")
                    }
                }
            }
        }
    }

    /**
     * Ensure session event subscription is active
     * Called before critical operations (like joining sign session) to ensure
     * the event stream hasn't silently disconnected
     *
     * @param signingPartyId Optional partyId for signing operations. If provided,
     *        the subscription will use this partyId instead of the device partyId.
     *        CRITICAL: For signing with restored wallets, this should be the original
     *        partyId from keygen (shareEntity.partyId).
     */
    private suspend fun ensureSessionEventSubscriptionActive(signingPartyId: String? = null) {
        // Check if the session event job is still active
        val isActive = sessionEventJob?.isActive == true
        val effectivePartyId = signingPartyId ?: currentSessionEventPartyId ?: partyId
        android.util.Log.d("TssRepository", "Checking session event subscription: isActive=$isActive, effectivePartyId=$effectivePartyId")

        // If subscribing with a different partyId (imported/restored share),
        // register it with the message-router first to avoid "party not registered" error.
        // Retry up to 3 times to handle transient network issues.
        if (effectivePartyId != partyId) {
            var registered = false
            for (attempt in 1..3) {
                android.util.Log.d("TssRepository", "Registering signing partyId before subscribing: $effectivePartyId (attempt $attempt/3)")
                val regResult = grpcClient.registerSigningParty(effectivePartyId)
                if (regResult.isSuccess) {
                    registered = true
                    break
                }
                android.util.Log.e("TssRepository", "Failed to register signing partyId (attempt $attempt/3): ${regResult.exceptionOrNull()?.message}")
                if (attempt < 3) {
                    kotlinx.coroutines.delay(1000L * attempt)
                }
            }
            if (!registered) {
                throw Exception("无法注册签名方 $effectivePartyId，请检查网络连接后重试")
            }
        }

        if (!isActive) {
            android.util.Log.w("TssRepository", "Session event subscription is not active, restarting...")
            startSessionEventSubscription(signingPartyId)
        } else {
            // Even if the job is "active", the gRPC stream may have silently disconnected
            // Force a restart to ensure we have a fresh connection
            // Also restart if we need to switch to a different partyId for signing
            val needsRestart = signingPartyId != null && signingPartyId != currentSessionEventPartyId
            if (needsRestart) {
                android.util.Log.d("TssRepository", "Switching session event subscription to signingPartyId: $signingPartyId")
            } else {
                android.util.Log.d("TssRepository", "Refreshing session event subscription to ensure fresh connection")
            }
            startSessionEventSubscription(signingPartyId)
        }
    }

    /**
     * Set session event callback (called by ViewModel)
     */
    fun setSessionEventCallback(callback: (SessionEventData) -> Unit) {
        sessionEventCallback = callback
    }

    /**
     * Check if signing has already been triggered (防重入检查)
     * Used by MainViewModel to prevent double execution
     */
    fun isSigningTriggered(): Boolean = signingTriggered

    /**
     * Set keygen timeout callback (called by ViewModel)
     * This callback is invoked when the 5-minute polling timeout is reached
     * without the keygen session starting (matching Electron's checkAndTriggerKeygen timeout)
     */
    fun setKeygenTimeoutCallback(callback: (String) -> Unit) {
        keygenTimeoutCallback = callback
    }

    /**
     * Set countdown tick callback (called by ViewModel)
     * This callback is invoked every second with the remaining seconds
     * for displaying countdown in the UI
     */
    fun setCountdownTickCallback(callback: (Long) -> Unit) {
        countdownTickCallback = callback
    }

    /**
     * Set progress callback (called by ViewModel)
     * This callback is invoked when the TSS protocol progresses through rounds
     * @param callback receives (currentRound, totalRounds)
     */
    fun setProgressCallback(callback: (Int, Int) -> Unit) {
        progressCallback = callback
    }

    /**
     * Start collecting progress from native bridge
     * Called when keygen/sign session starts
     */
    private fun startProgressCollection() {
        // Cancel any existing progress collection
        progressCollectionJob?.cancel()

        android.util.Log.d("TssRepository", "[PROGRESS] Starting progress collection from native bridge")

        progressCollectionJob = repositoryScope.launch {
            tssNativeBridge.progress.collect { (round, totalRounds) ->
                android.util.Log.d("TssRepository", "[PROGRESS] Round $round / $totalRounds")
                withContext(Dispatchers.Main) {
                    progressCallback?.invoke(round, totalRounds)
                }
            }
        }
    }

    /**
     * Stop collecting progress from native bridge
     * Called when session ends or is cancelled
     */
    private fun stopProgressCollection() {
        progressCollectionJob?.cancel()
        progressCollectionJob = null
        android.util.Log.d("TssRepository", "[PROGRESS] Progress collection stopped")
    }

    /**
     * Start polling session status as fallback mechanism
     * This matches Electron's checkAndTriggerKeygen() polling behavior:
     * - Polls every 2 seconds
     * - Times out after 5 minutes
     * - Triggers keygen when session status is "in_progress"
     * - Calls timeout callback if keygen doesn't start within 5 minutes
     *
     * Android needs this because gRPC streams can disconnect when app goes to background,
     * causing session_started events to be missed.
     *
     * @param sessionId The session to poll
     * @param sessionType "keygen" or "sign" - determines which callback to invoke
     */
    fun startSessionStatusPolling(sessionId: String, sessionType: String = "keygen") {
        // Cancel any existing polling job
        sessionStatusPollingJob?.cancel()

        android.util.Log.d("TssRepository", "[POLLING] Starting session status polling for $sessionType session: $sessionId")

        // Notify UI that countdown has started (5 minutes = 300 seconds)
        countdownTickCallback?.invoke(MAX_WAIT_MS / 1000)

        sessionStatusPollingJob = repositoryScope.launch {
            val startTime = System.currentTimeMillis()
            var pollCount = 0
            var lastTickSecond = MAX_WAIT_MS / 1000

            while (isActive && (System.currentTimeMillis() - startTime) < MAX_WAIT_MS) {
                pollCount++
                android.util.Log.d("TssRepository", "[POLLING] Poll #$pollCount for session: $sessionId")

                // Calculate and emit remaining seconds for countdown display
                val elapsedMs = System.currentTimeMillis() - startTime
                val remainingSeconds = (MAX_WAIT_MS - elapsedMs) / 1000
                if (remainingSeconds != lastTickSecond) {
                    lastTickSecond = remainingSeconds
                    withContext(Dispatchers.Main) {
                        countdownTickCallback?.invoke(remainingSeconds)
                    }
                }

                try {
                    val statusResult = getSessionStatus(sessionId)

                    statusResult.fold(
                        onSuccess = { status ->
                            android.util.Log.d("TssRepository", "[POLLING] Session status: ${status.status}, completed: ${status.completedParties}/${status.totalParties}")

                            // Check if session is in_progress (all parties joined and ready)
                            if (status.status == "in_progress") {
                                android.util.Log.d("TssRepository", "[POLLING] Session is in_progress, triggering $sessionType via callback")

                                // Clear countdown when session starts
                                withContext(Dispatchers.Main) {
                                    countdownTickCallback?.invoke(-1L)  // -1 indicates countdown stopped (success)
                                }

                                // Create synthetic session_started event to trigger keygen/sign
                                // This matches Electron's behavior of checking status and triggering manually
                                val eventData = SessionEventData(
                                    eventId = "polling_trigger_${System.currentTimeMillis()}",
                                    eventType = "session_started",
                                    sessionId = sessionId,
                                    thresholdN = status.thresholdN,
                                    thresholdT = status.thresholdT,
                                    selectedParties = status.participants.map { it.partyId },
                                    joinTokens = emptyMap(),
                                    messageHash = null
                                )

                                // Invoke the session event callback on main thread
                                try {
                                    withContext(Dispatchers.Main) {
                                        android.util.Log.d("TssRepository", "[IDLE_CRASH_DEBUG] Invoking sessionEventCallback on main thread")
                                        sessionEventCallback?.invoke(eventData)
                                        android.util.Log.d("TssRepository", "[IDLE_CRASH_DEBUG] sessionEventCallback completed")
                                    }
                                } catch (e: Exception) {
                                    android.util.Log.e("TssRepository", "[IDLE_CRASH_DEBUG] Exception in sessionEventCallback: ${e.message}")
                                    android.util.Log.e("TssRepository", "[IDLE_CRASH_DEBUG] Stack: ${e.stackTraceToString()}")
                                }

                                // Stop polling after triggering
                                android.util.Log.d("TssRepository", "[POLLING] Triggered $sessionType, stopping polling")
                                return@launch
                            }

                            // Check if session failed or was cancelled
                            if (status.status == "failed" || status.status == "cancelled") {
                                android.util.Log.d("TssRepository", "[POLLING] Session ${status.status}, stopping polling")
                                withContext(Dispatchers.Main) {
                                    countdownTickCallback?.invoke(-1L)  // Clear countdown
                                }
                                return@launch
                            }

                            // Check if session already completed (keygen finished)
                            if (status.status == "completed") {
                                android.util.Log.d("TssRepository", "[POLLING] Session completed, stopping polling")
                                withContext(Dispatchers.Main) {
                                    countdownTickCallback?.invoke(-1L)  // Clear countdown
                                }
                                return@launch
                            }
                        },
                        onFailure = { e ->
                            android.util.Log.w("TssRepository", "[POLLING] Failed to get session status: ${e.message}")
                            // Continue polling on failure (network hiccup, etc.)
                        }
                    )
                } catch (e: Exception) {
                    android.util.Log.w("TssRepository", "[POLLING] Exception during polling: ${e.message}")
                }

                // Wait before next poll
                delay(POLL_INTERVAL_MS)
            }

            // Timeout reached - keygen didn't start within 5 minutes
            if (isActive) {
                val elapsedSeconds = (System.currentTimeMillis() - startTime) / 1000
                android.util.Log.e("TssRepository", "[IDLE_CRASH_DEBUG] [POLLING] Timeout after ${elapsedSeconds}s waiting for $sessionType to start")

                try {
                    withContext(Dispatchers.Main) {
                        android.util.Log.d("TssRepository", "[IDLE_CRASH_DEBUG] Invoking timeout callbacks on main thread")
                        countdownTickCallback?.invoke(0L)  // Countdown reached zero
                        android.util.Log.d("TssRepository", "[IDLE_CRASH_DEBUG] countdownTickCallback completed")
                        keygenTimeoutCallback?.invoke("等待 $sessionType 启动超时（5分钟）")
                        android.util.Log.d("TssRepository", "[IDLE_CRASH_DEBUG] keygenTimeoutCallback completed")
                    }
                } catch (e: Exception) {
                    android.util.Log.e("TssRepository", "[IDLE_CRASH_DEBUG] Exception in timeout callback: ${e.message}")
                    android.util.Log.e("TssRepository", "[IDLE_CRASH_DEBUG] Stack: ${e.stackTraceToString()}")
                }
            } else {
                android.util.Log.d("TssRepository", "[IDLE_CRASH_DEBUG] Polling job cancelled before timeout")
            }
        }
    }

    /**
     * Stop session status polling
     * Called when:
     * - Session starts successfully (keygen/sign triggered)
     * - Session is cancelled
     * - User navigates away
     */
    fun stopSessionStatusPolling() {
        sessionStatusPollingJob?.cancel()
        sessionStatusPollingJob = null
        // Clear countdown display when polling is stopped
        countdownTickCallback?.invoke(-1L)
        android.util.Log.d("TssRepository", "[POLLING] Session status polling stopped")
    }

    /**
     * Get share count for startup check
     */
    suspend fun getShareCount(): Int {
        return shareRecordDao.getShareCount()
    }

    /**
     * Check Kava blockchain health
     */
    suspend fun checkKavaHealth(): Boolean {
        // TODO: Implement actual Kava RPC health check
        return true
    }

    /**
     * Create a new keygen session (as initiator)
     * Calls account-service API: POST /api/v1/co-managed/sessions
     * Then auto-joins the session via gRPC (matching Electron behavior)
     */
    suspend fun createKeygenSession(
        walletName: String,
        thresholdT: Int,
        thresholdN: Int,
        participantName: String
    ): Result<CreateKeygenSessionResult> {
        // Ensure session event subscription is active before creating keygen session
        // This handles the case where a previous keygen might have left the subscription inactive
        ensureSessionEventSubscriptionActive()

        return withContext(Dispatchers.IO) {
            try {
                val jsonMediaType = "application/json; charset=utf-8".toMediaType()

                // Calculate persistent and external counts (matching Electron)
                // persistent = n - t (platform backup parties)
                // external = t (user-held parties)
                val persistentCount = thresholdN - thresholdT
                val externalCount = thresholdT

                // Build request body matching account-service API (same as Electron)
                val requestBody = com.google.gson.JsonObject().apply {
                    addProperty("wallet_name", walletName)
                    addProperty("threshold_t", thresholdT)
                    addProperty("threshold_n", thresholdN)
                    addProperty("initiator_party_id", partyId)
                    addProperty("initiator_name", participantName)
                    addProperty("persistent_count", persistentCount)
                    addProperty("external_count", externalCount)
                    addProperty("expires_in_seconds", 86400) // 24 hours, same as Electron
                }.toString()

                val request = okhttp3.Request.Builder()
                    .url("$accountServiceUrl/api/v1/co-managed/sessions")
                    .post(requestBody.toRequestBody(jsonMediaType))
                    .header("Content-Type", "application/json")
                    .build()

                android.util.Log.d("TssRepository", "Creating keygen session: $requestBody")

                val response = httpClient.newCall(request).execute()
                val responseBody = response.body?.string()
                    ?: return@withContext Result.failure(Exception("空响应"))

                android.util.Log.d("TssRepository", "Create session response: $responseBody")

                if (!response.isSuccessful) {
                    val errorJson = try {
                        com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                    } catch (e: Exception) { null }
                    val errorMsg = errorJson?.get("message")?.asString
                        ?: errorJson?.get("error")?.asString
                        ?: "HTTP ${response.code}"
                    return@withContext Result.failure(Exception(errorMsg))
                }

                // Parse response
                val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                val sessionId = json.get("session_id").asString
                val inviteCode = json.get("invite_code").asString

                // Get join token - prioritize join_tokens map with partyId, then wildcard, then single join_token
                val joinTokensObj = json.get("join_tokens")?.asJsonObject
                val joinToken = joinTokensObj?.get(partyId)?.asString
                    ?: joinTokensObj?.get("*")?.asString
                    ?: json.get("join_token")?.asString
                    ?: ""

                android.util.Log.d("TssRepository", "Session created: sessionId=$sessionId, inviteCode=$inviteCode, joinToken length=${joinToken.length}")

                // CRITICAL: Set pendingSessionId BEFORE joinSession to avoid race condition
                // This ensures session_started events can be matched even if they arrive
                // before _currentSession is set
                pendingSessionId = sessionId
                android.util.Log.d("TssRepository", "Set pendingSessionId=$sessionId for event matching")

                // Auto-join session via gRPC (matching Electron behavior)
                var partyIndex = 0
                var sessionAlreadyInProgress = false
                if (joinToken.isNotEmpty()) {
                    android.util.Log.d("TssRepository", "Initiator auto-joining session via gRPC...")
                    val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken)

                    if (joinResult.isSuccess) {
                        val joinData = joinResult.getOrThrow()
                        partyIndex = joinData.partyIndex

                        android.util.Log.d("TssRepository", "Initiator joined session: partyIndex=$partyIndex, sessionStatus=${joinData.sessionStatus}")

                        // Update session state
                        val session = TssSession(
                            sessionId = sessionId,
                            sessionType = SessionType.KEYGEN,
                            thresholdT = thresholdT,
                            thresholdN = thresholdN,
                            participants = listOf(Participant(partyId, partyIndex, participantName)),
                            status = SessionStatus.WAITING,
                            inviteCode = inviteCode
                        )
                        _currentSession.value = session
                        _sessionStatus.value = SessionStatus.WAITING

                        // Start message subscription for this session
                        startMessageRouting(sessionId, partyIndex)

                        // Check if session is already in_progress (Solution B from Electron)
                        // This handles the case where all parties join before we check
                        if (joinData.sessionStatus == "in_progress") {
                            android.util.Log.d("TssRepository", "Session already in_progress, will trigger keygen immediately")
                            sessionAlreadyInProgress = true
                        }
                    } else {
                        android.util.Log.w("TssRepository", "Initiator failed to join session via gRPC: ${joinResult.exceptionOrNull()?.message}")
                    }
                } else {
                    android.util.Log.w("TssRepository", "No join token found for initiator partyId: $partyId")
                }

                // If session is already in_progress, notify callback immediately
                if (sessionAlreadyInProgress) {
                    val eventData = SessionEventData(
                        eventId = "immediate_start",
                        eventType = "session_started",
                        sessionId = sessionId,
                        thresholdN = thresholdN,
                        thresholdT = thresholdT,
                        selectedParties = listOf(partyId),
                        joinTokens = emptyMap(),
                        messageHash = null
                    )
                    sessionEventCallback?.invoke(eventData)
                }

                Result.success(CreateKeygenSessionResult(
                    sessionId = sessionId,
                    inviteCode = inviteCode,
                    partyIndex = partyIndex,
                    walletName = walletName,
                    thresholdT = thresholdT,
                    thresholdN = thresholdN
                ))
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "Create keygen session failed", e)
                Result.failure(e)
            }
        }
    }

    /**
     * Get all stored shares
     */
    fun getAllShares(): Flow<List<ShareRecord>> {
        return shareRecordDao.getAllShares().map { entities ->
            entities.map { it.toShareRecord() }
        }
    }

    /**
     * Get session status with full participant list
     * Calls account-service API: GET /api/v1/co-managed/sessions/{sessionId}
     * This is critical for getting the complete participant list before starting keygen
     */
    suspend fun getSessionStatus(sessionId: String): Result<SessionStatusResponse> {
        return withContext(Dispatchers.IO) {
            try {
                val request = okhttp3.Request.Builder()
                    .url("$accountServiceUrl/api/v1/co-managed/sessions/$sessionId")
                    .get()
                    .build()

                android.util.Log.d("TssRepository", "Getting session status: $sessionId")

                val response = httpClient.newCall(request).execute()
                val responseBody = response.body?.string()
                    ?: return@withContext Result.failure(Exception("空响应"))

                android.util.Log.d("TssRepository", "Session status response: $responseBody")

                if (!response.isSuccessful) {
                    val errorJson = try {
                        com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                    } catch (e: Exception) { null }
                    val errorMsg = errorJson?.get("message")?.asString
                        ?: errorJson?.get("error")?.asString
                        ?: "HTTP ${response.code}"
                    return@withContext Result.failure(Exception(errorMsg))
                }

                // Parse response
                val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                val status = json.get("status")?.asString ?: ""
                val thresholdT = json.get("threshold_t")?.asInt ?: 2
                val thresholdN = json.get("threshold_n")?.asInt ?: json.get("total_parties")?.asInt ?: 3
                val completedParties = json.get("completed_parties")?.asInt ?: 0
                val totalParties = json.get("total_parties")?.asInt ?: thresholdN
                val sessionType = json.get("session_type")?.asString ?: ""

                // Parse participants array with party_index
                val participantsArray = json.get("participants")?.asJsonArray
                val participants = mutableListOf<ParticipantStatusInfo>()
                participantsArray?.forEach { elem ->
                    val p = elem.asJsonObject
                    participants.add(ParticipantStatusInfo(
                        partyId = p.get("party_id")?.asString ?: "",
                        partyIndex = p.get("party_index")?.asInt ?: 0,
                        status = p.get("status")?.asString ?: ""
                    ))
                }

                android.util.Log.d("TssRepository", "Session status: status=$status, participants=${participants.size}, threshold=$thresholdT-of-$thresholdN")

                Result.success(SessionStatusResponse(
                    sessionId = sessionId,
                    status = status,
                    thresholdT = thresholdT,
                    thresholdN = thresholdN,
                    completedParties = completedParties,
                    totalParties = totalParties,
                    sessionType = sessionType,
                    participants = participants
                ))
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "Get session status failed", e)
                Result.failure(e)
            }
        }
    }

    /**
     * Validate an invite code and get session info
     * Calls account-service API: GET /api/v1/co-managed/sessions/by-invite-code/{inviteCode}
     */
    suspend fun validateInviteCode(inviteCode: String): Result<ValidateInviteCodeResult> {
        return withContext(Dispatchers.IO) {
            try {
                val request = okhttp3.Request.Builder()
                    .url("$accountServiceUrl/api/v1/co-managed/sessions/by-invite-code/$inviteCode")
                    .get()
                    .build()

                android.util.Log.d("TssRepository", "Validating invite code: $inviteCode")

                val response = httpClient.newCall(request).execute()
                val responseBody = response.body?.string()
                    ?: return@withContext Result.failure(Exception("空响应"))

                android.util.Log.d("TssRepository", "Validate response: $responseBody")

                if (!response.isSuccessful) {
                    val errorJson = try {
                        com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                    } catch (e: Exception) { null }
                    val errorMsg = errorJson?.get("message")?.asString
                        ?: errorJson?.get("error")?.asString
                        ?: "HTTP ${response.code}"
                    return@withContext Result.failure(Exception(errorMsg))
                }

                // Parse response
                val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                val sessionId = json.get("session_id").asString
                val walletName = json.get("wallet_name")?.asString ?: ""
                val thresholdT = json.get("threshold_t")?.asInt ?: 2
                val thresholdN = json.get("threshold_n")?.asInt ?: 3
                val joinedParties = json.get("joined_parties")?.asInt ?: 0
                val joinToken = json.get("join_token")?.asString ?: ""

                val sessionInfo = SessionInfoResponse(
                    sessionId = sessionId,
                    walletName = walletName,
                    thresholdT = thresholdT,
                    thresholdN = thresholdN,
                    initiator = "发起者", // API may not return this
                    currentParticipants = joinedParties,
                    totalParticipants = thresholdN
                )

                Result.success(ValidateInviteCodeResult(
                    sessionInfo = sessionInfo,
                    joinToken = joinToken
                ))
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "Validate invite code failed", e)
                Result.failure(e)
            }
        }
    }

    /**
     * Validate a sign session invite code and get session info
     * Calls account-service API: GET /api/v1/co-managed/sign/by-invite-code/{inviteCode}
     */
    suspend fun validateSignInviteCode(inviteCode: String): Result<ValidateSignInviteCodeResult> {
        return withContext(Dispatchers.IO) {
            try {
                val request = okhttp3.Request.Builder()
                    .url("$accountServiceUrl/api/v1/co-managed/sign/by-invite-code/$inviteCode")
                    .get()
                    .build()

                android.util.Log.d("TssRepository", "Validating sign invite code: $inviteCode")

                val response = httpClient.newCall(request).execute()
                val responseBody = response.body?.string()
                    ?: return@withContext Result.failure(Exception("空响应"))

                android.util.Log.d("TssRepository", "Validate sign response: $responseBody")

                if (!response.isSuccessful) {
                    val errorJson = try {
                        com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                    } catch (e: Exception) { null }
                    val errorMsg = errorJson?.get("message")?.asString
                        ?: errorJson?.get("error")?.asString
                        ?: "HTTP ${response.code}"
                    return@withContext Result.failure(Exception(errorMsg))
                }

                // Parse response
                val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                val sessionId = json.get("session_id").asString
                val keygenSessionId = json.get("keygen_session_id")?.asString ?: ""
                val walletName = json.get("wallet_name")?.asString ?: ""
                val messageHash = json.get("message_hash")?.asString ?: ""
                val thresholdT = json.get("threshold_t")?.asInt ?: 2
                val thresholdN = json.get("threshold_n")?.asInt ?: 3
                val joinedCount = json.get("joined_count")?.asInt ?: 0
                val joinToken = json.get("join_token")?.asString ?: ""

                // Parse parties array (matching Electron's sessionInfo.parties)
                val partiesArray = json.get("parties")?.asJsonArray
                val parties = mutableListOf<Participant>()
                partiesArray?.forEach { elem ->
                    val p = elem.asJsonObject
                    parties.add(Participant(
                        partyId = p.get("party_id")?.asString ?: "",
                        partyIndex = p.get("party_index")?.asInt ?: 0,
                        name = "参与方 ${(p.get("party_index")?.asInt ?: 0) + 1}"
                    ))
                }

                android.util.Log.d("TssRepository", "[CO-SIGN] validateSignInviteCode: found ${parties.size} parties")

                val signSessionInfo = SignSessionInfoResponse(
                    sessionId = sessionId,
                    keygenSessionId = keygenSessionId,
                    walletName = walletName,
                    messageHash = messageHash,
                    thresholdT = thresholdT,
                    thresholdN = thresholdN,
                    currentParticipants = joinedCount,
                    parties = parties
                )

                Result.success(ValidateSignInviteCodeResult(
                    signSessionInfo = signSessionInfo,
                    joinToken = joinToken
                ))
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "Validate sign invite code failed", e)
                Result.failure(e)
            }
        }
    }

    /**
     * Join a keygen session via gRPC only (matching Electron's grpc:joinSession)
     * This does NOT start keygen - that happens when session_started event is received
     *
     * Flow (matching Electron):
     * 1. Call grpcClient.joinSession(sessionId, partyId, joinToken)
     * 2. Set up activeKeygenSession state
     * 3. Prepare for keygen (subscribe to messages)
     * 4. Return - keygen will start when session_started event is received
     */
    suspend fun joinKeygenSessionViaGrpc(
        sessionId: String,
        joinToken: String,
        walletName: String,
        thresholdT: Int,
        thresholdN: Int
    ): Result<JoinKeygenViaGrpcResult> {
        return withContext(Dispatchers.IO) {
            try {
                android.util.Log.d("TssRepository", "Joining keygen session via gRPC: sessionId=$sessionId, joinToken length=${joinToken.length}")

                // CRITICAL: Set pendingSessionId BEFORE joinSession to avoid race condition
                // This ensures session_started events can be matched even if they arrive
                // before _currentSession is set
                pendingSessionId = sessionId
                android.util.Log.d("TssRepository", "Set pendingSessionId=$sessionId for event matching (joiner)")

                // Join session via gRPC (matching Electron's grpcClient.joinSession)
                val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken)
                if (joinResult.isFailure) {
                    android.util.Log.e("TssRepository", "gRPC join failed", joinResult.exceptionOrNull())
                    return@withContext Result.failure(joinResult.exceptionOrNull()!!)
                }

                val sessionData = joinResult.getOrThrow()
                val myPartyIndex = sessionData.partyIndex

                android.util.Log.d("TssRepository", "gRPC join successful: partyIndex=$myPartyIndex, sessionStatus=${sessionData.sessionStatus}")

                // Build participants list (matching Electron's logic)
                val participants = sessionData.participants.toMutableList()
                participants.add(Participant(partyId, myPartyIndex, "我"))
                participants.sortBy { it.partyIndex }

                // Set up active keygen session state (matching Electron's activeKeygenSession)
                val session = TssSession(
                    sessionId = sessionId,
                    sessionType = SessionType.KEYGEN,
                    thresholdT = thresholdT,
                    thresholdN = thresholdN,
                    participants = participants,
                    status = SessionStatus.WAITING,
                    inviteCode = null
                )
                _currentSession.value = session
                _sessionStatus.value = SessionStatus.WAITING

                // Start message subscription for this session (matching Electron's prepareForKeygen)
                startMessageRouting(sessionId, myPartyIndex)

                android.util.Log.d("TssRepository", "Session state set, waiting for session_started event")

                Result.success(JoinKeygenViaGrpcResult(
                    partyIndex = myPartyIndex,
                    sessionStatus = sessionData.sessionStatus
                ))
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "Join keygen session via gRPC failed", e)
                Result.failure(e)
            }
        }
    }

    /**
     * Execute keygen as joiner (called when session_started event is received)
     * This is the second part of the join flow, triggered by session_started event
     */
    suspend fun executeKeygenAsJoiner(
        sessionId: String,
        partyIndex: Int,
        thresholdT: Int,
        thresholdN: Int,
        password: String
    ): Result<ShareRecord> = coroutineScope {
        try {
            val session = _currentSession.value
            if (session == null || session.sessionId != sessionId) {
                return@coroutineScope Result.failure(Exception("No active session or session mismatch"))
            }

            android.util.Log.d("TssRepository", "Executing keygen as joiner: sessionId=$sessionId, partyIndex=$partyIndex")

            // CRITICAL: Fetch complete participant list from backend before starting keygen
            // This ensures we have all participants even if some joined after we did
            val statusResult = getSessionStatus(sessionId)
            if (statusResult.isFailure) {
                android.util.Log.e("TssRepository", "Failed to get session status before keygen", statusResult.exceptionOrNull())
                return@coroutineScope Result.failure(statusResult.exceptionOrNull()!!)
            }

            val sessionStatus = statusResult.getOrThrow()

            // Build complete participant list from backend response
            val updatedParticipants = mutableListOf<Participant>()
            for (p in sessionStatus.participants) {
                val name = if (p.partyId == partyId) {
                    session.participants.find { it.partyId == partyId }?.name ?: "我"
                } else {
                    session.participants.find { it.partyId == p.partyId }?.name ?: "参与方 ${p.partyIndex + 1}"
                }
                updatedParticipants.add(Participant(p.partyId, p.partyIndex, name))
            }

            // Find my party index from the updated list (may differ from initial partyIndex if backend assigned differently)
            val myInfo = updatedParticipants.find { it.partyId == partyId }
            val actualPartyIndex = myInfo?.partyIndex ?: partyIndex

            android.util.Log.d("TssRepository", "Joiner updated participants: ${updatedParticipants.map { "${it.partyId.take(8)}:${it.partyIndex}" }}")
            android.util.Log.d("TssRepository", "My actual party index: $actualPartyIndex (original: $partyIndex)")

            // Update the session with complete participant list
            _currentSession.value = session.copy(
                participants = updatedParticipants,
                thresholdT = sessionStatus.thresholdT,
                thresholdN = sessionStatus.thresholdN
            )

            // Use thresholds from backend (they may be more accurate)
            val actualThresholdT = sessionStatus.thresholdT
            val actualThresholdN = sessionStatus.thresholdN

            // Start TSS keygen with complete participant list
            val startResult = tssNativeBridge.startKeygen(
                sessionId = sessionId,
                partyId = partyId,
                partyIndex = actualPartyIndex,
                thresholdT = actualThresholdT,
                thresholdN = actualThresholdN,
                participants = updatedParticipants,
                password = password
            )

            if (startResult.isFailure) {
                return@coroutineScope Result.failure(startResult.exceptionOrNull()!!)
            }

            // Start collecting progress from native bridge
            startProgressCollection()

            _sessionStatus.value = SessionStatus.IN_PROGRESS

            // Mark ready
            grpcClient.markPartyReady(sessionId, partyId)

            // Wait for keygen result
            val keygenResult = tssNativeBridge.waitForKeygenResult(password)
            if (keygenResult.isFailure) {
                stopProgressCollection()
                _sessionStatus.value = SessionStatus.FAILED
                return@coroutineScope Result.failure(keygenResult.exceptionOrNull()!!)
            }

            val result = keygenResult.getOrThrow()

            // Derive address from public key
            val publicKeyBytes = Base64.decode(result.publicKey, Base64.NO_WRAP)
            val address = AddressUtils.deriveEvmAddress(publicKeyBytes)

            // Save share record (use actual thresholds and party index from backend)
            // CRITICAL: Save partyId - this is required for signing after backup/restore
            val shareEntity = ShareRecordEntity(
                sessionId = sessionId,
                publicKey = result.publicKey,
                encryptedShare = result.encryptedShare,
                thresholdT = actualThresholdT,
                thresholdN = actualThresholdN,
                partyIndex = actualPartyIndex,
                partyId = partyId,
                address = address
            )
            val id = shareRecordDao.insertShare(shareEntity)

            // Report completion
            grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes)

            stopProgressCollection()
            _sessionStatus.value = SessionStatus.COMPLETED
            pendingSessionId = null  // Clear pending session ID on completion

            android.util.Log.d("TssRepository", "Keygen as joiner completed: address=$address, partyIndex=$actualPartyIndex")

            Result.success(shareEntity.copy(id = id).toShareRecord())

        } catch (e: Exception) {
            android.util.Log.e("TssRepository", "Execute keygen as joiner failed", e)
            stopProgressCollection()
            _sessionStatus.value = SessionStatus.FAILED
            pendingSessionId = null  // Clear pending session ID on failure
            Result.failure(e)
        }
    }

    /**
     * Join a sign session via gRPC only (matching Electron's cosign:joinSession flow)
     * This does NOT start signing - that happens when session_started event is received
     *
     * Flow (matching Electron):
     * 1. Validate share exists and password is correct
     * 2. Call grpcClient.joinSession(sessionId, partyId, joinToken)
     * 3. Set up activeCoSignSession state
     * 4. Start message subscription (prepareForSign)
     * 5. If session is already in_progress, return that status so caller can trigger sign
     * 6. Otherwise return - sign will start when session_started event is received
     */
    suspend fun joinSignSessionViaGrpc(
        sessionId: String,
        joinToken: String,
        shareId: Long,
        password: String,
        walletName: String,
        messageHash: String,
        thresholdT: Int,
        thresholdN: Int,
        parties: List<Participant>
    ): Result<JoinSignViaGrpcResult> {
        return withContext(Dispatchers.IO) {
            try {
                android.util.Log.d("TssRepository", "Joining sign session via gRPC: sessionId=$sessionId")

                // Validate share exists (matching Electron)
                val shareEntity = shareRecordDao.getShareById(shareId)
                    ?: return@withContext Result.failure(Exception("Share not found"))

                // Note: Password is verified during actual sign execution, same as Electron

                // CRITICAL: Use the original partyId from the share (keygen time) for signing
                // This is essential for backup/restore - the partyId must match what was used during keygen
                // If shareEntity.partyId is empty (legacy data), fall back to current device's partyId
                val signingPartyId = if (shareEntity.partyId.isNotEmpty()) {
                    shareEntity.partyId
                } else {
                    android.util.Log.w("TssRepository", "Share has no partyId (legacy data), using current device partyId")
                    partyId
                }
                currentSigningPartyId = signingPartyId  // Save for later use in this flow
                android.util.Log.d("TssRepository", "Using signingPartyId=$signingPartyId (current device partyId=$partyId)")

                // CRITICAL: Set pendingSessionId BEFORE joinSession to avoid race condition
                // This ensures session_started events can be matched even if they arrive
                // before _currentSession is set
                pendingSessionId = sessionId
                android.util.Log.d("TssRepository", "Set pendingSessionId=$sessionId for event matching (sign joiner)")

                // Join session via gRPC using the original partyId from keygen (CRITICAL for backup/restore)
                val joinResult = grpcClient.joinSession(sessionId, signingPartyId, joinToken)
                if (joinResult.isFailure) {
                    android.util.Log.e("TssRepository", "gRPC sign join failed", joinResult.exceptionOrNull())
                    return@withContext Result.failure(joinResult.exceptionOrNull()!!)
                }

                val sessionData = joinResult.getOrThrow()
                // Use party_index from gRPC response (matching Electron's result.party_index)
                // This is more authoritative than local share's partyIndex
                val myPartyIndex = sessionData.partyIndex

                android.util.Log.d("TssRepository", "gRPC sign join successful: partyIndex=$myPartyIndex (from gRPC), localPartyIndex=${shareEntity.partyIndex}, sessionStatus=${sessionData.sessionStatus}")

                // Build participants list (matching Electron's logic)
                // Prefer using parties from validateInviteCode (complete list)
                // CRITICAL: Use signingPartyId (original partyId from keygen) for participant identification
                val participants = if (parties.isNotEmpty()) {
                    parties.toMutableList()
                } else {
                    // Fallback: use other_parties + self
                    val list = sessionData.participants.toMutableList()
                    list.add(Participant(signingPartyId, myPartyIndex, "我"))
                    list.sortBy { it.partyIndex }
                    list
                }

                // Set up active sign session state (matching Electron's activeCoSignSession)
                val session = TssSession(
                    sessionId = sessionId,
                    sessionType = SessionType.SIGN,
                    thresholdT = thresholdT,
                    thresholdN = thresholdN,
                    participants = participants,
                    status = SessionStatus.WAITING,
                    inviteCode = null,
                    messageHash = messageHash
                )
                _currentSession.value = session
                _sessionStatus.value = SessionStatus.WAITING

                // Start message subscription (matching Electron's prepareForSign)
                // CRITICAL: Use signingPartyId (original partyId from keygen) for message routing
                startMessageRouting(sessionId, myPartyIndex, signingPartyId)

                // CRITICAL: Ensure session event subscription is active with signingPartyId
                // The event stream may have silently disconnected, so refresh it
                // Use signingPartyId so that session events for restored wallets are received
                ensureSessionEventSubscriptionActive(signingPartyId)

                android.util.Log.d("TssRepository", "Sign session state set, waiting for session_started event or in_progress status")

                Result.success(JoinSignViaGrpcResult(
                    partyIndex = myPartyIndex,
                    sessionStatus = sessionData.sessionStatus,
                    shareId = shareId
                ))
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "Join sign session via gRPC failed", e)
                Result.failure(e)
            }
        }
    }

    /**
     * Execute sign as joiner (called when session_started event is received or session is already in_progress)
     * This is the second part of the join sign flow
     */
    suspend fun executeSignAsJoiner(
        sessionId: String,
        partyIndex: Int,
        thresholdT: Int,
        thresholdN: Int,
        messageHash: String,
        shareId: Long,
        password: String
    ): Result<SignResult> = coroutineScope {
        try {
            val session = _currentSession.value
            if (session == null || session.sessionId != sessionId) {
                return@coroutineScope Result.failure(Exception("No active session or session mismatch"))
            }

            android.util.Log.d("TssRepository", "Executing sign as joiner: sessionId=$sessionId, partyIndex=$partyIndex")

            // Get share record
            val shareEntity = shareRecordDao.getShareById(shareId)
                ?: return@coroutineScope Result.failure(Exception("Share not found"))

            // Update session status
            _sessionStatus.value = SessionStatus.IN_PROGRESS

            // Get all participants from session
            val allParticipants = session.participants

            android.util.Log.d("TssRepository", "Starting TSS sign with ${allParticipants.size} participants, thresholdT=$thresholdT")

            // Start TSS sign
            // Remove 0x prefix from messageHash - TSS library expects pure hex
            val cleanMessageHash = if (messageHash.startsWith("0x") || messageHash.startsWith("0X")) {
                messageHash.substring(2)
            } else {
                messageHash
            }
            // CRITICAL: Use shareEntity.partyId (original partyId from keygen) for signing
            // This is required for backup/restore to work - the partyId must match what was used during keygen
            val signingPartyId = shareEntity.partyId
            currentSigningPartyId = signingPartyId  // Save for later use in this flow
            android.util.Log.d("TssRepository", "Starting TSS sign with cleanMessageHash=${cleanMessageHash.take(20)}..., signingPartyId=$signingPartyId")
            val startResult = tssNativeBridge.startSign(
                sessionId = sessionId,
                partyId = signingPartyId,
                partyIndex = partyIndex,
                thresholdT = thresholdT,
                thresholdN = shareEntity.thresholdN,  // Use original N from keygen
                participants = allParticipants,
                messageHash = cleanMessageHash,
                shareData = shareEntity.encryptedShare,
                password = password
            )

            if (startResult.isFailure) {
                _sessionStatus.value = SessionStatus.FAILED
                return@coroutineScope Result.failure(startResult.exceptionOrNull()!!)
            }

            // Start collecting progress from native bridge
            startProgressCollection()

            // Mark ready - use signingPartyId (original partyId from keygen)
            grpcClient.markPartyReady(sessionId, signingPartyId)

            // Wait for sign result
            val signResult = tssNativeBridge.waitForSignResult()
            if (signResult.isFailure) {
                stopProgressCollection()
                _sessionStatus.value = SessionStatus.FAILED
                return@coroutineScope Result.failure(signResult.exceptionOrNull()!!)
            }

            val result = signResult.getOrThrow()

            // Report completion - use signingPartyId (original partyId from keygen)
            val signatureBytes = android.util.Base64.decode(result.signature, android.util.Base64.NO_WRAP)
            grpcClient.reportCompletion(sessionId, signingPartyId, signature = signatureBytes)

            stopProgressCollection()
            _sessionStatus.value = SessionStatus.COMPLETED
            pendingSessionId = null  // Clear pending session ID on completion
            messageCollectionJob?.cancel()
            currentSigningPartyId = null  // Clear after signing completes
            grpcClient.clearSigningPartyRegistration()

            android.util.Log.d("TssRepository", "Sign as joiner completed: signature=${result.signature.take(20)}...")

            Result.success(result)

        } catch (e: Exception) {
            android.util.Log.e("TssRepository", "Execute sign as joiner failed", e)
            stopProgressCollection()
            _sessionStatus.value = SessionStatus.FAILED
            pendingSessionId = null  // Clear pending session ID on failure
            currentSigningPartyId = null  // Clear on failure too
            grpcClient.clearSigningPartyRegistration()
            Result.failure(e)
        }
    }

    /**
     * Join a keygen session and execute keygen protocol (DEPRECATED - old flow)
     * First calls account-service to get session info and join token, then joins via gRPC
     * @deprecated Use joinKeygenSessionViaGrpc + executeKeygenAsJoiner instead
     */
    @Deprecated("Use joinKeygenSessionViaGrpc + executeKeygenAsJoiner for Electron-compatible flow")
    suspend fun joinKeygenSession(
        inviteCode: String,
        password: String
    ): Result<ShareRecord> = coroutineScope {
        try {
            android.util.Log.d("TssRepository", "Joining keygen session with invite code: $inviteCode")

            // Step 1: Call account-service API to join session and get party_index
            val joinApiResult = joinSessionViaApi(inviteCode)
            if (joinApiResult.isFailure) {
                return@coroutineScope Result.failure(joinApiResult.exceptionOrNull()!!)
            }
            val apiJoinData = joinApiResult.getOrThrow()

            android.util.Log.d("TssRepository", "API join successful: sessionId=${apiJoinData.sessionId}, partyIndex=${apiJoinData.partyIndex}")

            // Step 2: Join session via gRPC for message routing
            // Use the join_token from API response
            val joinResult = grpcClient.joinSession(apiJoinData.sessionId, partyId, apiJoinData.joinToken)
            if (joinResult.isFailure) {
                android.util.Log.e("TssRepository", "gRPC join failed", joinResult.exceptionOrNull())
                return@coroutineScope Result.failure(joinResult.exceptionOrNull()!!)
            }

            val sessionData = joinResult.getOrThrow()

            // Use party_index from API response (more reliable than gRPC response)
            val myPartyIndex = apiJoinData.partyIndex

            // Update session state
            val session = TssSession(
                sessionId = apiJoinData.sessionId,
                sessionType = SessionType.KEYGEN,
                thresholdT = apiJoinData.thresholdT,
                thresholdN = apiJoinData.thresholdN,
                participants = sessionData.participants,
                status = SessionStatus.WAITING,
                inviteCode = inviteCode
            )
            _currentSession.value = session
            _sessionStatus.value = SessionStatus.WAITING

            // Add self to participants
            val allParticipants = sessionData.participants + Participant(partyId, myPartyIndex)

            // Start TSS keygen
            val startResult = tssNativeBridge.startKeygen(
                sessionId = apiJoinData.sessionId,
                partyId = partyId,
                partyIndex = myPartyIndex,
                thresholdT = apiJoinData.thresholdT,
                thresholdN = apiJoinData.thresholdN,
                participants = allParticipants,
                password = password
            )

            if (startResult.isFailure) {
                return@coroutineScope Result.failure(startResult.exceptionOrNull()!!)
            }

            _sessionStatus.value = SessionStatus.IN_PROGRESS

            // Start message routing
            startMessageRouting(apiJoinData.sessionId, myPartyIndex)

            // Mark ready
            grpcClient.markPartyReady(apiJoinData.sessionId, partyId)

            // Wait for keygen result
            val keygenResult = tssNativeBridge.waitForKeygenResult(password)
            if (keygenResult.isFailure) {
                _sessionStatus.value = SessionStatus.FAILED
                return@coroutineScope Result.failure(keygenResult.exceptionOrNull()!!)
            }

            val result = keygenResult.getOrThrow()

            // Derive address from public key
            val publicKeyBytes = Base64.decode(result.publicKey, Base64.NO_WRAP)
            val address = AddressUtils.deriveEvmAddress(publicKeyBytes)

            // Save share record
            // CRITICAL: Save partyId - this is required for signing after backup/restore
            val shareEntity = ShareRecordEntity(
                sessionId = apiJoinData.sessionId,
                publicKey = result.publicKey,
                encryptedShare = result.encryptedShare,
                thresholdT = apiJoinData.thresholdT,
                thresholdN = apiJoinData.thresholdN,
                partyIndex = myPartyIndex,
                partyId = partyId,
                address = address
            )
            val id = shareRecordDao.insertShare(shareEntity)

            // Report completion
            grpcClient.reportCompletion(apiJoinData.sessionId, partyId, publicKeyBytes)

            _sessionStatus.value = SessionStatus.COMPLETED
            messageCollectionJob?.cancel()

            Result.success(shareEntity.copy(id = id).toShareRecord())

        } catch (e: Exception) {
            android.util.Log.e("TssRepository", "Join keygen session failed", e)
            _sessionStatus.value = SessionStatus.FAILED
            Result.failure(e)
        }
    }

    /**
     * Join session via account-service HTTP API
     * POST /api/v1/co-managed/sessions/{sessionId}/join
     */
    private suspend fun joinSessionViaApi(inviteCode: String): Result<ApiJoinSessionData> {
        return withContext(Dispatchers.IO) {
            try {
                // First, get session info by invite code to get session_id and join_token
                val sessionInfoResult = validateInviteCode(inviteCode)
                if (sessionInfoResult.isFailure) {
                    return@withContext Result.failure(sessionInfoResult.exceptionOrNull()!!)
                }
                val sessionInfo = sessionInfoResult.getOrThrow()
                val sessionId = sessionInfo.sessionInfo.sessionId
                val joinToken = sessionInfo.joinToken

                android.util.Log.d("TssRepository", "Got session info: sessionId=$sessionId, joinToken=$joinToken")

                // Now call join API
                val jsonMediaType = "application/json; charset=utf-8".toMediaType()
                val requestBody = com.google.gson.JsonObject().apply {
                    addProperty("party_id", partyId)
                    addProperty("join_token", joinToken)
                    addProperty("device_type", "mobile")
                    addProperty("device_id", partyId)
                }.toString()

                val request = okhttp3.Request.Builder()
                    .url("$accountServiceUrl/api/v1/co-managed/sessions/$sessionId/join")
                    .post(requestBody.toRequestBody(jsonMediaType))
                    .header("Content-Type", "application/json")
                    .build()

                android.util.Log.d("TssRepository", "Joining session via API: $requestBody")

                val response = httpClient.newCall(request).execute()
                val responseBody = response.body?.string()
                    ?: return@withContext Result.failure(Exception("空响应"))

                android.util.Log.d("TssRepository", "Join API response: $responseBody")

                if (!response.isSuccessful) {
                    val errorJson = try {
                        com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                    } catch (e: Exception) { null }
                    val errorMsg = errorJson?.get("message")?.asString
                        ?: errorJson?.get("error")?.asString
                        ?: "HTTP ${response.code}"
                    return@withContext Result.failure(Exception(errorMsg))
                }

                // Parse response
                val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                val partyIndex = json.get("party_index")?.asInt ?: 0
                val sessionInfoJson = json.get("session_info")?.asJsonObject
                val thresholdT = sessionInfoJson?.get("threshold_t")?.asInt ?: sessionInfo.sessionInfo.thresholdT
                val thresholdN = sessionInfoJson?.get("threshold_n")?.asInt ?: sessionInfo.sessionInfo.thresholdN

                Result.success(ApiJoinSessionData(
                    sessionId = sessionId,
                    partyIndex = partyIndex,
                    thresholdT = thresholdT,
                    thresholdN = thresholdN,
                    joinToken = joinToken
                ))
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "Join session via API failed", e)
                Result.failure(e)
            }
        }
    }

    /**
     * Join a sign session and execute sign protocol
     * First calls account-service to get session info and join, then joins via gRPC
     */
    suspend fun joinSignSession(
        inviteCode: String,
        shareId: Long,
        password: String
    ): Result<SignResult> = coroutineScope {
        try {
            android.util.Log.d("TssRepository", "Joining sign session with invite code: $inviteCode")

            // Get share record
            val shareEntity = shareRecordDao.getShareById(shareId)
                ?: return@coroutineScope Result.failure(Exception("Share not found"))

            // CRITICAL: Define signingPartyId BEFORE calling API
            // Use shareEntity.partyId (original partyId from keygen) for signing
            val signingPartyId = shareEntity.partyId
            currentSigningPartyId = signingPartyId  // Save for later use in this flow
            android.util.Log.d("TssRepository", "Using signingPartyId=$signingPartyId for API sign join (device partyId=$partyId)")

            // Step 1: Call account-service API to join sign session and get party info
            // CRITICAL: Pass signingPartyId (original partyId from keygen) to the API
            val joinApiResult = joinSignSessionViaApi(inviteCode, shareEntity.partyIndex, signingPartyId)
            if (joinApiResult.isFailure) {
                return@coroutineScope Result.failure(joinApiResult.exceptionOrNull()!!)
            }
            val apiJoinData = joinApiResult.getOrThrow()

            android.util.Log.d("TssRepository", "API sign join successful: sessionId=${apiJoinData.sessionId}, partyIndex=${apiJoinData.partyIndex}, messageHash=${apiJoinData.messageHash}")

            // Step 2: Join session via gRPC for message routing
            // CRITICAL: Use signingPartyId (original partyId from keygen) for backup/restore support
            val joinResult = grpcClient.joinSession(apiJoinData.sessionId, signingPartyId, apiJoinData.joinToken)
            if (joinResult.isFailure) {
                android.util.Log.e("TssRepository", "gRPC join failed", joinResult.exceptionOrNull())
                return@coroutineScope Result.failure(joinResult.exceptionOrNull()!!)
            }

            val sessionData = joinResult.getOrThrow()

            // Use party_index from API response (more reliable)
            val myPartyIndex = apiJoinData.partyIndex

            // Update session state
            val session = TssSession(
                sessionId = apiJoinData.sessionId,
                sessionType = SessionType.SIGN,
                thresholdT = apiJoinData.thresholdT,
                thresholdN = apiJoinData.thresholdN,
                participants = sessionData.participants,
                status = SessionStatus.WAITING,
                inviteCode = inviteCode,
                messageHash = apiJoinData.messageHash
            )
            _currentSession.value = session
            _sessionStatus.value = SessionStatus.WAITING

            // Add self to participants using signingPartyId
            val allParticipants = sessionData.participants + Participant(signingPartyId, myPartyIndex)

            // Start TSS sign
            val startResult = tssNativeBridge.startSign(
                sessionId = apiJoinData.sessionId,
                partyId = signingPartyId,
                partyIndex = myPartyIndex,
                thresholdT = apiJoinData.thresholdT,
                thresholdN = shareEntity.thresholdN,  // Use original N from keygen
                participants = allParticipants,
                messageHash = apiJoinData.messageHash,
                shareData = shareEntity.encryptedShare,
                password = password
            )

            if (startResult.isFailure) {
                return@coroutineScope Result.failure(startResult.exceptionOrNull()!!)
            }

            _sessionStatus.value = SessionStatus.IN_PROGRESS

            // Start message routing
            // CRITICAL: Use signingPartyId (original partyId from keygen) for message routing
            startMessageRouting(apiJoinData.sessionId, myPartyIndex, signingPartyId)

            // Mark ready - use signingPartyId (original partyId from keygen)
            grpcClient.markPartyReady(apiJoinData.sessionId, signingPartyId)

            // Wait for sign result
            val signResult = tssNativeBridge.waitForSignResult()
            if (signResult.isFailure) {
                _sessionStatus.value = SessionStatus.FAILED
                return@coroutineScope Result.failure(signResult.exceptionOrNull()!!)
            }

            val result = signResult.getOrThrow()

            // Report completion - use signingPartyId (original partyId from keygen)
            val signatureBytes = Base64.decode(result.signature, Base64.NO_WRAP)
            grpcClient.reportCompletion(apiJoinData.sessionId, signingPartyId, signature = signatureBytes)

            _sessionStatus.value = SessionStatus.COMPLETED
            messageCollectionJob?.cancel()
            currentSigningPartyId = null  // Clear after signing completes
            grpcClient.clearSigningPartyRegistration()

            Result.success(result)

        } catch (e: Exception) {
            android.util.Log.e("TssRepository", "Join sign session failed", e)
            _sessionStatus.value = SessionStatus.FAILED
            currentSigningPartyId = null  // Clear on failure too
            grpcClient.clearSigningPartyRegistration()
            Result.failure(e)
        }
    }

    /**
     * Join sign session via account-service HTTP API
     * Uses validateSignInviteCode to get session info, then joins
     *
     * @param inviteCode The invite code for the sign session
     * @param partyIndex The party index for this participant
     * @param signingPartyId The original partyId from keygen (shareEntity.partyId)
     *        CRITICAL: For backup/restore support, this must be the original partyId
     */
    private suspend fun joinSignSessionViaApi(inviteCode: String, partyIndex: Int, signingPartyId: String): Result<ApiJoinSignSessionData> {
        return withContext(Dispatchers.IO) {
            try {
                // First, get sign session info by invite code
                val sessionInfoResult = validateSignInviteCode(inviteCode)
                if (sessionInfoResult.isFailure) {
                    return@withContext Result.failure(sessionInfoResult.exceptionOrNull()!!)
                }
                val sessionInfo = sessionInfoResult.getOrThrow()
                val signSessionInfo = sessionInfo.signSessionInfo
                val sessionId = signSessionInfo.sessionId
                val joinToken = sessionInfo.joinToken

                android.util.Log.d("TssRepository", "Got sign session info: sessionId=$sessionId, messageHash=${signSessionInfo.messageHash}, joinToken=$joinToken")

                // Now call join API (same endpoint as keygen join, but for sign sessions)
                // CRITICAL: Use signingPartyId (original partyId from keygen) for backup/restore support
                val jsonMediaType = "application/json; charset=utf-8".toMediaType()
                val requestBody = com.google.gson.JsonObject().apply {
                    addProperty("party_id", signingPartyId)  // Use original partyId from keygen
                    addProperty("join_token", joinToken)
                    addProperty("party_index", partyIndex)
                    addProperty("device_type", "mobile")
                    addProperty("device_id", signingPartyId)  // Use original partyId from keygen
                }.toString()

                val request = okhttp3.Request.Builder()
                    .url("$accountServiceUrl/api/v1/co-managed/sign/$sessionId/join")
                    .post(requestBody.toRequestBody(jsonMediaType))
                    .header("Content-Type", "application/json")
                    .build()

                android.util.Log.d("TssRepository", "Joining sign session via API: $requestBody")

                val response = httpClient.newCall(request).execute()
                val responseBody = response.body?.string()
                    ?: return@withContext Result.failure(Exception("空响应"))

                android.util.Log.d("TssRepository", "Join sign API response: $responseBody")

                if (!response.isSuccessful) {
                    val errorJson = try {
                        com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                    } catch (e: Exception) { null }
                    val errorMsg = errorJson?.get("message")?.asString
                        ?: errorJson?.get("error")?.asString
                        ?: "HTTP ${response.code}"
                    return@withContext Result.failure(Exception(errorMsg))
                }

                // Parse response - extract party_index if provided, otherwise use the one from share
                val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                val responsePartyIndex = json.get("party_index")?.asInt ?: partyIndex

                Result.success(ApiJoinSignSessionData(
                    sessionId = sessionId,
                    partyIndex = responsePartyIndex,
                    thresholdT = signSessionInfo.thresholdT,
                    thresholdN = signSessionInfo.thresholdN,
                    messageHash = signSessionInfo.messageHash,
                    joinToken = joinToken
                ))
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "Join sign session via API failed", e)
                Result.failure(e)
            }
        }
    }

    /**
     * Start message routing between TSS and gRPC
     *
     * @param sessionId The session ID
     * @param partyIndex The party index for this participant
     * @param routingPartyId The party ID to use for message routing.
     *        CRITICAL: For signing with restored wallets, this MUST be the original partyId
     *        from keygen (shareEntity.partyId), not the current device's partyId.
     *        For keygen, this should be the device's partyId.
     */
    private fun startMessageRouting(sessionId: String, partyIndex: Int, routingPartyId: String? = null) {
        // Save params for reconnection recovery
        currentMessageRoutingSessionId = sessionId
        currentMessageRoutingPartyIndex = partyIndex

        // Use provided routingPartyId, or fall back to device partyId for keygen
        val effectivePartyId = routingPartyId ?: partyId
        // Save for reconnection recovery
        currentMessageRoutingPartyId = effectivePartyId

        messageCollectionJob?.cancel()
        messageCollectionJob = repositoryScope.launch {
            android.util.Log.d("TssRepository", "Starting message routing: sessionId=$sessionId, routingPartyId=$effectivePartyId")

            // Collect outgoing messages from TSS and route via gRPC
            launch {
                tssNativeBridge.outgoingMessages.collect { message ->
                    val payload = Base64.decode(message.payload, Base64.NO_WRAP)
                    grpcClient.routeMessage(
                        sessionId = sessionId,
                        fromParty = effectivePartyId,  // Use the correct partyId for routing
                        toParties = message.toParties ?: emptyList(),
                        roundNumber = 0,
                        messageType = if (message.isBroadcast) "broadcast" else "p2p",
                        payload = payload
                    )
                }
            }

            // Collect incoming messages from gRPC and send to TSS
            launch {
                grpcClient.subscribeMessages(sessionId, effectivePartyId)
                    .catch { e ->
                        // Log error but don't crash - connection will be restored by GrpcClient
                        android.util.Log.e("TssRepository", "Message stream error: ${e.message}")
                    }
                    .collect { message ->
                    // Find party index from party ID
                    val session = _currentSession.value
                    val fromPartyIndex = session?.participants?.find { it.partyId == message.fromParty }?.partyIndex
                        ?: return@collect

                    tssNativeBridge.sendIncomingMessage(
                        fromPartyIndex = fromPartyIndex,
                        isBroadcast = message.isBroadcast,
                        payload = message.payload
                    )
                }
            }
        }
    }

    /**
     * Start keygen as initiator (called when session_started event is received)
     *
     * IMPORTANT: Before starting keygen, we fetch the complete participant list from
     * the backend. This is critical because the local session only contains ourselves
     * as a participant initially. Without the full list, TSS keygen will fail.
     *
     * This matches Electron's behavior in checkAndTriggerKeygen() which calls
     * getSessionStatus() to get all participants before starting keygen.
     */
    suspend fun startKeygenAsInitiator(
        sessionId: String,
        thresholdT: Int,
        thresholdN: Int,
        password: String
    ): Result<ShareRecord> = coroutineScope {
        try {
            val session = _currentSession.value
            if (session == null || session.sessionId != sessionId) {
                return@coroutineScope Result.failure(Exception("No active session"))
            }

            android.util.Log.d("TssRepository", "Starting keygen as initiator: sessionId=$sessionId")

            // CRITICAL: Fetch complete participant list from backend before starting keygen
            // This matches Electron's checkAndTriggerKeygen() behavior
            val statusResult = getSessionStatus(sessionId)
            if (statusResult.isFailure) {
                android.util.Log.e("TssRepository", "Failed to get session status: ${statusResult.exceptionOrNull()?.message}")
                return@coroutineScope Result.failure(statusResult.exceptionOrNull()!!)
            }

            val sessionStatus = statusResult.getOrThrow()
            android.util.Log.d("TssRepository", "Got session status: ${sessionStatus.participants.size} participants")

            // Build complete participant list from backend response
            val updatedParticipants = mutableListOf<Participant>()
            for (p in sessionStatus.participants) {
                val name = if (p.partyId == partyId) {
                    session.participants.firstOrNull()?.name ?: "我"
                } else {
                    "参与方 ${p.partyIndex + 1}"
                }
                updatedParticipants.add(Participant(p.partyId, p.partyIndex, name))
            }

            // Find my party index from the updated list
            val myInfo = updatedParticipants.find { it.partyId == partyId }
            val myPartyIndex = myInfo?.partyIndex ?: session.participants.firstOrNull()?.partyIndex ?: 0

            android.util.Log.d("TssRepository", "Updated participants: ${updatedParticipants.map { "${it.partyId.take(8)}:${it.partyIndex}" }}")
            android.util.Log.d("TssRepository", "My party index: $myPartyIndex")

            // Update the session with complete participant list
            _currentSession.value = session.copy(
                participants = updatedParticipants,
                thresholdT = sessionStatus.thresholdT,
                thresholdN = sessionStatus.thresholdN
            )

            // Use thresholds from backend (they may be more accurate)
            val actualThresholdT = sessionStatus.thresholdT
            val actualThresholdN = sessionStatus.thresholdN

            // Start TSS keygen with complete participant list
            val startResult = tssNativeBridge.startKeygen(
                sessionId = sessionId,
                partyId = partyId,
                partyIndex = myPartyIndex,
                thresholdT = actualThresholdT,
                thresholdN = actualThresholdN,
                participants = updatedParticipants,
                password = password
            )

            if (startResult.isFailure) {
                return@coroutineScope Result.failure(startResult.exceptionOrNull()!!)
            }

            // Start collecting progress from native bridge
            startProgressCollection()

            _sessionStatus.value = SessionStatus.IN_PROGRESS

            // Mark ready
            grpcClient.markPartyReady(sessionId, partyId)

            // Wait for keygen result
            val keygenResult = tssNativeBridge.waitForKeygenResult(password)
            if (keygenResult.isFailure) {
                stopProgressCollection()
                _sessionStatus.value = SessionStatus.FAILED
                return@coroutineScope Result.failure(keygenResult.exceptionOrNull()!!)
            }

            val result = keygenResult.getOrThrow()

            // Derive address from public key
            val publicKeyBytes = Base64.decode(result.publicKey, Base64.NO_WRAP)
            val address = AddressUtils.deriveEvmAddress(publicKeyBytes)

            // Save share record (use actual thresholds from backend)
            // CRITICAL: Save partyId - this is required for signing after backup/restore
            val shareEntity = ShareRecordEntity(
                sessionId = sessionId,
                publicKey = result.publicKey,
                encryptedShare = result.encryptedShare,
                thresholdT = actualThresholdT,
                thresholdN = actualThresholdN,
                partyIndex = myPartyIndex,
                partyId = partyId,
                address = address
            )
            val id = shareRecordDao.insertShare(shareEntity)

            // Report completion
            grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes)

            stopProgressCollection()
            _sessionStatus.value = SessionStatus.COMPLETED
            pendingSessionId = null  // Clear pending session ID on completion
            // NOTE: Do NOT cancel sessionEventJob here - it's a global subscription
            // that should remain active for subsequent keygen/sign sessions

            Result.success(shareEntity.copy(id = id).toShareRecord())

        } catch (e: Exception) {
            android.util.Log.e("TssRepository", "Start keygen as initiator failed", e)
            stopProgressCollection()
            _sessionStatus.value = SessionStatus.FAILED
            pendingSessionId = null  // Clear pending session ID on failure
            Result.failure(e)
        }
    }

    /**
     * Cancel current session
     */
    fun cancelSession() {
        tssNativeBridge.cancelSession()
        messageCollectionJob?.cancel()
        sessionEventJob?.cancel()
        stopSessionStatusPolling()  // Stop polling when session is cancelled
        _currentSession.value = null
        _sessionStatus.value = SessionStatus.WAITING

        // Clear pending session ID (race condition prevention)
        pendingSessionId = null

        // Clear reconnection recovery params
        currentMessageRoutingSessionId = null
        currentMessageRoutingPartyIndex = null
    }

    /**
     * Reset session status to WAITING
     * Called when navigating away from session screens to ensure fresh state
     */
    fun resetSessionStatus() {
        android.util.Log.d("TssRepository", "resetSessionStatus called - clearing session state")
        _sessionStatus.value = SessionStatus.WAITING
        stopSessionStatusPolling()  // Stop polling when resetting session state

        // CRITICAL: Clear pending session ID to avoid stale session matching
        // Without this, events from new sessions would be ignored because
        // pendingSessionId still holds the old session ID
        pendingSessionId = null
        _currentSession.value = null

        android.util.Log.d("TssRepository", "Session state cleared: pendingSessionId=null, currentSession=null")
    }

    /**
     * Delete a share record
     */
    suspend fun deleteShare(id: Long) {
        shareRecordDao.deleteShareById(id)
    }

    /**
     * Export a share as backup JSON string
     * @param shareId The ID of the share to export
     * @return Result containing the backup JSON string
     */
    suspend fun exportShareBackup(shareId: Long): Result<String> {
        android.util.Log.d("TssRepository", "[EXPORT] ========== exportShareBackup START ==========")
        android.util.Log.d("TssRepository", "[EXPORT] shareId: $shareId")
        return withContext(Dispatchers.IO) {
            try {
                android.util.Log.d("TssRepository", "[EXPORT] Fetching share from database...")
                val share = shareRecordDao.getShareById(shareId)
                if (share == null) {
                    android.util.Log.e("TssRepository", "[EXPORT] Share not found in database!")
                    return@withContext Result.failure(Exception("钱包不存在"))
                }
                android.util.Log.d("TssRepository", "[EXPORT] Share found:")
                android.util.Log.d("TssRepository", "[EXPORT]   - address: ${share.address}")
                android.util.Log.d("TssRepository", "[EXPORT]   - sessionId: ${share.sessionId}")
                android.util.Log.d("TssRepository", "[EXPORT]   - partyId: ${share.partyId}")
                android.util.Log.d("TssRepository", "[EXPORT]   - partyIndex: ${share.partyIndex}")
                android.util.Log.d("TssRepository", "[EXPORT]   - threshold: ${share.thresholdT}-of-${share.thresholdN}")
                android.util.Log.d("TssRepository", "[EXPORT]   - publicKey length: ${share.publicKey.length}")
                android.util.Log.d("TssRepository", "[EXPORT]   - encryptedShare length: ${share.encryptedShare.length}")

                // If partyId is empty (legacy data from before MIGRATION_2_3), fetch it from backend
                var effectivePartyId = share.partyId
                if (effectivePartyId.isEmpty()) {
                    android.util.Log.w("TssRepository", "[EXPORT] partyId is empty (legacy data), fetching from backend...")
                    try {
                        val sessionStatusResult = getSessionStatus(share.sessionId)
                        if (sessionStatusResult.isSuccess) {
                            val sessionStatus = sessionStatusResult.getOrThrow()
                            // Find participant by partyIndex
                            val participant = sessionStatus.participants.find { it.partyIndex == share.partyIndex }
                            if (participant != null && participant.partyId.isNotEmpty()) {
                                effectivePartyId = participant.partyId
                                android.util.Log.d("TssRepository", "[EXPORT] Retrieved partyId from backend: $effectivePartyId")

                                // Also update the database for future exports
                                val updatedShare = share.copy(partyId = effectivePartyId)
                                shareRecordDao.insertShare(updatedShare)
                                android.util.Log.d("TssRepository", "[EXPORT] Updated partyId in database")
                            } else {
                                android.util.Log.w("TssRepository", "[EXPORT] Could not find participant with partyIndex=${share.partyIndex} in session")
                            }
                        } else {
                            android.util.Log.w("TssRepository", "[EXPORT] Failed to get session status: ${sessionStatusResult.exceptionOrNull()?.message}")
                        }
                    } catch (e: Exception) {
                        android.util.Log.w("TssRepository", "[EXPORT] Error fetching partyId from backend: ${e.message}")
                    }
                }

                android.util.Log.d("TssRepository", "[EXPORT] Converting to ShareBackup with partyId: $effectivePartyId")
                // Create ShareRecord with effective partyId (may be fetched from backend)
                val shareRecord = share.toShareRecord().copy(partyId = effectivePartyId)
                val backup = ShareBackup.fromShareRecord(shareRecord)
                android.util.Log.d("TssRepository", "[EXPORT] ShareBackup created, version: ${backup.version}")

                android.util.Log.d("TssRepository", "[EXPORT] Serializing to JSON...")
                val json = com.google.gson.Gson().toJson(backup)
                android.util.Log.d("TssRepository", "[EXPORT] JSON length: ${json.length}")
                android.util.Log.d("TssRepository", "[EXPORT] JSON preview: ${json.take(200)}...")

                android.util.Log.d("TssRepository", "[EXPORT] ========== exportShareBackup SUCCESS ==========")
                Result.success(json)
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "[EXPORT] ========== exportShareBackup FAILED ==========")
                android.util.Log.e("TssRepository", "[EXPORT] Exception: ${e.javaClass.simpleName}: ${e.message}", e)
                Result.failure(e)
            }
        }
    }

    /**
     * Import a share from backup JSON string
     * @param backupJson The backup JSON string to import
     * @return Result containing the imported ShareRecord
     */
    suspend fun importShareBackup(backupJson: String): Result<ShareRecord> {
        android.util.Log.d("TssRepository", "[IMPORT] ========== importShareBackup START ==========")
        android.util.Log.d("TssRepository", "[IMPORT] JSON length: ${backupJson.length}")
        android.util.Log.d("TssRepository", "[IMPORT] JSON preview: ${backupJson.take(200)}...")
        return withContext(Dispatchers.IO) {
            try {
                android.util.Log.d("TssRepository", "[IMPORT] Parsing JSON to ShareBackup...")
                val backup = com.google.gson.Gson().fromJson(backupJson, ShareBackup::class.java)
                if (backup == null) {
                    android.util.Log.e("TssRepository", "[IMPORT] Parsed backup is null!")
                    return@withContext Result.failure(Exception("无效的备份文件格式"))
                }
                android.util.Log.d("TssRepository", "[IMPORT] ShareBackup parsed successfully:")
                android.util.Log.d("TssRepository", "[IMPORT]   - version: ${backup.version}")
                android.util.Log.d("TssRepository", "[IMPORT]   - address: ${backup.address}")
                android.util.Log.d("TssRepository", "[IMPORT]   - sessionId: ${backup.sessionId}")
                android.util.Log.d("TssRepository", "[IMPORT]   - partyId: ${backup.partyId}")
                android.util.Log.d("TssRepository", "[IMPORT]   - partyIndex: ${backup.partyIndex}")
                android.util.Log.d("TssRepository", "[IMPORT]   - threshold: ${backup.thresholdT}-of-${backup.thresholdN}")
                android.util.Log.d("TssRepository", "[IMPORT]   - publicKey length: ${backup.publicKey.length}")
                android.util.Log.d("TssRepository", "[IMPORT]   - encryptedShare length: ${backup.encryptedShare.length}")
                android.util.Log.d("TssRepository", "[IMPORT]   - createdAt: ${backup.createdAt}")
                android.util.Log.d("TssRepository", "[IMPORT]   - exportedAt: ${backup.exportedAt}")

                // Check if wallet already exists
                android.util.Log.d("TssRepository", "[IMPORT] Checking if wallet already exists...")
                val existingShare = shareRecordDao.getShareByAddress(backup.address)
                if (existingShare != null) {
                    android.util.Log.e("TssRepository", "[IMPORT] Wallet already exists! id=${existingShare.id}")
                    return@withContext Result.failure(Exception("此钱包已存在 (地址: ${backup.address.take(10)}...)"))
                }
                android.util.Log.d("TssRepository", "[IMPORT] Wallet does not exist, proceeding with import")

                // Convert to entity and save
                // CRITICAL: Preserve the original partyId from backup - this is required for signing
                android.util.Log.d("TssRepository", "[IMPORT] Converting to ShareRecordEntity...")
                val shareRecord = backup.toShareRecord()
                val entity = ShareRecordEntity(
                    sessionId = shareRecord.sessionId,
                    publicKey = shareRecord.publicKey,
                    encryptedShare = shareRecord.encryptedShare,
                    thresholdT = shareRecord.thresholdT,
                    thresholdN = shareRecord.thresholdN,
                    partyIndex = shareRecord.partyIndex,
                    partyId = shareRecord.partyId,
                    address = shareRecord.address,
                    createdAt = shareRecord.createdAt
                )
                android.util.Log.d("TssRepository", "[IMPORT] Entity created, partyId preserved: ${entity.partyId}")

                android.util.Log.d("TssRepository", "[IMPORT] Inserting into database...")
                val newId = shareRecordDao.insertShare(entity)
                android.util.Log.d("TssRepository", "[IMPORT] Inserted with id: $newId")

                val savedShare = shareRecord.copy(id = newId)
                android.util.Log.d("TssRepository", "[IMPORT] ========== importShareBackup SUCCESS ==========")
                android.util.Log.d("TssRepository", "[IMPORT] Imported wallet: address=${backup.address}, partyId=${backup.partyId}")
                Result.success(savedShare)
            } catch (e: com.google.gson.JsonSyntaxException) {
                android.util.Log.e("TssRepository", "[IMPORT] ========== importShareBackup FAILED (JSON Error) ==========")
                android.util.Log.e("TssRepository", "[IMPORT] JsonSyntaxException: ${e.message}", e)
                Result.failure(Exception("备份文件格式错误"))
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "[IMPORT] ========== importShareBackup FAILED ==========")
                android.util.Log.e("TssRepository", "[IMPORT] Exception: ${e.javaClass.simpleName}: ${e.message}", e)
                Result.failure(e)
            }
        }
    }

    /**
     * Get balance for an address using eth_getBalance RPC
     */
    suspend fun getBalance(address: String, rpcUrl: String): Result<String> {
        return withContext(Dispatchers.IO) {
            try {
                val client = okhttp3.OkHttpClient()
                val jsonMediaType = "application/json; charset=utf-8".toMediaType()
                val requestBody = """
                    {
                        "jsonrpc": "2.0",
                        "method": "eth_getBalance",
                        "params": ["$address", "latest"],
                        "id": 1
                    }
                """.trimIndent()

                val request = okhttp3.Request.Builder()
                    .url(rpcUrl)
                    .post(requestBody.toRequestBody(jsonMediaType))
                    .build()

                val response = client.newCall(request).execute()
                val responseBody = response.body?.string() ?: return@withContext Result.failure(Exception("Empty response"))

                // Parse JSON response
                val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                if (json.has("error")) {
                    return@withContext Result.failure(Exception(json.get("error").asJsonObject.get("message").asString))
                }

                val hexBalance = json.get("result").asString
                // Convert hex to decimal (wei) then to KAVA (18 decimals)
                val weiBalance = java.math.BigInteger(hexBalance.removePrefix("0x"), 16)
                val kavaBalance = java.math.BigDecimal(weiBalance).divide(java.math.BigDecimal("1000000000000000000"), 6, java.math.RoundingMode.DOWN)

                Result.success(kavaBalance.toPlainString())
            } catch (e: Exception) {
                Result.failure(e)
            }
        }
    }

    /**
     * Get ERC-20 token balance for an address
     * Uses eth_call to call balanceOf(address) on the ERC-20 contract
     * @param address The wallet address
     * @param rpcUrl The RPC endpoint URL
     * @param contractAddress The ERC-20 token contract address
     * @param decimals The token decimals (default 6 for USDT-like tokens)
     */
    suspend fun getERC20Balance(
        address: String,
        rpcUrl: String,
        contractAddress: String,
        decimals: Int = 6
    ): Result<String> {
        return withContext(Dispatchers.IO) {
            try {
                val client = okhttp3.OkHttpClient()
                val jsonMediaType = "application/json; charset=utf-8".toMediaType()

                // Encode balanceOf(address) call data
                // Function selector: 0x70a08231
                // Address parameter: padded to 32 bytes
                val paddedAddress = address.removePrefix("0x").lowercase().padStart(64, '0')
                val callData = "${ERC20Selectors.BALANCE_OF}$paddedAddress"

                val requestBody = """
                    {
                        "jsonrpc": "2.0",
                        "method": "eth_call",
                        "params": [{
                            "to": "$contractAddress",
                            "data": "$callData"
                        }, "latest"],
                        "id": 1
                    }
                """.trimIndent()

                val request = okhttp3.Request.Builder()
                    .url(rpcUrl)
                    .post(requestBody.toRequestBody(jsonMediaType))
                    .build()

                val response = client.newCall(request).execute()
                val responseBody = response.body?.string() ?: return@withContext Result.failure(Exception("Empty response"))

                // Parse JSON response
                val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                if (json.has("error")) {
                    return@withContext Result.failure(Exception(json.get("error").asJsonObject.get("message").asString))
                }

                val hexBalance = json.get("result").asString
                // Convert hex to decimal, then apply decimals
                val rawBalance = java.math.BigInteger(hexBalance.removePrefix("0x"), 16)
                val divisor = java.math.BigDecimal.TEN.pow(decimals)
                val tokenBalance = java.math.BigDecimal(rawBalance).divide(
                    divisor,
                    decimals,
                    java.math.RoundingMode.DOWN
                )

                Result.success(tokenBalance.toPlainString())
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "Failed to get ERC20 balance for $contractAddress: ${e.message}")
                Result.failure(e)
            }
        }
    }

    /**
     * Get Green Points (绿积分/dUSDT) token balance for an address
     * Uses eth_call to call balanceOf(address) on the ERC-20 contract
     */
    suspend fun getGreenPointsBalance(address: String, rpcUrl: String): Result<String> {
        return getERC20Balance(
            address = address,
            rpcUrl = rpcUrl,
            contractAddress = GreenPointsToken.CONTRACT_ADDRESS,
            decimals = GreenPointsToken.DECIMALS
        )
    }

    /**
     * Get Energy Points (积分股/eUSDT) token balance for an address
     */
    suspend fun getEnergyPointsBalance(address: String, rpcUrl: String): Result<String> {
        return getERC20Balance(
            address = address,
            rpcUrl = rpcUrl,
            contractAddress = EnergyPointsToken.CONTRACT_ADDRESS,
            decimals = EnergyPointsToken.DECIMALS
        )
    }

    /**
     * Get Future Points (积分值/fUSDT) token balance for an address
     */
    suspend fun getFuturePointsBalance(address: String, rpcUrl: String): Result<String> {
        return getERC20Balance(
            address = address,
            rpcUrl = rpcUrl,
            contractAddress = FuturePointsToken.CONTRACT_ADDRESS,
            decimals = FuturePointsToken.DECIMALS
        )
    }

    /**
     * Get all token balances for an address (KAVA + all ERC-20 tokens)
     */
    suspend fun getWalletBalance(address: String, rpcUrl: String): Result<WalletBalance> {
        return withContext(Dispatchers.IO) {
            try {
                // Fetch all balances in parallel
                val kavaDeferred = async { getBalance(address, rpcUrl) }
                val greenPointsDeferred = async { getGreenPointsBalance(address, rpcUrl) }
                val energyPointsDeferred = async { getEnergyPointsBalance(address, rpcUrl) }
                val futurePointsDeferred = async { getFuturePointsBalance(address, rpcUrl) }

                val kavaResult = kavaDeferred.await()
                val greenPointsResult = greenPointsDeferred.await()
                val energyPointsResult = energyPointsDeferred.await()
                val futurePointsResult = futurePointsDeferred.await()

                val kavaBalance = kavaResult.getOrDefault("0")
                val greenPointsBalance = greenPointsResult.getOrDefault("0")
                val energyPointsBalance = energyPointsResult.getOrDefault("0")
                val futurePointsBalance = futurePointsResult.getOrDefault("0")

                Result.success(WalletBalance(
                    address = address,
                    kavaBalance = kavaBalance,
                    greenPointsBalance = greenPointsBalance,
                    energyPointsBalance = energyPointsBalance,
                    futurePointsBalance = futurePointsBalance
                ))
            } catch (e: Exception) {
                Result.failure(e)
            }
        }
    }

    // ========== Transfer / Sign Session Methods ==========

    /**
     * Prepare a transaction for signing
     */
    suspend fun prepareTransaction(
        from: String,
        to: String,
        amount: String,
        rpcUrl: String,
        chainId: Int = TransactionUtils.KAVA_TESTNET_CHAIN_ID,
        tokenType: TokenType = TokenType.KAVA
    ): Result<TransactionUtils.PreparedTransaction> {
        return TransactionUtils.prepareTransaction(
            TransactionUtils.TransactionParams(
                from = from,
                to = to,
                amount = amount,
                rpcUrl = rpcUrl,
                chainId = chainId,
                tokenType = tokenType
            )
        )
    }

    /**
     * Create a sign session for a transaction (as initiator)
     * Matches Electron's cosign:createSession flow exactly:
     * 1. Get keygen session status to get all participants with party_index
     * 2. Filter out co-managed-party-* (server backup parties)
     * 3. Create sign session with filtered signing parties
     * 4. Auto-join the session via gRPC
     * 5. Subscribe to message stream (prepareForSign)
     * 6. Check if session already in_progress and trigger sign immediately
     */
    suspend fun createSignSession(
        shareId: Long,
        messageHash: String,
        password: String,
        initiatorName: String
    ): Result<SignSessionResult> {
        return withContext(Dispatchers.IO) {
            try {
                // Get share record first to determine the signingPartyId
                val shareEntity = shareRecordDao.getShareById(shareId)
                    ?: return@withContext Result.failure(Exception("Share not found"))

                // CRITICAL: Use shareEntity.partyId (original partyId from keygen) for session events
                val signingPartyIdForEvents = shareEntity.partyId

                // CRITICAL: Ensure session event subscription is active BEFORE creating sign session
                // This prevents race condition where session_started event arrives before subscription is ready
                // Use signingPartyId so that session events for restored wallets are received
                android.util.Log.d("TssRepository", "[CO-SIGN] Ensuring session event subscription is active before creating sign session (signingPartyId=$signingPartyIdForEvents)")
                ensureSessionEventSubscriptionActive(signingPartyIdForEvents)

                android.util.Log.d("TssRepository", "[CO-SIGN] Creating sign session for share: ${shareEntity.sessionId}")

                // Step 1: Get keygen session status to get all participants with party_index
                // This matches Electron's: accountClient.getSessionStatus(share.session_id)
                val keygenStatusResult = getSessionStatus(shareEntity.sessionId)
                if (keygenStatusResult.isFailure) {
                    return@withContext Result.failure(Exception("无法获取 keygen 会话的参与者信息: ${keygenStatusResult.exceptionOrNull()?.message}"))
                }
                val keygenStatus = keygenStatusResult.getOrThrow()

                if (keygenStatus.participants.isEmpty()) {
                    return@withContext Result.failure(Exception("无法获取 keygen 会话的参与者信息"))
                }

                // Step 2: Filter out co-managed-party-* (server backup parties)
                // Only user-held signing parties should participate in signing
                // This matches Electron's: filter(p => !p.party_id.startsWith('co-managed-party-'))
                val signingParties = keygenStatus.participants
                    .filter { !it.partyId.startsWith("co-managed-party-") }
                    .map { Pair(it.partyId, it.partyIndex) }

                android.util.Log.d("TssRepository", "[CO-SIGN] Signing parties (excluding co-managed-party): ${signingParties.size} of ${keygenStatus.participants.size}")
                signingParties.forEach { (id, index) ->
                    android.util.Log.d("TssRepository", "[CO-SIGN]   party_id=${id.take(16)}, party_index=$index")
                }

                if (signingParties.size < shareEntity.thresholdT) {
                    return@withContext Result.failure(Exception(
                        "签名参与方不足: 需要 ${shareEntity.thresholdT} 个，但只有 ${signingParties.size} 个用户方"
                    ))
                }

                val jsonMediaType = "application/json; charset=utf-8".toMediaType()

                // Build parties array with all signing parties (not just initiator)
                val partiesArray = com.google.gson.JsonArray().apply {
                    signingParties.forEach { (partyIdStr, partyIndex) ->
                        add(com.google.gson.JsonObject().apply {
                            addProperty("party_id", partyIdStr)
                            addProperty("party_index", partyIndex)
                        })
                    }
                }

                // Build request body matching account-service API
                // Note: Android ShareRecordEntity doesn't store wallet_name, use address-based name
                val walletName = "Wallet ${shareEntity.address.take(8)}...${shareEntity.address.takeLast(4)}"
                // Remove 0x prefix from messageHash - backend expects pure hex
                val cleanMessageHash = if (messageHash.startsWith("0x") || messageHash.startsWith("0X")) {
                    messageHash.substring(2)
                } else {
                    messageHash
                }
                val requestBody = com.google.gson.JsonObject().apply {
                    addProperty("keygen_session_id", shareEntity.sessionId)
                    addProperty("wallet_name", walletName)
                    addProperty("message_hash", cleanMessageHash)
                    add("parties", partiesArray)
                    addProperty("threshold_t", shareEntity.thresholdT)
                    addProperty("initiator_name", initiatorName)
                }.toString()

                val request = okhttp3.Request.Builder()
                    .url("$accountServiceUrl/api/v1/co-managed/sign")
                    .post(requestBody.toRequestBody(jsonMediaType))
                    .header("Content-Type", "application/json")
                    .build()

                android.util.Log.d("TssRepository", "[CO-SIGN] Creating sign session: $requestBody")

                val response = httpClient.newCall(request).execute()
                val responseBody = response.body?.string()
                    ?: return@withContext Result.failure(Exception("空响应"))

                android.util.Log.d("TssRepository", "[CO-SIGN] Create sign session response: $responseBody")

                if (!response.isSuccessful) {
                    val errorJson = try {
                        com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                    } catch (e: Exception) { null }
                    val errorMsg = errorJson?.get("message")?.asString
                        ?: errorJson?.get("error")?.asString
                        ?: "HTTP ${response.code}"
                    return@withContext Result.failure(Exception(errorMsg))
                }

                // Parse response
                val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                val sessionId = json.get("session_id").asString
                val inviteCode = json.get("invite_code").asString
                val thresholdT = json.get("threshold_t")?.asInt ?: shareEntity.thresholdT

                // CRITICAL: Use shareEntity.partyId (original partyId from keygen) for join token lookup
                // The server generates join_tokens keyed by original partyId, not device partyId
                val originalPartyId = shareEntity.partyId

                // Get join token - support both new format (join_tokens map) and old format (join_token string)
                val joinToken = if (json.has("join_tokens") && json.get("join_tokens").isJsonObject) {
                    val joinTokens = json.getAsJsonObject("join_tokens")
                    // CRITICAL: Use originalPartyId (from keygen) to look up join token
                    joinTokens.get(originalPartyId)?.asString ?: joinTokens.get("*")?.asString
                } else {
                    json.get("join_token")?.asString
                }

                // Build participants list from signingParties
                // CRITICAL: Use originalPartyId for name comparison (not device partyId)
                val participants = signingParties.map { (pId, pIndex) ->
                    Participant(
                        partyId = pId,
                        partyIndex = pIndex,
                        name = if (pId == originalPartyId) initiatorName else "参与方 ${pIndex + 1}"
                    )
                }

                // CRITICAL: Set pendingSessionId BEFORE any async operations to avoid race condition
                // This ensures session events can be matched even if they arrive before other state is set
                pendingSessionId = sessionId
                android.util.Log.d("TssRepository", "[CO-SIGN] Set pendingSessionId=$sessionId for event matching (sign initiator)")

                // Update session state with complete participant list
                // Use original keygen thresholdN (matching Electron's share.threshold_n)
                val session = TssSession(
                    sessionId = sessionId,
                    sessionType = SessionType.SIGN,
                    thresholdT = thresholdT,
                    thresholdN = shareEntity.thresholdN, // Use original keygen N (matching Electron)
                    participants = participants,
                    status = SessionStatus.WAITING,
                    inviteCode = inviteCode,
                    messageHash = messageHash
                )
                _currentSession.value = session
                _sessionStatus.value = SessionStatus.WAITING

                // Step 4: Initiator auto-join via gRPC (matching Electron behavior)
                // CRITICAL: Use shareEntity.partyId (original partyId from keygen) for signing
                // This is essential for backup/restore to work correctly
                val signingPartyId = shareEntity.partyId
                currentSigningPartyId = signingPartyId  // Save for later use in this flow
                android.util.Log.d("TssRepository", "[CO-SIGN] Using signingPartyId=$signingPartyId (device partyId=$partyId)")

                var sessionAlreadyInProgress = false
                if (joinToken != null) {
                    android.util.Log.d("TssRepository", "[CO-SIGN] Initiator auto-joining session...")
                    val myPartyIndex = signingParties.find { it.first == signingPartyId }?.second ?: shareEntity.partyIndex

                    // CRITICAL: Use signingPartyId (original partyId from keygen) for joining
                    val joinResult = grpcClient.joinSession(sessionId, signingPartyId, joinToken)
                    if (joinResult.isSuccess) {
                        val joinData = joinResult.getOrThrow()
                        android.util.Log.d("TssRepository", "[CO-SIGN] Initiator joined: partyIndex=${joinData.partyIndex}, status=${joinData.sessionStatus}")

                        // Step 5: Start message routing (prepareForSign) BEFORE sign starts
                        // CRITICAL: Use signingPartyId for message routing
                        startMessageRouting(sessionId, myPartyIndex, signingPartyId)

                        // CRITICAL: Ensure session event subscription is active for sign initiator
                        // The event stream may have silently disconnected
                        // Use signingPartyId so that session events for restored wallets are received
                        ensureSessionEventSubscriptionActive(signingPartyId)

                        // Step 6: Check if session already in_progress
                        if (joinData.sessionStatus == "in_progress") {
                            android.util.Log.d("TssRepository", "[CO-SIGN] Session already in_progress, will trigger sign immediately")
                            sessionAlreadyInProgress = true
                        }
                    } else {
                        android.util.Log.w("TssRepository", "[CO-SIGN] Initiator failed to join: ${joinResult.exceptionOrNull()?.message}")
                    }
                } else {
                    android.util.Log.w("TssRepository", "[CO-SIGN] No join token found for initiator")
                }

                // If session is already in_progress, notify callback immediately
                if (sessionAlreadyInProgress) {
                    val selectedParties = signingParties.map { it.first }
                    val eventData = SessionEventData(
                        eventId = "immediate_sign_start",
                        eventType = "session_started",
                        sessionId = sessionId,
                        thresholdN = shareEntity.thresholdN, // Use original keygen N (matching Electron)
                        thresholdT = thresholdT,
                        selectedParties = selectedParties,
                        joinTokens = emptyMap(),
                        messageHash = messageHash
                    )
                    sessionEventCallback?.invoke(eventData)
                }

                Result.success(SignSessionResult(
                    sessionId = sessionId,
                    inviteCode = inviteCode,
                    sessionAlreadyInProgress = sessionAlreadyInProgress
                ))
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "[CO-SIGN] Create sign session failed", e)
                Result.failure(e)
            }
        }
    }

    /**
     * Start the signing process after all parties have joined
     * Note: Message routing should already be started in createSignSession (after auto-join)
     */
    suspend fun startSigning(
        sessionId: String,
        shareId: Long,
        password: String
    ): Result<Unit> {
        return withContext(Dispatchers.IO) {
            try {
                val session = _currentSession.value
                    ?: return@withContext Result.failure(Exception("No active session"))

                val shareEntity = shareRecordDao.getShareById(shareId)
                    ?: return@withContext Result.failure(Exception("Share not found"))

                // CRITICAL: Use shareEntity.partyId (original partyId from keygen) for signing
                // This is required for backup/restore to work - the partyId must match what was used during keygen
                val signingPartyId = shareEntity.partyId
                currentSigningPartyId = signingPartyId  // Save for waitForSignature
                android.util.Log.d("TssRepository", "[CO-SIGN] startSigning: participants=${session.participants.size}")
                android.util.Log.d("TssRepository", "[CO-SIGN] startSigning: sessionId=$sessionId, signingPartyId=$signingPartyId, partyIndex=${shareEntity.partyIndex}")
                android.util.Log.d("TssRepository", "[CO-SIGN] startSigning: thresholdT=${session.thresholdT}, thresholdN=${shareEntity.thresholdN}")
                android.util.Log.d("TssRepository", "[CO-SIGN] startSigning: messageHash=${session.messageHash?.take(20)}...")
                session.participants.forEachIndexed { idx, p ->
                    android.util.Log.d("TssRepository", "[CO-SIGN] startSigning: participant[$idx] = ${p.partyId.take(12)}..., index=${p.partyIndex}")
                }

                // Start TSS sign
                // Remove 0x prefix from messageHash - TSS library expects pure hex
                val rawMessageHash = session.messageHash ?: ""
                val cleanMessageHash = if (rawMessageHash.startsWith("0x") || rawMessageHash.startsWith("0X")) {
                    rawMessageHash.substring(2)
                } else {
                    rawMessageHash
                }
                android.util.Log.d("TssRepository", "[CO-SIGN] Calling tssNativeBridge.startSign with cleanMessageHash=${cleanMessageHash.take(20)}..., signingPartyId=$signingPartyId")
                val startResult = tssNativeBridge.startSign(
                    sessionId = sessionId,
                    partyId = signingPartyId,
                    partyIndex = shareEntity.partyIndex,
                    thresholdT = session.thresholdT,
                    thresholdN = shareEntity.thresholdN,
                    participants = session.participants,
                    messageHash = cleanMessageHash,
                    shareData = shareEntity.encryptedShare,
                    password = password
                )
                android.util.Log.d("TssRepository", "[CO-SIGN] tssNativeBridge.startSign returned: isSuccess=${startResult.isSuccess}")

                if (startResult.isFailure) {
                    android.util.Log.e("TssRepository", "[CO-SIGN] startSign FAILED: ${startResult.exceptionOrNull()?.message}")
                    return@withContext Result.failure(startResult.exceptionOrNull()!!)
                }
                android.util.Log.d("TssRepository", "[CO-SIGN] startSign succeeded, starting progress collection...")

                // Start collecting progress from native bridge
                startProgressCollection()

                _sessionStatus.value = SessionStatus.IN_PROGRESS

                // Note: Message routing is already started in createSignSession after auto-join
                // Only start if not already running (for backward compatibility with old flow)
                if (messageCollectionJob == null || messageCollectionJob?.isActive != true) {
                    // CRITICAL: Use signingPartyId for message routing
                    startMessageRouting(sessionId, shareEntity.partyIndex, signingPartyId)
                }

                // Mark ready - use signingPartyId (original partyId from keygen)
                grpcClient.markPartyReady(sessionId, signingPartyId)

                Result.success(Unit)
            } catch (e: Exception) {
                stopProgressCollection()
                _sessionStatus.value = SessionStatus.FAILED
                Result.failure(e)
            }
        }
    }

    /**
     * Wait for signing to complete and get signature
     */
    suspend fun waitForSignature(): Result<SignResult> {
        return withContext(Dispatchers.IO) {
            try {
                val signResult = tssNativeBridge.waitForSignResult()
                if (signResult.isFailure) {
                    stopProgressCollection()
                    _sessionStatus.value = SessionStatus.FAILED
                    return@withContext Result.failure(signResult.exceptionOrNull()!!)
                }

                val result = signResult.getOrThrow()

                // Report completion - use currentSigningPartyId (original partyId from keygen)
                val signatureBytes = Base64.decode(result.signature, Base64.NO_WRAP)
                val session = _currentSession.value
                val signingPartyId = currentSigningPartyId ?: partyId
                if (session != null) {
                    grpcClient.reportCompletion(session.sessionId, signingPartyId, signature = signatureBytes)
                }

                stopProgressCollection()
                _sessionStatus.value = SessionStatus.COMPLETED
                messageCollectionJob?.cancel()
                currentSigningPartyId = null  // Clear after signing completes
                grpcClient.clearSigningPartyRegistration()

                Result.success(result)
            } catch (e: Exception) {
                _sessionStatus.value = SessionStatus.FAILED
                grpcClient.clearSigningPartyRegistration()
                Result.failure(e)
            }
        }
    }

    /**
     * Finalize and broadcast a signed transaction
     */
    suspend fun broadcastTransaction(
        preparedTx: TransactionUtils.PreparedTransaction,
        signature: String,
        rpcUrl: String
    ): Result<String> {
        return withContext(Dispatchers.IO) {
            try {
                android.util.Log.d("TssRepository", "[BROADCAST] Input signature: ${signature.take(40)}...")
                android.util.Log.d("TssRepository", "[BROADCAST] Signature length: ${signature.length}")

                // Signature from TSS is Base64 encoded, need to decode and convert to hex
                // Format after decode: r (32 bytes) + s (32 bytes) + v (1 byte) = 65 bytes
                val sigBytes = try {
                    android.util.Base64.decode(signature, android.util.Base64.NO_WRAP)
                } catch (e: Exception) {
                    // If not Base64, try to parse as hex directly
                    android.util.Log.d("TssRepository", "[BROADCAST] Not Base64, trying hex...")
                    signature.removePrefix("0x").hexToByteArray()
                }

                android.util.Log.d("TssRepository", "[BROADCAST] Decoded signature bytes: ${sigBytes.size}")

                if (sigBytes.size != 65) {
                    return@withContext Result.failure(Exception("Invalid signature length: ${sigBytes.size} bytes, expected 65"))
                }

                // Extract r, s, v from decoded bytes
                val rBytes = sigBytes.copyOfRange(0, 32)
                val sBytes = sigBytes.copyOfRange(32, 64)
                val vByte = sigBytes[64].toInt() and 0xFF

                // Convert to hex for logging
                val rHex = rBytes.joinToString("") { "%02x".format(it) }
                val sHex = sBytes.joinToString("") { "%02x".format(it) }

                android.util.Log.d("TssRepository", "[BROADCAST] r: ${rHex.take(16)}...")
                android.util.Log.d("TssRepository", "[BROADCAST] s: ${sHex.take(16)}...")
                android.util.Log.d("TssRepository", "[BROADCAST] v: $vByte")

                val recoveryId = if (vByte >= 27) vByte - 27 else vByte

                // Use byte arrays directly for finalizeTransaction
                val r = rBytes
                val s = sBytes

                // Finalize transaction with signature
                val signedTx = TransactionUtils.finalizeTransaction(
                    preparedTx = preparedTx,
                    r = r,
                    s = s,
                    recoveryId = recoveryId
                )

                // Broadcast to network
                TransactionUtils.broadcastTransaction(signedTx, rpcUrl)
            } catch (e: Exception) {
                Result.failure(e)
            }
        }
    }

    /**
     * Get transaction receipt
     */
    suspend fun getTransactionReceipt(
        txHash: String,
        rpcUrl: String
    ): Result<TransactionUtils.TransactionReceipt?> {
        return TransactionUtils.getTransactionReceipt(txHash, rpcUrl)
    }

    // ========== Connection Test Methods ==========

    /**
     * Test Message Router connection
     * Returns success/failure with latency
     */
    suspend fun testMessageRouter(serverUrl: String): Result<Long> {
        return withContext(Dispatchers.IO) {
            try {
                val startTime = System.currentTimeMillis()

                val parts = serverUrl.split(":")
                val host = parts[0]
                val port = parts.getOrNull(1)?.toIntOrNull() ?: 443

                // Try to connect
                grpcClient.connect(host, port)
                val testPartyId = "test-${UUID.randomUUID()}"
                grpcClient.registerParty(testPartyId, "test", "1.0.0")

                val latency = System.currentTimeMillis() - startTime
                Result.success(latency)
            } catch (e: Exception) {
                Result.failure(e)
            }
        }
    }

    /**
     * Test Account Service connection
     * Makes a health check or simple GET request
     */
    suspend fun testAccountService(serviceUrl: String): Result<Long> {
        return withContext(Dispatchers.IO) {
            try {
                val startTime = System.currentTimeMillis()

                val client = okhttp3.OkHttpClient.Builder()
                    .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
                    .readTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
                    .build()

                // Try health endpoint or root
                val healthUrl = if (serviceUrl.endsWith("/")) {
                    "${serviceUrl}health"
                } else {
                    "$serviceUrl/health"
                }

                val request = okhttp3.Request.Builder()
                    .url(healthUrl)
                    .get()
                    .build()

                val response = client.newCall(request).execute()
                val latency = System.currentTimeMillis() - startTime

                // Accept 200, 404 (endpoint may not exist but server is up)
                if (response.isSuccessful || response.code == 404) {
                    Result.success(latency)
                } else {
                    Result.failure(Exception("HTTP ${response.code}"))
                }
            } catch (e: Exception) {
                Result.failure(e)
            }
        }
    }

    /**
     * Test Kava RPC connection
     * Makes an eth_chainId RPC call
     */
    suspend fun testKavaApi(rpcUrl: String): Result<Long> {
        return withContext(Dispatchers.IO) {
            try {
                val startTime = System.currentTimeMillis()

                val client = okhttp3.OkHttpClient.Builder()
                    .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
                    .readTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
                    .build()

                val jsonMediaType = "application/json; charset=utf-8".toMediaType()
                val requestBody = """
                    {
                        "jsonrpc": "2.0",
                        "method": "eth_chainId",
                        "params": [],
                        "id": 1
                    }
                """.trimIndent()

                val request = okhttp3.Request.Builder()
                    .url(rpcUrl)
                    .post(requestBody.toRequestBody(jsonMediaType))
                    .build()

                val response = client.newCall(request).execute()
                val latency = System.currentTimeMillis() - startTime

                if (!response.isSuccessful) {
                    return@withContext Result.failure(Exception("HTTP ${response.code}"))
                }

                val responseBody = response.body?.string()
                    ?: return@withContext Result.failure(Exception("空响应"))

                // Parse JSON to verify it's a valid JSON-RPC response
                val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                if (json.has("error")) {
                    return@withContext Result.failure(
                        Exception(json.get("error").asJsonObject.get("message").asString)
                    )
                }

                Result.success(latency)
            } catch (e: Exception) {
                Result.failure(e)
            }
        }
    }

    /**
     * Get a share by ID
     */
    suspend fun getShareById(shareId: Long): ShareRecord? {
        return shareRecordDao.getShareById(shareId)?.toShareRecord()
    }

    private fun String.hexToByteArray(): ByteArray {
        val len = this.length
        val data = ByteArray(len / 2)
        var i = 0
        while (i < len) {
            data[i / 2] = ((Character.digit(this[i], 16) shl 4) + Character.digit(this[i + 1], 16)).toByte()
            i += 2
        }
        return data
    }

/**
 * Result of creating a keygen session
 */
data class CreateKeygenSessionResult(
    val sessionId: String,
    val inviteCode: String,
    val partyIndex: Int,
    val walletName: String,
    val thresholdT: Int,
    val thresholdN: Int
)

/**
 * Result of creating a sign session
 */
data class SignSessionResult(
    val sessionId: String,
    val inviteCode: String,
    val sessionAlreadyInProgress: Boolean = false
)

/**
 * Result of validating an invite code
 */
data class ValidateInviteCodeResult(
    val sessionInfo: SessionInfoResponse,
    val joinToken: String
)

/**
 * Session info from validateInviteCode API
 */
data class SessionInfoResponse(
    val sessionId: String,
    val walletName: String,
    val thresholdT: Int,
    val thresholdN: Int,
    val initiator: String,
    val currentParticipants: Int,
    val totalParticipants: Int
)

/**
 * Result of validating a sign session invite code
 */
data class ValidateSignInviteCodeResult(
    val signSessionInfo: SignSessionInfoResponse,
    val joinToken: String
)

/**
 * Sign session info from validateSignInviteCode API
 */
data class SignSessionInfoResponse(
    val sessionId: String,
    val keygenSessionId: String,
    val walletName: String,
    val messageHash: String,
    val thresholdT: Int,
    val thresholdN: Int,
    val currentParticipants: Int,
    val parties: List<Participant> = emptyList()  // Complete parties list from API
)

/**
 * Data returned from joinSessionViaApi
 */
data class ApiJoinSessionData(
    val sessionId: String,
    val partyIndex: Int,
    val thresholdT: Int,
    val thresholdN: Int,
    val joinToken: String
)

/**
 * Data returned from joinSignSessionViaApi
 */
data class ApiJoinSignSessionData(
    val sessionId: String,
    val partyIndex: Int,
    val thresholdT: Int,
    val thresholdN: Int,
    val messageHash: String,
    val joinToken: String
)

/**
 * Result of joinKeygenSessionViaGrpc
 * Matches Electron's grpc:joinSession return value
 */
data class JoinKeygenViaGrpcResult(
    val partyIndex: Int,
    val sessionStatus: String?
)

/**
 * Result of joining sign session via gRPC
 * Matches Electron's cosign:joinSession return value
 */
data class JoinSignViaGrpcResult(
    val partyIndex: Int,
    val sessionStatus: String?,
    val shareId: Long
)

/**
 * Session status response from getSessionStatus API
 * Matches Electron's GetSessionStatusResponse
 */
data class SessionStatusResponse(
    val sessionId: String,
    val status: String,
    val thresholdT: Int,
    val thresholdN: Int,
    val completedParties: Int,
    val totalParties: Int,
    val sessionType: String,
    val participants: List<ParticipantStatusInfo>
)

/**
 * Participant status info with party_index
 */
data class ParticipantStatusInfo(
    val partyId: String,
    val partyIndex: Int,
    val status: String
)

// ========== Transaction Record Methods ==========

    /**
     * 保存交易记录
     * 转账发起后立即调用，初始状态为 PENDING
     */
    suspend fun saveTransactionRecord(
        shareId: Long,
        fromAddress: String,
        toAddress: String,
        amount: String,
        tokenType: TokenType,
        txHash: String,
        gasPrice: String,
        direction: String = "SENT"
    ): Long {
        return withContext(Dispatchers.IO) {
            // 统一转换为 lowercase 确保地址匹配
            val normalizedFrom = fromAddress.lowercase()
            val normalizedTo = toAddress.lowercase()

            android.util.Log.d("TssRepository", "[TX-RECORD] Saving transaction record: txHash=${txHash.take(20)}...")
            val entity = TransactionRecordEntity(
                shareId = shareId,
                fromAddress = normalizedFrom,
                toAddress = normalizedTo,
                amount = amount,
                tokenType = tokenType.name,
                txHash = txHash,
                gasPrice = gasPrice,
                status = "PENDING",
                direction = direction,
                createdAt = System.currentTimeMillis()
            )
            val id = transactionRecordDao.insertRecord(entity)
            android.util.Log.d("TssRepository", "[TX-RECORD] Saved with id: $id")
            id
        }
    }

    /**
     * 更新交易状态
     * 确认交易后调用，更新状态为 CONFIRMED 或 FAILED
     */
    suspend fun updateTransactionStatus(
        txHash: String,
        status: String,
        gasUsed: String,
        txFee: String,
        blockNumber: Long?,
        confirmedAt: Long?
    ) {
        withContext(Dispatchers.IO) {
            android.util.Log.d("TssRepository", "[TX-RECORD] Updating status for txHash=${txHash.take(20)}... to $status")
            val record = transactionRecordDao.getRecordByTxHash(txHash)
            if (record != null) {
                transactionRecordDao.updateStatus(
                    id = record.id,
                    status = status,
                    confirmedAt = confirmedAt,
                    blockNumber = blockNumber,
                    gasUsed = gasUsed,
                    txFee = txFee
                )
                android.util.Log.d("TssRepository", "[TX-RECORD] Status updated successfully")
            } else {
                android.util.Log.w("TssRepository", "[TX-RECORD] Record not found for txHash=${txHash.take(20)}...")
            }
        }
    }

    /**
     * 获取钱包的交易记录列表
     */
    fun getTransactionRecords(shareId: Long): Flow<List<TransactionRecordEntity>> {
        return transactionRecordDao.getRecordsForShare(shareId)
    }

    /**
     * 获取所有待确认的交易
     */
    suspend fun getPendingTransactions(): List<TransactionRecordEntity> {
        return withContext(Dispatchers.IO) {
            transactionRecordDao.getPendingRecords()
        }
    }

    /**
     * 确认交易状态
     * 查询链上交易收据并更新本地状态
     */
    suspend fun confirmTransaction(txHash: String, rpcUrl: String): Result<Boolean> {
        return withContext(Dispatchers.IO) {
            try {
                android.util.Log.d("TssRepository", "[TX-CONFIRM] Checking receipt for txHash=${txHash.take(20)}...")
                val receiptResult = TransactionUtils.getTransactionReceipt(txHash, rpcUrl)

                receiptResult.fold(
                    onSuccess = { receipt ->
                        if (receipt != null) {
                            val status = if (receipt.status) "CONFIRMED" else "FAILED"
                            val gasUsed = receipt.gasUsed.toString()
                            val blockNumber = try {
                                java.math.BigInteger(receipt.blockNumber.removePrefix("0x"), 16).toLong()
                            } catch (e: Exception) { null }

                            // 计算交易费用 (gasUsed * gasPrice)
                            val record = transactionRecordDao.getRecordByTxHash(txHash)
                            val txFee = if (record != null && gasUsed.isNotEmpty()) {
                                val gasUsedBig = java.math.BigInteger(gasUsed)
                                val gasPriceBig = java.math.BigInteger(record.gasPrice)
                                val feeWei = gasUsedBig.multiply(gasPriceBig)
                                // 转换为 KAVA (18 decimals)
                                val feeKava = feeWei.toBigDecimal().divide(java.math.BigDecimal("1000000000000000000"))
                                feeKava.stripTrailingZeros().toPlainString()
                            } else ""

                            updateTransactionStatus(
                                txHash = txHash,
                                status = status,
                                gasUsed = gasUsed,
                                txFee = txFee,
                                blockNumber = blockNumber,
                                confirmedAt = System.currentTimeMillis()
                            )
                            android.util.Log.d("TssRepository", "[TX-CONFIRM] Transaction $status: blockNumber=$blockNumber")
                            Result.success(true)
                        } else {
                            android.util.Log.d("TssRepository", "[TX-CONFIRM] Receipt not yet available")
                            Result.success(false)  // 交易还未确认
                        }
                    },
                    onFailure = { e ->
                        android.util.Log.e("TssRepository", "[TX-CONFIRM] Error: ${e.message}")
                        Result.failure(e)
                    }
                )
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "[TX-CONFIRM] Exception: ${e.message}", e)
                Result.failure(e)
            }
        }
    }

    // ========== Transaction History Sync Methods ==========

    /**
     * 同步 ERC-20 代币的历史交易
     * 使用 eth_getLogs 查询 Transfer 事件
     */
    /**
     * 获取当前区块号
     */
    private suspend fun getCurrentBlockNumber(client: okhttp3.OkHttpClient, rpcUrl: String): String? {
        return try {
            val jsonMediaType = "application/json; charset=utf-8".toMediaType()
            val request = """
                {
                    "jsonrpc": "2.0",
                    "method": "eth_blockNumber",
                    "params": [],
                    "id": 1
                }
            """.trimIndent()

            val response = client.newCall(
                okhttp3.Request.Builder()
                    .url(rpcUrl)
                    .post(request.toRequestBody(jsonMediaType))
                    .build()
            ).execute()

            val body = response.body?.string()
            if (body != null) {
                val json = com.google.gson.JsonParser.parseString(body).asJsonObject
                json.get("result")?.asString
            } else null
        } catch (e: Exception) {
            android.util.Log.w("TssRepository", "[BLOCK-NUMBER] Failed to get current block: ${e.message}")
            null
        }
    }

    suspend fun syncERC20TransactionHistory(
        shareId: Long,
        address: String,
        contractAddress: String,
        tokenType: TokenType,
        rpcUrl: String
    ): Result<Int> {
        return withContext(Dispatchers.IO) {
            try {
                android.util.Log.d("TssRepository", "[SYNC-ERC20] Syncing ${tokenType.name} history for $address")

                val client = okhttp3.OkHttpClient.Builder()
                    .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
                    .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
                    .build()
                val jsonMediaType = "application/json; charset=utf-8".toMediaType()

                // 获取当前区块号
                val currentBlockHex = getCurrentBlockNumber(client, rpcUrl)
                val currentBlock = currentBlockHex?.removePrefix("0x")?.toLongOrNull(16) ?: 0L

                // RPC 节点限制：每次最多查询 10000 个区块
                // 只扫描最近 20000 个区块（约 2 批，20-30 秒）
                val maxBlocksPerRequest = 10000L
                val totalBlocksToScan = 20000L
                val startBlock = if (currentBlock > totalBlocksToScan) {
                    currentBlock - totalBlocksToScan
                } else {
                    0L
                }

                android.util.Log.d("TssRepository", "[SYNC-ERC20] Current block: $currentBlock")
                android.util.Log.d("TssRepository", "[SYNC-ERC20] Scanning blocks $startBlock to $currentBlock (${currentBlock - startBlock} blocks, ~${(currentBlock - startBlock) / maxBlocksPerRequest} batches)")

                // Transfer event signature: keccak256("Transfer(address,address,uint256)")
                val transferTopic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"

                // 地址需要补齐到 32 字节（左侧补零）
                val paddedAddress = "0x" + address.removePrefix("0x").lowercase().padStart(64, '0')

                var totalSynced = 0

                // 分批查询，每批 10000 个区块
                var fromBlock = startBlock
                var batchCount = 0
                while (fromBlock < currentBlock) {
                    val toBlock = minOf(fromBlock + maxBlocksPerRequest - 1, currentBlock)
                    batchCount++

                    val fromBlockHex = "0x${fromBlock.toString(16)}"
                    val toBlockHex = "0x${toBlock.toString(16)}"

                    android.util.Log.d("TssRepository", "[SYNC-ERC20] Batch #$batchCount: blocks $fromBlock-$toBlock")

                    // 并行查询 SENT 和 RECEIVED（提速 2倍）
                    val batchSynced = kotlinx.coroutines.coroutineScope {
                        val sentDeferred = async {
                            val sentRequest = """
                                {
                                    "jsonrpc": "2.0",
                                    "method": "eth_getLogs",
                                    "params": [{
                                        "address": "$contractAddress",
                                        "topics": ["$transferTopic", "$paddedAddress", null],
                                        "fromBlock": "$fromBlockHex",
                                        "toBlock": "$toBlockHex"
                                    }],
                                    "id": 1
                                }
                            """.trimIndent()

                            val sentResponse = client.newCall(
                                okhttp3.Request.Builder()
                                    .url(rpcUrl)
                                    .post(sentRequest.toRequestBody(jsonMediaType))
                                    .build()
                            ).execute()

                            val sentBody = sentResponse.body?.string()
                            if (sentBody != null) {
                                parseAndSaveTransferLogs(sentBody, shareId, address, tokenType, "SENT")
                            } else 0
                        }

                        val receivedDeferred = async {
                            val receivedRequest = """
                                {
                                    "jsonrpc": "2.0",
                                    "method": "eth_getLogs",
                                    "params": [{
                                        "address": "$contractAddress",
                                        "topics": ["$transferTopic", null, "$paddedAddress"],
                                        "fromBlock": "$fromBlockHex",
                                        "toBlock": "$toBlockHex"
                                    }],
                                    "id": 2
                                }
                            """.trimIndent()

                            val receivedResponse = client.newCall(
                                okhttp3.Request.Builder()
                                    .url(rpcUrl)
                                    .post(receivedRequest.toRequestBody(jsonMediaType))
                                    .build()
                            ).execute()

                            val receivedBody = receivedResponse.body?.string()
                            if (receivedBody != null) {
                                parseAndSaveTransferLogs(receivedBody, shareId, address, tokenType, "RECEIVED")
                            } else 0
                        }

                        // 等待两个查询都完成
                        sentDeferred.await() + receivedDeferred.await()
                    }

                    totalSynced += batchSynced
                    fromBlock = toBlock + 1
                }

                android.util.Log.d("TssRepository", "[SYNC-ERC20] Completed $batchCount batches, synced $totalSynced ${tokenType.name} transactions")
                Result.success(totalSynced)
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "[SYNC-ERC20] Error: ${e.message}", e)
                Result.failure(e)
            }
        }
    }

    /**
     * 解析 Transfer 日志并保存到数据库
     */
    private suspend fun parseAndSaveTransferLogs(
        responseBody: String,
        shareId: Long,
        address: String,
        tokenType: TokenType,
        direction: String
    ): Int {
        var count = 0
        try {
            val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
            val error = json.get("error")?.asJsonObject
            if (error != null) {
                android.util.Log.e("TssRepository", "[SYNC-ERC20] RPC error: ${error.get("message")?.asString}")
                return 0
            }
            val result = json.getAsJsonArray("result")
            if (result == null) {
                android.util.Log.w("TssRepository", "[SYNC-ERC20] No result array in response")
                return 0
            }
            android.util.Log.d("TssRepository", "[SYNC-ERC20] Found ${result.size()} logs for $direction ${tokenType.name}")

            for (log in result) {
                try {
                    val logObj = log.asJsonObject
                    val txHash = logObj.get("transactionHash")?.asString ?: continue
                    val topics = logObj.getAsJsonArray("topics")
                    val data = logObj.get("data")?.asString ?: "0x0"
                    val blockNumber = logObj.get("blockNumber")?.asString

                    // 检查是否已存在
                    val existing = transactionRecordDao.getRecordByTxHash(txHash)
                    if (existing != null) continue

                    // 解析 from 和 to 地址 (topics[1] 和 topics[2])
                    val fromAddress = if (topics.size() > 1) {
                        "0x" + topics[1].asString.removePrefix("0x").takeLast(40).lowercase()
                    } else continue
                    val toAddress = if (topics.size() > 2) {
                        "0x" + topics[2].asString.removePrefix("0x").takeLast(40).lowercase()
                    } else continue

                    android.util.Log.d("TssRepository", "[SYNC-ERC20] Processing tx $txHash: $fromAddress -> $toAddress")

                    // 解析金额 (data 字段)
                    val valueHex = data.removePrefix("0x")
                    val valueBig = if (valueHex.isNotEmpty()) {
                        java.math.BigInteger(valueHex, 16)
                    } else java.math.BigInteger.ZERO

                    // 转换为人类可读格式 (根据代币精度)
                    val decimals = TokenConfig.getDecimals(tokenType)
                    val divisor = java.math.BigDecimal.TEN.pow(decimals)
                    val humanReadable = valueBig.toBigDecimal().divide(divisor).stripTrailingZeros().toPlainString()

                    // 解析区块号
                    val blockNum = blockNumber?.let {
                        java.math.BigInteger(it.removePrefix("0x"), 16).toLong()
                    }

                    // 保存记录
                    val entity = TransactionRecordEntity(
                        shareId = shareId,
                        fromAddress = fromAddress,
                        toAddress = toAddress,
                        amount = humanReadable,
                        tokenType = tokenType.name,
                        txHash = txHash,
                        gasPrice = "0",  // 历史交易无法获取 gasPrice
                        status = "CONFIRMED",
                        direction = direction,
                        blockNumber = blockNum,
                        confirmedAt = System.currentTimeMillis()
                    )
                    transactionRecordDao.insertRecord(entity)
                    count++
                } catch (e: Exception) {
                    android.util.Log.w("TssRepository", "[SYNC-ERC20] Error parsing log: ${e.message}")
                }
            }
        } catch (e: Exception) {
            android.util.Log.e("TssRepository", "[SYNC-ERC20] Error parsing response: ${e.message}")
        }
        return count
    }

    /**
     * 同步原生 KAVA 历史交易
     * 使用 KavaScan API
     */
    suspend fun syncNativeTransactionHistory(
        shareId: Long,
        address: String,
        networkType: NetworkType = NetworkType.MAINNET
    ): Result<Int> {
        return withContext(Dispatchers.IO) {
            try {
                android.util.Log.d("TssRepository", "[SYNC-KAVA] Syncing native KAVA history for $address")

                val baseUrl = if (networkType == NetworkType.TESTNET) {
                    "https://testnet.kavascan.com"
                } else {
                    "https://kavascan.com"
                }

                val client = okhttp3.OkHttpClient.Builder()
                    .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
                    .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
                    .build()

                // KavaScan API 格式（类似 Etherscan）
                val apiUrl = "$baseUrl/api?module=account&action=txlist&address=$address&sort=desc"

                val request = okhttp3.Request.Builder()
                    .url(apiUrl)
                    .get()
                    .build()

                val response = client.newCall(request).execute()
                val responseBody = response.body?.string()

                if (responseBody == null) {
                    android.util.Log.w("TssRepository", "[SYNC-KAVA] Empty response from KavaScan")
                    return@withContext Result.success(0)
                }

                android.util.Log.d("TssRepository", "[SYNC-KAVA] Response code: ${response.code}, body length: ${responseBody.length}")
                android.util.Log.d("TssRepository", "[SYNC-KAVA] Response preview: ${responseBody.take(500)}")

                var count = 0
                try {
                    val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                    val status = json.get("status")?.asString
                    val message = json.get("message")?.asString
                    val result = json.getAsJsonArray("result")

                    android.util.Log.d("TssRepository", "[SYNC-KAVA] Parsed: status=$status, message=$message, resultSize=${result?.size() ?: "null"}")

                    if (status == "1" && result != null) {
                        for (tx in result) {
                            try {
                                val txObj = tx.asJsonObject
                                val txHash = txObj.get("hash")?.asString ?: continue
                                val fromAddr = txObj.get("from")?.asString ?: continue
                                val toAddr = txObj.get("to")?.asString ?: continue
                                val valueStr = txObj.get("value")?.asString ?: "0"
                                val gasPrice = txObj.get("gasPrice")?.asString ?: "0"
                                val gasUsed = txObj.get("gasUsed")?.asString ?: "0"
                                val blockNum = txObj.get("blockNumber")?.asString?.toLongOrNull()
                                val timestamp = txObj.get("timeStamp")?.asString?.toLongOrNull()?.times(1000)
                                val isError = txObj.get("isError")?.asString == "1"

                                // 检查是否已存在
                                val existing = transactionRecordDao.getRecordByTxHash(txHash)
                                if (existing != null) continue

                                // 转换金额 (Wei -> KAVA, 18 decimals)
                                val valueBig = java.math.BigInteger(valueStr)
                                val valueKava = valueBig.toBigDecimal()
                                    .divide(java.math.BigDecimal("1000000000000000000"))
                                    .stripTrailingZeros()
                                    .toPlainString()

                                // 计算交易费
                                val txFee = if (gasUsed.isNotEmpty() && gasPrice.isNotEmpty()) {
                                    val fee = java.math.BigInteger(gasUsed).multiply(java.math.BigInteger(gasPrice))
                                    fee.toBigDecimal()
                                        .divide(java.math.BigDecimal("1000000000000000000"))
                                        .stripTrailingZeros()
                                        .toPlainString()
                                } else ""

                                // 统一转换为 lowercase
                                val normalizedFrom = fromAddr.lowercase()
                                val normalizedTo = toAddr.lowercase()

                                // 判断方向
                                val direction = if (normalizedFrom == address.lowercase()) "SENT" else "RECEIVED"

                                val entity = TransactionRecordEntity(
                                    shareId = shareId,
                                    fromAddress = normalizedFrom,
                                    toAddress = normalizedTo,
                                    amount = valueKava,
                                    tokenType = TokenType.KAVA.name,
                                    txHash = txHash,
                                    gasPrice = gasPrice,
                                    gasUsed = gasUsed,
                                    txFee = txFee,
                                    status = if (isError) "FAILED" else "CONFIRMED",
                                    direction = direction,
                                    blockNumber = blockNum,
                                    createdAt = timestamp ?: System.currentTimeMillis(),
                                    confirmedAt = timestamp ?: System.currentTimeMillis()
                                )
                                transactionRecordDao.insertRecord(entity)
                                count++
                            } catch (e: Exception) {
                                android.util.Log.w("TssRepository", "[SYNC-KAVA] Error parsing tx: ${e.message}")
                            }
                        }
                    }
                } catch (e: Exception) {
                    android.util.Log.e("TssRepository", "[SYNC-KAVA] Error parsing response: ${e.message}")
                }

                android.util.Log.d("TssRepository", "[SYNC-KAVA] Synced $count native KAVA transactions")
                Result.success(count)
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "[SYNC-KAVA] Error: ${e.message}", e)
                Result.failure(e)
            }
        }
    }

    /**
     * 同步所有类型的历史交易
     * 首次导入钱包时调用
     */
    /**
     * 使用 Kavascan Etherscan 兼容 API 同步交易历史
     * 一次性获取地址的所有交易，无需分批扫描区块
     */
    suspend fun syncTransactionHistoryViaBlockScout(
        shareId: Long,
        address: String,
        networkType: NetworkType = NetworkType.MAINNET
    ): Result<Int> {
        return withContext(Dispatchers.IO) {
            try {
                android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Syncing via Kavascan API for $address")

                val baseUrl = if (networkType == NetworkType.TESTNET) {
                    "https://explorer.evm-alpha.kava.io"
                } else {
                    "https://kavascan.com"
                }

                val client = okhttp3.OkHttpClient.Builder()
                    .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
                    .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
                    .build()

                var totalSynced = 0

                // 代币合约地址映射
                val contractToTokenType = mapOf(
                    GreenPointsToken.CONTRACT_ADDRESS.lowercase() to TokenType.GREEN_POINTS,
                    EnergyPointsToken.CONTRACT_ADDRESS.lowercase() to TokenType.ENERGY_POINTS,
                    FuturePointsToken.CONTRACT_ADDRESS.lowercase() to TokenType.FUTURE_POINTS
                )

                // 1. 获取 ERC-20 代币交易 (tokentx)
                val tokenUrl = "$baseUrl/api?module=account&action=tokentx&address=$address&startblock=0&endblock=99999999&sort=desc"
                android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Fetching token tx: $tokenUrl")

                val tokenRequest = okhttp3.Request.Builder().url(tokenUrl).get().build()
                val tokenResponse = client.newCall(tokenRequest).execute()
                val tokenBody = tokenResponse.body?.string()

                if (tokenBody != null && tokenResponse.code == 200) {
                    val tokenJson = com.google.gson.JsonParser.parseString(tokenBody).asJsonObject
                    val tokenStatus = tokenJson.get("status")?.asString
                    val tokenResult = tokenJson.getAsJsonArray("result")

                    if (tokenStatus == "1" && tokenResult != null) {
                        android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Processing ${tokenResult.size()} token transactions")

                        for (item in tokenResult) {
                            try {
                                val txObj = item.asJsonObject
                                val txHash = txObj.get("hash")?.asString ?: continue

                                // 检查是否已存在
                                val existing = transactionRecordDao.getRecordByTxHash(txHash)
                                if (existing != null) continue

                                val contractAddr = txObj.get("contractAddress")?.asString?.lowercase() ?: continue
                                val tokenType = contractToTokenType[contractAddr] ?: continue

                                val fromAddr = txObj.get("from")?.asString?.lowercase() ?: continue
                                val toAddr = txObj.get("to")?.asString?.lowercase() ?: continue
                                val value = txObj.get("value")?.asString ?: "0"
                                val decimals = txObj.get("tokenDecimal")?.asString?.toIntOrNull() ?: 18
                                val gasPrice = txObj.get("gasPrice")?.asString ?: "0"
                                val gasUsed = txObj.get("gasUsed")?.asString ?: "0"
                                val blockNumber = txObj.get("blockNumber")?.asString?.toLongOrNull()
                                val timestamp = txObj.get("timeStamp")?.asString?.toLongOrNull()?.times(1000)

                                val amount = java.math.BigInteger(value)
                                    .toBigDecimal()
                                    .divide(java.math.BigDecimal.TEN.pow(decimals))
                                    .stripTrailingZeros()
                                    .toPlainString()

                                val direction = if (fromAddr == address.lowercase()) "SENT" else "RECEIVED"

                                val entity = TransactionRecordEntity(
                                    shareId = shareId,
                                    fromAddress = fromAddr,
                                    toAddress = toAddr,
                                    amount = amount,
                                    tokenType = tokenType.name,
                                    txHash = txHash,
                                    gasPrice = gasPrice,
                                    gasUsed = gasUsed,
                                    txFee = "",
                                    status = "CONFIRMED",
                                    direction = direction,
                                    blockNumber = blockNumber,
                                    createdAt = timestamp ?: System.currentTimeMillis(),
                                    confirmedAt = timestamp ?: System.currentTimeMillis()
                                )
                                transactionRecordDao.insertRecord(entity)
                                totalSynced++
                            } catch (e: Exception) {
                                android.util.Log.w("TssRepository", "[SYNC-KAVASCAN] Error parsing token tx: ${e.message}")
                            }
                        }
                    } else {
                        android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Token tx response: status=$tokenStatus")
                    }
                } else {
                    android.util.Log.e("TssRepository", "[SYNC-KAVASCAN] Token tx request failed: code=${tokenResponse.code}")
                }

                // 2. 获取原生 KAVA 交易 (txlist)
                val txUrl = "$baseUrl/api?module=account&action=txlist&address=$address&startblock=0&endblock=99999999&sort=desc"
                android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Fetching native tx: $txUrl")

                val txRequest = okhttp3.Request.Builder().url(txUrl).get().build()
                val txResponse = client.newCall(txRequest).execute()
                val txBody = txResponse.body?.string()

                if (txBody != null && txResponse.code == 200) {
                    val txJson = com.google.gson.JsonParser.parseString(txBody).asJsonObject
                    val txStatus = txJson.get("status")?.asString
                    val txResult = txJson.getAsJsonArray("result")

                    if (txStatus == "1" && txResult != null) {
                        android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Processing ${txResult.size()} native transactions")

                        for (item in txResult) {
                            try {
                                val txObj = item.asJsonObject
                                val txHash = txObj.get("hash")?.asString ?: continue

                                // 检查是否已存在
                                val existing = transactionRecordDao.getRecordByTxHash(txHash)
                                if (existing != null) continue

                                val fromAddr = txObj.get("from")?.asString?.lowercase() ?: continue
                                val toAddr = txObj.get("to")?.asString?.lowercase() ?: continue
                                val value = txObj.get("value")?.asString ?: "0"
                                val gasPrice = txObj.get("gasPrice")?.asString ?: "0"
                                val gasUsed = txObj.get("gasUsed")?.asString ?: "0"
                                val blockNumber = txObj.get("blockNumber")?.asString?.toLongOrNull()
                                val timestamp = txObj.get("timeStamp")?.asString?.toLongOrNull()?.times(1000)
                                val isError = txObj.get("isError")?.asString

                                // 将 value 从 wei 转换为 KAVA
                                val amount = java.math.BigInteger(value)
                                    .toBigDecimal()
                                    .divide(java.math.BigDecimal("1000000000000000000"))
                                    .stripTrailingZeros()
                                    .toPlainString()

                                // 只保存有 value 的交易
                                if (java.math.BigInteger(value) > java.math.BigInteger.ZERO) {
                                    val direction = if (fromAddr == address.lowercase()) "SENT" else "RECEIVED"

                                    val entity = TransactionRecordEntity(
                                        shareId = shareId,
                                        fromAddress = fromAddr,
                                        toAddress = toAddr,
                                        amount = amount,
                                        tokenType = TokenType.KAVA.name,
                                        txHash = txHash,
                                        gasPrice = gasPrice,
                                        gasUsed = gasUsed,
                                        txFee = "",
                                        status = if (isError == "0") "CONFIRMED" else "FAILED",
                                        direction = direction,
                                        blockNumber = blockNumber,
                                        createdAt = timestamp ?: System.currentTimeMillis(),
                                        confirmedAt = timestamp ?: System.currentTimeMillis()
                                    )
                                    transactionRecordDao.insertRecord(entity)
                                    totalSynced++
                                }
                            } catch (e: Exception) {
                                android.util.Log.w("TssRepository", "[SYNC-KAVASCAN] Error parsing native tx: ${e.message}")
                            }
                        }
                    } else {
                        android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Native tx response: status=$txStatus")
                    }
                } else {
                    android.util.Log.e("TssRepository", "[SYNC-KAVASCAN] Native tx request failed: code=${txResponse.code}")
                }

                android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Completed: synced $totalSynced transactions")
                Result.success(totalSynced)
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "[SYNC-KAVASCAN] Error: ${e.message}", e)
                Result.failure(e)
            }
        }
    }

    suspend fun syncAllTransactionHistory(
        shareId: Long,
        address: String,
        rpcUrl: String,
        networkType: NetworkType = NetworkType.MAINNET
    ): Result<Int> {
        // 使用 BlockScout API 替代慢速的 eth_getLogs 扫描
        return syncTransactionHistoryViaBlockScout(shareId, address, networkType)
    }

    // ========== 2-of-3 服务器参与选项（新增功能，用于丢失设备后转出资产）==========
    // 新增日期：2026-01-27
    // 新增原因：允许 2-of-3 用户在丢失一个设备时，通过服务器参与签名转出资产
    // 影响范围：纯新增，不影响现有签名流程
    // 回滚方法：删除此段及相关 UI 代码即可恢复

    /**
     * 构建签名参与方列表（辅助方法）
     *
     * @param participants 所有参与方（从 getSessionStatus 获取）
     * @param currentPartyId 当前设备的 party ID
     * @param includeServerParties 是否包含服务器备份方（默认 false，保持现有行为）
     * @return 参与方列表 (partyId, partyIndex)
     *
     * 新增原因：将参与方过滤逻辑提取为可复用方法，支持选择性包含服务器
     * 默认行为：false - 过滤掉 co-managed-party-*（与现有逻辑一致）
     * 新行为：true - 只包含当前设备 + 服务器（用于丢失设备场景，总共2方）
     */
    private fun buildSigningParticipantList(
        participants: List<ParticipantStatusInfo>,
        currentPartyId: String,
        includeServerParties: Boolean = false
    ): List<Pair<String, Int>> {
        val filtered = if (includeServerParties) {
            // 设备丢失场景：只选择当前设备 + 服务器（共2方，满足 t=2）
            // 排除丢失的另一个用户设备
            android.util.Log.d("TssRepository", "[PARTICIPANT-LIST] Device lost mode: current device + server backup only")
            participants.filter {
                it.partyId == currentPartyId || it.partyId.startsWith("co-managed-party-")
            }.also {
                android.util.Log.d("TssRepository", "[PARTICIPANT-LIST] Selected ${it.size} parties for signing (current + server)")
                it.forEach { p ->
                    android.util.Log.d("TssRepository", "[PARTICIPANT-LIST]   - ${p.partyId.take(20)} (index=${p.partyIndex})")
                }
            }
        } else {
            // 过滤掉服务器方 - 现有默认行为（2个用户设备）
            android.util.Log.d("TssRepository", "[PARTICIPANT-LIST] Normal mode: excluding co-managed-party-* (default behavior)")
            participants.filter { !it.partyId.startsWith("co-managed-party-") }
        }
        return filtered.map { Pair(it.partyId, it.partyIndex) }
    }

    /**
     * 创建签名会话（支持选择是否包含服务器）
     *
     * 新增方法，不修改现有 createSignSession
     * 仅在 UI 层判断为 2-of-3 且用户主动勾选时调用此方法
     *
     * @param includeServerBackup 是否包含服务器备份参与方（仅 2-of-3 时使用）
     * @return SignSessionResult
     *
     * 使用场景：
     * - 2-of-3 用户丢失一个设备后，需要用剩余设备 + 服务器完成签名
     * - 用户在 UI 明确勾选"包含服务器备份"选项
     * - 其他配置（3-of-5等）不会调用此方法
     *
     * 安全性：
     * - UI 层限制仅 2-of-3 显示选项
     * - 服务器只有 1 个 key < t=2，无法单独控制钱包
     * - 用户主动选择，有操作审计日志
     */
    suspend fun createSignSessionWithOptions(
        shareId: Long,
        messageHash: String,
        password: String,
        initiatorName: String,
        includeServerBackup: Boolean = false  // 新增参数
    ): Result<SignSessionResult> {
        return withContext(Dispatchers.IO) {
            try {
                val shareEntity = shareRecordDao.getShareById(shareId)
                    ?: return@withContext Result.failure(Exception("Share not found"))

                val signingPartyIdForEvents = shareEntity.partyId

                android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Creating sign session with includeServerBackup=$includeServerBackup")
                android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Threshold: ${shareEntity.thresholdT}-of-${shareEntity.thresholdN}")
                ensureSessionEventSubscriptionActive(signingPartyIdForEvents)

                android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Creating sign session for share: ${shareEntity.sessionId}")

                // Step 1: Get keygen session status to get all participants
                val keygenStatusResult = getSessionStatus(shareEntity.sessionId)
                if (keygenStatusResult.isFailure) {
                    return@withContext Result.failure(Exception("无法获取 keygen 会话的参与者信息: ${keygenStatusResult.exceptionOrNull()?.message}"))
                }
                val keygenStatus = keygenStatusResult.getOrThrow()

                if (keygenStatus.participants.isEmpty()) {
                    return@withContext Result.failure(Exception("无法获取 keygen 会话的参与者信息"))
                }

                // Step 2: Build participant list using helper method
                // 关键修改：使用辅助方法，根据 includeServerBackup 参数决定是否包含服务器
                // includeServerBackup=true 时：只选择当前设备 + 服务器（共2方，用于设备丢失场景）
                // includeServerBackup=false 时：选择2个用户设备（现有默认行为）
                val signingParties = buildSigningParticipantList(
                    keygenStatus.participants,
                    signingPartyIdForEvents,  // 传入当前设备的 party ID
                    includeServerBackup
                )

                val serverCount = keygenStatus.participants.count { it.partyId.startsWith("co-managed-party-") }
                android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Total participants: ${keygenStatus.participants.size}")
                android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Server parties: $serverCount")
                android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Signing parties: ${signingParties.size} (includeServer=$includeServerBackup)")
                signingParties.forEach { (id, index) ->
                    android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS]   party_id=${id.take(16)}, party_index=$index")
                }

                if (signingParties.size < shareEntity.thresholdT) {
                    return@withContext Result.failure(Exception(
                        "签名参与方不足: 需要 ${shareEntity.thresholdT} 个，但只有 ${signingParties.size} 个参与方"
                    ))
                }

                val jsonMediaType = "application/json; charset=utf-8".toMediaType()

                // 以下逻辑与 createSignSession 完全相同
                val partiesArray = com.google.gson.JsonArray().apply {
                    signingParties.forEach { (partyIdStr, partyIndex) ->
                        add(com.google.gson.JsonObject().apply {
                            addProperty("party_id", partyIdStr)
                            addProperty("party_index", partyIndex)
                        })
                    }
                }

                val walletName = "Wallet ${shareEntity.address.take(8)}...${shareEntity.address.takeLast(4)}"
                val cleanMessageHash = if (messageHash.startsWith("0x") || messageHash.startsWith("0X")) {
                    messageHash.substring(2)
                } else {
                    messageHash
                }
                val requestBody = com.google.gson.JsonObject().apply {
                    addProperty("keygen_session_id", shareEntity.sessionId)
                    addProperty("wallet_name", walletName)
                    addProperty("message_hash", cleanMessageHash)
                    add("parties", partiesArray)
                    addProperty("threshold_t", shareEntity.thresholdT)
                    addProperty("initiator_name", initiatorName)
                }.toString()

                val request = okhttp3.Request.Builder()
                    .url("$accountServiceUrl/api/v1/co-managed/sign")
                    .post(requestBody.toRequestBody(jsonMediaType))
                    .header("Content-Type", "application/json")
                    .build()

                android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Creating sign session: $requestBody")

                val response = httpClient.newCall(request).execute()
                val responseBody = response.body?.string()
                    ?: return@withContext Result.failure(Exception("空响应"))

                android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Create sign session response: $responseBody")

                if (!response.isSuccessful) {
                    val errorJson = try {
                        com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                    } catch (e: Exception) { null }
                    val errorMsg = errorJson?.get("message")?.asString
                        ?: errorJson?.get("error")?.asString
                        ?: "HTTP ${response.code}"
                    return@withContext Result.failure(Exception(errorMsg))
                }

                val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
                val sessionId = json.get("session_id").asString
                val inviteCode = json.get("invite_code").asString
                val thresholdT = json.get("threshold_t")?.asInt ?: shareEntity.thresholdT

                val originalPartyId = shareEntity.partyId

                val joinToken = if (json.has("join_tokens") && json.get("join_tokens").isJsonObject) {
                    val joinTokens = json.getAsJsonObject("join_tokens")
                    joinTokens.get(originalPartyId)?.asString ?: joinTokens.get("*")?.asString
                } else {
                    json.get("join_token")?.asString
                }

                val participants = signingParties.map { (pId, pIndex) ->
                    Participant(
                        partyId = pId,
                        partyIndex = pIndex,
                        name = if (pId == originalPartyId) initiatorName else "参与方 ${pIndex + 1}"
                    )
                }

                pendingSessionId = sessionId
                android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Set pendingSessionId=$sessionId for event matching (sign initiator)")

                val session = TssSession(
                    sessionId = sessionId,
                    sessionType = SessionType.SIGN,
                    thresholdT = thresholdT,
                    thresholdN = shareEntity.thresholdN,
                    participants = participants,
                    status = SessionStatus.WAITING,
                    inviteCode = inviteCode,
                    messageHash = messageHash
                )
                _currentSession.value = session
                _sessionStatus.value = SessionStatus.WAITING

                val signingPartyId = shareEntity.partyId
                currentSigningPartyId = signingPartyId
                android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Using signingPartyId=$signingPartyId (device partyId=$partyId)")

                var sessionAlreadyInProgress = false
                if (joinToken != null) {
                    android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Initiator auto-joining session...")
                    val myPartyIndex = signingParties.find { it.first == signingPartyId }?.second ?: shareEntity.partyIndex

                    val joinResult = grpcClient.joinSession(sessionId, signingPartyId, joinToken)
                    if (joinResult.isSuccess) {
                        val joinData = joinResult.getOrThrow()
                        android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Initiator joined: partyIndex=${joinData.partyIndex}, status=${joinData.sessionStatus}")

                        startMessageRouting(sessionId, myPartyIndex, signingPartyId)
                        ensureSessionEventSubscriptionActive(signingPartyId)

                        // CRITICAL: Cache sign info BEFORE session_started arrives
                        // This prevents race condition where session_started arrives before
                        // MainViewModel's result.fold callback completes
                        // Pattern matches server-party-co-managed's PendingSessionCache
                        pendingSignInfo = PendingSignInfo(
                            sessionId = sessionId,
                            shareId = shareId,
                            password = password,
                            messageHash = messageHash
                        )
                        signingTriggered = false  // Reset flag for new signing session
                        android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Cached pendingSignInfo for sessionId=$sessionId to handle session_started event")

                        if (joinData.sessionStatus == "in_progress") {
                            android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Session already in_progress, will trigger sign immediately")
                            sessionAlreadyInProgress = true
                        }
                    } else {
                        android.util.Log.w("TssRepository", "[CO-SIGN-OPTIONS] Initiator failed to join: ${joinResult.exceptionOrNull()?.message}")
                    }
                } else {
                    android.util.Log.w("TssRepository", "[CO-SIGN-OPTIONS] No join token found for initiator")
                }

                if (sessionAlreadyInProgress) {
                    val selectedParties = signingParties.map { it.first }
                    val eventData = SessionEventData(
                        eventId = "immediate_sign_start",
                        eventType = "session_started",
                        sessionId = sessionId,
                        thresholdN = shareEntity.thresholdN,
                        thresholdT = thresholdT,
                        selectedParties = selectedParties,
                        joinTokens = emptyMap(),
                        messageHash = messageHash
                    )
                    sessionEventCallback?.invoke(eventData)
                }

                Result.success(SignSessionResult(
                    sessionId = sessionId,
                    inviteCode = inviteCode,
                    sessionAlreadyInProgress = sessionAlreadyInProgress
                ))
            } catch (e: Exception) {
                android.util.Log.e("TssRepository", "[CO-SIGN-OPTIONS] Create sign session failed", e)
                Result.failure(e)
            }
        }
    }
    // ========== 2-of-3 服务器参与选项结束 ==========
}

private fun ShareRecordEntity.toShareRecord() = ShareRecord(
    id = id,
    sessionId = sessionId,
    publicKey = publicKey,
    encryptedShare = encryptedShare,
    thresholdT = thresholdT,
    thresholdN = thresholdN,
    partyIndex = partyIndex,
    partyId = partyId,
    address = address,
    createdAt = createdAt
)
