package com.tsmobile.app.viewmodel

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.tsmobile.app.ConnectionService
import com.tsmobile.app.NetworkMonitor
import com.tsmobile.app.TSBridge
import com.tsmobile.app.data.*
import com.tsmobile.app.ui.theme.ThemeMode
import teamspeak.TextMsg
import teamspeak.Client
import teamspeak.Teamspeak
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import androidx.core.app.NotificationCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import com.tsmobile.app.App
import com.tsmobile.app.MainActivity
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json

/**
 * 连接按钮状态枚举。
 */
enum class ConnectState {
    IDLE,       // 空闲：等待用户输入并点击
    CONNECTING, // 连接中：Connect + WaitConnected 进行中
    SUCCESS,    // 连接成功：跳转频道列表页
    FAILED,     // 连接失败：显示错误信息和重试
    TIMEOUT,    // 连接超时：显示超时提示
}

/**
 * 验证错误。
 */
data class ValidationErrors(
    val address: String? = null,
    val nickname: String? = null,
)

/**
 * 服务器配置页状态。
 */
data class ServerScreenState(
    val address: String = "",
    val nickname: String = "",
    val password: String = "",
    val connectState: ConnectState = ConnectState.IDLE,
    val errorMessage: String? = null,
    val validationErrors: ValidationErrors = ValidationErrors(),
    val isCheckingUpdate: Boolean = false,
)

/**
 * 更新检测信息。
 */
data class UpdateInfo(
    val versionName: String,
    val releaseUrl: String,
    val body: String = "",
)

class ServerViewModel(application: Application) : AndroidViewModel(application) {

    companion object {
        private const val TAG = "ServerViewModel"
        private const val MAX_RECONNECT_ATTEMPTS = 5
        private val RECONNECT_DELAYS = longArrayOf(2000, 4000, 8000, 16000, 30000)
    }

    private val recentStore = RecentConnectionsStore(application)
    private val identityStore = IdentityStore(application)

    // ChannelViewModel 引用（由 NavGraph 设置）
    var channelViewModel: ChannelViewModel? = null

    // ChatViewModel 引用（由 NavGraph 设置）
    var chatViewModel: ChatViewModel? = null

    // VoiceViewModel 引用（由 NavGraph 设置）
    var voiceViewModel: VoiceViewModel? = null

    // 页面状态
    private val _state = MutableStateFlow(ServerScreenState())
    val state: StateFlow<ServerScreenState> = _state.asStateFlow()

    // 最近连接列表
    val recents: StateFlow<List<RecentConnection>> =
        recentStore.observeRecents()
            .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

    // ── 连接生命周期状态（步骤 09） ──
    private val _connectionState = MutableStateFlow<ConnectionState?>(null)
    /** null = 未进入频道列表页，非 null = 当前连接生命周期状态 */
    val connectionState: StateFlow<ConnectionState?> = _connectionState.asStateFlow()

    private val _kickReason = MutableStateFlow("")
    val kickReason: StateFlow<String> = _kickReason.asStateFlow()

    // ── 服务器详情 ──
    private val _serverInfo = MutableStateFlow<ServerInfo?>(null)
    val serverInfo: StateFlow<ServerInfo?> = _serverInfo.asStateFlow()

    // ── 更新检测 ──
    private val updatePreferences = UpdatePreferences(application)

    private val _updateCheckResult = MutableStateFlow<com.tsmobile.app.ui.components.UpdateCheckResult?>(null)
    val updateCheckResult: StateFlow<com.tsmobile.app.ui.components.UpdateCheckResult?> = _updateCheckResult.asStateFlow()

    // ── 主题切换（架构 4.4） ──
    private val themePreferences = ThemePreferences(application)

