package aquadx.net.games.mai2

import ext.*
import aquadx.net.db.AquaUserServices
import aquadx.net.games.*
import aquadx.net.utils.SUCCESS
import aquadx.net.utils.mai2Scores
import aquadx.sega.maimai2.handler.UploadUserPhotoHandler
import aquadx.sega.maimai2.model.Mai2Repos
import aquadx.sega.maimai2.model.Mai2UserDataRepo
import aquadx.sega.maimai2.model.Mai2UserMusicDetailRepo
import aquadx.sega.maimai2.model.Mai2UserPlaylogRepo
import aquadx.sega.maimai2.model.userdata.Mai2UserDetail
import aquadx.sega.maimai2.model.userdata.Mai2UserGeneralData
import aquadx.sega.maimai2.model.userdata.Mai2UserLoginBonus
import aquadx.sega.maimai2.model.userdata.Mai2UserOption
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController
import kotlin.jvm.optionals.getOrNull
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.memberProperties

@RestController
@API("api/v2/game/mai2")
class Maimai2(
    override val us: AquaUserServices,
    override val playlogRepo: Mai2UserPlaylogRepo,
    override val userDataRepo: Mai2UserDataRepo,
    override val userMusicRepo: Mai2UserMusicDetailRepo,
    val repos: Mai2Repos,
) : GameApiController<Mai2UserDetail>("mai2", Mai2UserDetail::class) {
    override suspend fun trend(@RP username: Str): List<TrendOut> = us.cardByName(username) { card ->
        findTrend(playlogRepo.findByUserCardExtId(card.extId)
            .map { TrendLog(it.playDate, it.afterRating) })
    }

    // Only show > S rank
    override val shownRanks = mai2Scores.filter { it.first >= 97 * 10000 }
    override val settableFields: Map<String, (Mai2UserDetail, String) -> Unit> by lazy { mapOf(
        "userName" to usernameCheck(SEGA_USERNAME_CHARS),
        "iconId" to { u, v -> u.iconId = v.int() },
        "plateId" to { u, v -> u.plateId = v.int() },
        "titleId" to { u, v -> u.titleId = v.int() },
        "frameId" to { u, v -> u.frameId = v.int() },
        "partnerId" to { u, v -> u.partnerId = v.int() },
        "charaSlot" to { u, v -> u.charaSlot = v.split(',').map { it.int() } },
        "charaLockSlot" to { u, v -> u.charaLockSlot = v.split(',').map { it.int() } },

        "lastRomVersion" to { u, v -> u.lastRomVersion = v },
        "lastDataVersion" to { u, v -> u.lastDataVersion = v },
    ) }
    override val gettableFields: Set<String> = setOf("lastGameId", "lastRomVersion", "classRank", "playerRating", "courseRank")

    override suspend fun userSummary(@RP username: Str, @RP token: String?) = us.cardByName(username) { card ->
        val extra = repos.userGeneralData.findByUser_Card_ExtId(card.extId)
            .associate { it.propertyKey to it.propertyValue }

        val ratingComposition = mapOf(
            "best35" to (extra["recent_rating"] ?: ""),
            "best15" to (extra["recent_rating_new"] ?: "")
        )

        // if isLogin than boolean or null
        // use the type to check user login in frontend
        val isMyRival = token?.let { t ->
            us.jwt.auth(t) { u ->
                if (u.username == username) return@auth null
                us.cardByName(u.username) { myCard ->
                    val user = repos.userData.findByCardExtId(card.extId) ?: (404 - "User not found")
                    val myRival = (repos.userGeneralData.findByUser_Card_ExtIdAndPropertyKey(myCard.extId, "favorite_rival")?.propertyValue?.split(',') ?: emptyList())
                        .filter { it.isNotEmpty() }.map { it.long() }
                    myRival.contains(user.id)
                }
            }
        }

        genericUserSummary(card, ratingComposition, isMyRival, extra["favorite_music"]?.split(",")?.mapNotNull{it -> it.toIntOrNull()})
    }

    @API("user-rating")
    suspend fun userRating(@RP username: Str) = us.cardByName(username) { card ->
        val extra = repos.userGeneralData.findByUser_Card_ExtId(card.extId)
            .associate { it.propertyKey to it.propertyValue }
        val b35Str = extra["recent_rating"] ?: (400 - "No rating found")
        val b15Str = extra["recent_rating_new"] ?: (400 - "No rating found")

        val b35 = b35Str.split(',').filterNot { it.isBlank() }.map { it.split(':') }
        val b15 = b15Str.split(',').filterNot { it.isBlank() }.map { it.split(':') }

        val musicIdList = listOf(
            b35.map { it[0].toInt() },
            b15.map { it[0].toInt() },
        ).flatten()

        val userMusicList = repos.userMusicDetail.findByUser_Card_ExtIdAndMusicIdIn(card.extId, musicIdList)

        // Dont leak extId
        mapOf(
            "best35" to b35,
            "best15" to b15,
            "musicList" to userMusicList,
        )
    }

    @API("user-name-plate")
    // legacy
    suspend fun userNamePlate(@RP username: Str) = this.userDetail(username)

    @API("user-favorite")
    suspend fun userFavorite(@RP username: Str) = us.cardByName(username) { card ->
        repos.userFavorite.findByUser_Card_ExtId(card.extId)
    }

    @PostMapping("change-name")
    suspend fun changeName(@RP token: String, @RP newName: String) = us.jwt.auth(token) { u ->
        val newNameFull = toFullWidth(newName)
        us.cardByName(u.username) { card ->
            val user = userDataRepo.findByCard(card) ?: (404 - "User not found")
            user.userName = newNameFull
            userDataRepo.save(user)
            cardService.updateCardTimestamp(card, "mai2")
        }
        mapOf("newName" to newNameFull)
    }

    @API("get-login-bonus")
    suspend fun getLoginBonus(@RP token: String) = us.jwt.auth(token) { u ->
        us.cardByName(u.username) { card ->
            repos.userLoginBonus.findByUser_Card_ExtId(card.extId)
        }
    }

    @PostMapping("set-current-login-bonus")
    suspend fun setCurrentLoginBonus(@RP token: String, @RP bonusId: Int) = us.jwt.auth(token) { u ->
        us.cardByName(u.username) { card ->
            val loginBonus = repos.userLoginBonus.findByUser_Card_ExtId(card.extId).mut
            for (bonus in loginBonus) {
                bonus.isCurrent = bonus.bonusId == bonusId
            }
            // if no bonus.bonusId == bonusId in loginBonus
            if (loginBonus.none { it.bonusId == bonusId }) {
                // create one
                val newBonus = Mai2UserLoginBonus().apply {
                    user = repos.userData.findByCardExtId(card.extId) ?: (404 - "User not found")
                    this.bonusId = bonusId
                    isCurrent = true
                }
                loginBonus.add(newBonus)
            }
            repos.userLoginBonus.saveAll(loginBonus)
            cardService.updateCardTimestamp(card, "mai2")
        }
        SUCCESS
    }

    @API("user-option")
    override suspend fun userOption(@RP token: String) = us.jwt.auth(token) { u ->
        repos.userOption.findByUser_Card_ExtId(u.ghostCard.extId).getOrNull(0)
    }
    @API("user-option-set")
    override suspend fun userOptionSet(@RP token: String, @RP field: String, @RP value: Int): Any = us.jwt.auth(token) { u ->
        val gameOptions = repos.userOption.findSingleByUser_Card_ExtId(u.ghostCard.extId)
        val property = Mai2UserOption::class.memberProperties.filterIsInstance<KMutableProperty1<Any, Any?>>().find{ it.name == field }

        if (property != null && gameOptions != null) {
            property.setter.call(gameOptions, value)
            repos.userOption.save(gameOptions)
            200 - "Success"
        } else
            400 - "Invalid parameters"

    }

    @API("owned-items")
    suspend fun ownedItems(@RP token: String) = us.jwt.auth(token) { u ->
        us.cardByName(u.username) { card ->
            repos.userItem.findByUser_Card_ExtId(card.extId)
        }
    }

    @PostMapping("set-rival")
    suspend fun setRival(@RP token: String, @RP rivalUserName: String, @RP isAdd: Boolean) = us.jwt.auth(token) { u ->
        us.cardByName(u.username) { myCard ->
            val rivalCard = us.cardByName(rivalUserName) { it }
            val rivalUser = repos.userData.findByCardExtId(rivalCard.extId) ?: (404 - "User not found")
            val myRival = repos.userGeneralData.findByUser_Card_ExtIdAndPropertyKey(myCard.extId, "favorite_rival")
                ?: Mai2UserGeneralData().apply {
                    user = repos.userData.findByCardExtId(myCard.extId) ?: (404 - "User not found")
                    propertyKey = "favorite_rival"
                }
            val myRivalList = myRival.propertyValue.split(',').filter { it.isNotEmpty() }.mut

            if (isAdd && myRivalList.size >= 4) {
                (400 - "Rival list is full")
            } else if (isAdd) {
                myRivalList.add(rivalUser.id.toString())
            } else {
                myRivalList.remove(rivalUser.id.toString())
            }

            myRival.propertyValue = myRivalList.joinToString(",")
            repos.userGeneralData.save(myRival)
            cardService.updateCardTimestamp(myCard, "mai2")
        }
        SUCCESS
    }

    val photoDir = UploadUserPhotoHandler.uploadDir.toFile().canonicalFile

    @API("my-photo")
    suspend fun myPhoto(@RP token: Str) = us.jwt.auth(token) { u ->
        val find = "${u.ghostCard.extId}-"
        photoDir.listFiles()
            ?.map { it.name }
            ?.filter { it.startsWith(find) }
            ?.sorted()
            ?: emptyList()
    }

    @API("my-photo/{fileName}", produces = [MediaType.IMAGE_JPEG_VALUE])
    suspend fun myPhoto(@RP token: Str, @PV fileName: Str) = us.jwt.auth(token) { u ->
        val f = (photoDir / fileName)
        if (!f.canonicalFile.startsWith(photoDir)) (403 - "Never gonna give you up")
        if (!f.name.startsWith("${u.ghostCard.extId}-")) (403 - "Not your photo")
        if (!f.exists()) (404 - "Photo not found")
        f.readBytes()
    }
}
