package ac.mdiq.podcini.net.feed

import ac.mdiq.podcini.PodciniApp.Companion.getAppContext
import ac.mdiq.podcini.R
import ac.mdiq.podcini.automation.AutoDownloadAlgorithm
import ac.mdiq.podcini.automation.AutoEnqueueAlgorithm
import ac.mdiq.podcini.config.CHANNEL_ID
import ac.mdiq.podcini.net.download.DownloadError
import ac.mdiq.podcini.net.download.DownloadRequest
import ac.mdiq.podcini.net.download.DownloadRequest.Companion.requestFor
import ac.mdiq.podcini.net.download.Downloader.Companion.downloaderFor
import ac.mdiq.podcini.net.feed.PodcastHandler.FeedHandlerResult
import ac.mdiq.podcini.net.utils.NetworkUtils.isFeedRefreshAllowed
import ac.mdiq.podcini.net.utils.NetworkUtils.mobileAllowFeedRefresh
import ac.mdiq.podcini.net.utils.NetworkUtils.networkMonitor
import ac.mdiq.podcini.shared.EpisodeIPC
import ac.mdiq.podcini.sources.EPISODE_BATCH_SIZE
import ac.mdiq.podcini.sources.sourceClients
import ac.mdiq.podcini.sources.typeClientMap
import ac.mdiq.podcini.storage.database.EPISODES_LIMIT
import ac.mdiq.podcini.storage.database.appAttribs
import ac.mdiq.podcini.storage.database.appPrefs
import ac.mdiq.podcini.storage.database.compileLanguages
import ac.mdiq.podcini.storage.database.compileTags
import ac.mdiq.podcini.storage.database.feedOperationText
import ac.mdiq.podcini.storage.database.getFeedList
import ac.mdiq.podcini.storage.database.realm
import ac.mdiq.podcini.storage.database.unmanaged
import ac.mdiq.podcini.storage.database.updateFeedFull
import ac.mdiq.podcini.storage.database.updateFeedSimple
import ac.mdiq.podcini.storage.database.updateLocalFeed
import ac.mdiq.podcini.storage.database.upsert
import ac.mdiq.podcini.storage.database.upsertBlk
import ac.mdiq.podcini.storage.model.DownloadResult
import ac.mdiq.podcini.storage.model.DownloadResult.Companion.logDownloadResult
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.toFeed
import ac.mdiq.podcini.storage.specs.VolumeAdaptionSetting
import ac.mdiq.podcini.storage.utils.toUF
import ac.mdiq.podcini.ui.compose.CommonConfirmAttrib
import ac.mdiq.podcini.ui.compose.commonConfirm
import ac.mdiq.podcini.utils.EventFlow
import ac.mdiq.podcini.utils.FlowEvent
import ac.mdiq.podcini.utils.LogFor
import ac.mdiq.podcini.utils.Logd
import ac.mdiq.podcini.utils.Loge
import ac.mdiq.podcini.utils.Logs
import ac.mdiq.podcini.utils.Logt
import android.Manifest
import android.app.Notification
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import io.github.xilinjia.krdb.ext.toRealmSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.io.IOException
import org.xml.sax.SAXException
import javax.xml.parsers.ParserConfigurationException
import kotlin.collections.listOf

class FeedUpdater(val feeds: List<Feed>, val fullUpdate: Boolean = false, val doItAnyway: Boolean = false, val removeUnlisted: Boolean = false) {
    private val context = getAppContext()
    private val notificationManager = NotificationManagerCompat.from(context)

    private var feedsToUpdate: MutableList<Feed> = mutableListOf()
    private val feedsToOnlyDownload: MutableList<Feed> = mutableListOf()
    private val feedsToOnlyEnqueue: MutableList<Feed> = mutableListOf()

    var force = false

