package app.dudu.ime

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.SystemClock
import android.util.Log
import androidx.core.content.ContextCompat
import app.dudu.BuildConfig
import app.dudu.audio.AudioStreamer
import app.dudu.data.AccountRepository
import app.dudu.data.AccountSnapshot
import app.dudu.data.AccountState
import app.dudu.data.MetricsQueue
import app.dudu.net.AsrSocket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.util.UUID

/** 键盘 UI 状态（Compose 直接收集渲染）。 */
data class KeyboardUiState(
    val account: AccountSnapshot,
    val recording: Boolean = false,
    /** 上滑超过取消阈值：MicBar 与提示文案给出"松开取消"视觉反馈 */
    val willCancel: Boolean = false,
    /** 已定稿文本（亮色）；松开后统一 commit，录音中不进宿主输入框 */
    val finalText: String = "",
    /** 流式未定稿文本（灰色） */
    val partialText: String = "",
    /** 录音计时（秒），工具条右侧 mono 显示 */
    val seconds: Int = 0,
    /** 错误/引导文案，占用 partial 条位置展示 */
    val notice: String? = null,
)

/**
 * 键盘控制器：按住-松开会话状态机，串起 AudioStreamer + AsrSocket + commitText。
 *
 * 设计取舍（重要）：
 *  - partial / final 都先进文本条，**松开时才整体 commit**——因为"上滑取消 = 丢弃本次结果"，
 *    若 final 到达即上屏，取消就无法撤回（InputConnection 无可靠 undo）。
 *  - 松开后发 stop，等服务端补发收尾 final（最多 FINALIZE_TIMEOUT_MS），到齐即 commit；
 *    超时则降级 commit 当前已有 final + partial。
 */
