package solutions.tretter.githugandroid

import android.os.Build
import android.content.Context
import java.io.File

class GitRepositoryRuntime private constructor(
    private val context: Context?,
    private val nativeGitOverride: File?,
    private val sandboxesRoot: File,
) {
    private companion object {
        const val SyntheticRemoteUrl = "remote"
    }

    constructor(context: Context) : this(
        context = context.applicationContext,
        nativeGitOverride = null,
        sandboxesRoot = File(context.applicationContext.filesDir, "githug-sandboxes"),
    )

    internal constructor(sandboxesRoot: File, nativeGitBinary: File) : this(
        context = null,
        nativeGitOverride = nativeGitBinary,
        sandboxesRoot = sandboxesRoot,
    )

    fun startupBanner(): String {
        return if (nativeGitBinary() != null) {
            "Welcome to GitHug Android. Native Git prototype ready."
        } else {
            "Welcome to GitHug Android. Native Git binary not bundled for this ABI yet; using in-memory fallback."
        }
    }

    fun prepareLevel(level: Level): RepoState {
        AppLog.d("GitRuntime", "Preparing level id=${level.id} title=${level.title}")
        val nativeGit = nativeGitBinary()
        if (nativeGit == null) {
            return level.setup()
        }

        val sandbox = sandboxDir(level)
        sandbox.deleteRecursively()
        sandbox.mkdirs()

        val desired = level.setup()
        desired.files.forEach { file ->
            File(sandbox, file.name).apply {
                parentFile?.mkdirs()
                writeText(file.content)
            }
        }

        val needsGit = desired.initialized || desired.files.any { it.staged || it.tracked } || desired.commits.isNotEmpty()
        if (needsGit) {
            val initResult = runGit(nativeGit, sandbox, listOf("init", "-b", desired.headBranch))
            if (initResult.exitCode != 0) {
                runGit(nativeGit, sandbox, listOf("init"))
                runGit(nativeGit, sandbox, listOf("checkout", "-B", desired.headBranch))
            }

            materializeNativeGitState(nativeGit, sandbox, desired, level.id)
        }

        return inspectSandbox(level)
    }

    fun execute(level: Level, currentRepo: RepoState, command: String): Pair<RepoState, List<String>> {
        AppLog.d("GitRuntime", "Executing command for level=${level.id}: $command")
        val nativeGit = nativeGitBinary()
        if (nativeGit == null) {
            return GitSandboxEngine.execute(currentRepo, command)
        }

        val sandbox = sandboxDir(level)
        if (!sandbox.exists()) {
            prepareLevel(level)
        }

        val sandboxRoot = sandbox.canonicalFile
        val workingDir = File(sandboxRoot, currentRepo.currentDir).canonicalFile
            .takeIf { it.path.startsWith(sandboxRoot.path) } ?: sandboxRoot

        val shellTokens = GitSandboxEngine.tokenizeShellCommand(command)
        val tokens = shellTokens.map { it.value }
        if (tokens.isEmpty()) return inspectSandbox(level).copy(currentDir = currentRepo.currentDir) to emptyList()
        val expandedTokens = if (tokens.firstOrNull() == "echo") {
            tokens
        } else {
            expandShellPathspecs(currentRepo, shellTokens)
        }

        executeSyntheticGitCommand(currentRepo, command, expandedTokens)?.let { return it }

        val result = when (expandedTokens.first()) {
            "git" -> currentRepo to runGit(nativeGit, workingDir, expandedTokens.drop(1)).outputLines
            "help", "?" -> currentRepo to commandReferenceLines()
            else -> executeHelperCommand(sandboxRoot, workingDir, currentRepo, expandedTokens)
        }

        val inspectedRepo = inspectSandbox(level).copy(currentDir = result.first.currentDir)
        return augmentObservedRepoFacts(currentRepo, inspectedRepo, expandedTokens) to result.second
    }

    fun completionCandidates(level: Level, currentRepo: RepoState, directoriesOnly: Boolean): List<String> {
        val nativeGit = nativeGitBinary()
        val sandbox = sandboxDir(level)
        if (nativeGit == null || !sandbox.exists()) {
            return if (directoriesOnly) {
                directoryCompletionCandidates(currentRepo)
            } else {
                fileCompletionCandidates(currentRepo)
            }
        }

        val sandboxRoot = sandbox.canonicalFile
        val workingDir = File(sandboxRoot, currentRepo.currentDir).canonicalFile
            .takeIf { it.path == sandboxRoot.path || it.path.startsWith(sandboxRoot.path + File.separator) }
            ?: sandboxRoot
        if (!workingDir.exists() || !workingDir.isDirectory) return emptyList()

        return workingDir.walkTopDown()
            .drop(1)
            .filter { file -> !directoriesOnly || file.isDirectory }
            .map { file ->
                val relativePath = file.relativeTo(workingDir).path
                if (file.isDirectory) "$relativePath/" else relativePath
            }
            .filter { it.isNotBlank() }
            .toList()
    }

    fun commandReferenceLines(): List<String> {
        return buildList {
            addAll(GitSandboxEngine.commandReferenceLines())
            add("Native Git prototype:")
            add("  binary path: nativeLibraryDir/libgit.so")
            add("  selected ABI: ${Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"}")
            add("  helper commands: ls/dir, pwd, cat, touch, mkdir/md, cd.., rm/del, echo")
            add("  visual editors: vi, nano, emacs, ed, ex")
            add("  git help <command>")
        }
    }

    fun gitManPage(level: Level, currentRepo: RepoState, topic: String): String {
        if (topic == "ignore") {
            return fallbackManPage(topic)
        }

        val nativeGit = nativeGitBinary()
        if (nativeGit == null) {
            return fallbackManPage(topic)
        }

        val sandboxRoot = sandboxDir(level).canonicalFile
        val workingDir = File(sandboxRoot, currentRepo.currentDir).canonicalFile
            .takeIf { it.path.startsWith(sandboxRoot.path) } ?: sandboxRoot
        val result = runGit(nativeGit, workingDir, listOf(topic, "-h"))
        return result.outputLines
            .filterNot { it.contains("no man viewer", ignoreCase = true) }
            .joinToString("\n")
            .ifBlank { fallbackManPage(topic) }
    }

    fun readEditorFile(level: Level, currentRepo: RepoState, path: String): Pair<String, List<String>> {
        val nativeGit = nativeGitBinary()
        if (nativeGit == null) {
            return currentRepo.files.find { it.name == path }?.content.orEmpty() to emptyList()
        }

        val sandboxRoot = sandboxDir(level).canonicalFile
        val workingDir = File(sandboxRoot, currentRepo.currentDir).canonicalFile
            .takeIf { it.path.startsWith(sandboxRoot.path) } ?: sandboxRoot
        val file = File(workingDir, path).canonicalFile
        if (!file.path.startsWith(sandboxRoot.path)) {
            return "" to listOf("editor: $path: Permission denied")
        }
        if (file.exists() && file.isDirectory) {
            return "" to listOf("editor: $path: Is a directory")
        }

        return if (file.exists()) file.readText() to emptyList() else "" to emptyList()
    }

    fun writeEditorFile(level: Level, currentRepo: RepoState, path: String, content: String): Pair<RepoState, List<String>> {
        val nativeGit = nativeGitBinary()
        if (nativeGit == null) {
            val updatedFiles = currentRepo.files.toMutableList()
            val index = updatedFiles.indexOfFirst { it.name == path }
            if (index == -1) {
                updatedFiles += GitFile(name = path, content = content)
            } else {
                updatedFiles[index] = updatedFiles[index].copy(content = content)
            }
            return currentRepo.copy(files = updatedFiles) to listOf("Saved $path")
        }

        val sandboxRoot = sandboxDir(level).canonicalFile
        val workingDir = File(sandboxRoot, currentRepo.currentDir).canonicalFile
            .takeIf { it.path.startsWith(sandboxRoot.path) } ?: sandboxRoot
        val file = File(workingDir, path).canonicalFile
        if (!file.path.startsWith(sandboxRoot.path)) {
            return currentRepo to listOf("editor: $path: Permission denied")
        }
        if (file.exists() && file.isDirectory) {
            return currentRepo to listOf("editor: $path: Is a directory")
        }

        file.parentFile?.mkdirs()
        file.writeText(content)
        return inspectSandbox(level).copy(currentDir = currentRepo.currentDir) to listOf("Saved $path")
    }

    fun executeGitEditorCommand(
        level: Level,
        currentRepo: RepoState,
        invocation: GitEditorInvocation,
        message: String,
    ): Pair<RepoState, List<String>> {
        val nativeGit = nativeGitBinary()
        if (nativeGit == null) {
            return GitSandboxEngine.execute(currentRepo, invocation.command.withFallbackMessage(invocation.kind, message))
        }

        val sandboxRoot = sandboxDir(level).canonicalFile
        if (!sandboxRoot.exists()) {
            prepareLevel(level)
        }
        val workingDir = File(sandboxRoot, currentRepo.currentDir).canonicalFile
            .takeIf { it.path.startsWith(sandboxRoot.path) } ?: sandboxRoot

        val messageFile = File(sandboxRoot, ".git/GITHUG_ANDROID_EDITMSG").apply {
            parentFile?.mkdirs()
            writeText(message)
        }
        val arguments = GitSandboxEngine.tokenizeCommand(invocation.command)
            .drop(1)
            .toMutableList()
            .apply { addAll(listOf("-F", messageFile.absolutePath)) }
        val result = runGit(nativeGit, workingDir, arguments)
        messageFile.delete()

        return inspectSandbox(level).copy(currentDir = currentRepo.currentDir) to result.outputLines
    }

    private fun executeHelperCommand(sandboxRoot: File, workingDir: File, currentRepo: RepoState, tokens: List<String>): Pair<RepoState, List<String>> {
        return when (tokens.first()) {
            "ls", "dir" -> currentRepo to workingDir.listFiles()
                ?.sortedBy { it.name }
                ?.map { it.name }
                .orEmpty()
            "pwd" -> currentRepo to listOf(
                "/sandbox" + if (workingDir == sandboxRoot) "" else "/${workingDir.relativeTo(sandboxRoot).path}"
            )
            "cat" -> {
                val target = tokens.getOrNull(1) ?: return currentRepo to listOf("usage: cat <file>")
                val file = File(workingDir, target)
                if (!file.exists() || file.isDirectory) currentRepo to listOf("cat: $target: No such file")
                else currentRepo to file.readLines().ifEmpty { listOf("") }
            }
            "touch" -> {
                val target = tokens.getOrNull(1) ?: return currentRepo to listOf("usage: touch <file>")
                val file = File(workingDir, target)
                if (file.exists()) {
                    currentRepo to listOf("$target already exists")
                } else {
                    file.parentFile?.mkdirs()
                    file.writeText("")
                    currentRepo to emptyList()
                }
            }
            "mkdir", "md" -> {
                val target = tokens.getOrNull(1) ?: return currentRepo to listOf("usage: mkdir <dir>")
                val dir = File(workingDir, target)
                if (dir.exists()) currentRepo to listOf("mkdir: $target: File exists") else {
                    dir.mkdirs()
                    currentRepo to emptyList()
                }
            }
            "cd", "cd.." -> {
                val target = if (tokens.first() == "cd..") ".." else tokens.getOrNull(1)
                    ?: return currentRepo to listOf("usage: cd <dir>")
                val dir = File(workingDir, target).canonicalFile
                when {
                    !dir.exists() -> currentRepo to listOf("cd: $target: does not exist")
                    !dir.isDirectory -> currentRepo to listOf("cd: $target: Not a directory")
                    !dir.path.startsWith(sandboxRoot.path) -> currentRepo to listOf("cd: $target: Permission denied")
                    else -> {
                        val relativeDir = sandboxRoot.toPath().relativize(dir.toPath()).toString().ifEmpty { "." }
                        currentRepo.copy(currentDir = relativeDir) to emptyList()
                    }
                }
            }
            "rm", "del" -> {
                val targets = tokens.drop(1)
                if (targets.isEmpty()) return currentRepo to listOf("usage: rm <path>")
                val errors = targets.mapNotNull { target ->
                    val file = File(workingDir, target)
                    if (!file.exists()) {
                        "${tokens.first()}: $target: No such file or directory"
                    } else {
                        file.deleteRecursively()
                        null
                    }
                }
                currentRepo to errors
            }
            "echo" -> executeEcho(workingDir, currentRepo, tokens)
            else -> currentRepo to listOf("Command not supported in prototype runtime. Try a git command or helper command.")
        }
    }

    private fun String.withFallbackMessage(kind: GitEditorCommandKind, message: String): String {
        val escaped = message.replace("\\", "\\\\").replace("\"", "\\\"").lineSequence().firstOrNull().orEmpty()
        return when (kind) {
            GitEditorCommandKind.COMMIT_MESSAGE -> "$this -m \"$escaped\""
            GitEditorCommandKind.TAG_MESSAGE -> "$this -m \"$escaped\""
        }
    }

    private fun fallbackManPage(topic: String): String {
        val body = when (topic) {
            "tag" -> """
                NAME
                    git-tag - Create, list, delete or verify a tag object

                SYNOPSIS
                    git tag <tagname>
                    git tag -d <tagname>
                    git tag

                DESCRIPTION
                    Tags name specific points in history. In GitHug Android,
                    tag levels usually expect you to create or reference a tag
                    by name.

                EXAMPLES
                    git tag new_tag
                    git tag v1.2
            """.trimIndent()
            "branch" -> """
                NAME
                    git-branch - List, create, or delete branches

                SYNOPSIS
                    git branch <branchname>
                    git branch -d <branchname>

                EXAMPLES
                    git branch test_code
                    git branch -d delete_me
            """.trimIndent()
            "checkout" -> """
                NAME
                    git-checkout - Switch branches or restore files

                SYNOPSIS
                    git checkout <branch>
                    git checkout -b <branch>
                    git checkout -- <file>

                EXAMPLES
                    git checkout -b my_branch
                    git checkout -- config.rb
            """.trimIndent()
            "add" -> """
                NAME
                    git-add - Add file contents to the index

                SYNOPSIS
                    git add <path>
                    git add .
                    git add -p <path>
            """.trimIndent()
            "commit" -> """
                NAME
                    git-commit - Record changes to the repository

                SYNOPSIS
                    git commit -m <message>
                    git commit --amend
            """.trimIndent()
            "ignore" -> """
                NAME
                    gitignore - Specifies intentionally untracked files to ignore

                SYNOPSIS
                    $'GIT_DIR/info/exclude', .gitignore

                DESCRIPTION
                    A gitignore file specifies intentionally untracked files
                    that Git should ignore. Each line contains a pattern.

                    Blank lines are ignored. Lines beginning with # are comments.
                    An optional ! prefix negates a pattern and re-includes a path.
                    A pattern ending with / matches directories.

                EXAMPLES
                    *.swp
                    *.a
                    !lib.a
            """.trimIndent()
            else -> """
                NAME
                    git-$topic

                DESCRIPTION
                    No bundled fallback manpage is available for git $topic.
                    Try the command suggestions or use git help for another command.
            """.trimIndent()
        }
        return body
    }

    private fun materializeNativeGitState(nativeGit: File, sandbox: File, desired: RepoState, levelId: String) {
        if (levelId == "push") {
            materializeNativePushLevel(nativeGit, sandbox)
            return
        }
        if (materializeNativeLevelFixture(levelId, sandbox) { directory, arguments ->
                runGit(nativeGit, directory, arguments).exitCode
            }
        ) {
            return
        }

        desired.config.forEach { (key, value) ->
            runGit(nativeGit, sandbox, listOf("config", key, value))
        }

        val trackedFiles = desired.files.filter { it.tracked }
        val desiredCommitCount = maxOf(
            desired.commits.size,
            desired.branches.values.maxOrNull() ?: 0,
            if (desired.tags.isNotEmpty()) 1 else 0,
        )
        if (desiredCommitCount > 0) {
            repeat(desiredCommitCount) { index ->
                filesForSetupCommit(trackedFiles, index, desiredCommitCount).forEach { file ->
                    runGit(nativeGit, sandbox, listOf("add", file.name))
                }
                val message = desired.commits.getOrNull(index)?.message ?: "Setup commit ${index + 1}"
                runGit(nativeGit, sandbox, listOf("commit", "--allow-empty", "-m", message))
            }

            desired.branches
                .keys
                .filterNot { it == desired.headBranch }
                .forEach { branch -> runGit(nativeGit, sandbox, listOf("branch", branch)) }
            if (desired.branches.containsKey(desired.headBranch)) {
                runGit(nativeGit, sandbox, listOf("checkout", desired.headBranch))
            }
        }

        desired.tags.forEach { tag -> runGit(nativeGit, sandbox, listOf("tag", tag)) }
        desired.remotes.forEach { (name, url) ->
            val materializedUrl = if (url == SyntheticRemoteUrl) {
                materializeSyntheticRemote(nativeGit, sandbox, name)
            } else {
                url
            }
            runGit(nativeGit, sandbox, listOf("remote", "add", name, materializedUrl))
            if (url == SyntheticRemoteUrl) {
                runGit(nativeGit, sandbox, listOf("fetch", name))
                runGit(nativeGit, sandbox, listOf("branch", "--set-upstream-to=$name/master", desired.headBranch))
            }
        }

        val stagedTargets = desired.files.filter { it.staged }.map { it.name }
        if (stagedTargets.isNotEmpty()) {
            runGit(nativeGit, sandbox, listOf("add") + stagedTargets)
        }
        desired.files.filter { it.deleted }.forEach { file ->
            File(sandbox, file.name).delete()
        }
    }

    private fun materializeNativePushLevel(nativeGit: File, sandbox: File) {
        sandbox.listFiles()
            ?.filterNot { it.name == ".git" }
            ?.forEach { it.deleteRecursively() }

        fun writeFile(name: String, content: String = "$name\n") {
            File(sandbox, name).apply {
                parentFile?.mkdirs()
                writeText(content)
            }
        }

        fun commitIn(directory: File, message: String, vararg paths: String) {
            runGit(nativeGit, directory, listOf("add") + paths)
            runGit(nativeGit, directory, listOf("commit", "-m", message))
        }

        writeFile("file1")
        commitIn(sandbox, "First commit", "file1")
        writeFile("file2")
        commitIn(sandbox, "Second commit", "file2")

        val parent = sandbox.parentFile ?: sandbox
        val remoteDir = File(parent, "${sandbox.name}-origin.git")
        val remoteWorkTree = File(parent, "${sandbox.name}-origin-work")
        remoteDir.deleteRecursively()
        remoteWorkTree.deleteRecursively()
        remoteDir.mkdirs()
        remoteWorkTree.mkdirs()
        runGit(nativeGit, remoteDir, listOf("init", "--bare", "-b", "master"))
        runGit(nativeGit, sandbox, listOf("remote", "add", "push-setup-origin", remoteDir.absolutePath))
        runGit(nativeGit, sandbox, listOf("push", "push-setup-origin", "master"))
        runGit(nativeGit, sandbox, listOf("remote", "remove", "push-setup-origin"))

        fun runRemoteGit(arguments: List<String>): ProcessExecutionResult {
            return runGit(
                nativeGit,
                sandbox,
                listOf("--git-dir", remoteDir.absolutePath, "--work-tree", remoteWorkTree.absolutePath) + arguments,
            )
        }

        runRemoteGit(listOf("checkout", "-f", "master"))
        File(remoteWorkTree, "file4").writeText("file4\n")
        runRemoteGit(listOf("add", "file4"))
        runRemoteGit(listOf("commit", "-m", "Fourth commit"))

        writeFile("file3")
        commitIn(sandbox, "Third commit", "file3")
        runGit(nativeGit, sandbox, listOf("remote", "add", "origin", remoteDir.absolutePath))
        runGit(nativeGit, sandbox, listOf("fetch", "origin"))
        runGit(nativeGit, sandbox, listOf("branch", "--set-upstream-to=origin/master", "master"))
        runGit(nativeGit, sandbox, listOf("config", "rebase.autoStash", "true"))
        File(sandbox, "file3").appendText("local worktree change\n")
    }

    private fun filesForSetupCommit(files: List<GitFile>, index: Int, commitCount: Int): List<GitFile> {
        if (files.isEmpty()) return emptyList()
        if (commitCount <= 1) return files
        if (index == 0) return files.take(1)
        if (index == commitCount - 1) return files.drop(index)
        return files.getOrNull(index)?.let { listOf(it) }.orEmpty()
    }

    private fun materializeSyntheticRemote(nativeGit: File, sandbox: File, remoteName: String): String {
        val remoteDir = File(sandbox.parentFile, "${sandbox.name}-$remoteName.git")
        remoteDir.deleteRecursively()
        val cloneResult = runGit(nativeGit, sandbox.parentFile ?: sandbox, listOf("clone", "--bare", sandbox.absolutePath, remoteDir.absolutePath))
        if (cloneResult.exitCode != 0) {
            remoteDir.mkdirs()
            runGit(nativeGit, remoteDir, listOf("init", "--bare"))
        }
        return remoteDir.absolutePath
    }

    private fun executeSyntheticGitCommand(
        currentRepo: RepoState,
        command: String,
        tokens: List<String>,
    ): Pair<RepoState, List<String>>? {
        if (tokens.firstOrNull() != "git") return null
        val gitCommand = tokens.getOrNull(1) ?: return null
        if (gitCommand == "clone" && tokens.getOrNull(2)?.startsWith("https://github.com/Gazler/cloneme") == true) {
            val target = tokens.getOrNull(3) ?: "cloneme"
            return currentRepo.copy(
                files = currentRepo.files + GitFile("$target/README", tracked = true),
            ) to listOf("Cloned ${tokens[2]} into $target")
        }
        val shouldUseSandboxSemantics = when (gitCommand) {
            "add" -> tokens.any { it == "-p" || it == "--patch" }
            "rebase" -> "-i" in tokens || "--onto" in tokens
            "merge" -> "--squash" in tokens || tokens.lastOrNull() == "mybranch" || tokens.lastOrNull() == "feature"
            "cherry-pick", "revert", "stash" -> true
            "checkout" -> tokens.any { it == "file3" } || tokens.takeLast(2) == listOf("--", "config.rb")
            "submodule" -> tokens.getOrNull(2) == "add"
            "commit" -> "merge-squash" in currentRepo.maintenanceActions || tokens.any { it == "--date" || it.startsWith("--date=") }
            else -> false
        }
        if (!shouldUseSandboxSemantics) return null

        val (updatedRepo, output) = GitSandboxEngine.execute(currentRepo, command)
        return updatedRepo to output
    }

    private fun executeEcho(workingDir: File, currentRepo: RepoState, tokens: List<String>): Pair<RepoState, List<String>> {
        val redirectIndex = tokens.indexOfFirst { it == ">" || it == ">>" }
        if (redirectIndex == -1 || redirectIndex == tokens.lastIndex) {
            return currentRepo to listOf(tokens.drop(1).joinToString(" "))
        }

        val file = File(workingDir, tokens[redirectIndex + 1])
        file.parentFile?.mkdirs()
        val content = tokens.subList(1, redirectIndex).joinToString(" ")
        if (tokens[redirectIndex] == ">>") {
            if (file.exists() && file.length() > 0L) {
                file.appendText("\n$content")
            } else {
                file.writeText(content)
            }
        } else {
            file.writeText(content)
        }
        return currentRepo to emptyList()
    }

    private fun inspectSandbox(level: Level): RepoState {
        val sandbox = sandboxDir(level)
        val nativeGit = nativeGitBinary()
        val filesOnDisk = sandbox.walkTopDown()
            .filter { it.isFile && !it.relativeTo(sandbox).path.startsWith(".git/") }
            .orEmpty()
            .toList()

        if (nativeGit == null || !File(sandbox, ".git").exists()) {
            return RepoState(
                initialized = File(sandbox, ".git").exists(),
                files = filesOnDisk.map { GitFile(name = it.name, content = it.readText()) },
            )
        }

        val statusResult = runGit(nativeGit, sandbox, listOf("status", "--porcelain"))
        val statusMap = mutableMapOf<String, Pair<Boolean, Boolean>>()
        val deletedStatusPaths = mutableSetOf<String>()
        statusResult.outputLines.forEach { line ->
            if (line.length < 4) return@forEach
            val x = line[0]
            val y = line[1]
            val path = line.substring(3).trim()
            val staged = x != ' ' && x != '?'
            val tracked = x != '?' || y != '?'
            statusMap[path] = staged to tracked
            if (x == 'D' || y == 'D') {
                deletedStatusPaths += path
            }
        }

        val logResult = runGit(nativeGit, sandbox, listOf("log", "--pretty=format:%h\t%s"))
        val branchResult = runGit(nativeGit, sandbox, listOf("branch", "--list"))
        val remoteBranchResult = runGit(nativeGit, sandbox, listOf("branch", "-r", "--list"))
        val tagResult = runGit(nativeGit, sandbox, listOf("tag", "--list"))
        val remoteResult = runGit(nativeGit, sandbox, listOf("remote", "-v"))
        val headResult = runGit(nativeGit, sandbox, listOf("branch", "--show-current"))
        val exactTagResult = runGit(nativeGit, sandbox, listOf("describe", "--tags", "--exact-match"))
        val userNameResult = runGit(nativeGit, sandbox, listOf("config", "--get", "user.name"))
        val userEmailResult = runGit(nativeGit, sandbox, listOf("config", "--get", "user.email"))
        val config = buildMap {
            userNameResult.outputLines.firstOrNull()
                ?.takeIf { userNameResult.exitCode == 0 && it.isNotBlank() }
                ?.let { put("user.name", it) }
            userEmailResult.outputLines.firstOrNull()
                ?.takeIf { userEmailResult.exitCode == 0 && it.isNotBlank() }
                ?.let { put("user.email", it) }
        }
        AppLog.d(
            "GitRuntime",
            "inspectSandbox level=${level.id} config=$config user.name.exit=${userNameResult.exitCode} user.name.output=${userNameResult.outputLines} user.email.exit=${userEmailResult.exitCode} user.email.output=${userEmailResult.outputLines}",
        )
        val commits = if (logResult.exitCode == 0) {
            logResult.outputLines.filter { it.isNotBlank() }.mapNotNull { line ->
                val parts = line.split('\t', limit = 2)
                if (parts.isEmpty()) null else CommitNode(parts[0], parts.getOrElse(1) { "" })
            }
        } else {
            emptyList()
        }

        val branches = branchResult.outputLines
            .map { it.removePrefix("*").trim() }
            .filter { it.isNotBlank() && !it.startsWith("(") }
            .associateWith { branch ->
                runGit(nativeGit, sandbox, listOf("rev-list", "--count", branch))
                    .outputLines
                    .firstOrNull()
                    ?.toIntOrNull()
                    ?: 0
            }
        val headBranch = headResult.outputLines.firstOrNull()
            ?.ifBlank { null }
            ?: exactTagResult.outputLines.firstOrNull()
                ?.takeIf { exactTagResult.exitCode == 0 && it.isNotBlank() }
                ?.let { "tags/$it" }
            ?: "DETACHED"

        return RepoState(
            initialized = true,
            files = filesOnDisk.map { file ->
                val relativePath = file.relativeTo(sandbox).path
                val (staged, tracked) = statusMap[relativePath] ?: (false to true)
                GitFile(
                    name = relativePath,
                    content = file.readText(),
                    staged = staged,
                    tracked = tracked,
                )
            } + deletedStatusPaths
                .filterNot { deletedPath -> filesOnDisk.any { it.relativeTo(sandbox).path == deletedPath } }
                .map { deletedPath ->
                    val (staged, tracked) = statusMap[deletedPath] ?: (false to true)
                    GitFile(
                        name = deletedPath,
                        staged = staged,
                        tracked = tracked,
                        deleted = true,
                    )
            },
            commits = commits,
            headBranch = headBranch,
            branches = branches,
            tags = tagResult.outputLines.filter { it.isNotBlank() },
            remotes = remoteResult.outputLines.mapNotNull { line ->
                val parts = line.trim().split(Regex("\\s+"))
                if (parts.size >= 2) parts[0] to parts[1] else null
            }.toMap(),
            config = config,
            fetchedBranches = remoteBranchResult.outputLines
                .map { it.removePrefix("*").trim() }
                .filter { it.isNotBlank() && " -> " !in it }
                .toSet(),
        )
    }

    private fun nativeGitBinary(): File? {
        return nativeGitOverride ?: packagedNativeGitBinary()
    }

    private fun packagedNativeGitBinary(): File? {
        val nativeLibraryDir = context?.applicationInfo?.nativeLibraryDir ?: return null
        val candidate = File(nativeLibraryDir, "libgit.so")
        return candidate.takeIf { it.exists() && it.canExecute() }
    }

    private fun sandboxDir(level: Level): File = File(sandboxesRoot, level.id)

    private fun expandShellPathspecs(repo: RepoState, tokens: List<GitSandboxEngine.ShellToken>): List<String> {
        if (tokens.size <= 1) return tokens.map { it.value }
        return listOf(tokens.first().value) + GitSandboxEngine.expandPathspecTokens(repo, tokens.drop(1))
    }

    private fun augmentObservedRepoFacts(previousRepo: RepoState, inspectedRepo: RepoState, tokens: List<String>): RepoState {
        if (tokens.firstOrNull() != "git") return inspectedRepo

        return when (tokens.getOrNull(1)) {
            "stash" -> inspectedRepo.copy(
                stashes = mergeDistinct(previousRepo.stashes, inspectedRepo.stashes + "stash@{${previousRepo.stashes.size}}"),
            )
            "fetch" -> {
                val remote = tokens.getOrNull(2)?.takeIf { !it.startsWith("-") } ?: "origin"
                inspectedRepo.copy(
                    fetchedBranches = inspectedRepo.fetchedBranches + previousRepo.fetchedBranches + "$remote/master" + "$remote/feature_branch",
                )
            }
            "pull" -> {
                val remote = tokens.getOrNull(2)?.takeIf { !it.startsWith("-") } ?: "origin"
                val branch = tokens.drop(2).lastOrNull()?.takeIf { !it.startsWith("-") && it != remote } ?: inspectedRepo.headBranch
                inspectedRepo.copy(
                    fetchedBranches = inspectedRepo.fetchedBranches + previousRepo.fetchedBranches + "$remote/$branch",
                )
            }
            "push" -> {
                val remote = tokens.drop(2).firstOrNull { !it.startsWith("-") } ?: "origin"
                val explicitBranches = tokens
                    .drop(2)
                    .dropWhile { it.startsWith("-") }
                    .drop(1)
                    .filter { !it.startsWith("-") }
                val pushedBranches = when {
                    tokens.any { it == "--all" } -> inspectedRepo.branches.keys.map { "$remote/$it" }
                    explicitBranches.isNotEmpty() -> explicitBranches.map { branch -> "$remote/${branch.substringAfterLast(':')}" }
                    else -> listOf("$remote/${inspectedRepo.headBranch}")
                }
                val pushedTags = if (tokens.any { it == "--tags" || it == "--follow-tags" }) inspectedRepo.tags.toSet() else emptySet()
                inspectedRepo.copy(
                    pushedBranches = inspectedRepo.pushedBranches + previousRepo.pushedBranches + pushedBranches,
                    pushedTags = inspectedRepo.pushedTags + previousRepo.pushedTags + pushedTags,
                )
            }
            "submodule" -> {
                if (tokens.getOrNull(2) == "add") {
                    val url = tokens.getOrNull(3)
                    val path = tokens.getOrNull(4)?.trimEnd('/')
                    if (url != null && path != null) {
                        inspectedRepo.copy(submodules = previousRepo.submodules + inspectedRepo.submodules + (path to url))
                    } else {
                        inspectedRepo
                    }
                } else {
                    inspectedRepo
                }
            }
            "repack" -> inspectedRepo.copy(
                maintenanceActions = inspectedRepo.maintenanceActions + previousRepo.maintenanceActions + "repack",
            )
            "merge" -> inspectedRepo.copy(
                maintenanceActions = if ("--squash" in tokens) {
                    inspectedRepo.maintenanceActions + previousRepo.maintenanceActions + "merge-squash"
                } else {
                    inspectedRepo.maintenanceActions + previousRepo.maintenanceActions
                },
            )
            else -> inspectedRepo.copy(
                stashes = mergeDistinct(previousRepo.stashes, inspectedRepo.stashes),
                fetchedBranches = previousRepo.fetchedBranches + inspectedRepo.fetchedBranches,
                pushedBranches = previousRepo.pushedBranches + inspectedRepo.pushedBranches,
                pushedTags = previousRepo.pushedTags + inspectedRepo.pushedTags,
                submodules = previousRepo.submodules + inspectedRepo.submodules,
                maintenanceActions = previousRepo.maintenanceActions + inspectedRepo.maintenanceActions,
            )
        }
    }

    private fun mergeDistinct(first: List<String>, second: List<String>): List<String> {
        return (first + second).distinct()
    }

    private fun runGit(binary: File, workingDir: File, arguments: List<String>): ProcessExecutionResult {
        return runProcess(binary, workingDir, arguments)
    }

    private fun runProcess(binary: File, workingDir: File, arguments: List<String>): ProcessExecutionResult {
        return try {
            val process = ProcessBuilder(listOf(binary.absolutePath) + arguments)
                .directory(workingDir)
                .redirectErrorStream(true)
                .apply {
                    environment()["HOME"] = workingDir.absolutePath
                    environment()["GIT_CONFIG_NOSYSTEM"] = "1"
                    environment()["GIT_AUTHOR_NAME"] = "GitHug"
                    environment()["GIT_AUTHOR_EMAIL"] = "githug@example.com"
                    environment()["GIT_COMMITTER_NAME"] = "GitHug"
                    environment()["GIT_COMMITTER_EMAIL"] = "githug@example.com"
                    environment()["LC_ALL"] = "C"
                }
                .start()

            val output = process.inputStream.bufferedReader().readLines()
            val exit = process.waitFor()
            ProcessExecutionResult(exitCode = exit, outputLines = output)
        } catch (error: Exception) {
            ProcessExecutionResult(exitCode = -1, outputLines = listOf("Native Git execution failed: ${error.message ?: error::class.java.simpleName}"))
        }
    }
}

private data class ProcessExecutionResult(
    val exitCode: Int,
    val outputLines: List<String>,
)
