package com.correx.apps.server.git

import com.correx.core.config.GitConfig
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.RunBranchPushedEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import java.nio.file.Path
import java.util.UUID
import java.util.concurrent.TimeUnit

private const val GIT_TIMEOUT_SECONDS = 30L

/**
 * Plain-Git transport for a server-owned checkout. The mutex intentionally spans a whole run:
 * checkout is process-global state, so parallel sessions cannot safely share one working tree.
 */
class GitRunBranchTransport(
    private val eventStore: EventStore,
    private val config: GitConfig,
) {
    private val checkoutLock = Mutex()

    suspend fun <T> onRunBranch(
        sessionId: SessionId,
        workspaceRoot: Path,
        block: suspend () -> T,
    ): T = checkoutLock.withLock {
        val prepared = prepare(sessionId, workspaceRoot)
        try {
            block()
        } finally {
            val terminalStage = eventStore.read(sessionId).lastOrNull { event ->
                event.payload is WorkflowCompletedEvent || event.payload is WorkflowFailedEvent
            }?.payload?.let { payload ->
                when (payload) {
                    is WorkflowCompletedEvent -> payload.terminalStageId.value
                    is WorkflowFailedEvent -> payload.stageId.value
                    else -> null
                }
            } ?: "unknown"
            pushTerminalBranch(sessionId, workspaceRoot, prepared, terminalStage)
        }
    }

    private suspend fun prepare(sessionId: SessionId, workspaceRoot: Path): PreparedBranch = withContext(Dispatchers.IO) {
        val branch = "run/${sessionId.value}"
        runGit(workspaceRoot, "fetch", config.remote, config.baseBranch)
        val baseRef = "${config.remote}/${config.baseBranch}"
        val baseSha = runGit(workspaceRoot, "rev-parse", baseRef).trim()
        runGit(workspaceRoot, "checkout", "-B", branch, baseRef)
        PreparedBranch(branch, baseSha)
    }

    private suspend fun pushTerminalBranch(
        sessionId: SessionId,
        workspaceRoot: Path,
        prepared: PreparedBranch,
        terminalStage: String,
    ) =
        withContext(Dispatchers.IO) {
            runGit(workspaceRoot, "add", "-A")
            if (runGitExitCode(workspaceRoot, "diff", "--cached", "--quiet") != 0) {
                val args = mutableListOf("commit", "-m", "correx run ${sessionId.value} terminal $terminalStage")
                if (config.author.isNotBlank()) args += "--author=${config.author}"
                runGit(workspaceRoot, *args.toTypedArray())
            }
            val headSha = runGit(workspaceRoot, "rev-parse", "HEAD").trim()
            runGit(workspaceRoot, "push", config.remote, "${prepared.branch}:${prepared.branch}")
            eventStore.append(
                NewEvent(
                    metadata = EventMetadata(
                        eventId = EventId(UUID.randomUUID().toString()),
                        sessionId = sessionId,
                        timestamp = Clock.System.now(),
                        schemaVersion = 1,
                        causationId = null,
                        correlationId = null,
                    ),
                    payload = RunBranchPushedEvent(sessionId, prepared.branch, prepared.baseSha, headSha),
                ),
            )
        }

    private fun runGit(root: Path, vararg args: String): String {
        val process = ProcessBuilder(listOf("git", "-C", root.toString()) + args)
            .redirectErrorStream(true)
            .start()
        val output = process.inputStream.bufferedReader().use { it.readText() }
        if (!process.waitFor(GIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
            process.destroyForcibly()
            error("git ${args.joinToString(" ")} timed out")
        }
        check(process.exitValue() == 0) { "git ${args.joinToString(" ")} failed: ${output.trim()}" }
        return output
    }

    private fun runGitExitCode(root: Path, vararg args: String): Int {
        val process = ProcessBuilder(listOf("git", "-C", root.toString()) + args)
            .redirectErrorStream(true)
            .start()
        process.inputStream.bufferedReader().use { it.readText() }
        if (!process.waitFor(GIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
            process.destroyForcibly()
            error("git ${args.joinToString(" ")} timed out")
        }
        return process.exitValue()
    }

    private data class PreparedBranch(val branch: String, val baseSha: String)
}
