package com.github.libretube.util

import androidx.media3.common.Player
import com.github.libretube.api.MediaServiceRepository
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.extensions.move
import com.github.libretube.extensions.runCatchingIO
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.util.PlayingQueue.queueMode
import kotlinx.coroutines.Job
import java.util.Collections

object PlayingQueue {
    // queue is a synchronized list to be safely accessible from different coroutine threads
    private val queue = Collections.synchronizedList(mutableListOf<StreamItem>())
    private var currentStream: StreamItem? = null
    private var currentStreamIndex: Int = -1  // Cache current index for better reliability
    private val playedVideoIds = mutableSetOf<String>()

    private val queueJobs = mutableListOf<Job>()

    /**
     * Current use case of the queue. Do NOT add any offline videos while the [queueMode] is online
     * or vice versa.
     */
    var queueMode: PlayingQueueMode = PlayingQueueMode.ONLINE

    // wrapper around PlayerHelper#repeatMode for compatibility
    var repeatMode: Int
        get() = PlayerHelper.repeatMode
        set(value) {
            PlayerHelper.repeatMode = value
        }

    private fun clearJobs() {
        queueJobs.forEach {
            it.cancel()
        }
        queueJobs.clear()
    }

    fun clear() {
        clearJobs()
        queue.clear()
        currentStream = null
        currentStreamIndex = -1
        playedVideoIds.clear()
    }

    /**
     * Remove all items after the current [StreamItem] from the queue
     *
     * I.e., the current and all previous streams are kept
     */
    fun clearAfterCurrent() {
        clearJobs()
        synchronized(queue) {
            val newQueue = queue.filterIndexed { index, item -> index <= currentIndex() }
            setStreams(newQueue)
            // Recalculate index since queue was modified
            currentStreamIndex = queue.indexOfLast { it.url?.toID() == currentStream?.url?.toID() }
        }
    }

    /**
     * @param skipExisting Whether to skip the [streamItem] if it's already part of the queue
     */
    fun add(vararg streamItem: StreamItem, skipExisting: Boolean = false) {
        synchronized(queue) {
            for (stream in streamItem) {
                if ((skipExisting && contains(stream)) || stream.title.isNullOrBlank()) continue

                queue.remove(stream)
                queue.add(stream)
            }
        }
    }

    fun addAsNext(streamItem: StreamItem) {
        synchronized(queue) {
            if (currentStream == streamItem) return
            if (queue.contains(streamItem)) queue.remove(streamItem)
            queue.add(currentIndex() + 1, streamItem)
        }
    }

    // return the next item, or if repeating enabled and no video left, the first one of the queue
    fun getNext(): String? {
        return synchronized(queue) {
            if (repeatMode == Player.REPEAT_MODE_ONE) {
                return@synchronized currentStream?.url?.toID()
            }

            val nextUnplayed = queue.drop(currentIndex() + 1).firstOrNull {
                it.url?.toID() !in playedVideoIds
            }
            if (nextUnplayed != null) return@synchronized nextUnplayed.url?.toID()

            if (repeatMode == Player.REPEAT_MODE_ALL) {
                val wrappedUnplayed = queue.take(currentIndex() + 1).firstOrNull {
                    it.url?.toID() !in playedVideoIds
                }
                if (wrappedUnplayed != null) return@synchronized wrappedUnplayed.url?.toID()

                playedVideoIds.clear()
                return@synchronized queue.firstOrNull()?.url?.toID()
            }

            return@synchronized null
        }
    }

    // return the previous item, or if repeating enabled and no video left, the last one of the queue
    fun getPrev(): String? {
        return synchronized(queue) {
            val prevItem = queue.getOrNull(currentIndex() - 1)
            if (prevItem != null) return@synchronized prevItem.url?.toID()

            if (repeatMode == Player.REPEAT_MODE_ALL) return@synchronized queue.lastOrNull()?.url?.toID()

            return@synchronized null
        }
    }

    fun hasPrev() = getPrev() != null

    fun hasNext() = getNext() != null

    fun updateCurrent(streamItem: StreamItem) = synchronized(queue) {
        currentStream = streamItem
        streamItem.url?.toID()?.let { playedVideoIds.add(it) }

        if (!contains(streamItem)) add(streamItem)
        
        // Update the cached index
        currentStreamIndex = queue.indexOfLast { it.url?.toID() == streamItem.url?.toID() }
    }

    fun isNotEmpty() = queue.isNotEmpty()

    fun isEmpty() = queue.isEmpty()

    fun size() = queue.size

    fun isLast() = currentIndex() == size() - 1

    fun currentIndex(): Int = synchronized(queue) {
        // Validate cached index first - if it's still valid, use it
        if (currentStreamIndex >= 0 && currentStreamIndex < queue.size &&
            queue[currentStreamIndex].url?.toID() == currentStream?.url?.toID()) {
            return currentStreamIndex
        }
        
        // If cached index is invalid, search for current stream
        val idx = queue.indexOfFirst { it.url?.toID() == currentStream?.url?.toID() }
        if (idx >= 0) {
            currentStreamIndex = idx
            return idx
        }
        
        // If current stream not found, keep last valid index or default to 0
        return if (currentStreamIndex >= 0 && currentStreamIndex < queue.size) {
            currentStreamIndex
        } else {
            0
        }
    }

