package de.kif.backend.repository

import de.kif.backend.database.DbSchedule
import de.kif.backend.database.dbQuery
import de.kif.backend.util.PushService
import de.kif.common.MessageType
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.Schedule
import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.*
import java.util.Date

object ScheduleRepository : Repository<Schedule> {

    override val onCreate = EventHandler<Long>()
    override val onUpdate = EventHandler<Long>()
    override val onDelete = EventHandler<Long>()

    private suspend fun rowToModel(row: ResultRow): Schedule {
        val id = row[DbSchedule.id]
        val workGroupId = row[DbSchedule.workGroupId]
        val roomId = row[DbSchedule.roomId]
        val day = row[DbSchedule.day]
        val time = row[DbSchedule.time]
        val lookRoom = row[DbSchedule.lookRoom]
        val lookTime = row[DbSchedule.lookTime]

        val createdAt = row[DbSchedule.createdAt]
        val updatedAt = row[DbSchedule.updatedAt]

        val workGroup = WorkGroupRepository.get(workGroupId)
            ?: throw IllegalStateException("Work group for schedule does not exist!")
        val room = RoomRepository.get(roomId)
            ?: throw IllegalStateException("Room for schedule does not exist!")

        return Schedule(id, workGroup, room, day, time, lookRoom, lookTime, createdAt, updatedAt)
    }

    override suspend fun get(id: Long): Schedule? {
        return dbQuery {
            val row = DbSchedule.select { DbSchedule.id eq id }.firstOrNull() ?: return@dbQuery null

            runBlocking {
                rowToModel(row)
            }
        }
    }

    override suspend fun create(model: Schedule): Long {
        val now = Date().time

        return dbQuery {
            val id = DbSchedule.insert {
                it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!")
                it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!")
                it[day] = model.day
                it[time] = model.time
                it[lookRoom] = model.lookRoom
                it[lookTime] = model.lookTime

                it[createdAt] = now
                it[updatedAt] = now
            }[DbSchedule.id] ?: throw IllegalStateException("Cannot create model!")

            onCreate.emit(id)

            id
        }
    }

    override suspend fun update(model: Schedule) {
        if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")

        val now = Date().time

        dbQuery {
            DbSchedule.update({ DbSchedule.id eq model.id }) {
                it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!")
                it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!")
                it[day] = model.day
                it[time] = model.time
                it[lookRoom] = model.lookRoom
                it[lookTime] = model.lookTime

                it[updatedAt] = now
            }

            onUpdate.emit(model.id)
        }
    }

    override suspend fun delete(id: Long) {
        onDelete.emit(id)

        dbQuery {
            DbSchedule.deleteWhere { DbSchedule.id eq id }
        }
    }

    override suspend fun all(): List<Schedule> {
        return dbQuery {
            val result = DbSchedule.selectAll()

            runBlocking {
                result.map {
                    rowToModel(it)
                }
            }
        }
    }

    suspend fun getByDay(day: Int): List<Schedule> {
        return dbQuery {
            val result = DbSchedule.select { DbSchedule.day eq day }

            runBlocking {
                result.map {
                    rowToModel(it)
                }
            }
        }
    }

    suspend fun getByWorkGroup(workGroupId: Long): List<Schedule> {
        return dbQuery {
            val result = DbSchedule.select { DbSchedule.workGroupId eq workGroupId }

            runBlocking {
                result.map {
                    rowToModel(it)
                }
            }
        }
    }

    suspend fun getByRoom(roomId: Long): List<Schedule> {
        return dbQuery {
            val result = DbSchedule.select { DbSchedule.roomId eq roomId }

            runBlocking {
                result.map {
                    rowToModel(it)
                }
            }
        }
    }

    suspend fun getDayRange(): IntRange {
        val schedules = all()

        val min = schedules.minBy { it.day }?.day ?: 0
        val max = schedules.maxBy { it.day }?.day ?: 0

        return min..max
    }

    init {
        RoomRepository.onUpdate { roomId ->
            runBlocking {
                getByRoom(roomId).forEach { schedule ->
                    if (schedule.id != null) onUpdate.emit(schedule.id)
                }
            }
        }
        RoomRepository.onDelete { roomId ->
            runBlocking {
                getByRoom(roomId).forEach { schedule ->
                    if (schedule.id != null) delete(schedule.id)
                }
            }
        }

        WorkGroupRepository.onUpdate { workGroupId ->
            runBlocking {
                getByWorkGroup(workGroupId).forEach { schedule ->
                    if (schedule.id != null) onUpdate.emit(schedule.id)
                }
            }
        }
        WorkGroupRepository.onDelete { workGroupId ->
            runBlocking {
                getByWorkGroup(workGroupId).forEach { schedule ->
                    if (schedule.id != null) delete(schedule.id)
                }
            }
        }
    }

    fun registerPushService() {
        onCreate {
            runBlocking {
                PushService.notify(MessageType.CREATE, RepositoryType.SCHEDULE, it)
            }
        }
        onUpdate {
            runBlocking {
                PushService.notify(MessageType.UPDATE, RepositoryType.SCHEDULE, it)
            }
        }
        onDelete {
            runBlocking {
                PushService.notify(MessageType.DELETE, RepositoryType.SCHEDULE, it)
            }
        }
    }
}
