package bou.amine.apps.readerforselfossv2.repository

import bou.amine.apps.readerforselfossv2.dao.*
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.*
import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class Repository(
    private val api: SelfossApi,
    private val appSettingsService: AppSettingsService,
    val isConnectionAvailable: MutableStateFlow<Boolean>,
    private val db: ReaderForSelfossDB,
) {
    var items = ArrayList<SelfossModel.Item>()
    var connectionMonitored = false

    var baseUrl = appSettingsService.getBaseUrl()

    var displayedItems = ItemType.UNREAD

    private var _tagFilter = MutableStateFlow<SelfossModel.Tag?>(null)
    var tagFilter = _tagFilter.asStateFlow()
    private var _sourceFilter = MutableStateFlow<SelfossModel.Source?>(null)
    var sourceFilter = _sourceFilter.asStateFlow()
    var searchFilter: String? = null

    var offlineOverride = false

    private val _badgeUnread = MutableStateFlow(0)
    val badgeUnread = _badgeUnread.asStateFlow()
    private val _badgeAll = MutableStateFlow(0)
    val badgeAll = _badgeAll.asStateFlow()
    private val _badgeStarred = MutableStateFlow(0)
    val badgeStarred = _badgeStarred.asStateFlow()

    private var fetchedTags = false
    private var fetchedSources = false

    private var _readerItems = ArrayList<SelfossModel.Item>()
    private var _selectedSource: SelfossModel.SourceDetail? = null

    suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
        var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
        if (isNetworkAvailable()) {
            fetchedItems =
                api.getItems(
                    displayedItems.type,
                    offset = 0,
                    tagFilter.value?.tag,
                    sourceFilter.value?.id?.toLong(),
                    searchFilter,
                    null,
                )
        } else if (appSettingsService.isItemCachingEnabled()) {
            var dbItems =
                getDBItems().filter {
                    displayedItems == ItemType.ALL ||
                        (it.unread && displayedItems == ItemType.UNREAD) ||
                        (it.starred && displayedItems == ItemType.STARRED)
                }
            if (tagFilter.value != null) {
                dbItems = dbItems.filter { it.tags.split(',').contains(tagFilter.value!!.tag) }
            }
            if (sourceFilter.value != null) {
                dbItems = dbItems.filter { it.sourcetitle == sourceFilter.value!!.title }
            }
            val itemsList = ArrayList(dbItems.map { it.toView() })
            itemsList.sortByDescending { DateUtils.parseDate(it.datetime) }
            fetchedItems =
                StatusAndData.succes(
                    itemsList,
                )
        }

        if (fetchedItems.success && fetchedItems.data != null) {
            items = ArrayList(fetchedItems.data!!)
        }
        return items
    }

    suspend fun getOlderItems(): ArrayList<SelfossModel.Item> {
        var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
        if (isNetworkAvailable()) {
            val offset = items.size
            fetchedItems =
                api.getItems(
                    displayedItems.type,
                    offset,
                    tagFilter.value?.tag,
                    sourceFilter.value?.id?.toLong(),
                    searchFilter,
                    null,
                )
        } // When using the db cache, we load everything the first time, so there should be nothing more to load.

        if (fetchedItems.success && fetchedItems.data != null) {
            items.addAll(fetchedItems.data!!)
        }
        return items
    }

    private suspend fun getMaxItemsForBackground(itemType: ItemType): List<SelfossModel.Item> {
        return if (isNetworkAvailable()) {
            val items =
                api.getItems(
                    itemType.type,
                    0,
                    null,
                    null,
                    null,
                    null,
                    200,
                )
            return if (items.success && items.data != null) {
                items.data
            } else {
                emptyList()
            }
        } else {
            emptyList()
        }
    }

    suspend fun reloadBadges(): Boolean {
        var success = false
        if (isNetworkAvailable()) {
            val response = api.stats()
            if (response.success && response.data != null) {
                _badgeUnread.value = response.data.unread ?: 0
                _badgeAll.value = response.data.total
                _badgeStarred.value = response.data.starred ?: 0
                success = true
            }
        } else if (appSettingsService.isItemCachingEnabled()) {
            // TODO: do this differently, because it's not efficient
            val dbItems = getDBItems()
            _badgeUnread.value = dbItems.filter { item -> item.unread }.size
            _badgeStarred.value = dbItems.filter { item -> item.starred }.size
            _badgeAll.value = dbItems.size
            success = true
        }
        return success
    }

    suspend fun getTags(): List<SelfossModel.Tag> {
        val isDatabaseEnabled =
            appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
        return if (isNetworkAvailable() && !fetchedTags) {
            val apiTags = api.tags()
            if (apiTags.success && apiTags.data != null && isDatabaseEnabled) {
                resetDBTagsWithData(apiTags.data)
                if (!appSettingsService.isUpdateSourcesEnabled()) {
                    fetchedTags = true
                }
            }
            apiTags.data ?: emptyList()
        } else if (isDatabaseEnabled) {
            getDBTags().map { it.toView() }
        } else {
            emptyList()
        }
    }

    suspend fun getSpouts(): Map<String, SelfossModel.Spout> {
        return if (isNetworkAvailable()) {
            val spouts = api.spouts()
            if (spouts.success && spouts.data != null) {
                spouts.data
            } else {
                emptyMap()
            }
        } else {
            throw NetworkUnavailableException()
        }
    }

    suspend fun getSourcesDetailsOrStats(): ArrayList<SelfossModel.Source> {
        var sources = ArrayList<SelfossModel.Source>()
        val isDatabaseEnabled =
            appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
        val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
        if (shouldFetch && isNetworkAvailable()) {
            if (appSettingsService.getPublicAccess()) {
                val apiSources = api.sourcesStats()
                if (apiSources.success && apiSources.data != null) {
                    fetchedSources = true
                    sources = apiSources.data as ArrayList<SelfossModel.Source>
                }
            } else {
                sources = getSourcesDetails() as ArrayList<SelfossModel.Source>
            }
        } else if (isDatabaseEnabled) {
            sources = getDBSources().map { it.toView() } as ArrayList<SelfossModel.Source>
        }

        return sources
    }

    suspend fun getSourcesDetails(): ArrayList<SelfossModel.SourceDetail> {
        var sources = ArrayList<SelfossModel.SourceDetail>()
        val isDatabaseEnabled =
            appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
        val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
        if (shouldFetch && isNetworkAvailable()) {
            val apiSources = api.sourcesDetailed()
            if (apiSources.success && apiSources.data != null) {
                fetchedSources = true
                sources = apiSources.data
                if (isDatabaseEnabled) {
                    resetDBSourcesWithData(sources)
                }
            }
        } else if (isDatabaseEnabled) {
            sources = getDBSources().map { it.toView() } as ArrayList<SelfossModel.SourceDetail>
        }
        return sources
    }

    suspend fun markAsRead(item: SelfossModel.Item): Boolean {
        val success = markAsReadById(item.id)

        if (success) {
            markAsReadLocally(item)
        }
        return success
    }

    private suspend fun markAsReadById(id: Int): Boolean {
        return if (isNetworkAvailable()) {
            api.markAsRead(id.toString()).isSuccess
        } else {
            insertDBAction(id.toString(), read = true)
            true
        }
    }

    suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
        val success = unmarkAsReadById(item.id)

        if (success) {
            unmarkAsReadLocally(item)
        }
        return success
    }

    private suspend fun unmarkAsReadById(id: Int): Boolean {
        return if (isNetworkAvailable()) {
            api.unmarkAsRead(id.toString()).isSuccess
        } else {
            insertDBAction(id.toString(), unread = true)
            true
        }
    }

    suspend fun starr(item: SelfossModel.Item): Boolean {
        val success = starrById(item.id)

        if (success) {
            starrLocally(item)
        }
        return success
    }

    private suspend fun starrById(id: Int): Boolean {
        return if (isNetworkAvailable()) {
            api.starr(id.toString()).isSuccess
        } else {
            insertDBAction(id.toString(), starred = true)
            true
        }
    }

    suspend fun unstarr(item: SelfossModel.Item): Boolean {
        val success = unstarrById(item.id)

        if (success) {
            unstarrLocally(item)
        }
        return success
    }

    private suspend fun unstarrById(id: Int): Boolean {
        return if (isNetworkAvailable()) {
            api.unstarr(id.toString()).isSuccess
        } else {
            insertDBAction(id.toString(), starred = true)
            true
        }
    }

    suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
        var success = false

        if (isNetworkAvailable() && api.markAllAsRead(items.map { it.id.toString() }).isSuccess) {
            success = true
            for (item in items) {
                markAsReadLocally(item)
            }
        }
        return success
    }

    private fun markAsReadLocally(item: SelfossModel.Item) {
        if (item.unread) {
            item.unread = false
            _badgeUnread.value -= 1
        }

        CoroutineScope(Dispatchers.Main).launch {
            updateDBItem(item)
        }
    }

    private fun unmarkAsReadLocally(item: SelfossModel.Item) {
        if (!item.unread) {
            item.unread = true
            _badgeUnread.value += 1
        }

        CoroutineScope(Dispatchers.Main).launch {
            updateDBItem(item)
        }
    }

    private fun starrLocally(item: SelfossModel.Item) {
        if (!item.starred) {
            item.starred = true
            _badgeStarred.value += 1
        }

        CoroutineScope(Dispatchers.Main).launch {
            updateDBItem(item)
        }
    }

    private fun unstarrLocally(item: SelfossModel.Item) {
        if (item.starred) {
            item.starred = false
            _badgeStarred.value -= 1
        }

        CoroutineScope(Dispatchers.Main).launch {
            updateDBItem(item)
        }
    }

    suspend fun createSource(
        title: String,
        url: String,
        spout: String,
        tags: String,
    ): Boolean {
        var response = false
        if (isNetworkAvailable()) {
            response = api.createSourceForVersion(
                title,
                url,
                spout,
                tags,
            ).isSuccess == true
        }

        return response
    }

    suspend fun updateSource(
        id: Int,
        title: String,
        url: String,
        spout: String,
        tags: String,
    ): Boolean {
        var response = false
        if (isNetworkAvailable()) {
            response = api.updateSourceForVersion(id, title, url, spout, tags).isSuccess == true
        }

        return response
    }

    suspend fun deleteSource(
        id: Int,
        title: String,
    ): Boolean {
        var success = false
        if (isNetworkAvailable()) {
            val response = api.deleteSource(id)
            success = response.isSuccess
        }

        // We filter on success or if the network isn't available
        if (success || !isNetworkAvailable()) {
            items = ArrayList(items.filter { it.sourcetitle != title })
            setReaderItems(items)
            db.itemsQueries.deleteItemsWhereSource(title)
        }

        return success
    }

    suspend fun updateRemote(): Boolean {
        return if (isNetworkAvailable()) {
            api.update().data.equals("finished")
        } else {
            false
        }
    }

    suspend fun login(): Boolean {
        var result = false
        if (isNetworkAvailable()) {
            try {
                val response = api.login()
                result = response.isSuccess == true
            } catch (cause: Throwable) {
                Napier.e("login failed", cause, tag = "RepositoryImpl.login")
            }
        }
        return result
    }

    suspend fun checkIfFetchFails(): Boolean {
        var fetchFailed = true
        if (isNetworkAvailable()) {
            try {
                // Trying to fetch one item, and check someone is trying to use the app with
                // a random rss feed, that would throw a NoTransformationFoundException
                fetchFailed = !api.getItemsWithoutCatch().success
            } catch (e: Throwable) {
                Napier.e("checkIfFetchFails failed", e, tag = "RepositoryImpl.shouldBeSelfossInstance")
            }
        }

        return fetchFailed
    }

    suspend fun logout() {
        if (isNetworkAvailable()) {
            try {
                val response = api.logout()
                if (!response.isSuccess) {
                    Napier.e("Couldn't logout.", tag = "RepositoryImpl.logout")
                }
            } catch (cause: Throwable) {
                Napier.e("logout failed", cause, tag = "RepositoryImpl.logout")
            }
            appSettingsService.clearAll()
        } else {
            appSettingsService.clearAll()
        }
    }

    fun refreshLoginInformation(
        url: String,
        login: String,
        password: String,
    ) {
        appSettingsService.refreshLoginInformation(url, login, password)
        baseUrl = url
        api.refreshLoginInformation()
    }

    suspend fun updateApiInformation() {
        val apiMajorVersion = appSettingsService.getApiVersion()

        if (isNetworkAvailable()) {
            val fetchedInformation = api.apiInformation()
            if (fetchedInformation.success && fetchedInformation.data != null) {
                if (fetchedInformation.data.getApiMajorVersion() != apiMajorVersion) {
                    appSettingsService.updateApiVersion(fetchedInformation.data.getApiMajorVersion())
                }
                // Check if we're accessing the instance in public mode
                // This happens when auth and public mode are enabled but
                // no credentials are provided to login
                if (appSettingsService.getUserName().isEmpty() &&
                    fetchedInformation.data.getApiConfiguration().isAuthEnabled() &&
                    fetchedInformation.data.getApiConfiguration().isPublicModeEnabled()
                ) {
                    appSettingsService.updatePublicAccess(true)
                }
            }
        }
    }

    fun isNetworkAvailable() = isConnectionAvailable.value && !offlineOverride

    private fun getDBActions(): List<ACTION> = db.actionsQueries.actions().executeAsList()

    private fun deleteDBAction(action: ACTION) = db.actionsQueries.deleteAction(action.id)

    private fun getDBTags(): List<TAG> = db.tagsQueries.tags().executeAsList()

    private fun getDBSources(): List<SOURCE> = db.sourcesQueries.sources().executeAsList()

    private fun resetDBTagsWithData(tagEntities: List<SelfossModel.Tag>) {
        db.tagsQueries.deleteAllTags()

        db.tagsQueries.transaction {
            tagEntities.forEach { tag ->
                db.tagsQueries.insertTag(tag.toEntity())
            }
        }
    }

    private fun resetDBSourcesWithData(sources: List<SelfossModel.SourceDetail>) {
        db.sourcesQueries.deleteAllSources()

        db.sourcesQueries.transaction {
            sources.forEach { source ->
                db.sourcesQueries.insertSource(source.toEntity())
            }
        }
    }

    private fun insertDBItems(items: List<SelfossModel.Item>) {
        db.itemsQueries.transaction {
            items.forEach { item ->
                db.itemsQueries.insertItem(item.toEntity())
            }
        }
    }

    private fun getDBItems(): List<ITEM> = db.itemsQueries.items().executeAsList()

    private fun insertDBAction(
        articleid: String,
        read: Boolean = false,
        unread: Boolean = false,
        starred: Boolean = false,
        unstarred: Boolean = false,
    ) = db.actionsQueries.insertAction(articleid, read, unread, starred, unstarred)

    private fun updateDBItem(item: SelfossModel.Item) =
        db.itemsQueries.updateItem(
            item.datetime,
            item.title.getHtmlDecoded(),
            item.content,
            item.unread,
            item.starred,
            item.thumbnail,
            item.icon,
            item.link,
            item.sourcetitle,
            item.tags.joinToString(","),
            item.author,
            item.id.toString(),
        )

    suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item> {
        try {
            val newItems = getMaxItemsForBackground(ItemType.UNREAD)
            val allItems = getMaxItemsForBackground(ItemType.ALL)
            val starredItems = getMaxItemsForBackground(ItemType.STARRED)
            insertDBItems(newItems + allItems + starredItems)
            return newItems
        } catch (e: Throwable) {
            // We do nothing
        }
        return emptyList()
    }

    suspend fun handleDBActions() {
        val actions: List<ACTION> = getDBActions()

        actions.forEach { action ->
            when {
                action.read ->
                    doAndReportOnFail(
                        markAsReadById(action.articleid.toInt()),
                        action,
                    )
                action.unread ->
                    doAndReportOnFail(
                        unmarkAsReadById(action.articleid.toInt()),
                        action,
                    )
                action.starred ->
                    doAndReportOnFail(
                        starrById(action.articleid.toInt()),
                        action,
                    )
                action.unstarred ->
                    doAndReportOnFail(
                        unstarrById(action.articleid.toInt()),
                        action,
                    )
            }
        }
    }

    private fun doAndReportOnFail(
        result: Boolean,
        action: ACTION,
    ) {
        if (result) {
            deleteDBAction(action)
        }
    }

    fun setTagFilter(tag: SelfossModel.Tag?) {
        _tagFilter.value = tag
    }

    fun setSourceFilter(source: SelfossModel.Source?) {
        _sourceFilter.value = source
    }

    fun setReaderItems(readerItems: ArrayList<SelfossModel.Item>) {
        _readerItems = readerItems
    }

    fun getReaderItems(): ArrayList<SelfossModel.Item> {
        return _readerItems
    }

    fun migrate(driverFactory: DriverFactory) {
        ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1)
    }

    fun setSelectedSource(source: SelfossModel.SourceDetail) {
        _selectedSource = source
    }

    fun unsetSelectedSource() {
        _selectedSource = null
    }

    fun getSelectedSource(): SelfossModel.SourceDetail? {
        return _selectedSource
    }
}
