package com.nisemoe.nise.controller

import com.nisemoe.generated.tables.records.UserScoresRecord
import com.nisemoe.generated.tables.references.BEATMAPS
import com.nisemoe.generated.tables.references.SCORES
import com.nisemoe.generated.tables.references.USER_SCORES
import com.nisemoe.generated.tables.references.USER_SCORES_SIMILARITY
import com.nisemoe.konata.Replay
import com.nisemoe.konata.compareSingleReplayWithSet
import com.nisemoe.nise.database.BeatmapService
import com.nisemoe.nise.integrations.CircleguardService
import com.nisemoe.nise.osu.OsuApi
import com.nisemoe.nise.osu.OsuReplay
import com.nisemoe.nise.scheduler.ImportScores
import com.nisemoe.nise.service.CompressJudgements
import org.jooq.DSLContext
import org.slf4j.LoggerFactory
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
import java.time.OffsetDateTime

@RestController
class UploadReplayController(
    private val dslContext: DSLContext,
    private val beatmapService: BeatmapService,
    private val compressJudgements: CompressJudgements,
    private val circleguardService: CircleguardService,
    private val osuApi: OsuApi
) {

    private val logger = LoggerFactory.getLogger(javaClass)

    private val maxFileSize: Long = 4 * 1024 * 1024 // ~4MB

    data class AnalyzeReplayResult(
        val id: String
    )

    @PostMapping("analyze")
    fun analyzeReplay(@RequestParam("replay") replayFile: MultipartFile): ResponseEntity<AnalyzeReplayResult> {
        // Basic pre-flights checks
        if (replayFile.size > maxFileSize) {
            return ResponseEntity.badRequest().build()
        }

        // Create an OsuReplay instance and decode the file
        val replay = OsuReplay(replayFile.bytes)

        if(replay.gameMode != 0 || replay.beatmapHash.isNullOrBlank()) {
            return ResponseEntity.badRequest().build()
        }

        val beatmapId: Int?
        val beatmapFile: String?

        val beatmapFromDatabase = dslContext.selectFrom(BEATMAPS)
            .where(BEATMAPS.BEATMAP_HASH.eq(replay.beatmapHash))
            .fetchOne()

        if(beatmapFromDatabase != null) {
            beatmapId = beatmapFromDatabase.get(BEATMAPS.BEATMAP_ID)
            beatmapFile = beatmapFromDatabase.get(BEATMAPS.BEATMAP_FILE)
        } else {
            val beatmap = this.osuApi.getBeatmapFromHash(replay.beatmapHash!!)
                ?: return ResponseEntity.badRequest().build()

            beatmapId = beatmap.id

            beatmapFile = this.beatmapService.getBeatmapFile(beatmap.id)
                ?: return ResponseEntity.badRequest().build()

            if(!dslContext.fetchExists(BEATMAPS, BEATMAPS.BEATMAP_ID.eq(beatmap.id))) {
                    dslContext.insertInto(BEATMAPS)
                        .set(BEATMAPS.BEATMAP_ID, beatmap.id)
                        .set(BEATMAPS.BEATMAPSET_ID, beatmap.beatmapset_id)
                        .set(BEATMAPS.ARTIST, beatmap.beatmapset.artist)
                        .set(BEATMAPS.TITLE, beatmap.beatmapset.title)
                        .set(BEATMAPS.VERSION, beatmap.version)
                        .set(BEATMAPS.CREATOR, beatmap.beatmapset.creator)
                        .set(BEATMAPS.STAR_RATING, beatmap.difficulty_rating)
                        .set(BEATMAPS.SOURCE, beatmap.beatmapset.source)
                        .set(BEATMAPS.BEATMAP_HASH, replay.beatmapHash)
                        .set(BEATMAPS.BEATMAP_FILE, beatmapFile)
                        .execute()
                }
            else {
                dslContext.update(BEATMAPS)
                    .set(BEATMAPS.BEATMAP_HASH, replay.beatmapHash)
                    .set(BEATMAPS.BEATMAP_FILE, beatmapFile)
                    .where(BEATMAPS.BEATMAP_ID.eq(beatmap.id))
                    .execute()
            }
        }

        // Analyze the replay
        val analysis = this.circleguardService.processReplay(
            replayData = replay.replayData!!,
            beatmapData = beatmapFile!!,
            mods = replay.modsUsed
        ).get()

        val newUserReplayId = dslContext.insertInto(USER_SCORES)
            .set(USER_SCORES.BEATMAP_ID, beatmapId)
            .set(USER_SCORES.BEATMAP_HASH, replay.beatmapHash)
            .set(USER_SCORES.GAME_MODE, replay.gameMode)
            .set(USER_SCORES.GAME_VERSION, replay.gameVersion)
            .set(USER_SCORES.PLAYER_NAME, replay.playerName)
            .set(USER_SCORES.REPLAY_HASH, replay.replayHash)
            .set(USER_SCORES.COUNT_300, replay.numberOf300s)
            .set(USER_SCORES.COUNT_100, replay.numberOf100s)
            .set(USER_SCORES.COUNT_50, replay.numberOf50s)
            .set(USER_SCORES.COUNT_GEKI, replay.numberOfGekis)
            .set(USER_SCORES.COUNT_KATU, replay.numberOfKatus)
            .set(USER_SCORES.COUNT_MISS, replay.numberOfMisses)
            .set(USER_SCORES.TOTAL_SCORE, replay.totalScore)
            .set(USER_SCORES.MAX_COMBO, replay.greatestCombo)
            .set(USER_SCORES.PERFECT, replay.perfectCombo)
            .set(USER_SCORES.MODS, replay.modsUsed)
            .set(USER_SCORES.LIFE_BAR_GRAPH, replay.lifeBarGraph)
            .set(USER_SCORES.TIMESTAMP, replay.timestamp)
            .set(USER_SCORES.REPLAY_LENGTH, replay.replayLength)
            .set(USER_SCORES.REPLAY_DATA, replay.replayData)
            .set(USER_SCORES.ONLINE_SCORE_ID, replay.onlineScoreID)
            .set(USER_SCORES.ADDITIONAL_MOD_INFO, replay.additionalModInfo)
            .set(USER_SCORES.UR, analysis.ur)
            .set(USER_SCORES.FRAMETIME, analysis.frametime)
            .set(USER_SCORES.EDGE_HITS, analysis.edge_hits)
            .set(USER_SCORES.SNAPS, analysis.snaps)
            .set(USER_SCORES.ADJUSTED_UR, analysis.adjusted_ur)
            .set(USER_SCORES.MEAN_ERROR, analysis.mean_error)
            .set(USER_SCORES.ERROR_VARIANCE, analysis.error_variance)
            .set(USER_SCORES.ERROR_STANDARD_DEVIATION, analysis.error_standard_deviation)
            .set(USER_SCORES.MINIMUM_ERROR, analysis.minimum_error)
            .set(USER_SCORES.MAXIMUM_ERROR, analysis.maximum_error)
            .set(USER_SCORES.ERROR_RANGE, analysis.error_range)
            .set(USER_SCORES.ERROR_COEFFICIENT_OF_VARIATION, analysis.error_coefficient_of_variation)
            .set(USER_SCORES.ERROR_KURTOSIS, analysis.error_kurtosis)
            .set(USER_SCORES.ERROR_SKEWNESS, analysis.error_skewness)
            .set(USER_SCORES.KEYPRESSES_TIMES, analysis.keypresses_times?.toTypedArray())
            .set(USER_SCORES.KEYPRESSES_MEDIAN, analysis.keypresses_median)
            .set(USER_SCORES.KEYPRESSES_STANDARD_DEVIATION, analysis.keypresses_standard_deviation)
            .set(USER_SCORES.SLIDEREND_RELEASE_TIMES, analysis.sliderend_release_times?.toTypedArray())
            .set(USER_SCORES.SLIDEREND_RELEASE_MEDIAN, analysis.sliderend_release_median)
            .set(USER_SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION, analysis.sliderend_release_standard_deviation)
            .set(USER_SCORES.KEYPRESSES_MEDIAN_ADJUSTED, analysis.keypresses_median_adjusted)
            .set(USER_SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED, analysis.keypresses_standard_deviation_adjusted)
            .set(USER_SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED, analysis.sliderend_release_median_adjusted)
            .set(USER_SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED, analysis.sliderend_release_standard_deviation_adjusted)
            .set(SCORES.JUDGEMENTS, compressJudgements.serialize(analysis.judgements))
            .returning(USER_SCORES.ID)
            .fetchOne()

        if(newUserReplayId == null) {
            return ResponseEntity.internalServerError().build()
        }

        if(replay.onlineScoreID != null) {
            try {
                checkSimilarity(beatmapId, replay, newUserReplayId)
            } catch (exception: Exception) {
                this.logger.error("Failed to process similarity.")
                this.logger.error(exception.stackTraceToString())
            }

        }

        return ResponseEntity.ok(AnalyzeReplayResult(newUserReplayId.get(USER_SCORES.ID).toString()))
    }

    private fun checkSimilarity(
        beatmapId: Int?,
        replay: OsuReplay,
        newUserReplayId: UserScoresRecord
    ) {
        val allReplays = dslContext.select(
            SCORES.REPLAY_ID.`as`("replayId"),
            SCORES.MODS.`as`("replayMods"),
            SCORES.REPLAY.`as`("replayData")
        )
            .from(SCORES)
            .where(SCORES.BEATMAP_ID.eq(beatmapId))
            .and(SCORES.REPLAY.isNotNull)
            .and(SCORES.IS_BANNED.isFalse)
            .fetchInto(ImportScores.ReplayDto::class.java)

        if (allReplays.size > 2) {

            val referenceReplay =
                Replay(string = replay.replayData!!, id = replay.onlineScoreID ?: -1, mods = replay.modsUsed)

            val replaysForKonata = allReplays
                .filter { it.replayId != referenceReplay.id }
                .map { Replay(string = it.replayData, id = it.replayId, mods = it.replayMods) }
                .toTypedArray()

            val comparisonResult = compareSingleReplayWithSet(referenceReplay, replaysForKonata)
            for (similarityEntry in comparisonResult) {
                if (similarityEntry.similarity < 10 || similarityEntry.correlation > 0.997) {

                    var cgSimilarity: Double? = null
                    var cgCorrelation: Double? = null

                    try {

                        val replayDto1 = ImportScores.ReplayDto(
                            replayId = similarityEntry.replay1Id,
                            replayMods = similarityEntry.replay1Mods,
                            replayData = allReplays.find { it.replayId == similarityEntry.replay1Id }!!.replayData
                        )

                        val replayDto2 = ImportScores.ReplayDto(
                            replayId = similarityEntry.replay2Id,
                            replayMods = similarityEntry.replay2Mods,
                            replayData = allReplays.find { it.replayId == similarityEntry.replay2Id }!!.replayData
                        )

                        val cgResult = circleguardService.processSimilarity(listOf(replayDto1, replayDto2))
                            .get()

                        cgSimilarity = cgResult.result.first().similarity
                        cgCorrelation = cgResult.result.first().correlation
                    } catch (exception: Exception) {
                        this.logger.error("Failed to process similarity with circleguard.")
                        this.logger.error(exception.stackTraceToString())
                    }

                    dslContext
                        .insertInto(USER_SCORES_SIMILARITY)
                        .set(USER_SCORES_SIMILARITY.BEATMAP_ID, beatmapId)
                        .set(USER_SCORES_SIMILARITY.REPLAY_ID_USER, newUserReplayId.id)
                        .set(USER_SCORES_SIMILARITY.REPLAY_ID_OSU, similarityEntry.replay2Id)
                        .set(USER_SCORES_SIMILARITY.SIMILARITY, similarityEntry.similarity)
                        .set(USER_SCORES_SIMILARITY.CORRELATION, similarityEntry.correlation)
                        .set(USER_SCORES_SIMILARITY.CREATED_AT, OffsetDateTime.now())
                        .set(USER_SCORES_SIMILARITY.CG_SIMILARITY, cgSimilarity)
                        .set(USER_SCORES_SIMILARITY.CG_CORRELATION, cgCorrelation)
                        .onDuplicateKeyIgnore()
                        .execute()
                }
            }
        }
    }
}