    val themeMode: StateFlow<ThemeMode> = themePreferences.themeMode
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), ThemeMode.SYSTEM)

    /** 主题模式循环：跟随系统 → 亮色 → 暗色 → 跟随系统 */
    fun toggleTheme() {
        viewModelScope.launch {
            val next = when (themeMode.value) {
                ThemeMode.SYSTEM -> ThemeMode.LIGHT
                ThemeMode.LIGHT -> ThemeMode.DARK
                ThemeMode.DARK -> ThemeMode.SYSTEM
            }
            themePreferences.setThemeMode(next)
        }
    }

    // ── Poke 通知 ──
    private val _pokeNotification = MutableStateFlow<com.tsmobile.app.data.PokeEvent?>(null)
    val pokeNotification: StateFlow<com.tsmobile.app.data.PokeEvent?> = _pokeNotification.asStateFlow()

    private val _showPokeNotification = MutableStateFlow(false)
    val showPokeNotification: StateFlow<Boolean> = _showPokeNotification.asStateFlow()

    // ── 重连状态 ──
    private var reconnectAttempt = 0
    private var reconnectJob: Job? = null
    private var lastConnectParams: ConnectParams? = null

    /** 断联前的频道 ID，用于伪重连后自动回到原频道（空字符串=不自动移动） */
    private var lastChannelId: String = ""

    // ── 网络监控 ──
    private val networkMonitor = NetworkMonitor(application)

    /** 保存连接参数，供重连使用 */
    private data class ConnectParams(
        val host: String,
        val nickname: String,
        val password: String,
        val defaultChannel: String = "",
        val defaultChannelPassword: String = "",
    )

    init {
        // 启动网络监控（持续运行，检测网络切换）
        networkMonitor.start()

        // 启动时自动检查更新
        checkForUpdate()

        // 网络变动检测：
        //   任何 NetworkCallback 触发 + 前台 + 已连接 → 重连（UDP socket 绑定在旧 IP 上已死）
        //   后台忽略：Android 挂起应用时可能限制网络导致误触发，
        //     真正的断线由 Go idle timeout（60s 无包）兜底，或回到前台时检测
        //
        //   两层 collector：
        //   1. networkChangeCount → 任何网络事件 → Connected 就重连（仅前台）
        //   2. isAvailable false→true → WaitingForNetwork 时网络恢复 → 重连（不限前后台）
        viewModelScope.launch {
            var firstEmission = true
            networkMonitor.networkChangeCount.collect { _ ->
                if (firstEmission) { firstEmission = false; return@collect }
                // 后台忽略：防止 Android 后台网络限制导致 NetworkCallback 误触发
                if (!isAppInForeground()) {
                    android.util.Log.d(TAG, "Network change ignored (app in background)")
                    return@collect
                }
                if (_connectionState.value is ConnectionState.Connected) {
                    android.util.Log.w(TAG, "Network changed while connected, reconnecting")
                    val chId = Repository.currentChannelId.value
                    if (chId != "0" && chId.isNotEmpty()) {
                        lastChannelId = chId
                    }
                    _connectionState.value = ConnectionState.WaitingForNetwork()
                    ConnectionService.stop(getApplication())
                    if (networkMonitor.isAvailable.value) {
                        startReconnect("网络变化，正在重连...")
                    }
                    // 若当前不可用 → 等 isAvailable 恢复 → 第二个 collector 触发
                }
            }
        }

        viewModelScope.launch {
            var wasAvailable = networkMonitor.isAvailable.value
            networkMonitor.isAvailable.collect { available ->
                if (available && !wasAvailable &&
                    _connectionState.value is ConnectionState.WaitingForNetwork
                ) {
                    android.util.Log.i(TAG, "Network recovered, starting reconnect")
                    startReconnect("网络变化，正在重连...")
                }
                wasAvailable = available
            }
        }
    }

    /** 应用是否在前台（RESUMED 状态） */
    private fun isAppInForeground(): Boolean =
        ProcessLifecycleOwner.get().lifecycle.currentState == Lifecycle.State.RESUMED

    override fun onCleared() {
        super.onCleared()
        networkMonitor.stop()
    }

    // --- 更新检测 ---

    /**
     * 检查 GitHub/Gitea 是否有新版本。
     * 用 semver 方式比较版本号，每次启动最多检查一次（1小时去重）。
     */
    fun checkForUpdate() {
        if (_state.value.isCheckingUpdate) {
            android.util.Log.d(TAG, "Update check: already in progress, skipping")
            return
        }
        android.util.Log.d(TAG, "Update check: starting...")
        _state.update { it.copy(isCheckingUpdate = true) }

        viewModelScope.launch {
            try {
                // 去重：1小时内不重复检查
                val lastCheckTime = updatePreferences.lastCheckTimestamp.first()
                val oneHourMs = 60 * 60 * 1000L
                val elapsed = System.currentTimeMillis() - lastCheckTime
                if (elapsed < oneHourMs) {
                    val waitMinutes = (oneHourMs - elapsed) / 60_000
                    android.util.Log.d(TAG, "Update check: debounced (last check was ${elapsed / 60_000}min ago, can retry in ${waitMinutes}min)")
                    _state.update { it.copy(isCheckingUpdate = false) }
                    return@launch
                }

                // 获取本地版本
                val app = getApplication<android.app.Application>()
                @Suppress("DEPRECATION")
                val pi = app.packageManager.getPackageInfo(app.packageName, 0)
                val localVersion = pi.versionName ?: "0"
                android.util.Log.d(TAG, "Update check: local version=$localVersion")

                // 获取远程版本
                val release = UpdateChecker.fetchLatestRelease()
                updatePreferences.setLastCheckTimestamp(System.currentTimeMillis())

                if (release == null) {
                    android.util.Log.d(TAG, "Update check: no release info from server (network error or no release yet)")
                } else if (UpdateChecker.compareVersion(release.versionName, localVersion) > 0) {
                    android.util.Log.d(TAG, "Update check: new version available! remote=${release.versionName} > local=$localVersion")
                    // 检查是否已被用户忽略（仅首次启动自动弹窗时检查）
                    val dismissed = updatePreferences.dismissedVersion.first()
                    if (release.versionName != dismissed) {
                        android.util.Log.d(TAG, "Update check: showing update dialog for version ${release.versionName}")
                        _updateCheckResult.value =
                            com.tsmobile.app.ui.components.UpdateCheckResult.UpdateAvailable(
                                UpdateInfo(
                                    versionName = release.versionName,
                                    releaseUrl = release.releaseUrl,
                                    body = release.body,
                                )
                            )
                    } else {
                        android.util.Log.d(TAG, "Update check: version ${release.versionName} was previously dismissed, skipping")
                    }
                } else {
                    android.util.Log.d(TAG, "Update check: no newer version (remote=${release.versionName} <= local=$localVersion)")
                }
                _state.update { it.copy(isCheckingUpdate = false) }
            } catch (e: Exception) {
                android.util.Log.w(TAG, "Update check: failed", e)
                _state.update { it.copy(isCheckingUpdate = false) }
            }
        }
    }

    /**
     * 手动检查更新（弹出对话框展示结果）。
     * 不走去重逻辑，每次点击都会实际请求。
     */
    fun manualCheckUpdate() {
        if (_updateCheckResult.value is com.tsmobile.app.ui.components.UpdateCheckResult.Checking) return

        _updateCheckResult.value = com.tsmobile.app.ui.components.UpdateCheckResult.Checking
        _state.update { it.copy(isCheckingUpdate = true) }

        viewModelScope.launch {
            try {
                // 获取本地版本
                val app = getApplication<android.app.Application>()
                @Suppress("DEPRECATION")
                val pi = app.packageManager.getPackageInfo(app.packageName, 0)
                val localVersion = pi.versionName ?: "0"
                android.util.Log.d(TAG, "Manual update check: local version=$localVersion")

                // 获取远程版本
                val release = UpdateChecker.fetchLatestRelease()
                updatePreferences.setLastCheckTimestamp(System.currentTimeMillis())

                if (release == null) {
                    android.util.Log.d(TAG, "Manual update check: server returned no release info")
                    _updateCheckResult.value = com.tsmobile.app.ui.components.UpdateCheckResult.Error("")
                } else if (UpdateChecker.compareVersion(release.versionName, localVersion) > 0) {
                    // 检查是否已被忽略
                    val dismissed = updatePreferences.dismissedVersion.first()
                    if (release.versionName != dismissed) {
                        android.util.Log.d(TAG, "Manual update check: new version ${release.versionName} available!")
                        _updateCheckResult.value = com.tsmobile.app.ui.components.UpdateCheckResult.UpdateAvailable(
                            UpdateInfo(
                                versionName = release.versionName,
                                releaseUrl = release.releaseUrl,
                                body = release.body,
                            )
                        )
                    } else {
                        // 已被忽略但用户主动检查，也显示有新版本（覆盖忽略状态）
                        android.util.Log.d(TAG, "Manual update check: version ${release.versionName} available (previously dismissed, showing anyway)")
                        _updateCheckResult.value = com.tsmobile.app.ui.components.UpdateCheckResult.UpdateAvailable(
                            UpdateInfo(
                                versionName = release.versionName,
                                releaseUrl = release.releaseUrl,
                                body = release.body,
                            )
                        )
                    }
                } else {
                    android.util.Log.d(TAG, "Manual update check: already latest (remote=${release.versionName} <= local=$localVersion)")
                    _updateCheckResult.value = com.tsmobile.app.ui.components.UpdateCheckResult.NoUpdate
                }
            } catch (e: Exception) {
                android.util.Log.w(TAG, "Manual update check: failed", e)
                _updateCheckResult.value = com.tsmobile.app.ui.components.UpdateCheckResult.Error(
                    e.localizedMessage ?: "网络连接失败"
                )
            }
        }
    }

    /**
     * 关闭更新检查弹窗。
     */
    fun closeUpdateDialog() {
        _updateCheckResult.value = null
        _state.update { it.copy(isCheckingUpdate = false) }
    }

    /**
     * 在浏览器中打开发布页面。
     */
    fun openReleaseUrl(url: String) {
        try {
            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            }
            getApplication<android.app.Application>().startActivity(intent)
        } catch (e: Exception) {
            android.util.Log.w(TAG, "Failed to open release URL", e)
        }
    }

    /**
     * 忽略当前版本的更新提示（持久化，同版本不再提示）。
     */
    fun dismissUpdate() {
        val dialogResult = _updateCheckResult.value
        if (dialogResult is com.tsmobile.app.ui.components.UpdateCheckResult.UpdateAvailable) {
            viewModelScope.launch {
                updatePreferences.setDismissedVersion(dialogResult.info.versionName)
            }
            _updateCheckResult.value = null
            _state.update { it.copy(isCheckingUpdate = false) }
        }
    }

    // --- 输入更新 ---

    fun updateAddress(value: String) {
        _state.update { it.copy(address = value, errorMessage = null) }
    }

    fun updateNickname(value: String) {
        _state.update { it.copy(nickname = value, errorMessage = null) }
    }

    fun updatePassword(value: String) {
        _state.update { it.copy(password = value) }
    }

    // --- 连接 ---

    fun connect() {
        val current = _state.value
        if (current.connectState == ConnectState.CONNECTING) return

        // 输入验证
        val config = ServerConfig(
            address = current.address.trim(),
            nickname = current.nickname.trim(),
            password = current.password,
        )
        val errors = validate(config)
        if (errors.address != null || errors.nickname != null) {
            _state.update {
                it.copy(
                    validationErrors = errors,
                    connectState = ConnectState.IDLE,
                    errorMessage = errors.address ?: errors.nickname,
                )
            }
            return
        }

        // 立即进入连接中状态，UI 即刻响应
        _state.update {
            it.copy(
                connectState = ConnectState.CONNECTING,
                errorMessage = null,
                validationErrors = ValidationErrors(),
            )
        }

        doConnect()
    }

    /**
     * 执行实际连接逻辑。
     * 调用前必须已设置 connectState = CONNECTING。
     */
    private fun doConnect() {
        val current = _state.value
        val config = ServerConfig(
            address = current.address.trim(),
            nickname = current.nickname.trim(),
            password = current.password,
        )

        // 保存连接参数供重连使用
        lastConnectParams = ConnectParams(
            host = config.address,
            nickname = config.nickname,
            password = config.password,
            defaultChannel = config.defaultChannel,
            defaultChannelPassword = config.defaultChannelPassword,
        )

        // 异步连接（阻塞调用放到 IO 线程，不阻塞 UI）
        viewModelScope.launch {
            val identity = withContext(Dispatchers.IO) {
                identityStore.getOrCreate { Teamspeak.generateIdentity(8) }
            }
            if (identity.isBlank()) {
                _state.update {
                    it.copy(
                        connectState = ConnectState.FAILED,
                        errorMessage = "身份生成失败",
                    )
                }
                return@launch
            }

            val result = withContext(Dispatchers.IO) {
                TSBridge.connectWithIdentity(
                    identity = identity,
                    host = config.address,
                    nickname = config.nickname,
                    password = config.password,
                    defaultChannel = config.defaultChannel,
                    defaultChannelPassword = config.defaultChannelPassword,
                    callbacks = createBridgeCallbacks(),
                )
            }

            if (result.isEmpty()) {
                // 连接成功（实际成功由 onConnected 回调确认）
                // 此处 Connect 已成功启动，等待 WaitConnected
            } else {
                // 连接失败
                val errorMsg = classifyError(result)
                _state.update {
                    it.copy(
                        connectState = ConnectState.FAILED,
                        errorMessage = errorMsg,
                    )
                }

                // 记录到最近连接（标记失败）
                recentStore.addRecent(
                    RecentConnection(
                        address = config.address,
                        nickname = config.nickname,
                        password = config.password,
                        lastConnectedAt = System.currentTimeMillis(),
                        lastSucceeded = false,
                    )
                )
            }
        }
    }

    /**
     * 最近连接快速连接。
     * 自动填充所有字段并立即进入连接中状态，再触发连接。
     */
    fun quickConnect(recent: RecentConnection) {
        // 防止连接中重复触发
        if (_state.value.connectState == ConnectState.CONNECTING) return

        // 先填充字段并立即切换到 CONNECTING，UI 即刻响应
        _state.update {
            it.copy(
                address = recent.address,
                nickname = recent.nickname,
                password = recent.password,
                connectState = ConnectState.CONNECTING,
                errorMessage = null,
                validationErrors = ValidationErrors(),
            )
        }
        doConnect()
    }

    fun removeRecent(recent: RecentConnection) {
        viewModelScope.launch {
            recentStore.removeRecent(recent.address, recent.nickname)
        }
    }

    fun clearRecents() {
        viewModelScope.launch {
            recentStore.clearAll()
        }
    }

    // --- 服务器详情 & Poke（步骤 11） ---

    /**
     * 获取服务器详情信息
     */
    fun fetchServerInfo() {
        viewModelScope.launch {
            try {
                val json = withContext(Dispatchers.IO) {
                    TSBridge.getServerInfoJSON()
                }
                if (json.isNotEmpty() && json != "{}") {
                    _serverInfo.value = Json.decodeFromString<ServerInfo>(json)
                }
            } catch (e: Exception) {
                android.util.Log.e(TAG, "Failed to fetch server info", e)
            }
        }
    }

    /**
     * 向指定用户发送 Poke
     */
    fun pokeClient(clientId: Int, message: String) {
        viewModelScope.launch {
            try {
                val error = withContext(Dispatchers.IO) {
                    TSBridge.poke(clientId.toString(), message)
                }
                if (error.isNotEmpty()) {
                    android.util.Log.w(TAG, "Poke failed: $error")
                } else {
                    android.util.Log.d(TAG, "Poke sent to client $clientId")
                }
            } catch (e: Exception) {
                android.util.Log.e(TAG, "Poke failed", e)
            }
        }
    }

    /**
     * 踢出客户端
     * @param clientId 客户端 ID
     * @param reasonID 4=频道踢出, 5=服务器踢出
     * @param reasonMsg 踢出原因
     */
    fun kickClient(clientId: Int, reasonID: Int, reasonMsg: String = "") {
        viewModelScope.launch {
            try {
                val error = withContext(Dispatchers.IO) {
                    TSBridge.kickClient(clientId.toString(), reasonID, reasonMsg)
                }
                if (error.isNotEmpty()) {
                    android.util.Log.w(TAG, "Kick failed: $error")
                }
            } catch (e: Exception) {
                android.util.Log.e(TAG, "Kick failed", e)
            }
        }
    }

    /**
     * 处理收到的 Poke 事件
     */
    fun handlePoked(event: com.tsmobile.app.data.PokeEvent) {
        android.util.Log.d(TAG, "Poked by ${event.invokerName}: ${event.message}")

        _pokeNotification.value = event
        _showPokeNotification.value = true

        // 震动反馈
        triggerVibration()

        // app 在后台时发送系统通知
        val isInForeground = ProcessLifecycleOwner.get().lifecycle.currentState == Lifecycle.State.RESUMED
        if (!isInForeground) {
            sendPokeSystemNotification(event)
        }

        // 自动隐藏 app 内气泡（5秒后）
        viewModelScope.launch {
            delay(5000)
            dismissPokeNotification()
        }
    }

    fun dismissPokeNotification() {
        _showPokeNotification.value = false
        _pokeNotification.value = null
    }

    /**
     * 清除所有消息和 Poke 系统通知。
     * 当用户主动回到 app 时调用，防止旧通知中的频道 ID 与实际频道不一致。
     */
    fun clearNotifications() {
        val nm = getApplication<Application>().getSystemService(NotificationManager::class.java) ?: return
        synchronized(activeNotificationIds) {
            for (id in activeNotificationIds) {
                try { nm.cancel(id) } catch (_: Exception) {}
            }
            activeNotificationIds.clear()
        }
    }

    private var pokeNotificationId = 1000

    /** 追踪所有已发送的消息/Poke 通知 ID，用于 app 回到前台时批量清除 */
    private val activeNotificationIds = mutableSetOf<Int>()

    /**
     * 发送 Poke 系统通知（app 在后台时调用）。
     */
    private fun sendPokeSystemNotification(event: com.tsmobile.app.data.PokeEvent) {
        try {
            val context = getApplication<Application>()

            val pokeId = pokeNotificationId++
            val pendingIntent = PendingIntent.getActivity(
                context,
                pokeId,
                Intent(context, MainActivity::class.java).apply {
                    flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
                },
                PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
            )

            val text = if (event.message.isNotEmpty()) {
                "${event.invokerName} 戳了你一下：${event.message}"
            } else {
                "${event.invokerName} 戳了你一下"
            }

            val notification = NotificationCompat.Builder(context, App.POKE_CHANNEL_ID)
                .setSmallIcon(android.R.drawable.ic_dialog_info)
                .setContentTitle("TeamSpeak Poke")
                .setContentText(text)
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setAutoCancel(true)
                .setContentIntent(pendingIntent)
                .build()

            val nm = context.getSystemService(NotificationManager::class.java)
            nm.notify(pokeId, notification)
            synchronized(activeNotificationIds) { activeNotificationIds.add(pokeId) }
        } catch (e: Exception) {
            android.util.Log.e(TAG, "Failed to send poke notification", e)
        }
    }

    /**
     * 发送消息系统通知。
     * 仅在 app 前台且正在查看对应聊天页时跳过，其余情况均推送。
     */
    private fun sendMessageNotification(targetMode: Int, targetId: Long, senderName: String, content: String) {
        val activeKey = Repository.activeChatKey
        val msgKey = "${targetMode}_${targetId}"
        val isInForeground = ProcessLifecycleOwner.get().lifecycle.currentState == Lifecycle.State.RESUMED

        // app 在前台且正在查看该会话 → 跳过（消息已直接显示在 UI 中）
        if (isInForeground && activeKey == msgKey) return

        try {
            val context = getApplication<Application>()

            // 使用 msgKey 的 hashCode 作为 requestCode，确保不同会话的通知有各自的 PendingIntent
            val requestCode = msgKey.hashCode()
            val pendingIntent = PendingIntent.getActivity(
                context,
                requestCode,
                Intent(context, MainActivity::class.java).apply {
                    flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
                    putExtra(MainActivity.EXTRA_DEEP_LINK, MainActivity.DEEP_LINK_CHAT)
                    putExtra(MainActivity.EXTRA_TARGET_MODE, targetMode)
                    putExtra(MainActivity.EXTRA_TARGET_ID, targetId)
                },
                PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
            )

            val title = if (targetMode == 2) "频道消息" else "私聊消息"
            val text = "$senderName: $content"

            val notification = NotificationCompat.Builder(context, App.POKE_CHANNEL_ID)
                .setSmallIcon(android.R.drawable.ic_dialog_info)
                .setContentTitle(title)
                .setContentText(text)
                .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                .setAutoCancel(true)
                .setContentIntent(pendingIntent)
                .build()

            // 使用 targetMode_targetId 作为通知 ID，不同会话各自覆盖
            val notificationId = 2000 + (msgKey.hashCode() and 0xFFFF)
            val nm = context.getSystemService(NotificationManager::class.java)
            nm.notify(notificationId, notification)
            synchronized(activeNotificationIds) { activeNotificationIds.add(notificationId) }
        } catch (e: Exception) {
            android.util.Log.e(TAG, "Failed to send message notification", e)
        }
    }

    private fun triggerVibration() {
        try {
            val vibrator = getApplication<Application>().getSystemService(android.os.Vibrator::class.java)
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                vibrator?.vibrate(android.os.VibrationEffect.createOneShot(200, android.os.VibrationEffect.DEFAULT_AMPLITUDE))
            } else {
                @Suppress("DEPRECATION")
                vibrator?.vibrate(200)
            }
        } catch (e: Exception) {
            android.util.Log.w(TAG, "Vibration failed", e)
        }
    }

    // --- 断开连接与重连（步骤 09） ---

    /**
     * 主动断开连接（用户触发）
     *
     * 对应 docs/流程/07_断开连接.md：
     * "用户明确要求离开服务器"
     * "立即阻止新聊天、移动和语音请求"
     */
    fun disconnect() {
        android.util.Log.i(TAG, "User requested disconnect")

        // 0. 停止前台服务
        ConnectionService.stop(getApplication())

        // 0.5 重置网络监控（主动断线不需要自动重连）
        networkMonitor.reset()

        // 1. 停止重连
        reconnectJob?.let { it.cancel() }
        reconnectJob = null

        // 2. 设置断开中状态
        _connectionState.value = ConnectionState.Disconnecting

        // 3. 停止语音
        voiceViewModel?.stopVoice()

        // 4. 调用 SDK 断开（IO 线程，避免阻塞 UI）
        viewModelScope.launch(Dispatchers.IO) {
            try {
                TSBridge.disconnect()
            } catch (e: Exception) {
                android.util.Log.e(TAG, "Disconnect error", e)
            }
        }

        // 5. 清理会话
        Repository.clearSession()
        channelViewModel?.clearChannels()
        chatViewModel?.clearMessages()

        // 6. 重置状态
        _connectionState.value = null
        _state.update { it.copy(connectState = ConnectState.IDLE) }
        lastConnectParams = null
    }

    /**
     * 处理被踢后的重新连接
     */
    fun reconnectAfterKick() {
        val params = lastConnectParams ?: return
        _kickReason.value = ""
        _connectionState.value = null

        // 重新填充表单并连接
        _state.update {
            it.copy(
                address = params.host,
                nickname = params.nickname,
                password = params.password,
                connectState = ConnectState.IDLE,
            )
        }
        connect()
    }

    /**
     * 返回主页（被踢后或断开后）
     */
    fun backToHome() {
        reconnectJob?.let { it.cancel() }
        reconnectJob = null
        lastChannelId = ""
        networkMonitor.reset()
        _kickReason.value = ""
        _connectionState.value = null
        lastConnectParams = null
        Repository.clearSession()
        channelViewModel?.clearChannels()
        _state.update { it.copy(connectState = ConnectState.IDLE) }
    }

    /**
     * 启动自动重连
     *
     * 最多尝试 5 次，间隔递增（2s → 4s → 8s → 16s → 30s）
     */
    private fun startReconnect(reason: String) {
        reconnectAttempt = 0
        _connectionState.value = ConnectionState.Reconnecting(
            attempt = 0, reason = reason
        )

        reconnectJob = viewModelScope.launch {
            while (reconnectAttempt < MAX_RECONNECT_ATTEMPTS &&
                _connectionState.value is ConnectionState.Reconnecting
            ) {
                val delayMs = RECONNECT_DELAYS[reconnectAttempt]
                android.util.Log.i(TAG, "Reconnect attempt ${reconnectAttempt + 1}/$MAX_RECONNECT_ATTEMPTS in ${delayMs}ms")
                delay(delayMs)

                reconnectAttempt++
                _connectionState.value = ConnectionState.Reconnecting(
                    attempt = reconnectAttempt, reason = reason
                )

                val params = lastConnectParams ?: break
                val error = withContext(Dispatchers.IO) {
                    val identity = identityStore.getOrCreate { Teamspeak.generateIdentity(8) }
                    if (identity.isBlank()) return@withContext "身份生成失败"
                    TSBridge.connectWithIdentity(
                        identity = identity,
                        host = params.host,
                        nickname = params.nickname,
                        password = params.password,
                        defaultChannel = params.defaultChannel,
                        defaultChannelPassword = params.defaultChannelPassword,
                        callbacks = createBridgeCallbacks(),
                    )
                }

                if (error.isEmpty()) {
                    android.util.Log.i(TAG, "Reconnect succeeded on attempt $reconnectAttempt")
                    // 重连成功由 onConnected 回调处理
                    return@launch
                }

                android.util.Log.w(TAG, "Reconnect attempt $reconnectAttempt failed: $error")
            }

            // 重连失败
            android.util.Log.w(TAG, "All reconnect attempts failed")
            Repository.clearSession()
            channelViewModel?.clearChannels()
            _connectionState.value = ConnectionState.Disconnected(
                reason = reason, wasKicked = false
            )
        }
    }

    /**
     * 手动重连（用户点击"手动重连"按钮）
     */
    fun manualReconnect() {
        reconnectJob?.let { it.cancel() }
        reconnectJob = null
        reconnectAttempt = 0

        val params = lastConnectParams ?: return
        _connectionState.value = ConnectionState.Reconnecting(attempt = 0, reason = "手动重连")

        viewModelScope.launch {
            val error = withContext(Dispatchers.IO) {
                val identity = identityStore.getOrCreate { Teamspeak.generateIdentity(8) }
                if (identity.isBlank()) return@withContext "身份生成失败"
                TSBridge.connectWithIdentity(
                    identity = identity,
                    host = params.host,
                    nickname = params.nickname,
                    password = params.password,
                    defaultChannel = params.defaultChannel,
                    defaultChannelPassword = params.defaultChannelPassword,
                    callbacks = createBridgeCallbacks(),
                )
            }

            if (error.isNotEmpty()) {
                _connectionState.value = ConnectionState.Disconnected(
                    reason = error, wasKicked = false
                )
            }
            // 成功由 onConnected 回调处理
        }
    }

    /**
     * 放弃重连
     */
    fun abandonReconnect() {
        reconnectJob?.let { it.cancel() }
        reconnectJob = null
        reconnectAttempt = 0

        lastChannelId = ""
        networkMonitor.reset()
        Repository.clearSession()
        channelViewModel?.clearChannels()
        _connectionState.value = ConnectionState.Disconnected(
            reason = "重连已放弃", wasKicked = false
        )
        lastConnectParams = null
        _state.update { it.copy(connectState = ConnectState.IDLE) }
    }

    // --- Bridge 回调 ---

    private fun createBridgeCallbacks(): TSBridge.Callbacks = object : TSBridge.Callbacks {
        override fun onConnected() {
            android.util.Log.i(TAG, "onConnected fired, channelViewModel=${channelViewModel != null}")
            voiceViewModel?.onReconnected()
            // 连接成功，开始首次同步
            viewModelScope.launch {
                // 记录到最近连接（标记成功）
                recentStore.addRecent(
                    RecentConnection(
                        address = _state.value.address.trim(),
                        nickname = _state.value.nickname.trim(),
                        password = _state.value.password,
                        lastConnectedAt = System.currentTimeMillis(),
                        lastSucceeded = true,
                    )
                )

                // 执行首次同步（最多重试 5 次）
                // 连接成功后 Go 侧可能尚未收到服务器的频道列表数据，需要等待重试
                // 通过 ChannelViewModel 执行同步，以正确更新 syncState
                val maxRetries = 5
                val retryDelays = longArrayOf(2000, 3000, 4000, 5000) // 每次重试之间的延迟
                var syncSuccess = false
                var retryCount = 0

                // 首次尝试前等待一小段时间，让 Go 侧接收服务器数据
                delay(1500)

                while (!syncSuccess && retryCount < maxRetries) {
                    android.util.Log.i(TAG, "Sync attempt ${retryCount + 1}/$maxRetries, channelViewModel=${channelViewModel != null}")
                    channelViewModel?.performInitialSync()
                    val currentState = channelViewModel?.syncState?.value
                    android.util.Log.i(TAG, "Sync attempt ${retryCount + 1} result: syncState=$currentState")
                    syncSuccess = currentState is com.tsmobile.app.viewmodel.SyncState.Synchronized
                    if (!syncSuccess) {
                        retryCount++
                        if (retryCount < maxRetries) {
                            val delayMs = retryDelays.getOrElse(retryCount - 1) { 5000 }
                            android.util.Log.i(TAG, "Sync failed, retrying in ${delayMs}ms...")
                            delay(delayMs)
                        }
                    }
                }

                if (syncSuccess) {
                    android.util.Log.i(TAG, "Sync succeeded, starting post-reconnect automation")
                    // 启动前台服务保活
                    ConnectionService.start(
                        getApplication(),
                        _state.value.address.trim(),
                    )
                    _state.update { it.copy(connectState = ConnectState.SUCCESS) }

                    // 异步预加载服务器详情
                    fetchServerInfo()

                    // 伪重连：回到断联前的频道（避开僵尸清理窗口）
                    // 注意：此时不设置 Connected —— 遮罩保持显示，防止用户干扰自动操作
                    val targetChannel = lastChannelId
                    lastChannelId = "" // 一次性消费
                    if (targetChannel.isNotEmpty() && targetChannel != "0") {
                        // 等待 4 秒：Go 侧 cleanupDuplicateIdentitySessions 在 3 秒后 Kick 僵尸
                        delay(4_000L)
                        android.util.Log.i(TAG, "Pseudo-reconnect: moving back to channel $targetChannel")
                        val error = withContext(Dispatchers.IO) {
                            TSBridge.moveToChannel(targetChannel, "")
                        }
                        if (error.isNotEmpty()) {
                            android.util.Log.w(TAG, "Pseudo-reconnect moveToChannel failed: $error")
                        }
                    }

                    // 所有自动操作完成 → 关闭遮罩，允许用户操作
                    android.util.Log.i(TAG, "Post-reconnect automation complete, showing Connected")
                    _connectionState.value = ConnectionState.Connected
                } else {
                    android.util.Log.e(TAG, "Sync failed after 3 attempts")
                    _state.update {
                        it.copy(
                            connectState = ConnectState.FAILED,
                            errorMessage = "数据同步失败，请重试",
                        )
                    }
                    TSBridge.disconnect()
                }
            }
        }

        override fun onDisconnected(message: String) {
            android.util.Log.w(TAG, "onDisconnected: $message")

            // 连接阶段断开视为失败
            if (_state.value.connectState == ConnectState.CONNECTING) {
                // Go 协程可能已经通过 WaitConnected 返回了具体错误，
                // 此处仅在有新消息时覆盖错误信息，避免空消息覆盖已有错误。
                val newError = classifyError(message)
                _state.update {
                    it.copy(
                        connectState = ConnectState.FAILED,
                        errorMessage = if (newError.isNotBlank()) newError else it.errorMessage,
                    )
                }
                return
            }

            // 主动断开不触发重连
            if (_connectionState.value is ConnectionState.Disconnecting) {
                return
            }

            // 已在 WaitingForNetwork 或 Reconnecting 状态 → 响应式监控已处理
            // 但仍需更新通知栏（否则断连后仍显示"已连接"）
            if (_connectionState.value is ConnectionState.WaitingForNetwork ||
                _connectionState.value is ConnectionState.Reconnecting
            ) {
                android.util.Log.d(TAG, "onDisconnected while already handling reconnect, stopping notification")
                ConnectionService.stop(getApplication())
                return
            }

            // 语音断开处理
            voiceViewModel?.onDisconnected()

            // 停止前台服务
            ConnectionService.stop(getApplication())

            // 判断断线原因：网络丢失 vs 服务器主动断线
            //   - ConnectivityManager 明确检测到网络切换 → 网络丢失
            //   - 断线原因为空或 timeout → TCP 静默断开（NAT 超时、运营商丢弃等），
            //     此时网络接口正常但连接已死，也视为网络丢失
            //   - 服务器主动断线（被踢、封禁等）会有明确的错误消息
            val isNetworkLoss = networkMonitor.consumeNetworkLossDisconnect() ||
                message.isEmpty() ||
                message.contains("timeout", ignoreCase = true)

            if (isNetworkLoss) {
                // 记录当前频道，供重连后自动回到原频道
                val currentChId = Repository.currentChannelId.value
                if (currentChId != "0" && currentChId.isNotEmpty()) {
                    lastChannelId = currentChId
                    android.util.Log.i(TAG, "Saved lastChannelId=$lastChannelId for pseudo-reconnect")
                }

                // 网络丢失 — 启动重连（保留会话数据，不清除频道/成员信息）
                // 注：正常情况下响应式监控（init block）已处理，此分支为 Go idle timeout 等兜底
                android.util.Log.i(TAG, "Network-loss disconnect detected (fallback path)")
                startReconnect("网络变化，正在重连...")
            } else {
                // 服务器主动断线 — 清理会话，回到首页
                android.util.Log.w(TAG, "Server-initiated disconnect: $message")
                lastChannelId = ""
                Repository.clearSession()
                channelViewModel?.clearChannels()
                _connectionState.value = ConnectionState.Disconnected(
                    reason = message, wasKicked = false
                )
            }
        }

        override fun onTextMessage(msg: TextMsg) {
            android.util.Log.d(TAG, "onTextMessage: targetMode=${msg.targetMode}, targetID=${msg.targetID}, " +
                "invokerName=${msg.invokerName}, invokerUID=${msg.invokerUID}, message=${msg.message}")
            var targetIdLong = msg.targetID.toLongOrNull() ?: run {
                android.util.Log.w(TAG, "onTextMessage: invalid targetID '${msg.targetID}', dropping")
                return
            }
            // TeamSpeak 协议：频道消息的 target=0 表示"发送者当前频道"，
            // 需要解析为实际频道 ID 才能正确匹配归档 key 和 matchesCurrentChat
            if (targetIdLong == 0L && msg.targetMode.toInt() == 2) {
                val resolved = Repository.currentChannelId.value.toLongOrNull() ?: 0L
                if (resolved > 0L) {
                    android.util.Log.d(TAG, "onTextMessage: resolved targetID 0 → currentChannelId=$resolved")
                    targetIdLong = resolved
                }
            }
            // 直接使用 Go 侧传来的 invokerID（AAR 已包含此字段）
            val selfId = Repository.selfClientId.value
            val invokerId = msg.invokerID.toInt()
            val senderId: Int = if (invokerId > 0) invokerId else {
                // 降级：invokerID 不可用时通过 UID 反查
                val selfUid = Repository.getClient(selfId)?.uid
                val senderClient = Repository.getClientByUid(msg.invokerUID)
                senderClient?.id ?: if (msg.invokerUID == selfUid) selfId else 0
            }
            val isSelf = senderId == selfId
            android.util.Log.d(TAG, "onTextMessage: senderId=$senderId (invokerID=${msg.invokerID}, isSelf=$isSelf)")
            val cvm = chatViewModel
            if (cvm == null) {
                android.util.Log.w(TAG, "onTextMessage: chatViewModel is null, message dropped!")
            } else {
                cvm.handleTextMessage(
                    targetMode = msg.targetMode.toInt(),
                    targetId = targetIdLong,
                    senderId = senderId,
                    senderName = msg.invokerName,
                    content = msg.message
                )
            }
            // 当用户不在对应聊天页时，推送系统通知
            sendMessageNotification(
                targetMode = msg.targetMode.toInt(),
                targetId = targetIdLong,
                senderName = msg.invokerName,
                content = msg.message,
            )
        }

        override fun onClientEnter(client: Client) {
            // 人员变动统一全量刷新
            channelViewModel?.handleClientEnter()
                ?: viewModelScope.launch { Repository.refreshClientList() }
        }

        override fun onClientLeave(id: Long, reasonMsg: String) {
            voiceViewModel?.removeRemoteClient(id)
            // 人员变动统一全量刷新
            channelViewModel?.handleClientLeave(id.toInt(), reasonMsg)
                ?: viewModelScope.launch { Repository.refreshClientList() }
        }

        override fun onClientMoved(id: Long, targetChannelID: String) {
            voiceViewModel?.onClientMoved(id, targetChannelID)
            // 人员变动统一全量刷新
            channelViewModel?.handleClientMoved(id.toInt(), targetChannelID)
                ?: viewModelScope.launch { Repository.refreshClientList() }
        }

        override fun onKicked(reason: String) {
            android.util.Log.w(TAG, "onKicked: $reason")

            // 停止前台服务
            ConnectionService.stop(getApplication())

            // 停止重连
            reconnectJob?.let { it.cancel() }
            reconnectJob = null

            // 停止语音
            voiceViewModel?.stopVoice()

            // 清理会话
            Repository.clearSession()
            channelViewModel?.clearChannels()

            // 设置被踢状态
            _kickReason.value = reason
            _connectionState.value = ConnectionState.Disconnected(
                reason = reason, wasKicked = true
            )

            _state.update {
                it.copy(connectState = ConnectState.IDLE)
            }
        }

        override fun onMixedVoicePCM(pcm: ByteArray) {
            voiceViewModel?.handleMixedVoicePCM(pcm)
        }

        override fun onClientSpeaking(clientID: Long, speaking: Boolean) {
            voiceViewModel?.handleClientSpeaking(clientID, speaking)
        }

        override fun onPoked(event: TSBridge.PokeEventData) {
            handlePoked(
                com.tsmobile.app.data.PokeEvent(
                    invokerID = event.invokerID,
                    invokerName = event.invokerName,
                    invokerUID = event.invokerUID,
                    message = event.message,
                )
            )
        }
    }

    // --- 错误分类 ---

    private fun classifyError(raw: String): String {
        val lower = raw.lowercase()
        return when {
            "password" in lower || "密码" in lower -> "服务器密码错误"
            "nickname" in lower || "昵称" in lower -> "昵称已被使用，请更换"
            "timeout" in lower || "unreachable" in lower || "network" in lower ->
                "无法连接到服务器，请检查网络"
            "resolve" in lower || "dns" in lower || "lookup" in lower ->
                "服务器地址无法解析"
            "full" in lower || "limit" in lower -> "服务器已满"
            else -> raw
        }
    }
}

