package com.ustadmobile.testservercontroller.runner

import com.ustadmobile.test.http.waitForUrl
import com.ustadmobile.testservercontroller.PROP_BASE_DIR
import com.ustadmobile.testservercontroller.PROP_ENV
import com.ustadmobile.testservercontroller.PROP_PORT_RANGE
import com.ustadmobile.testservercontroller.PROP_RUN_COMMAND
import com.ustadmobile.testservercontroller.PROP_SHUTDOWN_URL
import com.ustadmobile.testservercontroller.PROP_URLSUBSTITUTION
import com.ustadmobile.testservercontroller.RunningCmd
import com.ustadmobile.testservercontroller.util.DEFAULT_FROM_PORT
import com.ustadmobile.testservercontroller.util.DEFAULT_UNTIL_PORT
import com.ustadmobile.testservercontroller.util.findFreePort
import io.ktor.client.HttpClient
import io.ktor.http.URLBuilder
import io.ktor.http.Url
import io.ktor.http.toURI
import io.ktor.server.config.ApplicationConfig
import io.ktor.util.collections.ConcurrentMap
import io.ktor.util.logging.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone
import kotlinx.datetime.number
import kotlinx.datetime.toLocalDateTime
import okhttp3.OkHttpClient
import java.io.File
import kotlin.time.Clock
import kotlin.time.ExperimentalTime

class TestServersRunner(
    config: ApplicationConfig,
    private val okHttpClient: OkHttpClient,
    private val httpClient: HttpClient,
    private val logger: Logger,
) {
    data class StartServerRequest(
        val controlServerUrl: String,
        val waitForUrl: String?,
        val name: String?,
    )

    data class StartServerResponse(
        val port: Int,
        val url: String,
    )

    val runningCmdMap = ConcurrentMap<Int, RunningCmd>()

    val runCommand = config.property(PROP_RUN_COMMAND).getString()

    val workspaceBaseDir = File(config.propertyOrNull(PROP_BASE_DIR)?.getString() ?: DEFAULT_BASEDIR)

    val shutdownUrl = config.propertyOrNull(PROP_SHUTDOWN_URL)?.getString()

    val portRangeStr = config.propertyOrNull(PROP_PORT_RANGE)?.getString()
        ?: "$DEFAULT_FROM_PORT-$DEFAULT_UNTIL_PORT"

    val envVariables = config.propertyOrNull(PROP_ENV)?.getMap()?.mapNotNull { entry ->
        (entry.value as? String)?.let { entry.key to it }
    }?.toMap() ?: emptyMap()

    val split = portRangeStr.split("-").map { it.toInt() }.also {
        if(it.size != 2)
            throw IllegalArgumentException(
                "$$PROP_PORT_RANGE must be in the form of x-y e.g. $DEFAULT_FROM_PORT-$DEFAULT_UNTIL_PORT"
            )
    }

    val fromPort = split.first()
    val untilPort = split.last()

    val urlSubstitution = config.propertyOrNull(PROP_URLSUBSTITUTION)?.getString()

    @OptIn(ExperimentalTime::class)
    fun startServer(
        request: StartServerRequest,
    ) : StartServerResponse {
        val serverPort = findFreePort(fromPort, untilPort)
        val runArgs = runCommand.split(Regex("\\s+")).filter {
            it.isNotEmpty()
        }.toMutableList()

        val scope = CoroutineScope(Dispatchers.Default + Job())

        val dateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
        val dirName = request.name?.filter { it.isLetterOrDigit() || it == '_' }
            ?: ("run-${dateTime.date.year}_${dateTime.date.month.number.toString().padStart(2, '0')}_" +
                    "${dateTime.date.day.toString().padStart(2, '0')}_" +
                    "${dateTime.time.hour}_${dateTime.time.minute}_${dateTime.second}")

        val cmdWorkspaceDir = File(workspaceBaseDir, dirName).also {
            it.mkdirs()
        }

        logger.info("TestServerRunner: port=$serverPort starting command $runCommand (workingDir=${cmdWorkspaceDir.absolutePath})")

        val process = ProcessBuilder(runArgs)
            .directory(cmdWorkspaceDir)
            .redirectOutput(ProcessBuilder.Redirect.PIPE)
            .redirectError(ProcessBuilder.Redirect.PIPE).also { pb ->
                pb.environment().apply {
                    put("TESTSERVER_WORKSPACE", cmdWorkspaceDir.absolutePath)
                    put("TESTSERVER_PORT", serverPort.toString())
                    putAll(envVariables)
                }
            }
            .start()

        val pidFile = File(cmdWorkspaceDir, "process.pid").also { file ->
            file.writer().use {
                it.write(process.pid().toString())
            }
        }

        logger.info("TestServerRunner: port=$serverPort process started PID=${process.pid()} (saved to ${pidFile.path}}")
        scope.launch {
            File(cmdWorkspaceDir, "stdout.log").outputStream().bufferedWriter().use { writer ->
                process.inputStream.bufferedReader().use { inStream ->
                    inStream.forEachLine { writer.write(it + "\n") }
                }
            }
        }

        scope.launch {
            File(cmdWorkspaceDir, "stderr.log").outputStream().bufferedWriter().use { writer ->
                process.errorStream.bufferedReader().use { inStream ->
                    inStream.forEachLine { writer.write(it) }
                }
            }
        }

        val serverUrl = if(urlSubstitution != null && urlSubstitution.isNotEmpty()) {
            Url(urlSubstitution.replace("_PORT_", serverPort.toString()))
        }else {
            URLBuilder(request.controlServerUrl).apply {
                port = serverPort
            }.build()
        }

        runningCmdMap[serverPort] = RunningCmd(
            port = serverPort,
            process = process,
            serverUrl = serverUrl,
        )

        if(request.waitForUrl != null) {
            okHttpClient.waitForUrl(
                url = serverUrl.toURI().resolve(request.waitForUrl).toString()
            )
        }

        return StartServerResponse(
            port = serverPort,
            url = serverUrl.toString(),
        )
    }

    fun stopServer(
        port: Int
    ) {
        logger.info("TestServerRunner: request to stop server on port=$port")

        runningCmdMap.remove(port)
    }

    fun stopAll() {
        val runningServerPorts = runningCmdMap.keys
        runningServerPorts.forEach {
            try {
                stopServer(it)
            }catch(t: Throwable) {
                println("WARNING: stopAll couldn't stop server on port $it: ${t.message}")
            }
        }
    }

    companion object {

        const val DEFAULT_BASEDIR = "./build/testservercontroller/base"

    }

}