package com.pixelized.server.lwa.server


import com.pixelized.server.lwa.server.rest.alteration.deleteAlteration
import com.pixelized.server.lwa.server.rest.alteration.getAlteration
import com.pixelized.server.lwa.server.rest.alteration.getAlterations
import com.pixelized.server.lwa.server.rest.alteration.putAlteration
import com.pixelized.server.lwa.server.rest.campaign.deleteCampaignMap
import com.pixelized.server.lwa.server.rest.campaign.getCampaign
import com.pixelized.server.lwa.server.rest.campaign.putCampaignMap
import com.pixelized.server.lwa.server.rest.campaign.putCampaignCharacter
import com.pixelized.server.lwa.server.rest.campaign.putCampaignNpc
import com.pixelized.server.lwa.server.rest.campaign.putCampaignScene
import com.pixelized.server.lwa.server.rest.campaign.removeCampaignCharacter
import com.pixelized.server.lwa.server.rest.campaign.removeCampaignNpc
import com.pixelized.server.lwa.server.rest.character.deleteCharacter
import com.pixelized.server.lwa.server.rest.character.getCharacter
import com.pixelized.server.lwa.server.rest.character.getPreviewCharacter
import com.pixelized.server.lwa.server.rest.character.getPreviewCharacters
import com.pixelized.server.lwa.server.rest.character.putCharacter
import com.pixelized.server.lwa.server.rest.character.putCharacterAlteration
import com.pixelized.server.lwa.server.rest.character.putCharacterDamage
import com.pixelized.server.lwa.server.rest.character.putCharacterDiminished
import com.pixelized.server.lwa.server.rest.character.putCharacterFatigue
import com.pixelized.server.lwa.server.rest.inventory.changeInventoryItemCount
import com.pixelized.server.lwa.server.rest.inventory.consumeInventoryItem
import com.pixelized.server.lwa.server.rest.inventory.createInventoryItem
import com.pixelized.server.lwa.server.rest.inventory.deleteInventory
import com.pixelized.server.lwa.server.rest.inventory.deleteInventoryItem
import com.pixelized.server.lwa.server.rest.inventory.equipInventoryItem
import com.pixelized.server.lwa.server.rest.inventory.getInventory
import com.pixelized.server.lwa.server.rest.inventory.putInventory
import com.pixelized.server.lwa.server.rest.inventory.putPurse
import com.pixelized.server.lwa.server.rest.item.deleteItem
import com.pixelized.server.lwa.server.rest.item.getItem
import com.pixelized.server.lwa.server.rest.item.getItems
import com.pixelized.server.lwa.server.rest.item.putItem
import com.pixelized.server.lwa.server.rest.map.deleteMap
import com.pixelized.server.lwa.server.rest.map.getMap
import com.pixelized.server.lwa.server.rest.map.getMaps
import com.pixelized.server.lwa.server.rest.map.putMap
import com.pixelized.server.lwa.server.rest.tag.getAlterationTags
import com.pixelized.server.lwa.server.rest.tag.getCharacterTags
import com.pixelized.server.lwa.server.rest.tag.getItemTags
import com.pixelized.shared.lwa.SERVER_PORT
import com.pixelized.shared.lwa.protocol.websocket.SocketMessage
import com.pixelized.shared.lwa.sharedModuleDependencies
import com.pixelized.shared.lwa.utils.PathProvider
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.install
import io.ktor.server.engine.EmbeddedServer
import io.ktor.server.engine.embeddedServer
import io.ktor.server.http.content.staticFiles
import io.ktor.server.http.content.staticResources
import io.ktor.server.netty.Netty
import io.ktor.server.netty.NettyApplicationEngine
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.routing.delete
import io.ktor.server.routing.get
import io.ktor.server.routing.put
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import io.ktor.server.websocket.WebSockets
import io.ktor.server.websocket.pingPeriod
import io.ktor.server.websocket.timeout
import io.ktor.server.websocket.webSocket
import io.ktor.websocket.Frame
import io.ktor.websocket.readText
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import org.koin.ktor.ext.inject
import org.koin.ktor.plugin.Koin
import serverModuleDependencies
import java.io.File
import kotlin.time.Duration.Companion.seconds

// https://ktor.io/docs/server-websockets.html#handle-multiple-session
typealias Server = EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration>

class LocalServer {
    private var server: Server? = null