    private suspend fun onFail(feed: Feed, details: String, reason: DownloadError = DownloadError.ERROR_MISC) {
        LogFor(TAG, feed, false, details, reason = reason)
        upsert(feed) { it.lastUpdateFailed = true }
    }

    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    suspend fun start() {
        Logd(TAG, "start doItAnyway: $doItAnyway feeds: ${feeds.size}")
        prepare()
        val allLocalFeeds = run {
            for (f in feeds) {
                if (!f.isLocal) {
                    Logd(TAG, "start feed is not local: ${f.title}")
                    return@run false
                }
            }
            true
        }
        Logd(TAG, "start allLocalFeeds: $allLocalFeeds")
        when {
            allLocalFeeds -> scope.launch { refresh() }
            !networkMonitor.isConnected -> EventFlow.postEvent(FlowEvent.MessageEvent(context.getString(R.string.download_error_no_connection)))
            isFeedRefreshAllowed -> scope.launch { refresh() }
            else -> {
                commonConfirm = CommonConfirmAttrib(
                    title = context.getString(R.string.feed_refresh_title),
                    message = context.getString(if (networkMonitor.isNetworkRestricted && networkMonitor.isVpnOverWifi) R.string.confirm_mobile_feed_refresh_dialog_message_vpn else R.string.confirm_mobile_feed_refresh_dialog_message),
                    confirmRes = R.string.confirm_mobile_streaming_button_once,
                    cancelRes = R.string.no,
                    neutralRes = R.string.confirm_mobile_streaming_button_always,
                    onConfirm = { scope.launch { refresh() }  },
                    onNeutral = {
                        mobileAllowFeedRefresh = true
                        scope.launch { refresh() }
                    })
            }
        }
    }

    suspend fun prepare() {
        withContext(Dispatchers.Main) { feedOperationText = context.getString(R.string.preparing) }
        Logd(TAG, "prepare feeds: ${feeds.size}")
        if (feeds.isEmpty()) {
            val feedIds = appAttribs.feedIdsToRefresh
            if (feedIds.isNotEmpty()) {
                Logt(TAG, "prepare Partial refresh of ${feedIds.size} feeds")
                feedsToUpdate = realm.query(Feed::class, "id IN $0", feedIds).find().filter { it.inNormalVolume }.toMutableList()
            } else feedsToUpdate = getFeedList("keepUpdated == true").filter { it.inNormalVolume }.toMutableList()
        } else {
            feedsToUpdate = feeds.filter { it.inNormalVolume }.toMutableList()
            force = true
        }
        Logd(TAG, "prepare feedsToUpdate: ${feedsToUpdate.size}")
        if (!doItAnyway) {
            val itr = feedsToUpdate.iterator()
            while (itr.hasNext()) {
                val feed = itr.next()
                if (!feed.keepUpdated) {
                    LogFor(TAG, feed, true, "feed set not to update, igored.", toastAnyway = true)
                    if (feed.autoEnqueue) feedsToOnlyEnqueue.add(feed)
                    else if (feed.autoDownload) feedsToOnlyDownload.add(feed)
                    itr.remove()
                }
            }
        }
    }

    suspend fun refresh() {
        Logd(TAG, "refresh feedsToUpdate: ${feedsToUpdate.size}")
        withContext(Dispatchers.Main) { feedOperationText = context.getString(R.string.refreshing_label) }
        if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
            Loge(TAG, "refresh: require POST_NOTIFICATIONS permission")
            return
        }
        val titles = feedsToUpdate.map { it.title ?: "No title" }.toMutableList()
        val feedIdsToRefresh = feedsToUpdate.map { it.id }.toMutableList()
        var i = 0
        while (i < feedsToUpdate.size) {
            notificationManager.notify(R.id.notification_updating_feeds, createNotification(titles))
            val feed = unmanaged(feedsToUpdate[i++])
            try {
                Logd(TAG, "refresh updating local feed? ${feed.isLocal} ${feed.title}")
                when {
                    feed.isLocal -> updateLocalFeed(feed, null)
                    else -> refreshFeed(feed)
                }
            } catch (e: Exception) { onFail(feed, "refresh: update failed ${feed.title} ${e.message}") }
            titles.removeAt(0)
            feedIdsToRefresh.removeAt(0)
            upsertBlk(appAttribs) { it.feedIdsToRefresh = feedIdsToRefresh.toRealmSet() }
        }
        // TODO: not sure these need to be here
        compileLanguages()
        compileTags()

        notificationManager.cancel(R.id.notification_updating_feeds)
        withContext(Dispatchers.Main) { feedOperationText = context.getString(R.string.post_refreshing) }

