package de.steamwar.commands.dev

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.CliktError
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.help
import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.options.defaultLazy
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.help
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.file
import com.github.ajalt.clikt.parameters.types.long
import com.github.ajalt.clikt.parameters.types.path
import com.sun.security.auth.module.UnixSystem
import org.yaml.snakeyaml.Yaml
import java.io.File
import kotlin.io.path.absolute
import kotlin.io.path.absolutePathString
import kotlin.random.Random

const val LOG4J_CONFIG = """<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" packages="com.mojang.util">
    <Appenders>
        <Console name="WINDOWS_COMPAT" target="SYSTEM_OUT"></Console>
        <Queue name="TerminalConsole">
            <PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg{nolookups}%n" />
        </Queue>
        <RollingRandomAccessFile name="File" fileName="$\{'sys:logPath'}/latest.log" filePattern="$\{'sys:logPath'}/%d{yyyy.MM.dd}.log.gz">
            <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg{nolookups}%n" />
            <Policies>
                <TimeBasedTriggeringPolicy />
            </Policies>
            <DefaultRolloverStrategy max="7"/>
        </RollingRandomAccessFile>
    </Appenders>
    <Loggers>
        <Root level="info">
            <filters>
                <MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL" />
            </filters>
            <AppenderRef ref="WINDOWS_COMPAT" level="info"/>
            <AppenderRef ref="File"/>
            <AppenderRef ref="TerminalConsole" level="info"/>
        </Root>
    </Loggers>
</Configuration>"""

class DevCommand : CliktCommand("dev") {
    override fun help(context: Context): String =
        "Start a dev Server"

    override val treatUnknownOptionsAsArgs = true

    val server by argument().help("Server Template")
    val port by option("--port").long().defaultLazy { UnixSystem().uid + 1010 }.help("Port for Server")
    val world by option("--world", "-w").path(canBeFile = false).help("User World")
    val plugins by option("--plugins", "-p").path(true, canBeFile = false).help("Plugin Dir")
    val profile by option().flag().help("Add Profiling Arguments")
    val forceUpgrade by option().flag().help("Force Upgrade")
    val jar by option().file(true, canBeDir = false).help("Jar File")
    val jvm by option().file(true, canBeDir = false).help("Java Executable")
    val jvmArgs by argument().multiple()

    override val printHelpOnEmptyArgs = true

    val workingDir = File("").absoluteFile
    val log4jConfig = File(workingDir, "log4j2.xml")

    override fun run() {
        val args = mutableListOf<String>()

        var serverDir = resolveServerDirectory(server)

        if (isVelocity(server)) {
            runServer(
                args, jvmArgs, listOf(
                    jar?.absolutePath
                        ?: File("/jars/Velocity.jar").absolutePath
                ), serverDir
            )
        } else {
            setLogConfig(args)
            val version = findVersion(server)
                ?: throw CliktError("Unknown Server Version")
            val gameModeTemplate = if (serverDir.isDirectory) null else loadGameModeTemplate(server)
            if (gameModeTemplate != null) {
                serverDir = gameModeTemplate.serverDir
                args += "-Dconfig=$server.yml"
            }
            val worldFile = world?.absolute()?.toFile()
                ?: File(workingDir, "devtempworld")
            var jarFile = jar?.absolutePath
                ?: additionalVersions[server]?.let { supportedVersionJars[it] }
                ?: supportedVersionJars[version]
                ?: throw CliktError("Unknown Server Version")
            if (gameModeTemplate != null) {
                jarFile = if (gameModeTemplate.spigot) {
                    jarFile.replace("paper", "spigot")
                } else {
                    jarFile.replace("spigot", "paper")
                }
            }

            if (!worldFile.exists()) {
                val templateFile = gameModeTemplate?.worldTemplate ?: File(serverDir, "Bauwelt")
                if (!templateFile.exists()) {
                    throw CliktError("Could not find world template: ${templateFile.absolutePath}")
                }
                worldFile.parentFile?.mkdirs()
                templateFile.copyRecursively(worldFile)
            }
            val worldDir = worldFile.parentFile ?: workingDir

            val devFile = File("/configs/DevServer/${System.getProperty("user.name")}.$port.$version")
            if (System.getProperty("user.name") != "minecraft") {
                devFile.createNewFile()
            }

            runServer(
                args, jvmArgs, listOf(
                    jarFile,
                    *(if (forceUpgrade) arrayOf("-forceUpgrade") else arrayOf()),
                    "--log-strip-color",
                    "--port", port.toString(),
                    "--level-name", worldFile.name,
                    "--world-dir", worldDir.absolutePath,
                    "nogui",
                    *(if (plugins != null) arrayOf("--plugins", plugins!!.absolutePathString()) else arrayOf())
                ), serverDir
            )

            try {
                devFile.delete()
            } catch (_: Exception) { /* ignored */
            }
        }
    }

    data class GameModeTemplate(
        val serverDir: File,
        val worldTemplate: File,
        val spigot: Boolean
    )

