package com.hermes.app.core.network

import com.hermes.app.domain.HermesAnalyticsBatchRequest
import com.hermes.app.domain.HermesBetIntentRequest
import com.hermes.app.domain.HermesBetIntentResponse
import com.hermes.app.domain.HermesHealthResponse
import com.hermes.app.domain.HermesEvent
import com.hermes.app.domain.HermesEventManifest
import com.hermes.app.domain.HermesExperimentConfig
import com.hermes.app.domain.HermesLocalizationBundle
import com.hermes.app.domain.HermesMarket
import com.hermes.app.domain.HermesOddsVersion
import com.hermes.app.domain.HermesSessionResponse
import com.hermes.app.domain.HermesSessionStartRequest
import com.hermes.app.domain.HermesSettlement
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody

@OptIn(kotlinx.serialization.ExperimentalSerializationApi::class)
class HermesApiClient(
    private val baseUrl: HttpUrl,
    private val client: OkHttpClient = OkHttpClient(),
    private val json: Json = Json {
        ignoreUnknownKeys = true
        encodeDefaults = false
        explicitNulls = false
        namingStrategy = JsonNamingStrategy.SnakeCase
    },
    ) {
    private val jsonMediaType = "application/json; charset=utf-8".toMediaType()

    suspend fun startSession(request: HermesSessionStartRequest): HermesSessionResponse = post("api/v1/session/start", request)

    suspend fun health(): HermesHealthResponse = get("health")

    suspend fun endSession(): HermesSessionResponse = post("api/v1/session/end")

    suspend fun currentSession(): HermesSessionResponse = get("api/v1/session/me")

    suspend fun nextEvent(): HermesEvent = get("api/v1/feed/next")

    suspend fun eventManifest(eventId: String): HermesEventManifest = get("api/v1/events/$eventId/manifest")

    suspend fun markets(eventId: String): List<HermesMarket> = get("api/v1/events/$eventId/markets")

    suspend fun currentOdds(marketId: String): HermesOddsVersion = get("api/v1/markets/$marketId/odds/current")

    suspend fun submitBetIntent(request: HermesBetIntentRequest): HermesBetIntentResponse = post("api/v1/bets/intent", request)

    suspend fun betIntent(id: String): HermesBetIntentResponse = get("api/v1/bets/$id")

    suspend fun settlement(eventId: String): HermesSettlement = get("api/v1/events/$eventId/result")

    suspend fun experimentConfig(): HermesExperimentConfig = get("api/v1/experiments/config")

    suspend fun localization(localeCode: String): HermesLocalizationBundle = get("api/v1/localization/$localeCode")

    suspend fun submitAnalyticsBatch(request: HermesAnalyticsBatchRequest) {
        postNoContent("api/v1/analytics/batch", request)
    }

    private suspend inline fun <reified T> get(path: String): T = withContext(Dispatchers.IO) {
        execute(path = path, method = "GET")
    }

    private suspend inline fun <reified T> post(path: String): T = withContext(Dispatchers.IO) {
        execute(path = path, method = "POST")
    }

    private suspend inline fun <reified T, reified B> post(path: String, body: B): T = withContext(Dispatchers.IO) {
        execute(path = path, method = "POST", bodyJson = json.encodeToString(body))
    }

    private suspend inline fun <reified B> postNoContent(path: String, body: B) {
        withContext(Dispatchers.IO) {
            executeNoContent(path = path, method = "POST", bodyJson = json.encodeToString(body))
        }
    }

    private inline fun <reified T> execute(path: String, method: String, bodyJson: String? = null): T {
        val requestBody = when {
            bodyJson != null -> bodyJson.toRequestBody(jsonMediaType)
            method == "GET" -> null
            else -> "{}".toRequestBody(jsonMediaType)
        }

        val requestBuilder = Request.Builder()
            .url(baseUrl.newBuilder().addEncodedPathSegments(path.trimStart('/')).build())
            .method(method, requestBody)
            .header("Accept", "application/json")

        client.newCall(requestBuilder.build()).execute().use { response ->
            if (!response.isSuccessful) {
                throw HermesApiException.HttpStatus(response.code, response.body?.string().orEmpty())
            }

            val payload = response.body?.string().orEmpty()
            return json.decodeFromString(payload)
        }
    }

    private fun executeNoContent(path: String, method: String, bodyJson: String? = null) {
        val requestBody = when {
            bodyJson != null -> bodyJson.toRequestBody(jsonMediaType)
            method == "GET" -> null
            else -> "{}".toRequestBody(jsonMediaType)
        }

        val requestBuilder = Request.Builder()
            .url(baseUrl.newBuilder().addEncodedPathSegments(path.trimStart('/')).build())
            .method(method, requestBody)
            .header("Accept", "application/json")

        client.newCall(requestBuilder.build()).execute().use { response ->
            if (!response.isSuccessful) {
                throw HermesApiException.HttpStatus(response.code, response.body?.string().orEmpty())
            }
        }
    }
}

sealed class HermesApiException(message: String) : RuntimeException(message) {
    class HttpStatus(val code: Int, val body: String) : HermesApiException("HTTP $code: $body")
}
