package com.nisemoe.nise.osu

import com.nisemoe.nise.AllowCacheSerialization
import com.nisemoe.nise.service.CacheService
import java.io.File
import java.net.URI
import java.net.URLEncoder
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets
import java.time.Instant
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import java.time.Duration

@Service
class TokenService(
    private val cacheService: CacheService
) : InitializingBean {

    private val logger = LoggerFactory.getLogger(javaClass)

    private val httpClient: HttpClient = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_2)
        .build()

    @Serializable
    @AllowCacheSerialization
    data class AccessTokenResponse(
        val token_type: String,
        val expires_in: Int,
        val access_token: String
    )

    @Serializable
    @AllowCacheSerialization
    data class TokenWithExpiry(
        val accessTokenResponse: AccessTokenResponse,
        val expiryTime: Long
    )

    @Value("\${OSU_CLIENT_SECRET}")
    private lateinit var clientSecret: String

    @Value("\${OSU_CLIENT_ID}")
    private lateinit var clientId: String

    private var currentToken: TokenWithExpiry? = null

    override fun afterPropertiesSet() {
        val tokenFromCache = this.cacheService.getVariable("osuToken", TokenWithExpiry::class.java)
        if(tokenFromCache != null) {
            currentToken = tokenFromCache
            this.logger.debug("Loaded osu! token from cache: {}", currentToken)
        }
    }

    fun getAccessToken(): AccessTokenResponse {
        if(currentToken == null)
            return getNewTokenAndSave()

        val now = Instant.now().epochSecond
        val remainingTime = currentToken!!.expiryTime.minus(now)

        // Refresh the token if it is null, expired, or has less than 5 minutes left
        return if (remainingTime <= 300) {
            getNewTokenAndSave()
        } else {
            currentToken!!.accessTokenResponse
        }
    }

    fun getNewTokenAndSave(): AccessTokenResponse {
        val newToken = getNewAccessToken()
        val expiryTime = calculateExpiryTime(newToken)
        val tokenWithExpiry = TokenWithExpiry(newToken, expiryTime)
        currentToken = tokenWithExpiry
        this.cacheService.setVariable(
            key = "osuToken",
            value = tokenWithExpiry,
            ttl = Duration.ofSeconds(expiryTime - Instant.now().epochSecond)
        )
        return newToken
    }

    private fun calculateExpiryTime(tokenResponse: AccessTokenResponse): Long =
        Instant.now().epochSecond + tokenResponse.expires_in - 120

    private fun getNewAccessToken(): AccessTokenResponse {
        val parameters = mapOf(
            "client_id" to clientId,
            "client_secret" to clientSecret,
            "grant_type" to "client_credentials",
            "scope" to "public"
        )

        val form = parameters.map { (k, v) ->
            "${URLEncoder.encode(k, StandardCharsets.UTF_8)}=${URLEncoder.encode(v, StandardCharsets.UTF_8)}"
        }.joinToString("&")

        val request = HttpRequest.newBuilder()
            .uri(URI.create("https://osu.ppy.sh/oauth/token"))
            .header("Accept", "application/json")
            .header("Content-Type", "application/x-www-form-urlencoded")
            .POST(HttpRequest.BodyPublishers.ofString(form))
            .build()

        val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())

        if (response.statusCode() == 200) {
            return Json.decodeFromString(AccessTokenResponse.serializer(), response.body())
        } else {
            throw RuntimeException("Failed to obtain access token: ${response.body()}")
        }
    }

}