/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2026 SteamWar.de-Serverteam
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.File
import java.io.FileInputStream
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.security.MessageDigest
import java.util.Properties
import java.util.Random
import java.util.stream.Collectors

open class DevServer : DefaultTask() {

    @get:Input
    var debug: Boolean = false

    @get:Input
    var template: String? = null

    @get:Input
    @get:Optional
    var plugins: String? = null

    @get:Input
    @get:Optional
    var port: Int? = null

    @get:Input
    @get:Optional
    var jar: String? = null

    @get:Input
    @get:Optional
    var dParams: MutableMap<String, String> = HashMap()

    @get:Input
    @get:Optional
    var jvmArgs: String? = null

    @get:Input
    @get:Optional
    var checkpointFolder: String? = null

    @get:Input
    @get:Optional
    var profile: Boolean? = null

    @get:Input
    @get:Optional
    var forceUpgrade: Boolean? = null

    @get:Input
    @get:Optional
    var worldName: String? = null

    @get:Internal
    var processInput: BufferedWriter? = null

    @get:Internal
    var host: String? = null

    @get:Internal
    var debugPort: Int = 0

    @get:Internal
    var running: Boolean = true

    init {
        doFirst {
            checkpointFolder?.let { dParams.put("checkpoint", it) }

            val projects = mutableListOf<Project>()
            projects.add(project)
            while (projects[0].parent != null) {
                projects.add(0, projects[0].parent!!)
            }

            val properties = Properties()
            projects.forEach {
                val file = File(it.projectDir, "steamwar.properties")
                if (file.exists()) {
                    FileInputStream(file).use { input -> properties.load(input) }
                }
            }

            if (template!!.startsWith("Bau")) {
                if (properties.containsKey("worldName")) {
                    worldName = properties.getProperty("worldName")
                } else {
                    throw GradleException("Please supply the 'worldName' in a 'steamwar.properties' files either in this project dir or any parent project!")
                }
            }
            host = properties.getProperty("host")
            debugPort = Random().nextInt(5001, 10000)

            if (host == null) {
                throw GradleException("Please supply the 'host' in a 'steamwar.properties' files either in this project dir or any parent project!")
            }
        }
        doLast {
            setupTemplate(template!!)
            uploadDependencies()
            if (debug) startDebugPort()
            startDevServer()
        }
        finalizedBy(Finalizer())
    }

    inner class Finalizer : DefaultTask() {
        init {
            doLast {
                processInput?.let {
                    it.write(if (template!!.endsWith("Velocity")) "end\n" else "stop\n")
                    it.flush()
                }
                running = false
            }
        }
    }

    private fun run(vararg args: String): Process {
        val arguments = mutableListOf("ssh", host, "-T")
        arguments.addAll(args)
        val process = ProcessBuilder(arguments).start()
        process.waitFor()
        return process
    }

    private fun checkFileOnRemote(path: String): Boolean {
        val process = run("[ -e \"$path\" ] && echo \"true\"")
        process.errorStream.close()
        process.outputStream.close()
        return BufferedReader(InputStreamReader(process.inputStream)).use { reader ->
            reader.lines().count() > 0
        }
    }

    private fun closeProcess(process: Process) {
        process.outputStream.close()
        process.inputStream.close()
        process.errorStream.close()
    }

    fun setupTemplate(template: String) {
        if (checkFileOnRemote(template)) return
        if (checkFileOnRemote("/configs/GameModes/$template.yml")) {
            println("GameMode Config exists")
            val process = run("cat /configs/GameModes/$template.yml | grep \"Folder: \"")
            val serverTemplateName = BufferedReader(InputStreamReader(process.inputStream)).lines()
                .collect(Collectors.joining("\n"))
                .trim()
                .substring("Folder: ".length)
            closeProcess(process)
            setupTemplate(serverTemplateName)
            run("ln -s $serverTemplateName $template")
            return
        }
        if (!checkFileOnRemote("/servers/$template")) {
            throw GradleException("Used template ($template) is not in /servers/ directory of the given host $host")
        }
        closeProcess(run("cp -r /servers/$template $template"))
        closeProcess(run("chmod u+w $template"))
        closeProcess(run("rm -r $template/plugins/*WorldEdit/"))
        closeProcess(run("rm $template/log4j2.xml"))
    }

