package ac.mdiq.podcini.playback

import ac.mdiq.podcini.PodciniApp.Companion.getAppContext
import ac.mdiq.podcini.playback.base.InTheatre.bitrate
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
import ac.mdiq.podcini.playback.base.LocalMediaPlayer.Companion.exoPlayer
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.curDataSource
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCache
import ac.mdiq.podcini.storage.database.Episodes.getClipFile
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringShort
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.Loge
import ac.mdiq.podcini.util.Logt
import android.annotation.SuppressLint
import androidx.annotation.OptIn
import androidx.media3.common.Format
import androidx.media3.common.Timeline
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.exoplayer.ExoPlayer
import java.io.File
import java.io.FileOutputStream

@SuppressLint("StaticFieldLeak")
object Recorder {
    const val TAG = "Recorder"
    val context = getAppContext()

    @OptIn(UnstableApi::class)
    fun ExoPlayer.contentPositionToByte(positionMs: Long): Long? {
        val timeline = currentTimeline
        if (timeline.isEmpty) return null
        val window = Timeline.Window()
        timeline.getWindow(currentMediaItemIndex, window)
        val format = currentTracks.groups.firstOrNull { it.isSelected }?.getTrackFormat(0)
        val bitrate = format?.averageBitrate?.takeIf { it != Format.NO_VALUE } ?: return null
        return (positionMs * bitrate) / 8000 // bps to bytes
    }

    /**
     * Helper to extract bytes from an InputStream (local media).
     */
    private fun extractFromInputStream(input: java.io.InputStream, startByte: Long, bytesToRead: Long, fos: FileOutputStream) {
        val buffer = ByteArray(1024)
        var bytesRead: Int
        input.skip(startByte)
        var totalRead = 0L
        while (input.read(buffer).also { bytesRead = it } != -1 && totalRead < bytesToRead) {
            val toWrite = minOf(bytesRead.toLong(), bytesToRead - totalRead).toInt()
            fos.write(buffer, 0, toWrite)
            totalRead += toWrite
        }
        Logd(TAG, "Total written from local source: $totalRead bytes")
    }

    // Format adjustments
    fun adjustMp3Clip(bytes: ByteArray): ByteArray = bytes
    fun adjustRawAacClip(bytes: ByteArray): ByteArray = bytes

    @OptIn(UnstableApi::class)
    fun adjustOggClip(bytes: ByteArray, cache: SimpleCache, key: String, startByte: Long, endByte: Long): ByteArray {
        if (startByte > 0) {
            val headerBytes = getHeaderBytesFromCache(cache, key, 1024)
            return headerBytes?.plus(bytes) ?: bytes
        }
        return bytes
    }

    @OptIn(UnstableApi::class)
    fun adjustMp4Clip(bytes: ByteArray, cache: SimpleCache, key: String, startByte: Long, endByte: Long): ByteArray {
        if (startByte > 0 || endByte < spansTotalLength(cache, key)) {
            Logt(TAG, "MP4 clip may not be playable without re-muxing.")
            val fullFileBytes = getFullFileFromCache(cache, key)
            return fullFileBytes ?: bytes
        }
        return bytes
    }
    fun adjustLocalOggClip(bytes: ByteArray): ByteArray = bytes
    fun adjustLocalMp4Clip(bytes: ByteArray): ByteArray {
        Logt(TAG, "Local MP4 clip may not be playable without re-muxing.")
        return bytes
    }

    @OptIn(UnstableApi::class)
    fun getHeaderBytesFromCache(cache: SimpleCache, key: String, maxHeaderSize: Int): ByteArray? {
        val firstSpan = cache.getCachedSpans(key).minByOrNull { it.position } ?: return null
        if (firstSpan.position > 0 || firstSpan.file?.exists() != true) return null
        return firstSpan.file!!.inputStream().use { input ->
            val buffer = ByteArray(maxHeaderSize)
            val bytesRead = input.read(buffer, 0, maxHeaderSize)
            if (bytesRead > 0) buffer.copyOf(bytesRead) else null
        }
    }

    @OptIn(UnstableApi::class)
    fun getFullFileFromCache(cache: SimpleCache, key: String): ByteArray? {
        val spans = cache.getCachedSpans(key).sortedBy { it.position }
        if (spans.isEmpty()) return null
        val outputStream = java.io.ByteArrayOutputStream()
        spans.forEach { span -> span.file?.inputStream()?.use { it.copyTo(outputStream) } }
        return outputStream.toByteArray().takeIf { it.isNotEmpty() }
    }

    @OptIn(UnstableApi::class)
    fun spansTotalLength(cache: SimpleCache, key: String): Long = cache.getCachedSpans(key).sumOf { it.length }

