package com.performancecoachlab.posedetection.camera

import android.graphics.Bitmap
import android.hardware.camera2.CameraCharacteristics
import android.view.OrientationEventListener
import android.view.Surface
import androidx.annotation.OptIn
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA
import androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.FileOutputOptions
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.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.core.content.ContextCompat
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import co.touchlab.kermit.Logger
import com.google.mlkit.vision.pose.Pose
import com.google.mlkit.vision.pose.PoseDetection
import com.google.mlkit.vision.pose.PoseLandmark
import com.google.mlkit.vision.pose.defaults.PoseDetectorOptions
import com.performancecoachlab.posedetection.custom.CustomObjectRespository
import com.performancecoachlab.posedetection.custom.ObjectModel
import com.performancecoachlab.posedetection.recording.AnalysisObject
import com.performancecoachlab.posedetection.skeleton.Skeleton
import com.performancecoachlab.posedetection.skeleton.SkeletonRepository
import kotlinx.coroutines.CompletableDeferred
import java.io.File
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong

@OptIn(ExperimentalCamera2Interop::class)
private fun buildBackUltraWideSelectorOrNull(cameraProvider: ProcessCameraProvider): CameraSelector? {
    return try {
        val bestBackInfo: CameraInfo? = cameraProvider.availableCameraInfos
            .asSequence()
            .filter { info ->
                Camera2CameraInfo.from(info)
                    .getCameraCharacteristic(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK
            }
            .mapNotNull { info ->
                val focalLengths: FloatArray? = Camera2CameraInfo.from(info)
                    .getCameraCharacteristic(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
                val minFocal = focalLengths?.minOrNull()
                if (minFocal != null) info to minFocal else null
            }
            // Smallest focal length => widest FOV (ultra-wide on multi-cam devices)
            .minByOrNull { (_, minFocal) -> minFocal }
            ?.first

        bestBackInfo?.let { selectedInfo ->
            CameraSelector.Builder()
                .addCameraFilter { cameraInfos ->
                    // Defensive: ensure we ONLY ever return a back-facing camera.
                    cameraInfos.filter { candidate ->
                        candidate == selectedInfo &&
                                Camera2CameraInfo.from(candidate)
                                    .getCameraCharacteristic(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK
                    }
                }
                .build()
        }
    } catch (t: Throwable) {
        Logger.w(t) { "Failed to select ultra-wide back camera, falling back to default back." }
        null
    }
}

@OptIn(ExperimentalCamera2Interop::class)
@Composable
actual fun CameraView(
    skeletonRepository: SkeletonRepository,
    customObjectRepository: CustomObjectRespository,
    detectMode: DetectMode,
    drawSkeleton: Boolean,
    objectModel: ObjectModel?,
    drawObjects: ((List<AnalysisObject>) -> List<DrawableObject>)?,
    modifier: Modifier,
    frontCamera: Boolean,
    useUltraWide: Boolean,
    previewFillMode: PreviewFillMode,
    recordingId: String?,
    focusArea: Rect?,
    controller: CameraViewController?,
    onRecordToggled: (Boolean) -> Unit,
    onVideoSaved: (String, String) -> Unit,
) {
    // Replace per-frame UI bitmap with lightweight analysis state.
    var analysisSize by remember { mutableStateOf<Size?>(null) }
    var latestSkeleton by remember { mutableStateOf<Skeleton?>(null) }
    var latestObjects by remember { mutableStateOf<List<AnalysisObject>>(emptyList()) }

    // Throttle detectors to avoid doing heavy work every frame.
    // 33ms ≈ 30 FPS target — sufficient for smooth real-time feedback
    // while leaving CPU headroom for recording and other work.
    val objectIntervalMs = 33L
    val poseIntervalMs = 33L
    val lastObjectRunAtMs = remember { AtomicLong(0L) }
    val lastPoseRunAtMs = remember { AtomicLong(0L) }
    val analysisFrameCounter = remember { AtomicLong(0L) }

    val options = PoseDetectorOptions.Builder()
        .setDetectorMode(PoseDetectorOptions.STREAM_MODE)
        .build()
    val poseDetector = PoseDetection.getClient(options)

    val context = LocalContext.current
    val lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
    val previewView: PreviewView = remember { PreviewView(context) }
    val executor = remember { Executors.newSingleThreadExecutor() }

    var focus by remember { mutableStateOf(focusArea) }
    var objectDetector by remember { mutableStateOf(objectModel) }
    var currentDetectMode by remember { mutableStateOf(detectMode) }

    val textMeasurer = rememberTextMeasurer()
    LaunchedEffect(detectMode) {
        currentDetectMode = detectMode
        if (!detectMode.doPose()) latestSkeleton = null
        if (!detectMode.doObject()) latestObjects = emptyList()
    }

    // Update focus when focusArea changes
    LaunchedEffect(focusArea) {
        focus = focusArea
    }

    // Update object model when it changes
    LaunchedEffect(objectModel) {
        objectDetector = objectModel
    }

    // Track device orientation so we can update CameraX use-case target rotations.
    val currentRotation = remember { mutableIntStateOf(Surface.ROTATION_0) }
    var imageAnalysisRef by remember { mutableStateOf<ImageAnalysis?>(null) }
    var videoCaptureRef by remember { mutableStateOf<VideoCapture<Recorder>?>(null) }

    DisposableEffect(context) {
        val listener = object : OrientationEventListener(context) {
            override fun onOrientationChanged(orientation: Int) {
                if (orientation == ORIENTATION_UNKNOWN) return
                val rotation = when {
                    orientation >= 315 || orientation < 45  -> Surface.ROTATION_0
                    orientation in 45 until 135             -> Surface.ROTATION_270
                    // Skip 180° — most Android phones don't rotate the display
                    // to upside-down portrait, so the target rotation should stay
                    // at the previous value to match what the preview shows.
                    orientation in 135 until 225            -> return
                    orientation in 225 until 315            -> Surface.ROTATION_90
                    else -> return
                }
                if (currentRotation.intValue != rotation) {
                    currentRotation.intValue = rotation
                    imageAnalysisRef?.targetRotation = rotation
                    videoCaptureRef?.targetRotation = rotation
                }
            }
        }
        listener.enable()
        onDispose { listener.disable() }
    }

    // CameraX VideoCapture recording state
    var recorderRef by remember { mutableStateOf<Recorder?>(null) }
    var currentCxRecording by remember { mutableStateOf<Recording?>(null) }
    // Completes when the active recording's Finalize event fires.
    var finalizeLatch by remember { mutableStateOf<CompletableDeferred<Unit>?>(null) }

    // React to recordingId changes — start/stop CameraX VideoCapture recording
    LaunchedEffect(recordingId, recorderRef) {
        val recorder = recorderRef ?: return@LaunchedEffect

        // Stop any active recording and wait for it to fully finalize
        // before starting the next one. CameraX's Recorder only supports
        // one active recording at a time.
        currentCxRecording?.let { activeRec ->
            activeRec.stop()
            finalizeLatch?.await()
            finalizeLatch = null
            currentCxRecording = null
        }

        // Start new recording if recordingId is non-null.
        recordingId?.let { id ->
            val outputPath = File(
                context.cacheDir,
                "camera_recording_${id}_${System.currentTimeMillis()}.mp4"
            ).absolutePath
            val outputFile = File(outputPath)
            val fileOutputOptions = FileOutputOptions.Builder(outputFile).build()

            // Capture id and path for this specific recording so the Finalize
            // callback uses the correct values even if recordingId changes
            // before finalization completes (e.g. split-recording).
            val capturedId = id
            val capturedPath = outputPath

            val latch = CompletableDeferred<Unit>()
            finalizeLatch = latch

            val pendingRecording = recorder.prepareRecording(context, fileOutputOptions)

            currentCxRecording = pendingRecording.start(
                ContextCompat.getMainExecutor(context)
            ) { event ->
                when (event) {
                    is VideoRecordEvent.Start -> {
                        onRecordToggled(true)
                    }
                    is VideoRecordEvent.Finalize -> {
                        onRecordToggled(false)
                        onVideoSaved(capturedId, capturedPath)
                        currentCxRecording = null
                        latch.complete(Unit)
                    }
                }
            }
        }
    }

    val gate = remember { FrameGate() }

    LaunchedEffect(lifecycleOwner, frontCamera, useUltraWide, previewFillMode) {
        // Stop any in-progress recording before rebinding camera.
        currentCxRecording?.stop()
        currentCxRecording = null

        // Clear stale refs before unbinding.
        imageAnalysisRef = null
        videoCaptureRef = null

        val cameraProvider = ProcessCameraProvider.getInstance(context).get()
        cameraProvider.unbindAll()

        val cameraSelector = if (frontCamera) {
            DEFAULT_FRONT_CAMERA
        } else {
            if (useUltraWide) {
                buildBackUltraWideSelectorOrNull(cameraProvider) ?: DEFAULT_BACK_CAMERA
            } else {
                DEFAULT_BACK_CAMERA
            }
        }

        val rotation = currentRotation.intValue
        val preview = Preview.Builder().build().also {
            it.surfaceProvider = previewView.surfaceProvider
        }
        val imageAnalysis = ImageAnalysis.Builder()
            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
            .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
            .build()
            .also { analysis ->
                analysis.targetRotation = rotation
                analysis.setAnalyzer(executor) { imageProxy ->
                    // Drop frames while we're still working on the previous one.
                    if (!gate.tryEnter()) {
                        imageProxy.close()
                        return@setAnalyzer
                    }

                    // Convert sensor timestamp to epoch time.
                    // sensor is REALTIME ns, convert to epoch ms via boot offset.
                    val sensorMs = imageProxy.imageInfo.timestamp / 1_000_000L
                    val bootEpochOffset = System.currentTimeMillis() - android.os.SystemClock.elapsedRealtime()
                    val now = sensorMs + bootEpochOffset
                    val area = focus

                    // In BOTH mode, stagger pose and object on alternate frames
                    // to reduce per-frame processing time and improve tracking.
                    val frameNum = analysisFrameCounter.incrementAndGet()
                    val isBothMode = currentDetectMode == DetectMode.BOTH
                    val shouldRunObject = currentDetectMode.doObject() &&
                            (now - lastObjectRunAtMs.get() >= objectIntervalMs) &&
                            (!isBothMode || frameNum % 2 == 0L)
                    val shouldRunPose = currentDetectMode.doPose() &&
                            (now - lastPoseRunAtMs.get() >= poseIntervalMs) &&
                            (!isBothMode || frameNum % 2 != 0L)

                    // If neither detector is scheduled to run, just close quickly and reuse last results.
                    if (!shouldRunObject && !shouldRunPose) {
                        // still release the gate; no repositories updated
                        imageProxy.close()
                        gate.exit()
                        return@setAnalyzer
                    }

                    if (shouldRunObject) lastObjectRunAtMs.set(now)
                    if (shouldRunPose) lastPoseRunAtMs.set(now)

                    val poseClient = if (shouldRunPose) poseDetector else null
                    val objectClient = if (shouldRunObject) objectDetector?.getDetector() else null
                    val rotationDegrees = imageProxy.imageInfo.rotationDegrees

                    imageProxy.process(
                        objectClient,
                        poseClient,
                        now,
                        area
                    ) { analysisResult, frameBitmap ->
                        try {
                            // Only update repositories/results for detectors that actually ran.
                            if (shouldRunObject) {
                                customObjectRepository.updateCustomObject(analysisResult.objects)
                                latestObjects = analysisResult.objects
                            }
                            if (shouldRunPose) {
                                analysisResult.skeleton?.let { skeletonRepository.updateSkeleton(it) }
                                latestSkeleton = if (drawSkeleton) analysisResult.skeleton else null
                            }

                            // Size is based on the frame bitmap we got back.
                            analysisSize = androidx.compose.ui.geometry.Size(
                                width = frameBitmap.width.toFloat(),
                                height = frameBitmap.height.toFloat()
                            )

                            controller?.setRequestDataProvider {
                                CameraViewData(
                                    width = frameBitmap.width.toFloat(),
                                    height = frameBitmap.height.toFloat(),
                                    rotation = when (rotationDegrees) {
                                        0 -> SensorRotation.ROTATION_0
                                        90 -> SensorRotation.ROTATION_90
                                        180 -> SensorRotation.ROTATION_180
                                        270 -> SensorRotation.ROTATION_270
                                        else -> SensorRotation.ROTATION_0
                                    }
                                )
                            }

                        } finally {
                            gate.exit()
                        }
                    }
                }
            }
        imageAnalysisRef = imageAnalysis
        val recorder = Recorder.Builder()
            .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
            .build()
        val videoCapture = VideoCapture.withOutput(recorder)
        videoCapture.targetRotation = rotation
        videoCaptureRef = videoCapture

        val camera = try {
            cameraProvider.bindToLifecycle(
                lifecycleOwner,
                cameraSelector,
                preview,
                imageAnalysis,
                videoCapture
            ).also {
                recorderRef = recorder
            }
        } catch (e: IllegalArgumentException) {
            // Fallback for devices that cannot bind 3 use cases simultaneously.
            Logger.w(e) { "Cannot bind VideoCapture; recording disabled on this device." }
            recorderRef = null
            cameraProvider.bindToLifecycle(
                lifecycleOwner,
                cameraSelector,
                preview,
                imageAnalysis
            )
        }

        // Helpful debug: see which physical camera ID CameraX picked.
        runCatching {
            val cameraId = Camera2CameraInfo.from(camera.cameraInfo).cameraId
            Logger.d { "CameraX bound cameraId=$cameraId frontCamera=$frontCamera" }
        }

        // IMPORTANT: keep preview scale type.
        previewView.scaleType = when (previewFillMode) {
            PreviewFillMode.FILL -> PreviewView.ScaleType.FILL_CENTER
            PreviewFillMode.FIT -> PreviewView.ScaleType.FIT_CENTER
        }
    }

    Box(
        modifier = modifier.clipToBounds()
    ) {
        AndroidView(
            factory = { previewView },
            modifier = Modifier
                .fillMaxSize()
                .clipToBounds()
        )

        val overlaySize = analysisSize

        Canvas(
            modifier = Modifier
                .fillMaxSize()
                .clipToBounds()
                .scale(if (frontCamera) -1f else 1f, 1f)
        ) {
            val frameW = overlaySize?.width
            val frameH = overlaySize?.height
            if (frameW == null || frameH == null) return@Canvas

            val viewW = size.width.coerceAtLeast(1f)
            val viewH = size.height.coerceAtLeast(1f)

            // Map frame (analysisBitmap) coordinates into view coordinates based on PreviewFillMode.
            // This must match PreviewView.scaleType.
            val scale = when (previewFillMode) {
                // FILL_CENTER == center-crop
                PreviewFillMode.FILL -> maxOf(viewW / frameW, viewH / frameH)
                // FIT_CENTER == center-inside
                PreviewFillMode.FIT -> minOf(viewW / frameW, viewH / frameH)
            }
            val scaledW = frameW * scale
            val scaledH = frameH * scale

            // Center the scaled image within the view; dx/dy positive for FIT (letterbox), negative for FILL (crop).
            val dx = (viewW - scaledW) / 2f
            val dy = (viewH - scaledH) / 2f

            fun mapX(x: Float) = dx + x * scale
            fun mapY(y: Float) = dy + y * scale

            // Objects
            val drawable = drawObjects?.invoke(latestObjects) ?: emptyList()
            drawable.forEach { d ->
                val bb = d.obj.boundingBox
                val tl = Offset(mapX(bb.left), mapY(bb.top))
                val br = Offset(mapX(bb.right), mapY(bb.bottom))

                val left = tl.x
                val top = tl.y
                val right = br.x
                val bottom = br.y

                val w = (right - left).coerceAtLeast(1f)
                val h = (bottom - top).coerceAtLeast(1f)

                when (d.shape) {
                    DrawableShape.RECTANGLE -> drawRect(
                        color = d.colour,
                        topLeft = Offset(left, top),
                        size = Size(w, h),
                        style = if (d.style is androidx.compose.ui.graphics.drawscope.Fill) androidx.compose.ui.graphics.drawscope.Fill else Stroke(
                            (d.style as? Stroke)?.width ?: 3f
                        )
                    )

                    DrawableShape.OVAL -> drawOval(
                        color = d.colour,
                        topLeft = Offset(left, top),
                        size = Size(w, h),
                        style = if (d.style is androidx.compose.ui.graphics.drawscope.Fill) androidx.compose.ui.graphics.drawscope.Fill else Stroke(
                            (d.style as? Stroke)?.width ?: 3f
                        )
                    )

                    DrawableShape.LABEL -> {
                        drawLabelTextPlatform(
                            drawableObject = d.copy(
                                obj = d.obj.copy(
                                    boundingBox = Rect(
                                        offset = Offset(left, top),
                                        size = Size(w, h),
                                    )
                                )
                            ),
                            textMeasurer = textMeasurer
                        )
                    }
                }
            }

            // Skeleton
            latestSkeleton?.let { skel ->
                val joints = skel.joints()
                val bones = skel.bones()

                bones.forEach { (a, b) ->
                    val start = Offset(mapX(a.x), mapY(a.y))
                    val end = Offset(mapX(b.x), mapY(b.y))
                    drawLine(
                        color = Color.White,
                        start = start,
                        end = end,
                        strokeWidth = 4f
                    )
                }
                joints.forEach { j ->
                    val c = Offset(mapX(j.x), mapY(j.y))
                    drawCircle(
                        color = Color.Blue,
                        radius = 6f,
                        center = c
                    )
                }
            }
        }
    }

    // ...existing code...
}


private fun PoseLandmark?.toSkeletonCoords(): Skeleton.SkeletonCoordinate? {
    return this?.position?.let {
        Skeleton.SkeletonCoordinate(
            x = it.x,
            y = it.y,
        )
    }
}

fun Bitmap.rotate(angle: Float): Bitmap {
    val matrix = android.graphics.Matrix()
    matrix.postRotate(angle)
    return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
}

fun ImageBitmap.rotate(angle: Float): ImageBitmap {
    return asAndroidBitmap().rotate(angle).asImageBitmap()
}

fun skeleton(pose: Pose?, timestamp: Long, width: Int, height: Int): Skeleton {
    return Skeleton(
        timestamp = timestamp,
        leftShoulder = pose?.getPoseLandmark(PoseLandmark.LEFT_SHOULDER)?.toSkeletonCoords(),
        rightShoulder = pose?.getPoseLandmark(PoseLandmark.RIGHT_SHOULDER)?.toSkeletonCoords(),
        leftElbow = pose?.getPoseLandmark(PoseLandmark.LEFT_ELBOW)?.toSkeletonCoords(),
        rightElbow = pose?.getPoseLandmark(PoseLandmark.RIGHT_ELBOW)?.toSkeletonCoords(),
        leftWrist = pose?.getPoseLandmark(PoseLandmark.LEFT_WRIST)?.toSkeletonCoords(),
        rightWrist = pose?.getPoseLandmark(PoseLandmark.RIGHT_WRIST)?.toSkeletonCoords(),
        leftHip = pose?.getPoseLandmark(PoseLandmark.LEFT_HIP)?.toSkeletonCoords(),
        rightHip = pose?.getPoseLandmark(PoseLandmark.RIGHT_HIP)?.toSkeletonCoords(),
        leftKnee = pose?.getPoseLandmark(PoseLandmark.LEFT_KNEE)?.toSkeletonCoords(),
        rightKnee = pose?.getPoseLandmark(PoseLandmark.RIGHT_KNEE)?.toSkeletonCoords(),
        leftAnkle = pose?.getPoseLandmark(PoseLandmark.LEFT_ANKLE)?.toSkeletonCoords(),
        rightAnkle = pose?.getPoseLandmark(PoseLandmark.RIGHT_ANKLE)?.toSkeletonCoords(),
        width = width.toFloat(),
        height = height.toFloat(),
    )
}

private class FrameGate {
    private val busy = AtomicBoolean(false)
    fun tryEnter(): Boolean = busy.compareAndSet(false, true)
    fun exit() {
        busy.set(false)
    }
}
