package com.beancount.mobile.ppocr

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Base64
import android.util.Log
import ai.onnxruntime.OnnxTensor
import ai.onnxruntime.OrtEnvironment
import ai.onnxruntime.OrtSession
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.WritableNativeArray
import com.facebook.react.bridge.WritableNativeMap
import com.facebook.react.module.annotations.ReactModule
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.io.BufferedReader
import java.io.InputStreamReader
import java.nio.FloatBuffer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kotlin.math.max
import kotlin.math.min

/**
 * PP-OCRv6 (ONNX Runtime) React Native Bridge（plan.md「3.4 Layer 2」+「决策 4 Config Plugin」）。
 *
 * 引擎：ONNX Runtime（跨平台、微软官方、Windows 友好），替代 NCNN 路线。
 * 模型：ppocrv6_det.onnx + ppocrv6_rec.onnx（PP-OCRv6 small，从 PaddlePaddle 官方 HuggingFace 下载）。
 * 字典：ppocrv6_dict.txt（PP-OCRv6 多语言字典，18708 字符；rec 模型 18710 维输出 = 字典 + blank + 特殊位）。
 *
 * 流水线：
 *  1. det（文本检测）：bitmap → DB 后处理得到文本框
 *  2. rec（文本识别）：每个框 crop → resize 到 48px 高 → CTC 解码
 *
 * 性能优化（参考 AutoAccounting OcrProcessor.kt）：
 *  - 短边压缩到 720px（像素量比 1440p 减少约 75%）
 *  - CPU 执行（兼容性最稳，部分设备 GPU 会崩溃）
 *  - det 最大边限制 960（PaddleOCR 默认 limit_max_side_len）
 *
 * JS 层通过 NativeModules.PpOcr.recognizeText(base64) 调用。
 */
const val OCR_MODULE_NAME = "PpOcr"

