package xyz.etztech.stonks.api

import io.javalin.Javalin
import java.time.Duration
import java.time.Instant
import kotlinx.coroutines.*
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import xyz.etztech.stonks.dsl.Statistics

private var statisticsCache = ""

private var statisticsCacheTimestamp = Instant.now()

private var playersCache = ""

private var playersCacheTimestamp = Instant.now()

fun initApiServer(apiServerPort: Int, database: Database) {
    val app = Javalin.create().start(apiServerPort)
    println("Javalin web server started")

    app.get("/") { ctx ->
        run {
            transaction(database) {
                addLogger(StdOutSqlLogger)
                val dataPoints =
                        Statistics.slice(Statistics.playerId.count()).selectAll().limit(1).map {
                            it[Statistics.playerId.count()]
                        }[0]

                val playerCount =
                        Statistics.slice(Statistics.playerId.countDistinct())
                                .selectAll()
                                .limit(1)
                                .map { it[Statistics.playerId.countDistinct()] }[0]

                ctx.result("$dataPoints data points from $playerCount players!")
            }
        }
    }

    app.get("/statistics") { ctx ->
        run {
            if (statisticsCache.isEmpty() or
                    (Duration.between(Instant.now(), statisticsCacheTimestamp) >
                            Duration.ofHours(1))) {
                transaction(database) {
                    addLogger(StdOutSqlLogger)

                    val statistics =
                            Statistics.slice(Statistics.type, Statistics.name)
                                    .selectAll()
                                    .groupBy(Statistics.type, Statistics.name)
                                    .map { Statistic(it[Statistics.type], it[Statistics.name]) }

                    statisticsCache = Json.encodeToString(statistics)
                }
            }

            ctx.result(statisticsCache)
        }
    }

    app.get("/statistics/:type/:name") { ctx ->
        run {
            transaction(database) {
                addLogger(StdOutSqlLogger)
                val maxExpr = Statistics.value.max()

                val statistics =
                        Statistics.slice(
                                        Statistics.type,
                                        Statistics.name,
                                        Statistics.playerId,
                                        maxExpr)
                                .select {
                                    Statistics.type.eq(ctx.pathParam("type")) and
                                            Statistics.name.eq(ctx.pathParam("name"))
                                }
                                .groupBy(Statistics.playerId)
                                .orderBy(maxExpr, SortOrder.DESC)
                                .map {
                                    StatisticValue(
                                            it[Statistics.playerId],
                                            it[Statistics.type],
                                            it[Statistics.name],
                                            it[maxExpr]!!)
                                }

                ctx.result(Json { prettyPrint = true }.encodeToString(statistics))
            }
        }
    }

    app.get("/players") { ctx ->
        run {
            if (playersCache.isEmpty() or
                    (Duration.between(Instant.now(), playersCacheTimestamp) >
                            Duration.ofHours(1))) {
                transaction(database) {
                    addLogger(StdOutSqlLogger)

                    val players =
                            Statistics.slice(Statistics.playerId)
                                    .selectAll()
                                    .groupBy(Statistics.playerId)
                                    .map { it[Statistics.playerId] }
                    playersCache = Json.encodeToString(players)
                }
            }

            ctx.result(playersCache)
        }
    }

    app.get("/players/:playerId") { ctx ->
        run {
            transaction(database) {
                addLogger(StdOutSqlLogger)
                val maxExpr = Statistics.value.max()

                val statistics =
                        Statistics.slice(
                                        Statistics.type,
                                        Statistics.name,
                                        Statistics.playerId,
                                        maxExpr)
                                .select { Statistics.playerId.eq(ctx.pathParam("playerId")) }
                                .groupBy(Statistics.type, Statistics.name)
                                .map {
                                    StatisticValue(
                                            it[Statistics.playerId],
                                            it[Statistics.type],
                                            it[Statistics.name],
                                            it[maxExpr]!!)
                                }

                ctx.result(Json { prettyPrint = true }.encodeToString(statistics))
            }
        }
    }

    app.get("/players/:playerId/:type/:name") { ctx ->
        run {
            transaction(database) {
                addLogger(StdOutSqlLogger)
                val statistics =
                        Statistics.slice(
                                        Statistics.type,
                                        Statistics.name,
                                        Statistics.playerId,
                                        Statistics.timestamp,
                                        Statistics.value)
                                .select {
                                    Statistics.playerId.eq(ctx.pathParam("playerId")) and
                                            Statistics.type.eq(ctx.pathParam("type")) and
                                            Statistics.name.eq(ctx.pathParam("name"))
                                }
                                .orderBy(Statistics.timestamp, SortOrder.DESC)
                                .map {
                                    HistoricalStatisticValue(
                                            it[Statistics.playerId],
                                            it[Statistics.type],
                                            it[Statistics.name],
                                            it[Statistics.timestamp].toString(),
                                            it[Statistics.value])
                                }

                ctx.result(Json { prettyPrint = true }.encodeToString(statistics))
            }
        }
    }
}

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

@Serializable
data class StatisticValue(
        val playerId: String,
        val type: String,
        val name: String,
        val value: Long
)

@Serializable
data class HistoricalStatisticValue(
        val playerId: String,
        val type: String,
        val name: String,
        val timestamp: String,
        val value: Long
)
