package com.jaytux.simd.frontend.client

import com.jaytux.simd.frontend.*
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.delay
import kotlinx.datetime.LocalDate
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

@OptIn(ExperimentalUuidApi::class)
object Client {
    val httpClient: HttpClient by lazy { getKtorClient{ install(ContentNegotiation) { json() } } }
    val baseUrl = "https://simd.jaytux.com/api"

    object UUIDSerializer : KSerializer<Uuid> {
        override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)

        override fun deserialize(decoder: Decoder): Uuid = try {
            Uuid.parse(decoder.decodeString())
        } catch (e: IllegalArgumentException) {
            throw IllegalArgumentException("Invalid UUID format")
        }

        override fun serialize(encoder: Encoder, value: Uuid) = encoder.encodeString(value.toString())
    }

    @Serializable
    data class IntrinsicSummary constructor(@Serializable(with = UUIDSerializer::class) val id: Uuid, val name: String)

    @Serializable
    data class Param(val name: String, val type: String)

    @Serializable
    data class Instruction(val mnemonic: String, val xed: String, val form: String?)

    @Serializable
    data class PlatformPerformance(val platform: String, val latency: Float?, val throughput: Float?)

    @Serializable
    data class IntrinsicDetails(
        @Serializable(with = UUIDSerializer::class) val id: Uuid,
        val name: String,
        val returnType: String,
        val returnVar: String?,
        val description: String,
        val operations: String?,
        val category: String,
        val cpuid: String?,
        val tech: String,
        val params: List<Param>,
        val instructions: List<Instruction>?,
        val performance: List<PlatformPerformance>?
    )

    @Serializable
    data class Versioning(
        val intelVersion: String, val intelUpdate: LocalDate, val scrapeDate: LocalDate
    )

    @Serializable data class Paginated<T>(val page: Long, val totalPages: Long, val items: List<T>)

    interface PaginatedResponse<T> {
        fun currentPage(): Long
        fun totalPages(): Long
        fun currentItems(): List<T>
        fun hasNextPage(): Boolean
        suspend fun loadNextPage(): OrError<Unit>
    }

    private inline suspend fun <reified T> throttle(millis: Long, crossinline block: suspend () -> T): T {
        delay(millis)
        return block()
    }

    private suspend inline fun <reified T> makeBasicRequest(url: String, crossinline builder: URLBuilder.() -> Unit = {}): OrError<T> = try {
        val resp = httpClient.get(url) {
            header("Accept", "application/json")
            expectSuccess = true
            url { builder() }
        }
        resp.body<T>().right()
    } catch (e: Exception) {
        "Failed to load $url: ${e.message ?: "Unknown error"}".left()
    }

    private suspend inline fun <reified T> getPaginated(url: String): OrError<PaginatedResponse<T>> {
        val loader: suspend (Long?) -> OrError<Paginated<T>> = { page: Long? ->
            makeBasicRequest<Paginated<T>>(url + (if (page != null) "/$page" else ""))
        }

        return loader(null).map { p0 ->
            object : PaginatedResponse<T> {
                var page = p0.page
                val total = p0.totalPages
                var current = p0.items

                override fun currentItems(): List<T> = current
                override fun hasNextPage(): Boolean = page < total
                override fun currentPage(): Long = page
                override fun totalPages(): Long = total

                override suspend fun loadNextPage(): OrError<Unit> {
                    if(hasNextPage()) {
                        return loader(page + 1).map {
                            page = it.page
                            current = it.items
                        }
                    }
                    else {
                        return "No more pages (for URL ${url})".left()
                    }
                }

            }
        }
    }

    private suspend inline fun <reified T> getPaginatedQuery(url: String, crossinline addParams: URLBuilder.() -> Unit = {}): OrError<PaginatedResponse<T>> {
        val loader: suspend (Long?) -> OrError<Paginated<T>> = { page: Long? ->
            makeBasicRequest<Paginated<T>>(url + (if (page != null) "/$page" else "")) {
                addParams()
                if(page != null) parameters.append("page", "$page")
            }
        }

        return loader(null).map { p0 ->
            object : PaginatedResponse<T> {
                var page = p0.page
                val total = p0.totalPages
                var current = p0.items

                override fun currentItems(): List<T> = current
                override fun hasNextPage(): Boolean = page < total
                override fun currentPage(): Long = page
                override fun totalPages(): Long = total

                override suspend fun loadNextPage(): OrError<Unit> {
                    if(hasNextPage()) {
                        return loader(page + 1).map {
                            page = it.page
                            current = it.items
                        }
                    }
                    else {
                        return "No more pages (for URL ${url})".left()
                    }
                }

            }
        }
    }

    suspend fun getAll(): OrError<PaginatedResponse<IntrinsicSummary>> = getPaginated<IntrinsicSummary>("$baseUrl/all")
    suspend fun getCpuid(): OrError<PaginatedResponse<String>> = getPaginated<String>("$baseUrl/cpuid")
    suspend fun getTech(): OrError<PaginatedResponse<String>> = getPaginated<String>("$baseUrl/tech")
    suspend fun getCategory(): OrError<PaginatedResponse<String>> = getPaginated<String>("$baseUrl/category")
    suspend fun getTypes(): OrError<PaginatedResponse<String>> = getPaginated<String>("$baseUrl/types")

    suspend fun getSearch(
        name: String? = null, returnT: String? = null, cpuid: List<String> = listOf(),
        tech: List<String> = listOf(), category: List<String> = listOf(), desc: String? = null
    ): OrError<PaginatedResponse<IntrinsicSummary>> = getPaginatedQuery("$baseUrl/search") {
        if(name != null) parameters.append("name", name)
        if(returnT != null) parameters.append("returnT", returnT)
        if(cpuid.isNotEmpty()) parameters.append("cpuid", Json.encodeToString(cpuid))
        if(tech.isNotEmpty()) parameters.append("tech", Json.encodeToString(tech))
        if(category.isNotEmpty()) parameters.append("category", Json.encodeToString(category))
        if(desc != null) parameters.append("desc", desc)
    }

    suspend fun getDetails(id: Uuid): OrError<IntrinsicDetails> = makeBasicRequest("$baseUrl/details/$id")
    suspend fun getVersion(): OrError<Versioning> = makeBasicRequest("$baseUrl/version")
}