package com.tencent.qian.webview.filechooser

import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import android.webkit.WebChromeClient
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.tencent.qian.webview.model.TencentQianWebViewConfig.CameraCaptureBehavior
import com.tencent.qian.webview.permission.RuntimePermissionRequester
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean

/**
 * H5 `<input type="file">` 文件选择启动器。
 *
 * **支持**:
 * - `accept` 透传(MIME)
 * - `multiple` 多选
 * - 取消时调 `filePathCallback.onReceiveValue(null)` 释放 H5 端的 input(否则连续两次点击 input 失效)
 * - **HTML Media Capture**(`<input capture>`):识别 `params.isCaptureEnabled` 并按
 *   [CameraCaptureBehavior] 拉系统相机(优先级:capture 属性 > 老协议 MIME 兜底)
 * - **人脸核身老录制模式兜底**:`accept` 命中 [DEFAULT_LEGACY_FACE_CAPTURE_MIME_TYPES](默认 `video/kyc`、
 *   `video/faceid`)时改用 [MediaStore.ACTION_VIDEO_CAPTURE] 拉起系统相机录像;
 *   客户可通过 `TencentQianWebViewConfig.legacyFaceCaptureMimeTypes` 自定义或关闭(传 `emptyList()`)。
 *
 * **iOS 等价**:WKWebView 内核 iOS 14+ 原生处理 `<input>` 的 `accept` / `capture` / `multiple`,
 * 无需此封装。
 */
