package com.jaytux.simd.server

import com.jaytux.simd.data.*
import com.jaytux.simd.server.RouteCache.installRoot
import com.jaytux.simd.server.RouteCache.register
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.autohead.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.*

@Serializable data class ErrorResponse(val error: String)
class HttpError(msg: String, val status: HttpStatusCode = HttpStatusCode.BadRequest) : Exception(msg)

inline suspend fun <reified R: Any> RoutingContext.runCatching(crossinline block: suspend RoutingContext.() -> R) {
    try {
        call.respond(block())
    }
    catch(err: HttpError) {
        call.respond(err.status, ErrorResponse(err.message ?: "<no error message given>"))
    }
}

inline fun <reified T, reified R> Route.getPagedUrl(
    path: String,
    desc: String,
    crossinline perPage: RoutingContext.() -> Int,
    crossinline mapper: (T) -> R,
    crossinline getSet: RoutingContext.() -> SizedIterable<T>,
) {
    register(HttpMethod.Get, path, "${desc} (initial page)")
    get(path) {
        runCatching {
            transaction { getSet().paginated(0, perPage(), mapper) }
        }
    }

    register(HttpMethod.Get, "$path/{page}", "${desc} (paginated)")
    get("$path/{page}") {
        runCatching {
            val page = call.parameters["page"]?.toLongOrNull() ?: 0
            transaction { getSet().paginated(page, perPage(), mapper) }
        }
    }
}

inline fun <reified T, reified R> Route.getPagedRequest(
    path: String,
    desc: String,
    crossinline perPage: RoutingContext.() -> Int,
    crossinline mapper: (T) -> R,
    crossinline getSet: RoutingContext.() -> SizedIterable<T>,
) {
    register(HttpMethod.Get, path, desc)
    get(path) {
        runCatching {
            val page = call.request.queryParameters["page"]?.toLongOrNull() ?: 0
            transaction { getSet().paginated(page, perPage(), mapper) }
        }
    }
}

fun Application.configureRouting() {
    install(AutoHeadResponse)
    routing {
        installGetAll()
        installSearch()
        installDetails()
        installVersion()

        installRoot()
    }
}

fun Application.configureFallback() {
    routing {
        route("{...}") {
            handle {
                call.respond(HttpStatusCode.NotFound, ErrorResponse("Endpoint not found: ${call.request.httpMethod.value} ${call.request.path()}"))
            }
        }
    }
}

object RouteCache {
    @Serializable data class RouteOverview(val desc: String, val routes: List<RouteEntry>)
    @Serializable data class RouteEntry(val method: String, val path: String, val desc: String)
    private val cache = mutableListOf<RouteEntry>()

    fun Route.register(method: HttpMethod, path: String, desc: String) {
        cache += RouteEntry(method.value, path, desc)
        cache.sortWith(compareBy<RouteEntry> { it.path }.thenBy { it.method })
    }

    fun Routing.installRoot() {
        get("/") {
            call.respond(RouteOverview("Jay's C/C++ intrinsics API [using data from Intel]", cache))
        }
    }
}