package com.nisemoe.nise.database

import com.nisemoe.generated.tables.references.*
import com.nisemoe.nise.*
import com.nisemoe.nise.osu.Mod
import com.nisemoe.nise.service.AuthService
import com.nisemoe.nise.service.CompressJudgements
import org.jooq.DSLContext
import org.jooq.Record
import org.jooq.impl.DSL
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 authService: AuthService,
    private val compressJudgements: CompressJudgements
) {

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

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

    fun getReplayData(replayId: UUID): UserReplayData? {
        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,
            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.getCharts(result)

        val replayData = UserReplayData(
            replay_id = replayId,
            osu_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),
            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)
        )
        this.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 loadComparableReplayData(replayData: UserReplayData) {
        // Total samples
        val totalSamples = dslContext.fetchCount(
            SCORES, SCORES.BEATMAP_ID.eq(replayData.beatmap_id)
        )

        if(totalSamples <= 0) {
            return
        }

        // We will select same beatmap_id and same mods
        val otherScores = dslContext.select(
            DSL.avg(SCORES.MEAN_ERROR).`as`("avg_mean_error"),
            DSL.avg(SCORES.ERROR_VARIANCE).`as`("avg_error_variance"),
            DSL.avg(SCORES.ERROR_STANDARD_DEVIATION).`as`("avg_error_standard_deviation"),
            DSL.avg(SCORES.MINIMUM_ERROR).`as`("avg_minimum_error"),
            DSL.avg(SCORES.MAXIMUM_ERROR).`as`("avg_maximum_error"),
            DSL.avg(SCORES.ERROR_RANGE).`as`("avg_error_range"),
            DSL.avg(SCORES.ERROR_COEFFICIENT_OF_VARIATION).`as`("avg_error_coefficient_of_variation"),
            DSL.avg(SCORES.ERROR_KURTOSIS).`as`("avg_error_kurtosis"),
            DSL.avg(SCORES.ERROR_SKEWNESS).`as`("avg_error_skewness")
        )
            .from(SCORES)
            .where(SCORES.BEATMAP_ID.eq(replayData.beatmap_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 getHitDistribution(compressedJudgements: ByteArray): Map<Int, DistributionEntry> {
        val judgements = compressJudgements.deserialize(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()
            )
        }
    }

}