package com.nisemoe.nise.scheduler

import com.nisemoe.generated.tables.records.ScoresRecord
import com.nisemoe.generated.tables.references.*
import com.nisemoe.konata.Replay
import com.nisemoe.konata.ReplaySetComparison
import com.nisemoe.konata.compareReplaySet
import com.nisemoe.nise.UserQueueDetails
import com.nisemoe.nise.database.ScoreService
import com.nisemoe.nise.database.UserService
import com.nisemoe.nise.integrations.CircleguardService
import com.nisemoe.nise.integrations.DiscordEmbed
import com.nisemoe.nise.integrations.DiscordService
import com.nisemoe.nise.osu.Mod
import com.nisemoe.nise.osu.OsuApi
import com.nisemoe.nise.osu.OsuApiModels
import com.nisemoe.nise.service.CacheService
import com.nisemoe.nise.service.CompressJudgements
import com.nisemoe.nise.service.UpdateUserQueueService
import kotlinx.serialization.Serializable
import org.jooq.DSLContext
import org.jooq.Query
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Profile
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import org.springframework.util.StopWatch
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset

@Service
@RestController
@Profile("import:scores")
class ImportScores(
    private val dslContext: DSLContext,
    private val userService: UserService,
    private val osuApi: OsuApi,
    private val cacheService: CacheService,
    private val discordService: DiscordService,
    private val scoreService: ScoreService,
    private val updateUserQueueService: UpdateUserQueueService,
    private val circleguardService: CircleguardService,
    private val messagingTemplate: SimpMessagingTemplate,
    private val compressJudgements: CompressJudgements
) : InitializingBean {

    private val userToUpdateBucket = mutableListOf<Long>()

    override fun afterPropertiesSet() {
        try {
            val userBucketCached = cacheService.getVariable("userToUpdateBucket", List::class.java)
            if(userBucketCached != null) {
                @Suppress("UNCHECKED_CAST")
                userToUpdateBucket.addAll(userBucketCached as List<Long>)
            }
        } catch (e: Exception) {
            logger.error("Failed to fetch userToUpdateBucket from cache.")
            this.cacheService.deleteVariable("userToUpdateBucket")
        }
    }
    
    companion object {
        
        const val CURRENT_VERSION = 7
        const val SLEEP_AFTER_API_CALL = 500L
        const val UPDATE_USER_EVERY_DAYS = 7L
        const val UPDATE_BANNED_USERS_EVERY_DAYS = 3L
        
    }
    
    @Value("\${WEBHOOK_URL}")
    private lateinit var webhookUrl: String

    private val logger = LoggerFactory.getLogger(javaClass)

    
    data class UpdaterStatistics(
        var currentBeatmapsetPage: Int = 0,

        var currentScore: Int = 0,
        var totalScores: Int = 0,

        var beatmapsSkippedBecauseTooRecent: Int = 0,
        var beatmapsAddedToDatabase: Int = 0,
        var usersAddedToDatabase: Int = 0,

        var scoresSkippedBecauseAlreadyExists: Int = 0,
        var scoresAddedToDatabase: Int = 0,
        var scoresWithReplayAndAnalyzed: Int = 0,

        var rateLimitTotal: Long = 0,
        var rateLimitRemaining: Long = 0,
    ) {

        fun toMarkdown(): String {
            return """
            |**Updater Statistics:**
            |- Current Beatmapset Page: $currentBeatmapsetPage
            |- Beatmaps Skipped Because Too Recent: $beatmapsSkippedBecauseTooRecent
            |- Beatmaps Added To Database: $beatmapsAddedToDatabase
            |- Users Added To Database: $usersAddedToDatabase
            |- Scores Skipped Because Already Exists: $scoresSkippedBecauseAlreadyExists
            |- Scores Added To Database: $scoresAddedToDatabase
            |- Scores With Replay And Analyzed: $scoresWithReplayAndAnalyzed
        """.trimMargin()
        }

    }
    private var statistics: UpdaterStatistics = UpdaterStatistics()

    @GetMapping("status", produces = [MediaType.APPLICATION_JSON_VALUE])
    fun getStatus(): ResponseEntity<UpdaterStatistics> {
        val statisticsDto = this.statistics
        this.statistics.rateLimitTotal = this.osuApi.rateLimitTotal
        this.statistics.rateLimitRemaining = this.osuApi.rateLimitRemaining
        return ResponseEntity.ok(statisticsDto)
    }

    // Restart a minute after it's completed.
    @Scheduled(fixedDelay = 60000, initialDelay = 0)
    fun updateStuffScheduler() {
        this.statistics = UpdaterStatistics()
        try {
            this.doUpdateStuff()
        } catch (exception: Exception) {
            val errorEmbed = DiscordEmbed(
                title = "Exception ocurred",
                description = exception.stackTraceToString()
            )
            this.discordService.sendEmbeds(
                this.webhookUrl,
                listOf(errorEmbed)
            )
            this.logger.error(exception.stackTraceToString())
        } finally {
            val statisticsEmbed = DiscordEmbed(
                title = "Statistics",
                description = this.statistics.toMarkdown()
            )
            this.discordService.sendEmbeds(
                this.webhookUrl,
                listOf(statisticsEmbed)
            )
        }
    }

    fun updateScoreWeaving(beatmap: OsuApiModels.BeatmapCompact, score: OsuApiModels.Score) {
        // Check if we have any requests to update a specific user
        val queue = this.updateUserQueueService.getQueue()
        if(queue.isNotEmpty()) {
            this.logger.info("Processing ${queue.size} users from the queue.")
        }

        for(userId in queue) {
            val topUserScores = this.osuApi.getTopUserScores(userId = userId)
            val recentUserScores = this.osuApi.getTopUserScores(userId = userId, type = "recent")
            val firstPlaceUserScores = this.osuApi.getTopUserScores(userId = userId, type = "firsts")

            this.logger.info("Processing user with id = $userId")
            this.logger.info("Top scores: ${topUserScores?.size}")
            this.logger.info("Recent scores: ${recentUserScores?.size}")
            this.logger.info("First place scores: ${firstPlaceUserScores?.size}")

            Thread.sleep(SLEEP_AFTER_API_CALL)

            if(topUserScores == null || recentUserScores == null || firstPlaceUserScores == null) {
                this.logger.error("Failed to fetch top scores for user with id = $userId")
                this.updateUserQueueService.setUserAsProcessed(userId, failed = true)
                continue
            }

            val allUserScores = (topUserScores + recentUserScores + firstPlaceUserScores)
                .filter { it.beatmap != null && it.beatmapset != null }
                .distinctBy { it.best_id }

            this.logger.info("Unique scores: ${allUserScores.size}")

            val userExists = dslContext.fetchExists(USERS, USERS.USER_ID.eq(userId), USERS.SYS_LAST_UPDATE.greaterOrEqual(OffsetDateTime.now(ZoneOffset.UTC).minusDays(UPDATE_USER_EVERY_DAYS)))
            if(!userExists) {
                val apiUser = this.osuApi.getUserProfile(userId = userId.toString(), mode = "osu", key = "id")
                if(apiUser != null) {
                    this.userService.insertApiUser(apiUser)
                    this.statistics.usersAddedToDatabase++
                } else {
                    this.logger.error("Failed to fetch user with id = $userId")
                }
            }

            var current = 0

            val lastCompletedUpdate = dslContext.select(UPDATE_USER_QUEUE.PROCESSED_AT)
                .from(UPDATE_USER_QUEUE)
                .where(UPDATE_USER_QUEUE.USER_ID.eq(userId))
                .and(UPDATE_USER_QUEUE.PROCESSED.isTrue)
                .orderBy(UPDATE_USER_QUEUE.PROCESSED_AT.desc())
                .limit(1)
                .fetchOneInto(OffsetDateTime::class.java)

            for(topScore in allUserScores) {
                val beatmapExists = dslContext.fetchExists(BEATMAPS, BEATMAPS.BEATMAP_ID.eq(topScore.beatmap!!.id))
                if (!beatmapExists) {
                    val beatmapFile = this.osuApi.getBeatmapFile(beatmapId = topScore.beatmap.id)
                    dslContext.insertInto(BEATMAPS)
                        .set(BEATMAPS.BEATMAP_ID, topScore.beatmap.id)
                        .set(BEATMAPS.BEATMAP_HASH, topScore.beatmap.checksum)
                        .set(BEATMAPS.BEATMAPSET_ID, topScore.beatmapset!!.id)
                        .set(BEATMAPS.STAR_RATING, topScore.beatmap.difficulty_rating)
                        .set(BEATMAPS.VERSION, topScore.beatmap.version)
                        .set(BEATMAPS.ARTIST, topScore.beatmapset.artist)
                        .set(BEATMAPS.SOURCE, topScore.beatmapset.source)
                        .set(BEATMAPS.TITLE, topScore.beatmapset.title)
                        .set(BEATMAPS.SOURCE, topScore.beatmapset.source)
                        .set(BEATMAPS.CREATOR, topScore.beatmapset.creator)
                        .set(BEATMAPS.BEATMAP_FILE, beatmapFile)
                        .execute()
                    this.statistics.beatmapsAddedToDatabase++
                }

                // Update the database
                dslContext.update(UPDATE_USER_QUEUE)
                    .set(UPDATE_USER_QUEUE.PROGRESS_CURRENT, current)
                    .set(UPDATE_USER_QUEUE.PROGRESS_TOTAL, allUserScores.size)
                    .where(UPDATE_USER_QUEUE.USER_ID.eq(userId))
                    .and(UPDATE_USER_QUEUE.PROCESSED.isFalse)
                    .execute()

                val currentQueueDetails = UserQueueDetails(
                    isProcessing = true,
                    lastCompletedUpdate = lastCompletedUpdate,
                    canUpdate = false,
                    progressCurrent = current,
                    progressTotal = allUserScores.size
                )

                // Update the frontend
                messagingTemplate.convertAndSend(
                    "/topic/live-user/${userId}",
                    UpdateUserQueueService.UserQueueWebsocketPacket(message = "UPDATE_PROGRESS", data = currentQueueDetails)
                )

                this.insertAndProcessNewScore(topScore.beatmap.id, topScore, isUserQueue = true)

                current += 1
            }

            // Check for stolen replays.
            val uniqueBeatmapIds = allUserScores
                .groupBy { it.beatmap!!.id }

            this.logger.info("Checking similarity for ${uniqueBeatmapIds.size} beatmaps.")

            uniqueBeatmapIds.forEach { checkReplaySimilarity(it.key) }

            // Update the backend
            this.updateUserQueueService.setUserAsProcessed(userId)
        }

        // Check if we need to check the banned users
        val lastBannedUserCheck = this.cacheService.getVariable("lastBannedUserCheck", LocalDateTime::class.java)
        if(lastBannedUserCheck == null || lastBannedUserCheck.isBefore(LocalDateTime.now().minusDays(UPDATE_BANNED_USERS_EVERY_DAYS))) {
            this.logger.info("Checking for banned users.")

            val suspiciousScores = dslContext.select(SCORES.USER_ID)
                .from(SCORES)
                .where(SCORES.UR.lessOrEqual(25.0).and(SCORES.IS_BANNED.isFalse))
                .groupBy(SCORES.USER_ID)
                .fetchInto(Long::class.java)

            this.logger.info("Found ${suspiciousScores.size} suspicious scores to update.")

            val stolenReplays = dslContext
                .select(ScoreService.osuScoreAlias1.USER_ID, ScoreService.osuScoreAlias2.USER_ID)
                .from(SCORES_SIMILARITY)
                .join(ScoreService.osuScoreAlias1).on(ScoreService.osuScoreAlias1.REPLAY_ID.eq(SCORES_SIMILARITY.REPLAY_ID_1))
                .join(ScoreService.osuUserAlias1).on(ScoreService.osuScoreAlias1.USER_ID.eq(ScoreService.osuUserAlias1.USER_ID))
                .leftJoin(ScoreService.osuScoreAlias2).on(ScoreService.osuScoreAlias2.REPLAY_ID.eq(SCORES_SIMILARITY.REPLAY_ID_2))
                .leftJoin(ScoreService.osuUserAlias2).on(ScoreService.osuScoreAlias2.USER_ID.eq(ScoreService.osuUserAlias2.USER_ID))
                .join(BEATMAPS).on(BEATMAPS.BEATMAP_ID.eq(ScoreService.osuScoreAlias1.BEATMAP_ID))
                .where(SCORES_SIMILARITY.SIMILARITY.lt(10.0))
                .and(ScoreService.osuScoreAlias1.IS_BANNED.eq(false))
                .and(ScoreService.osuScoreAlias2.IS_BANNED.eq(false))
                .groupBy(ScoreService.osuScoreAlias1.USER_ID, ScoreService.osuScoreAlias2.USER_ID)
                .fetch()

            // Sum lists and remove duplicate user ids
            val suspiciousUserIds = (suspiciousScores + stolenReplays.map {
                it.get(
                    ScoreService.osuScoreAlias1.USER_ID,
                    Long::class.java
                )
            } + stolenReplays.map { it.get(ScoreService.osuScoreAlias2.USER_ID, Long::class.java) }).distinct()

            suspiciousUserIds.forEachIndexed { index, userId ->
                this.logger.info("Checking user $userId [$index/${suspiciousUserIds.size}]")

                if(dslContext.fetchExists(USERS, USERS.USER_ID.eq(userId).and(USERS.SYS_LAST_UPDATE.greaterOrEqual(OffsetDateTime.now(ZoneOffset.UTC).minusDays(UPDATE_USER_EVERY_DAYS))))){
                    this.logger.info("User $userId was updated recently. Skipping.")
                    return@forEachIndexed
                }

                val isBanned = this.osuApi.checkIfUserBanned(userId)
                dslContext.update(USERS)
                    .set(USERS.SYS_LAST_UPDATE, OffsetDateTime.now())
                    .where(USERS.USER_ID.eq(userId))
                    .execute()

                if(isBanned == true) {
                    dslContext.update(SCORES)
                        .set(SCORES.IS_BANNED, true)
                        .where(SCORES.USER_ID.eq(userId))
                        .execute()
                    this.logger.info("User $userId is banned.")
                }
                Thread.sleep(SLEEP_AFTER_API_CALL)
            }

            this.cacheService.setVariable("lastBannedUserCheck", LocalDateTime.now())
        }

        // Nothing? Good, process the score as usual.
        this.insertAndProcessNewScore(beatmap.id, score)
    }

    fun doUpdateStuff() {
        var cursor: OsuApiModels.BeatmapsetSearchResultCursor? = null

        do {
            val searchResults = this.osuApi.searchBeatmapsets(cursor = cursor)
            this.statistics.currentBeatmapsetPage++
            Thread.sleep(SLEEP_AFTER_API_CALL)

            if (searchResults == null) {
                this.logger.error("Failed to fetch beatmapsets. Skipping to next page...")
                Thread.sleep(SLEEP_AFTER_API_CALL * 2)
                return
            }

            this.logger.debug("Fetched ${searchResults.beatmapsets.size} beatmapsets.")
            this.logger.debug("Cursor is ${searchResults.cursor_string}")

            for (beatmapset in searchResults.beatmapsets) {

                if (beatmapset.beatmaps.isNullOrEmpty()) {
                    this.logger.error("Beatmapset has no beatmaps.")
                    continue
                }

                for (beatmap in beatmapset.beatmaps.filter { it.mode == "osu" }) {
                    // Check if it exists in database, and if not, insert.
                    val beatmapExists = dslContext.fetchExists(BEATMAPS, BEATMAPS.BEATMAP_ID.eq(beatmap.id))

                    if(beatmapExists) {

                        val threeDaysAgo = OffsetDateTime
                            .now(ZoneOffset.UTC)
                            .minusDays(3)

                        if(dslContext.fetchExists(BEATMAPS, BEATMAPS.BEATMAP_ID.eq(beatmap.id).and(BEATMAPS.SYS_LAST_UPDATE.greaterOrEqual(threeDaysAgo)))) {
                            this.statistics.beatmapsSkippedBecauseTooRecent++
                            this.logger.debug("Skipping beatmap since it's been updated in the last few days.")
                            this.logger.debug("Beatmap ID: ${beatmap.id} | Beatmapset ID: ${beatmapset.id}")
                            continue
                        }
                    }

                    if (!beatmapExists) {
                        val beatmapFile = this.osuApi.getBeatmapFile(beatmapId = beatmap.id)
                        dslContext.insertInto(BEATMAPS)
                            .set(BEATMAPS.BEATMAP_ID, beatmap.id)
                            .set(BEATMAPS.BEATMAP_HASH, beatmap.checksum)
                            .set(BEATMAPS.BEATMAPSET_ID, beatmapset.id.toInt())
                            .set(BEATMAPS.STAR_RATING, beatmap.difficulty_rating)
                            .set(BEATMAPS.VERSION, beatmap.version)
                            .set(BEATMAPS.ARTIST, beatmapset.artist)
                            .set(BEATMAPS.SOURCE, beatmapset.source)
                            .set(BEATMAPS.TITLE, beatmapset.title)
                            .set(BEATMAPS.SOURCE, beatmapset.source)
                            .set(BEATMAPS.CREATOR, beatmapset.creator)
                            .set(BEATMAPS.BEATMAP_FILE, beatmapFile)
                            .execute()
                        this.statistics.beatmapsAddedToDatabase++
                    }

                    val beatmapScores = this.osuApi.getTopBeatmapScores(beatmapId = beatmap.id)
                    Thread.sleep(SLEEP_AFTER_API_CALL)

                    if (beatmapScores == null) {
                        this.logger.error("Failed to fetch beatmap scores for beatmapId = ${beatmap.id}")
                        Thread.sleep(SLEEP_AFTER_API_CALL * 2)
                        continue
                    }

                    if (beatmapScores.scores.isEmpty()) {
                        this.logger.error("Beatmap has no scores. Deleting for good measure...")
                        dslContext.deleteFrom(BEATMAPS)
                            .where(BEATMAPS.BEATMAP_ID.eq(beatmap.id))
                            .execute()
                        continue
                    }

                    this.statistics.totalScores = beatmapScores.scores.size
                    var totalScoresForBeatmap = 0

                    for (score in beatmapScores.scores) {
                        this.statistics.currentScore++
                        this.logger.debug("Processing score: ${this.statistics.currentScore}/${beatmapScores.scores.size}")

                        val userExists = dslContext.fetchExists(USERS, USERS.USER_ID.eq(score.user_id), USERS.SYS_LAST_UPDATE.greaterOrEqual(OffsetDateTime.now(ZoneOffset.UTC).minusDays(UPDATE_USER_EVERY_DAYS)))
                        if(!userExists) {
                            this.userToUpdateBucket.add(score.user_id)

                            if(this.userToUpdateBucket.size >= 50) {
                                val usersBucket = this.osuApi.getUsersBatch(this.userToUpdateBucket.toList())
                                Thread.sleep(SLEEP_AFTER_API_CALL)
                                if(usersBucket == null) {
                                    this.logger.error("Failed to fetch users batch.")
                                    continue
                                } else {
                                    for (user in usersBucket.users) {
                                        this.statistics.usersAddedToDatabase++
                                        this.userService.insertApiUser(user)
                                    }
                                }

                                this.userToUpdateBucket.clear()
                            }

                            this.cacheService.setVariable("userToUpdateBucket", this.userToUpdateBucket)
                        }

                        val scoreExists = dslContext.fetchExists(SCORES, SCORES.REPLAY_ID.eq(score.best_id))
                        if (scoreExists) {
                            this.statistics.scoresSkippedBecauseAlreadyExists++
                            this.logger.debug("Cool! Score exists, skipping to next.")
                            continue
                        }

                        totalScoresForBeatmap += 1
                        this.updateScoreWeaving(beatmap, score)
                    }

                    if(totalScoresForBeatmap > 0)
                        checkReplaySimilarity(beatmap.id)

                    dslContext.update(BEATMAPS)
                        .set(BEATMAPS.SYS_LAST_UPDATE, OffsetDateTime.now(ZoneOffset.UTC))
                        .where(BEATMAPS.BEATMAP_ID.eq(beatmap.id))
                        .execute()
                }
            }

            cursor = searchResults.cursor
        } while (cursor != null)
    }

    @Serializable
    data class ReplayDto(
        val replayId: Long,
        val replayMods: Int,
        val replayData: String
    )

    val sw = StopWatch()

    fun checkReplaySimilarity(beatmapId: Int) {
        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(ReplayDto::class.java)

        if(allReplays.size < 2) {
            this.logger.debug("Not enough replays to compare for beatmapId = $beatmapId.")
            return
        }

        val replaySetId = allReplays.map { it.replayId }.sortedDescending().joinToString("/")

        if(dslContext.fetchExists(BEATMAPS, BEATMAPS.BEATMAP_ID.eq(beatmapId).and(BEATMAPS.LAST_REPLAY_CHECK.eq(replaySetId)))) {
            this.logger.debug("Replay set already processed for beatmapId = $beatmapId.")
            return
        }

        sw.start("konata")

        val konataResults: List<ReplaySetComparison> = try {
            val replaysForKonata = allReplays.map {
                Replay(string = it.replayData, id = it.replayId, mods = it.replayMods)
            }.toTypedArray()
            compareReplaySet(replaysForKonata)
        } catch (e: Exception) {
            this.logger.error("Failed to process similarity with Konata for beatmapId = $beatmapId.")
            this.logger.error(e.stackTraceToString())
            return
        } finally {
            sw.stop()
        }

        this.logger.info("Obtained result from Konata in ${sw.lastTaskInfo().timeSeconds}s for ${allReplays.size} replays.")
        this.logger.info("Pairs/s = ${konataResults.size / sw.lastTaskInfo().timeSeconds}")

        val queries = mutableListOf<Query>()

        for(similarityEntry in konataResults) {
            if(similarityEntry.similarity < 10 || similarityEntry.correlation > 0.997) {

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

                try {

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

                    val replayDto2 = 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())
                }

                queries.add(dslContext
                    .insertInto(SCORES_SIMILARITY)
                    .set(SCORES_SIMILARITY.BEATMAP_ID, beatmapId)
                    .set(SCORES_SIMILARITY.REPLAY_ID_1, similarityEntry.replay1Id)
                    .set(SCORES_SIMILARITY.REPLAY_ID_2, similarityEntry.replay2Id)
                    .set(SCORES_SIMILARITY.SIMILARITY, similarityEntry.similarity)
                    .set(SCORES_SIMILARITY.CORRELATION, similarityEntry.correlation)
                    .set(SCORES_SIMILARITY.CREATED_AT, LocalDateTime.now())
                    .set(SCORES_SIMILARITY.CG_SIMILARITY, cgSimilarity)
                    .set(SCORES_SIMILARITY.CG_CORRELATION, cgCorrelation)
                    .onDuplicateKeyIgnore()
                )

                // We insert the userId of the newest replay in the queue for check
                val scoreForReplay1 = dslContext.select(SCORES.USER_ID, SCORES.DATE)
                    .from(SCORES)
                    .where(SCORES.REPLAY_ID.eq(similarityEntry.replay1Id))
                    .fetchOneInto(ScoresRecord::class.java)

                val scoreForReplay2 = dslContext.select(SCORES.USER_ID, SCORES.DATE)
                    .from(SCORES)
                    .where(SCORES.REPLAY_ID.eq(similarityEntry.replay2Id))
                    .fetchOneInto(ScoresRecord::class.java)

                if(scoreForReplay1 != null && scoreForReplay2 != null) {
                    if(scoreForReplay1.date != null && scoreForReplay2.date != null) {
                        if(scoreForReplay1.date!!.isAfter(scoreForReplay2.date)) {
                            this.updateUserQueueService.insertUser(scoreForReplay1.userId!!)
                        } else {
                            this.updateUserQueueService.insertUser(scoreForReplay2.userId!!)
                        }
                    }
                }
            }
        }

        dslContext.batch(queries).execute()

        // Save the replay set so that it doesnt' get calculated again
        dslContext.update(BEATMAPS)
            .set(BEATMAPS.LAST_REPLAY_CHECK, replaySetId)
            .where(BEATMAPS.BEATMAP_ID.eq(beatmapId))
            .execute()
    }

    private fun insertAndProcessNewScore(beatmapId: Int, score: OsuApiModels.Score, isUserQueue: Boolean = false) {
        // Check if the score is already in the database
        val scoreExists = dslContext.fetchExists(SCORES, SCORES.REPLAY_ID.eq(score.best_id))
        if (scoreExists) {
            this.statistics.scoresSkippedBecauseAlreadyExists++
            this.logger.debug("Cool! Score exists, skipping to next.")
            return
        }

        dslContext.insertInto(SCORES)
            .set(SCORES.BEATMAP_ID, beatmapId)
            .set(SCORES.COUNT_300, score.statistics.count_300)
            .set(SCORES.COUNT_100, score.statistics.count_100)
            .set(SCORES.COUNT_50, score.statistics.count_50)
            .set(SCORES.COUNT_MISS, score.statistics.count_miss)
            .set(SCORES.COUNT_MISS, score.statistics.count_miss)
            .set(SCORES.DATE, OffsetDateTime.parse(score.created_at).toLocalDateTime())
            .set(SCORES.MAX_COMBO, score.max_combo)
            .set(SCORES.RANK, "Grade.${score.rank.name}")
            .set(SCORES.MODS, Mod.combineModStrings(score.mods))
            .set(SCORES.PERFECT, score.perfect)
            .set(SCORES.PP, score.pp)
            .set(SCORES.SCORE, score.score)
            .set(SCORES.REPLAY_AVAILABLE, score.replay)
            .set(SCORES.REPLAY_ID, score.best_id)
            .set(SCORES.USER_ID, score.user_id)
            .set(SCORES.VERSION, CURRENT_VERSION)
            .execute()

        this.statistics.scoresAddedToDatabase++

        if (score.best_id == null || !score.replay) {
            this.logger.debug("No replay available, skipping score.")
            return
        }

        val scoreReplay = this.osuApi.getReplay(scoreId = score.best_id)

        if(scoreReplay == null || scoreReplay.content.isBlank()) {
            this.logger.error("Failed to fetch replay with score_id = $score.best_id")
            return
        }

        // Fetch the beatmap file from database
        var beatmapFile = dslContext.select(BEATMAPS.BEATMAP_FILE)
            .from(BEATMAPS)
            .where(BEATMAPS.BEATMAP_ID.eq(beatmapId))
            .fetchOneInto(String::class.java)

        if(beatmapFile == null) {
            this.logger.warn("Failed to fetch beatmap file for beatmap_id = $beatmapId from database")

            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
            } else {
                dslContext.update(BEATMAPS)
                    .set(BEATMAPS.BEATMAP_FILE, beatmapFile)
                    .where(BEATMAPS.BEATMAP_ID.eq(beatmapId))
                    .execute()
            }
        }

        // Calculate UR
        val processedReplay: CircleguardService.ReplayResponse? = try {
            this.circleguardService.processReplay(
                replayData = scoreReplay.content, beatmapData = beatmapFile, mods = Mod.combineModStrings(score.mods)
            ).get()
        } catch (e: Exception) {
            this.logger.error("Circleguard failed to process replay with score_id: ${score.id}")
            return
        }

        if (processedReplay == null || processedReplay.judgements.isEmpty()) {
            this.logger.error("Circleguard returned null and failed to process replay with score_id: ${score.id}")
            return
        }

        val scoreId = dslContext.update(SCORES)
            .set(SCORES.REPLAY, scoreReplay.content.toByteArray())
            .set(SCORES.UR, processedReplay.ur)
            .set(SCORES.ADJUSTED_UR, processedReplay.adjusted_ur)
            .set(SCORES.FRAMETIME, processedReplay.frametime)
            .set(SCORES.MEAN_ERROR, processedReplay.mean_error)
            .set(SCORES.ERROR_VARIANCE, processedReplay.error_variance)
            .set(SCORES.ERROR_STANDARD_DEVIATION, processedReplay.error_standard_deviation)
            .set(SCORES.MINIMUM_ERROR, processedReplay.minimum_error)
            .set(SCORES.MAXIMUM_ERROR, processedReplay.maximum_error)
            .set(SCORES.ERROR_RANGE, processedReplay.error_range)
            .set(SCORES.ERROR_COEFFICIENT_OF_VARIATION, processedReplay.error_coefficient_of_variation)
            .set(SCORES.ERROR_KURTOSIS, processedReplay.error_kurtosis)
            .set(SCORES.ERROR_SKEWNESS, processedReplay.error_skewness)
            .set(SCORES.SNAPS, processedReplay.snaps)
            .set(SCORES.EDGE_HITS, processedReplay.edge_hits)
            .set(SCORES.KEYPRESSES_TIMES, processedReplay.keypresses_times?.toTypedArray())
            .set(SCORES.KEYPRESSES_MEDIAN, processedReplay.keypresses_median)
            .set(SCORES.KEYPRESSES_MEDIAN_ADJUSTED, processedReplay.keypresses_median_adjusted)
            .set(SCORES.KEYPRESSES_STANDARD_DEVIATION, processedReplay.keypresses_standard_deviation)
            .set(SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED, processedReplay.keypresses_standard_deviation_adjusted)
            .set(SCORES.SLIDEREND_RELEASE_TIMES, processedReplay.sliderend_release_times?.toTypedArray())
            .set(SCORES.SLIDEREND_RELEASE_MEDIAN, processedReplay.sliderend_release_median)
            .set(SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED, processedReplay.sliderend_release_median_adjusted)
            .set(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION, processedReplay.sliderend_release_standard_deviation)
            .set(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED, processedReplay.sliderend_release_standard_deviation_adjusted)
            .set(SCORES.JUDGEMENTS, compressJudgements.serialize(processedReplay.judgements))
            .where(SCORES.REPLAY_ID.eq(score.best_id))
            .returningResult(SCORES.ID)
            .fetchOne()?.getValue(SCORES.ID)

        val replayData = this.scoreService.getReplayData(replayId = score.best_id)
        if(replayData == null) {
            this.logger.error("Weird, failed to fetch score with replay_id = ${score.best_id}. Cannot broadcast to Websocket.")
        } else {
            messagingTemplate.convertAndSend(
                "/topic/live-scores/",
                replayData
            )
        }

        this.statistics.scoresWithReplayAndAnalyzed++

        if (scoreId == null) {
            this.logger.error("Weird, failed to insert score into scores table. At least, it did not return an ID.")
            return
        }

        if(processedReplay.adjusted_ur != null && processedReplay.adjusted_ur < 25.0) {
            if(isUserQueue) {
                messagingTemplate.convertAndSend(
                    "/topic/live-user/${score.user_id}",
                    UpdateUserQueueService.UserQueueWebsocketPacket(message = "UPDATE_SCORES")
                )
            }

            if(!isUserQueue) {
                this.logger.info("Inserting user into queue for update: ${score.user_id}")
                this.logger.info("UR: ${processedReplay.ur} on their replay with id = ${score.best_id}")
                this.updateUserQueueService.insertUser(score.user_id)
            }
        }
    }

}