package com.beancount.mobile.accessibility

import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.content.Context
import android.graphics.Bitmap
import android.hardware.display.DisplayManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.Display
import android.view.Surface
import android.view.accessibility.AccessibilityEvent
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableNativeMap
import com.facebook.react.bridge.WritableNativeArray
import java.io.ByteArrayOutputStream
import android.util.Base64
import android.view.accessibility.AccessibilityNodeInfo

/**
 * 无障碍账单识别服务（plan.md「3.6 无障碍服务」+「决策 4 Config Plugin」）。
 *
 * 参考 AutoAccounting 的 SelectToSpeakService：
 * - 监听支付 App 的页面切换（TYPE_WINDOW_STATE_CHANGED）
 * - 页面签名匹配时自动截图 → OCR → 推送到 JS 层
 * - 横屏免打扰（游戏/视频时不触发）
 * - ocrDoing 守卫（防止重复触发）
 *
 * 伪装说明（绕过微信 8.0.52+ 节点混淆）：
 * 微信按 ComponentName（包名/类名）识别系统服务白名单——TalkBack、SelectToSpeak
 * 等系统服务不受节点混淆影响，第三方服务则被混淆。
 * 本服务的 package + class 全部伪装为
 *   com.google.android.accessibility.selecttospeak.SelectToSpeakService，
 * 使微信将其识别为系统 SelectToSpeak 服务，从而拿到未混淆的真实节点文本。
 *
 * 伪装由 Config Plugin（plugins/accessibility/app.plugin.js）在 prebuild 时完成：
 * - kt 源码 package 被 rewrite 为 com.google.android.accessibility.selecttospeak
 * - Manifest android:name 写成完整全限定名
 *
 * 注意：plan.md 决策 3「纯开源侧载」保留无障碍伪装（非应用商店分发）。
 *
 * 通过 DeviceEventEmitter 把识别事件推送到 JS 层 automationStore。
 *
 * ⚠️ 与 src/domain/constants.ts 同步：PAYMENT_PACKAGES
 *    修改时需两边同时更新。
 */
class SelectToSpeakService : AccessibilityService() {

    companion object {
        private const val TAG = "BillingAccessibility"
        private const val PREFS_NAME = "billing_accessibility_prefs"
        private const val PREF_PAGE_SIGNATURES = "page_signatures"

        @Volatile
        var instance: SelectToSpeakService? = null
            private set

        /** 悬浮球全局开关（由 JS 层通过 AccessibilityBridge 控制）。 */
        @Volatile
        var floatingBallEnabled = true

        /**
         * 支付 App 包名（自动生成，来源：src/domain/constants.ts PAYMENT_PACKAGES）。
         * 修改时只需编辑 constants.ts，运行 expo prebuild 即可同步。
         */
        val PAYMENT_PACKAGES = setOf(
            "com.eg.android.AlipayGphone",   // 支付宝
            "com.tencent.mm",                 // 微信
            "com.unionpay",                   // 银联
            "com.cmbchina",                   // 招商银行
            "com.icbc",                       // 工商银行
            "com.chinamworld.main",           // 中国银行
            "com.ccbrcb",                     // 建设银行
            "com.bankcomm.Bankcomm",           // 交通银行
            "com.tencent.mobileqq",           // 手机QQ
            "com.tencent.tim"                 // TIM
        )

        /** 厂商桌面包名（过滤，不触发 OCR）。 */
        private val LAUNCHER_PACKAGES = setOf(
            "com.google.android.apps.nexuslauncher",
            "com.sec.android.app.launcher",
            "com.miui.home",
            "com.huawei.android.launcher",
            "com.oppo.launcher",
            "com.bbk.launcher2",
            "com.android.launcher3",
        )

        /** OCR 触发防抖：500ms 内的内容变化合并为一次。 */
        private const val CONTENT_CHANGE_DEBOUNCE_MS = 500L
    }

    private var ocrDoing = false
    private var ocrDoingTimestamp = 0L
    private val handler = Handler(Looper.getMainLooper())
    private val debounceRunnable = Runnable { processContentChange() }
    @Volatile private var topPackage: String? = null
    @Volatile private var topActivity: String? = null
    /** 已记住的页面签名（pkg|activity），匹配时自动触发 OCR。持久化到 SharedPreferences。 */
    private val pageSignatures = java.util.concurrent.CopyOnWriteArraySet<String>()
    private var floatingHelper: FloatingHelper? = null

