package com.performancecoachlab.posedetection.recording

import co.touchlab.kermit.Logger
import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.ObjCObjectVar
import kotlinx.cinterop.alloc
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.value
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
import platform.AVFoundation.AVAsset
import platform.AVFoundation.AVAssetImageGenerator
import platform.AVFoundation.AVAssetReader
import platform.AVFoundation.AVAssetReaderTrackOutput
import platform.AVFoundation.AVAssetTrack
import platform.AVFoundation.AVMediaTypeVideo
import platform.AVFoundation.CMTimeValue
import platform.AVFoundation.tracksWithMediaType
import platform.AVFoundation.valueWithCMTime
import platform.CoreFoundation.CFRelease
import platform.CoreMedia.CMSampleBufferGetPresentationTimeStamp
import platform.CoreMedia.CMTimeGetSeconds
import platform.CoreMedia.CMTimeMake
import platform.CoreMedia.CMTimeMakeWithSeconds
import platform.Foundation.NSError
import platform.Foundation.NSFileManager
import platform.Foundation.NSURL
import platform.Foundation.NSValue

@OptIn(ExperimentalForeignApi::class)
actual suspend fun extractFrame(
    videoPath: String,
    frameTimestamp: Long,
): InputFrame? {
    return withContext(Dispatchers.IO) {
        val asset = AVAsset.assetWithURL(createUrl(url = videoPath))
        val generator = AVAssetImageGenerator.assetImageGeneratorWithAsset(asset)
        generator.appliesPreferredTrackTransform = true
        generator.requestedTimeToleranceBefore = CMTimeMake(0, 600)
        generator.requestedTimeToleranceAfter = CMTimeMake(0, 600)
        val t = CMTimeMakeWithSeconds(frameTimestamp.toDouble() / 1000.0, preferredTimescale = 600)
        val time = NSValue.valueWithCMTime(t)
        try {
            val cgImage =
                generator.copyCGImageAtTime(time.CMTimeValue, actualTime = null, error = null)
            cgImage?.let {
                return@withContext InputFrame(cgImage = it, timestamp = frameTimestamp)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return@withContext null
    }
}

actual fun extractFrames(
    videoPath: String, frameTimestamps: List<Long>, cropRect: CropRect?
): Flow<InputFrame> = flow {
    for (ts in frameTimestamps) {
        val frame = extractFrame(videoPath, ts) ?: continue
        val output = if (cropRect != null) {
            frame.crop(androidx.compose.ui.geometry.Rect(
                cropRect.x.toFloat(), cropRect.y.toFloat(),
                (cropRect.x + cropRect.width).toFloat(),
                (cropRect.y + cropRect.height).toFloat()
            ))
        } else frame
        emit(output)
    }
}.flowOn(Dispatchers.IO)

private fun NSURL.safeDescription(): String {
    val scheme = this.scheme ?: "unknown"
    val fileName = this.lastPathComponent ?: "unknown"
    return "$scheme://.../$fileName"
}

private fun safeFileName(path: String?): String {
    return path?.substringAfterLast("/") ?: "unknown"
}

@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
actual suspend fun listVideoFrameTimestamps(
    videoPath: String,
): List<Long> = withContext(Dispatchers.IO) {
    val out = mutableListOf<Long>()

    try {
        val resolvedUrl = createUrl(url = videoPath)
        Logger.i { "[iOS] listVideoFrameTimestamps using URL: ${resolvedUrl.safeDescription()}" }

        // AVAssetReader requires a local file URL. Bail out early for remote URLs.
        if (!resolvedUrl.isFileURL()) {
            Logger.i { "AVAssetReader requires a local file URL; got scheme: ${resolvedUrl.scheme}" }
            return@withContext out
        }

        // Verify the file actually exists at path
        val path = resolvedUrl.path
        if (path == null || !NSFileManager.defaultManager.fileExistsAtPath(path)) {
            Logger.i { "File does not exist: ${safeFileName(path)}" }
            return@withContext out
        }

        val asset = AVAsset.assetWithURL(resolvedUrl)

        // Use synchronous approach - get tracks directly using tracksWithMediaType
        val videoTracks = asset.tracksWithMediaType(AVMediaTypeVideo)

        if (videoTracks.isEmpty()) {
            Logger.i { "No video tracks found in video: ${safeFileName(videoPath)}" }
            return@withContext out
        }

        val videoTrack = videoTracks.first() as AVAssetTrack
        Logger.i { "Found video track with ID: ${videoTrack.trackID}" }

        memScoped {
            val errorPtr = alloc<ObjCObjectVar<NSError?>>()
            val reader = AVAssetReader.assetReaderWithAsset(asset, error = errorPtr.ptr)

            if (reader == null) {
                val err = errorPtr.value
                Logger.i { "Failed to create AVAssetReader: ${err?.localizedDescription}" }
                return@withContext out
            }

            val trackOutput = AVAssetReaderTrackOutput.assetReaderTrackOutputWithTrack(
                track = videoTrack,
                outputSettings = null
            )

            if (!reader.canAddOutput(trackOutput)) {
                Logger.i { "Cannot add track output to reader" }
                return@withContext out
            }

            reader.addOutput(trackOutput)

            if (!reader.startReading()) {
                Logger.i { "Failed to start reading: ${reader.error?.localizedDescription}" }
                return@withContext out
            }

            Logger.i { "Started reading video frames..." }
            var frameCount = 0
            while (true) {
                val sampleBuffer = trackOutput.copyNextSampleBuffer()
                if (sampleBuffer == null) {
                    break
                }

                val presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
                val timeInSeconds = CMTimeGetSeconds(presentationTime)
                val timeInMillis = (timeInSeconds * 1000.0).toLong()

                out.add(timeInMillis)
                frameCount++

                // Release the sample buffer
                CFRelease(sampleBuffer)

                if (out.size >= Int.MAX_VALUE) break
            }

            Logger.i { "Extracted $frameCount frame timestamps" }
        }

    } catch (e: Exception) {
        Logger.i {
            val safeName = safeFileName(videoPath)
            "Exception extracting frames from $safeName: ${e::class.simpleName} - ${e.message ?: "No message"}"
        }
        e.printStackTrace()
    }

    Logger.i { "Returning ${out.size} timestamps" }
    out
}

fun createUrl(url: String): NSURL {
    val trimmed = url.trim()

    // Remote URLs: return as-is
    if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
        return requireNotNull(NSURL.URLWithString(trimmed)) { "Invalid http(s) URL: ${'$'}trimmed" }
    }

    // Well-formed file URLs
    if (trimmed.startsWith("file://")) {
        // Normalize to a proper file URL via its path component to avoid odd host parts
        NSURL.URLWithString(trimmed)?.let { fileUrl ->
            fileUrl.path?.let { path ->
                return NSURL.fileURLWithPath(path)
            }
            return fileUrl
        }
    }

    // Malformed file: prefix (e.g., "file:/var/..."), coerce to path
    if (trimmed.startsWith("file:")) {
        var path = trimmed.removePrefix("file:")
        // Remove duplicate leading slashes
        while (path.startsWith("//")) path = path.removePrefix("//")
        if (!path.startsWith("/")) path = "/${'$'}path"
        return NSURL.fileURLWithPath(path)
    }

    // Otherwise treat as a filesystem path
    return NSURL.fileURLWithPath(trimmed)
}

actual object VideoExtractionContext