    /**
     * Wrapper to handle start/stop/save with SegmentSavingDataSource
     * startPositionMs: Long? = null, // Null for stop/save
     * endPositionMs: Long? = null,   // Null for start
     */
    @OptIn(UnstableApi::class)
    fun saveClipInOriginalFormat(startPositionMs: Long, endPositionMs: Long? = null) {
        val mediaItem = exoPlayer!!.currentMediaItem ?: run {
            Loge(TAG, "No current media item.")
            return
        }
        val uri = mediaItem.localConfiguration?.uri ?: run {
            Loge(TAG, "No URI in MediaItem.")
            return
        }
        if (endPositionMs == null) {
            if (uri.scheme == "file" || uri.scheme == "content") return
            curDataSource?.startRecording(startPositionMs, bitrate, context.cacheDir)
            return
        }
        val tracks = exoPlayer!!.currentTracks
        val audioFormat = tracks.groups
            .asSequence()
            .flatMap { group -> (0 until group.length).map { group.getTrackFormat(it) } }
            .firstOrNull { it.sampleMimeType?.startsWith("audio/") == true }
        if (audioFormat == null) {
            Loge(TAG, "No audio track found.")
            return
        }
        val mimeType = audioFormat.sampleMimeType
        Logd(TAG, "mimeType: [$mimeType]")
        val ext = getFileExtensionFromMimeType(mimeType)
        if (ext == null) {
            Loge(TAG, "Audio format not supported: $ext")
            return
        }

        val startBytePlayer = exoPlayer?.contentPositionToByte(startPositionMs)
        val endBytePlayer = exoPlayer?.contentPositionToByte(endPositionMs)

        val clipname = "${getDurationStringShort(startPositionMs, false)}-${getDurationStringShort(endPositionMs, false)}.$ext"
        val outputFile = getClipFile(curEpisode!!, clipname)
        runOnIOScope {
            when {
                uri.scheme == "file" || uri.scheme == "content" -> {
                    val bytesPerSecond = bitrate / 8.0
                    val startByte = (startPositionMs * bytesPerSecond / 1000).toLong()
                    val endByte = (endPositionMs * bytesPerSecond / 1000).toLong()
                    val bytesToRead = endByte - startByte
                    val tempFile = File(outputFile.parent, "temp_segment.${outputFile.extension}")
                    FileOutputStream(tempFile).use { fos ->
                        when (uri.scheme) {
                            "file" -> File(uri.path ?: "").inputStream().use { input -> extractFromInputStream(input, startByte, bytesToRead, fos) }
                            "content" -> context.contentResolver.openInputStream(uri)?.use { input ->
                                extractFromInputStream(input, startByte, bytesToRead, fos)
                            } ?: run {
                                Loge(TAG, "Failed to open content URI: $uri")
                                return@runOnIOScope
                            }
                        }
                    }
                    val segment = tempFile.readBytes()
                    tempFile.delete()
                    if (segment.isNotEmpty()) {
                        val adjustedSegment = when (audioFormat.sampleMimeType) {
                            "audio/mp3" -> adjustMp3Clip(segment)
                            "audio/aac" -> adjustRawAacClip(segment)
                            "audio/ogg" -> adjustLocalOggClip(segment)
                            "audio/mp4" -> adjustLocalMp4Clip(segment)
                            else -> segment
                        }
                        FileOutputStream(outputFile).use { fos -> fos.write(adjustedSegment) }
                        upsert(curEpisode!!) { it.clips.add(clipname) }
                        Logd(TAG, "Saved local clip to: ${outputFile.absolutePath}")
                    } else Loge(TAG, "Failed to extract segment from local media")
                }
                else -> {
                    val tempFileDS = curDataSource?.stopRecording(endPositionMs)
                    val cache = getCache(context)
                    val bytesPerSecond = bitrate / 8.0
                    val startByte = startBytePlayer ?: (startPositionMs * bytesPerSecond / 1000).toLong()
                    val endByte = endBytePlayer ?: (endPositionMs * bytesPerSecond / 1000).toLong()
                    val bytesToRead = endByte - startByte
                    val key = curEpisode!!.id.toString()
                    val cacheSpan = cache.getCachedSpans(key).firstOrNull { span -> span.position <= startByte && (span.position + span.length) >= endByte }
                    Logd(TAG, "cacheSpan found: ${cacheSpan != null}")
                    if (cacheSpan?.file?.exists() == true) {
                        val tempFile = File(outputFile.parent, "temp_segment.${outputFile.extension}")
                        FileOutputStream(tempFile).use { fos ->
                            cacheSpan.file!!.inputStream().use { input ->
                                val buffer = ByteArray(1024)
                                var bytesRead: Int
                                val bytesToSkip = if (startByte >= cacheSpan.position) startByte - cacheSpan.position else 0L
                                Logd(TAG, "Cache span: pos=${cacheSpan.position}, len=${cacheSpan.length}")
                                Logd(TAG, "Skipping $bytesToSkip, reading $bytesToRead")
                                input.skip(bytesToSkip)
                                var totalRead = 0L
                                while (input.read(buffer).also { bytesRead = it } != -1 && totalRead < bytesToRead) {
                                    val toWrite = minOf(bytesRead.toLong(), bytesToRead - totalRead).toInt()
                                    fos.write(buffer, 0, toWrite)
                                    totalRead += toWrite
                                }
                                Logd(TAG, "Total written: $totalRead bytes")
                            }
                        }
                        val segment = tempFile.readBytes()
                        tempFile.delete()
                        if (segment.isNotEmpty()) {
                            val adjustedSegment = when (audioFormat.sampleMimeType) {
                                "audio/mp3" -> adjustMp3Clip(segment)
                                "audio/aac" -> adjustRawAacClip(segment)
                                "audio/ogg" -> adjustOggClip(segment, cache, key, startByte, endByte)
                                "audio/mp4" -> adjustMp4Clip(segment, cache, key, startByte, endByte)
                                else -> segment
                            }
                            FileOutputStream(outputFile).use { fos -> fos.write(adjustedSegment) }
                            upsert(curEpisode!!) { it.clips.add(clipname) }
                            Logd(TAG, "Saved cached segment of ${(endPositionMs - startPositionMs) / 1000} seconds to: ${outputFile.absolutePath}")
                            return@runOnIOScope
                        } else Logd(TAG, "Failed to extract segment from cache")
                    }
                    Logd(TAG, "Single span not found for range $startByte to $endByte or failed to extract segment from cache. Attempting full extraction.")
                    val fullBytes = getFullFileFromCache(cache, key)
                    if (fullBytes != null && audioFormat.sampleMimeType == "audio/mp4") {
                        FileOutputStream(outputFile).use { fos -> fos.write(fullBytes) }
                        upsert(curEpisode!!) { it.clips.add(clipname) }
                        Logd(TAG, "Saved full MP4 file to: ${outputFile.absolutePath} (re-muxing needed for partial clip)")
                        return@runOnIOScope
                    }
                    if (tempFileDS != null) {
                        Logd(TAG, "Segment not available in cache or full file extraction. Trying with player extract")
                        val bytesPerSecond = bitrate / 8.0
                        val startByte = (startPositionMs * bytesPerSecond / 1000).toLong()
                        val endByte = (endPositionMs * bytesPerSecond / 1000).toLong()
                        val bytesToRead = endByte - startByte
                        val tempOutput = File(outputFile.parent, "temp_segment.${outputFile.extension}")
                        FileOutputStream(tempOutput).use { fos ->
                            tempFileDS.inputStream().use { input ->
                                val buffer = ByteArray(1024)
                                var bytesRead: Int
//                                input.skip(startByte)
                                var totalRead = 0L
                                while (input.read(buffer).also { bytesRead = it } != -1 && totalRead < bytesToRead) {
                                    val toWrite = minOf(bytesRead.toLong(), bytesToRead - totalRead).toInt()
                                    fos.write(buffer, 0, toWrite)
                                    totalRead += toWrite
                                }
                                Logd(TAG, "Extracted $totalRead bytes from temp file")
                            }
                        }
                        val segment = tempOutput.readBytes()
                        tempOutput.delete()
                        if (segment.isNotEmpty()) {
                            val adjustedSegment = when (audioFormat.sampleMimeType) {
                                "audio/mp3" -> adjustMp3Clip(segment)
                                "audio/aac" -> adjustRawAacClip(segment)
                                "audio/ogg" -> adjustOggClip(segment, cache, key, startByte, endByte)
                                "audio/mp4" -> adjustMp4Clip(segment, cache, key, startByte, endByte)
                                else -> segment
                            }
                            FileOutputStream(outputFile).use { fos -> fos.write(adjustedSegment) }
                            upsert(curEpisode!!) { it.clips.add(clipname) }
                            Logd(TAG, "Saved clip to: ${outputFile.absolutePath}")
                        } else Loge(TAG, "Failed to extract segment from temp file")
                        tempFileDS.delete()
                    } else Loge(TAG, "Failed saving clip: No temp file available after stopping recording")
                }
            }
        }
    }

    fun getFileExtensionFromMimeType(mimeType: String?): String? {
        return when (mimeType) {
            "audio/mp3", "audio/mpeg" -> "mp3"
            "audio/aac" -> "aac"
            "audio/mp4", "audio/mp4a-latm" -> "m4a"
            "audio/ogg" -> "ogg"
            else -> null
        }
    }
}