package net.hivetechnology.o20010_06.api

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.hivetechnology.o20010_06.model.MobileSession
import net.hivetechnology.o20010_06.model.RedeemResult
import net.hivetechnology.o20010_06.model.SessionCredentials
import net.hivetechnology.o20010_06.model.SessionStatus
import net.hivetechnology.o20010_06.model.VpnConfiguration
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.IOException
import java.util.concurrent.TimeUnit

class ApiException(val statusCode: Int, val errorCode: String?, message: String) : IOException(message)

class MobileVpnApi(
    private val baseUrl: String = "https://rest.portal.hivetechnology.net/mobile-vpn",
    private val client: OkHttpClient = OkHttpClient.Builder()
        .connectTimeout(12, TimeUnit.SECONDS)
        .readTimeout(20, TimeUnit.SECONDS)
        .callTimeout(30, TimeUnit.SECONDS)
        .build(),
    private val fallbackDeviceName: String = "Device",
) {
    private val jsonType = "application/json; charset=utf-8".toMediaType()

    suspend fun redeem(ticket: String, appVersion: String, installationId: String, portalReturnUrl: String?): RedeemResult {
        val body = JSONObject()
            .put("ticket", ticket)
            .put("platform", "android")
            .put("appVersion", appVersion)
            .put("installationId", installationId)
        val json = execute("handoffs/redeem", "POST", body)
        val session = MobileSession.fromJson(json.getJSONObject("session"))
        return RedeemResult(
            session,
            SessionCredentials(
                session.id,
                json.getString("sessionToken"),
                json.getString("websocketUrl"),
                session.device?.name ?: fallbackDeviceName,
                json.optString("portalReturnUrl").takeIf { it.isNotBlank() }
                    ?: portalReturnUrl
                    ?: "https://portal.hivetechnology.net/",
            ),
        )
    }

    suspend fun start(credentials: SessionCredentials): MobileSession =
        MobileSession.fromJson(execute("sessions/${credentials.sessionId}/start", "POST", JSONObject(), credentials).getJSONObject("session"))

    suspend fun session(credentials: SessionCredentials): MobileSession =
        MobileSession.fromJson(execute("sessions/${credentials.sessionId}", "GET", credentials = credentials).getJSONObject("session"))

    suspend fun configuration(credentials: SessionCredentials): VpnConfiguration {
        val configuration = VpnConfiguration.fromJson(
            execute("sessions/${credentials.sessionId}/config", "GET", credentials = credentials),
        )
        require(configuration.sessionId == credentials.sessionId) { "Mobile VPN configuration session mismatch" }
        return configuration
    }

    suspend fun update(
        credentials: SessionCredentials,
        status: SessionStatus,
        transport: String? = null,
        quality: String? = null,
        latencyMs: Long? = null,
        errorCode: String? = null,
        errorMessage: String? = null,
    ): MobileSession {
        val body = JSONObject().put("status", status.wireValue)
        body.put("transport", transport ?: JSONObject.NULL)
        body.put("quality", quality ?: JSONObject.NULL)
        body.put("latencyMs", latencyMs ?: JSONObject.NULL)
        body.put("errorCode", errorCode ?: JSONObject.NULL)
        body.put("errorMessage", errorMessage ?: JSONObject.NULL)
        return MobileSession.fromJson(
            execute("sessions/${credentials.sessionId}/status", "PUT", body, credentials).getJSONObject("session"),
        )
    }

    suspend fun stop(credentials: SessionCredentials) {
        execute("sessions/${credentials.sessionId}", "DELETE", credentials = credentials, allowEmpty = true)
    }

    private suspend fun execute(
        path: String,
        method: String,
        body: JSONObject? = null,
        credentials: SessionCredentials? = null,
        allowEmpty: Boolean = false,
    ): JSONObject = withContext(Dispatchers.IO) {
        val builder = Request.Builder()
            .url("${baseUrl.trimEnd('/')}/${path.trimStart('/')}")
            .header("Accept", "application/json")
            .header("Cache-Control", "no-store")
        if (credentials != null) builder.header("Authorization", "Bearer ${credentials.token}")
        val requestBody = body?.toString()?.toRequestBody(jsonType)
        when (method) {
            "GET" -> builder.get()
            "POST" -> builder.post(requestBody ?: ByteArray(0).toRequestBody(jsonType))
            "PUT" -> builder.put(requestBody ?: ByteArray(0).toRequestBody(jsonType))
            "DELETE" -> builder.delete(requestBody)
            else -> error("Unsupported HTTP method")
        }
        client.newCall(builder.build()).execute().use { response ->
            val text = response.body?.string().orEmpty()
            if (!response.isSuccessful) {
                val error = runCatching { JSONObject(text) }.getOrNull()
                throw ApiException(
                    response.code,
                    error?.optString("code")?.takeIf { it.isNotBlank() },
                    error?.optString("message")?.takeIf { it.isNotBlank() } ?: "HTTP ${response.code}",
                )
            }
            if (text.isBlank() && allowEmpty) JSONObject() else JSONObject(text)
        }
    }
}