/**
 * 服务器地址格式验证。
 * 支持：域名、IP（v4/v6）、TSDNS、带端口号。
 */
private fun isValidServerAddress(address: String): Boolean {
    val trimmed = address.trim()
    if (trimmed.isBlank()) return false

    // 允许格式：
    // - example.com
    // - example.com:9987
    // - 192.168.1.1
    // - 192.168.1.1:9987
    // - [::1]:9987
    val ip4Pattern = Regex("""^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$""")
    val domainPattern = Regex("""^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*(:\d+)?$""")
    val ip6Pattern = Regex("""^\[?[a-fA-F0-9:]+\]?(:\d+)?$""")

    return ip4Pattern.matches(trimmed) ||
        domainPattern.matches(trimmed) ||
        ip6Pattern.matches(trimmed)
}

/**
 * 输入验证。
 */
fun validate(config: ServerConfig): ValidationErrors {
    var errors = ValidationErrors()

    // 地址验证：非空 + 基本格式（包含字母或数字，含可选端口）
    if (config.address.isBlank()) {
        errors = errors.copy(address = "请输入服务器地址")
    } else if (!isValidServerAddress(config.address)) {
        errors = errors.copy(address = "请输入有效的服务器地址")
    }

    // 昵称验证：非空
    if (config.nickname.isBlank()) {
        errors = errors.copy(nickname = "请输入昵称")
    }

    return errors
}

/**
 * 格式化时间戳为相对时间描述。
 * 例：刚刚、5分钟前、2小时前、昨天、3天前、2024-01-15
 */
fun formatRelativeTime(timestamp: Long): String {
    if (timestamp <= 0) return ""
    val now = System.currentTimeMillis()
    val diff = now - timestamp

    return when {
        diff < 60_000L -> "刚刚"
        diff < 3_600_000L -> "${diff / 60_000}分钟前"
        diff < 86_400_000L -> "${diff / 3_600_000}小时前"
        diff < 172_800_000L -> "昨天"
        diff < 604_800_000L -> "${diff / 86_400_000}天前"
        else -> {
            val sdf = java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault())
            sdf.format(java.util.Date(timestamp))
        }
    }
}