    fun getCurrent(): StreamItem? = currentStream

    fun contains(streamItem: StreamItem) = synchronized(queue) {
        queue.any { it.url?.toID() == streamItem.url?.toID() }
    }

    // only returns a copy of the queue, no write access
    fun getStreams() = queue.toList()

    fun setStreams(streams: List<StreamItem>) = synchronized(queue) {
        queue.clear()
        queue.addAll(streams)
        currentStreamIndex = currentStream?.url?.toID()?.let { currentId ->
            queue.indexOfFirst { it.url?.toID() == currentId }
        } ?: -1
    }

    fun remove(index: Int) = synchronized(queue) {
        queue.removeAt(index)
        return@synchronized
    }

    fun move(from: Int, to: Int) = synchronized(queue) {
        queue.move(from, to)
    }

    /**
     * Adds a list of videos to the current queue while updating the position of the current stream
     * @param isMainList whether the videos are part of the list that initially has been used to
     * start the queue, either from a channel or playlist. If it's false, the current stream won't
     * be touched, since it's an independent list.
     */
    private fun addToQueueAsync(
        streams: List<StreamItem>, currentStreamItem: StreamItem? = null, isMainList: Boolean = true
    ) {
        synchronized(queue) {
            if (!isMainList) {
                add(*streams.toTypedArray())
                return@synchronized
            }
            val currentStream = currentStreamItem ?: this.currentStream
            // if the stream already got added to the queue earlier, although it's not yet
            // been found in the playlist, remove it and re-add it later
            var reAddStream = true
            if (currentStream != null && streams.any { it.url?.toID() == currentStream.url?.toID() }) {
                queue.removeAll { it.url?.toID() == currentStream.url?.toID() }
                reAddStream = false
            }
            // add all new stream items to the queue
            add(*streams.toTypedArray())

            if (currentStream != null && reAddStream) {
                // re-add the stream to the end of the queue
                updateCurrent(currentStream)
            }
        }
    }

    private suspend fun fetchMoreFromPlaylist(
        playlistId: String,
        nextPage: String?,
        isMainList: Boolean
    ) {
        var playlistNextPage = nextPage
        while (playlistNextPage != null) {
            MediaServiceRepository.instance.getPlaylistNextPage(playlistId, playlistNextPage).run {
                addToQueueAsync(relatedStreams, isMainList = isMainList)
                playlistNextPage = this.nextpage
            }
        }
    }

    fun insertPlaylist(playlistId: String, newCurrentStream: StreamItem?) = runCatchingIO {
        val playlist = PlaylistsHelper.getPlaylist(playlistId)
        val isMainList = newCurrentStream != null
        addToQueueAsync(playlist.relatedStreams, newCurrentStream, isMainList)
        if (playlist.nextpage == null) return@runCatchingIO
        fetchMoreFromPlaylist(playlistId, playlist.nextpage, isMainList)
    }.let { queueJobs.add(it) }

    private suspend fun fetchMoreFromChannel(channelId: String, nextPage: String?) {
        var channelNextPage = nextPage
        var pageIndex = 1
        while (channelNextPage != null && pageIndex < 10) {
            MediaServiceRepository.instance.getChannelNextPage(channelId, channelNextPage).run {
                addToQueueAsync(relatedStreams)
                channelNextPage = this.nextpage
                pageIndex++
            }
        }
    }

    private fun insertChannel(channelId: String, newCurrentStream: StreamItem) = runCatchingIO {
        val channel = MediaServiceRepository.instance.getChannel(channelId)
        addToQueueAsync(channel.relatedStreams, newCurrentStream)
        if (channel.nextpage == null) return@runCatchingIO
        fetchMoreFromChannel(channelId, channel.nextpage)
    }.let { queueJobs.add(it) }

    fun insertByVideoId(videoId: String) = runCatchingIO {
        val streams = MediaServiceRepository.instance.getStreams(videoId.toID())
        add(streams.toStreamItem(videoId))
    }

    fun updateQueue(
        streamItem: StreamItem,
        playlistId: String?,
        channelId: String?,
        relatedStreams: List<StreamItem> = emptyList()
    ) {
        updateCurrent(streamItem)

        if (playlistId != null) {
            insertPlaylist(playlistId, streamItem)
        } else if (channelId != null) {
            insertChannel(channelId, streamItem)
        } else if (relatedStreams.isNotEmpty()) {
            insertRelatedStreams(relatedStreams)
        }
    }

    fun insertRelatedStreams(streams: List<StreamItem>) {
        if (!PlayerHelper.autoInsertRelatedVideos) return

        // don't add new videos to the queue if the user chose to repeat only the current queue
        if (isLast() && repeatMode == Player.REPEAT_MODE_ALL) return

        val unplayedStreams = streams.filter {
            !it.isLive && it.url?.toID() !in playedVideoIds
        }

        add(*unplayedStreams.toTypedArray(), skipExisting = true)
    }
}

enum class PlayingQueueMode {
    ONLINE,
    OFFLINE
}