package com.github.libretube.services

import android.content.Intent
import android.os.Bundle
import androidx.annotation.OptIn
import androidx.core.os.bundleOf
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaItem.SubtitleConfiguration
import androidx.media3.common.MimeTypes
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.FileDataSource
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.MergingMediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.exoplayer.source.SingleSampleMediaSource
import com.github.libretube.constants.IntentData
import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.db.obj.DownloadWithItems
import com.github.libretube.db.obj.filterByTab
import com.github.libretube.enums.FileType
import com.github.libretube.extensions.parcelable
import com.github.libretube.extensions.setMetadata
import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.extensions.toAndroidUri
import com.github.libretube.extensions.toID
import com.github.libretube.extensions.updateParameters
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.parcelable.PlayerData
import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.activities.NoInternetActivity
import com.github.libretube.ui.fragments.DownloadTab
import com.github.libretube.ui.fragments.DownloadsFragmentPage.Companion.sortDownloadList
import com.github.libretube.util.PlayingQueue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.io.path.exists
import kotlin.io.path.fileSize

/**
 * A service to play downloaded audio in the background
 */
@OptIn(UnstableApi::class)
open class OfflinePlayerService : AbstractPlayerService() {
    override val isOfflinePlayer: Boolean = true
    private var noInternetService: Boolean = false

    private var downloadWithItems: DownloadWithItems? = null
    private lateinit var playerData: PlayerData
    private var lastHistoryVideoId: String? = null

    private val scope = CoroutineScope(Dispatchers.Main)