@ReactModule(name = OCR_MODULE_NAME)
class OcrModule(private val context: ReactApplicationContext) :
    ReactContextBaseJavaModule(context) {

    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    private val lock = ReentrantLock()

    private var ortEnv: OrtEnvironment? = null
    private var detSession: OrtSession? = null
    private var recSession: OrtSession? = null
    private var dictionary: List<String> = emptyList()
    @Volatile private var initialized = false
    @Volatile private var initFailed = false

    /** 模型文件目录（filesystem 绝对路径）。非空时从该目录加载模型，否则回退到 assets。 */
    @Volatile private var modelDir: String? = null
    /** 初始化完成信号，ensureReady 可等待异步 initEngine 完成（最多等 15 秒）。 */
    @Volatile private var initLatch = CountDownLatch(1)

    override fun getName(): String = OCR_MODULE_NAME

    override fun initialize() {
        super.initialize()
        // 异步加载模型，避免阻塞 RN 桥初始化
        scope.launch { initEngine() }
    }

    /** 从 assets 或 filesystem 加载 det/rec ONNX 模型与字典。 */
    private fun initEngine() {
        lock.lock()
        try {
            if (initialized || initFailed) return
            val env = OrtEnvironment.getEnvironment()
            val opts = OrtSession.SessionOptions().apply {
                // CPU 线程数：2 是兼容性/性能的稳妥折中（高端机可调高）
                setInterOpNumThreads(2)
                setIntraOpNumThreads(2)
                // 移动端关闭内存优化里的图优化级别过高（部分模型会崩）
                setOptimizationLevel(OrtSession.SessionOptions.OptLevel.BASIC_OPT)
            }
            val dir = modelDir
            val (det, rec, dict) = if (dir != null) {
                // 从 filesystem 加载（P8：模型按需下载到本地目录）
                val detPath = dir + java.io.File.separator + ASSET_DET_MODEL
                val recPath = dir + java.io.File.separator + ASSET_REC_MODEL
                val dictPath = dir + java.io.File.separator + ASSET_DICT
                Log.i(OCR_MODULE_NAME, "从 filesystem 加载模型: det=$detPath, rec=$recPath")
                Triple(
                    env.createSession(detPath, opts),
                    env.createSession(recPath, opts),
                    loadDictionaryFromFile(dictPath)
                )
            } else {
                // 回退：从 assets 加载（兼容未迁移的老用户）
                val detBytes = context.assets.open(ASSET_DET_MODEL).use { it.readBytes() }
                val recBytes = context.assets.open(ASSET_REC_MODEL).use { it.readBytes() }
                Log.i(OCR_MODULE_NAME, "从 assets 加载模型（兼容模式）")
                Triple(
                    env.createSession(detBytes, opts),
                    env.createSession(recBytes, opts),
                    loadDictionaryFromAssets()
                )
            }

            detSession = det
            recSession = rec
            ortEnv = env
            dictionary = dict
            initialized = true
            initLatch.countDown()
            Log.i(OCR_MODULE_NAME, "PP-OCRv6 ONNX 模型加载成功（det+rec, dict=${dict.size}）")
        } catch (e: Exception) {
            initFailed = true
            initLatch.countDown()
            Log.e(OCR_MODULE_NAME, "OCR 初始化失败: ${e.message}", e)
            val dir = modelDir
            if (dir != null) {
                Log.e(OCR_MODULE_NAME, "请确认 $dir 下存在 $ASSET_DET_MODEL / $ASSET_REC_MODEL / $ASSET_DICT")
            } else {
                Log.e(OCR_MODULE_NAME, "请确认 assets 下存在 $ASSET_DET_MODEL / $ASSET_REC_MODEL / $ASSET_DICT")
            }
        } finally {
            lock.unlock()
        }
    }

    /** rec 推理用的 OrtEnvironment（复用 ortEnv 单例）。 */
    private val recEnv: OrtEnvironment? get() = ortEnv

    /**
     * 从 assets 加载 ppocrv6_dict.txt 字典。
     *
     * PaddleOCR CTC 约定：模型输出 logits 的 index 0 是 blank，字符从 index 1 开始；
     * 字典条目 dictionary[i] 对应模型输出 index i+1。解码时 dictIdx = argmaxIdx - 1。
     * 字典本身不含 blank，运行时固定用 index 0 作 blank（见 ctcGreedyDecode）。
     */
    private fun loadDictionaryFromAssets(): List<String> {
        val words = mutableListOf<String>()
        context.assets.open(ASSET_DICT).use { stream ->
            BufferedReader(InputStreamReader(stream, Charsets.UTF_8)).useLines { lines ->
                lines.forEach { line ->
                    // PaddleOCR 字典每行一个字符（去掉行尾换行）
                    words.add(line.trimEnd('\r', '\n'))
                }
            }
        }
        return words
    }

    /** 从 filesystem 路径加载字典文件。 */
    private fun loadDictionaryFromFile(path: String): List<String> {
        val words = mutableListOf<String>()
        java.io.File(path).bufferedReader(Charsets.UTF_8).useLines { lines ->
            lines.forEach { line ->
                words.add(line.trimEnd('\r', '\n'))
            }
        }
        return words
    }

    /**
     * 识别图片文本。
     * @param imageBase64 base64 编码的图片（JPEG/PNG）
     * @return 识别出的纯文本（所有行用 \n 连接）
     */
    @ReactMethod
    fun recognizeText(imageBase64: String, promise: Promise) {
        scope.launch {
            var bitmap: Bitmap? = null
            var scaled: Bitmap? = null
            try {
                ensureReady()
                bitmap = decodeBase64(imageBase64)
                if (bitmap == null) {
                    promise.reject("DECODE_FAILED", "base64 解码失败")
                    return@launch
                }
                scaled = capLongEdge(bitmap, CAP_LONG_EDGE)
                val blocks = runInference(scaled)
                val text = blocks.joinToString("\n") { it.text }
                promise.resolve(text)
            } catch (e: Exception) {
                Log.e(OCR_MODULE_NAME, "recognizeText 异常: ${e.message}", e)
                promise.reject("OCR_ERROR", e.message)
            } finally {
                bitmap?.recycle()
                if (scaled !== bitmap) {
                    scaled?.recycle()
                }
            }
        }
    }

    /**
     * 识别并返回带坐标的文本块（用于复杂版面）。
     * @return JSON 数组字符串：[{text, x, y, width, height, confidence}]
     */
    @ReactMethod
    fun recognizeTextBlocks(imageBase64: String, promise: Promise) {
        scope.launch {
            var bitmap: Bitmap? = null
            var scaled: Bitmap? = null
            try {
                ensureReady()
                bitmap = decodeBase64(imageBase64)
                if (bitmap == null) {
                    promise.reject("DECODE_FAILED", "base64 解码失败")
                    return@launch
                }
                scaled = capLongEdge(bitmap, CAP_LONG_EDGE)
                val blocks = runInference(scaled)
                // 序列化为 RN WritableArray
                val arr = WritableNativeArray()
                for (b in blocks) {
                    val map: WritableMap = WritableNativeMap()
                    map.putString("text", b.text)
                    map.putDouble("x", b.x.toDouble())
                    map.putDouble("y", b.y.toDouble())
                    map.putDouble("width", b.width.toDouble())
                    map.putDouble("height", b.height.toDouble())
                    map.putDouble("confidence", b.confidence.toDouble())
                    arr.pushMap(map)
                }
                promise.resolve(arr)
            } catch (e: Exception) {
                Log.e(OCR_MODULE_NAME, "recognizeTextBlocks 异常: ${e.message}", e)
                promise.reject("OCR_ERROR", e.message)
            } finally {
                bitmap?.recycle()
                if (scaled !== bitmap) {
                    scaled?.recycle()
                }
            }
        }
    }

    /** 引擎是否已就绪（模型加载完成）。 */
    @ReactMethod
    fun isReady(promise: Promise) {
        promise.resolve(initialized)
    }

    /** 设置模型文件目录（绝对路径）。若引擎已初始化则释放并重新加载。 */
    @ReactMethod
    fun setModelDir(dir: String, promise: Promise) {
    // 修复：expo-file-system 返回 file:// URI，需转为文件系统绝对路径
    val newDir = dir.removePrefix("file://")
    // 修复：目录未变且引擎已就绪时跳过重新加载，避免冗余 release 导致竞态
    if (newDir == modelDir && initialized) {
        promise.resolve(true)
        return
    }
    modelDir = newDir
    // 修复：无论之前是成功还是失败，设置新目录后都应重新加载模型
    if (initialized || initFailed) {
        release()
        initFailed = false
        initLatch = CountDownLatch(1)
        scope.launch { initEngine() }
    }
    promise.resolve(true)
    }

    // ============== 推理流水线 ==============

    private fun ensureReady() {
        if (!initialized && !initFailed) initEngine()
        // 修复：异步 initEngine 进行中时等待完成（最多 15 秒），而非立即抛异常
        if (!initialized && !initFailed) {
            initLatch.await(15, TimeUnit.SECONDS)
        }
        if (!initialized) throw IllegalStateException("OCR 引擎未就绪（模型未加载，${if (initFailed) "初始化失败" else "加载中"}）")
    }

    /** 完整推理：det 检测文本框 → 对每个框 rec 识别 → 返回带坐标的文本块。 */
    private fun runInference(bitmap: Bitmap): List<OcrBlock> {
        val totalStartTime = System.currentTimeMillis()
        Log.i(OCR_MODULE_NAME, "runInference 开始: bitmap 尺寸 = ${bitmap.width}x${bitmap.height}")
        val det = detSession ?: run {
            Log.e(OCR_MODULE_NAME, "detSession 为空，放弃推理")
            return emptyList()
        }
        val rec = recSession ?: run {
            Log.e(OCR_MODULE_NAME, "recSession 为空，放弃推理")
            return emptyList()
        }

        var resized: Bitmap? = null
        var detInputTensor: OnnxTensor? = null
        var detOutputs: OrtSession.Result? = null
        val results = mutableListOf<OcrBlock>()

        var detTime = 0L
        var postTime = 0L
        var recTime = 0L
        var boxCount = 0

        try {
            // ---- 1. 文本检测（DB）----
            val detStartTime = System.currentTimeMillis()
            resized = resizeForDet(bitmap, DET_LIMIT_MAX_SIDE)
            Log.i(OCR_MODULE_NAME, "det 图像缩放后尺寸 = ${resized.width}x${resized.height}")
            val ratioX = bitmap.width.toFloat() / resized.width
            val ratioY = bitmap.height.toFloat() / resized.height

            val detInput = preprocessDet(resized)
            detInputTensor = OnnxTensor.createTensor(recEnv, FloatBuffer.wrap(detInput.data), longArrayOf(1L, 3L, detInput.h.toLong(), detInput.w.toLong()))
            val detInputs = mapOf("x" to detInputTensor)
            
            val detRunStart = System.currentTimeMillis()
            detOutputs = det.run(detInputs)
            detTime = System.currentTimeMillis() - detRunStart

            @Suppress("UNCHECKED_CAST")
            val detProb = (detOutputs[0].value as Array<Array<Array<FloatArray>>>)[0][0]  // [H,W]
            Log.i(OCR_MODULE_NAME, "det 推理完成 (耗时: ${detTime}ms)，概率图尺寸 = ${detProb.size}x${detProb[0].size}")

            // DB 后处理：threshold → 轮廓 → 最小外接矩形
            val postStart = System.currentTimeMillis()
            val boxes = dbPostprocess(detProb, detInput.h, detInput.w, ratioX, ratioY)
            postTime = System.currentTimeMillis() - postStart
            boxCount = boxes.size
            Log.i(OCR_MODULE_NAME, "dbPostprocess 后处理完成 (耗时: ${postTime}ms)，检测到文本框数量 = ${boxCount}")

            if (boxes.isEmpty()) {
                val totalTime = System.currentTimeMillis() - totalStartTime
                Log.i(OCR_MODULE_NAME, "PP-OCRv6 推理完成(无文本): 总耗时 = ${totalTime}ms (det模型推理 = ${detTime}ms, det后处理 = ${postTime}ms)")
                return emptyList()
            }

            // ---- 2. 文本识别（CRNN+CTC）----
            val recStart = System.currentTimeMillis()
            for ((idx, box) in boxes.withIndex()) {
                var crop: Bitmap? = null
                var recInputTensor: OnnxTensor? = null
                var recOutputs: OrtSession.Result? = null
                try {
                    crop = cropBox(bitmap, box)
                    if (crop == null) {
                        Log.w(OCR_MODULE_NAME, "裁剪文本框失败 (index = $idx)")
                        continue
                    }
                    val recInput = preprocessRec(crop)
                    recInputTensor = OnnxTensor.createTensor(recEnv, FloatBuffer.wrap(recInput.data), longArrayOf(1L, 3L, REC_IMAGE_HEIGHT.toLong(), recInput.w.toLong()))
                    val recInputs = mapOf("x" to recInputTensor)
                    recOutputs = rec.run(recInputs)
                    // 输出 shape: [1, T, numClasses]
                    @Suppress("UNCHECKED_CAST")
                    val logits = (recOutputs[0].value as Array<Array<FloatArray>>)[0]

                    val (text, conf) = ctcGreedyDecode(logits)
                    val xs = box.map { it[0] }
                    val ys = box.map { it[1] }
                    val minX = (xs.minOrNull() ?: 0f).toInt()
                    val minY = (ys.minOrNull() ?: 0f).toInt()
                    val maxX = (xs.maxOrNull() ?: 0f).toInt()
                    val maxY = (ys.maxOrNull() ?: 0f).toInt()
                    val w = maxX - minX
                    val h = maxY - minY
                    Log.i(OCR_MODULE_NAME, "文本框 $idx 识别结果 = '$text', 坐标 = ($minX, $minY, $w, $h), 置信度 = $conf")
                    if (text.isNotEmpty()) {
                        results.add(OcrBlock(text, minX.toFloat(), minY.toFloat(), w.toFloat(), h.toFloat(), conf))
                    }
                } finally {
                    crop?.recycle()
                    recInputTensor?.close()
                    recOutputs?.close()
                }
            }
            recTime = System.currentTimeMillis() - recStart
        } finally {
            if (resized !== bitmap) {
                resized?.recycle()
            }
            detInputTensor?.close()
            detOutputs?.close()
        }

        val totalTime = System.currentTimeMillis() - totalStartTime
        Log.i(
            OCR_MODULE_NAME,
            "PP-OCRv6 ONNX 推理整体完成: 总耗时 = ${totalTime}ms (det模型推理 = ${detTime}ms, det后处理 = ${postTime}ms, rec文本识别 = ${recTime}ms, 识别文本框数 = ${results.size}/${boxCount})"
        )

        return results
    }

    // ============== 前处理 ==============

    /** det 前处理：resize → BCHW → normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225])。 */
    private fun preprocessDet(bmp: Bitmap): TensorData {
        val w = bmp.width
        val h = bmp.height
        val pixels = IntArray(w * h)
        bmp.getPixels(pixels, 0, w, 0, 0, w, h)
        val data = FloatArray(3 * w * h)
        // BCHW 顺序
        val means = floatArrayOf(0.485f, 0.456f, 0.406f)
        val stds = floatArrayOf(0.229f, 0.224f, 0.225f)
        for (c in 0..2) {
            val mean = means[c]
            val std = stds[c]
            for (y in 0 until h) {
                for (x in 0 until w) {
                    val px = pixels[y * w + x]
                    // 对齐官方训练通道序 BGR（c=0→B, 1→G, 2→R）；mean/std 数值按 BGR 顺序配套
                    val channelVal = (px shr (8 * c)) and 0xFF
                    data[c * w * h + y * w + x] = (channelVal / 255.0f - mean) / std
                }
            }
        }
        return TensorData(data, w, h)
    }

    /** rec 前处理：crop → resize 到 48 高（保持宽高比）→ pad 到整除 4 → normalize。 */
    private fun preprocessRec(bmp: Bitmap): TensorData {
        var w = bmp.width
        val h = bmp.height
        // resize 到高度 48，宽度等比缩放（ceil 取整对齐官方，避免右缘字符因 floor 被裁）
        // 注：java.lang.Math.ceil 仅有 double 重载，故用 toDouble()（Math.round 有 float 重载所以无需）
        var resizedW = Math.ceil(w.toDouble() / h * REC_IMAGE_HEIGHT).toInt()
        // 宽度上限，避免单行过长爆显存（官方 cap=3200，移动端折中 REC_MAX_WIDTH=1280）
        resizedW = min(resizedW, REC_MAX_WIDTH)
        resizedW = max(resizedW, 1)
        val resized = if (resizedW == w && h == REC_IMAGE_HEIGHT) bmp
                      else Bitmap.createScaledBitmap(bmp, resizedW, REC_IMAGE_HEIGHT, true)
        w = resized.width
        val pixels = IntArray(w * REC_IMAGE_HEIGHT)
        resized.getPixels(pixels, 0, w, 0, 0, w, REC_IMAGE_HEIGHT)
        val data = FloatArray(3 * w * REC_IMAGE_HEIGHT)
        val means = floatArrayOf(0.5f, 0.5f, 0.5f)
        val stds = floatArrayOf(0.5f, 0.5f, 0.5f)
        for (c in 0..2) {
            val mean = means[c]
            val std = stds[c]
            for (y in 0 until REC_IMAGE_HEIGHT) {
                for (x in 0 until w) {
                    val px = pixels[y * w + x]
                    val channelVal = (px shr (16 - 8 * c)) and 0xFF
                    data[c * w * REC_IMAGE_HEIGHT + y * w + x] = (channelVal / 255.0f - mean) / std
                }
            }
        }
        if (resized !== bmp) resized.recycle()
        return TensorData(data, w, REC_IMAGE_HEIGHT)
    }

    // ============== DB 后处理（连通域法，对齐 PaddleOCR 官方） ==============
    // sigmoid → 阈值二值化 → 4-连通域标记 → 每域 bbox 按官方 unclip 公式外扩 → box_score_fast 过滤。
    // 取代旧的「水平/垂直投影法」：投影法会把基线孤立小数点切到行外导致金额丢点（¥143.97→¥14397）。

    /**
     * DB 后处理：sigmoid + 阈值 0.3 → 二值图 → 连通域外接矩形。
     * 简化版：用水平投影切行 + 垂直投影切列，得到矩形框（对账单类版面够用）。
     */
    private fun dbPostprocess(prob: Array<FloatArray>, h: Int, w: Int, ratioX: Float, ratioY: Float): List<List<FloatArray>> {
        // 自动检测是否需要 Sigmoid
        var minVal = Float.MAX_VALUE
        var maxVal = Float.MIN_VALUE
        for (y in 0 until h) {
            for (x in 0 until w) {
                val v = prob[y][x]
                if (v < minVal) minVal = v
                if (v > maxVal) maxVal = v
            }
        }
        val needSigmoid = minVal < -0.05f || maxVal > 1.05f
        Log.i(OCR_MODULE_NAME, "DB prob min=$minVal, max=$maxVal, needSigmoid=$needSigmoid")

        // 过滤状态栏（前8%）和导航栏（后8%），避免其上的干扰字符（如电池、时间、返回键）影响识别或导致行粘连
        val startY = (h * 0.08).toInt()
        val endY = (h * 0.92).toInt()

        val binMask = Array(h) { IntArray(w) }
        val sigMap = Array(h) { FloatArray(w) }  // 概率图（sigmoid 后），供连通域 box_score_fast 取文本像素均值
        var activeCountTotal = 0
        for (y in 0 until h) {
            for (x in 0 until w) {
                if (y < startY || y > endY) {
                    binMask[y][x] = 0
                    continue
                }
                val raw = prob[y][x]
                val sig = if (needSigmoid) {
                    1.0f / (1.0f + Math.exp(-raw.toDouble()).toFloat())
                } else {
                    raw
                }
                sigMap[y][x] = sig
                val isActive = if (sig > DET_THRESH) 1 else 0
                binMask[y][x] = isActive
                if (isActive == 1) activeCountTotal++
            }
        }
        Log.i(OCR_MODULE_NAME, "二值化完成: 活跃像素 = $activeCountTotal / ${w * h}")

        // 清理垂直干扰线（如滚动条、背景边框线）：如果某列在文本有效区域内的活跃像素超过该区域高度的 30%，视为干扰列，整列清零
        val maxColActive = ((endY - startY) * 0.3).toInt()
        var clearedColsCount = 0
        for (x in 0 until w) {
            var colActive = 0
            for (y in startY..endY) {
                if (binMask[y][x] == 1) colActive++
            }
            if (colActive > maxColActive) {
                clearedColsCount++
                for (y in 0 until h) {
                    binMask[y][x] = 0
                }
            }
        }
        Log.i(OCR_MODULE_NAME, "垂直线噪清理完成: 清理了 $clearedColsCount / $w 列")

        // 连通域标记（4-连通，迭代 BFS 防栈溢出）：取代旧的「水平投影切行 + 垂直投影切列」。
        // 投影法会把基线/顶线上的孤立标点（小数点等，该行水平投影 < w/20）切到行外，
        // 导致 crop 不含标点、rec 漏识（金额 ¥143.97 → ¥14397 的根因，已用官方同模型对照坐实）。
        // 连通域天然把「数字 + 基线点」归为同一域，根治丢点；并正确处理断裂字符与多栏版面。
        val labels = Array(h) { IntArray(w) }
        var nComp = 0
        val compX0 = mutableListOf<Int>()
        val compY0 = mutableListOf<Int>()
        val compX1 = mutableListOf<Int>()
        val compY1 = mutableListOf<Int>()
        val compArea = mutableListOf<Int>()
        val compSum = mutableListOf<Float>()  // 域内文本像素 sig 累加，供 box_score_fast 取均值
        val stack = ArrayDeque<IntArray>()
        for (y in 0 until h) {
            for (x in 0 until w) {
                if (binMask[y][x] == 1 && labels[y][x] == 0) {
                    nComp++
                    val id = nComp
                    var x0 = x; var y0 = y; var x1 = x; var y1 = y
                    var area = 0; var sum = 0f
                    stack.clear()
                    stack.addLast(intArrayOf(x, y))
                    labels[y][x] = id
                    while (stack.isNotEmpty()) {
                        val p = stack.removeLast()
                        val px = p[0]; val py = p[1]
                        area++
                        sum += sigMap[py][px]
                        if (px < x0) x0 = px; if (px > x1) x1 = px
                        if (py < y0) y0 = py; if (py > y1) y1 = py
                        if (px > 0 && binMask[py][px - 1] == 1 && labels[py][px - 1] == 0) { labels[py][px - 1] = id; stack.addLast(intArrayOf(px - 1, py)) }
                        if (px < w - 1 && binMask[py][px + 1] == 1 && labels[py][px + 1] == 0) { labels[py][px + 1] = id; stack.addLast(intArrayOf(px + 1, py)) }
                        if (py > 0 && binMask[py - 1][px] == 1 && labels[py - 1][px] == 0) { labels[py - 1][px] = id; stack.addLast(intArrayOf(px, py - 1)) }
                        if (py < h - 1 && binMask[py + 1][px] == 1 && labels[py + 1][px] == 0) { labels[py + 1][px] = id; stack.addLast(intArrayOf(px, py + 1)) }
                    }
                    compX0.add(x0); compY0.add(y0); compX1.add(x1); compY1.add(y1)
                    compArea.add(area); compSum.add(sum)
                }
            }
        }
        Log.i(OCR_MODULE_NAME, "dbPostprocess 连通域标记完成，连通域数 = $nComp")

        val boxes = mutableListOf<List<FloatArray>>()
        var dropSmall = 0; var dropScore = 0
        for (i in 0 until nComp) {
            val bx0 = compX0[i]; val by0 = compY0[i]; val bx1 = compX1[i]; val by1 = compY1[i]
            val bw = bx1 - bx0 + 1; val bh = by1 - by0 + 1
            val area = compArea[i]
            val perim = 2 * (bw + bh)
            // 官方 unclip 公式：distance = 连通域面积 × 比例 / 周长（水平文本下≈各向同性外扩，把基线标点纳入框）
            var d = if (perim > 0) area * UNCLIP_RATIO / perim else 0f
            if (d < 1f) d = 1f
            val nx0 = max(0, Math.round(bx0 - d))
            val ny0 = max(0, Math.round(by0 - d))
            val nx1 = min(w - 1, Math.round(bx1 + d))
            val ny1 = min(h - 1, Math.round(by1 + d))
            if ((nx1 - nx0 + 1) < MIN_SIZE || (ny1 - ny0 + 1) < MIN_SIZE) { dropSmall++; continue }
            // box_score_fast：域内文本像素 prob 均值（非整 bbox 均值，否则被背景稀释而误杀）
            val score = compSum[i] / area
            if (score < BOX_THRESH) { dropScore++; continue }
            val fx0 = nx0 * ratioX; val fy0 = ny0 * ratioY
            val fx1 = nx1 * ratioX; val fy1 = ny1 * ratioY
            boxes.add(listOf(
                floatArrayOf(fx0, fy0),
                floatArrayOf(fx1, fy0),
                floatArrayOf(fx1, fy1),
                floatArrayOf(fx0, fy1),
            ))
        }
        Log.i(OCR_MODULE_NAME, "dbPostprocess 取框完成: 扩后min_size丢=$dropSmall, box_score丢=$dropScore, 保留=${boxes.size}")
        return boxes
    }

    // ============== CTC 解码 ==============

    /**
     * CTC greedy decode：每个时间步取 argmax，去 blank 去重复。返回 (text, avgConfidence)。
     *
     * PaddleOCR 约定：logits 的 index 0 固定是 blank，字符从 index 1 起，
     * dictionary[i] 对应模型输出 index i+1。因此 dictIdx = argmaxIdx - 1。
     *
     * PP-OCRv6 模型输出已经是概率分布（值域 [0,1]），无需额外 softmax。
     * 直接取 argmax 对应的值作为该时间步的置信度。
     */
    private fun ctcGreedyDecode(logits: Array<FloatArray>): Pair<String, Float> {
        if (logits.isEmpty()) return "" to 0f
        val numClasses = logits[0].size
        val blankIdx = 0  // PaddleOCR CTC：blank 固定在 index 0

        val sb = StringBuilder()
        var lastIdx = -1
        var confSum = 0.0f
        var confCount = 0
        for (t in logits.indices) {
            var maxIdx = 0
            var maxVal = logits[t][0]
            for (i in 1 until numClasses) {
                if (logits[t][i] > maxVal) { maxVal = logits[t][i]; maxIdx = i }
            }

            if (maxIdx != blankIdx && maxIdx != lastIdx) {
                val dictIdx = maxIdx - 1  // index 1..N → dictionary[0..N-1]
                if (dictIdx in 0 until dictionary.size) {
                    sb.append(dictionary[dictIdx])
                    confSum += maxVal  // v6 输出已是概率，直接用
                    confCount++
                }
            }
            lastIdx = maxIdx
        }
        val avgConf = if (confCount > 0) confSum / confCount else 0f
        return sb.toString() to avgConf
    }

    // ============== Bitmap 工具 ==============

    private fun cropBox(bmp: Bitmap, box: List<FloatArray>): Bitmap? {
        val xs = box.map { it[0] }
        val ys = box.map { it[1] }
        val paddingX = 4
        val paddingY = 2
        val minX = max(0, (xs.minOrNull() ?: 0f).toInt() - paddingX)
        val minY = max(0, (ys.minOrNull() ?: 0f).toInt() - paddingY)
        val maxX = min(bmp.width, ((xs.maxOrNull() ?: 0f) + 1).toInt() + paddingX)
        val maxY = min(bmp.height, ((ys.maxOrNull() ?: 0f) + 1).toInt() + paddingY)
        val w = maxX - minX
        val h = maxY - minY
        if (w < 2 || h < 2) return null
        var crop = Bitmap.createBitmap(bmp, minX, minY, w, h)
        // 竖排文本旋转 90°（对齐官方 get_rotate_crop_image：高/宽 ≥ 1.5 视为竖排，
        // 否则 rec 模型按水平行识别会乱码；账单侧边竖排小字借此可识别）
        if (crop.height >= crop.width * 1.5f) {
            val m = android.graphics.Matrix()
            m.postRotate(90f)
            val rotated = Bitmap.createBitmap(crop, 0, 0, crop.width, crop.height, m, true)
            if (rotated !== crop) crop.recycle()
            crop = rotated
        }
        return crop
    }

    private fun resizeForDet(bmp: Bitmap, maxSide: Int): Bitmap {
        // 长边上限 + 无条件对齐 32（det 下采样要求），half-up 取整对齐官方 round。
        // 关键修复：旧实现 ratio>=1 时直接 return 不对齐，若整图未预压且非 32 倍数会喂入非法尺寸，
        // 导致 det 内部特征图广播报错；现无条件对齐，且 cap 后图通常 ≤32 倍数时仅轻微取整。
        val ratioC = minOf(1f, maxSide.toFloat() / max(bmp.width, bmp.height))
        val newW = Math.round(bmp.width * ratioC)
        val newH = Math.round(bmp.height * ratioC)
        val alignedW = max(32, Math.round(newW / 32f) * 32)
        val alignedH = max(32, Math.round(newH / 32f) * 32)
        if (alignedW == bmp.width && alignedH == bmp.height) return bmp
        return Bitmap.createScaledBitmap(bmp, alignedW, alignedH, true)
    }

    // ============== 公共工具（复用） ==============

    /** 解码 base64 图片为 Bitmap。 */
    private fun decodeBase64(base64: String): Bitmap? {
        return try {
            // 去除 data:image/...;base64, 前缀
            val data = if (base64.contains(",")) base64.substringAfter(",") else base64
            val bytes = Base64.decode(data, Base64.DEFAULT)
            BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
        } catch (e: Exception) {
            Log.e(OCR_MODULE_NAME, "base64 解码失败: ${e.message}")
            null
        }
    }

    /**
     * 整图长边上限降采样：仅当长边超过 cap 才按比例缩小（half-up 取整），否则原样返回。
     * 使 rec 的 crop 源尽量高清（手机截图通常不触发），仅防超大图 OOM。
     * 取代旧的「短边压 720」——旧法把整图先砍掉 75% 像素，叠加 det resize 后小数点等细笔画被严重淡化。
     */
    private fun capLongEdge(bitmap: Bitmap, capLong: Int): Bitmap {
        val width = bitmap.width
        val height = bitmap.height
        val longEdge = maxOf(width, height)
        if (longEdge <= capLong) return bitmap
        val scale = capLong.toFloat() / longEdge
        val newWidth = Math.round(width * scale)
        val newHeight = Math.round(height * scale)
        return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
    }

    override fun onCatalystInstanceDestroy() {
        super.onCatalystInstanceDestroy()
        release()
    }

    override fun invalidate() {
        super.invalidate()
        release()
    }

    private fun release() {
        lock.lock()
        try {
            detSession?.close()
            recSession?.close()
            // OrtEnvironment 是单例，不主动 close（进程级）
            detSession = null
            recSession = null
            ortEnv = null
            initialized = false
        } catch (_: Exception) {
        } finally {
            lock.unlock()
        }
    }

    /** 识别结果块。 */
    private data class OcrBlock(val text: String, val x: Float, val y: Float, val width: Float, val height: Float, val confidence: Float)
    /** 预处理后的张量数据 + 宽高。 */
    private data class TensorData(val data: FloatArray, val w: Int, val h: Int)

    companion object {
        /** 整图长边上限：仅超大图降采样防 OOM；手机截图(≤2400)不预压，使 rec 的 crop 源为高清原图。
         *  对齐官方「只放大不缩小」语义的移动端折中（官方 max_side_limit=4000）。 */
        private const val CAP_LONG_EDGE = 3000
        /** det 输入长边上限（官方 OCR 管线不降采样；移动端为性能折中取 1600，旧值 960 会让小数点仅 1-2px 而糊掉）。 */
        private const val DET_LIMIT_MAX_SIDE = 1600
        /** DB 二值化阈值（对齐官方模型 inference.yml=0.2，对细笔画/小数点更敏感；噪声框由 BOX_THRESH 兜底）。 */
        private const val DET_THRESH = 0.2f
        /** box_score_fast 过滤阈值：连通域文本像素 prob 均值低于此值视为噪声框（对齐官方 OCR 管线=0.6）。 */
        private const val BOX_THRESH = 0.6f
        /** 官方 unclip 外扩比例：distance = 连通域面积 × 比例 / 周长。 */
        private const val UNCLIP_RATIO = 1.5f
        /** 外扩后文本框短边下限（像素），小于此值丢弃（对齐官方 min_size=5）。 */
        private const val MIN_SIZE = 5
        /** rec 固定图像高度。 */
        private const val REC_IMAGE_HEIGHT = 48
        /** rec 单行最大宽度（官方 cap=3200，移动端折中 1280；旧值 320 会把长商户名水平压扁 5×+）。 */
        private const val REC_MAX_WIDTH = 1280
        /** assets 中的模型/字典文件名。 */
        private const val ASSET_DET_MODEL = "ppocrv6_det.onnx"
        private const val ASSET_REC_MODEL = "ppocrv6_rec.onnx"
        // PP-OCRv6 多语言识别模型的配套字典（18708 字符 + 运行时 1 blank = 18710 维输出）。
        // 注意：必须与 rec 模型配套，错用 v5 字典（18383）会导致 CTC 解码丢字符。
        private const val ASSET_DICT = "ppocrv6_dict.txt"
    }
}
