/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2025 SteamWar.de-Serverteam
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   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 Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package de.steamwar.routes

import de.steamwar.ResponseError
import de.steamwar.plugins.SWPermissionCheck
import de.steamwar.sql.*
import de.steamwar.sql.EventGroup.EventGroupType
import de.steamwar.sql.EventRelation.FromType
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import java.sql.Timestamp
import java.time.Instant
import java.util.*

@Serializable
data class ShortEvent(val id: Int, val name: String, val start: Long, val end: Long) {
    constructor(event: Event) : this(event.eventID, event.eventName, event.start.time, event.end.time)
}

@Serializable
data class ResponseGroups(
    val id: Int,
    val name: String,
    val pointsPerWin: Int,
    val pointsPerLoss: Int,
    val pointsPerDraw: Int,
    val type: EventGroupType,
    val points: Map<Int, Int>
) {
    constructor(group: EventGroup, short: Boolean = false) : this(
        group.getId(),
        group.name,
        group.pointsPerWin,
        group.pointsPerLoss,
        group.pointsPerDraw,
        group.type,
        if (short) mapOf() else group.calculatePoints().mapKeys { it.key.teamId })
}

@Serializable
data class ResponseRelation(
    val id: Int,
    val fight: Int,
    val team: EventRelation.FightTeam,
    val type: FromType,
    val fromFight: ResponseEventFight? = null,
    val fromGroup: ResponseGroups? = null,
    val fromPlace: Int
) {
    constructor(relation: EventRelation) : this(
        relation.getId(),
        relation.fightId,
        relation.fightTeam,
        relation.fromType,
        relation.fromFight?.let { ResponseEventFight(it) },
        relation.fromGroup?.let { ResponseGroups(it) },
        relation.fromPlace
    )
}

@Serializable
data class ResponseEvent(
    val id: Int,
    val name: String,
    val deadline: Long,
    val start: Long,
    val end: Long,
    val maxTeamMembers: Int,
    val schemType: String?,
    val publicSchemsOnly: Boolean,
) {
    constructor(event: Event) : this(
        event.eventID,
        event.eventName,
        event.deadline.time,
        event.start.time,
        event.end.time,
        event.maximumTeamMembers,
        event.schematicType?.toDB(),
        event.publicSchemsOnly(),
    )
}

@Serializable
data class ExtendedResponseEvent(
    val event: ResponseEvent,
    val teams: List<ResponseTeam>,
    val groups: List<ResponseGroups>,
    val fights: List<ResponseEventFight>,
    val referees: List<ResponseUser>,
    val relations: List<ResponseRelation>
) {
    constructor(event: Event) : this(
        ResponseEvent(event),
        TeamTeilnahme.getTeams(event.eventID).map { ResponseTeam(it) },
        EventGroup.get(event).map { ResponseGroups(it) },
        EventFight.getEvent(event.eventID).map { ResponseEventFight(it) },
        Referee.get(event.eventID).map { ResponseUser(SteamwarUser.byId(it)!!) },
        EventRelation.get(event).map { ResponseRelation(it) }
    )
}

@Serializable
data class CreateEvent(val name: String, val start: Long, val end: Long)

@Serializable
data class UpdateEvent(
    val name: String? = null,
    val deadline: Long? = null,
    val start: Long? = null,
    val end: Long? = null,
    val maxTeamMembers: Int? = null,
    val schemType: String? = null,
    val publicSchemsOnly: Boolean? = null,
    val addReferee: Set<String>? = null,
    val removeReferee: Set<String>? = null,
)

