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 val processRunner = GitProcessRunner(context, sandboxesRoot)
    private val helperCommands = GitHelperCommands(processRunner)
    private val levelMaterializer = GitLevelMaterializer(::runGit)

    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 isNativeGitAvailable(): Boolean = nativeGitBinary() != null

    fun unavailableMessage(): String {
        val selectedAbi = Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"
        val nativeLibraryDir = context?.applicationInfo?.nativeLibraryDir ?: "unknown"
        return buildString {
            append("GitHug Android cannot start because this build does not include a native Git binary for this device ABI.")
            append("\n\nCurrent device ABI: ")
            append(selectedAbi)
            append("\nSupported device ABIs: ")
            append(Build.SUPPORTED_ABIS.joinToString(", ").ifBlank { "unknown" })
            append("\nSupported 64-bit ABIs: ")
            append(Build.SUPPORTED_64_BIT_ABIS.joinToString(", ").ifBlank { "none" })
            append("\nSupported 32-bit ABIs: ")
            append(Build.SUPPORTED_32_BIT_ABIS.joinToString(", ").ifBlank { "none" })
            append("\nPlatform: Android ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})")
            append("\nDevice: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE}; ${Build.HARDWARE})")
            append("\nExpected binary: $nativeLibraryDir/libgit.so")
            append("\n\nPlease report this information to the developer so support can be added for this device/platform.")
            append("\nInstall a build that bundles the cross-compiled Git binary for this device.")
        }
    }

    fun startupBanner(): String {
        requireNativeGit()
        return "Welcome to GitHug Android. Native Git ready."
    }

    fun prepareLevel(level: Level): RepoState {
        AppLog.d("GitRuntime", "Preparing level id=${level.id} title=${level.title}")
        val nativeGit = requireNativeGit()

        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))
            }

            levelMaterializer.materialize(nativeGit, sandbox, desired, level)
        }

        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 = requireNativeGit()

        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 invocation = parseEnvironmentPrefixedCommand(shellTokens)
        val tokens = invocation.command.map { it.value }
        if (tokens.isEmpty()) return inspectSandbox(level).copy(currentDir = currentRepo.currentDir) to emptyList()
        if (currentRepo.interactiveAddSession != null) {
            val (updatedRepo, output) = GitSandboxEngine.execute(currentRepo, command)
            val newlyStagedPaths = updatedRepo.files.filter { updatedFile ->
                updatedFile.staged && currentRepo.files.firstOrNull { it.name == updatedFile.name }?.staged != true
            }.map { it.name }
            if (newlyStagedPaths.isNotEmpty()) {
                runGit(nativeGit, workingDir, listOf("add") + newlyStagedPaths)
            }
            val inspectedRepo = inspectSandbox(level).copy(
                currentDir = updatedRepo.currentDir,
                interactiveAddSession = updatedRepo.interactiveAddSession,
            )
            return augmentObservedRepoFacts(updatedRepo, inspectedRepo, tokens) to output
        }
        val expandedTokens = if (tokens.firstOrNull() == "echo") {
            tokens
        } else {
            expandShellPathspecs(currentRepo, invocation.command)
        }
            .normalizeGitStageAlias()
            .normalizeGitBisectRunScriptShortcut()

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

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

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

    fun completionCandidates(level: Level, currentRepo: RepoState, directoriesOnly: Boolean): List<String> {
        requireNativeGit()
        val sandbox = sandboxDir(level)
        if (!sandbox.exists()) return emptyList()

        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()

        val relativeCandidates = 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()

        return relativeCandidates + relativeCandidates.map { "./$it" }
    }

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

    fun gitManPage(level: Level, currentRepo: RepoState, topic: String): String {
        bundledGitManPage(context, topic)?.let { return it }

        val nativeGit = requireNativeGit()

        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 { placeholderGitManPage(context, topic) }
    }

    fun readEditorFile(level: Level, currentRepo: RepoState, path: String): Pair<String, List<String>> {
        requireNativeGit()

        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>> {
        requireNativeGit()

        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 gitEditorInitialContent(level: Level, currentRepo: RepoState, invocation: GitEditorInvocation): String {
        if (invocation.kind != GitEditorCommandKind.REBASE_TODO) return invocation.initialContent

        val nativeGit = requireNativeGit()
        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 target = rebaseTarget(invocation.command) ?: return invocation.initialContent
        val result = runGit(nativeGit, workingDir, listOf("log", "--reverse", "--format=%h %s", "$target..HEAD"))
        if (result.exitCode != 0 || result.outputLines.isEmpty()) return invocation.initialContent

        return buildString {
            result.outputLines
                .filter { it.isNotBlank() }
                .forEach { line -> appendLine("pick $line") }
            appendLine()
            appendLine("# Rebase $target..HEAD onto $target")
            appendLine("#")
            appendLine("# Commands:")
            appendLine("# p, pick <commit> = use commit")
            appendLine("# r, reword <commit> = use commit, but edit the commit message")
            appendLine("# e, edit <commit> = use commit, but stop for amending")
            appendLine("# s, squash <commit> = use commit, but meld into previous commit")
            appendLine("# f, fixup [-C | -c] <commit> = like squash but keep only the previous commit's log message")
            appendLine("# d, drop <commit> = remove commit")
            appendLine("#")
            appendLine("# These lines can be re-ordered; they are executed from top to bottom.")
        }
    }

    fun executeGitEditorCommand(
        level: Level,
        currentRepo: RepoState,
        invocation: GitEditorInvocation,
        message: String,
    ): Pair<RepoState, List<String>> {
        val nativeGit = requireNativeGit()

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

        if (invocation.kind == GitEditorCommandKind.REBASE_TODO) {
            return executeInteractiveRebaseEditorCommand(level, currentRepo, nativeGit, sandboxRoot, workingDir, invocation, message)
        }

        if (invocation.kind == GitEditorCommandKind.PATCH_HUNK) {
            val (updatedRepo, output) = GitSandboxEngine.applyPatchHunkEdit(currentRepo, message)
            val newlyStagedPaths = updatedRepo.files.filter { updatedFile ->
                updatedFile.staged && currentRepo.files.firstOrNull { it.name == updatedFile.name }?.staged != true
            }.map { it.name }
            if (newlyStagedPaths.isNotEmpty()) {
                runGit(nativeGit, workingDir, listOf("add") + newlyStagedPaths)
            }
            val inspectedRepo = inspectSandbox(level).copy(
                currentDir = updatedRepo.currentDir,
                interactiveAddSession = updatedRepo.interactiveAddSession,
            )
            return inspectedRepo to output
        }

        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 executeInteractiveRebaseEditorCommand(
        level: Level,
        currentRepo: RepoState,
        nativeGit: File,
        sandboxRoot: File,
        workingDir: File,
        invocation: GitEditorInvocation,
        todo: String,
    ): Pair<RepoState, List<String>> {
        val gitDir = File(sandboxRoot, ".git").apply { mkdirs() }
        val todoFile = File(gitDir, "GITHUG_ANDROID_REBASE_TODO").apply {
            writeText(todo)
        }
        val editorScript = File(gitDir, "githug-android-sequence-editor.sh").apply {
            writeText(
                """
                |#!/bin/sh
                |cat ${todoFile.absolutePath.toShellSingleQuoted()} > "$1"
                |""".trimMargin(),
            )
            setReadable(true, true)
        }
        val arguments = GitSandboxEngine.tokenizeCommand(invocation.command).drop(1)
        val result = runGit(
            nativeGit,
            workingDir,
            arguments,
            mapOf(
                "GIT_SEQUENCE_EDITOR" to "${shellExecutable().toShellSingleQuoted()} ${editorScript.absolutePath.toShellSingleQuoted()}",
                "GIT_EDITOR" to "true",
            ),
        )
        todoFile.delete()
        editorScript.delete()

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

    private fun executeSyntheticGitCommand(
        currentRepo: RepoState,
        command: String,
        tokens: List<String>,
        environment: Map<String, String> = emptyMap(),
    ): Pair<RepoState, List<String>>? {
        if (environment.isNotEmpty()) return null
        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" || it == "-i" || it == "--interactive" }
            "rebase" -> "--onto" in tokens
            "merge" -> "--squash" in tokens || tokens.lastOrNull() == "feature"
            "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
            else -> false
        }
        if (!shouldUseSandboxSemantics) return null

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

    private fun List<String>.normalizeGitStageAlias(): List<String> {
        return if (size >= 2 && this[0] == "git" && this[1] == "stage") {
            toMutableList().also { it[1] = "add" }
        } else {
            this
        }
    }

    private fun List<String>.normalizeGitBisectRunScriptShortcut(): List<String> {
        return if (
            size >= 4 &&
            this[0] == "git" &&
            this[1] == "bisect" &&
            this[2] == "run" &&
            this[3].startsWith("./")
        ) {
            take(3) + listOf("sh") + drop(3)
        } else {
            this
        }
    }

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

        if (!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%at\t%P\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 fetchHeadCount = File(sandbox, ".git/FETCH_HEAD")
            .takeIf { it.isFile }
            ?.readLines()
            ?.count { it.isNotBlank() }
            ?: 0
        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 = 4)
                if (parts.isEmpty()) {
                    null
                } else {
                    val parentHashes = parts.getOrNull(2).orEmpty().split(Regex("\\s+")).filter { it.isNotBlank() }
                    CommitNode(
                        id = parts[0],
                        authorTimestampSeconds = parts.getOrNull(1)?.toLongOrNull(),
                        parentCount = parentHashes.size,
                        message = parts.getOrElse(3) { "" },
                    )
                }
            }
        } 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(),
            fetchHeadCount = fetchHeadCount,
        )
    }

    private fun nativeGitBinary(): File? {
        return nativeGitOverride
            ?.takeIf { it.exists() && it.canExecute() }
            ?: packagedNativeGitBinary()
    }

    private fun requireNativeGit(): File {
        return nativeGitBinary() ?: error(unavailableMessage())
    }

    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>,
        outputLines: List<String> = emptyList(),
    ): RepoState {
        if (tokens.firstOrNull() != "git") {
            return inspectedRepo.copy(
                stashes = mergeDistinct(previousRepo.stashes, inspectedRepo.stashes),
                fetchedBranches = previousRepo.fetchedBranches + inspectedRepo.fetchedBranches,
                fetchHeadCount = inspectedRepo.fetchHeadCount,
                pushedBranches = previousRepo.pushedBranches + inspectedRepo.pushedBranches,
                pushedTags = previousRepo.pushedTags + inspectedRepo.pushedTags,
                submodules = previousRepo.submodules + inspectedRepo.submodules,
                maintenanceActions = previousRepo.maintenanceActions + inspectedRepo.maintenanceActions,
            )
        }

        return when (tokens.getOrNull(1)) {
            "bisect" -> {
                if (tokens.getOrNull(2) == "run" && outputLines.any { it.contains("is the first bad commit") }) {
                    inspectedRepo.copy(maintenanceActions = previousRepo.maintenanceActions + inspectedRepo.maintenanceActions + "bisect-found")
                } else {
                    inspectedRepo.copy(maintenanceActions = previousRepo.maintenanceActions + inspectedRepo.maintenanceActions)
                }
            }
            "stash" -> inspectedRepo.copy(
                stashes = mergeDistinct(previousRepo.stashes, inspectedRepo.stashes + "stash@{${previousRepo.stashes.size}}"),
            )
            "fetch" -> {
                inspectedRepo.copy(
                    fetchedBranches = inspectedRepo.fetchedBranches + previousRepo.fetchedBranches,
                    maintenanceActions = inspectedRepo.maintenanceActions + previousRepo.maintenanceActions + "fetch",
                )
            }
            "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",
                    fetchHeadCount = inspectedRepo.fetchHeadCount,
                    maintenanceActions = inspectedRepo.maintenanceActions + previousRepo.maintenanceActions + "pull",
                )
            }
            "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 + "merge"
                },
            )
            else -> inspectedRepo.copy(
                stashes = mergeDistinct(previousRepo.stashes, inspectedRepo.stashes),
                fetchedBranches = previousRepo.fetchedBranches + inspectedRepo.fetchedBranches,
                fetchHeadCount = inspectedRepo.fetchHeadCount,
                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 data class EnvironmentPrefixedCommand(
        val environment: Map<String, String>,
        val command: List<GitSandboxEngine.ShellToken>,
    )

    private fun parseEnvironmentPrefixedCommand(tokens: List<GitSandboxEngine.ShellToken>): EnvironmentPrefixedCommand {
        val environment = linkedMapOf<String, String>()
        var commandStart = 0
        while (commandStart < tokens.size) {
            val value = tokens[commandStart].value
            val assignmentIndex = value.indexOf('=')
            if (assignmentIndex <= 0) break
            val name = value.take(assignmentIndex)
            if (!name.isShellEnvironmentName()) break
            environment[name] = value.substring(assignmentIndex + 1)
            commandStart += 1
        }
        return EnvironmentPrefixedCommand(environment, tokens.drop(commandStart))
    }

    private fun rebaseTarget(command: String): String? {
        val tokens = GitSandboxEngine.tokenizeCommand(command)
        if (tokens.size < 3 || tokens[0] != "git" || tokens[1] != "rebase") return null
        return tokens.drop(2).lastOrNull { it != "-i" && it != "--interactive" && !it.startsWith("-") }
    }

    private fun String.isShellEnvironmentName(): Boolean {
        if (isEmpty()) return false
        if (first() != '_' && !first().isLetter()) return false
        return all { it == '_' || it.isLetterOrDigit() }
    }

    private fun String.toShellSingleQuoted(): String {
        return "'" + replace("'", "'\"'\"'") + "'"
    }

    private fun shellExecutable(): String = processRunner.shellExecutable()

    private fun runGit(
        binary: File,
        workingDir: File,
        arguments: List<String>,
        environment: Map<String, String> = emptyMap(),
    ): ProcessExecutionResult = processRunner.runGit(binary, workingDir, arguments, environment)

}
