package com.edde746.plezy.exoplayer

import android.app.Activity
import android.app.ActivityManager
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.edde746.plezy.mpv.MpvPlayerCore
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel

class ExoPlayerPlugin :
  FlutterPlugin,
  MethodChannel.MethodCallHandler,
  EventChannel.StreamHandler,
  ActivityAware,
  ExoPlayerDelegate {

  companion object {
    private const val TAG = "ExoPlayerPlugin"
    private const val METHOD_CHANNEL = "com.plezy/exo_player"
    private const val EVENT_CHANNEL = "com.plezy/exo_player/events"
  }

  private lateinit var methodChannel: MethodChannel
  private lateinit var eventChannel: EventChannel
  private var eventSink: EventChannel.EventSink? = null
  private var playerCore: ExoPlayerCore? = null
  private var mpvCore: MpvPlayerCore? = null // MPV fallback player
  private var usingMpvFallback: Boolean = false
  private var fallbackInProgress: Boolean = false
  private var activity: Activity? = null
  private var activityBinding: ActivityPluginBinding? = null
  private val nameToId = mutableMapOf<String, Int>()
  private val mainHandler = Handler(Looper.getMainLooper())
  private var configuredBufferSizeBytes: Int? = null

  private var sessionGeneration = 0
  private var debugLoggingEnabled: Boolean = false
  private val pendingMpvProperties = mutableListOf<Pair<String, String>>()

  // FlutterPlugin

  override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    methodChannel = MethodChannel(binding.binaryMessenger, METHOD_CHANNEL)
    methodChannel.setMethodCallHandler(this)

    eventChannel = EventChannel(binding.binaryMessenger, EVENT_CHANNEL)
    eventChannel.setStreamHandler(this)

    Log.d(TAG, "Attached to engine")
  }

  override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    methodChannel.setMethodCallHandler(null)
    eventChannel.setStreamHandler(null)
    Log.d(TAG, "Detached from engine")
  }

  // ActivityAware

  override fun onAttachedToActivity(binding: ActivityPluginBinding) {
    activity = binding.activity
    activityBinding = binding
    Log.d(TAG, "Attached to activity")
  }

  override fun onDetachedFromActivity() {
    sessionGeneration++
    playerCore?.dispose()
    playerCore = null
    mpvCore?.dispose()
    mpvCore = null
    usingMpvFallback = false
    fallbackInProgress = false
    activity = null
    activityBinding = null
    Log.d(TAG, "Detached from activity")
  }

  override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
    activity = binding.activity
    activityBinding = binding
    Log.d(TAG, "Reattached to activity for config changes")
  }

  override fun onDetachedFromActivityForConfigChanges() {
    sessionGeneration++
    fallbackInProgress = false
    activity = null
    activityBinding = null
    Log.d(TAG, "Detached from activity for config changes")
  }

  // EventChannel.StreamHandler

  override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
    eventSink = events
    Log.d(TAG, "Event stream connected")
  }

  override fun onCancel(arguments: Any?) {
    eventSink = null
    Log.d(TAG, "Event stream disconnected")
  }

  // MethodChannel.MethodCallHandler

  override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
    when (call.method) {
      "initialize" -> handleInitialize(call, result)
      "dispose" -> handleDispose(result)
      "open" -> handleOpen(call, result)
      "play" -> handlePlay(result)
      "pause" -> handlePause(result)
      "stop" -> handleStop(result)
      "seek" -> handleSeek(call, result)
      "setVolume" -> handleSetVolume(call, result)
      "setRate" -> handleSetRate(call, result)
      "selectAudioTrack" -> handleSelectAudioTrack(call, result)
      "selectSubtitleTrack" -> handleSelectSubtitleTrack(call, result)
      "addSubtitleTrack" -> handleAddSubtitleTrack(call, result)
      "setVisible" -> handleSetVisible(call, result)
      "updateFrame" -> handleUpdateFrame(result)
      "setVideoFrameRate" -> handleSetVideoFrameRate(call, result)
      "clearVideoFrameRate" -> handleClearVideoFrameRate(result)
      "requestAudioFocus" -> handleRequestAudioFocus(result)
      "abandonAudioFocus" -> handleAbandonAudioFocus(result)
      "isInitialized" -> result.success(
        if (usingMpvFallback) {
          mpvCore?.isInitialized ?: false
        } else {
          playerCore?.isInitialized ?: false
        }
      )
      "getStats" -> handleGetStats(result)
      "getPlayerType" -> result.success(if (usingMpvFallback) "mpv" else "exoplayer")
      "getHeapSize" -> {
        val am = activity?.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
        result.success(am?.largeMemoryClass ?: 0)
      }
      "setSubtitleStyle" -> handleSetSubtitleStyle(call, result)
      "setBoxFitMode" -> handleSetBoxFitMode(call, result)
      "setDvConversionMode" -> handleSetDvConversionMode(call, result)
      "observeProperty" -> handleObserveProperty(call, result)
      "setMpvProperty" -> handleSetMpvProperty(call, result)
      "setLogLevel" -> {
        val level = call.argument<String>("level") ?: "warn"
        debugLoggingEnabled = (level == "v" || level == "debug" || level == "trace")
        playerCore?.debugLoggingEnabled = debugLoggingEnabled
        result.success(null)
      }
      "triggerFallback" -> {
        playerCore?.triggerFallback()
        result.success(null)
      }
      else -> result.notImplemented()
    }
  }

  private fun handleInitialize(call: MethodCall, result: MethodChannel.Result) {
    val currentActivity = activity
    if (currentActivity == null) {
      result.error("NO_ACTIVITY", "Activity not available", null)
      return
    }

    if (playerCore?.isInitialized == true) {
      Log.d(TAG, "Already initialized")
      result.success(true)
      return
    }

    val bufferSizeBytes = call.argument<Int>("bufferSizeBytes")
    val tunnelingEnabled = call.argument<Boolean>("tunnelingEnabled") ?: true
    configuredBufferSizeBytes = bufferSizeBytes

    currentActivity.runOnUiThread {
      sessionGeneration++

      if (mpvCore != null || fallbackInProgress) {
        mpvCore?.dispose()
        mpvCore = null
        usingMpvFallback = false
        fallbackInProgress = false
      }

      try {
        playerCore = ExoPlayerCore(currentActivity).apply {
          delegate = this@ExoPlayerPlugin
          this.debugLoggingEnabled = this@ExoPlayerPlugin.debugLoggingEnabled
        }
        val success = playerCore?.initialize(
          bufferSizeBytes = bufferSizeBytes,
          tunnelingEnabled = tunnelingEnabled
        ) ?: false

        // Start hidden
        playerCore?.setVisible(false)

        Log.d(TAG, "Initialized: $success")
        result.success(success)
      } catch (e: Exception) {
        Log.e(TAG, "Failed to initialize: ${e.message}", e)
        result.error("INIT_FAILED", e.message, null)
      }
    }
  }

  private fun handleDispose(result: MethodChannel.Result) {
    activity?.runOnUiThread {
      sessionGeneration++
      playerCore?.dispose()
      playerCore = null
      mpvCore?.dispose()
      mpvCore = null
      usingMpvFallback = false
      fallbackInProgress = false
      Log.d(TAG, "Disposed")
      result.success(null)
    } ?: result.success(null)
  }

  @Suppress("UNCHECKED_CAST")
  private fun handleOpen(call: MethodCall, result: MethodChannel.Result) {
    val uri = call.argument<String>("uri")
    val headers = call.argument<Map<String, String>>("headers")
    val startPositionMs = call.argument<Number>("startPositionMs")?.toLong() ?: 0L
    val autoPlay = call.argument<Boolean>("autoPlay") ?: true
    val isLive = call.argument<Boolean>("isLive") ?: false
    val externalSubtitles = call.argument<List<Map<String, String?>>>("externalSubtitles")

    if (uri == null) {
      result.error("INVALID_ARGS", "Missing 'uri'", null)
      return
    }

    // Only clear pending MPV properties when MPV is the active backend.
    // When ExoPlayer is active, keep them for potential ExoPlayer→MPV fallback.
    if (usingMpvFallback) {
      pendingMpvProperties.clear()
    }

    activity?.runOnUiThread {
      if (usingMpvFallback) {
        // MPV: Build loadfile command with options
        val startSeconds = startPositionMs / 1000.0
        val options = mutableListOf<String>()
        options.add("start=$startSeconds")
        if (!autoPlay) options.add("pause=yes")
        headers?.forEach { (key, value) ->
          options.add("http-header-fields-append=$key: $value")
        }
        val optionsStr = options.joinToString(",")
        // Convert content:// URIs to fdclose:// for MPV (SAF SD card downloads)
        val mpvUri = openContentFd(uri)?.let { "fdclose://$it" } ?: uri
        mpvCore?.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))
      } else {
        playerCore?.open(uri, headers, startPositionMs, autoPlay, isLive, externalSubtitles)
      }
      result.success(null)
    } ?: result.error("NO_ACTIVITY", "Activity not available", null)
  }

  private fun handlePlay(result: MethodChannel.Result) {
    activity?.runOnUiThread {
      if (usingMpvFallback) {
        mpvCore?.setProperty("pause", "no")
      } else {
        playerCore?.play()
      }
      result.success(null)
    } ?: result.success(null)
  }

  private fun handlePause(result: MethodChannel.Result) {
    activity?.runOnUiThread {
      if (usingMpvFallback) {
        mpvCore?.setProperty("pause", "yes")
      } else {
        playerCore?.pause()
      }
      result.success(null)
    } ?: result.success(null)
  }

  private fun handleStop(result: MethodChannel.Result) {
    activity?.runOnUiThread {
      if (usingMpvFallback) {
        mpvCore?.command(arrayOf("stop"))
        mpvCore?.setVisible(false)
      } else {
        playerCore?.stop()
      }
      result.success(null)
    } ?: result.success(null)
  }

  private fun handleSeek(call: MethodCall, result: MethodChannel.Result) {
    val positionMs = call.argument<Number>("positionMs")?.toLong()

    if (positionMs == null) {
      result.error("INVALID_ARGS", "Missing 'positionMs'", null)
      return
    }

    activity?.runOnUiThread {
      if (usingMpvFallback) {
        val positionSeconds = positionMs / 1000.0
        mpvCore?.command(arrayOf("seek", positionSeconds.toString(), "absolute"))
      } else {
        playerCore?.seekTo(positionMs)
      }
      result.success(null)
    } ?: result.success(null)
  }

  private fun handleSetVolume(call: MethodCall, result: MethodChannel.Result) {
    val volume = call.argument<Number>("volume")?.toFloat()

    if (volume == null) {
      result.error("INVALID_ARGS", "Missing 'volume'", null)
      return
    }

    activity?.runOnUiThread {
      if (usingMpvFallback) {
        mpvCore?.setProperty("volume", volume.toString())
      } else {
        playerCore?.setVolume(volume / 100f) // Convert 0-100 to 0-1
      }
      result.success(null)
    } ?: result.success(null)
  }

  private fun handleSetRate(call: MethodCall, result: MethodChannel.Result) {
    val rate = call.argument<Number>("rate")?.toFloat()

    if (rate == null) {
      result.error("INVALID_ARGS", "Missing 'rate'", null)
      return
    }

    activity?.runOnUiThread {
      if (usingMpvFallback) {
        mpvCore?.setProperty("speed", rate.toString())
      } else {
        playerCore?.setPlaybackSpeed(rate)
      }
      result.success(null)
    } ?: result.success(null)
  }

  private fun handleSelectAudioTrack(call: MethodCall, result: MethodChannel.Result) {
    val trackId = call.argument<String>("trackId")

    if (trackId == null) {
      result.error("INVALID_ARGS", "Missing 'trackId'", null)
      return
    }

    activity?.runOnUiThread {
      if (usingMpvFallback) {
        // After fallback, track IDs come from mpv's track-list (already 1-indexed)
        mpvCore?.setProperty("aid", trackId)
      } else {
        playerCore?.selectAudioTrack(trackId)
      }
      result.success(null)
    } ?: result.success(null)
  }

  private fun handleSelectSubtitleTrack(call: MethodCall, result: MethodChannel.Result) {
    val trackId = call.argument<String>("trackId")

    // trackId can be null or "no" to disable subtitles
    activity?.runOnUiThread {
      if (usingMpvFallback) {
        mpvCore?.setProperty("sid", trackId ?: "no")
      } else {
        playerCore?.selectSubtitleTrack(trackId)
      }
      result.success(null)
    } ?: result.success(null)
  }

  private fun handleAddSubtitleTrack(call: MethodCall, result: MethodChannel.Result) {
    val uri = call.argument<String>("uri")
    val title = call.argument<String>("title")
    val language = call.argument<String>("language")
    val mimeType = call.argument<String>("mimeType")
    val select = call.argument<Boolean>("select") ?: false

    if (uri == null) {
      result.error("INVALID_ARGS", "Missing 'uri'", null)
      return
    }

    activity?.runOnUiThread {
      if (usingMpvFallback) {
        val selectFlag = if (select) "select" else "auto"
        mpvCore?.command(arrayOf("sub-add", uri, selectFlag, title ?: "External"))
      } else {
        playerCore?.addSubtitleTrack(uri, title, language, mimeType, select)
      }
      result.success(null)
    } ?: result.success(null)
  }

  private fun handleSetVisible(call: MethodCall, result: MethodChannel.Result) {
    val visible = call.argument<Boolean>("visible")

    if (visible == null) {
      result.error("INVALID_ARGS", "Missing 'visible'", null)
      return
    }

    if (usingMpvFallback) {
      mpvCore?.setVisible(visible)
    } else {
      playerCore?.setVisible(visible)
    }
    result.success(null)
  }

  private fun handleUpdateFrame(result: MethodChannel.Result) {
    if (usingMpvFallback) {
      mpvCore?.updateFrame()
    } else {
      playerCore?.updateFrame()
    }
    result.success(null)
  }

  private fun handleSetVideoFrameRate(call: MethodCall, result: MethodChannel.Result) {
    val fps = call.argument<Double>("fps")?.toFloat() ?: 0f
    val duration = call.argument<Number>("duration")?.toLong() ?: 0L
    val extraDelayMs = call.argument<Number>("extraDelayMs")?.toLong() ?: 0L

    Log.d(TAG, "setVideoFrameRate: fps=$fps, duration=$duration, extraDelayMs=$extraDelayMs")
    val onComplete: (Boolean) -> Unit = { switched -> result.success(switched) }
    if (usingMpvFallback) {
      val core = mpvCore
      if (core == null) {
        result.success(false)
      } else {
        core.setVideoFrameRate(fps, duration, extraDelayMs, onComplete)
      }
    } else {
      val core = playerCore
      if (core == null) {
        result.success(false)
      } else {
        core.setVideoFrameRate(fps, duration, extraDelayMs, onComplete)
      }
    }
  }

  private fun handleClearVideoFrameRate(result: MethodChannel.Result) {
    Log.d(TAG, "clearVideoFrameRate")
    if (usingMpvFallback) {
      mpvCore?.clearVideoFrameRate()
    } else {
      playerCore?.clearVideoFrameRate()
    }
    result.success(null)
  }

  private fun handleRequestAudioFocus(result: MethodChannel.Result) {
    Log.d(TAG, "requestAudioFocus")
    val granted = if (usingMpvFallback) {
      mpvCore?.requestAudioFocus() ?: false
    } else {
      playerCore?.requestAudioFocus() ?: false
    }
    result.success(granted)
  }

  private fun handleAbandonAudioFocus(result: MethodChannel.Result) {
    Log.d(TAG, "abandonAudioFocus")
    if (usingMpvFallback) {
      mpvCore?.abandonAudioFocus()
    } else {
      playerCore?.abandonAudioFocus()
    }
    result.success(null)
  }

  private fun handleObserveProperty(call: MethodCall, result: MethodChannel.Result) {
    val name = call.argument<String>("name")
    val id = call.argument<Int>("id")

    if (name == null || id == null) {
      result.error("INVALID_ARGS", "Missing 'name' or 'id'", null)
      return
    }

    nameToId[name] = id
    result.success(null)
  }

  private fun handleSetSubtitleStyle(call: MethodCall, result: MethodChannel.Result) {
    val fontSize = call.argument<Number>("fontSize")?.toFloat() ?: 55f
    val textColor = call.argument<String>("textColor") ?: "#FFFFFF"
    val borderSize = call.argument<Number>("borderSize")?.toFloat() ?: 3f
    val borderColor = call.argument<String>("borderColor") ?: "#000000"
    val bgColor = call.argument<String>("bgColor") ?: "#000000"
    val bgOpacity = call.argument<Number>("bgOpacity")?.toInt() ?: 0
    val subtitlePosition = call.argument<Number>("subtitlePosition")?.toInt() ?: 100
    val bold = call.argument<Boolean>("bold") ?: false
    val italic = call.argument<Boolean>("italic") ?: false

    if (usingMpvFallback) {
      // MPV fallback handles styling via setProperty, no-op here
      result.success(null)
      return
    }

    playerCore?.setSubtitleStyle(fontSize, textColor, borderSize, borderColor, bgColor, bgOpacity, subtitlePosition, bold, italic)
    result.success(null)
  }

  private fun handleSetBoxFitMode(call: MethodCall, result: MethodChannel.Result) {
    val mode = call.argument<Number>("mode")?.toInt()
    if (mode == null) {
      result.error("INVALID_ARGS", "Missing 'mode'", null)
      return
    }
    // The MPV-property side (panscan / sub-ass-force-margins / video-aspect-override)
    // is driven from Dart via setProperty and routed through setMpvProperty, which
    // already handles both the fallback and pendingMpvProperties cases.
    if (usingMpvFallback) {
      result.success(null)
      return
    }
    activity?.runOnUiThread {
      playerCore?.setBoxFitMode(mode)
      result.success(null)
    } ?: result.success(null)
  }

  private fun handleSetDvConversionMode(call: MethodCall, result: MethodChannel.Result) {
    val mode = call.argument<String>("mode")
    if (mode == null) {
      result.error("INVALID_ARGS", "Missing 'mode'", null)
      return
    }
    if (usingMpvFallback) {
      result.success(false)
      return
    }
    activity?.runOnUiThread {
      val handled = playerCore?.setDebugDvConversionMode(mode) == true
      if (handled) {
        result.success(true)
      } else {
        result.error("INVALID_ARGS", "Invalid DV conversion mode: $mode", null)
      }
    } ?: result.error("NO_ACTIVITY", "Activity not available", null)
  }

  private fun handleSetMpvProperty(call: MethodCall, result: MethodChannel.Result) {
    val name = call.argument<String>("name")
    val value = call.argument<String>("value")

    if (name == null || value == null) {
      result.error("INVALID_ARGS", "Missing 'name' or 'value'", null)
      return
    }

    // Apply sync offsets to ExoPlayer when active
    if (!usingMpvFallback) {
      when (name) {
        "audio-delay" -> playerCore?.setAudioDelay(value.toDoubleOrNull() ?: 0.0)
        "sub-delay" -> playerCore?.setSubtitleDelay(value.toDoubleOrNull() ?: 0.0)
      }
    }

    if (usingMpvFallback) {
      mpvCore?.setProperty(name, value)
    } else {
      // Store for later application if ExoPlayer falls back to MPV
      pendingMpvProperties.add(Pair(name, value))
    }
    result.success(null)
  }

  private fun handleGetStats(result: MethodChannel.Result) {
    if (usingMpvFallback) {
      Thread {
        val stats = getMpvStats()
        activity?.runOnUiThread { result.success(stats) }
      }.start()
    } else {
      activity?.runOnUiThread {
        val coreStats = playerCore?.getStats() ?: emptyMap()
        result.success(coreStats + mapOf("playerType" to "exoplayer"))
      } ?: result.success(mapOf("playerType" to "unknown"))
    }
  }

  /**
   * Get playback stats from MPV when in fallback mode.
   * Queries relevant MPV properties and returns them in a map format
   * compatible with the performance overlay.
   */
  private fun getMpvStats(): Map<String, Any?> {
    val mpv = mpvCore ?: return mapOf("playerType" to "mpv")

    val hasVideo = mpv.getProperty("video-params/w") != null

    val stats = mutableMapOf<String, Any?>(
      "playerType" to "mpv",
      // Video metrics
      "video-codec" to mpv.getProperty("video-codec"),
      "video-params/w" to mpv.getProperty("video-params/w"),
      "video-params/h" to mpv.getProperty("video-params/h"),
      "videoWidth" to mpv.getProperty("dwidth"),
      "videoHeight" to mpv.getProperty("dheight"),
      "container-fps" to mpv.getProperty("container-fps"),
      "estimated-vf-fps" to mpv.getProperty("estimated-vf-fps"),
      "video-bitrate" to mpv.getProperty("video-bitrate"),
      "hwdec-current" to mpv.getProperty("hwdec-current"),
      // Audio metrics
      "audio-codec-name" to mpv.getProperty("audio-codec-name"),
      "audio-params/samplerate" to mpv.getProperty("audio-params/samplerate"),
      "audio-params/hr-channels" to mpv.getProperty("audio-params/hr-channels"),
      "audio-bitrate" to mpv.getProperty("audio-bitrate"),
      // Performance metrics
      "total-avsync-change" to mpv.getProperty("total-avsync-change"),
      "cache-speed" to mpv.getProperty("cache-speed"),
      "frame-drop-count" to mpv.getProperty("frame-drop-count"),
      "decoder-frame-drop-count" to mpv.getProperty("decoder-frame-drop-count"),
      "demuxer-cache-duration" to mpv.getProperty("demuxer-cache-duration")
    )

    // Only query properties that require an active video track
    if (hasVideo) {
      stats["display-fps"] = mpv.getProperty("display-fps")
      // Color/Format properties
      stats["video-params/pixelformat"] = mpv.getProperty("video-params/pixelformat")
      stats["video-params/hw-pixelformat"] = mpv.getProperty("video-params/hw-pixelformat")
      stats["video-params/colormatrix"] = mpv.getProperty("video-params/colormatrix")
      stats["video-params/primaries"] = mpv.getProperty("video-params/primaries")
      stats["video-params/gamma"] = mpv.getProperty("video-params/gamma")
      // HDR metadata
      stats["video-params/max-luma"] = mpv.getProperty("video-params/max-luma")
      stats["video-params/min-luma"] = mpv.getProperty("video-params/min-luma")
      stats["video-params/max-cll"] = mpv.getProperty("video-params/max-cll")
      stats["video-params/max-fall"] = mpv.getProperty("video-params/max-fall")
      // Other
      stats["video-params/aspect-name"] = mpv.getProperty("video-params/aspect-name")
      stats["video-params/rotate"] = mpv.getProperty("video-params/rotate")
    }

    return stats
  }

  // PiP Mode handling

  fun onPipModeChanged(isInPipMode: Boolean) {
    activity?.runOnUiThread {
      if (usingMpvFallback) {
        mpvCore?.onPipModeChanged(isInPipMode)
      } else {
        playerCore?.onPipModeChanged(isInPipMode)
      }
    }
  }

  // ExoPlayerDelegate

  override fun onPropertyChange(name: String, value: Any?) {
    val propId = nameToId[name] ?: return
    mainHandler.post { eventSink?.success(listOf(propId, value)) }
  }

  override fun onEvent(name: String, data: Map<String, Any>?) {
    val event = mutableMapOf<String, Any>(
      "type" to "event",
      "name" to name
    )
    data?.let { event["data"] = it }
    mainHandler.post { eventSink?.success(event) }
  }

  /**
   * Opens a content:// URI via ContentResolver and returns the raw FD number,
   * or null if the URI is not a content:// scheme or opening fails.
   * The returned FD is detached so MPV can own and close it via fdclose://.
   */
  private fun openContentFd(
    uriString: String,
    resolver: ContentResolver? = activity?.contentResolver
  ): Int? {
    if (!uriString.startsWith("content://")) return null
    return try {
      val uri = Uri.parse(uriString)
      val pfd = resolver?.openFileDescriptor(uri, "r") ?: return null
      val fd = pfd.detachFd()
      Log.d(TAG, "Opened content FD $fd for $uriString")
      fd
    } catch (e: Exception) {
      Log.e(TAG, "Failed to open content FD: ${e.message}", e)
      null
    }
  }

  override fun onFormatUnsupported(
    uri: String,
    headers: Map<String, String>?,
    positionMs: Long,
    errorMessage: String
  ): Boolean {
    if (usingMpvFallback || fallbackInProgress) {
      Log.w(TAG, "Fallback already active/in-progress, ignoring duplicate request")
      return true
    }

    val currentActivity = activity ?: return false
    fallbackInProgress = true

    Log.i(TAG, "ExoPlayer error, switching to MPV fallback at ${positionMs}ms: $errorMessage")
    if (debugLoggingEnabled) {
      onEvent(
        "log-message",
        mapOf(
          "prefix" to "fallback",
          "level" to "warn",
          "text" to "Switching to MPV at ${positionMs}ms: $errorMessage"
        )
      )
    }

    currentActivity.runOnUiThread {
      try {
        // Dispose ExoPlayer
        playerCore?.dispose()
        playerCore = null
        mpvCore?.dispose()
        mpvCore = null
        usingMpvFallback = false // Clear before handoff

        val generation = sessionGeneration

        Handler(Looper.getMainLooper()).post {
          if (generation != sessionGeneration) {
            fallbackInProgress = false
            return@post
          }
          val act = activity
          if (act == null) {
            fallbackInProgress = false
            return@post
          }

          try {
            val core = MpvPlayerCore(act).apply {
              delegate = this@ExoPlayerPlugin
            }
            mpvCore = core // publish so dispose/init can reach it

            core.initialize { success ->
              if (generation != sessionGeneration) {
                if (mpvCore === core) {
                  core.dispose()
                  mpvCore = null
                }
                fallbackInProgress = false
                return@initialize
              }
              if (!success) {
                if (mpvCore === core) {
                  core.dispose()
                  mpvCore = null
                }
                fallbackInProgress = false
                Log.e(TAG, "Failed to initialize MPV fallback")
                onEvent("end-file", mapOf("reason" to "error", "message" to "Fallback failed: $errorMessage"))
                return@initialize
              }

              usingMpvFallback = true
              fallbackInProgress = false

              // Snapshot pending properties on main thread before clearing
              val pendingProps = pendingMpvProperties.toList()
              pendingMpvProperties.clear()

              // Compute content FD on main thread (needs contentResolver)
              val mpvUri = openContentFd(uri, act.contentResolver)
                ?.let { "fdclose://$it" } ?: uri

              // Buffer size for closure
              val bufferSize = configuredBufferSizeBytes

              if (mpvCore !== core) {
                core.dispose()
                fallbackInProgress = false
                return@initialize
              }
              // Configure basic MPV properties for Plex playback
              core.setProperty("hwdec", "mediacodec,mediacodec-copy")
              core.setProperty("vo", "gpu")
              core.setProperty("ao", "audiotrack")

              // Forward user's buffer config to MPV fallback
              if (bufferSize != null && bufferSize > 0) {
                core.setProperty("demuxer-max-bytes", bufferSize.toString())
              }

              // Apply pending MPV properties from Dart
              for ((propName, propValue) in pendingProps) {
                core.setProperty(propName, propValue)
              }

              // Setup property observers
              core.observeProperty("time-pos", "double")
              core.observeProperty("duration", "double")
              core.observeProperty("seekable", "flag")
              core.observeProperty("pause", "flag")
              core.observeProperty("paused-for-cache", "flag")
              core.observeProperty("demuxer-cache-time", "double")
              core.observeProperty("eof-reached", "flag")
              core.observeProperty("track-list", "string")
              core.observeProperty("aid", "string")
              core.observeProperty("sid", "string")
              core.observeProperty("volume", "double")
              core.observeProperty("speed", "double")

              // Show the MPV surface (internally posts to UI)
              core.setVisible(true)

              // Load media at the same position
              val startSeconds = positionMs / 1000.0
              val options = mutableListOf<String>()
              options.add("start=$startSeconds")
              headers?.forEach { (key, value) ->
                options.add("http-header-fields-append=$key: $value")
              }
              val optionsStr = options.joinToString(",")
              core.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))

              // On GPUs without compute shaders, MPV can't do dynamic peak detection
              // and spline tone-mapping produces dim/washed-out results with extreme
              // static HDR peak metadata. Use reinhard which handles this better.
              Thread {
                val peakDetection = core.getProperty("hdr-compute-peak")
                if (peakDetection == "no") {
                  Log.i(TAG, "No compute shaders — overriding tone-mapping to reinhard")
                  core.setProperty("tone-mapping", "reinhard")
                  core.setProperty("tone-mapping-param", "0.7")
                  core.setProperty("tone-mapping-mode", "luma")
                }
              }.start()

              // Request audio focus
              core.requestAudioFocus()

              // Emit backend-switched event on main thread
              activity?.runOnUiThread {
                onEvent("backend-switched", null)
              }

              Log.i(TAG, "Successfully switched to MPV fallback")
            }
          } catch (e: Exception) {
            fallbackInProgress = false
            Log.e(TAG, "Failed to switch to MPV fallback", e)
            onEvent("end-file", mapOf("reason" to "error", "message" to "Fallback failed: ${e.message}"))
          }
        }
      } catch (e: Exception) {
        fallbackInProgress = false
        Log.e(TAG, "Failed to switch to MPV fallback", e)
        onEvent("end-file", mapOf("reason" to "error", "message" to "Fallback failed: ${e.message}"))
      }
    }

    return true // Fallback is being handled
  }
}