    fun uploadDependencies() {
        val base = if (plugins == null) "$template/plugins" else plugins
        println("Uploading to ~/$base")
        this.dependsOn.forEach {
            if (it !is String) {
                throw GradleException("Illegal argument for uploading dependencies")
            }
            val resolved = project.findProject(it.substring(0, it.lastIndexOf(':')))!!
            val archiveTask = resolved.tasks.findByName(it.substring(it.lastIndexOf(':') + 1)) as AbstractArchiveTask

            val archive = archiveTask.archiveFile.get().asFile

            var process = ProcessBuilder("ssh", host, "-T", "sha1sum $base/${archive.name.replace("-all", "")}").start()
            val bytes = MessageDigest.getInstance("sha1").digest(archive.readBytes())
            val sb = StringBuilder()
            for (b in bytes) {
                sb.append(String.format("%02X", b))
            }
            var same = false
            process.inputStream.bufferedReader().readLines().forEach { line ->
                same = same || line.startsWith(sb.toString().lowercase())
            }
            closeProcess(process)
            if (same) {
                println("Skipping  $archive")
                return@forEach
            }

            println("Uploading $archive")
            process = ProcessBuilder("ssh", host, "-T", "rm $base/${archive.name.replace("-all", "")}").start()
            process.waitFor()
            closeProcess(process)

            process = ProcessBuilder("scp", archive.absolutePath, "$host:~/$base/${archive.name.replace("-all", "")}").start()
            process.waitFor()
            closeProcess(process)
            println("Uploaded  $archive")
        }
    }

    fun startDebugPort() {
        val process = ProcessBuilder("ssh", host, "-L", "5005:localhost:$debugPort").start()
        val processOutput = BufferedReader(InputStreamReader(process.inputStream))
        Thread {
            while (running) {
            }
            processOutput.close()
            process.errorStream.close()
        }.start()
    }

    fun startDevServer() {
        val devPy = StringBuilder().append("sw dev")
        if (port != null) devPy.append(" --port $port")
        if (worldName != null) devPy.append(" -w $template/$worldName")
        if (plugins != null) devPy.append(" -p $plugins")
        if (profile != null) devPy.append(" --profile")
        if (forceUpgrade != null) devPy.append(" --forceUpgrade")
        if (jar != null) devPy.append(" --jar $jar")
        devPy.append(" $template")
        for ((key, value) in dParams) {
            devPy.append(" -D$key=$value")
        }
        devPy.append(" -Dpaper.disablePluginRemapping=true")
        if (debug) devPy.append(" -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:$debugPort")
        devPy.append(" -javaagent:/jars/AccessWidener.jar=start")
        if (jvmArgs != null) devPy.append(" $jvmArgs")
        println("Starting $template with command $devPy")

        val process = ProcessBuilder("ssh", host, "-T", devPy.toString()).start()
        val processOutput = BufferedReader(InputStreamReader(process.inputStream))
        Thread {
            while (running) {
                if (processOutput.ready()) {
                    println(processOutput.readLine())
                }
            }
            processOutput.close()
            process.errorStream.close()
        }.start()

        processInput = BufferedWriter(OutputStreamWriter(process.outputStream))
        val input = BufferedReader(InputStreamReader(System.`in`))
        Thread {
            while (running) {
                val text = input.readLine() ?: break
                processInput?.write(text)
                processInput?.newLine()
                processInput?.flush()
            }
        }.start()

        process.waitFor()
        processInput?.close()
        processInput = null
        running = false
    }
}
