package com.nisemoe.nise.service

import com.nisemoe.generated.tables.records.UpdateUserQueueRecord
import com.nisemoe.generated.tables.references.UPDATE_USER_QUEUE
import com.nisemoe.nise.UserQueueDetails
import org.jooq.DSLContext
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.stereotype.Service
import java.time.Duration
import java.time.OffsetDateTime
import java.time.ZoneOffset

@Service
class UpdateUserQueueService(
    private val dslContext: DSLContext,
    private val messagingTemplate: SimpMessagingTemplate
) {

    companion object {

        const val USER_UPDATE_INTERVAL_HOURS = 4

    }

    data class UserQueueWebsocketPacket(
        val message: String,
        val data: UserQueueDetails? = null
    )

    /**
     * Retrieves the user queue details for the given user ID.
     *
     * @param userId The osu!id of the user.
     */
    fun getUserQueueDetails(userId: Long): UserQueueDetails {
        val isProcessing = dslContext.fetchExists(
            UPDATE_USER_QUEUE,
            UPDATE_USER_QUEUE.USER_ID.eq(userId).and(UPDATE_USER_QUEUE.PROCESSED.isFalse)
        )

        val lastCompletedUpdateQueue = 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)

        var canUpdate = !isProcessing
        if (lastCompletedUpdateQueue != null) {
            val now = OffsetDateTime.now(ZoneOffset.UTC)
            val duration = Duration.between(lastCompletedUpdateQueue, now)
            val hoursSinceLastUpdate = duration.toHours()

            if (hoursSinceLastUpdate < USER_UPDATE_INTERVAL_HOURS)
                canUpdate = false
        }

        val currentProgress = dslContext.select(
            UPDATE_USER_QUEUE.PROGRESS_CURRENT,
            UPDATE_USER_QUEUE.PROGRESS_TOTAL
        )
            .from(UPDATE_USER_QUEUE)
            .where(UPDATE_USER_QUEUE.USER_ID.eq(userId))
            .and(UPDATE_USER_QUEUE.PROCESSED.isFalse)
            .orderBy(UPDATE_USER_QUEUE.PROCESSED_AT.desc())
            .limit(1)
            .fetchOneInto(UpdateUserQueueRecord::class.java)

        return UserQueueDetails(
            isProcessing,
            lastCompletedUpdateQueue,
            canUpdate,
            currentProgress?.progressCurrent,
            currentProgress?.progressTotal
        )
    }

    /**
     * Retrieves the full update queue, only pending users.
     */
    fun getQueue(): List<Long> {
        return dslContext.select(UPDATE_USER_QUEUE.USER_ID)
            .from(UPDATE_USER_QUEUE)
            .where(UPDATE_USER_QUEUE.PROCESSED.isFalse)
            .orderBy(UPDATE_USER_QUEUE.CREATED_AT.asc())
            .fetchInto(Long::class.java)
    }

    /**
     * Inserts a user into the update queue if they are not already in the queue.
     * @return true if the user was inserted, false if the user was already in the queue
     */
    fun insertUser(userId: Long, addedByUserId: Long? = null): Boolean {
        val exists = dslContext.fetchExists(UPDATE_USER_QUEUE,
            UPDATE_USER_QUEUE.USER_ID.eq(userId),
            UPDATE_USER_QUEUE.PROCESSED.isFalse
        )
        if (exists)
            return false

        val insertedRows = dslContext.insertInto(UPDATE_USER_QUEUE)
            .set(UPDATE_USER_QUEUE.USER_ID, userId)
            .set(UPDATE_USER_QUEUE.ADDED_BY_USER_ID, addedByUserId)
            .execute()

        return insertedRows == 1
    }

    /**
     * Sets the given user as processed in the update queue.
     *
     * @param userId The user ID of the user to set as processed.
     */
    fun setUserAsProcessed(userId: Long, failed: Boolean = false) {
        dslContext.update(UPDATE_USER_QUEUE)
            .set(UPDATE_USER_QUEUE.PROCESSED, true)
            .set(UPDATE_USER_QUEUE.PROCESSED_AT, OffsetDateTime.now(ZoneOffset.UTC))
            .set(UPDATE_USER_QUEUE.HAS_FAILED, failed)
            .where(UPDATE_USER_QUEUE.USER_ID.eq(userId))
            .and(UPDATE_USER_QUEUE.PROCESSED.isFalse)
            .execute()

        // Notify the user that their queue has been processed with fresh info
        messagingTemplate.convertAndSend(
            "/topic/live-user/${userId}",
            UserQueueWebsocketPacket(message = "UPDATE_PROGRESS", data = this.getUserQueueDetails(userId))
        )
    }

}