package melody.offline.music.util

import android.app.Notification
import android.content.Context
import android.net.Uri
import androidx.annotation.OptIn
import androidx.core.net.toUri
import androidx.media3.common.PlaybackException
import androidx.media3.common.util.NotificationUtil
import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util
import androidx.media3.database.DatabaseProvider
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.ResolvingDataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cronet.CronetDataSource
import androidx.media3.datasource.cronet.CronetUtil
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadNotificationHelper
import melody.offline.music.R
import melody.offline.music.innertube.Innertube
import melody.offline.music.innertube.Innertube.TAG
import melody.offline.music.innertube.models.bodies.PlayerBody
import melody.offline.music.innertube.requests.player
import melody.offline.music.service.LoginRequiredException
import melody.offline.music.service.PlayableFormatNotFoundException
import melody.offline.music.service.UnplayableException
import melody.offline.music.service.VideoIdMismatchException
import melody.offline.music.service.ViewModelMain
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import melody.offline.music.App
import java.net.CookieHandler
import java.net.CookieManager
import java.net.CookiePolicy
import java.util.concurrent.Executor
import java.util.concurrent.Executors


@OptIn(markerClass = [UnstableApi::class])
object DownloadUtil {
    private const val DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel"

    private const val USE_CRONET_FOR_NETWORKING = true

    private var httpDataSourceFactory: DataSource.Factory? = null
    private var databaseProvider: DatabaseProvider? = null
    private val playerCache = CacheManager.getPlayerCache()
    private val downloadCache = CacheManager.getDownloadCache()
    private var downloadManager: DownloadManager? = null

    private var downloadNotificationHelper: DownloadNotificationHelper? = null

