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.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.*
import com.github.ln_12.library.ConnectivityStatus
import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class Repository(private val api: SelfossApi, private val appSettingsService: AppSettingsService, connectivityStatus: ConnectivityStatus, private val db: ReaderForSelfossDB) {

    var items = ArrayList<SelfossModel.Item>()
    val isConnectionAvailable = connectivityStatus.isNetworkConnected
    var connectionMonitored = false

    var baseUrl = appSettingsService.getBaseUrl()
    lateinit var dateUtils: DateUtils

    var displayedItems = ItemType.UNREAD

    var tagFilter: SelfossModel.Tag? = null
    var sourceFilter: SelfossModel.Source? = null
    var searchFilter: String? = null

    var offlineOverride = false

    var badgeUnread = 0
    set(value) {field = if (value < 0) { 0 } else { value } }
    var badgeAll = 0
    set(value) {field = if (value < 0) { 0 } else { value } }
    var badgeStarred = 0
    set(value) {field = if (value < 0) { 0 } else { value } }

    init {
        // TODO: Dispatchers.IO not available in KMM, an alternative solution should be found
        CoroutineScope(Dispatchers.Main).launch {
            updateApiVersion()
            dateUtils = DateUtils(appSettingsService)
            reloadBadges()
        }
    }

    suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
        // TODO: Use the updatedSince parameter
        var fetchedItems: SelfossModel.StatusAndData<List<SelfossModel.Item>> = SelfossModel.StatusAndData.error()
        var fromDB = false
        if (isNetworkAvailable()) {
            fetchedItems = api.getItems(
                displayedItems.type,
                offset = 0,
                tagFilter?.tag,
                sourceFilter?.id?.toLong(),
                searchFilter,
                null
            )
        } else {
            if (appSettingsService.isItemCachingEnabled()) {
                fromDB = true
                fetchedItems = SelfossModel.StatusAndData.succes(
                    getDBItems().filter {
                        displayedItems == ItemType.ALL ||
                                (it.unread && displayedItems == ItemType.UNREAD) ||
                                (it.starred && displayedItems == ItemType.STARRED)
                    }.map { it.toView() }
                )
            }
        }

        if (fetchedItems.success && fetchedItems.data != null) {
            items = ArrayList(fetchedItems.data!!)
            if (fromDB) {
                items.sortByDescending { dateUtils.parseDate(it.datetime) }
            }
        }
        return items
    }

    suspend fun getOlderItems(): ArrayList<SelfossModel.Item> {
        var fetchedItems: SelfossModel.StatusAndData<List<SelfossModel.Item>> = SelfossModel.StatusAndData.error()
        if (isNetworkAvailable()) {
            val offset = items.size
            fetchedItems = api.getItems(
                displayedItems.type,
                offset,
                tagFilter?.tag,
                sourceFilter?.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,
                tagFilter?.tag,
                sourceFilter?.id?.toLong(),
                searchFilter,
                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 = response.data.unread
                badgeAll = response.data.total
                badgeStarred = response.data.starred
                success = true
            }
        } else {
            // TODO: do this differently, because it's not efficient
            val dbItems = getDBItems()
            badgeUnread = dbItems.filter { item -> item.unread }.size
            badgeStarred = dbItems.filter { item -> item.starred }.size
            badgeAll = items.size
        }
        return success
    }

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

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

    suspend fun getSources(): ArrayList<SelfossModel.Source>? {
        return if (isNetworkAvailable()) {
            val apiSources = api.sources()
            if (apiSources.success && apiSources.data != null && (appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled())) {
                resetDBSourcesWithData(apiSources.data)
            }
            apiSources.data
        } else {
            ArrayList(getDBSources().map { it.toView() })
        }
    }

    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 -= 1
        }

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

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

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

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

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

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

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

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

        return response
    }

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

        return success
    }

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

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

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

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

        if (isNetworkAvailable()) {
            val fetchedVersion = api.version()
            if (fetchedVersion.success && fetchedVersion.data != null && fetchedVersion.data.getApiMajorVersion() != apiMajorVersion) {
                appSettingsService.updateApiVersion(fetchedVersion.data.getApiMajorVersion())
            }
        }
    }

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

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

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

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

    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.Source>) {
        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.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)
        }
    }
}