package com.correx.core.kernel.orchestration

import com.correx.core.tools.process.ChildProcess
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import java.nio.file.Path

/**
 * Process-spawning [StaticAnalysisRunner]: splits each operator-configured command on whitespace
 * into argv (no shell), runs it in the workspace root with stderr merged into stdout, and returns
 * its exit code with the combined output. A spawn failure or timeout is reported as a non-zero exit
 * so the gate treats a hung or unrunnable check as a failure rather than silently passing.
 *
 * Commands come from the operator's workflow config (trusted, like prompt files), not from the LLM,
 * so they bypass the tool-approval pipeline by design. Mirrors the streaming/timeout shape of
 * ShellTool to avoid pipe-buffer deadlock on large output.
 */
class ProcessStaticAnalysisRunner(
    private val timeoutMs: Long = DEFAULT_TIMEOUT_MS,
) : StaticAnalysisRunner {

    override suspend fun run(workingDir: Path, command: String): StaticAnalysisRunResult =
        withContext(Dispatchers.IO) {
            val argv = command.trim().split(WHITESPACE).filter { it.isNotEmpty() }
            if (argv.isEmpty()) return@withContext StaticAnalysisRunResult(EXIT_NOT_RUN, "empty command")
            if (argv.first() == STATIC_FLOOR_COMMAND) return@withContext runStaticFloor(workingDir, argv)
            runProcess(workingDir, argv, command)
        }

    @Suppress("ReturnCount")
    private suspend fun runStaticFloor(workingDir: Path, argv: List<String>): StaticAnalysisRunResult {
        val paths = argv.dropWhile { it != "--" }.drop(1)
        if (paths.isEmpty()) return StaticAnalysisRunResult(0, "static floor skipped: no concrete files")
        val checks = paths.mapNotNull { path -> checkerFor(path)?.let { it + path } }
        if (checks.isEmpty()) {
            return StaticAnalysisRunResult(0, "static floor skipped: no resolvable checker for ${paths.joinToString()}")
        }
        val outputs = mutableListOf<String>()
        for (check in checks) {
            val result = runProcess(workingDir, check, check.joinToString(" "))
            outputs += "${check.joinToString(" ")}: ${result.output}".trim()
            if (result.exitCode != 0) return StaticAnalysisRunResult(result.exitCode, outputs.joinToString("\n"))
        }
        val skipped = paths.size - checks.size
        if (skipped > 0) outputs += "static floor skipped $skipped file(s) without a checker"
        return StaticAnalysisRunResult(0, outputs.joinToString("\n"))
    }

    private fun checkerFor(path: String): List<String>? = when (path.substringAfterLast('.', "").lowercase()) {
        "js", "mjs", "cjs" -> listOf("node", "--check")
        "py" -> listOf("python3", "-m", "py_compile")
        "sh", "bash" -> listOf("bash", "-n")
        "rb" -> listOf("ruby", "-c")
        else -> null
    }

    private suspend fun runProcess(
        workingDir: Path,
        argv: List<String>,
        displayCommand: String,
    ): StaticAnalysisRunResult {
            val process = runCatching {
                ChildProcess.builder(argv, workingDir.toFile()).redirectErrorStream(true).start()
            }.getOrElse {
                return StaticAnalysisRunResult(EXIT_NOT_RUN, "failed to start '$displayCommand': ${it.message}")
            }
            return runCatching {
                coroutineScope {
                    withTimeout(timeoutMs) {
                        val outputDeferred = async { process.inputStream.bufferedReader().use { it.readText() } }
                        val exit = process.waitFor()
                        StaticAnalysisRunResult(exit, outputDeferred.await())
                    }
                }
            }.getOrElse {
                process.destroyForcibly()
                val reason = if (it is TimeoutCancellationException) {
                    "static analysis '$displayCommand' timed out after ${timeoutMs}ms"
                } else {
                    it.message ?: "error running '$displayCommand'"
                }
                StaticAnalysisRunResult(EXIT_NOT_RUN, reason)
            }
    }

    private companion object {
        const val DEFAULT_TIMEOUT_MS = 300_000L
        const val EXIT_NOT_RUN = -1
        const val STATIC_FLOOR_COMMAND = "correx-static-floor"
        val WHITESPACE = Regex("\\s+")
    }
}
