package theorycrafter.esi

import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.RequestBody
import theorycrafter.Theorycrafter
import theorycrafter.utils.ValueOrError
import java.security.MessageDigest
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.random.Random
import kotlin.time.Duration.Companion.seconds


/**
 * The URL of Eve's OAuth token service.
 */
private const val EVE_OAUTH_TOKEN_URL = "https://login.eveonline.com/v2/oauth/token"


/**
 * The value to set the "Host" header to.
 */
private const val HOST_HEADER_VALUE = "login.eveonline.com"


/**
 * Returns the URL to send the user to in order to start the authorization process.
 */
@OptIn(ExperimentalEncodingApi::class)
fun authStartUrl(codeVerifier: String, vararg scopes: EsiAuthScope): String {
    val codeChallenge = Base64.UrlSafe.encode(
        MessageDigest.getInstance("SHA-256").digest(codeVerifier.encodeToByteArray())
    ).trimEnd('=')
    return "https://login.eveonline.com/v2/oauth/authorize/".toHttpUrl().newBuilder()
        .addEncodedQueryParameter("response_type", "code")
        .addEncodedQueryParameter("redirect_uri", Theorycrafter.EveSsoCallbackUrl)
        .addEncodedQueryParameter("client_id", Theorycrafter.EveSsoClientId)
        .addEncodedQueryParameter("scope", scopes.joinToString(separator = ",") { it.id })
        .addEncodedQueryParameter("code_challenge", codeChallenge)
        .addEncodedQueryParameter("code_challenge_method", "S256")
        .addEncodedQueryParameter("state", "Theorycrafter rules")
        .toString()
}


/**
 * Generates the code challenge for EVE SSO login.
 */
@OptIn(ExperimentalEncodingApi::class)
fun generateCodeVerifier(): String {
    return Base64.UrlSafe.encode(Random.nextBytes(64))
}


/**
 * Obtains the EVE SSO tokens.
 */
fun obtainEveSsoTokens(authCode: String, codeVerifier: String): EveSsoTokensResponse {
    val requestBody: RequestBody = FormBody.Builder()
        .add("grant_type", "authorization_code")
        .add("client_id", Theorycrafter.EveSsoClientId)
        .add("code", authCode)
        .add("code_verifier", codeVerifier)
        .build()

    val request: Request = Request.Builder()
        .url(EVE_OAUTH_TOKEN_URL)
        .header("Content-Type", "application/x-www-form-urlencoded")
        .header("Host", HOST_HEADER_VALUE)
        .post(requestBody)
        .build()

    return sendEveSsoTokensRequestAndParseResponse(request)
}


/**
 * Refreshes the EVE SSO tokens.
 */
fun refreshEveSsoTokens(ssoTokens: EveSsoTokens): EveSsoTokensResponse {
    val requestBody: RequestBody = FormBody.Builder()
        .add("grant_type", "refresh_token")
        .add("refresh_token", ssoTokens.refreshToken)
        .add("client_id", Theorycrafter.EveSsoClientId)
        .build()

    val request: Request = Request.Builder()
        .url(EVE_OAUTH_TOKEN_URL)
        .header("Content-Type", "application/x-www-form-urlencoded")
        .header("Host", HOST_HEADER_VALUE)
        .post(requestBody)
        .build()

    return sendEveSsoTokensRequestAndParseResponse(request)
}


/**
 * The result of an SSO tokens request.
 */
data class EveSsoTokensResponse(
    val result: ValueOrError<EveSsoTokens, String>,
    val responseCode: Int
)


/**
 * Sends the given request, reads, parses and returns the EVE SSO tokens in the response.
 */
private fun sendEveSsoTokensRequestAndParseResponse(request: Request): EveSsoTokensResponse {
    try {
        ESI_HTTP_CLIENT.newCall(request).execute().use { response ->
            val responseBody = response.body
            if ((response.code != 200) || (responseBody == null))
                return EveSsoTokensResponse(
                    result = ValueOrError.failure("Error retrieving tokens" +
                        "\nResponse code: ${response.code}" +
                        "\nResponse body: ${responseBody?.string()}"),
                    responseCode = response.code
                )

            val bodyString = responseBody.string()
            try {
                val json = (Json.parseToJsonElement(bodyString) as? JsonObject)
                    ?: throw SerializationException("Unexpected root JSON element")

                val accessToken = (json["access_token"] as JsonPrimitive).content
                val expiresInSec = (json["expires_in"] as JsonPrimitive).int
                val refreshToken = (json["refresh_token"] as JsonPrimitive).content

                return EveSsoTokensResponse(
                    result = ValueOrError.success(
                        EveSsoTokens(
                            accessToken = accessToken,
                            expirationUtcMillis = System.currentTimeMillis() + expiresInSec.seconds.inWholeMilliseconds,
                            refreshToken = refreshToken,
                            lastRefreshedUtcMillis = System.currentTimeMillis()
                        )
                    ),
                    responseCode = response.code
                )
            } catch (e: Exception) {
                return EveSsoTokensResponse(
                    result = ValueOrError.failure("Invalid response retrieving tokens: ${e.message}"),
                    responseCode = response.code
                )
            }
        }
    } catch (e: Exception) {
        return EveSsoTokensResponse(
            result = ValueOrError.failure("Unable to send SSO tokens request: ${e.message}"),
            responseCode = 0
        )
    }
}


/**
 * Runs the [block] with a fresh [EveSsoTokens] instance. If the given instance is expired, a new one is obtained
 * and passed to [block].
 *
 * [onTokensUpdated] will be called if the SSO tokens have been updated. It will be called with `null`, if the request
 * to refresh the tokens returned an error indicating that the given [tokens] are invalid/expired.
 */
suspend fun <T> withFreshEveSsoTokens(
    tokens: EveSsoTokens,
    onTokensUpdated: suspend (EveSsoTokens?) -> Unit,
    block: suspend (EveSsoTokens) -> ValueOrError<T, String>
): ValueOrError<T, String> {
    val freshTokens =
        if (tokens.expirationUtcMillis > System.currentTimeMillis() + 10_000)  // Give ourselves some extra time
            tokens
        else {
            // Access token is expired; refresh it
            val tokensResponse = refreshEveSsoTokens(tokens)

            if (tokensResponse.responseCode == 400) {
                // 400 indicates our refresh token is bad/expired
                onTokensUpdated(null)
            }

            val result = tokensResponse.result
            if (result.isFailure) {
                // Some other error; maybe there's no internet.
                return ValueOrError.failure(result.failure())
            }

            result.value().also {
                onTokensUpdated(it)
            }
        }

    currentCoroutineContext().ensureActive()
    return block(freshTokens)
}


/**
 * The tokens returned to us by EVE SSO.
 *
 * These can be used to access ESI.
 */
@OptIn(ExperimentalEncodingApi::class)
data class EveSsoTokens(
    val accessToken: String,
    val expirationUtcMillis: Long,  // Expiration of the access token
    val refreshToken: String,
    val lastRefreshedUtcMillis: Long
) {


    /**
     * The payload of the access token.
     */
    private val accessTokenPayloadJson = Json.parseToJsonElement(
        Base64.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL)
            .decode(accessToken.split('.')[1])
            .toString(Charsets.UTF_8)
    ) as JsonObject


    /**
     * The id of the character.
     */
    val characterId = (accessTokenPayloadJson["sub"] as JsonPrimitive).content.split(':')[2].toInt()


    /**
     * The name of the character.
     */
    val characterName = (accessTokenPayloadJson["name"] as JsonPrimitive).content


}

