package com.nisemoe.nise.database

import com.nisemoe.generated.enums.JudgementType
import com.nisemoe.generated.tables.records.ScoresJudgementsRecord
import com.nisemoe.generated.tables.records.ScoresRecord
import com.nisemoe.generated.tables.references.*
import com.nisemoe.nise.*
import com.nisemoe.nise.integrations.CircleguardService
import com.nisemoe.nise.osu.Mod
import com.nisemoe.nise.osu.OsuApi
import com.nisemoe.nise.service.AuthService
import com.nisemoe.nise.service.CompressJudgements
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream
import org.jooq.Condition
import org.jooq.DSLContext
import org.jooq.Record
import org.jooq.impl.DSL
import org.jooq.impl.DSL.avg
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.time.LocalDateTime
import java.util.*
import kotlin.math.roundToInt

@Service
class ScoreService(
    private val dslContext: DSLContext,
    private val beatmapService: BeatmapService,
    private val authService: AuthService,
    private val osuApi: OsuApi,
    private val compressJudgements: CompressJudgements
) {

    private val logger = LoggerFactory.getLogger(javaClass)

    companion object {

        val osuScoreAlias1 = SCORES.`as`("osu_score_alias1")
        val osuScoreAlias2 = SCORES.`as`("osu_score_alias2")
        val osuUserAlias1 = USERS.`as`("osu_user_alias1")
        val osuUserAlias2 = USERS.`as`("osu_user_alias2")

    }

    fun getCharts(db: Record): List<ReplayDataChart> {
        if (!authService.isAdmin()) return emptyList()

        return listOf(SCORES.SLIDEREND_RELEASE_TIMES to "slider end release times", SCORES.KEYPRESSES_TIMES to "keypress times")
            .mapNotNull { (scoreType, title) -> db.get(scoreType)?.let { ReplayDataChart(title, it.filterNotNull()) } }
    }

    fun getReplayPairViewerData(replay1Id: Long, replay2Id: Long): ReplayPairViewerData? {
        val replay1 = getReplayViewerData(replay1Id) ?: return null
        val replay2 = getReplayViewerData(replay2Id) ?: return null

        if(replay1.beatmap != replay2.beatmap) {
            return null
        }

        return ReplayPairViewerData(
            beatmap = replay1.beatmap,
            replay1 = replay1.replay,
            replay2 = replay2.replay,
            mods = replay1.mods,
            judgements1 = replay1.judgements,
            judgements2 = replay2.judgements
        )
    }

    fun getReplayViewerData(replayId: Long): ReplayViewerData? {
        val beatmapId = dslContext.select(SCORES.BEATMAP_ID)
            .from(SCORES)
            .where(SCORES.REPLAY_ID.eq(replayId))
            .fetchOneInto(Int::class.java) ?: return null

        val result = dslContext.select(
            SCORES.REPLAY,
            SCORES.MODS,
            BEATMAPS.BEATMAP_FILE
        )
            .from(SCORES)
            .join(BEATMAPS).on(SCORES.BEATMAP_ID.eq(BEATMAPS.BEATMAP_ID))
            .where(SCORES.REPLAY_ID.eq(replayId))
            .fetchOne() ?: return null

        val replayData = result.get(SCORES.REPLAY, String::class.java) ?: return null

        val replay = decompressData(replayData)

        var beatmapFile = result.get(BEATMAPS.BEATMAP_FILE, String::class.java)
        if(beatmapFile == null) {
            beatmapFile = this.osuApi.getBeatmapFile(beatmapId = beatmapId)

            if(beatmapFile == null) {
                this.logger.error("Failed to fetch beatmap file for beatmap_id = $beatmapId from osu!api")
                return null
            } else {
                dslContext.update(BEATMAPS)
                    .set(BEATMAPS.BEATMAP_FILE, beatmapFile)
                    .where(BEATMAPS.BEATMAP_ID.eq(beatmapId))
                    .execute()
            }
        }

        val mods = result.get(SCORES.MODS, Int::class.java)

        return ReplayViewerData(
            beatmap = beatmapFile,
            replay = String(replay, Charsets.UTF_8).trimEnd(','),
            judgements = getJudgements(replayId),
            mods = mods
        )
    }

    private fun decompressData(replayString: String): ByteArray =
        Base64.getDecoder().decode(replayString).inputStream().use { byteStream ->
            LZMACompressorInputStream(byteStream).readBytes()
        }

    fun getReplayData(replayId: Long): ReplayData? {
        val result = dslContext.select(
            SCORES.ID,
            SCORES.USER_ID,
            USERS.USERNAME,
            SCORES.DATE,
            BEATMAPS.BEATMAP_ID,
            BEATMAPS.BEATMAPSET_ID,
            BEATMAPS.ARTIST,
            BEATMAPS.TITLE,
            BEATMAPS.STAR_RATING,
            BEATMAPS.CREATOR,
            BEATMAPS.VERSION,
            SCORES.PP,
            SCORES.FRAMETIME,
            SCORES.UR,
            SCORES.ADJUSTED_UR,
            SCORES.SCORE,
            SCORES.MODS,
            SCORES.RANK,
            SCORES.SNAPS,
            SCORES.EDGE_HITS,
            SCORES.PERFECT,
            SCORES.MAX_COMBO,
            SCORES.COUNT_300,
            SCORES.COUNT_100,
            SCORES.COUNT_50,
            SCORES.COUNT_MISS,
            SCORES.MEAN_ERROR,
            SCORES.ERROR_VARIANCE,
            SCORES.ERROR_STANDARD_DEVIATION,
            SCORES.MINIMUM_ERROR,
            SCORES.MAXIMUM_ERROR,
            SCORES.ERROR_RANGE,
            SCORES.ERROR_COEFFICIENT_OF_VARIATION,
            SCORES.ERROR_KURTOSIS,
            SCORES.ERROR_SKEWNESS,
            SCORES.SLIDEREND_RELEASE_TIMES,
            SCORES.KEYPRESSES_TIMES
        )
            .from(SCORES)
            .join(USERS).on(SCORES.USER_ID.eq(USERS.USER_ID))
            .join(BEATMAPS).on(SCORES.BEATMAP_ID.eq(BEATMAPS.BEATMAP_ID))
            .where(SCORES.REPLAY_ID.eq(replayId))
            .fetchOne() ?: return null

        val beatmapId = result.get(BEATMAPS.BEATMAP_ID, Int::class.java)
        val averageUR = beatmapService.getAverageUR(beatmapId = beatmapId, excludeReplayId = replayId)
        val hitDistribution = this.getHitDistribution(scoreId = result.get(SCORES.ID, Int::class.java))
        val charts = this.getCharts(result)

        val replayData = ReplayData(
            replay_id = replayId,
            user_id = result.get(SCORES.USER_ID, Int::class.java),
            username = result.get(USERS.USERNAME, String::class.java),
            date = Format.formatLocalDateTime(result.get(SCORES.DATE, LocalDateTime::class.java)),
            beatmap_id = beatmapId,
            beatmap_beatmapset_id = result.get(BEATMAPS.BEATMAPSET_ID, Int::class.java),
            beatmap_artist = result.get(BEATMAPS.ARTIST, String::class.java),
            beatmap_title = result.get(BEATMAPS.TITLE, String::class.java),
            beatmap_star_rating = result.get(BEATMAPS.STAR_RATING, Double::class.java),
            beatmap_creator = result.get(BEATMAPS.CREATOR, String::class.java),
            beatmap_version = result.get(BEATMAPS.VERSION, String::class.java),
            pp = result.get(SCORES.PP, Double::class.java),
            frametime = result.get(SCORES.FRAMETIME, Double::class.java).toInt(),
            ur = result.get(SCORES.UR, Double::class.java),
            adjusted_ur = result.get(SCORES.ADJUSTED_UR, Double::class.java),
            score = result.get(SCORES.SCORE, Int::class.java),
            mods = Mod.parseModCombination(result.get(SCORES.MODS, Int::class.java)),
            rank = result.get(SCORES.RANK, String::class.java),
            average_ur = averageUR,
            snaps = result.get(SCORES.SNAPS, Int::class.java),
            hits = result.get(SCORES.EDGE_HITS, Int::class.java),
            perfect = result.get(SCORES.PERFECT, Boolean::class.java),
            max_combo = result.get(SCORES.MAX_COMBO, Int::class.java),
            count_300 = result.get(SCORES.COUNT_300, Int::class.java),
            count_100 = result.get(SCORES.COUNT_100, Int::class.java),
            count_50 = result.get(SCORES.COUNT_50, Int::class.java),
            count_miss = result.get(SCORES.COUNT_MISS, Int::class.java),
            error_distribution = hitDistribution,
            mean_error = result.get(SCORES.MEAN_ERROR, Double::class.java),
            error_variance = result.get(SCORES.ERROR_VARIANCE, Double::class.java),
            error_standard_deviation = result.get(SCORES.ERROR_STANDARD_DEVIATION, Double::class.java),
            minimum_error = result.get(SCORES.MINIMUM_ERROR, Double::class.java),
            maximum_error = result.get(SCORES.MAXIMUM_ERROR, Double::class.java),
            error_range = result.get(SCORES.ERROR_RANGE, Double::class.java),
            error_coefficient_of_variation = result.get(SCORES.ERROR_COEFFICIENT_OF_VARIATION, Double::class.java),
            error_kurtosis = result.get(SCORES.ERROR_KURTOSIS, Double::class.java),
            error_skewness = result.get(SCORES.ERROR_SKEWNESS, Double::class.java),
            charts = charts,
            similar_scores = this.getSimilarScores(replayId)
        )
        this.loadComparableReplayData(replayData)
        return replayData
    }

    fun getDefaultCondition(): Condition {
        return SCORES.UR.lessOrEqual(25.0)
            .and(SCORES.IS_BANNED.eq(false))
    }

    fun getSuspiciousScores(condition: Condition = getDefaultCondition()): List<SuspiciousScoreEntry> {
        val result = dslContext.select(
            SCORES.USER_ID,
            USERS.USERNAME,
            SCORES.REPLAY_ID,
            SCORES.DATE,
            BEATMAPS.BEATMAP_ID,
            BEATMAPS.BEATMAPSET_ID,
            BEATMAPS.TITLE,
            BEATMAPS.STAR_RATING,
            SCORES.PP,
            SCORES.FRAMETIME,
            SCORES.UR
        )
            .from(SCORES)
            .join(USERS).on(SCORES.USER_ID.eq(USERS.USER_ID))
            .join(BEATMAPS).on(SCORES.BEATMAP_ID.eq(BEATMAPS.BEATMAP_ID))
            .where(condition)
            .orderBy(SCORES.DATE.desc())
            .fetch()

        val response = result.map {

            SuspiciousScoreEntry(
                user_id = it.get(SCORES.USER_ID, Long::class.java),
                username = it.get(USERS.USERNAME, String::class.java),
                replay_id = it.get(SCORES.REPLAY_ID, Long::class.java),
                date = Format.formatLocalDateTime(it.get(SCORES.DATE, LocalDateTime::class.java)),
                beatmap_id = it.get(BEATMAPS.BEATMAP_ID, Long::class.java),
                beatmap_beatmapset_id = it.get(BEATMAPS.BEATMAPSET_ID, Long::class.java),
                beatmap_title = it.get(BEATMAPS.TITLE, String::class.java),
                beatmap_star_rating = it.get(BEATMAPS.STAR_RATING, Double::class.java),
                pp = it.get(SCORES.PP, Double::class.java),
                frametime = it.get(SCORES.FRAMETIME, Double::class.java),
                ur = it.get(SCORES.UR, Double::class.java)
            )

        }

        return response
    }

    fun getPairInfo(replay1Id: Long, replay2Id: Long): ReplayPairStatistics? {
        // Attempt to fetch in the order (replay1Id, replay2Id)
        var pairStatistics = dslContext.select(SCORES_SIMILARITY.SIMILARITY, SCORES_SIMILARITY.CORRELATION)
            .from(SCORES_SIMILARITY)
            .where(
                SCORES_SIMILARITY.REPLAY_ID_1.eq(replay1Id)
                    .and(SCORES_SIMILARITY.REPLAY_ID_2.eq(replay2Id))
            )
            .fetchOne()

        // If no result, attempt to fetch in the order (replay2Id, replay1Id)
        if (pairStatistics == null) {
            pairStatistics = dslContext.select(SCORES_SIMILARITY.SIMILARITY, SCORES_SIMILARITY.CORRELATION)
                .from(SCORES_SIMILARITY)
                .where(
                    SCORES_SIMILARITY.REPLAY_ID_1.eq(replay2Id)
                        .and(SCORES_SIMILARITY.REPLAY_ID_2.eq(replay1Id))
                )
                .fetchOne()
        }

        // Return null if no results found in both attempts
        if (pairStatistics == null) return null

        // Return the found statistics
        return ReplayPairStatistics(
            similarity = pairStatistics.get(SCORES_SIMILARITY.SIMILARITY, Double::class.java),
            correlation = pairStatistics.get(SCORES_SIMILARITY.CORRELATION, Double::class.java)
        )
    }

    fun getSimilarReplaysRecords(condition: Condition = DSL.noCondition(), includeBanned: Boolean = false): List<Record> {
        return dslContext
            .select(
                osuScoreAlias1.REPLAY_ID,
                osuScoreAlias1.USER_ID,
                osuUserAlias1.USERNAME,
                osuScoreAlias1.DATE,
                osuScoreAlias1.PP,
                osuScoreAlias2.REPLAY_ID,
                osuScoreAlias2.USER_ID,
                osuUserAlias2.USERNAME,
                osuScoreAlias2.DATE,
                osuScoreAlias2.PP,
                BEATMAPS.BEATMAP_ID,
                BEATMAPS.TITLE,
                BEATMAPS.STAR_RATING,
                BEATMAPS.BEATMAPSET_ID,
                SCORES_SIMILARITY.SIMILARITY
            )
            .from(SCORES_SIMILARITY)
            .join(osuScoreAlias1).on(osuScoreAlias1.REPLAY_ID.eq(SCORES_SIMILARITY.REPLAY_ID_1))
            .join(osuUserAlias1).on(osuScoreAlias1.USER_ID.eq(osuUserAlias1.USER_ID))
            .leftJoin(osuScoreAlias2).on(osuScoreAlias2.REPLAY_ID.eq(SCORES_SIMILARITY.REPLAY_ID_2))
            .leftJoin(osuUserAlias2).on(osuScoreAlias2.USER_ID.eq(osuUserAlias2.USER_ID))
            .join(BEATMAPS).on(BEATMAPS.BEATMAP_ID.eq(SCORES_SIMILARITY.BEATMAP_ID))
            .where(SCORES_SIMILARITY.SIMILARITY.lt(10.0))
            .apply {
                if (!includeBanned) {
                    and(osuScoreAlias1.IS_BANNED.eq(false))
                    and(osuScoreAlias2.IS_BANNED.eq(false))
                }
            }
            .and(condition)
            .orderBy(osuScoreAlias2.DATE.desc(), SCORES_SIMILARITY.SIMILARITY.asc())
            .fetch()
    }

    fun getSimilarReplaysForUserId(userId: Long): List<SimilarReplayEntry> {
        val condition = osuScoreAlias1.USER_ID.eq(userId).or(osuScoreAlias2.USER_ID.eq(userId))
        val replays = getSimilarReplaysRecords(condition, includeBanned = true)

        // We only return replays where the user is the second user in the pair
        // The second user is the one who has the most recent replay
        // Which means it's *probably* the cheater.
        return mapSimilarReplays(replays)
            .filter { it.user_id_2 == userId }
    }

    fun getSimilarReplays(condition: Condition = DSL.noCondition()): List<SimilarReplayEntry> {
        val replays = getSimilarReplaysRecords(condition)
        return mapSimilarReplays(replays)
    }

    private fun mapSimilarReplays(replays: List<Record>) = replays.map {
        // Extract necessary fields
        var replayId1 = it.get(osuScoreAlias1.REPLAY_ID, Long::class.java)
        var replayId2 = it.get(osuScoreAlias2.REPLAY_ID, Long::class.java)

        var userId1 = it.get(osuScoreAlias1.USER_ID, Long::class.java)
        var userId2 = it.get(osuScoreAlias2.USER_ID, Long::class.java)

        var username1 = it.get(osuUserAlias1.USERNAME, String::class.java)
        var username2 = it.get(osuUserAlias2.USERNAME, String::class.java)

        var replayDate1 = it.get(osuScoreAlias1.DATE, LocalDateTime::class.java)
        var replayDate2 = it.get(osuScoreAlias2.DATE, LocalDateTime::class.java)

        var replayPp1 = it.get(osuScoreAlias1.PP, Double::class.java)
        var replayPp2 = it.get(osuScoreAlias2.PP, Double::class.java)

        // Swap logic if replayDate1 is after replayDate2
        if (replayDate1.isAfter(replayDate2)) {
            val tempReplayId = replayId1
            replayId1 = replayId2
            replayId2 = tempReplayId

            val tempUserId = userId1
            userId1 = userId2
            userId2 = tempUserId

            val tempUsername = username1
            username1 = username2
            username2 = tempUsername

            val tempReplayDate = replayDate1
            replayDate1 = replayDate2
            replayDate2 = tempReplayDate

            val tempReplayPp = replayPp1
            replayPp1 = replayPp2
            replayPp2 = tempReplayPp
        }

        SimilarReplayEntry(
            replay_id_1 = replayId1,
            replay_id_2 = replayId2,
            user_id_1 = userId1,
            user_id_2 = userId2,
            username_1 = username1,
            username_2 = username2,
            beatmap_beatmapset_id = it.get(BEATMAPS.BEATMAPSET_ID, Long::class.java),
            replay_date_1 = Format.formatLocalDateTime(replayDate1),
            replay_date_2 = Format.formatLocalDateTime(replayDate2),
            replay_pp_1 = replayPp1,
            replay_pp_2 = replayPp2,
            beatmap_id = it.get(BEATMAPS.BEATMAP_ID, Long::class.java),
            beatmap_title = it.get(BEATMAPS.TITLE, String::class.java),
            beatmap_star_rating = it.get(BEATMAPS.STAR_RATING, Double::class.java),
            similarity = it.get(SCORES_SIMILARITY.SIMILARITY, Double::class.java)
        )
    }.distinctBy {
        val (smallerId, largerId) = listOf(it.replay_id_1, it.replay_id_2).sorted()
        "$smallerId-$largerId"
    }.sortedWith(compareBy({ it.replay_date_2 }, { it.similarity })).reversed()

    fun getSimilarScores(scoreId: Long): List<ReplayDataSimilarScore> {
        val similarScoresWithReplayData = dslContext
            .select(
                SCORES_SIMILARITY.REPLAY_ID_1,
                SCORES_SIMILARITY.REPLAY_ID_2,
                SCORES_SIMILARITY.SIMILARITY,
                SCORES_SIMILARITY.CORRELATION,
                SCORES.REPLAY_ID,
                SCORES.USER_ID,
                USERS.USERNAME,
                SCORES.DATE,
                SCORES.PP,
                SCORES.BEATMAP_ID
            )
            .from(SCORES_SIMILARITY)
            .join(SCORES)
            .on(SCORES.REPLAY_ID.eq(SCORES_SIMILARITY.REPLAY_ID_1).or(SCORES.REPLAY_ID.eq(SCORES_SIMILARITY.REPLAY_ID_2)))
            .join(USERS)
            .on(SCORES.USER_ID.eq(USERS.USER_ID))
            .where(SCORES_SIMILARITY.REPLAY_ID_1.eq(scoreId).or(SCORES_SIMILARITY.REPLAY_ID_2.eq(scoreId)))
            .and(SCORES.REPLAY_ID.ne(scoreId))
            .fetch()

        return similarScoresWithReplayData.map { record ->
            ReplayDataSimilarScore(
                replay_id = record.get(SCORES.REPLAY_ID, Long::class.java),
                user_id = record.get(SCORES.USER_ID, Int::class.java),
                username = record.get(USERS.USERNAME, String::class.java),
                date = Format.formatLocalDateTime(record.get(SCORES.DATE, LocalDateTime::class.java)),
                pp = record.get(SCORES.PP, Double::class.java),
                similarity = record.get(SCORES_SIMILARITY.SIMILARITY, Double::class.java),
                correlation = record.get(SCORES_SIMILARITY.CORRELATION, Double::class.java)
            )
        }
    }

    fun loadComparableReplayData(replayData: ReplayData) {
        // Total samples
        val totalSamples = dslContext.fetchCount(
            SCORES, SCORES.BEATMAP_ID.eq(replayData.beatmap_id).and(SCORES.REPLAY_ID.notEqual(replayData.replay_id))
        )

        if(totalSamples <= 0) {
            return
        }

        // We will select same beatmap_id and same mods
        val otherScores = dslContext.select(
            avg(SCORES.MEAN_ERROR).`as`("avg_mean_error"),
            avg(SCORES.ERROR_VARIANCE).`as`("avg_error_variance"),
            avg(SCORES.ERROR_STANDARD_DEVIATION).`as`("avg_error_standard_deviation"),
            avg(SCORES.MINIMUM_ERROR).`as`("avg_minimum_error"),
            avg(SCORES.MAXIMUM_ERROR).`as`("avg_maximum_error"),
            avg(SCORES.ERROR_RANGE).`as`("avg_error_range"),
            avg(SCORES.ERROR_COEFFICIENT_OF_VARIATION).`as`("avg_error_coefficient_of_variation"),
            avg(SCORES.ERROR_KURTOSIS).`as`("avg_error_kurtosis"),
            avg(SCORES.ERROR_SKEWNESS).`as`("avg_error_skewness")
        )
            .from(SCORES)
            .where(SCORES.BEATMAP_ID.eq(replayData.beatmap_id))
            .and(SCORES.REPLAY_ID.notEqual(replayData.replay_id))
            .fetchOne() ?: return

        replayData.comparable_samples = totalSamples

        replayData.comparable_mean_error = otherScores.get("avg_mean_error", Double::class.java)
        replayData.comparable_error_variance = otherScores.get("avg_error_variance", Double::class.java)
        replayData.comparable_error_standard_deviation = otherScores.get("avg_error_standard_deviation", Double::class.java)
        replayData.comparable_minimum_error = otherScores.get("avg_minimum_error", Double::class.java)
        replayData.comparable_maximum_error = otherScores.get("avg_maximum_error", Double::class.java)
        replayData.comparable_error_range = otherScores.get("avg_error_range", Double::class.java)
        replayData.comparable_error_coefficient_of_variation = otherScores.get("avg_error_coefficient_of_variation", Double::class.java)
        replayData.comparable_error_kurtosis = otherScores.get("avg_error_kurtosis", Double::class.java)
        replayData.comparable_error_skewness = otherScores.get("avg_error_skewness", Double::class.java)
    }

    fun mapLegacyJudgement(judgementType: JudgementType): CircleguardService.JudgementType {
        return when(judgementType) {
            JudgementType.Miss -> CircleguardService.JudgementType.MISS
            JudgementType.`300` -> CircleguardService.JudgementType.THREE_HUNDRED
            JudgementType.`100` -> CircleguardService.JudgementType.ONE_HUNDRED
            JudgementType.`50` -> CircleguardService.JudgementType.FIFTY
        }
    }

    fun getJudgements(replayId: Long): List<CircleguardService.ScoreJudgement> {
        val judgementsRecord = dslContext.select(SCORES.JUDGEMENTS)
            .from(SCORES)
            .where(SCORES.REPLAY_ID.eq(replayId))
            .fetchOneInto(ScoresRecord::class.java)

        if(judgementsRecord?.judgements == null) {
            val scoreId = dslContext.select(SCORES.ID)
                .from(SCORES)
                .where(SCORES.REPLAY_ID.eq(replayId))
                .fetchOneInto(Int::class.java)

            val judgementRecords = dslContext.selectFrom(SCORES_JUDGEMENTS)
                .where(SCORES_JUDGEMENTS.SCORE_ID.eq(scoreId))
                .fetchInto(ScoresJudgementsRecord::class.java)
            return judgementRecords.map {
                CircleguardService.ScoreJudgement(
                    x = it.x!!,
                    y = it.y!!,
                    error = it.error!!,
                    distance_center = it.distanceCenter!!,
                    distance_edge = it.distanceEdge!!,
                    time = it.time!!,
                    type = mapLegacyJudgement(it.type!!)
                )
            }
        }

        return compressJudgements.deserialize(judgementsRecord.judgements!!)
    }

    fun getHitDistribution(scoreId: Int): Map<Int, DistributionEntry> {
        val judgementsRecord = dslContext.select(SCORES.JUDGEMENTS)
            .from(SCORES)
            .where(SCORES.ID.eq(scoreId))
            .fetchOneInto(ScoresRecord::class.java)

        if(judgementsRecord?.judgements == null) {
            return this.getHitDistributionLegacy(scoreId)
        }

        val judgements = compressJudgements.deserialize(judgementsRecord.judgements!!)

        val errorDistribution = mutableMapOf<Int, MutableMap<String, Int>>()
        var totalHits = 0

        judgements.forEach { hit ->
            val error = hit.error.roundToInt()
            val judgementType = hit.type // Assuming this is how you get the judgement type
            errorDistribution.getOrPut(error) { mutableMapOf("MISS" to 0, "THREE_HUNDRED" to 0, "ONE_HUNDRED" to 0, "FIFTY" to 0) }
                .apply {
                    this[judgementType.toString()] = this.getOrDefault(judgementType.toString(), 0) + 1
                }
            totalHits += 1
        }

        return errorDistribution.mapValues { (_, judgementCounts) ->
            judgementCounts.values.sum()
            DistributionEntry(
                countMiss = judgementCounts.getOrDefault("MISS", 0).toDouble(),
                count300 = judgementCounts.getOrDefault("THREE_HUNDRED", 0).toDouble(),
                count100 = judgementCounts.getOrDefault("ONE_HUNDRED", 0).toDouble(),
                count50 = judgementCounts.getOrDefault("FIFTY", 0).toDouble()
            )
        }
    }

    fun getHitDistributionLegacy(scoreId: Int): Map<Int, DistributionEntry> {
        val judgements = dslContext.selectFrom(SCORES_JUDGEMENTS)
            .where(SCORES_JUDGEMENTS.SCORE_ID.eq(scoreId))
            .fetchInto(ScoresJudgementsRecord::class.java)

        val errorDistribution = mutableMapOf<Int, MutableMap<String, Int>>()
        var totalHits = 0

        judgements.forEach { hit ->
            val error = hit.error!!.roundToInt()
            val judgementType = hit.type // Assuming this is how you get the judgement type
            errorDistribution.getOrPut(error) { mutableMapOf("Miss" to 0, "300" to 0, "100" to 0, "50" to 0) }
                .apply {
                    this[judgementType.toString()] = this.getOrDefault(judgementType.toString(), 0) + 1
                }
            totalHits += 1
        }

        return errorDistribution.mapValues { (_, judgementCounts) ->
            judgementCounts.values.sum()
            DistributionEntry(
                countMiss = judgementCounts.getOrDefault("MISS", 0).toDouble(),
                count300 = judgementCounts.getOrDefault("THREE_HUNDRED", 0).toDouble(),
                count100 = judgementCounts.getOrDefault("ONE_HUNDRED", 0).toDouble(),
                count50 = judgementCounts.getOrDefault("FIFTY", 0).toDouble()
            )
        }
    }

}