package aquadx.sega.maimai2

import aquadx.net.utils.simpleDescribe
import aquadx.sega.allnet.TokenChecker
import aquadx.sega.general.BaseHandler
import aquadx.sega.general.MeowApi
import aquadx.sega.general.RequestContext
import aquadx.sega.general.SpecialHandler
import aquadx.sega.general.toSpecial
import aquadx.spring.Metrics
import ext.*
import aquadx.net.games.mai2.Maimai2
import aquadx.net.utils.ApiException
import aquadx.sega.general.*
import aquadx.sega.maimai2.handler.*
import aquadx.sega.maimai2.model.Mai2Repos
import jakarta.servlet.http.HttpServletRequest
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.time.format.DateTimeFormatter
import kotlin.reflect.full.declaredMemberProperties

/**
 * @author samnyan (privateamusement@protonmail.com)
 */
@Suppress("unused")
@RestController
@RequestMapping(path = ["/g/mai2/Maimai2Servlet/", "/g/mai2/"])
class Maimai2ServletController(
    val upsertUserAll: UpsertUserAllHandler,
    val getUserItem: GetUserItemHandler,
    val getUserRating: GetUserRatingHandler,
    val uploadUserPhoto: UploadUserPhotoHandler,
    val uploadUserPlaylog: UploadUserPlaylogHandler,
    val getUserPortrait: GetUserPortraitHandler,
    val uploadUserPortrait: UploadUserPortraitHandler,
    val upsertUserPrint: UpsertUserPrintHandler,
    val getUserFavoriteItem: GetUserFavoriteItemHandler,
    val getUserCharacter: GetUserCharacterHandler,
    val getGameRanking: GetGameRankingHandler,
    val db: Mai2Repos,
    val net: Maimai2,
): MeowApi(serialize = { _, resp -> if (resp is String) resp else resp.toJson() }) {
    companion object {
        private val log = logger()
        private val empty = listOf<Any>()
        private val GAME_SETTING_DATE_FMT = DateTimeFormatter.ofPattern("2010-01-01 HH:mm:00.0")
        private val GAME_SETTING_TIME_FMT = DateTimeFormatter.ofPattern("HH:mm:00")
    }

    init { initApis() }

    val endpointList = setOf("GetGameRankingApi","GetUserCharacterApi","GetUserItemApi","GetUserPortraitApi",
        "GetUserRatingApi","UploadUserPhotoApi","UploadUserPlaylogApi","UploadUserPortraitApi","UpsertUserAllApi",
        "CMGetUserCardApi","CMGetUserCardPrintErrorApi","CMGetUserDataApi","CMGetUserItemApi","CMUpsertUserPrintApi",
        "GetUserFavoriteItemApi")

    val noopEndpoint = setOf("GetUserScoreRankingApi", "UpsertClientBookkeepingApi",
        "UpsertClientSettingApi", "UpsertClientTestmodeApi", "UpsertClientUploadApi", "Ping", "RemoveTokenApi",
        "CMLoginApi", "CMLogoutApi", "CMUpsertBuyCardApi", "UserLogoutApi", "GetGameMapAreaConditionApi",
        "UpsertUserChargelogApi","UpsertClientPlayTimeApi")

    val members = this::class.declaredMemberProperties
    val handlers: Map<String, SpecialHandler> = initH + endpointList.associateWith { api ->
        val name = api.replace("Api", "").lowercase()
        (members.find { it.name.lowercase() == name } ?: members.find { it.name.lowercase() == name.replace("cm", "") })
            ?.let { (it.call(this) as BaseHandler).toSpecial() }
            ?: initH[api] ?: initH[api.replace("CM", "")]
            ?: throw IllegalArgumentException("Mai2: No handler found for $api")
    }

    @API("/{api}")
    fun handle(@PathVariable api: String, @RequestBody data: Map<String, Any>, req: HttpServletRequest): Any {
        val token = TokenChecker.tokenShort()
        log.info("$token : $api < ${data.toJson()}")

        val noop = """{"returnCode":1,"apiName":"com.sega.maimai2servlet.api.$api"}"""
        if (api !in noopEndpoint && !handlers.containsKey(api)) {
            log.warn("$token : $api > not found")
            return noop
        }

        // Only record the counter metrics if the API is known.
        Metrics.counter("aquadx_maimai2_api_call", "api" to api).increment()

        if (api in noopEndpoint) {
            log.info("$token : $api > no-op")
            return noop
        }

        return try {
            Metrics.timer("aquadx_maimai2_api_latency", "api" to api).recordCallable {
                val ctx = RequestContext(req, data.mut)
                serialize(api, handlers[api]!!(ctx) ?: noop).also {
                    log.info("$token : $api > ${it.truncate(500)}")
                }
            }!!
        } catch (e: Exception) {
            Metrics.counter(
                "aquadx_maimai2_api_error",
                "api" to api, "error" to e.simpleDescribe()
            ).increment()

            if (e is ApiException) {
                // It's a bad practice to return 200 ok on error, but this is what maimai does so we have to follow
                return mapOf("returnCode" to 0, "apiName" to "com.sega.maimai2servlet.api.$api", "message" to "${e.message} - ${e.code}").toJson()
            } else throw e
        }
    }
}