public class FileChooserLauncher private constructor(
    private val launcher: ActivityResultLauncher<Intent>,
    private val state: State,
    private val contextProvider: () -> Context?,
    private val permissionRequester: RuntimePermissionRequester?,
    private val activityProvider: () -> Activity?
) {

    private class State {
        var pending: WebChromeClient.FileChooserParams? = null
        var pathCallback: SafeValueCallback? = null
        /** 标记本次启动是否走的系统相机分支(回调 result.data 解析逻辑不同)。 */
        var isCameraCapture: Boolean = false
        /** 相机拍摄时通过 EXTRA_OUTPUT 指定的输出 Uri(部分 OEM 需要)。 */
        var cameraOutputUri: Uri? = null

        /**
         * 本次 Activity 生命周期内创建的所有临时拍摄文件路径。
         *
         * onDestroy 时精确删除这些文件,避免残留。
         * 使用 File 路径而非 Uri,因为 content:// Uri 无法直接用于文件删除。
         */
        val capturedFiles: MutableList<File> = mutableListOf()
    }



    /**
     * 包装 WebView 的 [android.webkit.ValueCallback],确保 `onReceiveValue` **只被调用一次**。
     *
     * **背景**:Android WebView 内核在某些版本(尤其 Android 10 及以下)中,
     * 当新的 `onShowFileChooser` 触发时会自动对旧 callback 调用 `onReceiveValue(null)`,
     * 如果 SDK 也对旧 callback 调了一次清理,就会触发
     * `IllegalStateException: Duplicate showFileChooser result` → **FATAL crash**。
     *
     * 本包装器通过 `consumed` 标志位保证幂等,第二次调用静默忽略。
     */
    private class SafeValueCallback(
        private val delegate: android.webkit.ValueCallback<Array<Uri>>?
    ) {
        private val consumed = AtomicBoolean(false)

        fun onReceiveValue(value: Array<Uri>?) {
            if (!consumed.compareAndSet(false, true)) return
            delegate?.onReceiveValue(value)
        }

        val isConsumed: Boolean get() = consumed.get()
    }

    /**
     * 启动选择器。客户在 [WebChromeClient.onShowFileChooser] 内调此方法返回 `true`。
     *
     * @param legacyFaceCaptureMimeTypes 命中此列表中任一 MIME 时,改走系统相机录像(老录制人脸核身兜底);
     *                                   传 `emptyList()` 关闭兜底。默认 [DEFAULT_LEGACY_FACE_CAPTURE_MIME_TYPES]。
     * @param cameraCaptureBehavior `<input capture>` 处理策略,默认 [CameraCaptureBehavior.PREFER_FRONT]。
     * @return `true` 已成功启动
     */
    @JvmOverloads
    public fun launch(
        params: WebChromeClient.FileChooserParams,
        filePathCallback: android.webkit.ValueCallback<Array<Uri>>?,
        legacyFaceCaptureMimeTypes: List<String> = DEFAULT_LEGACY_FACE_CAPTURE_MIME_TYPES,
        cameraCaptureBehavior: CameraCaptureBehavior = CameraCaptureBehavior.PREFER_FRONT
    ): Boolean {
        // 先把上一次未完成的 callback 清理掉,避免泄漏
        // 使用 SafeValueCallback 保证幂等:即使 WebView 内核已自动取消旧 callback,
        // 这里再调一次也不会触发 "Duplicate showFileChooser result" crash。
        state.pathCallback?.onReceiveValue(null)
        val safeCallback = SafeValueCallback(filePathCallback)
        state.pathCallback = safeCallback
        state.pending = params
        state.isCameraCapture = false

        // ── 优先级 1:H5 显式声明 `capture` 属性 ──
        //  Android `FileChooserParams.isCaptureEnabled` 对 capture / capture="user" /
        //  capture="environment" 统一返回 true(拿不到镜头偏好,需走 config 兜底)。
        if (cameraCaptureBehavior != CameraCaptureBehavior.DISABLED && params.isCaptureEnabled) {
            return launchCameraForCaptureAttribute(params, cameraCaptureBehavior)
        }

        // ── 优先级 2:老录制模式 MIME 兜底(主流 H5 已切 TRTC,仅降级路径命中) ──
        val needsLegacyCapture = legacyFaceCaptureMimeTypes.isNotEmpty() &&
            matchesAny(params.acceptTypes, legacyFaceCaptureMimeTypes)
        if (needsLegacyCapture) {
            return launchLegacyVideoCapture(params)
        }

        // ── 优先级 3:默认走系统文件选择器 ──
        return launchDefault(params)
    }

    private fun launchDefault(
        params: WebChromeClient.FileChooserParams
    ): Boolean {
        return try {
            val intent = params.createIntent()
            // createIntent() 生成的 Intent 可能包含非标准 MIME(如 image/jpg),
            // Android 系统文件选择器/Photo Picker 对 MIME 做严格匹配,
            // 非标准 MIME 会导致过滤不到文件(显示 "No items")。
            // 这里对 Intent 的 type 和 EXTRA_MIME_TYPES 进行规范化修正。
            normalizeIntentMimeTypes(intent, params)
            // createIntent() 不会设置 EXTRA_ALLOW_MULTIPLE,需手动补上
            if (params.mode == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE) {
                intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
                // Android 13+(API 33)Photo Picker:部分 OEM(三星 One UI 6.0)会忽略
                // EXTRA_ALLOW_MULTIPLE,需额外设置 EXTRA_PICK_IMAGES_MAX 来确保多选生效
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    try {
                        val maxLimit = MediaStore.getPickImagesMaxLimit()
                        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxLimit)
                    } catch (_: Exception) {
                        // getPickImagesMaxLimit() 在部分 OEM 上可能不可用,忽略
                    }
                }
            }
            // 使用 createChooser() 包装:
            // 1. 华为 EMUI 8.x 等 OEM 文件管理器直接接收 ACTION_GET_CONTENT 时
            //    会忽略 EXTRA_ALLOW_MULTIPLE,导致多选失效;createChooser 可规避
            // 2. 弹出选择器让用户选择支持多选的应用(如 Google Files)
            // 3. 高版本 Android 若只有一个匹配应用会自动跳过选择器,无副作用
            launcher.launch(Intent.createChooser(intent, null))
            true
        } catch (e: Exception) {
            // createIntent() 失败(非标准 MIME 等):手动构造 ACTION_GET_CONTENT 兜底
            Log.w(TAG, "createIntent() failed, fallback to manual ACTION_GET_CONTENT", e)
            return try {
                val allMimes = extractAllMimes(params.acceptTypes)
                val fallbackIntent = Intent(Intent.ACTION_GET_CONTENT).apply {
                    addCategory(Intent.CATEGORY_OPENABLE)
                    if (allMimes.size > 1) {
                        // 多个 MIME 类型:需要通过 EXTRA_MIME_TYPES 传入完整列表
                        if (hasMultipleTopLevelTypes(allMimes)) {
                            // 多种顶级类型(如 image/ + application/):必须用 */* 通配
                            type = "*/*"
                        } else {
                            // 单一顶级类型但多个子类型(如 image/png + image/jpeg):
                            // 用顶级通配让 EXTRA_MIME_TYPES 生效
                            val topLevel = allMimes.first().split("/").firstOrNull() ?: "*"
                            type = "$topLevel/*"
                        }
                        putExtra(Intent.EXTRA_MIME_TYPES, allMimes.toTypedArray())
                    } else {
                        // 单个 MIME 类型:直接设为 Intent.type
                        type = extractFirstMime(params.acceptTypes) ?: "*/*"
                    }
                    if (params.mode == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE) {
                        putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
                    }
                }
                launcher.launch(Intent.createChooser(fallbackIntent, null))
                true
            } catch (fallbackEx: Exception) {
                Log.w(TAG, "Fallback file chooser also failed", fallbackEx)
                // 彻底失败,释放 H5 input
                state.pathCallback?.onReceiveValue(null)
                state.pathCallback = null
                state.pending = null
                false
            }
        }
    }

    /**
     * 从 acceptTypes 中提取第一个有效的 MIME 类型,用于 fallback Intent。
     * 返回 null 表示无有效 MIME,调用方应使用通配符。
     * 会自动将非标准 MIME(如 image/jpg)规范化为系统可识别的标准 MIME(如 image/jpeg)。
     */
    private fun extractFirstMime(acceptTypes: Array<String>?): String? {
        if (acceptTypes.isNullOrEmpty()) return null
        for (raw in acceptTypes) {
            raw.split(",").forEach { piece ->
                val norm = piece.trim()
                if (norm.isNotEmpty() && norm.contains("/")) return normalizeMime(norm)
            }
        }
        return null
    }

    /**
     * 从 acceptTypes 中提取所有有效的 MIME 类型列表。
     *
     * 用于 Android 13+ Photo Picker 场景:当 H5 传入多种顶级类型(如 `image/`+`video/` 通配)时,
     * 需要通过 [Intent.EXTRA_MIME_TYPES] 传入完整列表,否则 Photo Picker 只能选单一类型。
     * 会自动将非标准 MIME 规范化,并去重(如 image/jpg 和 image/jpeg 规范化后合并为一条)。
     */
    private fun extractAllMimes(acceptTypes: Array<String>?): List<String> {
        if (acceptTypes.isNullOrEmpty()) return emptyList()
        val seen = linkedSetOf<String>()
        for (raw in acceptTypes) {
            raw.split(",").forEach { piece ->
                val norm = piece.trim()
                if (norm.isNotEmpty() && norm.contains("/")) {
                    seen.add(normalizeMime(norm))
                }
            }
        }
        return seen.toList()
    }

    /**
     * 判断 MIME 列表是否包含多种顶级类型(如同时含 image/ 和 video/)。
     */
    private fun hasMultipleTopLevelTypes(mimes: List<String>): Boolean {
        val topLevels = mimes.mapNotNull { it.split("/").firstOrNull()?.lowercase() }.toSet()
        return topLevels.size > 1
    }

    // 常见非标准 MIME → IANA 标准 MIME 映射。
    // H5 开发者常用 image/jpg 等非标准写法,浏览器能容忍但 Android Intent 系统做严格匹配,
    // 导致系统文件选择器/Photo Picker 过滤不到文件。此映射在 SDK 层做兼容,无需 H5 侧修改。
    private val mimeNormalizationMap: Map<String, String> = mapOf(
        "image/jpg" to "image/jpeg",
        "image/pjpeg" to "image/jpeg",
        "image/x-png" to "image/png",
        "audio/mp3" to "audio/mpeg"
    )

    // 将可能的非标准 MIME 类型规范化为 Android 系统可识别的标准 MIME。
    // 如果不在映射表中则原样返回,不做额外修改。
    private fun normalizeMime(mime: String): String {
        return mimeNormalizationMap[mime.lowercase()] ?: mime
    }

    // 对 createIntent() 生成的 Intent 进行 MIME 规范化修正。
    // createIntent() 内部使用 H5 原始 accept 值构造 Intent,如果 accept 含非标准 MIME(如 image/jpg),
    // Intent.type 和 EXTRA_MIME_TYPES 都会是非标准值,导致系统文件选择器过滤不到文件。
    // 此方法在 createIntent() 成功后修正这些值,确保系统能正确识别。
    private fun normalizeIntentMimeTypes(intent: Intent, params: WebChromeClient.FileChooserParams) {
        // 修正 Intent.type
        val originalType = intent.type
        if (!originalType.isNullOrEmpty() && originalType.contains("/")) {
            val normalized = normalizeMime(originalType)
            if (normalized != originalType) {
                intent.type = normalized
            }
        }

        // 修正 EXTRA_MIME_TYPES(如果存在)
        val extraMimes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES)
        if (extraMimes != null && extraMimes.isNotEmpty()) {
            val normalizedMimes = extraMimes.map { normalizeMime(it) }.distinct().toTypedArray()
            intent.putExtra(Intent.EXTRA_MIME_TYPES, normalizedMimes)
        }

        // 额外处理:如果 createIntent() 只设了单一 type 但实际 H5 指定了多个 MIME,
        // 需要用规范化后的完整列表设置 EXTRA_MIME_TYPES,否则系统只按单一 type 过滤。
        // 例如 accept="image/png,image/jpg,image/jpeg" 时,createIntent() 可能只设
        // type="image/png",导致只能看到 PNG 而看不到 JPEG。
        if (extraMimes == null) {
            val allMimes = extractAllMimes(params.acceptTypes)
            if (allMimes.size > 1) {
                if (hasMultipleTopLevelTypes(allMimes)) {
                    // 多种顶级类型(如 image/ + application/):必须用 */* 通配
                    intent.type = "*/*"
                } else {
                    // 单一顶级类型但多个子类型(如 image/png + image/jpeg):
                    // 用顶级通配(如 image/*)让 EXTRA_MIME_TYPES 生效
                    val topLevel = allMimes.first().split("/").firstOrNull() ?: "*"
                    intent.type = "$topLevel/*"
                }
                intent.putExtra(Intent.EXTRA_MIME_TYPES, allMimes.toTypedArray())
            }
        }
    }

    /**
     * 命中 H5 `<input capture>` 属性时拉系统相机。
     *
     * - `accept` 命中 `image/` 前缀 → [MediaStore.ACTION_IMAGE_CAPTURE]
     * - `accept` 命中 `video/` 前缀或留空 → [MediaStore.ACTION_VIDEO_CAPTURE]
     *
     * **权限**:Android 6+ 需要 `CAMERA` 运行时权限,SDK 在启动相机前自动申请;
     * 用户拒绝则 fallback 到普通文件选择器。
     *
     * 镜头偏好通过 [MediaStore.EXTRA_USE_FRONT_CAMERA] 等私有 extra 暗示;不同 OEM 行为不一致,
     * 属于尽力而为,不强保命中。
     */
    private fun launchCameraForCaptureAttribute(
        params: WebChromeClient.FileChooserParams,
        behavior: CameraCaptureBehavior
    ): Boolean {
        // Android 6+ 动态申请 CAMERA 权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            val ctx = contextProvider() ?: return launchDefault(params)
            val hasCameraPermission = ContextCompat.checkSelfPermission(
                ctx, Manifest.permission.CAMERA
            ) == PackageManager.PERMISSION_GRANTED

            if (!hasCameraPermission) {
                val activity = activityProvider()
                val requester = permissionRequester
                if (activity == null || requester == null) {
                    Log.w(TAG, "Activity/PermissionRequester unavailable, fallback to file chooser")
                    return launchDefault(params)
                }
                Log.d(TAG, "requesting CAMERA permission for <input capture>")
                requester.request(activity, arrayOf(Manifest.permission.CAMERA)) { grants ->
                    if (grants[Manifest.permission.CAMERA] == true) {
                        Log.d(TAG, "CAMERA granted, launching camera for capture attribute")
                        doLaunchCameraForCapture(params, behavior)
                    } else {
                        Log.d(TAG, "CAMERA denied, fallback to file chooser")
                        launchDefault(params)
                    }
                }
                return true // 异步等待权限结果,已持有 callback
            }
        }

        return doLaunchCameraForCapture(params, behavior)
    }

    /**
     * 实际启动相机(权限已确认)。
     */
    private fun doLaunchCameraForCapture(
        params: WebChromeClient.FileChooserParams,
        behavior: CameraCaptureBehavior
    ): Boolean {
        val wantsImage = matchesPrefix(params.acceptTypes, "image/")
        val wantsVideo = matchesPrefix(params.acceptTypes, "video/")

        val intent = if (wantsImage && !wantsVideo) {
            Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        } else {
            // 同时含 image+video,或只含 video,或都不含 → 优先视频(电子签自拍活体主场景)
            Intent(MediaStore.ACTION_VIDEO_CAPTURE).apply {
                putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1)
                // capture 属性场景不强加时长限制,交给 H5 自定 UI;老协议兜底才加 10s 限时
            }
        }
        applyCameraLensHint(intent, behavior)

        // 为相机 Intent 提供 EXTRA_OUTPUT Uri,避免部分设备/模拟器因无输出路径而 crash
        val outputUri = createCaptureOutputUri(
            isImage = (wantsImage && !wantsVideo)
        )
        if (outputUri != null) {
            intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri)
            // 必须显式授予写权限,否则三星/OPPO/Vivo Android 11 相机 App 会抛 SecurityException
            intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
            state.cameraOutputUri = outputUri
        }

        return try {
            state.isCameraCapture = true
            launcher.launch(intent)
            true
        } catch (e: Exception) {
            // 设备无相机 App / 系统拒绝:回退到普通文件选择,而非直接失败
            state.isCameraCapture = false
            state.cameraOutputUri = null
            launchDefault(params)
        }
    }

    /**
     * 人脸核身老录制模式 MIME 兜底(`video/kyc` / `video/faceid` 等)。
     *
     * 不读 [CameraCaptureBehavior]:老协议固定走前置相机录像,与人脸核身 SDK 行为一致。
     *
     * **权限**:Android 6+ 需要 `CAMERA` 运行时权限,SDK 在启动相机前自动申请;
     * 用户拒绝则 fallback 到普通文件选择器。
     */
    private fun launchLegacyVideoCapture(
        params: WebChromeClient.FileChooserParams
    ): Boolean {
        // Android 6+ 动态申请 CAMERA 权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            val ctx = contextProvider() ?: return launchDefault(params)
            val hasCameraPermission = ContextCompat.checkSelfPermission(
                ctx, Manifest.permission.CAMERA
            ) == PackageManager.PERMISSION_GRANTED

            if (!hasCameraPermission) {
                val activity = activityProvider()
                val requester = permissionRequester
                if (activity == null || requester == null) {
                    Log.w(TAG, "Activity/PermissionRequester unavailable, fallback to file chooser")
                    return launchDefault(params)
                }
                Log.d(TAG, "requesting CAMERA permission for legacy video capture")
                requester.request(activity, arrayOf(Manifest.permission.CAMERA)) { grants ->
                    if (grants[Manifest.permission.CAMERA] == true) {
                        Log.d(TAG, "CAMERA granted, launching legacy video capture")
                        doLaunchLegacyVideoCapture(params)
                    } else {
                        Log.d(TAG, "CAMERA denied, fallback to file chooser")
                        launchDefault(params)
                    }
                }
                return true // 异步等待权限结果,已持有 callback
            }
        }

        return doLaunchLegacyVideoCapture(params)
    }

    /**
     * 实际启动老录制模式相机(权限已确认)。
     */
    private fun doLaunchLegacyVideoCapture(
        params: WebChromeClient.FileChooserParams
    ): Boolean {
        val captureIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE).apply {
            // 高清录制(0=低,1=高);老人脸核身需要清晰画面
            putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1)
            // 兜底超时:10s,与人脸核身老协议一致
            putExtra(MediaStore.EXTRA_DURATION_LIMIT, 10)
        }
        // 老协议默认前置(人脸核身自拍活体)
        applyCameraLensHint(captureIntent, CameraCaptureBehavior.PREFER_FRONT)

        // 为录像 Intent 提供 EXTRA_OUTPUT Uri
        val outputUri = createCaptureOutputUri(isImage = false)
        if (outputUri != null) {
            captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri)
            // 必须显式授予写权限,否则部分 OEM Android 11 相机 App 会抛 SecurityException
            captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
            state.cameraOutputUri = outputUri
        }

        return try {
            state.isCameraCapture = true
            launcher.launch(captureIntent)
            true
        } catch (e: Exception) {
            // 设备无相机 App / 系统拒绝:回退到普通文件选择,而非直接失败
            state.isCameraCapture = false
            state.cameraOutputUri = null
            launchDefault(params)
        }
    }

    /**
     * 创建相机拍摄输出的临时文件 Uri。
     *
     * 通过 FileProvider 生成 `content://` Uri,解决:
     * 1. Android 7.0+(API 24)禁止 `file://` Uri 跨进程传递
     * 2. 部分设备/模拟器相机 App 在没有 EXTRA_OUTPUT 时 crash
     * 3. 没有 EXTRA_OUTPUT 时只能拿到缩略图 Bitmap,无法获取完整照片
     *
     * @return content:// Uri,创建失败返回 null(降级为不带 EXTRA_OUTPUT 启动)
     */
    private fun createCaptureOutputUri(isImage: Boolean): Uri? {
        val ctx = contextProvider() ?: return null
        return try {
            val cacheDir = File(ctx.cacheDir, CAPTURE_DIR_NAME).apply { mkdirs() }
            val suffix = if (isImage) ".jpg" else ".mp4"
            val prefix = if (isImage) "IMG_" else "VID_"
            val tempFile = File.createTempFile(prefix, suffix, cacheDir)
            // 记录本次创建的临时文件,onDestroy 时精确清理
            state.capturedFiles.add(tempFile)
            FileProvider.getUriForFile(
                ctx,
                "${ctx.packageName}.qian.fileprovider",
                tempFile
            )
        } catch (e: Exception) {
            Log.w(TAG, "Failed to create capture output Uri, camera will launch without EXTRA_OUTPUT", e)
            null
        }
    }

    /**
     * 清理 [State.capturedFiles] 中记录的本次 Activity 生命周期内创建的临时文件。
     *
     * **调用时机**:Activity onDestroy。
     *
     * **安全性**:即使 H5 正在通过 content:// Uri 上传文件,Activity 销毁意味着 WebView
     * 也会被销毁,上传本身已中断,删除文件不会造成数据丢失。
     * 此外,Linux fd 语义保证:已打开的 fd 在文件被 unlink 后仍可继续读取。
     */
    private fun cleanCapturedFiles() {
        try {
            state.capturedFiles.forEach { file ->
                if (file.exists()) {
                    file.delete()
                }
            }
            state.capturedFiles.clear()
        } catch (e: Exception) {
            Log.w(TAG, "Failed to clean captured files on destroy", e)
        }
    }

    /**
     * 给系统相机 Intent 暗示镜头偏好。
     *
     * 注意:`EXTRA_USE_FRONT_CAMERA` 不是 Android 公开 API,部分 OEM(三星 / 小米 / 华为老内核)
     * 支持但不保证;`EXTRA_LENS_FACING` 是 Android 12+(API 31+)的 Camera2 公开 extra,
     * 我们两个都加上,兼容性最大化。
     */
    private fun applyCameraLensHint(intent: Intent, behavior: CameraCaptureBehavior) {
        when (behavior) {
            CameraCaptureBehavior.PREFER_FRONT -> {
                intent.putExtra("android.intent.extras.CAMERA_FACING", 1) // 老 OEM 私有 extra
                intent.putExtra("android.intent.extras.LENS_FACING_FRONT", 1) // Android 12+ Camera2
                intent.putExtra("android.intent.extra.USE_FRONT_CAMERA", true)
                // 小米 MIUI 12-14 使用不同的 extra key
                intent.putExtra("android.intent.extras.CAMERA_FACING_FRONT", 1)
            }
            CameraCaptureBehavior.PREFER_BACK -> {
                intent.putExtra("android.intent.extras.CAMERA_FACING", 0)
                intent.putExtra("android.intent.extras.LENS_FACING_BACK", 1)
                intent.putExtra("android.intent.extra.USE_FRONT_CAMERA", false)
                // 小米 MIUI 12-14 使用不同的 extra key
                intent.putExtra("android.intent.extras.CAMERA_FACING_BACK", 1)
            }
            CameraCaptureBehavior.SYSTEM_DEFAULT,
            CameraCaptureBehavior.DISABLED -> Unit
        }
    }

    public companion object {

        private const val TAG = "FileChooserLauncher"

        /**
         * 临时拍摄文件存放目录名(位于 cacheDir 下)。
         */
        private const val CAPTURE_DIR_NAME = "qian_capture"

        /**
         * 孤儿文件最大保留时长:1 小时。
         *
         * 用于 register 时兜底清理:进程被系统杀死(OOM killer)时 onDestroy 不会被调用,
         * 残留的临时文件通过此阈值在下次启动时清理。
         * 1 小时足够覆盖:拍照 → 填写表单 → 提交上传 的完整流程。
         */
        private const val ORPHAN_FILE_MAX_AGE_MS = 60 * 60 * 1000L

        /**
         * 清理 [dir] 下超过 [maxAgeMs] 毫秒的孤儿临时文件。
         *
         * **调用时机**:register(Activity onCreate)时兜底清理。
         *
         * **设计考量**:
         * - 正常流程:onDestroy 精确删除本次 capturedFiles 列表中的文件
         * - 异常流程(进程被杀):onDestroy 未执行,残留文件通过此方法在下次启动时清理
         * - 使用时间阈值(1 小时)而非全部清空,避免多 Activity 场景误删正在使用的文件
         */
        private fun cleanOrphanCaptureFiles(ctx: Context) {
            try {
                val dir = File(ctx.cacheDir, CAPTURE_DIR_NAME)
                if (!dir.exists()) return
                val now = System.currentTimeMillis()
                dir.listFiles()?.forEach { file ->
                    if (file.isFile && (now - file.lastModified()) > ORPHAN_FILE_MAX_AGE_MS) {
                        file.delete()
                    }
                }
            } catch (e: Exception) {
                // 清理失败不影响主流程
                Log.w(TAG, "Failed to clean orphan capture files", e)
            }
        }

        /**
         * 校验 [uri] 对应的文件是否有实际内容(size > 0)。
         *
         * **背景**:华为 EMUI 10/11 部分机型相机 App 在拍照后,既不在 result.data 中返回 Uri,
         * 也不往 EXTRA_OUTPUT 指定的 Uri 写入数据(文件大小为 0)。
         * 如果不校验直接返回空文件 Uri,H5 端上传会静默失败。
         */
        private fun isUriContentValid(ctx: Context, uri: Uri): Boolean {
            return try {
                ctx.contentResolver.openInputStream(uri)?.use { stream ->
                    stream.read() != -1 // 至少能读到 1 字节
                } ?: false
            } catch (e: Exception) {
                false
            }
        }

        /**
         * 默认命中老录制模式的 H5 `accept` MIME 类型。
         *
         * - `video/kyc`:人脸核身早期约定
         * - `video/faceid`:部分老 H5 仍在使用
         *
         * **当前(2025+)电子签 / 人脸核身主流路径已切换 TRTC(WebRTC)**;本兜底仅服务于:
         * - 设备不支持 WebRTC(理论 `minSdk 24` 已规避,但部分国产 ROM WebView 内核异常)
         * - 用户多次拒绝相机权限导致 H5 端降级
         *
         * 客户可在 [com.tencent.qian.webview.model.TencentQianWebViewConfig.legacyFaceCaptureMimeTypes]
         * 中自定义此列表,或传 `emptyList()` 关闭兜底。
         */
        @JvmField
        public val DEFAULT_LEGACY_FACE_CAPTURE_MIME_TYPES: List<String> = listOf(
            "video/kyc",
            "video/faceid"
        )

        /**
         * 在 [activity] 的 `onCreate()` 中注册,返回可复用的 FileChooserLauncher。
         *
         * @param permissionRequester 用于动态申请 CAMERA 权限(拍照/录像场景);
         *                            传 null 时拍照分支不申请权限,直接尝试启动相机(可能被系统拒绝后 fallback)。
         * @param activityProvider 提供当前 Activity 引用,用于权限申请弹窗的宿主。
         */
        @JvmStatic
        @JvmOverloads
        public fun register(
            activity: ComponentActivity,
            permissionRequester: RuntimePermissionRequester? = null,
            activityProvider: () -> Activity? = { activity }
        ): FileChooserLauncher {
            val state = State()
            val contextProvider: () -> Context? = { activity }

            // 兜底清理:清除上次进程被杀后可能残留的孤儿临时文件
            cleanOrphanCaptureFiles(activity)
            val launcher = activity.registerForActivityResult(
                ActivityResultContracts.StartActivityForResult()
            ) { result ->
                val cb = state.pathCallback ?: return@registerForActivityResult
                val isCameraCapture = state.isCameraCapture
                val cameraOutputUri = state.cameraOutputUri
                state.pathCallback = null
                state.pending = null
                state.isCameraCapture = false
                state.cameraOutputUri = null

                if (result.resultCode != android.app.Activity.RESULT_OK) {
                    cb.onReceiveValue(null) // 释放 input
                    return@registerForActivityResult
                }

                val uris: Array<Uri>? = if (isCameraCapture) {
                    // ACTION_IMAGE_CAPTURE / ACTION_VIDEO_CAPTURE 不走 parseResult;
                    // 录制/拍摄结果在 result.data?.data,部分 OEM 放在 clipData 中,
                    // 还有部分 OEM(华为 EMUI 等)通过 EXTRA_OUTPUT 指定的 Uri 返回。
                    val ctx: Context? = activity
                    val uri = result.data?.data
                        ?: result.data?.clipData?.let { clip ->
                            if (clip.itemCount > 0) clip.getItemAt(0)?.uri else null
                        }
                        ?: cameraOutputUri?.takeIf { outputUri ->
                            // 华为 EMUI 10/11 兜底:校验 EXTRA_OUTPUT Uri 是否有实际内容,
                            // 避免返回空文件 Uri 导致 H5 上传静默失败
                            ctx != null && isUriContentValid(ctx, outputUri)
                        }
                    if (uri != null) arrayOf(uri) else null // null 等同取消,避免 H5 input 状态异常
                } else {
                    if (result.data == null) {
                        cb.onReceiveValue(null)
                        return@registerForActivityResult
                    }
                    // 多选时结果在 clipData 中,parseResult 只能解析单文件
                    val data = result.data!!
                    val clipData = data.clipData
                    if (clipData != null && clipData.itemCount > 0) {
                        // 多选:从 clipData 中提取所有 URI
                        Array(clipData.itemCount) { i -> clipData.getItemAt(i).uri }
                    } else {
                        // 单选:走标准 parseResult
                        WebChromeClient.FileChooserParams
                            .parseResult(result.resultCode, data) ?: emptyArray()
                    }
                }

                cb.onReceiveValue(uris)
            }

            val fileChooserLauncher = FileChooserLauncher(
                launcher, state, contextProvider, permissionRequester, activityProvider
            )

            // 注册生命周期观察:Activity 销毁时释放残留 callback 并清理本次临时文件
            activity.lifecycle.addObserver(object : DefaultLifecycleObserver {
                override fun onDestroy(owner: LifecycleOwner) {
                    state.pathCallback?.onReceiveValue(null)
                    state.pathCallback = null
                    state.pending = null
                    state.isCameraCapture = false
                    state.cameraOutputUri = null
                    // 精确清理本次生命周期内创建的所有临时拍摄文件
                    fileChooserLauncher.cleanCapturedFiles()
                }
            })

            return fileChooserLauncher
        }

        /**
         * 检查 H5 传入的 `acceptTypes` 是否命中给定的兜底 MIME 列表(大小写不敏感、忽略空白)。
         *
         * 可见性:`internal` 越包可见仅服务于单测,客户不应调用。
         */
        @JvmStatic
        internal fun matchesAny(acceptTypes: Array<String>?, target: List<String>): Boolean {
            if (acceptTypes.isNullOrEmpty() || target.isEmpty()) return false
            val targetSet = target.asSequence()
                .map { it.trim().lowercase() }
                .filter { it.isNotEmpty() }
                .toSet()
            if (targetSet.isEmpty()) return false
            for (raw in acceptTypes) {
                // 一个 acceptType entry 可能形如 "video/kyc, image/*",这里再按逗号拆
                raw.split(",").forEach { piece ->
                    val norm = piece.trim().lowercase()
                    if (norm.isNotEmpty() && norm in targetSet) return true
                }
            }
            return false
        }

        /**
         * 检查 `acceptTypes` 是否包含某个前缀(如 `image/` / `video/`),大小写不敏感、逗号容错。
         *
         * 可见性:`internal` 越包可见仅服务于单测,客户不应调用。
         */
        @JvmStatic
        internal fun matchesPrefix(acceptTypes: Array<String>?, prefix: String): Boolean {
            if (acceptTypes.isNullOrEmpty()) return false
            val lowerPrefix = prefix.lowercase()
            for (raw in acceptTypes) {
                raw.split(",").forEach { piece ->
                    val norm = piece.trim().lowercase()
                    if (norm.startsWith(lowerPrefix)) return true
                }
            }
            return false
        }
    }
}