fun Route.configureEventsRoute() {
    route("/events") {
        install(SWPermissionCheck) {
            allowMethod(HttpMethod.Get)
            permission = UserPerm.MODERATION
        }
        get {
            call.respond(Event.getAll().map { ShortEvent(it) })
        }
        post {
            val createEvent = call.receiveNullable<CreateEvent>()
            if (createEvent == null) {
                call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body"))
                return@post
            }
            val event = Event.create(
                createEvent.name,
                Timestamp.from(Instant.ofEpochMilli(createEvent.start)),
                Timestamp.from(Instant.ofEpochMilli(createEvent.end))
            )
            call.respond(HttpStatusCode.Created, ResponseEvent(event))
        }
        route("/{id}") {
            get {
                val event = call.receiveEvent()
                    ?: return@get
                call.respond(
                    ExtendedResponseEvent(event)
                )
            }
            get("/csv") {
                val event = call.receiveEvent()
                    ?: return@get

                val fights = EventFight.getEvent(event.eventID)
                val csv = StringBuilder();
                csv.append(arrayOf("Start", "BlueTeam", "RedTeam", "WinnerTeam", "Group").joinToString(","))
                fights.forEach {
                    csv.appendLine()
                    val blue = Team.byId(it.teamBlue)
                    val red = Team.byId(it.teamRed)
                    val winner = when (it.ergebnis) {
                        1 -> blue.teamName
                        2 -> red.teamName
                        3 -> "Tie"
                        else -> "Unknown"
                    }
                    csv.append(
                        arrayOf(
                            it.startTime.toString(),
                            Team.byId(it.teamBlue).teamName,
                            Team.byId(it.teamRed).teamName,
                            winner,
                            it.group.map { it.name }.orElse("Ungrouped")
                        ).joinToString(",")
                    )
                }
                call.response.header("Content-Disposition", "attachment; filename=\"${event.eventName}.csv\"")
                call.response.header("Content-Type", "text/csv")
                call.response.header("Content-Transfer-Encoding", "binary")
                call.response.header("Pragma", "no-cache")
                call.respondText(csv.toString())
            }
            put {
                val event = call.receiveEvent()
                    ?: return@put

                val updateEvent = call.receiveNullable<UpdateEvent>()
                if (updateEvent == null) {
                    call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body"))
                    return@put
                }
                val eventName = updateEvent.name
                    ?: event.eventName
                val deadline = updateEvent.deadline?.let { Timestamp.from(Instant.ofEpochMilli(it)) }
                    ?: event.deadline
                val start = updateEvent.start?.let { Timestamp.from(Instant.ofEpochMilli(it)) }
                    ?: event.start
                val end = updateEvent.end?.let { Timestamp.from(Instant.ofEpochMilli(it)) }
                    ?: event.end
                val maxTeamMembers = updateEvent.maxTeamMembers
                    ?: event.maximumTeamMembers

                val schemType =
                    if (updateEvent.schemType == "null") null else updateEvent.schemType?.let { SchematicType.fromDB(it) }
                        ?: event.schematicType
                val publicSchemsOnly = updateEvent.publicSchemsOnly
                    ?: event.publicSchemsOnly()

                if (updateEvent.addReferee != null) {
                    updateEvent.addReferee.forEach {
                        Referee.add(event.eventID, SteamwarUser.get(UUID.fromString(it))!!.getId())
                    }
                }

                if (updateEvent.removeReferee != null) {
                    updateEvent.removeReferee.forEach {
                        Referee.remove(event.eventID, SteamwarUser.get(UUID.fromString(it))!!.getId())
                    }
                }
                event.update(eventName, deadline, start, end, schemType, maxTeamMembers, publicSchemsOnly)
                call.respond(ResponseEvent(Event.byId(event.eventID)!!))
            }
            delete {
                val id = call.parameters["id"]?.toIntOrNull()
                if (id == null) {
                    call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID"))
                    return@delete
                }
                val event = Event.byId(id)
                if (event == null) {
                    call.respond(HttpStatusCode.NotFound, ResponseError("Event not found"))
                    return@delete
                }
                event.delete()
                call.respond(HttpStatusCode.NoContent)
            }
            configureEventFightRoutes()
            configureEventTeams()
            configureEventGroups()
            configureEventRelations()
            configureEventRefereesRouting()
        }
    }
}

suspend fun ApplicationCall.receiveEvent(fieldName: String = "id"): Event? {
    val eventId = parameters[fieldName]?.toIntOrNull()
    if (eventId == null) {
        respond(HttpStatusCode.BadRequest, ResponseError("Invalid event ID"))
        return null
    }

    val event = Event.byId(eventId)
    if (event == null) {
        respond(HttpStatusCode.NotFound, ResponseError("Event not found"))
        return null
    }

    return event
}