package cy.hexor.furumi.playback

import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.PlaybackException
import androidx.media3.common.Timeline
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import cy.hexor.furumi.domain.model.TrackCard
import cy.hexor.furumi.domain.model.ConnectedPlaybackState
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import javax.inject.Inject
import javax.inject.Singleton

data class AudioPlaybackState(
    val queue: List<TrackCard> = emptyList(),
    val currentIndex: Int = -1,
    val isPlaying: Boolean = false,
    val isBuffering: Boolean = false,
    val positionMs: Long = 0L,
    val durationMs: Long = 0L,
    val shuffle: Boolean = false,
    val repeatMode: String = REPEAT_MODE_OFF,
    val volume: Float = 1f,
    val errorMessage: String? = null
) {
    val currentTrack: TrackCard?
        get() = queue.getOrNull(currentIndex)

    val progress: Float
        get() = if (durationMs > 0L) {
            (positionMs.toFloat() / durationMs.toFloat()).coerceIn(0f, 1f)
        } else {
            0f
        }

    val canGoPrevious: Boolean
        get() = currentIndex > 0 || positionMs > RESTART_WINDOW_MS

    val canGoNext: Boolean
        get() = currentIndex in 0 until queue.lastIndex

    private companion object {
        const val RESTART_WINDOW_MS = 3_000L
        const val REPEAT_MODE_OFF = "off"
    }
}

