package com.nisemoe.nise.scheduler

import com.nisemoe.generated.tables.records.ScoresRecord
import com.nisemoe.generated.tables.records.UsersRecord
import com.nisemoe.generated.tables.references.SCORES
import com.nisemoe.generated.tables.references.USERS
import com.nisemoe.nise.osu.OsuApi
import org.jooq.DSLContext
import org.springframework.context.annotation.Profile
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt

data class UserReport(
    val username: String,
    val susScore: Double,
    val urgencyScore: Double,
    val userId: Long,
    val userScores: List<Agent.ScoreMetrics>
) {

    override fun toString(): String {
        val s = "User: $username (https://nise.moe/u/${username})- Suspiciousness score: ${susScore.roundToInt()} | Urgency score: ${urgencyScore.roundToInt()}"
        val s2 = "Average PP: ${userScores.map { it.pp }.average()} | Average UR: ${userScores.map { it.adjustedUr }.average()} | Average Sliderend Release: ${userScores.map { it.sliderendReleaseStandardDeviationAdjusted }.average()}"
        return s + "\n" + s2 + "\n"
    }

}

@Service
@Profile("agent")
class Agent(
    private val dslContext: DSLContext,
    private val osuApi: OsuApi
) {

    @Scheduled(fixedDelay = 1200000, initialDelay = 0)
    fun buildReports() {
        val reports = mutableListOf<UserReport>()

        val userIds = dslContext.select(SCORES.USER_ID)
            .from(SCORES)
            .where(SCORES.ADJUSTED_UR.lessOrEqual(256.0))
            .and(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED.isNotNull)
            .and(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED.lessOrEqual(16.0))
            .and(SCORES.IS_BANNED.isFalse)
            .and(SCORES.VERSION.greaterOrEqual(4))
            .groupBy(SCORES.USER_ID)
            .fetchInto(Long::class.java)

        println("Found ${userIds.size} users to check.")

        // We check if any of them are banned.
//        val bannedIds = mutableListOf<Long>()
//        for(chunk in userIds.chunked(50)) {
//
//            val result = osuApi.getUsersBatch(chunk)
//            if(result == null) {
//                println("Failed to get users from osu! api")
//                return
//            }
//
//            // Compare which userIds aren't in the result and mark them as banned.
//            val bannedUsers = chunk.filter { it !in result.users.map { it.id } }
//            bannedIds.addAll(bannedUsers)
//        }
//
//        if(bannedIds.isNotEmpty()) {
//            // Remove all banned users from the list
//            susUsers.removeIf { it.userId in bannedIds }
//
//            // Mark all their scores as banned
//            dslContext.update(SCORES)
//                .set(SCORES.IS_BANNED, true)
//                .where(SCORES.USER_ID.`in`(bannedIds))
//                .execute()
//
//            println("Marked ${bannedIds.size} users as banned: $bannedIds")
//        }

        for(userId in userIds) {
            val userDetails = dslContext.selectFrom(USERS)
                .where(USERS.USER_ID.eq(userId))
                .fetchOneInto(UsersRecord::class.java) ?: continue

            val username = userDetails.username!!
            val userScores =  dslContext.select(
                SCORES.DATE,
                SCORES.ADJUSTED_UR,
                SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED,
                SCORES.PP,
                SCORES.EDGE_HITS,
                SCORES.SNAPS,
                SCORES.KEYPRESSES_MEDIAN_ADJUSTED,
                SCORES.ERROR_KURTOSIS
            )
                .from(SCORES)
                .where(SCORES.ADJUSTED_UR.lessOrEqual(256.0))
                .and(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED.isNotNull)
                .and(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED.lessOrEqual(16.0))
                .and(SCORES.IS_BANNED.isFalse)
                .and(SCORES.VERSION.greaterOrEqual(4))
                .and(SCORES.USER_ID.eq(userId))
                .fetchInto(ScoresRecord::class.java)

            if(areMetricsBlatant(userScores.map { mapScoreToMetrics(userDetails, it) }))
                continue

            val susScore = calculateSuspiciousnessScore(userScores.map { mapScoreToMetrics(userDetails, it) })

            val newReport = UserReport(
                userId = userId,
                username = username,
                userScores = userScores.map { mapScoreToMetrics(userDetails, it) },
                susScore = susScore.first,
                urgencyScore = susScore.second
            )
            reports.add(newReport)
            println(newReport)
        }

        reports.sortByDescending { it.urgencyScore }
    }

    fun mapScoreToMetrics(user: UsersRecord, score: ScoresRecord): ScoreMetrics {
        return ScoreMetrics(
            date = score.date!!,
            country = user.country!!,
            pp = score.pp!!,
            adjustedUr = score.adjustedUr!!,
            sliderendReleaseStandardDeviationAdjusted = score.sliderendReleaseStandardDeviationAdjusted!!,
            edgeHits = score.edgeHits!!,
            snaps = score.snaps!!,
            keypressesMedianAdjusted = score.keypressesMedianAdjusted!!,
            errorKurtosis = score.errorKurtosis!!
        )
    }

    data class ScoreMetrics(
        val date: LocalDateTime,
        val country: String,
        val pp: Double,
        val adjustedUr: Double,
        val sliderendReleaseStandardDeviationAdjusted: Double,
        val edgeHits: Int,
        val snaps: Int,
        val keypressesMedianAdjusted: Double,
        val errorKurtosis: Double
    )

    val ppWeight = 1.0
    val urWeight = 2.0
    val sliderendReleaseWeight = 2.0
    val edgeHitsWeight = 2.0
    val snapsWeight = 2.0
    val keypressesWeight = 2.0
    val errorKurtosisWeight = 1.0
    val recencyBoost = 10.0
    val spreadBoost = 5.0

    fun areMetricsBlatant(metrics: List<ScoreMetrics>): Boolean {
        return metrics.map { it.adjustedUr }.average() < 24.0
    }

    fun calculateSuspiciousnessScore(scores: List<ScoreMetrics>): Pair<Double, Double> {
        if (scores.isEmpty()) return Pair(0.0, 0.0)

        val currentDate = LocalDateTime.now()
        val mostRecentScoreDate = scores.maxByOrNull { it.date }?.date ?: return Pair(0.0, 0.0)
        val daysSinceMostRecentScore = ChronoUnit.DAYS.between(mostRecentScoreDate, currentDate)
        val recencyScore = if (daysSinceMostRecentScore <= 30) recencyBoost else 0.0

        val dateSpread = scores.map { score -> ChronoUnit.MONTHS.between(score.date, currentDate) }.average()
        val spreadScore = if (dateSpread > 12) spreadBoost else 0.0

        val (suspiciousnessScore, ppTotal) = scores.fold(Pair(0.0, 0.0)) { acc, score ->
            var suspiciousness = acc.first
            var ppSum = acc.second

            suspiciousness += score.pp * ppWeight

            if (score.adjustedUr < 6.0) {
                suspiciousness += (6.0 - score.adjustedUr) * urWeight
            }

            if (score.sliderendReleaseStandardDeviationAdjusted < 6.0) {
                suspiciousness += (6.0 - score.sliderendReleaseStandardDeviationAdjusted) * sliderendReleaseWeight
            }

            suspiciousness += when {
                score.edgeHits > 4 -> (score.edgeHits - 4) * edgeHitsWeight
                else -> 0.0
            }

            suspiciousness += when {
                score.snaps > 4 -> (score.snaps - 4) * snapsWeight
                else -> 0.0
            }

            if (score.keypressesMedianAdjusted < 6) {
                suspiciousness += (6.0 - score.keypressesMedianAdjusted) * keypressesWeight
            }

            if (score.errorKurtosis <= 0.0) {
                suspiciousness += (6.0 - score.errorKurtosis) * errorKurtosisWeight
            }

            ppSum += score.pp
            Pair(suspiciousness, ppSum)
        }

        val averageSuspiciousnessScore = suspiciousnessScore / scores.size
        val urgencyScore = ppTotal / scores.size + recencyScore + spreadScore

        return Pair(averageSuspiciousnessScore, urgencyScore)
    }

}