/*
 *   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.sql

import de.steamwar.sql.internal.useDb
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.core.dao.id.CompositeID
import org.jetbrains.exposed.v1.core.dao.id.CompositeIdTable
import org.jetbrains.exposed.v1.core.dao.id.EntityID
import org.jetbrains.exposed.v1.dao.CompositeEntity
import org.jetbrains.exposed.v1.dao.CompositeEntityClass
import org.jetbrains.exposed.v1.jdbc.insertIgnore
import org.jetbrains.exposed.v1.jdbc.select
import java.util.concurrent.ConcurrentHashMap

object UserEloTable : CompositeIdTable("UserElo") {
    val season = integer("Season").entityId()
    val gameMode = varchar("GameMode", 16).entityId()
    val userId = reference("UserID", SteamwarUserTable)
    val elo = integer("Elo")

    override val primaryKey = PrimaryKey(season, gameMode, userId)

    init {
        addIdColumn(season)
        addIdColumn(gameMode)
    }
}

class UserElo(id: EntityID<CompositeID>) : CompositeEntity(id) {
    companion object : CompositeEntityClass<UserElo>(UserEloTable) {
        private const val ELO_DEFAULT = 0

        private val gameModeUserEloCache: MutableMap<String, MutableMap<Int, Int?>> = ConcurrentHashMap()
        private val emblemCache: MutableMap<Int, String> = ConcurrentHashMap()

        @JvmStatic
        fun clear() {
            gameModeUserEloCache.clear()
            emblemCache.clear()
        }

        @JvmStatic
        fun getEloOrDefault(userId: Int, gameMode: String) =
            getElo(userId, gameMode) ?: ELO_DEFAULT

        @JvmStatic
        fun getElo(userId: Int, gameMode: String) =
            gameModeUserEloCache.getOrPut(gameMode) { mutableMapOf() }
                .getOrPut(userId) { getEloFromDb(userId, gameMode)?.elo }

        private fun getEloFromDb(userId: Int, gameMode: String) = useDb {
            find { (UserEloTable.userId eq userId) and (UserEloTable.gameMode eq gameMode) and (UserEloTable.season eq Season.getSeason()) }.firstOrNull()
        }

        @JvmStatic
        fun getFightsOfSeason(userId: Int, gamemode: String) = useDb {
            exec(
                "SELECT COUNT(*) AS Fights FROM FightPlayer INNER JOIN Fight F on FightPlayer.FightID = F.FightID WHERE UserID = ? AND GameMode = ? AND UNIX_TIMESTAMP(StartTime) + Duration >= UNIX_TIMESTAMP(?)",
                args = listOf(
                    IntegerColumnType() to userId,
                    VarCharColumnType() to gamemode,
                    VarCharColumnType() to Season.getSeasonStart()
                )
            ) {
                return@exec if (it.next()) {
                    it.getInt("Fights")
                } else {
                    0
                }
            } ?: 0
        }

        @JvmStatic
        fun setElo(userId: Int, gameMode: String, elo: Int) {
            emblemCache.remove(userId)
            gameModeUserEloCache.getOrDefault(gameMode, mutableMapOf()).remove(userId)
            useDb {
                findByIdAndUpdate(CompositeID {
                    it[UserEloTable.userId] = userId
                    it[UserEloTable.gameMode] = gameMode
                    it[UserEloTable.season] = Season.getSeason()
                }) {
                    it.elo = elo
                } ?: UserEloTable.insertIgnore {
                    it[UserEloTable.userId] = userId
                    it[UserEloTable.gameMode] = gameMode
                    it[UserEloTable.season] = Season.getSeason()
                    it[UserEloTable.elo] = elo
                }
            }
        }

        @JvmStatic
        fun getPlacement(elo: Int, gamemode: String) = useDb {
            UserEloTable.select(UserEloTable.userId.count()).where {
                (UserEloTable.gameMode eq gamemode) and (UserEloTable.elo greater elo) and (UserEloTable.season eq Season.getSeason())
            }.firstOrNull()?.get(UserEloTable.userId.count())?.let { it + 1 }?.toInt() ?: -1
        }

        @JvmStatic
        fun getEmblem(user: SteamwarUser, rankedModes: List<String>) =
            emblemCache.getOrPut(user.id.value) {
                var emblemProgression = -1
                for (mode in rankedModes) {
                    if (getFightsOfSeason(user.id.value, mode) == 0) continue
                    val progression = getProgression(user.id.value, mode)
                    if (progression > emblemProgression) {
                        emblemProgression = progression
                    }
                }
                return toEmblem(emblemProgression)
            }

        @JvmStatic
        fun getEmblemProgression(gameMode: String, userId: Int): String =
            when (getProgression(userId, gameMode)) {
                -1 -> "§8❱❱❱❱ ❂"
                0 -> "§e❱§8❱❱❱ ❂"
                1 -> "§e❱❱§8❱❱ ❂"
                2 -> "§e❱❱❱§8❱ ❂"
                3 -> "§e❱❱❱❱§8 ❂"
                4 -> "§8❱❱❱❱ §5❂"
                else -> throw SecurityException("Progression is not in range")
            }


        @JvmStatic
        fun getProgression(userId: Int, gameMode: String) = useDb { getElo(userId, gameMode) ?: -1 }.let {
            when {
                it < 0 -> -1
                it < 150 -> 0
                it < 350 -> 1
                it < 600 -> 2
                it < 900 -> 3
                else -> 4
            }
        }

        @JvmStatic
        fun toEmblem(progression: Int) = when (progression) {
            -1 -> ""
            0 -> "§e❱ "
            1 -> "§e❱❱ "
            2 -> "§e❱❱❱ "
            3 -> "§e❱❱❱❱ "
            4 -> "§5❂ "
            else -> throw SecurityException("Progression out of range")
        }
    }

    var elo by UserEloTable.elo
}