package de.kif.common

import de.kif.common.model.ConstraintType
import de.kif.common.model.Room
import de.kif.common.model.Schedule
import kotlinx.serialization.Serializable

@Serializable
data class ConstraintError(
    val reason: String = ""
)

@Serializable
data class ConstraintMap(
    val map: List<Pair<Long, List<ConstraintError>>>
)

fun checkConstraints(
    check: List<Schedule>,
    against: List<Schedule>,
    rooms: List<Room>,
    resoDay: Int,
    resoTime: Int
): ConstraintMap {
    val map = mutableMapOf<Long, List<ConstraintError>>()

    val roomMap = rooms.associateBy { it.id }

    for (schedule in check) {
        if (schedule.id == null) continue
        val errors = mutableListOf<ConstraintError>()

        if (schedule.workGroup.projector && !schedule.room.projector) {
            errors += ConstraintError("Work group requires projector, but room does not have one!")
        }
        if (schedule.workGroup.internet && !schedule.room.internet) {
            errors += ConstraintError("Work group requires internet, but room does not have one!")
        }
        if (schedule.workGroup.whiteboard && !schedule.room.whiteboard) {
            errors += ConstraintError("Work group requires whiteboard, but room does not have one!")
        }
        if (schedule.workGroup.blackboard && !schedule.room.blackboard) {
            errors += ConstraintError("Work group requires blackboard, but room does not have one!")
        }
        if (schedule.workGroup.accessible && !schedule.room.accessible) {
            errors += ConstraintError("Work group requires accessible, but room does not have one!")
        }

        val blocked = schedule.room.blocked.map {
            it.checkBlock(
                schedule.day,
                schedule.time,
                schedule.time + schedule.workGroup.length
            )
        }.any { it }
        if (blocked) {
            errors += ConstraintError("The room ${schedule.room.name} is blocked!")
        }

        val start = schedule.getAbsoluteStartTime()
        val end = schedule.getAbsoluteEndTime()


        if (schedule.workGroup.resolution) {
            val resoDeadline = resoDay * 24 * 60 + resoTime

            if (end > resoDeadline) {
                errors += ConstraintError("The work group is ${end - resoDeadline} minutes after resolution deadline")
            }
        }

        for (leader in schedule.workGroup.leader) {
            for (s in against) {
                if (
                    schedule != s &&
                    schedule.day == s.day &&
                    leader in s.workGroup.leader &&
                    start < s.getAbsoluteEndTime() &&
                    s.getAbsoluteStartTime() < end
                ) {
                    errors += ConstraintError("Work group with leader $leader cannot be at same time with ${s.workGroup.name}!")
                }
            }
        }

        for (s in against) {
            if (
                schedule != s &&
                schedule.day == s.day &&
                schedule.room.id == s.room.id &&
                start < s.getAbsoluteEndTime() &&
                s.getAbsoluteStartTime() < end
            ) {
                errors += ConstraintError("Work group cannot be at same time with ${s.workGroup.name} at the same room ${schedule.room.name}!")
            }
        }

        for ((type, constraints) in schedule.workGroup.constraints.groupBy { it.type }) {
            when (type) {
                ConstraintType.OnlyOnDay -> {
                    val onlyOnDay = constraints.map { it.day == schedule.day }
                    if (onlyOnDay.none { it }) {
                        val dayList = constraints.mapNotNull { it.day }.distinct().sorted()
                        errors += ConstraintError("Work group requires days $dayList, but is on ${schedule.day}!")
                    }
                }

                ConstraintType.NotOnDay -> {
                    val notOnDay = constraints.map { it.day != schedule.day }
                    if (notOnDay.none { it }) {
                        val dayList = constraints.mapNotNull { it.day }.distinct().sorted()
                        errors += ConstraintError("Work group requires not days $dayList, but is on ${schedule.day}!")
                    }
                }

                ConstraintType.OnlyBeforeTime -> {
                    for (it in constraints) {
                        if (it.time == null) continue
                        if (it.day == null) {
                            if (it.time < schedule.time) {
                                errors += ConstraintError("Work group requires before time ${it.time}, but is on ${schedule.time}!")
                            }
                        } else {
                            if (it.day == schedule.day && it.time < schedule.time) {
                                errors += ConstraintError("Work group requires before time ${it.time} on day ${it.day}, but is on ${schedule.time}!")
                            }
                        }
                    }
                }

                ConstraintType.OnlyAfterTime -> {
                    for (it in constraints) {
                        if (it.time == null) continue
                        if (it.day == null) {
                            if (it.time > schedule.time) {
                                errors += ConstraintError("Work group requires after time ${it.time}, but is on ${schedule.time}!")
                            }
                        } else {
                            if (it.day == schedule.day && it.time > schedule.time) {
                                errors += ConstraintError("Work group requires after time ${it.time} on day ${it.day}, but is on ${schedule.time}!")
                            }
                        }
                    }
                }

                ConstraintType.ExactTime -> {
                    for (it in constraints) {
                        if (it.time == null) continue
                        if (it.day == null) {
                            if (it.time != schedule.time) {
                                errors += ConstraintError("Work group requires exact time ${it.time}, but is on ${schedule.time}!")
                            }
                        } else {
                            if (it.day == schedule.day && it.time != schedule.time) {
                                errors += ConstraintError("Work group requires exact time ${it.time} on day ${it.day}, but is on ${schedule.time}!")
                            }
                        }
                    }
                }

                ConstraintType.NotAtSameTime -> {
                    for (constraint in constraints) {
                        for (s in against) {
                            if (
                                s.workGroup.id == constraint.workGroup &&
                                schedule.day == s.day &&
                                start <= s.getAbsoluteEndTime() &&
                                s.getAbsoluteStartTime() <= end
                            ) {
                                errors += ConstraintError("Work group requires not same time with ${s.workGroup.name}!")
                            }
                        }
                    }
                }

                ConstraintType.OnlyAfterWorkGroup -> {
                    for (constraint in constraints) {
                        for (s in against) {
                            if (
                                s.workGroup.id == constraint.workGroup && (
                                        s.day > schedule.day ||
                                                s.day == schedule.day && s.getAbsoluteEndTime() > start
                                        )
                            ) {
                                errors += ConstraintError("Work group requires after ${s.workGroup.name}!")
                            }
                        }
                    }
                }
                ConstraintType.Room -> {
                    val roomBools = constraints.map { it.room == schedule.room.id }
                    if (roomBools.none { it }) {
                        val roomList = constraints.mapNotNull { it.room }.distinct().sorted().map {
                            "${(roomMap[it]?.name ?: "")}($it)"
                        }
                        errors += ConstraintError("Work group requires rooms $roomList, but is in room ${schedule.room.name}(${schedule.room.id})")
                    }
                }
            }
        }

        map[schedule.id] = errors
    }

    return ConstraintMap(map.map { it.toPair() })
}