    override fun onServiceConnected() {
        super.onServiceConnected()
        instance = this
        Log.i(TAG, "无障碍账单识别服务已连接")
        loadPageSignatures()
        configureService()
        // 修复：检查当前前台 App，恢复悬浮球
        val root = rootInActiveWindow
        val pkg = root?.packageName?.toString()
        root?.recycle()
        if (pkg != null) {
            topPackage = pkg
            updateFloatingHelperVisibility(pkg)
        }
    }

    /** 动态配置服务能力（截图 + 页面变化监听）。 */
    private fun configureService() {
        val info = AccessibilityServiceInfo().apply {
            eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED or
                         AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
            feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC
            flags = AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY or
                    AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS or
                    AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS or
                    AccessibilityServiceInfo.DEFAULT
            notificationTimeout = 100L
        }
        serviceInfo = info
    }

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {
        // ocrDoing 超时兜底：5秒后强制重置
        if (ocrDoing) {
            if (System.currentTimeMillis() - ocrDoingTimestamp > 5000) {
                Log.w(TAG, "ocrDoing 超时重置")
                ocrDoing = false
            } else {
                return
            }
        }
        val eventPackage = event?.packageName?.toString() ?: return

        when (event.eventType) {
            AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {
                val activityName = event.className?.toString() ?: ""
                if (filterPackage(eventPackage, activityName)) return
                topPackage = eventPackage
                topActivity = activityName
                Log.d(TAG, "页面切换: $eventPackage / $activityName")

                // 更新悬浮窗助手状态
                updateFloatingHelperVisibility(eventPackage)

                // 页面切换时检查是否有已记住的签名需要触发文本提取
                scheduleContentChange()

                // 注意：自动监听调试日志已移除（微信 8.0.52+ 混淆节点文本，自动监听无意义）。
                // 账单识别仅在用户点击悬浮球时触发（isManual=true），此时提取完整节点文本。
            }
            AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> {
                // 内容变化时也检查（防抖合并）
                scheduleContentChange()
            }
        }
    }

    private fun updateFloatingHelperVisibility(pkg: String?) {
        handler.post {
            val shouldShow = floatingBallEnabled
                && pkg != null
                && !filterPackage(pkg, topActivity ?: "")
                && !isLandscape()

            if (shouldShow) {
                if (floatingHelper == null) {
                    floatingHelper = FloatingHelper(this)
                    floatingHelper?.show()
                }
            } else {
                floatingHelper?.dismiss()
                floatingHelper = null
            }
        }
    }

    /** 防抖：500ms 内的多次内容变化合并。 */
    private fun scheduleContentChange() {
        handler.removeCallbacks(debounceRunnable)
        handler.postDelayed(debounceRunnable, CONTENT_CHANGE_DEBOUNCE_MS)
    }

    /** 内容变化处理：检查页面签名 → 提取文本 → 发送给 JS（JS 控制是否 OCR 兜底）。 */
    private fun processContentChange() {
        if (ocrDoing) return
        val pkg = topPackage ?: return

        // 横屏免打扰（plan.md「3.10」）
        if (isLandscape()) {
            Log.d(TAG, "横屏免打扰，跳过")
            return
        }

        // 页面签名匹配（若已记住页面则触发）
        val activity = topActivity ?: ""
        val sigKey = "$pkg|$activity"
        if (!pageSignatures.contains(sigKey)) {
            return  // 未记住的页面不自动触发
        }

        // 抓取并提取屏幕所有无障碍文本（含 WebView 子窗口），并推送至 JS 侧
        val texts = dumpAllTexts()

        val reactContext = ReactContextHolder.context
        if (reactContext != null) {
            try {
                val map = WritableNativeMap().apply {
                    putString("package", pkg)
                    putString("activity", activity)
                    putString("signature", sigKey)
                    val array = WritableNativeArray()
                    for (t in texts) {
                        array.pushString(t)
                    }
                    putArray("texts", array)
                }
                reactContext
                    .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
                    .emit("billingDebugNodes", map)
            } catch (e: Exception) {
                Log.e(TAG, "推送内容变化节点文本失败: ${e.message}")
            }
        }
    }

