package com.nisemoe.nise.database

import com.nisemoe.generated.tables.references.*
import com.nisemoe.nise.DistributionEntry
import com.nisemoe.nise.Format
import com.nisemoe.nise.ReplayData
import com.nisemoe.nise.ReplayDataSimilarScore
import com.nisemoe.nise.osu.Mod
import org.jooq.DSLContext
import org.nisemoe.mari.judgements.CompressJudgements
import org.springframework.stereotype.Service
import java.time.LocalDateTime
import java.util.*
import kotlin.math.roundToInt

@Service
class UserScoreService(
    private val dslContext: DSLContext,
    private val scoreService: ScoreService
) {

    fun getReplayData(replayId: UUID): ReplayData? {
        val result = dslContext.select(
            USER_SCORES.ID,
            USER_SCORES.PLAYER_NAME,
            BEATMAPS.BEATMAP_ID,
            BEATMAPS.BEATMAPSET_ID,
            BEATMAPS.ARTIST,
            BEATMAPS.TITLE,
            BEATMAPS.STAR_RATING,
            BEATMAPS.CREATOR,
            BEATMAPS.VERSION,
            BEATMAPS.TOTAL_LENGTH,
            BEATMAPS.MAX_COMBO,
            BEATMAPS.BPM,
            BEATMAPS.ACCURACY,
            BEATMAPS.AR,
            BEATMAPS.CS,
            BEATMAPS.DRAIN,
            BEATMAPS.COUNT_CIRCLES,
            BEATMAPS.COUNT_SPINNERS,
            BEATMAPS.COUNT_SLIDERS,
            USER_SCORES.ONLINE_SCORE_ID,
            USER_SCORES.TOTAL_SCORE,
            USER_SCORES.FRAMETIME,
            USER_SCORES.UR,
            USER_SCORES.ADJUSTED_UR,
            USER_SCORES.MODS,
            USER_SCORES.SNAPS,
            USER_SCORES.EDGE_HITS,
            USER_SCORES.PERFECT,
            USER_SCORES.MAX_COMBO,
            USER_SCORES.COUNT_300,
            USER_SCORES.COUNT_100,
            USER_SCORES.COUNT_50,
            USER_SCORES.COUNT_MISS,
            USER_SCORES.MEAN_ERROR,
            USER_SCORES.ERROR_VARIANCE,
            USER_SCORES.ERROR_STANDARD_DEVIATION,
            USER_SCORES.MINIMUM_ERROR,
            USER_SCORES.MAXIMUM_ERROR,
            USER_SCORES.ERROR_RANGE,
            USER_SCORES.ERROR_COEFFICIENT_OF_VARIATION,
            USER_SCORES.ERROR_KURTOSIS,
            USER_SCORES.ERROR_SKEWNESS,
            USER_SCORES.SLIDEREND_RELEASE_TIMES,
            USER_SCORES.KEYPRESSES_TIMES,
            USER_SCORES.JUDGEMENTS
        )
            .from(USER_SCORES)
            .join(BEATMAPS).on(USER_SCORES.BEATMAP_ID.eq(BEATMAPS.BEATMAP_ID))
            .where(USER_SCORES.ID.eq(replayId))
            .fetchOne() ?: return null

        val beatmapId = result.get(BEATMAPS.BEATMAP_ID, Int::class.java)
        val hitDistribution = this.getHitDistribution(result.get(USER_SCORES.JUDGEMENTS, ByteArray::class.java))
        val charts = this.scoreService.getCharts(result)

        val replayData = ReplayData(
            replay_id = result.get(USER_SCORES.ONLINE_SCORE_ID, Long::class.java),
            username = result.get(USER_SCORES.PLAYER_NAME, String::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),
            beatmap_bpm = result.get(BEATMAPS.BPM, Double::class.java),
            beatmap_max_combo = result.get(BEATMAPS.MAX_COMBO, Int::class.java),
            beatmap_total_length = result.get(BEATMAPS.TOTAL_LENGTH, Int::class.java),
            beatmap_accuracy = result.get(BEATMAPS.ACCURACY, Double::class.java),
            beatmap_ar = result.get(BEATMAPS.AR, Double::class.java),
            beatmap_cs = result.get(BEATMAPS.CS, Double::class.java),
            beatmap_drain = result.get(BEATMAPS.DRAIN, Double::class.java),
            beatmap_count_circles = result.get(BEATMAPS.COUNT_CIRCLES, Int::class.java),
            beatmap_count_spinners = result.get(BEATMAPS.COUNT_SPINNERS, Int::class.java),
            beatmap_count_sliders = result.get(BEATMAPS.COUNT_SLIDERS, Int::class.java),
            frametime = result.get(USER_SCORES.FRAMETIME, Double::class.java).toInt(),
            ur = result.get(USER_SCORES.UR, Double::class.java),
            adjusted_ur = result.get(USER_SCORES.ADJUSTED_UR, Double::class.java),
            score = result.get(USER_SCORES.TOTAL_SCORE, Int::class.java),
            mods = Mod.parseModCombination(result.get(USER_SCORES.MODS, Int::class.java)),
            snaps = result.get(USER_SCORES.SNAPS, Int::class.java),
            hits = result.get(USER_SCORES.EDGE_HITS, Int::class.java),
            perfect = result.get(USER_SCORES.PERFECT, Boolean::class.java),
            max_combo = result.get(USER_SCORES.MAX_COMBO, Int::class.java),
            count_300 = result.get(USER_SCORES.COUNT_300, Int::class.java),
            count_100 = result.get(USER_SCORES.COUNT_100, Int::class.java),
            count_50 = result.get(USER_SCORES.COUNT_50, Int::class.java),
            count_miss = result.get(USER_SCORES.COUNT_MISS, Int::class.java),
            error_distribution = hitDistribution,
            mean_error = result.get(USER_SCORES.MEAN_ERROR, Double::class.java),
            error_variance = result.get(USER_SCORES.ERROR_VARIANCE, Double::class.java),
            error_standard_deviation = result.get(USER_SCORES.ERROR_STANDARD_DEVIATION, Double::class.java),
            minimum_error = result.get(USER_SCORES.MINIMUM_ERROR, Double::class.java),
            maximum_error = result.get(USER_SCORES.MAXIMUM_ERROR, Double::class.java),
            error_range = result.get(USER_SCORES.ERROR_RANGE, Double::class.java),
            error_coefficient_of_variation = result.get(USER_SCORES.ERROR_COEFFICIENT_OF_VARIATION, Double::class.java),
            error_kurtosis = result.get(USER_SCORES.ERROR_KURTOSIS, Double::class.java),
            error_skewness = result.get(USER_SCORES.ERROR_SKEWNESS, Double::class.java),
            charts = charts,
            similar_scores = this.getSimilarScores(replayId),
            date = null,
            pp = null,
            rank = null,
            user_id = null
        )
        this.scoreService.loadComparableReplayData(replayData)
        return replayData
    }

    fun getSimilarScores(scoreId: UUID): List<ReplayDataSimilarScore> {
        val similarScoresWithReplayData = dslContext
            .select(
                USER_SCORES_SIMILARITY.SIMILARITY,
                USER_SCORES_SIMILARITY.CORRELATION,
                SCORES.REPLAY_ID,
                SCORES.USER_ID,
                USERS.USERNAME,
                SCORES.DATE,
                SCORES.PP,
                SCORES.BEATMAP_ID
            )
            .from(USER_SCORES_SIMILARITY)
            .join(SCORES)
            .on(SCORES.REPLAY_ID.eq(USER_SCORES_SIMILARITY.REPLAY_ID_OSU))
            .join(USERS)
            .on(SCORES.USER_ID.eq(USERS.USER_ID))
            .where(USER_SCORES_SIMILARITY.REPLAY_ID_USER.eq(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 getHitDistribution(compressedJudgements: ByteArray): Map<Int, DistributionEntry> {
        val judgements = CompressJudgements.decompress(compressedJudgements)

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

        judgements.forEach { hit ->
            val error = (hit.error.roundToInt() / 2) * 2
            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()
            )
        }
    }

}