@file:androidx.media3.common.util.UnstableApi

package cn.learningpad.oraltrainer.sdk

import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import java.util.concurrent.CopyOnWriteArraySet
import kotlin.math.max
import kotlin.math.min

class OralTrainerController internal constructor(
    context: Context,
    private val sdkConfig: OralTrainerSdkConfig,
    initialConfig: PlayerConfig,
    private val imitationAssessor: ImitationQualityAssessor,
) {
    private val appContext = context.applicationContext
    private val listeners = CopyOnWriteArraySet<OralTrainerListener>()
    private val mediaItems = mutableListOf<TrainingMediaItem>()
    private val mainHandler = Handler(Looper.getMainLooper())
    private var released = false
    private var lastSentenceIndex: Int? = null
    private var stopAtSentenceEnd = false
    private var stopAtSentenceEndIndex: Int? = null

    var config: PlayerConfig = initialConfig
        private set

    var loopMode: LoopMode = LoopMode.ALL
        private set

    val player: ExoPlayer

    private val ticker = object : Runnable {
        override fun run() {
            if (released) {
                return
            }
            enforceSentenceBoundary()
            notifySentenceIfChanged()
            notifySnapshot()
            mainHandler.postDelayed(this, 250L)
        }
    }

    init {
        val mediaSourceFactory = DefaultMediaSourceFactory(
            StreamingCache.dataSourceFactory(appContext, sdkConfig)
        )
        player = ExoPlayer.Builder(appContext)
            .setMediaSourceFactory(mediaSourceFactory)
            .build()
            .also { exoPlayer ->
                exoPlayer.repeatMode = Player.REPEAT_MODE_ALL
                exoPlayer.playWhenReady = initialConfig.autoPlay
                exoPlayer.addListener(object : Player.Listener {
                    override fun onPlaybackStateChanged(playbackState: Int) {
                        notifySnapshot()
                    }

                    override fun onIsPlayingChanged(isPlaying: Boolean) {
                        notifySnapshot()
                    }

                    override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
                        lastSentenceIndex = null
                        listeners.forEach { it.onMediaChanged(currentTrainingItem()) }
                        notifySentenceIfChanged(force = true)
                        notifySnapshot()
                    }

                    override fun onPositionDiscontinuity(
                        oldPosition: Player.PositionInfo,
                        newPosition: Player.PositionInfo,
                        reason: Int,
                    ) {
                        notifySentenceIfChanged(force = true)
                        notifySnapshot()
                    }

                    override fun onPlayerError(error: PlaybackException) {
                        listeners.forEach { it.onPlayerError(error) }
                    }
                })
            }
        mainHandler.post(ticker)
    }

    fun addListener(listener: OralTrainerListener) {
        listeners.add(listener)
    }

    fun removeListener(listener: OralTrainerListener) {
        listeners.remove(listener)
    }

    fun updateConfig(config: PlayerConfig) {
        this.config = config
    }

    @JvmOverloads
    fun loadCourse(
        course: TrainingCourse,
        startIndex: Int = 0,
        startPositionMs: Long = 0L,
    ) {
        require(course.items.isNotEmpty()) { "TrainingCourse must contain at least one media item." }
        loadItems(course.items, startIndex, startPositionMs)
    }

    @JvmOverloads
    fun loadItems(
        items: List<TrainingMediaItem>,
        startIndex: Int = 0,
        startPositionMs: Long = 0L,
    ) {
        require(items.isNotEmpty()) { "At least one TrainingMediaItem is required." }
        val safeIndex = startIndex.coerceIn(0, items.lastIndex)
        mediaItems.clear()
        mediaItems.addAll(items)
        lastSentenceIndex = null
        stopAtSentenceEnd = false
        stopAtSentenceEndIndex = null
        player.setMediaItems(items.map { it.toMedia3Item() }, safeIndex, max(0L, startPositionMs))
        player.prepare()
        player.playWhenReady = config.autoPlay
        listeners.forEach { it.onMediaChanged(currentTrainingItem()) }
        notifySentenceIfChanged(force = true)
        notifySnapshot()
    }

    fun loadItem(item: TrainingMediaItem) {
        loadItems(listOf(item))
    }

    /**
     * Replaces sentences without preparing/reloading media, preserving playback
     * position and state so a background boundary refresh can apply immediately.
     */
    fun updateSentences(mediaId: String, sentences: List<SentenceBoundary>): Boolean {
        val index = mediaItems.indexOfFirst { it.id == mediaId }
        if (index < 0) {
            return false
        }
        val updatedItem = mediaItems[index].copy(sentences = sentences)
        if (mediaItems[index] == updatedItem) {
            return true
        }
        mediaItems[index] = updatedItem
        if (index == player.currentMediaItemIndex) {
            listeners.forEach { it.onMediaChanged(updatedItem) }
            notifySentenceIfChanged(force = true)
            notifySnapshot()
        }
        return true
    }

    fun play() {
        player.play()
    }

    fun pause() {
        stopAtSentenceEnd = false
        stopAtSentenceEndIndex = null
        player.pause()
    }

    fun togglePlayPause() {
        if (player.isPlaying) {
            pause()
        } else {
            play()
        }
    }

    fun stop() {
        stopAtSentenceEnd = false
        stopAtSentenceEndIndex = null
        player.stop()
    }

    fun seekTo(positionMs: Long) {
        stopAtSentenceEnd = false
        stopAtSentenceEndIndex = null
        player.seekTo(max(0L, positionMs))
    }

    fun seekRelative(deltaMs: Long) {
        seekTo(player.currentPosition + deltaMs)
    }

    fun rewind(stepMs: Long = config.defaultSeekStepMs) {
        seekRelative(-max(0L, stepMs))
    }

    fun fastForward(stepMs: Long = config.defaultSeekStepMs) {
        seekRelative(max(0L, stepMs))
    }

    fun previousItem() {
        player.seekToPreviousMediaItem()
    }

    fun nextItem() {
        player.seekToNextMediaItem()
    }

    fun setPlaybackSpeed(speed: Float) {
        val clamped = min(config.maxPlaybackSpeed, max(config.minPlaybackSpeed, speed))
        player.setPlaybackSpeed(clamped)
    }

    fun setVolume(volume: Float) {
        player.volume = volume.coerceIn(0f, 1f)
    }

    fun setLoopMode(loopMode: LoopMode) {
        this.loopMode = loopMode
        player.repeatMode = when (loopMode) {
            LoopMode.OFF -> Player.REPEAT_MODE_OFF
            LoopMode.ONE -> Player.REPEAT_MODE_ONE
            LoopMode.ALL -> Player.REPEAT_MODE_ALL
        }
    }

    fun setSentenceMode(enabled: Boolean) {
        config = config.copy(sentenceMode = enabled)
    }

    fun setContinuousPlayback(enabled: Boolean) {
        config = config.copy(continuousPlayback = enabled)
    }

    fun playCurrentSentenceAndStop() {
        val sentence = currentSentence() ?: return
        player.seekTo(sentence.startMs)
        stopAtSentenceEnd = true
        stopAtSentenceEndIndex = sentence.index
        player.play()
    }

    fun currentTrainingItem(): TrainingMediaItem? {
        val index = player.currentMediaItemIndex
        return mediaItems.getOrNull(index)
    }

    fun currentSentence(): SentenceBoundary? {
        val item = currentTrainingItem() ?: return null
        val position = player.currentPosition
        return item.sentences.lastOrNull { sentence ->
            position >= sentence.startMs && position < sentence.endMs
        }
    }

    fun seekToPreviousSentence(): Boolean {
        val item = currentTrainingItem() ?: return false
        if (item.sentences.isEmpty()) {
            return false
        }
        val position = player.currentPosition
        val target = item.sentences
            .asReversed()
            .firstOrNull { it.startMs < position - PREVIOUS_SENTENCE_TOLERANCE_MS }
            ?: item.sentences.first()
        seekTo(target.startMs)
        return true
    }

    fun seekToNextSentence(): Boolean {
        val item = currentTrainingItem() ?: return false
        if (item.sentences.isEmpty()) {
            return false
        }
        val position = player.currentPosition
        val target = item.sentences.firstOrNull {
            it.startMs > position + NEXT_SENTENCE_TOLERANCE_MS
        } ?: return false
        seekTo(target.startMs)
        return true
    }

    fun previousSentenceOrRewind() {
        if (!config.sentenceMode || !seekToPreviousSentence()) {
            rewind()
        }
    }

    fun nextSentenceOrForward() {
        if (!config.sentenceMode || !seekToNextSentence()) {
            fastForward()
        }
    }

    fun performSwipeAction(action: SwipeAction) {
        when (action) {
            SwipeAction.NONE -> Unit
            SwipeAction.REWIND -> rewind()
            SwipeAction.FORWARD -> fastForward()
            SwipeAction.PREVIOUS_SENTENCE -> seekToPreviousSentence()
            SwipeAction.NEXT_SENTENCE -> seekToNextSentence()
            SwipeAction.PREVIOUS_SENTENCE_OR_REWIND -> previousSentenceOrRewind()
            SwipeAction.NEXT_SENTENCE_OR_FORWARD -> nextSentenceOrForward()
        }
    }

    internal fun dispatchGesture(event: GestureEvent) {
        listeners.forEach { it.onGesture(event) }
    }

    fun assessCurrentSentence(
        recordingUri: Uri,
        callback: ImitationAssessmentCallback,
        referenceAudioUri: Uri? = null,
        locale: String? = null,
        metadata: Map<String, String> = emptyMap(),
    ): CancellableAssessment {
        val item = currentTrainingItem()
            ?: return callback.unsupported("No media item is currently loaded.")
        val sentence = currentSentence()
            ?: return callback.unsupported("No active sentence is available at the current position.")
        val request = ImitationAssessmentRequest(
            mediaId = item.id,
            sentence = sentence,
            recordingUri = recordingUri,
            referenceAudioUri = referenceAudioUri,
            locale = locale,
            metadata = metadata,
        )
        return imitationAssessor.assess(request, callback)
    }

    fun snapshot(): PlaybackSnapshot {
        val item = currentTrainingItem()
        val duration = player.duration.takeUnless { it == C.TIME_UNSET } ?: -1L
        return PlaybackSnapshot(
            mediaId = item?.id,
            positionMs = max(0L, player.currentPosition),
            durationMs = duration,
            bufferedPositionMs = max(0L, player.bufferedPosition),
            isPlaying = player.isPlaying,
            playbackState = player.playbackState.toOralTrainerState(),
            playbackSpeed = player.playbackParameters.speed,
            sentenceIndex = currentSentence()?.index,
        )
    }

    fun release() {
        if (released) {
            return
        }
        released = true
        mainHandler.removeCallbacks(ticker)
        listeners.clear()
        player.release()
    }

    private fun notifySnapshot() {
        if (released) {
            return
        }
        val snapshot = snapshot()
        listeners.forEach { it.onPlaybackSnapshot(snapshot) }
    }

    private fun notifySentenceIfChanged(force: Boolean = false) {
        val sentence = currentSentence()
        val index = sentence?.index
        if (!force && index == lastSentenceIndex) {
            return
        }
        lastSentenceIndex = index
        listeners.forEach { it.onSentenceChanged(sentence) }
    }

    private fun enforceSentenceBoundary() {
        if (!config.sentenceMode || !player.isPlaying) {
            return
        }
        val item = currentTrainingItem() ?: return
        if (item.sentences.isEmpty()) {
            return
        }
        val position = player.currentPosition
        if (stopAtSentenceEnd) {
            val index = stopAtSentenceEndIndex ?: return
            val sentence = item.sentences.getOrNull(index) ?: return
            if (position >= sentence.endMs) {
                stopAtSentenceEnd = false
                stopAtSentenceEndIndex = null
                player.pause()
                player.seekTo(sentence.startMs)
            }
            return
        }
        if (config.continuousPlayback) {
            return
        }
        val anchorIndex = lastSentenceIndex ?: return
        val anchor = item.sentences.getOrNull(anchorIndex) ?: return
        if (position >= anchor.endMs) {
            player.seekTo(anchor.startMs)
        }
    }

    private fun TrainingMediaItem.toMedia3Item(): MediaItem {
        val builder = MediaItem.Builder()
            .setMediaId(id)
            .setUri(uri)
        mimeType?.let(builder::setMimeType)
        customCacheKey?.let(builder::setCustomCacheKey)
        subtitleUri?.let { uri ->
            builder.setSubtitleConfigurations(
                listOf(
                    MediaItem.SubtitleConfiguration.Builder(uri)
                        .setMimeType(subtitleMimeType)
                        .setLanguage(subtitleLanguage)
                        .setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
                        .build()
                )
            )
        }
        return builder.build()
    }

    private fun Int.toOralTrainerState(): OralTrainerPlaybackState {
        return when (this) {
            Player.STATE_BUFFERING -> OralTrainerPlaybackState.BUFFERING
            Player.STATE_READY -> OralTrainerPlaybackState.READY
            Player.STATE_ENDED -> OralTrainerPlaybackState.ENDED
            else -> OralTrainerPlaybackState.IDLE
        }
    }

    private fun ImitationAssessmentCallback.unsupported(message: String): CancellableAssessment {
        onError(IllegalStateException(message))
        return CancellableAssessment {}
    }

    private companion object {
        const val PREVIOUS_SENTENCE_TOLERANCE_MS = 250L
        const val NEXT_SENTENCE_TOLERANCE_MS = 150L
    }
}
