package com.nisemoe.nise.database

import com.nisemoe.generated.tables.records.UpdateUserQueueRecord
import com.nisemoe.generated.tables.records.UsersRecord
import com.nisemoe.generated.tables.references.SCORES
import com.nisemoe.generated.tables.references.UPDATE_USER_QUEUE
import com.nisemoe.generated.tables.references.USERS
import com.nisemoe.nise.Format
import com.nisemoe.nise.UserDetails
import com.nisemoe.nise.UserQueueDetails
import com.nisemoe.nise.osu.OsuApi
import com.nisemoe.nise.osu.OsuApiModels
import org.jooq.DSLContext
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset

@Service
class UserService(
    private val dslContext: DSLContext,
    private val osuApi: OsuApi
) {

    private val logger = LoggerFactory.getLogger(javaClass)

    fun mapUserToDatabase(dto: OsuApiModels.UserExtended): UserDetails {

        return UserDetails(
            user_id = dto.id,
            username = dto.username,
            rank = dto.statistics?.global_rank,
            pp_raw = dto.statistics?.pp,
            join_date = if (dto.join_date != null) Format.formatLocalDateTime(OffsetDateTime.parse(dto.join_date).toLocalDateTime()) else null,
            seconds_played = dto.statistics?.play_time,
            country = dto.country?.code,
            country_rank = dto.statistics?.country_rank,
            playcount = dto.statistics?.play_count
        )

    }

    fun getTotalUserScores(userId: Long): Int {
        return dslContext.fetchCount(SCORES, SCORES.USER_ID.eq(userId))
    }

    fun getUserById(userId: Long): UserDetails? {
        val user = dslContext.selectFrom(USERS)
            .where(USERS.USER_ID.eq(userId))
            .fetchOneInto(UsersRecord::class.java)

        if (user != null) {
            return UserDetails(
                user.userId!!,
                user.username!!,
                user.rank,
                user.ppRaw,
                user.joinDate?.let { Format.formatLocalDateTime(it) },
                user.secondsPlayed,
                user.country,
                user.countryRank,
                user.playcount
            )
        }

        // The database does NOT have the user; we will now use the osu!api
        val apiUser = this.osuApi.getUserProfile(userId = userId.toString(), mode = "osu", key = "id")
            ?: return null

        // Persist to database
        insertApiUser(apiUser)

        return this.mapUserToDatabase(apiUser)
    }

    fun getUserDetails(username: String): UserDetails? {
        val user = dslContext.selectFrom(USERS)
            .where(USERS.USERNAME.equalIgnoreCase(username.lowercase()))
            .fetchOneInto(UsersRecord::class.java)

        if (user != null) {
            return UserDetails(
                user.userId!!,
                user.username!!,
                user.rank,
                user.ppRaw,
                user.joinDate?.let { Format.formatLocalDateTime(it) },
                user.secondsPlayed,
                user.country,
                user.countryRank,
                user.playcount
            )
        }

        // The database does NOT have the user; we will now use the osu!api
        val apiUser = this.osuApi.getUserProfile(userId = username, mode = "osu", key = "username")
            ?: return null

        // Persist to database
        insertApiUser(apiUser)

        return this.mapUserToDatabase(apiUser)
    }

    fun insertApiUser(apiUser: OsuApiModels.UserExtended) {
        this.logger.debug("Saving user ${apiUser.username}")

        val userStatistics = apiUser.getStatisticsEither()

        if(dslContext.fetchExists(USERS, USERS.USER_ID.eq(apiUser.id))) {

            dslContext.update(USERS)
                .set(USERS.USERNAME, apiUser.username)
                .set(USERS.RANK, userStatistics?.global_rank)
                .set(USERS.PP_RAW, userStatistics?.pp)
                .set(USERS.ACCURACY, userStatistics?.hit_accuracy)
                .set(USERS.TOTAL_SCORE, userStatistics?.total_score)
                .set(USERS.RANKED_SCORE, userStatistics?.ranked_score)
                .set(USERS.COUNT_300, userStatistics?.count_300)
                .set(USERS.COUNT_100, userStatistics?.count_100)
                .set(USERS.COUNT_50, userStatistics?.count_50)
                .set(USERS.COUNT_MISS, userStatistics?.count_miss)
                .apply {
                    if(apiUser.join_date != null) {
                        set(USERS.JOIN_DATE, OffsetDateTime.parse(apiUser.join_date).toLocalDateTime())
                    }
                }
                .set(USERS.SECONDS_PLAYED, userStatistics?.play_time)
                .set(USERS.COUNTRY, apiUser.country?.code)
                .set(USERS.COUNTRY_RANK, userStatistics?.country_rank)
                .set(USERS.PLAYCOUNT, userStatistics?.play_count)
                .set(USERS.SYS_LAST_UPDATE, OffsetDateTime.now(ZoneOffset.UTC))
                .where(USERS.USER_ID.eq(apiUser.id))
                .execute()

            return
        }

        val affectedRows = dslContext.insertInto(USERS)
            .set(USERS.USER_ID, apiUser.id)
            .set(USERS.USERNAME, apiUser.username)
            .set(USERS.RANK, userStatistics?.global_rank)
            .set(USERS.PP_RAW, userStatistics?.pp)
            .set(USERS.ACCURACY, userStatistics?.hit_accuracy)
            .set(USERS.TOTAL_SCORE, userStatistics?.total_score)
            .set(USERS.RANKED_SCORE, userStatistics?.ranked_score)
            .set(USERS.COUNT_300, userStatistics?.count_300)
            .set(USERS.COUNT_100, userStatistics?.count_100)
            .set(USERS.COUNT_50, userStatistics?.count_50)
            .apply {
                if(apiUser.join_date != null) {
                    set(USERS.JOIN_DATE, OffsetDateTime.parse(apiUser.join_date).toLocalDateTime())
                }
            }
            .set(USERS.SECONDS_PLAYED, userStatistics?.play_time)
            .set(USERS.COUNTRY, apiUser.country?.code)
            .set(USERS.COUNTRY_RANK, userStatistics?.country_rank)
            .set(USERS.PLAYCOUNT, userStatistics?.play_count)
            .set(USERS.SYS_LAST_UPDATE, OffsetDateTime.now(ZoneOffset.UTC))
            .onDuplicateKeyIgnore()
            .execute()

        if (affectedRows == 0) {
            this.logger.error("Tried to insert ${apiUser.username} but failed.")
            this.logger.error(apiUser.toString())
        }
    }

}