    @Synchronized
    fun getHttpDataSourceFactory(context: Context): DataSource.Factory? {
        if (httpDataSourceFactory == null) {
            if (USE_CRONET_FOR_NETWORKING) {
                val cronetEngine = CronetUtil.buildCronetEngine(context.applicationContext)
                if (cronetEngine != null) {
                    httpDataSourceFactory =
                        CronetDataSource.Factory(cronetEngine, Executors.newSingleThreadExecutor())
                }
            }
            if (httpDataSourceFactory == null) {
                // We don't want to use Cronet, or we failed to instantiate a CronetEngine.
                val cookieManager = CookieManager()
                cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER)
                CookieHandler.setDefault(cookieManager)
                httpDataSourceFactory = DefaultHttpDataSource.Factory()
            }
        }
        return httpDataSourceFactory
    }

    /**
     * Returns a [DataSource.Factory].
     */
    @Synchronized
    fun getDataSourceFactory(context: Context): DataSource.Factory {
        val upstreamFactory = DefaultDataSource.Factory(
            context.applicationContext,
            getHttpDataSourceFactory(context.applicationContext)!!
        )
        val ringBuffer = RingBuffer<Pair<String, Uri>?>(2) { null }
        return ResolvingDataSource.Factory(
            CacheDataSource.Factory()
                .setCache(playerCache!!)
                .setUpstreamDataSourceFactory(upstreamFactory)
        ) { dataSpec ->
            val videoId = dataSpec.key ?: error("A key must be set")
            val length = if (dataSpec.length >= 0) dataSpec.length else 1

            if (playerCache.isCached(videoId, dataSpec.position, length)) {
                LogTag.LogD(TAG, "下载 getDataSourceFactory playerCache")
                return@Factory dataSpec
            }
            when (videoId) {
                ringBuffer.getOrNull(0)?.first -> dataSpec.withUri(ringBuffer.getOrNull(0)!!.second)
                ringBuffer.getOrNull(1)?.first -> dataSpec.withUri(ringBuffer.getOrNull(1)!!.second)
                else -> {
                    val urlResult = runBlocking(Dispatchers.IO) {
                        Innertube.player(PlayerBody(videoId = videoId))
                    }?.mapCatching { body ->
                        if (body.videoDetails?.videoId != videoId) {
                            throw VideoIdMismatchException()
                        }

                        when (val status = body.playabilityStatus?.status) {
                            "OK" -> body.streamingData?.highestQualityFormat?.let { format ->
                                format.url
                            } ?: throw PlayableFormatNotFoundException()

                            "UNPLAYABLE" -> throw UnplayableException()
                            "LOGIN_REQUIRED" -> throw LoginRequiredException()
                            else -> throw PlaybackException(
                                status,
                                null,
                                PlaybackException.ERROR_CODE_REMOTE_ERROR
                            )
                        }
                    }
                    LogTag.LogD(TAG, "DownloadUtil urlResult->$urlResult")

                    urlResult?.getOrThrow()?.let { url ->
                        ringBuffer.append(videoId to url.toUri())
                        dataSpec.withUri(url.toUri())
                    } ?: throw PlaybackException(
                        null,
                        urlResult?.exceptionOrNull(),
                        PlaybackException.ERROR_CODE_REMOTE_ERROR
                    )
                }
            }
        }

    }

    @Synchronized
    fun getDownloadNotificationHelper(
        context: Context?
    ): DownloadNotificationHelper? {
        if (downloadNotificationHelper == null) {
            downloadNotificationHelper =
                DownloadNotificationHelper(context!!, DOWNLOAD_NOTIFICATION_CHANNEL_ID)
        }
        return downloadNotificationHelper
    }

    fun downloadResourceExist(id: String): Boolean {
        var isExist = false
        if (downloadManager != null) {
            val downloadIndex = downloadManager!!.downloadIndex
            downloadIndex.getDownloads()
                .use { cursor ->
                    while (cursor.moveToNext()) {
                        val download = cursor.download
                        if (download.request.id == id && download.state == Download.STATE_COMPLETED) {
                            isExist = true
                        }
                    }
                }
        }
        return isExist
    }

    fun getCurrentIdDownload(id: String): Download? {
        if (downloadManager != null) {
            val downloadIndex = downloadManager!!.downloadIndex
            downloadIndex.getDownloads()
                .use { cursor ->
                    while (cursor.moveToNext()) {
                        val download = cursor.download
                        if (download.request.id == id) {
                            return download
                        }
                    }
                }
        }
        return null
    }

    fun getDownloadCount(): Int {
        var count = 0
        if (downloadManager != null) {
            val downloadIndex = downloadManager!!.downloadIndex
            downloadIndex.getDownloads()
                .use { cursor ->
                    while (cursor.moveToNext()) {
                        count++
                    }
                }
        }
        return count
    }

    @Synchronized
    fun getDownloadManager(context: Context): DownloadManager? {
        ensureDownloadManagerInitialized(context)
        return downloadManager
    }

    fun getCurrentDownloads(): Int {
        val currentDownloads = downloadManager?.currentDownloads
        if (currentDownloads != null) {
            return currentDownloads.size
        }
        return 0
    }

    @Synchronized
    private fun ensureDownloadManagerInitialized(context: Context) {
        if (downloadManager == null) {
            downloadManager = DownloadManager(
                context,
                getDatabaseProvider(context)!!,
                downloadCache!!,
                getDataSourceFactory(context)!!,
                Executor(Runnable::run)
            )
        }
    }

    @Synchronized
    private fun getDatabaseProvider(context: Context): DatabaseProvider? {
        if (databaseProvider == null) {
            databaseProvider = StandaloneDatabaseProvider(context)
        }
        return databaseProvider
    }

    class TerminalStateNotificationHelper(
        private val context: Context,
        private val notificationHelper: DownloadNotificationHelper,
        private var nextNotificationId: Int
    ) :
        DownloadManager.Listener {

        override fun onDownloadChanged(
            downloadManager: DownloadManager,
            download: Download,
            finalException: Exception?
        ) {
            val downloadsMap = mutableMapOf<String, Download>()
            downloadsMap[download.request.id] = download
            ViewModelMain.modelDownloadsFlow.postValue(downloadsMap)

            val notification: Notification = when (download.state) {
                Download.STATE_COMPLETED -> {
                    //更新大小
                    CoroutineScope(Dispatchers.IO).launch {
                        val offlineBean =
                            App.appOfflineDBManager.getOfflineBeanByID(download.request.id)
                        if (offlineBean != null) {
                            offlineBean.size =
                                FileSizeConverter(download.bytesDownloaded).formattedSize()
                            offlineBean.bytesDownloaded = download.bytesDownloaded
                            App.appOfflineDBManager.updateOfflineBean(offlineBean)
                        }
                    }

                    notificationHelper.buildDownloadCompletedNotification(
                        context,
                        R.drawable.download_done_icon,
                        null,
                        Util.fromUtf8Bytes(download.request.data)
                    )
                }

                Download.STATE_FAILED -> {
                    notificationHelper.buildDownloadFailedNotification(
                        context,
                        R.drawable.error,
                        null,
                        Util.fromUtf8Bytes(download.request.data)
                    )
                }

                else -> {
                    return
                }
            }
            NotificationUtil.setNotification(context, nextNotificationId++, notification)
        }
    }
}