package com.dronex.rec.recording

import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CaptureRequest
import android.os.Build
import android.util.Log
import androidx.camera.camera2.interop.Camera2CameraControl
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.camera2.interop.CaptureRequestOptions
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
import androidx.camera.core.Camera
import androidx.camera.core.CameraControl
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.camera.video.VideoRecordEvent
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import java.io.File
import java.util.concurrent.Executor
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import kotlin.math.atan
import kotlin.math.round

class RecordingCameraController(
        private val appContext: Context,
) {
    private val mainExecutor: Executor = ContextCompat.getMainExecutor(appContext)
    private val ioExecutor = Executors.newSingleThreadExecutor()

    private var cameraProvider: ProcessCameraProvider? = null
    private var preview: Preview? = null
    private var videoCapture: VideoCapture<Recorder>? = null
    private var camera: Camera? = null
    private var mainCameraId: String? = null
    private var ultraWideCameraId: String? = null
    private var ultraWideZoomRatio: Float = DEFAULT_ULTRA_WIDE_ZOOM_RATIO
    private var currentLensMode: LensMode = LensMode.MAIN
    private var activeRecording: Recording? = null
    private var boundLifecycleOwner: LifecycleOwner? = null
    private var currentZoomRatio: Float = 1f
    private var activeWithAudio: Boolean = true
    private var activeDisplayName: String? = null
    private var currentSegmentFile: File? = null
    private val segmentFiles = mutableListOf<File>()
    private var nextSegmentIndex: Int = 1
    private var isSwitchingLens: Boolean = false

    var status: RecordingStatus = RecordingStatus(RecordingState.IDLE)
        private set

    var statusListener: ((RecordingStatus) -> Unit)? = null

    private var recordingStartedAt: Long = 0L
    private var latestOutputPath: String? = null
    private var latestSegmentOutputPaths: List<String> = emptyList()
    private var pendingStopCallback: ((String?) -> Unit)? = null
    private var pendingSegmentSwitch: PendingSegmentSwitch? = null

    fun bindPreview(
            lifecycleOwner: LifecycleOwner,
            previewView: PreviewView,
            onReady: (Boolean) -> Unit,
    ) {
        val future = ProcessCameraProvider.getInstance(appContext)
        future.addListener(
                {
                    try {
                        val provider = future.get()
                        cameraProvider = provider
                        boundLifecycleOwner = lifecycleOwner

                        preview =
                                Preview.Builder().build().also {
                                    it.surfaceProvider = previewView.surfaceProvider
                                }

                        val recorder =
                                Recorder.Builder()
                                        .setQualitySelector(QualitySelector.from(Quality.HD))
                                        .build()
                        videoCapture = VideoCapture.withOutput(recorder)

                        discoverBackCameras(provider)
                        bindUseCases(provider, lifecycleOwner, selectorForCurrentLensMode())
                        applyCurrentZoom()

                        updateStatus(RecordingStatus(RecordingState.PREVIEWING))
                        onReady(true)
                    } catch (error: Exception) {
                        Log.e(TAG, "bindPreview failed", error)
                        updateStatus(
                                RecordingStatus(
                                        RecordingState.ERROR,
                                        message = error.message,
                                ),
                        )
                        onReady(false)
                    }
                },
                mainExecutor,
        )
    }

    fun rebindForRecording(
            lifecycleOwner: LifecycleOwner,
            previewView: PreviewView,
            onReady: (Boolean) -> Unit,
    ) {
        val provider = cameraProvider
        if (provider == null) {
            bindPreview(lifecycleOwner, previewView, onReady)
            return
        }

        if (
                boundLifecycleOwner === lifecycleOwner &&
                        preview != null &&
                        videoCapture != null
        ) {
            onReady(true)
            return
        }

        try {
            boundLifecycleOwner = lifecycleOwner
            bindUseCases(provider, lifecycleOwner, selectorForCurrentLensMode())
            applyCurrentZoom()
            onReady(true)
        } catch (error: Exception) {
            Log.e(TAG, "rebindForRecording failed", error)
            onReady(false)
        }
    }

    fun startRecording(
            withAudio: Boolean,
            displayName: String?,
            onStarted: (Boolean, String?) -> Unit,
    ) {
        val capture = videoCapture
        if (capture == null || boundLifecycleOwner == null) {
            onStarted(false, "Camera not ready")
            return
        }

        if (activeRecording != null || isSwitchingLens) {
            onStarted(false, "Already recording")
            return
        }

        activeWithAudio = withAudio
        activeDisplayName = displayName
        segmentFiles.clear()
        nextSegmentIndex = 1
        latestOutputPath = null
        latestSegmentOutputPaths = emptyList()

        recordingStartedAt = System.currentTimeMillis()
        updateStatus(
                RecordingStatus(
                        RecordingState.RECORDING,
                        outputPath = latestOutputPath,
                ),
        )

        startNewSegment(updateStatusOnStart = false, onStarted = onStarted)
    }

    private fun startNewSegment(
            updateStatusOnStart: Boolean,
            onStarted: (Boolean, String?) -> Unit,
    ) {
        val capture = videoCapture
        if (capture == null || boundLifecycleOwner == null) {
            onStarted(false, "Camera not ready")
            return
        }
        if (activeRecording != null) {
            onStarted(false, "Already recording")
            return
        }

        val segmentFile =
                RecordingOutputFactory.createSegmentFile(
                        appContext,
                        activeDisplayName,
                        nextSegmentIndex++,
                )
        currentSegmentFile = segmentFile
        val outputOptions = RecordingOutputFactory.buildSegmentOutputOptions(segmentFile)

        val pending =
                capture.output.prepareRecording(appContext, outputOptions).apply {
                    if (activeWithAudio) {
                        val granted =
                                ContextCompat.checkSelfPermission(
                                        appContext,
                                        android.Manifest.permission.RECORD_AUDIO,
                                ) == android.content.pm.PackageManager.PERMISSION_GRANTED
                        if (granted) {
                            withAudioEnabled()
                        }
                    }
                }

        if (updateStatusOnStart) {
            updateStatus(
                    RecordingStatus(
                            RecordingState.RECORDING,
                            outputPath = latestOutputPath,
                            elapsedMillis = elapsedMillis(),
                    ),
            )
        }

        activeRecording =
                pending.start(mainExecutor) { event ->
                    when (event) {
                        is VideoRecordEvent.Start -> Unit
                        is VideoRecordEvent.Finalize -> handleFinalize(event, segmentFile)
                    }
                }

        onStarted(true, segmentFile.absolutePath)
    }

    fun stopRecording(onStopped: (String?) -> Unit) {
        val recording = activeRecording
        if (recording == null) {
            onStopped(latestOutputPath)
            return
        }
        if (isSwitchingLens) {
            onStopped(null)
            return
        }

        pendingStopCallback = onStopped
        val elapsed = elapsedMillis()
        updateStatus(
                RecordingStatus(
                        RecordingState.STOPPING,
                        outputPath = latestOutputPath,
                        elapsedMillis = elapsed,
                ),
        )

        recording.stop()
        activeRecording = null
    }

    private fun handleFinalize(
            event: VideoRecordEvent.Finalize,
            segmentFile: File,
    ) {
        activeRecording = null
        if (!event.hasError() && segmentFile.exists() && segmentFile.length() > 0L) {
            segmentFiles.add(segmentFile)
        }

        val segmentSwitch = pendingSegmentSwitch
        if (segmentSwitch != null) {
            pendingSegmentSwitch = null
            if (event.hasError()) {
                isSwitchingLens = false
                val message = event.cause?.message ?: "Recording segment failed"
                updateStatus(RecordingStatus(RecordingState.ERROR, message = message))
                segmentSwitch.onComplete(false, zoomCapabilitiesMap(), message)
                return
            }
            continueSegmentSwitch(segmentSwitch)
            return
        }

        val stopCallback = pendingStopCallback
        pendingStopCallback = null
        if (stopCallback == null) {
            return
        }

        if (event.hasError()) {
            val message = event.cause?.message ?: "Recording failed"
            updateStatus(RecordingStatus(RecordingState.ERROR, message = message))
            stopCallback.invoke(null)
            return
        }

        finalizeRecordingOutput(stopCallback)
    }

    private fun finalizeRecordingOutput(stopCallback: (String?) -> Unit) {
        val segments = segmentFiles.toList()
        val displayName = activeDisplayName
        ioExecutor.execute {
            try {
                val mergedFile = RecordingOutputFactory.createMergeFile(appContext, displayName)
                RecordingSegmentMuxer.mergeOrCopy(segments, mergedFile)
                val outputUri =
                        RecordingOutputFactory.publishToMediaStore(
                                appContext,
                                displayName,
                                mergedFile,
                        )
                latestOutputPath = outputUri
                latestSegmentOutputPaths = emptyList()
                cleanupSegmentFiles(segments + mergedFile)
                mainExecutor.execute {
                    val elapsed = System.currentTimeMillis() - recordingStartedAt
                    updateStatus(
                            RecordingStatus(
                                    RecordingState.PREVIEWING,
                                    outputPath = latestOutputPath,
                                    elapsedMillis = elapsed,
                            ),
                    )
                    stopCallback.invoke(latestOutputPath)
                }
            } catch (error: Exception) {
                Log.e(TAG, "finalizeRecordingOutput failed", error)
                val publishedParts = publishSegmentsAfterMergeFailure(segments, displayName)
                latestOutputPath = publishedParts.firstOrNull()
                latestSegmentOutputPaths = publishedParts
                mainExecutor.execute {
                    updateStatus(
                            RecordingStatus(
                                    RecordingState.ERROR,
                                    outputPath = latestOutputPath,
                                    message = error.message ?: "保存到文件夹失败",
                            ),
                    )
                    stopCallback.invoke(latestOutputPath)
                }
            }
        }
    }

    private fun publishSegmentsAfterMergeFailure(
            segments: List<File>,
            displayName: String?,
    ): List<String> {
        return segments.mapIndexedNotNull { index, segment ->
            try {
                RecordingOutputFactory.publishPartToMediaStore(
                        appContext,
                        displayName,
                        segment,
                        index + 1,
                )
            } catch (error: Exception) {
                Log.e(TAG, "publish segment after merge failure failed", error)
                null
            }
        }
    }

    private fun cleanupSegmentFiles(files: List<File>) {
        files.forEach { file ->
            try {
                file.delete()
            } catch (_: Exception) {
            }
        }
    }

    fun zoomCapabilitiesMap(): Map<String, Any> {
        val zoomState = camera?.cameraInfo?.zoomState?.value
        val cameraXMin = zoomState?.minZoomRatio ?: 1f
        val cameraXMax = zoomState?.maxZoomRatio ?: 3f
        val camera2Range = mainCameraZoomRatioRange()
        val logicalMin = camera2Range?.lower?.let { minOf(cameraXMin, it) } ?: cameraXMin
        val logicalMax = camera2Range?.upper?.let { maxOf(cameraXMax, it) } ?: cameraXMax
        val minZoom =
                if (hasUltraWideCamera()) {
                    minOf(ultraWideZoomRatio, logicalMin)
                } else {
                    logicalMin
                }
        val maxZoom = logicalMax
        val zoom =
                if (currentLensMode == LensMode.ULTRA_WIDE) {
                    ultraWideZoomRatio
                } else {
                    currentZoomRatio.coerceIn(minZoom, maxZoom)
                }
        currentZoomRatio = zoom
        Log.d(
                TAG,
                "zoomCapabilities hasUltraWide=${hasUltraWideCamera()} cameraXMin=$cameraXMin " +
                        "cameraXMax=$cameraXMax camera2Range=${camera2Range?.description()} " +
                        "ultraWideZoomRatio=$ultraWideZoomRatio minZoom=$minZoom maxZoom=$maxZoom zoom=$zoom",
        )
        return mapOf(
                "zoomRatio" to zoom.toDouble(),
                "minZoomRatio" to minZoom.toDouble(),
                "maxZoomRatio" to maxZoom.toDouble(),
        )
    }

    fun setZoomRatio(
            ratio: Double,
            onComplete: (Boolean, Map<String, Any>, String?) -> Unit,
    ) {
        val boundCamera = camera
        if (boundCamera == null) {
            if (isSwitchingLens) {
                onComplete(false, zoomCapabilitiesMap(), "Camera lens switch is already in progress")
                return
            }
            val clamped =
                    if (ratio < 1.0 && hasUltraWideCamera()) {
                        ultraWideZoomRatio
                    } else {
                        ratio.toFloat().coerceAtLeast(1f)
                    }
            currentZoomRatio = clamped
            onComplete(true, zoomCapabilitiesMap(), null)
            return
        }

        if (ratio < 1.0 && hasUltraWideCamera()) {
            val logicalRange = mainCameraZoomRatioRange()
            if (logicalRange == null || !logicalRange.contains(ratio.toFloat())) {
                switchToUltraWide(onComplete)
                return
            }
        }

        if (currentLensMode == LensMode.ULTRA_WIDE) {
            switchToMainAndZoom(ratio, onComplete)
            return
        }

        val zoomState = boundCamera.cameraInfo.zoomState.value
        val camera2Range = mainCameraZoomRatioRange()
        val minZoom = camera2Range?.lower ?: zoomState?.minZoomRatio ?: 1f
        val maxZoom = camera2Range?.upper ?: zoomState?.maxZoomRatio ?: clampedMaxZoom()
        val nextZoom = ratio.toFloat().coerceIn(minZoom, maxZoom)
        currentZoomRatio = nextZoom

        val future =
                if (
                        Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
                                camera2Range?.contains(nextZoom) == true
                ) {
                    applyCamera2ZoomRatio(boundCamera, nextZoom)
                } else {
                    boundCamera.cameraControl.setZoomRatio(nextZoom)
                }
        future.addListener(
                {
                    try {
                        future.get()
                        val capabilities = zoomCapabilitiesMap()
                        logSetZoomResult(ratio, nextZoom, capabilities)
                        onComplete(true, capabilities, null)
                    } catch (error: Exception) {
                        if (isSupersededCamera2ZoomRequest(error)) {
                            Log.d(TAG, "setZoomRatio superseded by newer Camera2 options")
                            val capabilities = zoomCapabilitiesMap()
                            logSetZoomResult(ratio, nextZoom, capabilities)
                            onComplete(true, capabilities, null)
                            return@addListener
                        }
                        Log.e(TAG, "setZoomRatio failed", error)
                        onComplete(false, zoomCapabilitiesMap(), error.message)
                    }
                },
                mainExecutor,
        )
    }

    fun unbind() {
        activeRecording?.stop()
        activeRecording = null
        pendingSegmentSwitch = null
        pendingStopCallback = null
        isSwitchingLens = false
        cameraProvider?.unbindAll()
        cameraProvider = null
        preview = null
        videoCapture = null
        camera = null
        boundLifecycleOwner = null
        currentSegmentFile = null
        cleanupSegmentFiles(segmentFiles)
        segmentFiles.clear()
        nextSegmentIndex = 1
        activeDisplayName = null
        currentLensMode = LensMode.MAIN
        currentZoomRatio = 1f
        ultraWideZoomRatio = DEFAULT_ULTRA_WIDE_ZOOM_RATIO
        updateStatus(RecordingStatus(RecordingState.IDLE))
    }

    fun elapsedMillis(): Long {
        if (status.state != RecordingState.RECORDING) return 0L
        return System.currentTimeMillis() - recordingStartedAt
    }

    fun segmentOutputPaths(): List<String> = latestSegmentOutputPaths

    private fun updateStatus(next: RecordingStatus) {
        status = next
        statusListener?.invoke(next)
    }

    private fun applyCurrentZoom() {
        val boundCamera = camera ?: return
        if (currentLensMode == LensMode.ULTRA_WIDE) {
            currentZoomRatio = ultraWideZoomRatio
            boundCamera.cameraControl.setZoomRatio(1f)
            return
        }
        val zoomState = boundCamera.cameraInfo.zoomState.value
        val camera2Range = mainCameraZoomRatioRange()
        val minZoom = camera2Range?.lower ?: zoomState?.minZoomRatio ?: 1f
        val maxZoom = camera2Range?.upper ?: zoomState?.maxZoomRatio ?: clampedMaxZoom()
        currentZoomRatio = currentZoomRatio.coerceIn(minZoom, maxZoom)
        if (
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
                        camera2Range?.contains(currentZoomRatio) == true
        ) {
            applyCamera2ZoomRatio(boundCamera, currentZoomRatio)
        } else {
            boundCamera.cameraControl.setZoomRatio(currentZoomRatio)
        }
    }

    private fun clampedMaxZoom(): Float {
        return camera?.cameraInfo?.zoomState?.value?.maxZoomRatio ?: 3f
    }

    private fun discoverBackCameras(provider: ProcessCameraProvider) {
        val manager = appContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
        if (mainCameraId == null) {
            mainCameraId = cameraIdForSelector(provider, CameraSelector.DEFAULT_BACK_CAMERA)
        }
        logPublicCameraDiagnostics(provider, manager, mainCameraId)
        val ultraWideCamera = findUltraWideCamera(provider, mainCameraId)
        ultraWideCameraId = ultraWideCamera?.cameraId
        ultraWideZoomRatio = ultraWideCamera?.zoomRatio ?: DEFAULT_ULTRA_WIDE_ZOOM_RATIO
        if (ultraWideCamera == null && currentLensMode == LensMode.ULTRA_WIDE) {
            currentLensMode = LensMode.MAIN
            currentZoomRatio = 1f
        }
        Log.d(
                TAG,
                "mainCameraId=$mainCameraId ultraWideCameraId=$ultraWideCameraId " +
                        "ultraWideZoomRatio=$ultraWideZoomRatio",
        )
    }

    private fun cameraIdForSelector(
            provider: ProcessCameraProvider,
            selector: CameraSelector,
    ): String? {
        return try {
            val infos = selector.filter(provider.availableCameraInfos)
            infos.firstOrNull()?.let { Camera2CameraInfo.from(it).cameraId }
        } catch (error: Exception) {
            Log.w(TAG, "cameraIdForSelector failed", error)
            null
        }
    }

    private fun findUltraWideCamera(
            provider: ProcessCameraProvider,
            excludedCameraId: String?,
    ): UltraWideCamera? {
        val manager = appContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
        val candidates =
                manager.cameraIdList
                        .mapNotNull { cameraId -> backCameraProfile(manager, cameraId) }
                        .onEach { profile ->
                            Log.d(
                                    TAG,
                                    "backCamera ${profile.description()} " +
                                            "bindable=${provider.hasCameraSafely(selectorForCameraId(profile.cameraId))}",
                            )
                        }
                        .filter { it.cameraId != excludedCameraId }
                        .filter { provider.hasCameraSafely(selectorForCameraId(it.cameraId)) }
                        .sortedWith(
                                compareByDescending<CameraProfile> { it.horizontalFov }
                                        .thenBy { it.minFocalLength },
                        )

        val mainProfile = excludedCameraId?.let { backCameraProfile(manager, it) }
        val widest =
                candidates.firstOrNull()
                        ?: run {
                            logPhysicalOnlyUltraWideDiagnostics(manager, mainProfile, excludedCameraId)
                            return null
                        }
        val candidatesDesc =
                candidates.joinToString { it.description() }
        val mainDesc =
                mainProfile?.description()
        Log.d(TAG, "ultraWide candidates=[$candidatesDesc] main=$mainDesc")
        if (mainProfile == null) {
            return UltraWideCamera(widest.cameraId, DEFAULT_ULTRA_WIDE_ZOOM_RATIO)
        }

        val meaningfullyWider =
                widest.horizontalFov > mainProfile.horizontalFov * ULTRA_WIDE_FOV_FACTOR ||
                        widest.minFocalLength < mainProfile.minFocalLength * ULTRA_WIDE_FOCAL_FACTOR
        Log.d(
                TAG,
                "ultraWide decision widest=${widest.cameraId} meaningfullyWider=$meaningfullyWider " +
                        "(fovFactor=$ULTRA_WIDE_FOV_FACTOR focalFactor=$ULTRA_WIDE_FOCAL_FACTOR)",
        )
        if (!meaningfullyWider) {
            logPhysicalOnlyUltraWideDiagnostics(manager, mainProfile, excludedCameraId)
            return null
        }

        return UltraWideCamera(widest.cameraId, estimateUltraWideZoomRatio(widest, mainProfile))
    }

    private fun logPublicCameraDiagnostics(
            provider: ProcessCameraProvider,
            manager: CameraManager,
            mainCameraId: String?,
    ) {
        Log.d(TAG, "publicCameraIds=[${manager.cameraIdList.joinToString()}] mainCameraId=$mainCameraId")
        manager.cameraIdList.forEach { cameraId ->
            try {
                val characteristics = manager.getCameraCharacteristics(cameraId)
                val bindable = provider.hasCameraSafely(selectorForCameraId(cameraId))
                val physicalIds = physicalCameraIds(characteristics)
                Log.d(
                        TAG,
                        "publicCamera id=$cameraId facing=${lensFacingDescription(characteristics)} " +
                                "physicalIds=[${physicalIds.joinToString()}] " +
                                "capabilities=[${capabilitiesDescription(characteristics)}] " +
                                "zoomRange=${zoomRatioRangeFromCharacteristics(characteristics)?.description()} " +
                                "focals=${focalLengthsDescription(characteristics)} " +
                                "sensor=${sensorSizeDescription(characteristics)} bindable=$bindable",
                )
                if (cameraId == mainCameraId) {
                    logMainPhysicalCameraDiagnostics(manager, cameraId, physicalIds)
                }
            } catch (error: Exception) {
                Log.w(TAG, "publicCamera diagnostics failed for cameraId=$cameraId", error)
            }
        }
    }

    private fun logMainPhysicalCameraDiagnostics(
            manager: CameraManager,
            mainCameraId: String,
            physicalIds: Set<String>,
    ) {
        Log.d(TAG, "mainCamera id=$mainCameraId physicalIds=[${physicalIds.joinToString()}]")
        physicalIds.forEach { physicalId ->
            try {
                val characteristics = manager.getCameraCharacteristics(physicalId)
                Log.d(
                        TAG,
                        "mainPhysicalCamera id=$physicalId facing=${lensFacingDescription(characteristics)} " +
                                "zoomRange=${zoomRatioRangeFromCharacteristics(characteristics)?.description()} " +
                                "focals=${focalLengthsDescription(characteristics)} " +
                                "sensor=${sensorSizeDescription(characteristics)}",
                )
            } catch (error: Exception) {
                Log.w(TAG, "mainPhysicalCamera diagnostics failed for physicalId=$physicalId", error)
            }
        }
    }

    private fun logPhysicalOnlyUltraWideDiagnostics(
            manager: CameraManager,
            mainProfile: CameraProfile?,
            mainCameraId: String?,
    ) {
        if (mainProfile == null || mainCameraId == null) {
            return
        }
        val characteristics =
                try {
                    manager.getCameraCharacteristics(mainCameraId)
                } catch (error: Exception) {
                    Log.w(TAG, "physicalOnlyUltraWide diagnostics failed for mainCameraId=$mainCameraId", error)
                    return
                }
        val physicalProfiles =
                physicalCameraIds(characteristics)
                        .mapNotNull { physicalId -> backCameraProfile(manager, physicalId) }
                        .filter { it.cameraId != mainCameraId }
                        .sortedBy { it.minFocalLength }
        val widestPhysical = physicalProfiles.firstOrNull()
        if (widestPhysical == null) {
            Log.d(TAG, "physicalOnlyUltraWide none main=${mainProfile.description()}")
            return
        }
        val meaningfullyWider =
                widestPhysical.horizontalFov > mainProfile.horizontalFov * ULTRA_WIDE_FOV_FACTOR ||
                        widestPhysical.minFocalLength < mainProfile.minFocalLength * ULTRA_WIDE_FOCAL_FACTOR
        Log.d(
                TAG,
                "physicalOnlyUltraWide widest=${widestPhysical.description()} " +
                        "main=${mainProfile.description()} meaningfullyWider=$meaningfullyWider " +
                        "exposedAsBindableCamera=false action=diagnostic_only",
        )
    }

    private fun backCameraProfile(
            manager: CameraManager,
            cameraId: String,
    ): CameraProfile? {
        return try {
            val characteristics = manager.getCameraCharacteristics(cameraId)
            val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
            if (facing != CameraCharacteristics.LENS_FACING_BACK) {
                return null
            }
            val focalLengths =
                    characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
                            ?: return null
            val physicalSize =
                    characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)
                            ?: return null
            val minFocalLength = focalLengths.minOrNull() ?: return null
            val horizontalFov =
                    2.0 * atan((physicalSize.width / (2.0f * minFocalLength)).toDouble())
            CameraProfile(
                    cameraId,
                    minFocalLength,
                    horizontalFov,
                    zoomRatioRangeFromCharacteristics(characteristics),
            )
        } catch (error: Exception) {
            Log.w(TAG, "backCameraProfile failed for cameraId=$cameraId", error)
            null
        }
    }

    private fun mainCameraZoomRatioRange(): ZoomRatioRange? {
        val cameraId = mainCameraId ?: activeCameraId()
        return zoomRatioRangeForCamera(cameraId)
    }

    private fun activeCameraId(): String? {
        val boundCamera = camera ?: return null
        return try {
            Camera2CameraInfo.from(boundCamera.cameraInfo).cameraId
        } catch (error: Exception) {
            null
        }
    }

    private fun zoomRatioRangeForCamera(cameraId: String?): ZoomRatioRange? {
        if (cameraId == null) return null
        return try {
            val manager = appContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
            zoomRatioRangeFromCharacteristics(manager.getCameraCharacteristics(cameraId))
        } catch (error: Exception) {
            Log.w(TAG, "zoomRatioRangeForCamera failed for cameraId=$cameraId", error)
            null
        }
    }

    private fun zoomRatioRangeFromCharacteristics(
            characteristics: CameraCharacteristics,
    ): ZoomRatioRange? {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
            return null
        }
        val range = characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)
                ?: return null
        return ZoomRatioRange(range.lower, range.upper)
    }

    private fun physicalCameraIds(characteristics: CameraCharacteristics): Set<String> {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
            return emptySet()
        }
        return characteristics.physicalCameraIds
    }

    private fun lensFacingDescription(characteristics: CameraCharacteristics): String {
        return when (val facing = characteristics.get(CameraCharacteristics.LENS_FACING)) {
            CameraCharacteristics.LENS_FACING_BACK -> "BACK"
            CameraCharacteristics.LENS_FACING_FRONT -> "FRONT"
            CameraCharacteristics.LENS_FACING_EXTERNAL -> "EXTERNAL"
            null -> "null"
            else -> "UNKNOWN($facing)"
        }
    }

    private fun capabilitiesDescription(characteristics: CameraCharacteristics): String {
        val capabilities =
                characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
                        ?: return "null"
        return capabilities.joinToString { capabilityDescription(it) }
    }

    private fun capabilityDescription(capability: Int): String {
        return when (capability) {
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE ->
                    "BACKWARD_COMPATIBLE"
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR -> "MANUAL_SENSOR"
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_POST_PROCESSING ->
                    "MANUAL_POST_PROCESSING"
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW -> "RAW"
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING ->
                    "PRIVATE_REPROCESSING"
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_READ_SENSOR_SETTINGS ->
                    "READ_SENSOR_SETTINGS"
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE -> "BURST_CAPTURE"
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING ->
                    "YUV_REPROCESSING"
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT -> "DEPTH_OUTPUT"
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO ->
                    "CONSTRAINED_HIGH_SPEED_VIDEO"
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING -> "MOTION_TRACKING"
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA ->
                    "LOGICAL_MULTI_CAMERA"
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME -> "MONOCHROME"
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_SECURE_IMAGE_DATA -> "SECURE_IMAGE_DATA"
            else -> "UNKNOWN($capability)"
        }
    }

    private fun focalLengthsDescription(characteristics: CameraCharacteristics): String {
        val focalLengths =
                characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
                        ?: return "null"
        return focalLengths.joinToString(prefix = "[", postfix = "]")
    }

    private fun sensorSizeDescription(characteristics: CameraCharacteristics): String {
        val size = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)
                ?: return "null"
        return "${size.width}x${size.height}"
    }

    private fun estimateUltraWideZoomRatio(
            ultraWide: CameraProfile,
            main: CameraProfile,
    ): Float {
        if (main.minFocalLength <= 0f) {
            return DEFAULT_ULTRA_WIDE_ZOOM_RATIO
        }
        val rawRatio = ultraWide.minFocalLength / main.minFocalLength
        val roundedRatio = round(rawRatio * 10f) / 10f
        return roundedRatio.coerceIn(MIN_ULTRA_WIDE_ZOOM_RATIO, MAX_ULTRA_WIDE_ZOOM_RATIO)
    }

    @androidx.annotation.OptIn(ExperimentalCamera2Interop::class)
    private fun applyCamera2ZoomRatio(
            boundCamera: Camera,
            zoomRatio: Float,
    ) =
            Camera2CameraControl.from(boundCamera.cameraControl)
                    .setCaptureRequestOptions(
                            CaptureRequestOptions.Builder()
                                    .setCaptureRequestOption(
                                            CaptureRequest.CONTROL_ZOOM_RATIO,
                                            zoomRatio,
                                    )
                                    .build(),
                    )

    private fun isSupersededCamera2ZoomRequest(error: Exception): Boolean {
        val cause =
                if (error is ExecutionException) {
                    error.cause
                } else {
                    error
                }
        return cause is CameraControl.OperationCanceledException &&
                cause.message?.contains("updated with new options") == true
    }

    private fun logSetZoomResult(
            requestedRatio: Double,
            nextZoom: Float,
            capabilities: Map<String, Any>,
    ) {
        Log.d(
                TAG,
                "setZoomRatio result requestedRatio=$requestedRatio nextZoom=$nextZoom " +
                        "currentZoomRatio=$currentZoomRatio returnedZoom=${capabilities["zoomRatio"]}",
        )
    }

    private fun selectorForCurrentLensMode(): CameraSelector {
        val cameraId =
                if (currentLensMode == LensMode.ULTRA_WIDE) {
                    ultraWideCameraId
                } else {
                    mainCameraId
                }
        return if (cameraId != null) {
            selectorForCameraId(cameraId)
        } else {
            CameraSelector.DEFAULT_BACK_CAMERA
        }
    }

    private fun selectorForCameraId(cameraId: String): CameraSelector {
        return CameraSelector.Builder()
                .addCameraFilter { cameraInfos ->
                    cameraInfos.filter { Camera2CameraInfo.from(it).cameraId == cameraId }
                }
                .build()
    }

    private fun bindUseCases(
            provider: ProcessCameraProvider,
            lifecycleOwner: LifecycleOwner,
            selector: CameraSelector,
    ) {
        val boundPreview = preview ?: throw IllegalStateException("Preview is not ready")
        val boundVideoCapture =
                videoCapture ?: throw IllegalStateException("Video capture is not ready")
        provider.unbindAll()
        camera =
                provider.bindToLifecycle(
                        lifecycleOwner,
                        selector,
                        boundPreview,
                        boundVideoCapture,
                )
    }

    private fun switchToUltraWide(
            onComplete: (Boolean, Map<String, Any>, String?) -> Unit,
    ) {
        val ultraWideId = ultraWideCameraId
        if (ultraWideId == null) {
            onComplete(false, zoomCapabilitiesMap(), "Ultra-wide camera is unavailable")
            return
        }
        if (currentLensMode == LensMode.ULTRA_WIDE) {
            currentZoomRatio = ultraWideZoomRatio
            onComplete(true, zoomCapabilitiesMap(), null)
            return
        }
        if (activeRecording != null) {
            switchRecordingSegmentToLens(
                    LensMode.ULTRA_WIDE,
                    ultraWideZoomRatio.toDouble(),
                    onComplete,
            )
            return
        }
        val provider = cameraProvider
        val lifecycleOwner = boundLifecycleOwner
        if (provider == null || lifecycleOwner == null) {
            onComplete(false, zoomCapabilitiesMap(), "Camera is not ready")
            return
        }
        try {
            currentLensMode = LensMode.ULTRA_WIDE
            currentZoomRatio = ultraWideZoomRatio
            bindUseCases(provider, lifecycleOwner, selectorForCameraId(ultraWideId))
            applyCurrentZoom()
            onComplete(true, zoomCapabilitiesMap(), null)
        } catch (error: Exception) {
            Log.e(TAG, "switchToUltraWide failed", error)
            currentLensMode = LensMode.MAIN
            currentZoomRatio = 1f
            try {
                bindUseCases(provider, lifecycleOwner, selectorForCurrentLensMode())
                applyCurrentZoom()
            } catch (restoreError: Exception) {
                Log.e(TAG, "restore main camera after ultra-wide failure failed", restoreError)
            }
            onComplete(false, zoomCapabilitiesMap(), error.message)
        }
    }

    private fun switchToMainAndZoom(
            ratio: Double,
            onComplete: (Boolean, Map<String, Any>, String?) -> Unit,
    ) {
        if (activeRecording != null) {
            switchRecordingSegmentToLens(LensMode.MAIN, ratio, onComplete)
            return
        }
        val provider = cameraProvider
        val lifecycleOwner = boundLifecycleOwner
        if (provider == null || lifecycleOwner == null) {
            onComplete(false, zoomCapabilitiesMap(), "Camera is not ready")
            return
        }
        try {
            currentLensMode = LensMode.MAIN
            currentZoomRatio = ratio.toFloat().coerceAtLeast(1f)
            bindUseCases(provider, lifecycleOwner, selectorForCurrentLensMode())
            setZoomRatio(ratio, onComplete)
        } catch (error: Exception) {
            Log.e(TAG, "switchToMainAndZoom failed", error)
            onComplete(false, zoomCapabilitiesMap(), error.message)
        }
    }

    private fun hasUltraWideCamera(): Boolean {
        return ultraWideCameraId != null
    }

    private fun switchRecordingSegmentToLens(
            targetLensMode: LensMode,
            targetRatio: Double,
            onComplete: (Boolean, Map<String, Any>, String?) -> Unit,
    ) {
        val recording = activeRecording
        if (recording == null) {
            onComplete(false, zoomCapabilitiesMap(), "Recording is not active")
            return
        }
        if (isSwitchingLens || pendingSegmentSwitch != null) {
            onComplete(false, zoomCapabilitiesMap(), "Camera lens switch is already in progress")
            return
        }
        pendingSegmentSwitch = PendingSegmentSwitch(targetLensMode, targetRatio, onComplete)
        isSwitchingLens = true
        recording.stop()
        activeRecording = null
    }

    private fun continueSegmentSwitch(request: PendingSegmentSwitch) {
        val provider = cameraProvider
        val lifecycleOwner = boundLifecycleOwner
        if (provider == null || lifecycleOwner == null) {
            isSwitchingLens = false
            request.onComplete(false, zoomCapabilitiesMap(), "Camera is not ready")
            return
        }

        try {
            currentLensMode = request.targetLensMode
            currentZoomRatio =
                    if (request.targetLensMode == LensMode.ULTRA_WIDE) {
                        ultraWideZoomRatio
                    } else {
                        request.targetRatio.toFloat().coerceAtLeast(1f)
                    }
            bindUseCases(provider, lifecycleOwner, selectorForCurrentLensMode())
            applyCurrentZoom()
            startNewSegment(updateStatusOnStart = true) { started, message ->
                isSwitchingLens = false
                if (started) {
                    request.onComplete(true, zoomCapabilitiesMap(), null)
                } else {
                    updateStatus(RecordingStatus(RecordingState.ERROR, message = message))
                    request.onComplete(false, zoomCapabilitiesMap(), message)
                }
            }
        } catch (error: Exception) {
            Log.e(TAG, "continueSegmentSwitch failed", error)
            isSwitchingLens = false
            updateStatus(RecordingStatus(RecordingState.ERROR, message = error.message))
            request.onComplete(false, zoomCapabilitiesMap(), error.message)
        }
    }

    private fun ProcessCameraProvider.hasCameraSafely(selector: CameraSelector): Boolean {
        return try {
            hasCamera(selector)
        } catch (error: Exception) {
            false
        }
    }

    private enum class LensMode {
        MAIN,
        ULTRA_WIDE,
    }

    private data class CameraProfile(
            val cameraId: String,
            val minFocalLength: Float,
            val horizontalFov: Double,
            val zoomRatioRange: ZoomRatioRange?,
    ) {
        fun description(): String {
            return "id=$cameraId fov=$horizontalFov focal=$minFocalLength " +
                    "zoomRange=${zoomRatioRange?.description()}"
        }
    }

    private data class ZoomRatioRange(
            val lower: Float,
            val upper: Float,
    ) {
        fun contains(ratio: Float): Boolean {
            return ratio >= lower && ratio <= upper
        }

        fun description(): String {
            return "$lower..$upper"
        }
    }

    private data class UltraWideCamera(
            val cameraId: String,
            val zoomRatio: Float,
    )

    private data class PendingSegmentSwitch(
            val targetLensMode: LensMode,
            val targetRatio: Double,
            val onComplete: (Boolean, Map<String, Any>, String?) -> Unit,
    )

    companion object {
        private const val TAG = "RecordingCamera"
        private const val DEFAULT_ULTRA_WIDE_ZOOM_RATIO = 0.6f
        private const val MIN_ULTRA_WIDE_ZOOM_RATIO = 0.3f
        private const val MAX_ULTRA_WIDE_ZOOM_RATIO = 0.99f
        // 适度放宽判定宽容度，覆盖更多机型(更小的 FOV/焦距差异也视为超广角)。
        private const val ULTRA_WIDE_FOV_FACTOR = 1.04
        private const val ULTRA_WIDE_FOCAL_FACTOR = 0.96
    }
}