    private val playerListener = object : Player.Listener {
        override fun onPlaybackStateChanged(playbackState: Int) {
            if (playbackState == Player.STATE_ENDED) {
                if (exoPlayer?.hasNextMediaItem() == true) return
                playNextVideo()
            }

            if (playbackState == Player.STATE_READY && PlayerHelper.watchHistoryEnabled) {
                val readyVideoId = videoId
                if (lastHistoryVideoId != readyVideoId) {
                    lastHistoryVideoId = readyVideoId
                    scope.launch(Dispatchers.IO) {
                        val currentDownload = Database.downloadDao().findById(readyVideoId)
                            ?: return@launch
                        val watchHistoryItem = currentDownload.download
                            .toStreamItem()
                            .toWatchHistoryItem(readyVideoId)
                        DatabaseHelper.addToWatchHistory(watchHistoryItem)
                    }
                }
            }
        }

        override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
            super.onMediaItemTransition(mediaItem, reason)
            val newVideoId = mediaItem?.mediaMetadata?.extras?.getString(IntentData.videoId)
                ?: return
            if (newVideoId == videoId) return

            videoId = newVideoId
            updatePlaylistMetadata {
                setExtras(bundleOf(IntentData.videoId to newVideoId))
            }
            PlayingQueue.getStreams()
                .firstOrNull { it.url?.toID() == newVideoId }
                ?.let(PlayingQueue::updateCurrent)
            resetSponsorBlockSegments()

            scope.launch(Dispatchers.IO) {
                val currentDownload = Database.downloadDao().findById(newVideoId) ?: return@launch
                downloadWithItems = currentDownload
                val segments = currentDownload.downloadSponsorBlockSegments.map { it.toSegment() }
                withContext(Dispatchers.Main) {
                    configureOfflineTracks(currentDownload)
                    setSponsorBlockSegments(segments)
                    addNextOfflineMediaItemIfAvailable()
                }
            }
        }
    }

    override suspend fun onServiceCreated(args: Bundle) {
        if (args.isEmpty) return

        playerData = args.parcelable(IntentData.playerData)!!
        noInternetService = args.getBoolean(IntentData.noInternet, false)
        isAudioOnlyPlayer = args.getBoolean(IntentData.audioOnly, false)

        PlayingQueue.clear()

        this.videoId = if (playerData.shuffle) {
            runBlocking(Dispatchers.IO) {
                if (playerData.downloadTab == DownloadTab.PLAYLIST) {
                    Database.downloadDao()
                        .getDownloadPlaylistById(playerData.playlistId!!).downloadVideos.randomOrNull()
                } else {
                    Database.downloadDao().getAll().filterByTab(playerData.downloadTab!!)
                        .randomOrNull()?.download
                }
            }?.videoId
        } else {
            playerData.videoId
        } ?: return

        exoPlayer?.addListener(playerListener)
        trackSelector?.updateParameters {
            setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, isAudioOnlyPlayer)
        }

        fillQueue()
    }

    override fun getIntentActivity(): Class<*> {
        return if (noInternetService) NoInternetActivity::class.java else MainActivity::class.java
    }

    /**
     * Attempt to start an audio player with the given download items
     */
    override suspend fun startPlayback() {
        super.startPlayback()

        val downloadWithItems = withContext(Dispatchers.IO) {
            Database.downloadDao().findById(videoId)
        } ?: return
        this.downloadWithItems = downloadWithItems

        PlayingQueue.updateCurrent(downloadWithItems.download.toStreamItem())

        withContext(Dispatchers.Main) {
            setSponsorBlockSegments(
                downloadWithItems.downloadSponsorBlockSegments.map { it.toSegment() }
            )

            if (!setMediaItem(downloadWithItems)) {
                // Keep the UI/service alive and fail gracefully instead of tearing down the app
                // when an old/interrupted download has no complete playable file.
                toastFromMainThread(com.github.libretube.R.string.downloadfailed)
                return@withContext
            }
            addNextOfflineMediaItemIfAvailable()

            // automatically start playback when using the audio player
            exoPlayer?.playWhenReady = PlayerHelper.playAutomatically || isAudioOnlyPlayer
            exoPlayer?.prepare()

            // Disable restore from saved watch position to always start playback from the beginning.
            // if (watchPositionsEnabled) {
            //     DatabaseHelper.getWatchPosition(videoId)?.let {
            //         if (!DatabaseHelper.isVideoWatched(
            //                 it,
            //                 downloadWithItems.download.duration
            //             )
            //         ) exoPlayer?.seekTo(it)
            //     }
            // }
        }
    }

    private fun setMediaItem(downloadWithItems: DownloadWithItems): Boolean {
        configureOfflineTracks(downloadWithItems)
        val mediaSource = buildOfflineMediaSource(downloadWithItems) ?: return false
        exoPlayer?.setMediaSource(mediaSource)
        exoPlayer?.seekTo(0L)
        return true
    }

    private fun buildOfflineMediaSource(downloadWithItems: DownloadWithItems): MediaSource? {
        val downloadFiles = downloadWithItems.downloadItems.filter { item ->
            runCatching { item.path.exists() && item.path.fileSize() > 0L }.getOrDefault(false)
        }
        if (downloadFiles.isEmpty()) return null

        val videoUri = downloadFiles.firstOrNull { it.type == FileType.VIDEO }?.path?.toAndroidUri()
        val audioUri = downloadFiles.firstOrNull { it.type == FileType.AUDIO }?.path?.toAndroidUri()
        val subtitleInfo = downloadFiles.firstOrNull { it.type == FileType.SUBTITLE }

        val videoSource = videoUri?.let { uri ->
            val item = MediaItem.Builder()
                .setUri(uri)
                .setMetadata(downloadWithItems)
                .build()
            ProgressiveMediaSource.Factory(FileDataSource.Factory()).createMediaSource(item)
        }

        val audioSource = audioUri?.let { uri ->
            val item = MediaItem.Builder()
                .setUri(uri)
                .setMetadata(downloadWithItems)
                .build()
            ProgressiveMediaSource.Factory(FileDataSource.Factory()).createMediaSource(item)
        }

        val subtitleSource = subtitleInfo?.let { subtitle ->
            val config = SubtitleConfiguration.Builder(subtitle.path.toAndroidUri())
                .setMimeType(MimeTypes.APPLICATION_TTML)
                .setLanguage(subtitle.language ?: "en")
                .build()
            SingleSampleMediaSource.Factory(FileDataSource.Factory())
                .createMediaSource(config, C.TIME_UNSET)
        }

        if (isAudioOnlyPlayer && audioSource == null) return null

        var merged: MediaSource? = null
        listOfNotNull(videoSource, audioSource, subtitleSource).forEach { source ->
            merged = if (merged == null) source else MergingMediaSource(merged!!, source)
        }
        return merged
    }

    private fun configureOfflineTracks(downloadWithItems: DownloadWithItems) {
        val subtitleLanguage = downloadWithItems.downloadItems
            .firstOrNull { it.type == FileType.SUBTITLE && it.path.exists() }
            ?.language ?: "en"
        trackSelector?.updateParameters {
            setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)
            setPreferredTextLanguage(subtitleLanguage)
            setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, isAudioOnlyPlayer)
        }
    }

    private suspend fun fillQueue() {
        if (playerData.downloadTab == DownloadTab.PLAYLIST) {
            var videos = withContext(Dispatchers.IO) {
                Database.downloadDao().getDownloadPlaylistById(playerData.playlistId!!)
            }.downloadVideos

            if (playerData.shuffle) videos = listOf(videos.first { it.videoId == videoId }) +
                    videos.filter { it.videoId != videoId }.shuffled()
            else if (playerData.downloadSortingOrder != null) videos =
                sortDownloadList(videos, playerData.downloadSortingOrder!!)
            PlayingQueue.setStreams(videos.map { it.toStreamItem() })
        } else {
            var downloads = withContext(Dispatchers.IO) {
                Database.downloadDao().getAll()
            }
                .filterByTab(playerData.downloadTab!!)
                .map { it.download }

            if (playerData.shuffle) downloads = downloads.shuffled()
            else if (playerData.downloadSortingOrder != null) downloads = sortDownloadList(downloads, playerData.downloadSortingOrder!!)

            PlayingQueue.add(*downloads.map { it.toStreamItem() }.toTypedArray())
        }
    }

    private fun addNextOfflineMediaItemIfAvailable() {
        val nextVideoId = PlayingQueue.getNext() ?: return
        if (nextVideoId == videoId || isOfflineMediaQueued(nextVideoId)) return

        scope.launch(Dispatchers.IO) {
            val nextDownloadWithItems = Database.downloadDao().findById(nextVideoId) ?: return@launch
            val nextMediaSource = buildOfflineMediaSource(nextDownloadWithItems) ?: return@launch

            withContext(Dispatchers.Main) {
                if (PlayingQueue.getNext() == nextVideoId && !isOfflineMediaQueued(nextVideoId)) {
                    exoPlayer?.addMediaSource(nextMediaSource)
                }
            }
        }
    }

    private fun isOfflineMediaQueued(videoId: String): Boolean {
        val player = exoPlayer ?: return false
        for (index in 0 until player.mediaItemCount) {
            val itemVideoId = player.getMediaItemAt(index).mediaMetadata.extras
                ?.getString(IntentData.videoId)
            if (itemVideoId == videoId) return true
        }
        return false
    }

    private fun playNextVideo(videoId: String? = null) {
        if (!PlayerHelper.isAutoPlayEnabled() || !shouldHandleAutoplay) return

        val nextId = videoId ?: PlayingQueue.getNext() ?: return
        navigateVideo(nextId)
    }

    override fun onTaskRemoved(rootIntent: Intent?) {
        // Keep downloaded playback alive when the UI is dismissed.
        super.onTaskRemoved(rootIntent)
    }
}
