package de.kif.backend.util

import de.kif.backend.Configuration
import de.kif.backend.database.Connection
import de.kif.backend.repository.*
import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.common.model.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import mu.KotlinLogging
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.*
import kotlin.concurrent.thread

@Serializable
data class Backup(
    val posts: List<Post> = emptyList(),
    val rooms: List<Room> = emptyList(),
    val schedules: List<Schedule> = emptyList(),
    val tracks: List<Track> = emptyList(),
    val users: List<User> = emptyList(),
    val workGroups: List<WorkGroup> = emptyList()
) {
    companion object {
        private val logger = KotlinLogging.logger {}

        suspend fun backup(vararg repositories: RepositoryType): String {
            var backup = Backup()

            val repositorySet = repositories.toMutableSet()

            if (RepositoryType.SCHEDULE in repositorySet) {
                repositorySet += RepositoryType.ROOM
                repositorySet += RepositoryType.WORK_GROUP
            }

            if (RepositoryType.WORK_GROUP in repositorySet) {
                repositorySet += RepositoryType.TRACK
            }

            for (repository in repositorySet) {
                backup = when (repository) {
                    RepositoryType.ROOM -> backup.copy(rooms = RoomRepository.all())
                    RepositoryType.SCHEDULE -> backup.copy(schedules = ScheduleRepository.all())
                    RepositoryType.TRACK -> backup.copy(tracks = TrackRepository.all())
                    RepositoryType.USER -> backup.copy(users = UserRepository.all())
                    RepositoryType.WORK_GROUP -> backup.copy(workGroups = WorkGroupRepository.all())
                    RepositoryType.POST -> backup.copy(posts = PostRepository.all())
                    else -> backup
                }
            }

            return Serialization.stringify(serializer(), backup)
        }

        @Suppress("UNUSED_VARIABLE")
        suspend fun import(data: String) {
            val backup = Serialization.parse(serializer(), data)

            backup.users.forEach { UserRepository.create(it); println("Import user ${it.username}") }
            backup.posts.forEach { PostRepository.create(it); println("Import post") }

            val oldRooms = RoomRepository.all()
            val newRooms = backup.rooms.filterNot { oldRooms.any { i -> i.equalsIgnoreId(it) } }
            val roomMap = (oldRooms.associateWith { it.id!! } +
                    newRooms.map {
                        val id = RoomRepository.create(it)
                        println("Import room ${it.name}")
                        it to id
                    }).toList()

            val oldTracks = TrackRepository.all()
            val newTracks = backup.tracks.filterNot { oldTracks.any { i -> i.equalsIgnoreId(it) } }
            val trackMap = (oldTracks.associateWith { it.id!! } +
                    newTracks.map {
                        val id = TrackRepository.create(it)
                        println("Import track ${it.name}")
                        it to id
                    }).toList()

            val oldWorkGroups = WorkGroupRepository.all()
            val newWorkGroups = backup.workGroups.filterNot { oldWorkGroups.any { i -> i.equalsIgnoreId(it) } }
            val workGroupMap = (oldWorkGroups.associateWith { it.id!! } +
                    newWorkGroups.mapNotNull {
                        var workGroup = it
                        val track = workGroup.track
                        if (track != null) {
                            workGroup = workGroup.copy(track = track.copy(id = trackMap.firstOrNull { (i, _) ->
                                i.equalsIgnoreId(track)
                            }?.second ?: run {
                                println("Cannot import work group, due to missing track")
                                return@mapNotNull null
                            }))
                        }

                        val id = WorkGroupRepository.create(workGroup)
                        println("Import work group ${it.name}")
                        it to id
                    }).toList()

            val oldSchedules = ScheduleRepository.all()
            val newSchedules = backup.schedules.filterNot { oldSchedules.any { i -> i.equalsIgnoreId(it) } }
            newSchedules.forEach {
                ScheduleRepository.create(
                    it.copy(
                        room = it.room.copy(
                            id = roomMap.firstOrNull { (i, _) -> i.equalsIgnoreId(it.room) }?.second ?: run {
                                println("Cannot import schedule, due to missing room")
                                return@forEach
                            }),
                        workGroup = it.workGroup.copy(
                            id = workGroupMap.firstOrNull { (i, _) -> i.equalsIgnoreId(it.workGroup) }?.second ?: run {
                                println("Cannot import schedule, due to missing work group")
                                return@forEach
                            })
                    )
                )

                println("Import schedule day=${it.day}, time=${it.time}, work group=${it.workGroup.name}")
            }
        }

        suspend fun restore(data: String) {
            Connection.reset()

            import(data)
        }

        fun startBackupService() {
            val backupPath = Configuration.Path.backupPath.toFile()
            val backupInterval = Configuration.General.backupInterval

            val formatter = SimpleDateFormat("yyyyMMdd'T'HHmmss")

            thread(
                start = true,
                isDaemon = true,
                name = "backup-service"
            ) {
                while (true) {
                    try {
                        Thread.sleep(backupInterval)

                        if (!backupPath.exists()) {
                            backupPath.mkdirs()
                        }

                        val date = formatter.format(Date())

                        val backupFile = backupPath.resolve("backup_$date.json")

                        val backup = runBlocking {
                            backup(*RepositoryType.values())
                        }
                        backupFile.writeText(backup)
                    } catch (e: Exception) {
                        logger.error(e) { "Cannot create backup" }
                    }
                }
            }
        }
    }
}