class KeyboardController(
    private val context: Context,
    private val scope: CoroutineScope,
    /** final 文本上屏：currentInputConnection.commitText */
    private val commitText: (String) -> Unit,
    /** 打开主 App（dest: login / buy / permission） */
    private val openMainApp: (dest: String) -> Unit,
) : AsrSocket.Listener {

    private val repo = AccountRepository.get(context)
    private val metrics = MetricsQueue.get(context)

    // ─── 埋点计时基准（事件名对齐 protocol.MetricWhitelist，flush 见 MetricsFlushWorker） ──
    private var pressAtMs = 0L          // MicBar 按下时刻
    private var releaseAtMs = 0L        // 松开时刻
    private var wsConnectAtMs = 0L      // WS 发起连接时刻
    private var gotFirstFrame = false   // audio.start_ms 已记
    private var gotFirstPartial = false // asr.first_partial_ms 已记
    private var partialCount = 0
    private var sessionErrorCode: String? = null
    private var sessionCancelled = false
    private var sessionTracked = false

    private val _state = MutableStateFlow(KeyboardUiState(account = repo.snapshot.value))
    val state: StateFlow<KeyboardUiState> = _state

    private var socket: AsrSocket? = null
    private var audio: AudioStreamer? = null
    private var sessionId: String = ""
    private var tickerJob: Job? = null
    private var finalizeJob: Job? = null

    /** 会话阶段 */
    private enum class Phase { IDLE, RECORDING, FINALIZING }

    private var phase = Phase.IDLE
    private var discarded = false // 上滑取消后置位：后续 final 全部丢弃
    private var started = false   // start 帧是否已写出（onOpen 内置位）
    private var pendingStop = false // 用户在 onOpen 前松开：缓冲 stop，待 start 发完补发

    init {
        // 账户缓存变化（主 App 切 mock 态 / usage 帧刷新）→ 工具条实时更新
        scope.launch {
            repo.snapshot.collect { snap ->
                _state.value = _state.value.copy(account = snap)
            }
        }
    }

    // ─── 手势入口 ────────────────────────────────────────────────────────────

    /** 按下 MicBar。 */
    fun onPressDown() {
        when (_state.value.account.state) {
            AccountState.GUEST -> { openMainApp("login"); return }
            AccountState.QUOTA -> { openMainApp("buy"); return }
            else -> Unit
        }
        if (phase != Phase.IDLE) return

        // IME 内无法弹权限框 → 引导去主 App 授权
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO)
            != PackageManager.PERMISSION_GRANTED
        ) {
            _state.value = _state.value.copy(notice = "先在 dudu App 开启麦克风权限")
            openMainApp("permission")
            return
        }

        phase = Phase.RECORDING
        discarded = false
        started = false
        pendingStop = false
        sessionId = "c-" + UUID.randomUUID()
        _state.value = _state.value.copy(
            recording = true, willCancel = false,
            finalText = "", partialText = "", seconds = 0, notice = null,
        )

        // 埋点基准复位
        pressAtMs = SystemClock.elapsedRealtime()
        releaseAtMs = 0L
        gotFirstFrame = false
        gotFirstPartial = false
        partialCount = 0
        sessionErrorCode = null
        sessionCancelled = false
        sessionTracked = false

        wsConnectAtMs = SystemClock.elapsedRealtime()
        socket = AsrSocket(BuildConfig.WS_URL, _state.value.account.token ?: "", repo.deviceId, this)
        socket?.connect()

        tickerJob?.cancel()
        tickerJob = scope.launch {
            while (true) {
                delay(1000)
                _state.value = _state.value.copy(seconds = _state.value.seconds + 1)
            }
        }
    }

    /** 手指上下移动：dy 为相对按下点的纵向位移（px），上滑为负。 */
    fun onDrag(beyondCancelThreshold: Boolean) {
        if (phase != Phase.RECORDING) return
        if (_state.value.willCancel != beyondCancelThreshold) {
            _state.value = _state.value.copy(willCancel = beyondCancelThreshold)
        }
    }

    /** 松开（未越过取消阈值）→ stop + 等收尾 final 后上屏。 */
    fun onRelease() {
        if (phase != Phase.RECORDING) return
        releaseAtMs = SystemClock.elapsedRealtime()
        phase = Phase.FINALIZING
        stopAudioAndTicker()
        // 弱网下握手 >300ms 时 start 尚未写出：缓冲 stop，待 onOpen 发完 start 后补发，
        // 保证服务端始终收到有序的 start→audio→stop（参考 iOS DictationController pendingFrames）。
        if (started) {
            socket?.sendStop(sessionId)
        } else {
            pendingStop = true
        }
        _state.value = _state.value.copy(recording = false, willCancel = false)

        // 收尾兜底：超时仍未收到 final → commit 已有内容
        finalizeJob = scope.launch {
            delay(FINALIZE_TIMEOUT_MS)
            commitAndReset(includePartial = true)
        }
    }

    /** 松开（已越过取消阈值）→ cancel，丢弃结果（服务端仍计量）。 */
    fun onCancel() {
        if (phase != Phase.RECORDING) return
        sessionCancelled = true
        discarded = true
        stopAudioAndTicker()
        socket?.sendCancel(sessionId)
        teardown()
        _state.value = _state.value.copy(
            recording = false, willCancel = false,
            finalText = "", partialText = "", seconds = 0,
        )
    }

    /** 键盘隐藏 / 服务销毁时兜底。 */
    fun abort() {
        if (phase == Phase.IDLE) return
        sessionCancelled = true
        discarded = true
        stopAudioAndTicker()
        runCatching { socket?.sendCancel(sessionId) }
        teardown()
        _state.value = _state.value.copy(
            recording = false, willCancel = false,
            finalText = "", partialText = "", seconds = 0,
        )
    }

    fun clearNotice() {
        if (_state.value.notice != null) _state.value = _state.value.copy(notice = null)
    }

    // ─── AsrSocket.Listener（OkHttp 线程回调 → 切主线程） ─────────────────────

    override fun onOpen() {
        scope.launch {
            // 会话已彻底结束（取消/断连）才忽略；FINALIZING（onOpen 前松开）仍需先发 start。
            if (phase == Phase.IDLE) return@launch
            metrics.track("ws.connect_ms", mapOf("ms" to SystemClock.elapsedRealtime() - wsConnectAtMs))
            socket?.sendStart(sessionId)
            started = true
            // 用户在握手完成前就松开：start 已发出，立刻补发缓冲的 stop，保证 start→stop 有序。
            if (pendingStop) {
                pendingStop = false
                socket?.sendStop(sessionId)
                return@launch
            }
            // 仍在录音：start 发出后才开始推音频帧
            if (phase != Phase.RECORDING) return@launch
            audio = AudioStreamer(
                onFrame = { frame ->
                    if (!gotFirstFrame) {
                        gotFirstFrame = true
                        metrics.track("audio.start_ms", mapOf("ms" to SystemClock.elapsedRealtime() - pressAtMs))
                    }
                    socket?.sendAudio(frame)
                },
                onError = { t ->
                    scope.launch {
                        Log.w(TAG, "audio error", t)
                        showError("麦克风不可用，稍后再试")
                    }
                },
            ).also { it.start() }
        }
    }

    override fun onPartial(sessionId: String, text: String) {
        scope.launch {
            if (discarded) return@launch
            partialCount++
            if (!gotFirstPartial) {
                gotFirstPartial = true
                metrics.track("asr.first_partial_ms", mapOf("ms" to SystemClock.elapsedRealtime() - pressAtMs))
            }
            _state.value = _state.value.copy(partialText = text)
        }
    }

    override fun onFinal(sessionId: String, text: String) {
        scope.launch {
            if (discarded) return@launch
            _state.value = _state.value.copy(
                finalText = _state.value.finalText + text,
                partialText = "",
            )
            // 松开后等到的收尾 final → 立即上屏并结束会话
            if (phase == Phase.FINALIZING) {
                finalizeJob?.cancel()
                commitAndReset(includePartial = false)
            }
        }
    }

    override fun onUsage(sessionSeconds: Int, balanceSeconds: Long, trialRemaining: Int) {
        scope.launch {
            repo.applyUsage(balanceSeconds, trialRemaining)
            if (phase == Phase.RECORDING && sessionSeconds > 0) {
                // 以服务端实收时长校准本地计时
                _state.value = _state.value.copy(seconds = sessionSeconds)
            }
        }
    }

    override fun onServerError(code: String, message: String) {
        scope.launch {
            sessionErrorCode = code
            // 错误码与文案对齐 server/pkg/protocol/errors.go
            when (code) {
                "QUOTA_EXCEEDED" -> {
                    repo.markQuotaExceeded()
                    // 扣穿前的当句服务端会让说完（final 已到），已识别内容照常上屏
                    commitAndReset(includePartial = false)
                    showError(message.ifEmpty { "今日试用已用完，去购买时长" })
                }
                "SESSION_LIMIT" -> {
                    // 服务端已自动截断定稿：上屏已识别内容
                    commitAndReset(includePartial = false)
                    showError(message.ifEmpty { "单次最长 3 分钟，松开后可继续" })
                }
                "UNAUTHORIZED" -> {
                    repo.markUnauthorized()
                    discardAndReset()
                    showError(message.ifEmpty { "登录已失效，请重新登录" })
                }
                "RATE_LIMITED" -> {
                    discardAndReset()
                    showError(message.ifEmpty { "操作过于频繁，稍后再试" })
                }
                else -> {
                    discardAndReset()
                    showError(message.ifEmpty { "识别服务暂不可用，稍后再试" })
                }
            }
        }
    }

    override fun onClosed() {
        scope.launch {
            if (phase == Phase.FINALIZING) {
                // 服务端 stop 后直接关连接：把已有内容上屏
                finalizeJob?.cancel()
                commitAndReset(includePartial = true)
            }
        }
    }

    override fun onFailure(t: Throwable) {
        scope.launch {
            Log.w(TAG, "ws failure", t)
            if (phase == Phase.IDLE) return@launch
            if (phase == Phase.FINALIZING) {
                // 断连兜底：已有内容尽量上屏
                finalizeJob?.cancel()
                commitAndReset(includePartial = true)
            } else {
                discardAndReset()
                showError("连接中断，松开重试")
            }
        }
    }

    // ─── 内部 ────────────────────────────────────────────────────────────────

    /** 把 finalText（可选拼上未定稿 partial）commit 到宿主，然后复位。 */
    private fun commitAndReset(includePartial: Boolean) {
        if (phase == Phase.IDLE) return
        val s = _state.value
        val committed = if (includePartial) s.finalText + s.partialText else s.finalText
        if (!discarded && committed.isNotEmpty()) {
            commitText(committed)
            if (releaseAtMs > 0) {
                metrics.track(
                    "asr.release_to_commit_ms",
                    mapOf("ms" to SystemClock.elapsedRealtime() - releaseAtMs),
                )
            }
        }
        discarded = true // 防止迟到帧重复 commit
        stopAudioAndTicker()
        teardown()
        _state.value = _state.value.copy(
            recording = false, willCancel = false,
            finalText = "", partialText = "", seconds = 0,
        )
    }

    private fun discardAndReset() {
        discarded = true
        stopAudioAndTicker()
        teardown()
        _state.value = _state.value.copy(
            recording = false, willCancel = false,
            finalText = "", partialText = "", seconds = 0,
        )
    }

    private fun showError(text: String) {
        _state.value = _state.value.copy(notice = text)
        // 文案短暂停留后自动消失
        scope.launch {
            delay(NOTICE_DISMISS_MS)
            if (_state.value.notice == text) clearNotice()
        }
    }

    private fun stopAudioAndTicker() {
        audio?.stop()
        audio = null
        tickerJob?.cancel()
        tickerJob = null
    }

    private fun teardown() {
        trackSessionEnd()
        finalizeJob?.cancel()
        finalizeJob = null
        socket?.close()
        socket = null
        phase = Phase.IDLE
    }

    /** 会话质量埋点（asr.session），teardown 单点触发、每会话一次。 */
    private fun trackSessionEnd() {
        if (sessionTracked || phase == Phase.IDLE) return
        sessionTracked = true
        metrics.track(
            "asr.session",
            buildMap {
                put("audio_seconds", _state.value.seconds)
                put("partial_count", partialCount)
                put("cancel", sessionCancelled)
                sessionErrorCode?.let { put("error_code", it) }
            },
        )
    }

    companion object {
        private const val TAG = "DuduKeyboard"
        private const val FINALIZE_TIMEOUT_MS = 2000L
        private const val NOTICE_DISMISS_MS = 3000L
    }
}