        try {
            if (feedsToOnlyEnqueue.isNotEmpty()) feedsToUpdate.addAll(feedsToOnlyEnqueue)
            if (feedsToOnlyDownload.isNotEmpty()) feedsToUpdate.addAll(feedsToOnlyDownload)
            AutoEnqueueAlgorithm().run(feedsToUpdate)
            if (appPrefs.enableAutoDl) AutoDownloadAlgorithm().run(feedsToUpdate)
        } finally {
            feedsToUpdate.clear()
            feedsToOnlyEnqueue.clear()
            feedsToOnlyDownload.clear()
            withContext(Dispatchers.Main + NonCancellable) { feedOperationText = "" }
        }
    }

    private suspend fun downloadFeed(feed: Feed): Feed? {
        var feed_: Feed? = null
        val nextPage = false
        //        val nextPage = (inputData.getBoolean(EXTRA_NEXT_PAGE, false) && feed.nextPageLink != null)
        if (nextPage) feed.pageNr += 1
        val builder = requestFor(feed)
        if (force || feed.lastUpdateFailed) builder.lastModified = null
        if (nextPage) builder.source = feed.nextPageLink
        val request = builder.build()
        val downloader = downloaderFor(request) ?: throw Exception("Unable to create downloader")
        var downloadResult: DownloadResult? = null
        var isSuccessful = true
        var feedRaw = Feed()
        var reason: DownloadError? = null
        var reasonDetailed: String? = null
        var feedHandlerResult: FeedHandlerResult? = null
        downloader.download { source ->
            feedRaw = Feed(request.source, request.lastModified)
            feedRaw.id = request.feedfileId
            feedRaw.limitEpisodesCount = feed.limitEpisodesCount
            feedRaw.fillPreferences(false, Feed.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, request.username, request.password)
            if (request.arguments != null) feedRaw.pageNr = request.arguments.getInt(DownloadRequest.REQUEST_ARG_PAGE_NR, 0)

            try {
                feedHandlerResult = PodcastHandler.parseFeed(source, feedRaw)
                Logd(TAG,  "downloadFeed Parsed ${feedRaw.title}")
                if (feedRaw.title.isNullOrBlank()) throw InvalidFeedException("Feed has no title")
                for (item in feedRaw.episodes) if (item.title.isNullOrBlank()) LogFor(TAG, feedRaw, true, "episode ${item.id} title is empty", toastAnyway = true)
                if (feedRaw.imageUrl.isNullOrEmpty()) feedRaw.imageUrl = feedRaw.downloadUrl
                if (feedHandlerResult != null) feed_ = feedHandlerResult!!.feed
                Logd(TAG, "downloadFeed completed feed_: ${feed_?.title}")
            } catch (e: SAXException) {
                isSuccessful = false
                Logs(TAG, e, "SAXException")
                reason = DownloadError.ERROR_PARSER_EXCEPTION
                reasonDetailed = e.message
            } catch (e: IOException) {
                isSuccessful = false
                Logs(TAG, e, "IOException")
                reason = DownloadError.ERROR_IO_ERROR
                reasonDetailed = e.message
            } catch (e: ParserConfigurationException) {
                isSuccessful = false
                Logs(TAG, e, "ParserConfigurationException")
                reason = DownloadError.ERROR_PARSER_EXCEPTION
                reasonDetailed = e.message
            } catch (e: PodcastHandler.UnsupportedFeedtypeException) {
                Logs(TAG, e, "UnsupportedFeedtypeException")
                isSuccessful = false
                reason = DownloadError.ERROR_UNSUPPORTED_TYPE
                if ("html".equals(e.rootElement, ignoreCase = true)) reason = DownloadError.ERROR_UNSUPPORTED_TYPE_HTML
                reasonDetailed = e.message
            } catch (e: InvalidFeedException) {
                Logs(TAG, e, "InvalidFeedException")
                isSuccessful = false
                reason = DownloadError.ERROR_PARSER_EXCEPTION
                reasonDetailed = e.message
            } finally {
                val feedFile = (request.destination).toUF()
                if (feedFile.exists()) {
                    feedFile.delete()
                    Logd(TAG, "downloadFeed Deletion of file '" + feedFile.absPath + "' ")
                }
            }
            if (isSuccessful) downloadResult = DownloadResult(feedRaw, DownloadError.SUCCESS, true, "")

            if (!request.source.isNullOrEmpty()) {
                fun updateFeedDownloadURL(original: String, updated: String) {
                    Logd(TAG, "updateFeedDownloadURL(original: $original, updated: $updated)")
                    val feed = realm.query(Feed::class).query("downloadUrl == $0", original).first().find()
                    if (feed != null) upsertBlk(feed) { it.downloadUrl = updated }
                }
                val redirectUrl: String? = feedHandlerResult?.redirectUrl
                when {
                    !downloader.permanentRedirectUrl.isNullOrEmpty() -> updateFeedDownloadURL(request.source, downloader.permanentRedirectUrl!!)
                    !redirectUrl.isNullOrBlank() && redirectUrl != request.source -> updateFeedDownloadURL(request.source, redirectUrl)
                }
            }
            if (downloadResult?.isSuccessful != true) {
                if (downloader.cancelled || downloader.result.reason == DownloadError.ERROR_DOWNLOAD_CANCELLED) {
                    Logd(TAG, "downloadFeed: feed refresh cancelled, likely due to feed not changed: ${feed.title}")
                    return@download
                }
                LogFor(TAG, feed, false, "downloadFeed: feed update failed: unsuccessful. cancelled?")
                upsert(feed) { it.lastUpdateFailed = true }
                logDownloadResult(downloader.result)
                return@download
            }
        }
        if (!isSuccessful) {
            onFail(feedRaw, reasonDetailed ?: "", reason ?: DownloadError.ERROR_NOT_FOUND)
            return null
        }
        return feed_
    }

    suspend fun refreshFeed(feed: Feed) {
        if (feed.downloadUrl.isNullOrBlank()) return

        val client = if (feed.type != null) typeClientMap.get(feed.type) else null
        val feed_ = if (client != null) {
            val feedIpc = client.withProvider { it.feedToUpdate(feed.downloadUrl!!) }
            if (feedIpc != null) {
                val eList = mutableListOf<EpisodeIPC>()
                var episodes = client.withProvider { it.getEpisodes(EPISODE_BATCH_SIZE, if (fullUpdate) 0L else feed.lastUpdateTime) }?: listOf()
                while (episodes.isNotEmpty()) {
                    if (fullUpdate) eList.addAll(episodes)
                    else {
                        val eps = mutableListOf<EpisodeIPC>()
                        for (e in episodes) if (e.pubDate > feed.lastUpdateTime) eps.add(e)
                        if (eps.isEmpty()) break
                        eList.addAll(eps)
                    }
                    val numEpisodes = eList.size
                    if (feed.limitEpisodesCount in 1..<numEpisodes || numEpisodes > EPISODES_LIMIT || episodes.size < EPISODE_BATCH_SIZE) break
                    Logd(TAG, "Subscribing eList: ${eList.size}")
                    episodes = client.withProvider { it.getEpisodes(EPISODE_BATCH_SIZE, if (fullUpdate) 0L else feed.lastUpdateTime) }?: listOf()
                }
                feedIpc.episodes = eList
            }
            feedIpc?.toFeed()
        } else downloadFeed(feed)

        if (feed_ != null) {
            val downloadStatus = DownloadResult(feed_, DownloadError.SUCCESS, true, "")
            if (fullUpdate) updateFeedFull(feed_, removeUnlistedItems = removeUnlisted, downloadStatus = downloadStatus)
            else updateFeedSimple(feed_, downloadStatus)

            if (!downloadStatus.isSuccessful) {
                LogFor(TAG, feed, false, "refreshFeed: feed update failed: unsuccessful. cancelled?")
                upsert(feed) { it.lastUpdateFailed = true }
            }
            logDownloadResult(downloadStatus)
        } else {
            LogFor(TAG, feed, false, "refreshFeed: feed update failed: unsuccessful. cancelled?")
            upsert(feed) { it.lastUpdateFailed = true }
        }
    }

    class InvalidFeedException(message: String?) : Exception(message)

    companion object {
        private const val TAG = "FeedUpdater"

        fun createNotification(titles: List<String>?): Notification {
            val context = getAppContext()
            var contentText = ""
            var bigText: String? = ""
            if (titles != null) {
                contentText = context.resources.getQuantityString(R.plurals.downloads_left, titles.size, titles.size)
                bigText = titles.joinToString("\n") { "• $it" }
            }
            return NotificationCompat.Builder(context, CHANNEL_ID.refreshing.name)
                .setContentTitle(context.getString(R.string.download_notification_title_feeds))
                .setContentText(contentText)
                .setStyle(NotificationCompat.BigTextStyle().bigText(bigText))
                .setSmallIcon(R.drawable.ic_notification_sync)
                .setOngoing(true)
//                .addAction(R.drawable.ic_cancel, context.getString(R.string.cancel_label), WorkManager.getInstance(context).createCancelPendingIntent(id))
                .build()
        }
    }
}