@OptIn(UnstableApi::class)
@Singleton
class PlaybackController @Inject constructor(
    @param:ApplicationContext private val context: Context,
    okHttpClient: OkHttpClient
) {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
    private val _state = MutableStateFlow(AudioPlaybackState())

    private var playStartedAtMs: Long = 0L
    private var playStartTrackId: Long = -1L
    private var listenedDurationMs: Long = 0L
    private var lastPlaybackResumedAtMs: Long = 0L

    var onTrackPlayReported: ((trackId: Long, startedAt: Long, durationListened: Int, completed: Boolean) -> Unit)? = null

    val player: ExoPlayer
    val state: StateFlow<AudioPlaybackState> = _state.asStateFlow()

    init {
        val dataSourceFactory = DefaultDataSource.Factory(
            context,
            OkHttpDataSource.Factory(okHttpClient)
        )
        val mediaSourceFactory = DefaultMediaSourceFactory(dataSourceFactory)

        player = ExoPlayer.Builder(context)
            .setMediaSourceFactory(mediaSourceFactory)
            .build()
            .apply {
                setAudioAttributes(
                    AudioAttributes.Builder()
                        .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
                        .setUsage(C.USAGE_MEDIA)
                        .build(),
                    true
                )
                setHandleAudioBecomingNoisy(true)
                setWakeMode(C.WAKE_MODE_LOCAL)
                volume = ANDROID_MEDIA_VOLUME_MULTIPLIER
            }

        player.addListener(
            object : Player.Listener {
                override fun onIsPlayingChanged(isPlaying: Boolean) {
                    updateListeningTimer(isPlaying)
                    publishState()
                }

                override fun onPlaybackStateChanged(playbackState: Int) {
                    if (playbackState == Player.STATE_ENDED) {
                        reportCurrentTrackPlay(completed = true)
                    }
                    publishState()
                }

                override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
                    publishState()
                    startPlaybackService()
                }

                override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
                    val completed = reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO
                    reportCurrentTrackPlay(completed = completed)
                    startTrackTimer()
                    publishState()
                    // If service was stopped but player continues (e.g. gapless), ensure it's up
                    if (player.isPlaying) startPlaybackService()
                }

                override fun onTimelineChanged(timeline: Timeline, reason: Int) {
                    publishState()
                }

                override fun onPlayerError(error: PlaybackException) {
                    _state.value = _state.value.copy(
                        isPlaying = false,
                        isBuffering = false,
                        errorMessage = error.message ?: "Playback failed"
                    )
                }
            }
        )

        scope.launch {
            while (isActive) {
                publishState()
                delay(PROGRESS_TICK_MS)
            }
        }
    }

    fun playQueue(queue: List<TrackCard>, startIndex: Int = 0) {
        val playableQueue = queue.filter { it.streamUrl.isNotBlank() }
        if (playableQueue.isEmpty()) {
            _state.value = AudioPlaybackState(errorMessage = "No playable tracks")
            return
        }

        reportCurrentTrackPlay(completed = false)

        val safeStartIndex = startIndex.coerceIn(0, playableQueue.lastIndex)
        val mediaItems = playableQueue.map { track -> track.toMediaItem() }

        _state.value = AudioPlaybackState(
            queue = playableQueue,
            currentIndex = safeStartIndex,
            isBuffering = true,
            shuffle = player.shuffleModeEnabled,
            repeatMode = player.repeatMode.toFurumiRepeatMode(),
            volume = player.volume
        )

        player.setMediaItems(mediaItems, safeStartIndex, 0L)
        player.prepare()
        player.playWhenReady = true
        startTrackTimer()
        startPlaybackService()
    }

    fun playTrack(track: TrackCard, queue: List<TrackCard>) {
        val playableQueue = queue
            .ifEmpty { listOf(track) }
            .filter { it.streamUrl.isNotBlank() }
            .ifEmpty { listOf(track).filter { it.streamUrl.isNotBlank() } }

        val startIndex = playableQueue.indexOfFirst { it.id == track.id }
            .takeIf { it >= 0 }
            ?: 0

        playQueue(playableQueue, startIndex)
    }

    fun togglePlayPause() {
        if (_state.value.currentTrack == null) return

        if (player.isPlaying) {
            pause()
        } else {
            resume()
        }
    }

    fun pause() {
        player.pause()
        publishState()
    }

    fun resume() {
        if (_state.value.currentTrack == null) return
        player.play()
        startPlaybackService()
        publishState()
    }

    fun next() {
        if (player.hasNextMediaItem()) {
            reportCurrentTrackPlay(completed = false)
            player.seekToNextMediaItem()
            player.play()
        }
        publishState()
    }

    fun previous() {
        if (player.currentPosition > RESTART_WINDOW_MS || !player.hasPreviousMediaItem()) {
            player.seekTo(0L)
        } else {
            reportCurrentTrackPlay(completed = false)
            player.seekToPreviousMediaItem()
        }
        player.play()
        publishState()
    }

    fun seekToProgress(progress: Float) {
        val duration = player.duration.takeIf { it > 0L } ?: return
        player.seekTo((duration * progress.coerceIn(0f, 1f)).toLong())
        publishState()
    }

    fun seekToSeconds(seconds: Double) {
        player.seekTo((seconds.coerceAtLeast(0.0) * 1_000.0).toLong())
        publishState()
    }

    fun setOptions(shuffle: Boolean?, repeatMode: String?) {
        shuffle?.let { player.shuffleModeEnabled = it }
        repeatMode?.let { player.repeatMode = it.toPlayerRepeatMode() }
        publishState()
    }

    fun playConnectedState(playbackState: ConnectedPlaybackState) {
        val queue = playbackState.tracks.ifEmpty {
            playbackState.track?.let(::listOf).orEmpty()
        }.filter { it.streamUrl.isNotBlank() }
        if (queue.isEmpty()) return

        setOptions(playbackState.shuffle, playbackState.repeatMode)
        playQueue(queue, playbackState.index.coerceIn(0, queue.lastIndex))
        player.seekTo((playbackState.positionSeconds.coerceAtLeast(0.0) * 1_000.0).toLong())
        if (playbackState.paused) {
            player.pause()
        } else {
            player.play()
            startPlaybackService()
        }
        publishState()
    }

    fun release() {
        reportCurrentTrackPlay(completed = false)
        scope.cancel()
        player.release()
    }

    fun stop() {
        reportCurrentTrackPlay(completed = false)
        player.stop()
        _state.value = AudioPlaybackState()
    }

    fun addNext(track: TrackCard) {
        if (track.streamUrl.isBlank()) return
        val current = _state.value
        if (current.queue.isEmpty()) {
            playQueue(listOf(track))
            return
        }

        val insertIndex = current.currentIndex + 1
        val mediaItem = track.toMediaItem()

        player.addMediaItem(insertIndex, mediaItem)
        val updatedQueue = current.queue.toMutableList().apply { add(insertIndex, track) }
        _state.value = current.copy(queue = updatedQueue)
    }

    fun addToEnd(track: TrackCard) {
        if (track.streamUrl.isBlank()) return
        val current = _state.value
        if (current.queue.isEmpty()) {
            playQueue(listOf(track))
            return
        }

        val mediaItem = track.toMediaItem()

        player.addMediaItem(mediaItem)
        val updatedQueue = current.queue + track
        _state.value = current.copy(queue = updatedQueue)
    }

    fun addNext(tracks: List<TrackCard>) {
        tracks.filter { it.streamUrl.isNotBlank() }.forEachIndexed { offset, track ->
            val current = _state.value
            if (current.queue.isEmpty()) {
                playQueue(listOf(track))
            } else {
                val insertIndex = (current.currentIndex + 1 + offset).coerceIn(0, current.queue.size)
                player.addMediaItem(insertIndex, track.toMediaItem())
                _state.value = current.copy(
                    queue = current.queue.toMutableList().apply { add(insertIndex, track) }
                )
            }
        }
        publishState()
    }

    fun addToEnd(tracks: List<TrackCard>) {
        val playableTracks = tracks.filter { it.streamUrl.isNotBlank() }
        if (playableTracks.isEmpty()) return
        val current = _state.value
        if (current.queue.isEmpty()) {
            playQueue(playableTracks)
            return
        }

        player.addMediaItems(playableTracks.map { it.toMediaItem() })
        _state.value = current.copy(queue = current.queue + playableTracks)
        publishState()
    }

    fun removeQueueIndex(index: Int) {
        val current = _state.value
        if (index !in current.queue.indices) return

        player.removeMediaItem(index)
        val updatedQueue = current.queue.toMutableList().apply { removeAt(index) }
        val updatedIndex = when {
            updatedQueue.isEmpty() -> -1
            index < current.currentIndex -> current.currentIndex - 1
            current.currentIndex >= updatedQueue.size -> updatedQueue.lastIndex
            else -> current.currentIndex
        }
        _state.value = current.copy(queue = updatedQueue, currentIndex = updatedIndex)
        publishState()
    }

    fun moveQueueItem(fromIndex: Int, toIndex: Int) {
        val current = _state.value
        if (fromIndex !in current.queue.indices || toIndex !in current.queue.indices) return

        player.moveMediaItem(fromIndex, toIndex)
        val updatedQueue = current.queue.toMutableList().apply {
            val item = removeAt(fromIndex)
            add(toIndex, item)
        }
        _state.value = current.copy(queue = updatedQueue)
        publishState()
    }

    fun clearQueue() {
        reportCurrentTrackPlay(completed = false)
        player.clearMediaItems()
        _state.value = AudioPlaybackState(
            shuffle = player.shuffleModeEnabled,
            repeatMode = player.repeatMode.toFurumiRepeatMode(),
            volume = player.volume
        )
    }

    private fun startTrackTimer() {
        val currentQueue = _state.value.queue
        val currentIndex = player.currentMediaItemIndex
            .takeIf { it >= 0 }
            ?: _state.value.currentIndex
        val current = currentQueue.getOrNull(currentIndex) ?: return
        val now = System.currentTimeMillis()
        playStartedAtMs = now
        playStartTrackId = current.id
        listenedDurationMs = 0L
        lastPlaybackResumedAtMs = if (player.isPlaying) now else 0L
    }

    private fun updateListeningTimer(isPlaying: Boolean) {
        val now = System.currentTimeMillis()
        if (isPlaying) {
            if (playStartTrackId >= 0 && lastPlaybackResumedAtMs <= 0L) {
                lastPlaybackResumedAtMs = now
            }
        } else if (lastPlaybackResumedAtMs > 0L) {
            listenedDurationMs += (now - lastPlaybackResumedAtMs).coerceAtLeast(0L)
            lastPlaybackResumedAtMs = 0L
        }
    }

    private fun reportCurrentTrackPlay(completed: Boolean) {
        if (playStartTrackId < 0 || playStartedAtMs <= 0L) return
        updateListeningTimer(isPlaying = false)

        val durationMs = listenedDurationMs.takeIf { it > 0L }
            ?: (System.currentTimeMillis() - playStartedAtMs).coerceAtLeast(0L)
        if (durationMs < 1_000L) return
        val durationSec = (durationMs / 1_000L)
            .coerceAtLeast(1L)
            .coerceAtMost(Int.MAX_VALUE.toLong())
            .toInt()

        onTrackPlayReported?.invoke(
            playStartTrackId,
            playStartedAtMs / 1_000L,
            durationSec,
            completed
        )
        playStartTrackId = -1L
        playStartedAtMs = 0L
        listenedDurationMs = 0L
        lastPlaybackResumedAtMs = 0L
    }

    private fun publishState() {
        val existingErrorMessage = _state.value.errorMessage
        val currentQueue = _state.value.queue
        
        val playerIndex = player.currentMediaItemIndex
        val mediaId = player.currentMediaItem?.mediaId
        
        // Sync index by Media ID because player index can drift during shuffle or queue changes
        val currentIndex = if (mediaId != null) {
            val indexInQueue = currentQueue.indexOfFirst { it.id.toString() == mediaId }
            if (indexInQueue != -1) indexInQueue else playerIndex.coerceIn(0, currentQueue.lastIndex.coerceAtLeast(0))
        } else {
            playerIndex.coerceIn(0, currentQueue.lastIndex.coerceAtLeast(0))
        }

        val duration = player.duration.takeIf { it > 0L } ?: currentQueue
            .getOrNull(currentIndex)
            ?.durationSeconds
            ?.times(1_000L)
            ?: 0L

        _state.value = _state.value.copy(
            currentIndex = currentIndex,
            isPlaying = player.isPlaying,
            isBuffering = player.playbackState == Player.STATE_BUFFERING,
            positionMs = player.currentPosition.coerceAtLeast(0L),
            durationMs = duration,
            shuffle = player.shuffleModeEnabled,
            repeatMode = player.repeatMode.toFurumiRepeatMode(),
            volume = player.volume,
            errorMessage = existingErrorMessage
        )
    }

    private companion object {
        const val PROGRESS_TICK_MS = 500L
        const val RESTART_WINDOW_MS = 3_000L
        const val ANDROID_MEDIA_VOLUME_MULTIPLIER = 1f
    }

    private fun startPlaybackService() {
        val intent = Intent(context, FurumiPlaybackService::class.java)
        ContextCompat.startForegroundService(context, intent)
    }
}

private fun TrackCard.toMediaMetadata(): MediaMetadata {
    val artist = artists.joinToString(", ")
        .ifBlank { releaseTitle.orEmpty() }
        .ifBlank { "Unknown artist" }

    return MediaMetadata.Builder()
        .setTitle(title)
        .setDisplayTitle(title)
        .setArtist(artist)
        .setSubtitle(artist)
        .setAlbumTitle(releaseTitle)
        .setArtworkUri(coverUrl?.let(Uri::parse))
        .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
        .build()
}

private fun TrackCard.toMediaItem(): MediaItem {
    return MediaItem.Builder()
        .setMediaId(id.toString())
        .setUri(streamUrl)
        .setMediaMetadata(toMediaMetadata())
        .build()
}

private fun Int.toFurumiRepeatMode(): String {
    return when (this) {
        Player.REPEAT_MODE_ONE -> "one"
        Player.REPEAT_MODE_ALL -> "all"
        else -> "off"
    }
}

private fun String.toPlayerRepeatMode(): Int {
    return when (lowercase()) {
        "one" -> Player.REPEAT_MODE_ONE
        "all" -> Player.REPEAT_MODE_ALL
        else -> Player.REPEAT_MODE_OFF
    }
}