    /** 截图并触发 OCR 处理（Android 11+）。 */
    private fun takeScreenshotAndProcess(packageName: String) {
        if (ocrDoing) return
        ocrDoing = true
        ocrDoingTimestamp = System.currentTimeMillis()
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
            ocrDoing = false
            return
        }
        try {
            takeScreenshot(
                Display.DEFAULT_DISPLAY,
                mainExecutor,
                object : TakeScreenshotCallback {
                    override fun onSuccess(result: ScreenshotResult) {
                        try {
                            val bitmap = Bitmap.wrapHardwareBuffer(result.hardwareBuffer, result.colorSpace)
                            result.hardwareBuffer.close()
                            if (bitmap != null) {
                                val base64 = bitmapToBase64(bitmap, packageName)
                                bitmap.recycle()
                                // 推送到 JS 层（NativeEventEmitter）
                                sendScreenshotEvent(base64, packageName)
                            }
                        } finally {
                            ocrDoing = false
                        }
                    }
                    override fun onFailure(errorCode: Int) {
                        Log.e(TAG, "截图失败: errorCode=$errorCode")
                        ocrDoing = false
                    }
                }
            )
        } catch (e: Throwable) {
            Log.e(TAG, "调用 takeScreenshot 失败: ${e.message}", e)
            ocrDoing = false
        }
    }

    /** 把截图以 base64 推送到 JS 层（由 JS 端 OcrProcessor 处理）。 */
    private fun sendScreenshotEvent(base64: String, packageName: String) {
        val reactContext = ReactContextHolder.context ?: return
        try {
            reactContext
                .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
                .emit("billingScreenshot", writableMapOf(
                    "base64" to base64,
                    "packageName" to packageName,
                    "timestamp" to System.currentTimeMillis()
                ))
        } catch (e: Exception) {
            Log.e(TAG, "推送截图事件失败: ${e.message}")
        }
    }

    /** 手动触发一次 OCR（临时隐藏悬浮窗避开遮挡，并在 150ms 后触发截图）。 */
    fun triggerManualOcr() {
        val pkg = topPackage ?: return
        handler.post {
            floatingHelper?.collapse()
            floatingHelper?.hideTemporarily()
        }
        
        // 150ms 后触发截图（此时悬浮窗已瞬间隐藏，避免遮挡和误匹配）
        handler.postDelayed({
            try {
                takeScreenshotAndProcess(pkg)
            } catch (e: Exception) {
                Log.e(TAG, "手动触发 OCR 失败: ${e.message}")
            } finally {
                // 截图完成，瞬间恢复显示悬浮球
                handler.post {
                    floatingHelper?.showTemporarily()
                }
            }
        }, 150)
    }

    /**
     * 手动触发一次节点文本提取并发送到 JS，从而让 JS 优先尝试直接文本解析。
     *
     * 使用 dumpAllTexts() 覆盖所有窗口（含 WebView 子窗口），而非仅 rootInActiveWindow。
     * 伪装生效后这条路径会成为微信账单页的主识别路径，必须保证完整性。
     */
    fun triggerManualExtraction() {
        handler.post {
            floatingHelper?.collapse()
        }
        val pkg = topPackage ?: return
        val activity = topActivity ?: ""
        val sigKey = "$pkg|$activity"

        val texts = dumpAllTexts()

        val reactContext = ReactContextHolder.context
        if (reactContext != null) {
            try {
                val map = WritableNativeMap().apply {
                    putString("package", pkg)
                    putString("activity", activity)
                    putString("signature", sigKey)
                    putBoolean("isManual", true)
                    val array = WritableNativeArray()
                    for (t in texts) {
                        array.pushString(t)
                    }
                    putArray("texts", array)
                }
                reactContext
                    .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
                    .emit("billingDebugNodes", map)
            } catch (e: Exception) {
                Log.e(TAG, "手动触发节点文本提取失败: ${e.message}")
            }
        }
    }

    /** 记住当前页面（用户主动标记应触发 OCR 的页面）。持久化到 SharedPreferences。 */
    fun rememberCurrentPage() {
        val pkg = topPackage ?: return
        val activity = topActivity ?: ""
        val sig = "$pkg|$activity"
        val uiLabels = FloatingUiConfigStore.load(this).labels
        if (pageSignatures.add(sig)) {
            savePageSignatures()
            Log.i(TAG, "已记住页面: $sig")
            handler.post {
                android.widget.Toast.makeText(this, "${uiLabels.pageRemembered}:\n$sig", android.widget.Toast.LENGTH_LONG).show()
            }
            
            // 抓取并提取屏幕所有无障碍文本
            val texts = mutableListOf<String>()
            val rootNode = rootInActiveWindow
            dumpNodeTexts(rootNode, texts)
            rootNode?.recycle()

            // 推送事件到 JS 端，使 npx expo start 终端控制台可以接收并打印日志
            val reactContext = ReactContextHolder.context
            if (reactContext != null) {
                try {
                    val map = WritableNativeMap().apply {
                        putString("package", pkg)
                        putString("activity", activity)
                        putString("signature", sig)
                        val array = WritableNativeArray()
                        for (t in texts) {
                            array.pushString(t)
                        }
                        putArray("texts", array)
                    }
                    reactContext
                        .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
                        .emit("billingPageRemembered", map)
                } catch (e: Exception) {
                    Log.e(TAG, "推送记住页面事件失败: ${e.message}")
                }
            }
        } else {
            handler.post {
                android.widget.Toast.makeText(this, "${uiLabels.pageSignatureExists}:\n$sig", android.widget.Toast.LENGTH_LONG).show()
            }
        }
    }

    /** 获取已记住的页面签名列表（供 JS 端展示）。 */
    fun getPageSignatures(): Set<String> {
        return pageSignatures.toSet()
    }

    /** 清空所有已记住的页面签名。 */
    fun clearPageSignatures() {
        pageSignatures.clear()
        try {
            val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
            prefs.edit().remove(PREF_PAGE_SIGNATURES).apply()
            Log.i(TAG, "已清空全部记住的页面并从 SharedPreferences 中移除键值")
        } catch (e: Exception) {
            Log.e(TAG, "清空 SharedPreferences 失败: ${e.message}")
        }
    }

    /** 删除指定页面签名。 */
    fun removePageSignature(sig: String) {
        if (pageSignatures.remove(sig)) {
            savePageSignatures()
            Log.i(TAG, "已删除页面签名: $sig")
        }
    }

    /** 获取当前顶部包名（供 JS 判断当前页面）。 */
    fun getTopPackage(): String? = topPackage

    /** 获取当前顶部 Activity。 */
    fun getTopActivity(): String? = topActivity

    /** 从 SharedPreferences 加载已记住的页面签名。 */
    private fun loadPageSignatures() {
        try {
            val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
            val saved = prefs.getStringSet(PREF_PAGE_SIGNATURES, emptySet()) ?: emptySet()
            pageSignatures.clear()
            pageSignatures.addAll(saved)
            Log.i(TAG, "已加载 ${pageSignatures.size} 个记住的页面签名")
        } catch (e: Exception) {
            Log.e(TAG, "加载页面签名失败: ${e.message}")
        }
    }

    /** 保存页面签名到 SharedPreferences。 */
    private fun savePageSignatures() {
        try {
            val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
            if (pageSignatures.isEmpty()) {
                prefs.edit().remove(PREF_PAGE_SIGNATURES).apply()
            } else {
                prefs.edit().putStringSet(PREF_PAGE_SIGNATURES, HashSet(pageSignatures)).apply()
            }
        } catch (e: Exception) {
            Log.e(TAG, "保存页面签名失败: ${e.message}")
        }
    }

    /** 关闭悬浮球（由 JS 层 setFloatingBallEnabled(false) 调用）。 */
    fun dismissFloatingHelper() {
        handler.post {
            floatingHelper?.dismiss()
            floatingHelper = null
        }
    }

    /** 刷新悬浮球状态（由 JS 层 setFloatingBallEnabled(true) 调用）。 */
    fun refreshFloatingHelper() {
        handler.post {
            updateFloatingHelperVisibility(topPackage)
        }
    }

    /** 横屏检测（plan.md「3.10 横屏免打扰」）。 */
    private fun isLandscape(): Boolean {
        val dm = getSystemService(DisplayManager::class.java)?.getDisplay(Display.DEFAULT_DISPLAY)
            ?: return false
        val rotation = dm.rotation
        return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270
    }

    /** 过滤不应处理的系统组件（桌面启动器、SystemUI、输入法等）。 */
    private fun filterPackage(pkg: String, className: String): Boolean {
        val p = pkg.lowercase()
        // 自身 App：只放行 MainActivity，避免悬浮球容器触发自毁式关闭
        if (pkg == packageName) {
            if (className.isEmpty()) return false
            return !className.endsWith(".MainActivity")
        }
        // 桌面启动器（悬浮球在桌面无意义）
        if (LAUNCHER_PACKAGES.any { p.contains(it) }) return true
        // SystemUI / 输入法（悬浮球在这些组件上会遮挡系统 UI）
        if (p.contains("systemui") || p.contains("inputmethod") || p.contains("keyboard")) return true
        return false
    }

    /** Bitmap → base64（JPEG 质量 60，参考 AutoAccounting bitmapToBase64）。 */
    private fun bitmapToBase64(bitmap: Bitmap, packageName: String = ""): String {
        // 安全起见，如果 bitmap 是 HARDWARE 格式，将其复制为 ARGB_8888 软件格式
        val softwareBitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && bitmap.config == Bitmap.Config.HARDWARE) {
            bitmap.copy(Bitmap.Config.ARGB_8888, false)
        } else {
            bitmap
        }
        // 诊断：分析截图有效性（区分「截到空白帧」与「有内容但 OCR 失败」）
        analyzeScreenshot(softwareBitmap, packageName)
        val baos = ByteArrayOutputStream()
        softwareBitmap.compress(Bitmap.CompressFormat.JPEG, 60, baos)
        if (softwareBitmap !== bitmap) {
            softwareBitmap.recycle()
        }
        return "data:image/jpeg;base64," + Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP)
    }

    /**
     * 截图有效性诊断（纯日志，不影响主流程）。
     *
     * 通过网格采样统计像素，输出：
     * - 尺寸、采样数
     * - 是否纯色（所有采样像素颜色完全一致 → 疑似空白/纯色帧）
     * - 亮度均值与方差（方差≈0 → 无内容；方差高 → 有内容）
     * - 深色/浅色像素占比
     *
     * 用于定位微信 WebView 场景：takeScreenshot 是否截到了未渲染的空白帧。
     */
    private fun analyzeScreenshot(bitmap: Bitmap, packageName: String) {
        try {
            val w = bitmap.width
            val h = bitmap.height
            if (w <= 0 || h <= 0) {
                Log.w(TAG, "[截图诊断] pkg=$packageName 无效尺寸: ${w}x${h}")
                return
            }
            // 网格采样：目标约 400 个采样点，避免大图全像素扫描阻塞
            val stepX = Math.max(1, w / 20)
            val stepY = Math.max(1, h / 20)
            var sum = 0L
            var sumSq = 0L
            var count = 0
            var firstPixel = 0
            var isSolidColor = true
            var darkCount = 0   // 亮度 < 30（近黑）
            var lightCount = 0  // 亮度 > 225（近白）
            for (y in 0 until h step stepY) {
                for (x in 0 until w step stepX) {
                    val pixel = bitmap.getPixel(x, y)
                    if (count == 0) {
                        firstPixel = pixel
                    } else if (pixel != firstPixel) {
                        isSolidColor = false
                    }
                    // 亮度（ITU-R BT.601）：0.299R + 0.587G + 0.114B
                    val r = (pixel shr 16) and 0xFF
                    val g = (pixel shr 8) and 0xFF
                    val b = pixel and 0xFF
                    val lum = (r * 299 + g * 587 + b * 114) / 1000
                    sum += lum
                    sumSq += lum.toLong() * lum
                    if (lum < 30) darkCount++
                    if (lum > 225) lightCount++
                    count++
                }
            }
            val mean = if (count > 0) sum.toDouble() / count else 0.0
            val variance = if (count > 0) sumSq.toDouble() / count - mean * mean else 0.0
            val darkRatio = if (count > 0) darkCount.toDouble() / count else 0.0
            val lightRatio = if (count > 0) lightCount.toDouble() / count else 0.0
            // 判定：纯色 或 方差<10 视为「疑似空白帧」
            val likelyBlank = isSolidColor || variance < 10.0
            Log.i(TAG, "[截图诊断] pkg=$packageName 尺寸=${w}x${h} 采样=$count " +
                "纯色=$isSolidColor 亮度均值=${"%.1f".format(mean)} 方差=${"%.1f".format(variance)} " +
                "深色占比=${"%.2f".format(darkRatio)} 浅色占比=${"%.2f".format(lightRatio)} " +
                "疑似空白帧=$likelyBlank base64大小约=${(w * h) / 8}字节")
        } catch (e: Exception) {
            Log.w(TAG, "[截图诊断] pkg=$packageName 分析异常: ${e.message}")
        }
    }

    /** 辅助：构造 WritableNativeMap。 */
    private fun writableMapOf(vararg pairs: Pair<String, Any?>): WritableNativeMap {
        val map = WritableNativeMap()
        for ((k, v) in pairs) {
            when (v) {
                is String -> map.putString(k, v)
                is Int -> map.putInt(k, v)
                is Long -> map.putDouble(k, v.toDouble())
                is Boolean -> map.putBoolean(k, v)
                is Number -> map.putDouble(k, v.toDouble())
                is WritableNativeMap -> map.putMap(k, v)
                is WritableNativeArray -> map.putArray(k, v)
                else -> map.putNull(k)
            }
        }
        return map
    }

    /** 将当前应用拉到前台以显示确认弹窗 */
    fun bringAppToForeground() {
        try {
            val intent = packageManager.getLaunchIntentForPackage(packageName)
            if (intent != null) {
                intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK or android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
                startActivity(intent)
                Log.i(TAG, "已成功拉起本应用到前台显示弹窗")
            }
        } catch (e: Exception) {
            Log.e(TAG, "拉起应用到前台失败: ${e.message}")
        }
    }

    /** 递归提取无障碍节点树的全部文本（text + contentDescription）。 */
    private fun dumpNodeTexts(node: AccessibilityNodeInfo?, list: MutableList<String>) {
        if (node == null) return
        val text = node.text?.toString()
        if (!text.isNullOrBlank()) {
            list.add(text)
        }
        // 修复：微信 8.0.52+ 混淆了 text，但部分节点仍通过 contentDescription 暴露内容
        val desc = node.contentDescription?.toString()
        if (!desc.isNullOrBlank() && desc != text) {
            list.add(desc)
        }
        val childCount = node.childCount
        for (i in 0 until childCount) {
            val child = node.getChild(i) ?: continue
            dumpNodeTexts(child, list)
            child.recycle()
        }
    }

    /**
     * 增强版文本提取：先尝试 rootInActiveWindow，若为空则遍历所有窗口。
     * 修复：新版 Android WebView 可能不在 activeWindow 中暴露节点，
     * 需要通过 getWindows() 获取 WebView 子窗口的节点树。
     */
    private fun dumpAllTexts(): List<String> {
        val texts = mutableListOf<String>()

        // 1. 先尝试 rootInActiveWindow（常规路径）
        val rootNode = rootInActiveWindow
        if (rootNode != null) {
            dumpNodeTexts(rootNode, texts)
            rootNode.recycle()
        }

        // 2. 如果 activeWindow 为空或无文本，遍历所有窗口（WebView 子窗口可能在这里）
        if (texts.isEmpty()) {
            try {
                val allWindows = windows
                for (window in allWindows) {
                    val wRoot = window.root ?: continue
                    dumpNodeTexts(wRoot, texts)
                    wRoot.recycle()
                }
            } catch (e: Exception) {
                Log.w(TAG, "getWindows() 遍历失败: ${e.message}")
            }
        }

        return texts
    }

    /** 递归遍历无障碍节点树，检查是否包含指定的任意一个关键字。 */
    private fun findTextInNode(node: AccessibilityNodeInfo?, keywords: List<String>): Boolean {
        if (node == null) return false
        val text = node.text?.toString()
        if (text != null) {
            for (keyword in keywords) {
                if (text.contains(keyword)) {
                    return true
                }
            }
        }
        val childCount = node.childCount
        for (i in 0 until childCount) {
            val child = node.getChild(i) ?: continue
            val found = findTextInNode(child, keywords)
            child.recycle()
            if (found) return true
        }
        return false
    }

    override fun onInterrupt() {
        Log.w(TAG, "无障碍服务被中断")
    }

    override fun onUnbind(intent: android.content.Intent?): Boolean {
        floatingHelper?.dismiss()
        floatingHelper = null
        instance = null
        return super.onUnbind(intent)
    }
}

/**
 * RN 上下文持有者（由 MainApplication 注入）。
 * 无障碍服务运行在系统进程，需通过静态引用访问 RN 上下文以发送事件。
 */
object ReactContextHolder {
    @Volatile var context: ReactApplicationContext? = null
}
