package com.smplayer.media3_engine

import android.content.Context
import android.os.Handler
import android.view.Surface
import android.view.View
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.Tracks
import androidx.media3.common.VideoSize
import androidx.media3.common.text.CueGroup
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DataSpec
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.TransferListener
import androidx.media3.ui.SubtitleView
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.analytics.AnalyticsListener
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlin.math.max
import kotlin.math.min


internal class Media3PlayerInstance(
    val id: Int,
    private val context: Context,
    private val mainHandler: Handler,
    private val emit: (Map<String, Any?>) -> Unit,
) {

    private var player: ExoPlayer? = null


    private var surface: Surface? = null
    private var attachedView: Media3AspectRatioFrameLayout? = null
    private var listener: Player.Listener? = null
    private var positionRunnable: Runnable? = null
    private var pendingOpenListener: Player.Listener? = null
    private var pendingOpenResult: MethodChannel.Result? = null
    private var pendingOpenGen: Int = -1
    private var pendingSurfaceTimeout: Runnable? = null
    private var currentResizeMode: Int = Media3AspectRatioFrameLayout.RESIZE_FIT
    private var lastAspectRatio: Float = 0f


    private var attachedSubtitleView: SubtitleView? = null
    private var lastCueGroup: CueGroup? = null


    private var attachedShutterView: View? = null
    private var firstFrameRendered: Boolean = false


    private var generation: Int = 0
    private var disposed: Boolean = false
    private var ffmpegAvailable: Boolean = false
    private var ffmpegVersion: String? = null

    @Volatile
    private var totalRxBytes: Long = 0L


    private var pendingPrepareGen: Int = -1


    private var lastDurationMs: Long = 0L
    private var lastVideoWidth: Int = 0
    private var lastVideoHeight: Int = 0
    private var lastTracksPayload: Map<String, Any?>? = null
    private var lastPlaying: Boolean = false
    private var lastPositionMs: Long = 0L


    fun create(): Map<String, Any?> {
        ffmpegAvailable = probeFfmpegAvailability()
        ffmpegVersion = probeFfmpegVersion()

        val renderersFactory = DefaultRenderersFactory(context)
            .setExtensionRendererMode(
                DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
            )


            .setEnableDecoderFallback(true)
        val loadControl = DefaultLoadControl.Builder()
            .setBufferDurationsMs(
                15_000,
                50_000,
                500,
                2_000,
            )
            .build()
        val exo = ExoPlayer.Builder(context, renderersFactory)
            .setLoadControl(loadControl)
            .build()
        exo.playWhenReady = false


        exo.addAnalyticsListener(object : AnalyticsListener {
            override fun onVideoDecoderInitialized(
                eventTime: AnalyticsListener.EventTime,
                decoderName: String,
                initializedTimestampMs: Long,
                initializationDurationMs: Long,
            ) {
                android.util.Log.i(TAG, "video decoder initialized: $decoderName")
            }
        })
        player = exo


        return mapOf(
            "playerId" to id,
            "ffmpegAvailable" to ffmpegAvailable,
            "ffmpegVersion" to (ffmpegVersion ?: ""),
        )
    }


    fun attachSurface(s: Surface) {
        surface = s
        player?.setVideoSurface(s)


        val pendingGen = pendingPrepareGen
        if (pendingGen >= 0 && pendingGen == generation) {
            pendingPrepareGen = -1
            clearPendingSurfaceTimeout()
            android.util.Log.i(TAG, "deferred prepare: surface arrived, executing prepare (gen=$pendingGen)")
            player?.prepare()
        }
    }

    fun attachView(view: Media3AspectRatioFrameLayout) {
        attachedView = view
        view.setResizeMode(currentResizeMode)
        view.setAspectRatio(lastAspectRatio)
    }

    fun detachView(view: Media3AspectRatioFrameLayout) {
        if (attachedView === view) attachedView = null
    }


    fun detachSurface() {
        surface = null
        try {
            player?.clearVideoSurface()
        } catch (t: Throwable) {
            android.util.Log.w(TAG, "clearVideoSurface failed", t)
        }
    }

    fun attachSubtitleView(view: SubtitleView) {
        attachedSubtitleView = view
        lastCueGroup?.let { group ->
            val hasBitmap = group.cues.any { it.bitmap != null }
            if (hasBitmap) {
                view.setCues(group.cues)
            } else {
                view.setCues(emptyList())
            }
        }
    }

    fun detachSubtitleView(view: SubtitleView) {
        if (attachedSubtitleView === view) attachedSubtitleView = null
    }


    fun attachShutter(view: View) {
        attachedShutterView = view
        view.visibility = if (firstFrameRendered) View.GONE else View.VISIBLE
    }


    fun detachShutter(view: View) {
        if (attachedShutterView === view) attachedShutterView = null
    }


    private fun attachListener(exo: ExoPlayer, openGen: Int) {
        val lst = object : Player.Listener {
            private fun expired(): Boolean {
                if (disposed || openGen != generation) {
                    try { exo.removeListener(this) } catch (_: Throwable) {}
                    if (listener === this) listener = null
                    return true
                }
                return false
            }

            override fun onPlaybackStateChanged(playbackState: Int) {
                if (expired()) return
                when (playbackState) {
                    Player.STATE_READY -> {
                        val dur = exo.duration
                        if (dur != C.TIME_UNSET) {
                            lastDurationMs = dur
                            emitTyped("duration", mapOf("durationMs" to dur), openGen)
                        }
                    }
                    Player.STATE_ENDED -> emitTyped("completed", emptyMap(), openGen)
                    Player.STATE_BUFFERING, Player.STATE_IDLE -> Unit
                }
            }

            override fun onIsPlayingChanged(isPlaying: Boolean) {
                if (expired()) return
                lastPlaying = isPlaying
                emitTyped("playing", mapOf("playing" to isPlaying), openGen)
                if (isPlaying) startPositionTimer(openGen) else stopPositionTimer()
            }

            override fun onVideoSizeChanged(videoSize: VideoSize) {
                if (expired()) return
                val w = videoSize.width
                val h = videoSize.height
                if (w <= 0 || h <= 0) return
                val pixelWidthHeightRatio = videoSize.pixelWidthHeightRatio
                lastAspectRatio = if (pixelWidthHeightRatio > 0f) {
                    w * pixelWidthHeightRatio / h
                } else {
                    0f
                }
                attachedView?.setAspectRatio(lastAspectRatio)
                lastVideoWidth = w
                lastVideoHeight = h

                emitTyped("videoSize", mapOf("width" to w, "height" to h), openGen)
            }

            override fun onRenderedFirstFrame() {
                if (expired()) return
                android.util.Log.i(TAG, "rendered first frame (gen=$openGen)")
                firstFrameRendered = true
                attachedShutterView?.visibility = View.GONE
            }

            override fun onTracksChanged(tracks: Tracks) {
                if (expired()) return
                val payload = serializeTracks(tracks)
                lastTracksPayload = payload
                emitTyped("tracks", payload, openGen)
            }

            override fun onCues(cueGroup: CueGroup) {
                if (expired()) return
                lastCueGroup = cueGroup
                val cues = cueGroup.cues
                var bitmapDetected = false
                for (cue in cues) {
                    if (cue.bitmap != null) {
                        bitmapDetected = true
                        break
                    }
                }
                if (bitmapDetected) {


                    attachedSubtitleView?.setCues(cues)
                    emitTyped("subtitle", mapOf("lines" to emptyList<String>()), openGen)
                    return
                }

                attachedSubtitleView?.setCues(emptyList())
                val lines = ArrayList<String>(cues.size)
                for (cue in cues) {
                    val text = cue.text?.toString()
                    if (!text.isNullOrEmpty()) lines.add(text)
                }
                emitTyped("subtitle", mapOf("lines" to lines), openGen)
            }

            override fun onPlayerError(error: PlaybackException) {
                if (expired()) return
                emitTyped(
                    "error",
                    mapOf(
                        "code" to (error.errorCodeName ?: "playback_error"),
                        "message" to (error.message ?: ""),
                    ),
                    openGen,
                )
            }
        }

        listener?.let { try { exo.removeListener(it) } catch (_: Throwable) {} }
        listener = lst
        exo.addListener(lst)
    }


    fun open(call: MethodCall, result: MethodChannel.Result) {
        val exo = player ?: run {
            result.error("not_created", "player not created", null)
            return
        }
        val url = call.argument<String>("url") ?: run {
            result.error("missing_url", "open without url", null)
            return
        }


        val scheme = try { android.net.Uri.parse(url).scheme?.lowercase() } catch (_: Throwable) { null }
        if (scheme != "http" && scheme != "https" && scheme != "file") {
            result.error(
                "invalid_url_scheme",
                "media3 only accepts http/https/file urls; got: $scheme",
                null,
            )
            return
        }
        val headersRaw = call.argument<Map<String, String>>("headers") ?: emptyMap()
        val startMs = (call.argument<Number>("startMs"))?.toLong() ?: 0L
        val play = call.argument<Boolean>("play") ?: true

        cancelPendingOpen("open_cancelled", "open superseded before completion")


        val dartGen = (call.argument<Number>("generation"))?.toInt() ?: -1
        generation = max(generation + 1, dartGen)
        val openGen = generation


        lastDurationMs = 0L
        lastVideoWidth = 0
        lastVideoHeight = 0
        lastAspectRatio = 0f
        attachedView?.setAspectRatio(0f)
        lastTracksPayload = null
        lastPositionMs = 0L
        lastPlaying = false
        lastCueGroup = null
        attachedSubtitleView?.setCues(emptyList())


        firstFrameRendered = false
        attachedShutterView?.visibility = View.VISIBLE


        val once = object : Player.Listener {
            private var settled = false

            fun settle(success: Boolean, error: PlaybackException?) {
                if (settled) return
                if (pendingOpenListener !== this) {
                    settled = true
                    return
                }
                settled = true
                try { exo.removeListener(this) } catch (_: Throwable) {}
                pendingOpenListener = null
                pendingOpenResult = null
                pendingOpenGen = -1
                pendingPrepareGen = -1
                clearPendingSurfaceTimeout()
                if (openGen != generation) {
                    result.error("open_cancelled", "open cancelled before completion", null)
                    return
                }
                if (disposed) {
                    result.error("open_cancelled", "player disposed before open completed", null)
                    return
                }
                if (success) {
                    result.success(null)
                } else {
                    result.error(
                        "open_failed",
                        error?.message ?: "playback error",
                        error?.errorCodeName,
                    )
                }
            }

            override fun onPlaybackStateChanged(playbackState: Int) {
                if (playbackState == Player.STATE_READY) settle(true, null)
            }

            override fun onPlayerError(error: PlaybackException) {
                settle(false, error)
            }
        }
        exo.addListener(once)
        pendingOpenListener = once
        pendingOpenResult = result
        pendingOpenGen = openGen


        attachListener(exo, openGen)


        exo.stop()
        exo.clearMediaItems()

        totalRxBytes = 0L
        val transferListener = object : TransferListener {
            override fun onTransferInitializing(source: DataSource, dataSpec: DataSpec, isNetwork: Boolean) {}
            override fun onTransferStart(source: DataSource, dataSpec: DataSpec, isNetwork: Boolean) {}
            override fun onBytesTransferred(
                source: DataSource,
                dataSpec: DataSpec,
                isNetwork: Boolean,
                bytesTransferred: Int,
            ) {
                if (isNetwork) totalRxBytes += bytesTransferred.toLong()
            }
            override fun onTransferEnd(source: DataSource, dataSpec: DataSpec, isNetwork: Boolean) {}
        }
        val httpFactory = DefaultHttpDataSource.Factory()
            .setAllowCrossProtocolRedirects(true)
            .setDefaultRequestProperties(sanitizeHeaders(headersRaw))
        val dataSourceFactory = DefaultDataSource.Factory(context, httpFactory)
            .setTransferListener(transferListener)
        val mediaSourceFactory = DefaultMediaSourceFactory(dataSourceFactory)
        exo.setMediaSource(mediaSourceFactory.createMediaSource(MediaItem.fromUri(url)))

        if (startMs > 0L) exo.seekTo(startMs)
        exo.playWhenReady = play


        if (surface != null) {
            pendingPrepareGen = -1
            clearPendingSurfaceTimeout()
            exo.prepare()
        } else {
            android.util.Log.i(TAG, "open: surface not ready, deferring prepare (gen=$openGen)")
            pendingPrepareGen = openGen
            scheduleSurfaceTimeout(openGen)
        }


        if (exo.playerError != null) {
            once.settle(false, exo.playerError)
        } else if (exo.playbackState == Player.STATE_READY) {
            once.settle(true, null)
        }
    }

    fun play() = player?.let { it.playWhenReady = true }
    fun pause() = player?.let { it.playWhenReady = false }

    fun stop(generationFromDart: Int) {

        if (generationFromDart >= 0) generation = max(generation, generationFromDart)
        cancelPendingOpen("open_cancelled", "open cancelled by stop")
        player?.stop()
        stopPositionTimer()
        lastCueGroup = null
        attachedSubtitleView?.setCues(emptyList())
    }

    fun seek(positionMs: Long) {
        player?.seekTo(positionMs)
        lastPositionMs = positionMs
        emitTyped("position", mapOf("positionMs" to positionMs), generation)
    }

    fun setRate(rate: Float) {
        player?.let { it.setPlaybackSpeed(max(0.1f, rate)) }
    }

    fun setVolume(volume0to200: Float) {
        val mapped = min(1f, max(0f, volume0to200 / 100f))
        player?.volume = mapped

    }

    fun setResizeMode(mode: Int) {
        currentResizeMode = mode
        attachedView?.setResizeMode(mode)
    }


    fun setAudioTrack(id: String) {
        applyTrackOverride(C.TRACK_TYPE_AUDIO, id)
    }

    fun setSubtitleTrack(id: String) {
        applyTrackOverride(C.TRACK_TYPE_TEXT, id)
    }

    private fun applyTrackOverride(trackType: @C.TrackType Int, id: String) {
        val exo = player ?: return
        val builder = exo.trackSelectionParameters.buildUpon()
            .clearOverridesOfType(trackType)
            .setTrackTypeDisabled(trackType, false)
        when (id) {
            "auto" -> {} 
            "no" -> builder.setTrackTypeDisabled(trackType, true)
            else -> {

                val parts = id.split(":")
                if (parts.size != 3) return
                val groupIdx = parts[1].toIntOrNull() ?: return
                val trackIdx = parts[2].toIntOrNull() ?: return
                val tracks = exo.currentTracks
                val typedGroups = tracks.groups.filter { it.type == trackType }
                val target = typedGroups.getOrNull(groupIdx) ?: return
                if (trackIdx !in 0 until target.length) return
                builder.addOverride(
                    TrackSelectionOverride(target.mediaTrackGroup, trackIdx)
                )
            }
        }
        exo.trackSelectionParameters = builder.build()
    }

    fun getProperty(name: String): Any {
        return when (name) {
            "ffmpegAvailable" -> if (ffmpegAvailable) "true" else "false"
            "ffmpegVersion" -> ffmpegVersion.orEmpty()
            "audioRenderer" -> currentAudioRendererName()
            "playbackState" -> playbackStateName()
            "totalRxBytes" -> totalRxBytes
            else -> ""
        }
    }

    private fun currentAudioRendererName(): String {
        val exo = player ?: return ""
        return try {
            for (i in 0 until exo.rendererCount) {
                if (exo.getRendererType(i) == C.TRACK_TYPE_AUDIO) {

                    val renderer = exo.javaClass.getMethod("getRenderer", Int::class.java)
                        .invoke(exo, i)
                    if (renderer != null) return renderer.javaClass.simpleName
                }
            }
            ""
        } catch (t: Throwable) {
            ""
        }
    }

    private fun playbackStateName(): String {
        val state = player?.playbackState ?: return ""
        return when (state) {
            Player.STATE_IDLE -> "idle"
            Player.STATE_BUFFERING -> "buffering"
            Player.STATE_READY -> "ready"
            Player.STATE_ENDED -> "ended"
            else -> "unknown"
        }
    }

    fun dispose() {
        if (disposed) return
        cancelPendingOpen("open_cancelled", "player disposed before open completed")
        disposed = true
        ++generation
        stopPositionTimer()
        try {
            listener?.let { player?.removeListener(it) }
        } catch (t: Throwable) {
            android.util.Log.w(TAG, "removeListener failed", t)
        }
        listener = null
        try {
            player?.clearVideoSurface()
        } catch (t: Throwable) {
            android.util.Log.w(TAG, "clearVideoSurface failed", t)
        }
        try {
            player?.release()
        } catch (t: Throwable) {
            android.util.Log.w(TAG, "player.release failed", t)
        }
        player = null

        surface = null
        attachedView = null
        attachedSubtitleView = null
    }


    fun replayKeyState() {
        if (disposed) return
        val gen = generation
        if (lastDurationMs > 0) emitTyped("duration", mapOf("durationMs" to lastDurationMs), gen)
        if (lastVideoWidth > 0 && lastVideoHeight > 0) {
            emitTyped(
                "videoSize",
                mapOf("width" to lastVideoWidth, "height" to lastVideoHeight),
                gen,
            )
        }
        lastTracksPayload?.let { emitTyped("tracks", it, gen) }
        emitTyped("playing", mapOf("playing" to lastPlaying), gen)
        emitTyped("position", mapOf("positionMs" to lastPositionMs), gen)
    }


    private fun emitTyped(type: String, payload: Map<String, Any?>, gen: Int) {
        val merged = HashMap<String, Any?>(payload.size + 3)
        merged["playerId"] = id
        merged["generation"] = gen
        merged["type"] = type
        merged.putAll(payload)
        emit(merged)
    }


    private fun sanitizeHeaders(headers: Map<String, String>): Map<String, String> {
        val sanitized = HashMap<String, String>(headers.size)
        for ((k, v) in headers) {
            if (k.contains('\n') || k.contains('\r')) continue
            if (v.contains('\n') || v.contains('\r')) continue
            sanitized[k] = v
        }
        return sanitized
    }

    private fun startPositionTimer(openGen: Int) {
        stopPositionTimer()
        val r = object : Runnable {
            override fun run() {
                if (disposed || openGen != generation) return
                val exo = player ?: return
                val pos = exo.currentPosition
                val buf = exo.bufferedPosition
                if (pos != lastPositionMs) {
                    lastPositionMs = pos
                    emitTyped(
                        "position",
                        mapOf("positionMs" to pos, "bufferMs" to buf),
                        openGen,
                    )
                }
                mainHandler.postDelayed(this, POSITION_INTERVAL_MS)
            }
        }
        positionRunnable = r
        mainHandler.postDelayed(r, POSITION_INTERVAL_MS)
    }

    private fun stopPositionTimer() {
        positionRunnable?.let { mainHandler.removeCallbacks(it) }
        positionRunnable = null
    }

    private fun scheduleSurfaceTimeout(openGen: Int) {
        clearPendingSurfaceTimeout()
        val r = Runnable {
            if (disposed || openGen != generation || pendingPrepareGen != openGen) return@Runnable
            android.util.Log.w(TAG, "deferred prepare timed out waiting for surface (gen=$openGen)")
            cancelPendingOpen("surface_timeout", "surface not attached within timeout")
        }
        pendingSurfaceTimeout = r
        mainHandler.postDelayed(r, SURFACE_ATTACH_TIMEOUT_MS)
    }

    private fun clearPendingSurfaceTimeout() {
        pendingSurfaceTimeout?.let { mainHandler.removeCallbacks(it) }
        pendingSurfaceTimeout = null
    }

    private fun cancelPendingOpen(code: String, message: String) {
        clearPendingSurfaceTimeout()
        pendingPrepareGen = -1
        val once = pendingOpenListener
        val result = pendingOpenResult
        pendingOpenListener = null
        pendingOpenResult = null
        pendingOpenGen = -1
        if (once != null) {
            try { player?.removeListener(once) } catch (_: Throwable) {}
        }
        if (result != null) {
            try {
                result.error(code, message, null)
            } catch (t: Throwable) {
                android.util.Log.w(TAG, "complete pending open failed", t)
            }
        }
    }


    private fun serializeTracks(tracks: Tracks): Map<String, Any?> {
        val audio = ArrayList<Map<String, Any?>>()
        val subtitle = ArrayList<Map<String, Any?>>()
        var audioGroupIdx = 0
        var textGroupIdx = 0
        for (group in tracks.groups) {
            when (group.type) {
                C.TRACK_TYPE_AUDIO -> {
                    val gi = audioGroupIdx++
                    for (ti in 0 until group.length) {
                        if (!group.isTrackSupported(ti)) continue
                        val fmt = group.getTrackFormat(ti)
                        audio.add(
                            mapOf(
                                "id" to "audio:$gi:$ti",
                                "index" to ti,
                                "title" to (fmt.label ?: ""),
                                "language" to (fmt.language ?: ""),
                            )
                        )
                    }
                }
                C.TRACK_TYPE_TEXT -> {
                    val gi = textGroupIdx++
                    for (ti in 0 until group.length) {
                        if (!group.isTrackSupported(ti)) continue
                        val fmt = group.getTrackFormat(ti)
                        subtitle.add(
                            mapOf(
                                "id" to "text:$gi:$ti",
                                "index" to ti,
                                "title" to (fmt.label ?: ""),
                                "language" to (fmt.language ?: ""),
                            )
                        )
                    }
                }
            }
        }
        return mapOf("audio" to audio, "subtitle" to subtitle)
    }

    private fun probeFfmpegAvailability(): Boolean {

        return try {
            val cls = Class.forName("androidx.media3.decoder.ffmpeg.FfmpegLibrary")
            cls.getMethod("isAvailable").invoke(null) as? Boolean ?: false
        } catch (t: Throwable) {
            false
        }
    }

    private fun probeFfmpegVersion(): String? {
        return try {
            val cls = Class.forName("androidx.media3.decoder.ffmpeg.FfmpegLibrary")
            cls.getMethod("getVersion").invoke(null) as? String
        } catch (t: Throwable) {
            null
        }
    }

    companion object {
        private const val TAG = "Media3PlayerInstance"
        private const val POSITION_INTERVAL_MS = 500L
        private const val SURFACE_ATTACH_TIMEOUT_MS = 10_000L
    }
}