    fun create(
        port: Int = SERVER_PORT, // 16030
    ): LocalServer {
        server = embeddedServer(
            factory = Netty,
            port = port,
            module = {
                install(Koin) {
                    modules(sharedModuleDependencies + serverModuleDependencies)
                }

                val json by inject<Json>()
                val engine by inject<Engine>()
                val pathProvider by inject<PathProvider>()

                install(ContentNegotiation) {
                    json(json)
                }

                install(WebSockets) {
                    pingPeriod = 15.seconds
                    timeout = 15.seconds
                    maxFrameSize = Long.MAX_VALUE
                    masking = false
                }

                routing {
                    webSocket(
                        path = "/ws",
                        handler = {
                            val job = launch {
                                // send local message to the clients
                                engine.webSocket.collect { message ->
                                    val data = json.encodeToString(message)
                                    val frame = Frame.Text(text = data)
                                    try {
                                        send(frame)
                                    } catch (exception: Exception) {
                                        // TODO proper exception handling
                                        println("WebSocket exception: ${exception.localizedMessage}")
                                    }
                                }
                            }
                            runCatching {
                                // watching for clients incoming message
                                incoming.consumeEach { frame ->
                                    if (frame is Frame.Text) {
                                        val data = frame.readText()
                                        val message = json.decodeFromString<SocketMessage>(data)
                                        // log the message
                                        engine.handle(message)
                                        // broadcast to clients the message
                                        engine.webSocket.emit(message)
                                    }
                                }
                            }.onFailure { exception ->
                                // TODO proper exception handling
                                println("WebSocket exception: ${exception.message}")
                            }.also {
                                job.cancel()
                            }
                        }
                    )
                    staticFiles(
                        remotePath = "/resources",
                        dir = File(pathProvider.resourcesPath()),
                        index = null,
                    )
                    route(path = "/campaign") {
                        get(
                            path = "",
                            body = engine.getCampaign(),
                        )
                        route(path = "/character") {
                            put(
                                path = "/add",
                                body = engine.putCampaignCharacter(),
                            )
                            delete(
                                path = "/delete",
                                body = engine.removeCampaignCharacter(),
                            )
                        }
                        route(path = "/npc") {
                            put(
                                path = "/add",
                                body = engine.putCampaignNpc(),
                            )
                            delete(
                                path = "/delete",
                                body = engine.removeCampaignNpc(),
                            )
                        }
                        put(
                            path = "/scene",
                            body = engine.putCampaignScene(),
                        )
                        route(
                            path = "/map",
                        ) {
                            put(
                                path = "/update",
                                body = engine.putCampaignMap(),
                            )
                            put(
                                path = "/delete",
                                body = engine.deleteCampaignMap(),
                            )
                        }
                    }
                    route(
                        path = "/character",
                    ) {
                        get(
                            path = "/detail",
                            body = engine.getCharacter(),
                        )
                        delete(
                            path = "/delete",
                            body = engine.deleteCharacter(),
                        )
                        route(
                            path = "/update",
                        ) {
                            put(
                                path = "/sheet",
                                body = engine.putCharacter(),
                            )
                            put(
                                path = "/damage",
                                body = engine.putCharacterDamage(),
                            )
                            put(
                                path = "/fatigue",
                                body = engine.putCharacterFatigue(),
                            )
                            put(
                                path = "/diminished",
                                body = engine.putCharacterDiminished(),
                            )
                            put(
                                path = "/alteration",
                                body = engine.putCharacterAlteration(),
                            )
                        }
                        route(
                            path = "/preview"
                        ) {
                            get(
                                path = "/all",
                                body = engine.getPreviewCharacters(),
                            )
                            get(
                                path = "/detail",
                                body = engine.getPreviewCharacter(),
                            )
                        }
                    }
                    route(
                        path = "/alteration",
                    ) {
                        get(
                            path = "/all",
                            body = engine.getAlterations(),
                        )
                        get(
                            path = "/detail",
                            body = engine.getAlteration(),
                        )
                        put(
                            path = "/update",
                            body = engine.putAlteration()
                        )
                        delete(
                            path = "/delete",
                            body = engine.deleteAlteration()
                        )
                    }
                    route(path = "/item") {
                        get(
                            path = "/all",
                            body = engine.getItems(),
                        )
                        get(
                            path = "/detail",
                            body = engine.getItem(),
                        )
                        put(
                            path = "/update",
                            body = engine.putItem(),
                        )
                        delete(
                            path = "/delete",
                            body = engine.deleteItem(),
                        )
                    }
                    route(path = "/tag") {
                        get(
                            path = "/character",
                            body = engine.getCharacterTags(),
                        )
                        get(
                            path = "/alteration",
                            body = engine.getAlterationTags(),
                        )
                        get(
                            path = "/item",
                            body = engine.getItemTags(),
                        )
                    }
                    route(path = "/inventory") {
                        get(
                            path = "/detail",
                            body = engine.getInventory(),
                        )
                        put(
                            path = "/update",
                            body = engine.putInventory()
                        )
                        put(
                            path = "/update/purse",
                            body = engine.putPurse(),
                        )
                        delete(
                            path = "/delete",
                            body = engine.deleteInventory()
                        )
                        route(
                            path = "/item"
                        ) {
                            put(
                                path = "/create",
                                body = engine.createInventoryItem()
                            )
                            put(
                                path = "/count",
                                body = engine.changeInventoryItemCount()
                            )
                            put(
                                path = "/consume",
                                body = engine.consumeInventoryItem()
                            )
                            put(
                                path = "/equip",
                                body = engine.equipInventoryItem()
                            )
                            delete(
                                path = "/delete",
                                body = engine.deleteInventoryItem()
                            )
                        }
                    }
                    route(path = "/map") {
                        get(
                            path = "/all",
                            body = engine.getMaps(),
                        )
                        get(
                            path = "/detail",
                            body = engine.getMap(),
                        )
                        put(
                            path = "/update",
                            body = engine.putMap(),
                        )
                        delete(
                            path = "/delete",
                            body = engine.deleteMap(),
                        )
                    }
                }
            }
        )
        return this
    }

    fun start() {
        runBlocking {
            try {
                server?.start(wait = true)
            } catch (exception: Exception) {
                println("WebSocket exception: ${exception.localizedMessage}")
            } finally {
                println("Server close")
            }
        }
    }
}