package com.dronex.rec.recording

import android.content.Context
import android.util.Log
import androidx.camera.core.Camera
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.util.concurrent.Executor

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

    private var cameraProvider: ProcessCameraProvider? = null
    private var preview: Preview? = null
    private var videoCapture: VideoCapture<Recorder>? = null
    private var camera: Camera? = null
    private var activeRecording: Recording? = null
    private var boundLifecycleOwner: LifecycleOwner? = null
    private var currentZoomRatio: Float = 1f

    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 pendingStopCallback: ((String?) -> Unit)? = 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)

                        provider.unbindAll()
                        camera =
                                provider.bindToLifecycle(
                                        lifecycleOwner,
                                        CameraSelector.DEFAULT_BACK_CAMERA,
                                        preview,
                                        videoCapture,
                                )
                        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
            provider.unbindAll()
            camera =
                    provider.bindToLifecycle(
                            lifecycleOwner,
                            CameraSelector.DEFAULT_BACK_CAMERA,
                            preview,
                            videoCapture,
                    )
            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) {
            onStarted(false, "Already recording")
            return
        }

        val outputOptions =
                RecordingOutputFactory.buildMediaStoreOutputOptions(
                        appContext,
                        displayName,
                )
        latestOutputPath = null

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

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

        activeRecording =
                pending.start(mainExecutor) { event ->
                    when (event) {
                        is VideoRecordEvent.Start -> Unit
                        is VideoRecordEvent.Finalize -> {
                            activeRecording = null
                            if (event.hasError()) {
                                updateStatus(
                                        RecordingStatus(
                                                RecordingState.ERROR,
                                                message = event.cause?.message
                                                                ?: "Recording failed",
                                        ),
                                )
                            } else {
                                latestOutputPath = event.outputResults.outputUri.toString()
                                updateStatus(
                                        RecordingStatus(
                                                RecordingState.PREVIEWING,
                                                outputPath = latestOutputPath,
                                                elapsedMillis =
                                                        System.currentTimeMillis() -
                                                                recordingStartedAt,
                                        ),
                                )
                            }
                            val stopCallback = pendingStopCallback
                            pendingStopCallback = null
                            stopCallback?.invoke(latestOutputPath)
                        }
                    }
                }

        onStarted(true, latestOutputPath ?: "recording")
    }

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

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

        recording.stop()
        activeRecording = null
    }

    fun zoomCapabilitiesMap(): Map<String, Any> {
        val zoomState = camera?.cameraInfo?.zoomState?.value
        val minZoom = zoomState?.minZoomRatio ?: 1f
        val maxZoom = zoomState?.maxZoomRatio ?: 3f
        val zoom = (zoomState?.zoomRatio ?: currentZoomRatio).coerceIn(minZoom, maxZoom)
        currentZoomRatio = 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) {
            val clamped = ratio.toFloat().coerceAtLeast(1f)
            currentZoomRatio = clamped
            onComplete(true, zoomCapabilitiesMap(), null)
            return
        }

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

        val future = boundCamera.cameraControl.setZoomRatio(nextZoom)
        future.addListener(
                {
                    try {
                        future.get()
                        onComplete(true, zoomCapabilitiesMap(), null)
                    } catch (error: Exception) {
                        Log.e(TAG, "setZoomRatio failed", error)
                        onComplete(false, zoomCapabilitiesMap(), error.message)
                    }
                },
                mainExecutor,
        )
    }

    fun unbind() {
        activeRecording?.stop()
        activeRecording = null
        cameraProvider?.unbindAll()
        cameraProvider = null
        preview = null
        videoCapture = null
        camera = null
        boundLifecycleOwner = null
        currentZoomRatio = 1f
        updateStatus(RecordingStatus(RecordingState.IDLE))
    }

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

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

    private fun applyCurrentZoom() {
        val boundCamera = camera ?: return
        val zoomState = boundCamera.cameraInfo.zoomState.value
        val minZoom = zoomState?.minZoomRatio ?: 1f
        val maxZoom = zoomState?.maxZoomRatio ?: clampedMaxZoom()
        currentZoomRatio = currentZoomRatio.coerceIn(minZoom, maxZoom)
        boundCamera.cameraControl.setZoomRatio(currentZoomRatio)
    }

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

    companion object {
        private const val TAG = "RecordingCamera"
    }
}
