package launchers

import java.lang.management.ManagementFactory
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.StandardOpenOption
import java.security.MessageDigest
import java.time.Duration
import kotlin.io.path.exists
import kotlin.io.path.name

private data class DownloadLauncher(
    val displayName: String,
    val xdgSubdirectory: String,
    val jarFilename: String,
    val lockedArtifact: LockedArtifact,
)

private val httpClient =
    HttpClient
        .newBuilder()
        .connectTimeout(Duration.ofSeconds(30))
        .followRedirects(HttpClient.Redirect.NORMAL)
        .build()

private val minecraftJavaServer =
    DownloadLauncher(
        displayName = "Minecraft Java server",
        xdgSubdirectory = "net.minecraft.JavaServer",
        jarFilename = "minecraft_server.${Versions.minecraft.version}.jar",
        lockedArtifact = Versions.minecraft,
    )

private val minecraftFabricServer =
    DownloadLauncher(
        displayName = "Minecraft Fabric server",
        xdgSubdirectory = "net.fabricmc.FabricServer",
        jarFilename = "fabric-server.${Versions.fabric.version}.jar",
        lockedArtifact = Versions.fabric,
    )

private val geyserStandalone =
    DownloadLauncher(
        displayName = "Geyser Standalone",
        xdgSubdirectory = "org.geysermc.GeyserStandalone",
        jarFilename = "Geyser-Standalone.${Versions.geyser.version}.jar",
        lockedArtifact = Versions.geyser,
    )

private val mcXboxBroadcastStandalone =
    DownloadLauncher(
        displayName = "MCXboxBroadcast Standalone",
        xdgSubdirectory = "com.rtm516.MCXboxBroadcastStandalone",
        jarFilename = "MCXboxBroadcastStandalone.${Versions.mcXboxBroadcast.version}.jar",
        lockedArtifact = Versions.mcXboxBroadcast,
    )

object MinecraftJavaServerMain {
    @JvmStatic
    fun main(args: Array<String>) = launch(minecraftJavaServer, args)
}

object MinecraftFabricServerMain {
    @JvmStatic
    fun main(args: Array<String>) = launch(minecraftFabricServer, args)
}

object GeyserStandaloneMain {
    @JvmStatic
    fun main(args: Array<String>) = launch(geyserStandalone, args)
}

object McXboxBroadcastStandaloneMain {
    @JvmStatic
    fun main(args: Array<String>) = launch(mcXboxBroadcastStandalone, args)
}

private fun launch(
    launcher: DownloadLauncher,
    args: Array<String>,
) {
    val jarPath = dataHome().resolve(launcher.xdgSubdirectory).resolve(launcher.jarFilename)
    ensureJar(launcher, jarPath)

    val command =
        applicationCommand(
            javaExecutable(),
            ManagementFactory.getRuntimeMXBean().inputArguments,
            jarPath,
            args,
        )
    exec(launcher.displayName, command)
}

internal fun applicationCommand(
    javaExecutable: String,
    jvmArguments: List<String>,
    jarPath: Path,
    applicationArguments: Array<String>,
): List<String> =
    buildList {
        add(javaExecutable)
        addAll(jvmArguments)
        add("-jar")
        add(jarPath.toString())
        addAll(applicationArguments)
    }

private fun ensureJar(
    launcher: DownloadLauncher,
    destination: Path,
) {
    if (hasExpectedSha256(destination, launcher.lockedArtifact.sha256)) return

    Files.createDirectories(destination.parent)
    val temporary = destination.resolveSibling(".${destination.name}.part.${ProcessHandle.current().pid()}")
    try {
        System.err.println(
            "minecraft-server-rs: downloading ${launcher.displayName} ${launcher.lockedArtifact.version}",
        )
        val request =
            HttpRequest
                .newBuilder(URI.create(launcher.lockedArtifact.url))
                .header("User-Agent", "minecraft-server-rs-wrapper/${launcher.lockedArtifact.version}")
                .timeout(Duration.ofMinutes(15))
                .build()
        val response =
            httpClient.send(
                request,
                HttpResponse.BodyHandlers.ofFile(
                    temporary,
                    StandardOpenOption.CREATE,
                    StandardOpenOption.TRUNCATE_EXISTING,
                    StandardOpenOption.WRITE,
                ),
            )
        check(response.statusCode() in 200..299) {
            "Could not download ${launcher.displayName} ${launcher.lockedArtifact.version}: HTTP ${response.statusCode()}"
        }
        val foundHash = sha256(temporary)
        check(foundHash == launcher.lockedArtifact.sha256) {
            "${launcher.displayName} ${launcher.lockedArtifact.version} failed SHA-256 verification: expected ${launcher.lockedArtifact.sha256}, found $foundHash"
        }
        Files.move(
            temporary,
            destination,
            StandardCopyOption.ATOMIC_MOVE,
            StandardCopyOption.REPLACE_EXISTING,
        )
        System.err.println(
            "minecraft-server-rs: downloaded and verified ${launcher.displayName} ${launcher.lockedArtifact.version}",
        )
    } finally {
        Files.deleteIfExists(temporary)
    }
}

internal fun hasExpectedSha256(
    path: Path,
    expectedSha256: String,
): Boolean = path.exists() && sha256(path) == expectedSha256

private fun sha256(path: Path): String {
    val digest = MessageDigest.getInstance("SHA-256")
    Files.newInputStream(path).use { input ->
        val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
        while (true) {
            val count = input.read(buffer)
            if (count < 0) break
            digest.update(buffer, 0, count)
        }
    }
    return digest.digest().joinToString("") { byte -> "%02x".format(byte) }
}

private fun dataHome(): Path {
    val configured = System.getenv("XDG_DATA_HOME")
    if (!configured.isNullOrBlank() && Path.of(configured).isAbsolute) return Path.of(configured)

    val home = System.getenv("HOME") ?: error("XDG_DATA_HOME is unavailable because HOME is not set")
    return Path.of(home, ".local", "share")
}