    val jvmDefaultParams = arrayOf(
        "-Xmx1G",
        "-Xgc:excessiveGCratio=80",
        "-Xsyslog:none",
        "-Xtrace:none",
        "-Xnoclassgc",
        "-Xdisableexplicitgc",
        "-XX:+AlwaysPreTouch",
        "-XX:+CompactStrings",
        "-XX:-HeapDumpOnOutOfMemory",
        "-XX:+ExitOnOutOfMemoryError"
    )

    val jvmArgOverrides = arrayOf("--add-opens", "java.base/jdk.internal.misc=ALL-UNNAMED")

    val jvmNonJava8Params = arrayOf(
        *jvmArgOverrides,
        "-XX:-CRIUSecProvider"
    )

    val extendedStartupParams = arrayOf(
        "-Dpaper.disablePluginRemapping=true",
        "-javaagent:/jars/AccessWidener.jar=start"
    )

    val supportedVersionJars = mapOf(
        8 to "/jars/paper-1.8.8.jar",
        9 to "/jars/spigot-1.9.4.jar",
        10 to "/jars/paper-1.10.2.jar",
        12 to "/jars/spigot-1.12.2.jar",
        14 to "/jars/spigot-1.14.4.jar",
        15 to "/jars/spigot-1.15.2.jar",
        18 to "/jars/paper-1.18.2.jar",
        19 to "/jars/paper-1.19.3.jar",
        20 to "/jars/paper-1.20.1.jar",
        21 to "/jars/paper-1.21.6.jar"
    )

    val additionalVersions = mapOf(
        "Lobby" to 21
    )

    fun findVersion(server: String): Int? =
        server.dropWhile { !it.isDigit() }.toIntOrNull()

    fun isJava8(server: String): Boolean =
        findVersion(server)?.let { it <= 10 }
            ?: false

    fun isVelocity(server: String): Boolean =
        server.endsWith("Velocity")

    fun resolveServerDirectory(server: String): File {
        val localServer = File(workingDir, server)
        if (localServer.isDirectory) {
            return localServer
        }
        return File("/servers", server)
    }

    fun loadGameModeTemplate(server: String): GameModeTemplate? {
        val configFile = File("/configs/GameModes/$server.yml")
        if (!configFile.exists()) {
            throw CliktError("Server/GameMode not found")
        }

        val document = configFile.reader().use { reader ->
            Yaml().load<Map<String, Any?>>(reader)
        } ?: throw CliktError("GameMode config is empty: ${configFile.absolutePath}")
        val serverConfig = document["Server"] as? Map<*, *>
            ?: throw CliktError("GameMode config is missing Server section: ${configFile.absolutePath}")
        val folder = serverConfig["Folder"] as? String
            ?: throw CliktError("GameMode config is missing Server.Folder: ${configFile.absolutePath}")
        val maps = (serverConfig["Maps"] as? List<*>)
            ?.filterIsInstance<String>()
            ?.takeIf { it.isNotEmpty() }
            ?: throw CliktError("GameMode config is missing Server.Maps: ${configFile.absolutePath}")

        val serverDir = File("/servers", folder)
        val worldTemplate = File(File(serverDir, "arenas"), maps[Random.nextInt(maps.size)])
        return GameModeTemplate(
            serverDir = serverDir,
            worldTemplate = worldTemplate,
            spigot = serverConfig["Spigot"] == true
        )
    }

    fun setLogConfig(args: MutableList<String>) {
        args += "-DlogPath=${workingDir.absolutePath}/logs"
        args += "-Dlog4j.configurationFile=${log4jConfig.absolutePath}"

        if (!log4jConfig.exists()) {
            log4jConfig.writeText(LOG4J_CONFIG)
        }
    }

    fun runServer(args: List<String>, jvmArgs: List<String>, cmd: List<String>, serverDir: File) {
        val effectiveJvmArgs = mutableListOf<String>()
        effectiveJvmArgs += jvmArgs
        if (!isVelocity(server)) {
            extendedStartupParams.forEach { arg ->
                if (effectiveJvmArgs.none { it == arg }) {
                    effectiveJvmArgs += arg
                }
            }
        }

        val process = ProcessBuilder(
            jvm?.absolutePath
                ?: if (isJava8(server)) "/usr/lib/jvm/openj9-8/bin/java" else "/usr/lib/jvm/openj9-21/bin/java",
            *effectiveJvmArgs.toTypedArray(),
            *args.toTypedArray(),
            *jvmDefaultParams,
            *(if (isJava8(server)) arrayOf() else jvmNonJava8Params),
            *(if (profile) arrayOf("-javaagent:/jars/LixfelsProfiler.jar=start") else arrayOf()),
            "-Xshareclasses:nonfatal,name=$server",
            "-jar",
            *cmd.toTypedArray()
        ).directory(serverDir).inheritIO().start()
        Runtime.getRuntime().addShutdownHook(Thread { if (process.isAlive) process.destroyForcibly() })
        process.waitFor()
    }
}
