/*
 * TimeLimit Copyright <C> 2019 Jonas Lochmann
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package io.timelimit.android.sync

import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.Database
import io.timelimit.android.data.model.*
import io.timelimit.android.integration.platform.PlatformIntegration
import io.timelimit.android.sync.actions.DatabaseValidation
import io.timelimit.android.sync.actions.DeleteCategoryAction
import io.timelimit.android.sync.actions.RemoveUserAction
import io.timelimit.android.sync.actions.dispatch.LocalDatabaseParentActionDispatcher
import io.timelimit.android.sync.network.ServerDataStatus

object ApplyServerDataStatus {
    suspend fun applyServerDataStatusCoroutine(status: ServerDataStatus, database: Database, platformIntegration: PlatformIntegration) {
        Threads.database.executeAndWait {
            applyServerDataStatusSync(status, database, platformIntegration)
        }
    }

    fun applyServerDataStatusSync(status: ServerDataStatus, database: Database, platformIntegration: PlatformIntegration) {
        database.beginTransaction()

        try {
            run {
                // apply ful version until and message

                database.config().setFullVersionUntilSync(status.fullVersionUntil)
                database.config().setServerMessage(status.message)
            }

            run {
                val newUserList = status.newUserList

                if (newUserList != null) {
                    val oldUserList = database.user().getAllUsersSync()

                    run {
                        // update/ create entries (first because there must be always one parent user)

                        newUserList.data.forEach {
                            newData ->
                            val oldEntry = oldUserList.find { it.id == newData.id }

                            if (oldEntry == null) {
                                // create entry

                                database.user().addUserSync(newData)
                            } else {
                                // eventually update entry

                                if (newData != oldEntry) {
                                    database.user().updateUserSync(newData)
                                }
                            }
                        }
                    }

                    run {
                        // remove old entries
                        val newUserIds = newUserList.data.map { it.id }
                        val oldUserIds = oldUserList.map { it.id }

                        val oldUserIdsToRemove = ArrayList(oldUserIds)
                        oldUserIdsToRemove.removeAll(newUserIds)

                        oldUserIdsToRemove.forEach {
                            oldUserId ->

                            LocalDatabaseParentActionDispatcher.dispatchParentActionSync(
                                    RemoveUserAction(userId = oldUserId, authentication = null),
                                    database
                            )
                        }
                    }

                    run {
                        // update version

                        database.config().setUserListVersionSync(newUserList.version)
                    }
                }
            }

            run {
                // apply new device list
                val newDeviceList = status.newDeviceList

                if (newDeviceList != null) {
                    val oldDeviceList = database.device().getAllDevicesSync()

                    run {
                        // remove obsolete entries

                        val newDeviceListIds = newDeviceList.data.map { it.deviceId }
                        val oldDeviceListIds = oldDeviceList.map { it.id }

                        val removedDeviceEntryIds = ArrayList(oldDeviceListIds)
                        removedDeviceEntryIds.removeAll(newDeviceListIds)

                        if (removedDeviceEntryIds.isNotEmpty()) {
                            database.device().removeDevicesById(removedDeviceEntryIds)
                        }
                    }

                    run {
                        // add/ update entries
                        val thisDeviceId = database.config().getOwnDeviceIdSync()!!

                        newDeviceList.data.forEach {
                            newDevice ->
                            val oldDeviceEntry = oldDeviceList.find { it.id == newDevice.deviceId }

                            if (oldDeviceEntry == null) {
                                // create new entry

                                database.device().addDeviceSync(Device(
                                        id = newDevice.deviceId,
                                        name = newDevice.name,
                                        model = newDevice.model,
                                        addedAt = newDevice.addedAt,
                                        currentUserId = newDevice.currentUserId,
                                        installedAppsVersion = "",
                                        networkTime = newDevice.networkTime,
                                        currentProtectionLevel = newDevice.currentProtectionLevel,
                                        highestProtectionLevel = newDevice.highestProtectionLevel,
                                        currentUsageStatsPermission = newDevice.currentUsageStatsPermission,
                                        highestUsageStatsPermission = newDevice.highestUsageStatsPermission,
                                        currentNotificationAccessPermission = newDevice.currentNotificationAccessPermission,
                                        highestNotificationAccessPermission = newDevice.highestNotificationAccessPermission,
                                        currentAppVersion = newDevice.currentAppVersion,
                                        highestAppVersion = newDevice.highestAppVersion,
                                        manipulationTriedDisablingDeviceAdmin = newDevice.triedDisablingAdmin,
                                        manipulationDidReboot = newDevice.didReboot,
                                        hadManipulation = newDevice.hadManipulation,
                                        didReportUninstall = newDevice.didReportUninstall,
                                        isUserKeptSignedIn = newDevice.isUserKeptSignedIn,
                                        showDeviceConnected = newDevice.showDeviceConnected,
                                        defaultUser = newDevice.defaultUser,
                                        defaultUserTimeout = newDevice.defaultUserTimeout,
                                        considerRebootManipulation = newDevice.considerRebootManipulation
                                ))
                            } else {
                                // eventually update old entry

                                val updatedDeviceEntry = oldDeviceEntry.copy(
                                        name = newDevice.name,
                                        model = newDevice.model,
                                        addedAt = newDevice.addedAt,
                                        currentUserId = newDevice.currentUserId,
                                        networkTime = newDevice.networkTime,
                                        currentProtectionLevel = newDevice.currentProtectionLevel,
                                        highestProtectionLevel = newDevice.highestProtectionLevel,
                                        currentUsageStatsPermission = newDevice.currentUsageStatsPermission,
                                        highestUsageStatsPermission = newDevice.highestUsageStatsPermission,
                                        currentNotificationAccessPermission = newDevice.currentNotificationAccessPermission,
                                        highestNotificationAccessPermission = newDevice.highestNotificationAccessPermission,
                                        currentAppVersion = newDevice.currentAppVersion,
                                        highestAppVersion = newDevice.highestAppVersion,
                                        manipulationTriedDisablingDeviceAdmin = newDevice.triedDisablingAdmin,
                                        manipulationDidReboot = newDevice.didReboot,
                                        hadManipulation = newDevice.hadManipulation,
                                        didReportUninstall = newDevice.didReportUninstall,
                                        isUserKeptSignedIn = newDevice.isUserKeptSignedIn,
                                        showDeviceConnected = newDevice.showDeviceConnected,
                                        defaultUser = newDevice.defaultUser,
                                        defaultUserTimeout = newDevice.defaultUserTimeout,
                                        considerRebootManipulation = newDevice.considerRebootManipulation
                                )

                                if (updatedDeviceEntry != oldDeviceEntry) {
                                    database.device().updateDeviceEntry(updatedDeviceEntry)
                                }

                                if (updatedDeviceEntry.id == thisDeviceId) {
                                    if (updatedDeviceEntry.currentUserId != oldDeviceEntry.currentUserId) {
                                        runAsync {
                                            platformIntegration.stopSuspendingForAllApps()
                                        }
                                    }
                                }
                            }
                        }
                    }

                    database.config().setDeviceListVersionSync(newDeviceList.version)
                }
            }

            run {
                status.newInstalledApps.forEach {
                    item ->

                    DatabaseValidation.assertDeviceExists(database, item.deviceId)

                    run {
                        // apply apps
                        database.app().deleteAllAppsByDeviceId(item.deviceId)
                        database.app().addAppsSync(item.apps.map {
                            App(
                                    deviceId = item.deviceId,
                                    packageName = it.packageName,
                                    title = it.title,
                                    isLaunchable = it.isLaunchable,
                                    recommendation = it.recommendation
                            )
                        })
                    }

                    run {
                        // apply changed version number
                        database.device().updateAppsVersion(
                                deviceId = item.deviceId,
                                appsVersion = item.version
                        )
                    }
                }
            }

            run {
                // apply removed categories
                // there is much data related to a removed category, so we use an existing function for this

                status.removedCategories.forEach {
                    val categoryId = it

                    if (database.category().getCategoryByIdSync(categoryId) == null) {
                        // category was likely deleted with the user, ignore it
                    } else {
                        LocalDatabaseParentActionDispatcher.dispatchParentActionSync(
                                DeleteCategoryAction(
                                        categoryId = categoryId
                                ),
                                database
                        )
                    }
                }
            }

            run {
                // apply category base data
                val newCategories = status.newCategoryBaseData

                if (newCategories.isNotEmpty()) {
                    newCategories.forEach {
                        val newCategory = it
                        val oldCategory = database.category().getCategoryByIdSync(it.categoryId)

                        DatabaseValidation.assertChildExists(database, newCategory.childId)

                        if (oldCategory == null) {
                            // create new category

                            database.category().addCategory(Category(
                                    id = newCategory.categoryId,
                                    childId = newCategory.childId,
                                    title = newCategory.title,
                                    blockedMinutesInWeek = newCategory.blockedMinutesInWeek,
                                    extraTimeInMillis = newCategory.extraTimeInMillis,
                                    temporarilyBlocked = newCategory.temporarilyBlocked,
                                    baseVersion = newCategory.baseDataVersion,
                                    assignedAppsVersion = "",
                                    timeLimitRulesVersion = "",
                                    usedTimesVersion = "",
                                    parentCategoryId = newCategory.parentCategoryId
                            ))
                        } else {
                            val updatedCategory = oldCategory.copy(
                                    childId = newCategory.childId,
                                    title = newCategory.title,
                                    blockedMinutesInWeek = newCategory.blockedMinutesInWeek,
                                    extraTimeInMillis = newCategory.extraTimeInMillis,
                                    temporarilyBlocked = newCategory.temporarilyBlocked,
                                    baseVersion = newCategory.baseDataVersion,
                                    parentCategoryId = newCategory.parentCategoryId
                            )

                            if (updatedCategory != oldCategory) {
                                database.category().updateCategorySync(updatedCategory)
                            }
                        }
                    }
                }
            }

            run {
                // apply used times

                val newUsedTimes = status.newCategoryUsedTimes

                newUsedTimes.forEach {
                    newUsedTime ->
                    val categoryId = newUsedTime.categoryId

                    DatabaseValidation.assertCategoryExists(database, categoryId)

                    // replace items
                    database.usedTimes().deleteUsedTimeItems(categoryId)
                    database.usedTimes().insertUsedTimes(
                            newUsedTime.usedTimeItems.map {
                                UsedTimeItem(
                                        dayOfEpoch = it.dayOfEpoch,
                                        usedMillis = it.usedMillis,
                                        categoryId = categoryId
                                )
                            }
                    )

                    // update version
                    database.category().updateCategoryUsedTimesVersion(
                            categoryId = categoryId,
                            usedTimesVersion = newUsedTime.version
                    )
                }
            }

            run {
                // apply assigned apps
                val thisDeviceId = database.config().getOwnDeviceIdSync()!!
                val thisDeviceEntry = database.device().getDeviceByIdSync(thisDeviceId)!!
                val thisDeviceUserCategories = if (thisDeviceEntry.currentUserId == "")
                    emptyList()
                else
                    database.category().getCategoriesByChildIdSync(thisDeviceEntry.currentUserId)
                val thisDeviceUserCategoryIds = thisDeviceUserCategories.map { it.id }.toSet()

                status.newCategoryAssignedApps.forEach {
                    item ->

                    DatabaseValidation.assertCategoryExists(database, item.categoryId)

                    database.categoryApp().deleteCategoryAppsByCategoryId(item.categoryId)
                    database.categoryApp().addCategoryAppsSync(item.assignedApps.map {
                        CategoryApp(
                                categoryId = item.categoryId,
                                packageName = it
                        )
                    })

                    database.category().updateCategoryAssignedAppsVersion(
                            categoryId = item.categoryId,
                            assignedAppsVersion = item.version
                    )

                    if (thisDeviceUserCategoryIds.contains(item.categoryId)) {
                        runAsync {
                            platformIntegration.setSuspendedApps(item.assignedApps, false)
                        }
                    }
                }
            }

            run {
                // apply time limit rules
                status.newCategoryTimeLimitRules.forEach {
                    newTimeLimitRulesItem ->

                    val categoryId = newTimeLimitRulesItem.categoryId
                    val newRules = newTimeLimitRulesItem.rules
                    val oldRules = database.timeLimitRules().getTimeLimitRulesByCategorySync(categoryId)
                    val oldRuleIds = HashSet(oldRules.map { it.id })

                    DatabaseValidation.assertCategoryExists(database, categoryId)

                    newRules.forEach {
                        newRule ->
                        val oldRule = oldRules.find { it.id == newRule.id }

                        if (oldRule == null) {
                            // create new rule
                            database.timeLimitRules().addTimeLimitRule(newRule.toRealRule(categoryId))
                        } else {
                            // eventually update rule
                            oldRuleIds.remove(oldRule.id)

                            val newRuleEntry = newRule.toRealRule(categoryId)

                            if (newRuleEntry != oldRule) {
                                database.timeLimitRules().updateTimeLimitRule(newRuleEntry)
                            }
                        }
                    }

                    if (oldRuleIds.isNotEmpty()) {
                        database.timeLimitRules().deleteTimeLimitRulesByIdsSync(oldRuleIds.toList())
                    }

                    // save new version
                    database.category().updateCategoryRulesVersion(
                            categoryId = categoryId,
                            rulesVersion = newTimeLimitRulesItem.version
                    )
                }
            }

            database.setTransactionSuccessful()
        } finally {
            database.endTransaction()
        }
    }
}
