package com.nisemoe.nise.scheduler

import com.nisemoe.generated.tables.records.ScoresRecord
import com.nisemoe.generated.tables.references.BEATMAPS
import com.nisemoe.generated.tables.references.SCORES
import com.nisemoe.nise.integrations.CircleguardService
import com.nisemoe.nise.osu.OsuApi
import com.nisemoe.nise.service.CompressReplay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.jooq.DSLContext
import org.nisemoe.mari.judgements.CompressJudgements
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Profile
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import java.time.OffsetDateTime

@Profile("fix:scores")
@Service
class FixOldScores(
    private val dslContext: DSLContext
){

    companion object {

        const val CURRENT_VERSION = 8

    }

    @Value("\${OLD_SCORES_WORKERS:4}")
    private var workers: Int = 6

    @Value("\${OLD_SCORES_PAGE_SIZE:5000}")
    private var pageSize: Int = 5000

    private val logger = LoggerFactory.getLogger(javaClass)

    data class Task(val offset: Int, val limit: Int)

    /**
     * This is the minimum cutoff date for scores to be processed.
     * You can set a really old date (e.g. 2023-01-01) here to process all scores.
     */
    private val minCutoff = OffsetDateTime.of(
        2024, 1, 1, 0, 0, 0, 0, OffsetDateTime.now().offset
    )

    @Scheduled(fixedDelay = 120000, initialDelay = 0)
    fun fixOldScores() {
        val condition = SCORES.REPLAY.isNotNull
            .and(SCORES.VERSION.lessThan(CURRENT_VERSION))
            .and(SCORES.ADDED_AT.greaterOrEqual(minCutoff))

        val totalRows = dslContext.fetchCount(SCORES, condition)

        if(totalRows <= 0) {
            this.logger.debug("Fixing old scores but there are none, total rows: $totalRows")
            return
        }

        val tasks = Channel<Task>(Channel.UNLIMITED)
        val readyForWork = Channel<Unit>(Channel.CONFLATED)

        val numTasks = (totalRows + pageSize - 1) / pageSize
        val taskThreshold = workers * 2
        var activeTasks = 0

        runBlocking {
            launch {
                for (i in 0 until numTasks) {
                    while (activeTasks >= taskThreshold) {
                        readyForWork.receive()
                    }
                    val offset = i * pageSize
                    tasks.send(Task(offset, pageSize))
                    activeTasks++
                }
                tasks.close()
            }

            val workerJobs = List(workers) { workerId ->
                launch(Dispatchers.Default) {
                    for (task in tasks) {
                        val scores = dslContext.selectFrom(SCORES)
                            .where(condition)
                            .orderBy(SCORES.ADDED_AT.desc())
                            .limit(task.limit)
                            .offset(task.offset)
                            .fetchInto(ScoresRecord::class.java)

                        scores.forEach { score ->
                            println("Worker $workerId processing score_id: ${score.id} | Active tasks: $activeTasks")
                            try {
                                processScore(score)
                            } catch(exception: Exception) {
                                logger.error(exception.stackTraceToString())
                            }
                        }
                        activeTasks--
                        readyForWork.send(Unit)
                    }
                }
            }

            workerJobs.joinAll()
        }
    }

    fun processScore(score: ScoresRecord) {
        if(score.replay == null) {
            dslContext.update(SCORES)
                .set(SCORES.VERSION, CURRENT_VERSION)
                .where(SCORES.REPLAY_ID.eq(score.replayId))
                .execute()
            return
        }

        val compressReplay = CompressReplay.compressReplay(score.replay!!)

        // sanity check
        val decompressedReplay = CompressReplay.decompressReplay(compressReplay)
        if(!decompressedReplay.contentEquals(score.replay!!)) {
            logger.error("Decompressed replay does not match original replay for score_id: ${score.id}")
            return
        }

        dslContext.update(SCORES)
            .set(SCORES.REPLAY, compressReplay)
            .set(SCORES.VERSION, CURRENT_VERSION)
            .where(SCORES.REPLAY_ID.eq(score.replayId))
            .execute()
    }

}