package solutions.tretter.githugandroid

internal object SandboxCommandEngine {
    fun execute(repo: RepoState, shellParts: List<GitSandboxEngine.ShellToken>): Pair<RepoState, List<String>> {
        val parts = shellParts.map { it.value }
        return when {
            parts[0] == "help" || parts[0] == "?" -> repo to GitSandboxEngine.commandReferenceLines()
            parts[0] == "touch" && parts.size >= 2 -> {
                val name = parts[1]
                if (repo.files.any { it.name == name && !it.deleted }) repo to listOf("$name already exists")
                else repo.copy(files = repo.files + GitFile(name = name)) to emptyList()
            }
            (parts[0] == "mkdir" || parts[0] == "md") && parts.size >= 2 -> repo to emptyList()
            (parts[0] == "rm" || parts[0] == "del") && parts.size >= 2 -> {
                val target = parts[1]
                repo.copy(files = repo.files.mapNotNull { file ->
                    when {
                        file.name != target -> file
                        file.tracked -> file.copy(deleted = true, staged = false)
                        else -> null
                    }
                }) to emptyList()
            }
            parts[0] == "echo" -> writeEcho(repo, shellParts)
            parts[0] == "ls" || parts[0] == "dir" -> repo to (
                if (repo.initialized) listOf(".git") else emptyList()
            ) + repo.files.filterNot { it.deleted }.map { it.name }
            parts[0] == "cd.." -> repo.copy(currentDir = parentDirectory(repo.currentDir)) to emptyList()
            parts[0] != "git" -> repo to listOf("Command not supported in sandbox. Try a git command or 'touch'.")
            parts.size >= 2 && parts[1] == "init" -> repo.copy(initialized = true, branches = mapOf("master" to repo.commits.size)) to listOf("Initialized empty Git repository")
            !repo.initialized -> repo to listOf("fatal: not a git repository")
            parts.size >= 2 && parts[1] == "help" -> repo to GitSandboxEngine.commandReferenceLines()
            parts.size >= 2 && parts[1] == "status" -> repo to statusLines(repo)
            parts.size >= 2 && parts[1] == "stash" -> {
                val updatedFiles = repo.files.map { file ->
                    if (file.tracked && !file.staged) file.copy(content = "") else file
                }
                repo.copy(files = updatedFiles, stashes = repo.stashes + "stash@{${repo.stashes.size}}") to listOf("Saved working directory and index state")
            }
            parts.size >= 2 && parts[1] == "fetch" -> {
                val remote = parts.getOrNull(2)?.takeIf { !it.startsWith("-") } ?: "origin"
                repo.copy(
                    fetchedBranches = repo.fetchedBranches + listOf("$remote/master", "$remote/new_branch"),
                    fetchHeadCount = 2,
                    maintenanceActions = repo.maintenanceActions + "fetch",
                ) to emptyList()
            }
            parts.size >= 2 && parts[1] == "pull" -> {
                val remote = parts.getOrNull(2)?.takeIf { !it.startsWith("-") } ?: "origin"
                val branch = parts.drop(2).lastOrNull()?.takeIf { !it.startsWith("-") && it != remote } ?: repo.headBranch
                repo.copy(
                    fetchedBranches = repo.fetchedBranches + "$remote/$branch",
                    fetchHeadCount = 1,
                    branches = repo.branches + (repo.headBranch to maxOf(repo.branches[repo.headBranch] ?: 0, 2)),
                    maintenanceActions = repo.maintenanceActions + "pull",
                ) to emptyList()
            }
            parts.size >= 2 && parts[1] == "push" -> pushRefs(repo, parts.drop(2))
            parts.size >= 3 && parts[1] == "submodule" && parts[2] == "add" -> {
                val url = parts.getOrNull(3)
                val path = parts.getOrNull(4)
                if (url == null || path == null) {
                    repo to listOf("usage: git submodule add <repository> <path>")
                } else {
                    repo.copy(submodules = repo.submodules + (path.trimEnd('/') to url)) to emptyList()
                }
            }
            parts.size >= 2 && parts[1] == "repack" -> {
                repo.copy(maintenanceActions = repo.maintenanceActions + "repack") to emptyList()
            }
            parts.size >= 3 && parts[1] == "tag" -> {
                val tag = parts[2]
                if (tag in repo.tags) repo to listOf("fatal: tag '$tag' already exists")
                else repo.copy(tags = repo.tags + tag) to listOf(tag)
            }
            parts.size >= 4 && parts[1] == "config" -> {
                val key = parts[2]
                val value = parts.drop(3).joinToString(" ")
                repo.copy(config = repo.config + (key to value)) to emptyList()
            }
            parts.size >= 2 && (parts[1] == "stage" || parts[1] == "add") -> stagePaths(repo, parts)
            parts.size >= 3 && parts[1] == "rm" -> removeGitPath(repo, parts.drop(2))
            parts.size >= 4 && parts[1] == "mv" -> moveGitPath(repo, SandboxShell.expandPathspecTokens(repo, shellParts.drop(2)))
            parts.size >= 2 && parts[1] == "commit" -> commit(repo, parts.drop(2))
            parts.size >= 2 && parts[1] == "log" -> {
                repo to if (repo.commits.isEmpty()) listOf("fatal: your current branch '${repo.headBranch}' does not have any commits yet")
                else repo.commits.reversed().flatMap { listOf("commit ${it.id}", "    ${it.message}") }
            }
            parts.size >= 3 && parts[1] == "remote" && parts[2] == "add" -> {
                val name = parts.getOrNull(3)
                val url = parts.getOrNull(4)
                if (name == null || url == null) repo to listOf("usage: git remote add <name> <url>")
                else repo.copy(remotes = repo.remotes + (name to url)) to emptyList()
            }
            parts.size >= 3 && parts[1] == "branch" -> branch(repo, parts.drop(2))
            parts.size >= 3 && parts[1] == "checkout" -> checkout(repo, parts.drop(2))
            parts.size >= 3 && parts[1] == "reset" -> reset(repo, parts.drop(2))
            parts.size >= 3 && parts[1] == "merge" -> merge(repo, parts.drop(2))
            parts.size >= 2 && parts[1] == "rebase" -> rebase(repo, parts.drop(2))
            parts.size >= 2 && parts[1] == "cherry-pick" -> cherryPick(repo)
            parts.size >= 2 && parts[1] == "revert" -> {
                repo.copy(commits = repo.commits + CommitNode("${repo.commits.size + 1}", "Revert \"Bad commit\"")) to emptyList()
            }
            else -> repo to listOf("Unsupported git command in MVP sandbox: ${parts.drop(1).joinToString(" ")}")
        }
    }

    private fun writeEcho(repo: RepoState, shellParts: List<GitSandboxEngine.ShellToken>): Pair<RepoState, List<String>> {
        val parts = shellParts.map { it.value }
        val redirectIndex = parts.indexOfFirst { it == ">" || it == ">>" }
        if (redirectIndex == -1 || redirectIndex == parts.lastIndex) {
            return repo to listOf(parts.drop(1).joinToString(" "))
        }

        val append = parts[redirectIndex] == ">>"
        val content = parts.subList(1, redirectIndex).joinToString(" ")
        val target = parts[redirectIndex + 1]
        val updatedFiles = repo.files.toMutableList()
        val index = updatedFiles.indexOfFirst { it.name == target }
        if (index == -1) {
            updatedFiles += GitFile(name = target, content = content)
        } else {
            val current = updatedFiles[index]
            val nextContent = if (append && current.content.isNotEmpty()) "${current.content}\n$content" else content
            updatedFiles[index] = current.copy(content = nextContent, deleted = false)
        }

        return repo.copy(files = updatedFiles) to emptyList()
    }

    private fun parentDirectory(currentDir: String): String {
        if (currentDir == ".") return "."
        return currentDir.substringBeforeLast('/', missingDelimiterValue = ".").ifBlank { "." }
    }

    private fun pushRefs(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
        val remote = arguments.firstOrNull { !it.startsWith("-") } ?: "origin"
        val explicitBranches = arguments
            .dropWhile { it.startsWith("-") }
            .drop(1)
            .filter { !it.startsWith("-") }
        val pushedBranches = when {
            arguments.any { it == "--all" } -> repo.branches.keys.map { "$remote/$it" }
            explicitBranches.isNotEmpty() -> explicitBranches.map { branch -> "$remote/${branch.substringAfterLast(':')}" }
            else -> listOf("$remote/${repo.headBranch}")
        }
        val pushedTags = if (arguments.any { it == "--tags" || it == "--follow-tags" }) {
            repo.tags.toSet()
        } else {
            emptySet()
        }
        return repo.copy(
            pushedBranches = repo.pushedBranches + pushedBranches,
            pushedTags = repo.pushedTags + pushedTags,
        ) to emptyList()
    }

    private fun stagePaths(repo: RepoState, parts: List<String>): Pair<RepoState, List<String>> {
        if (parts.drop(2).any { it == "-i" || it == "--interactive" }) {
            return InteractiveAddEngine.start(repo, parts.drop(2))
        }
        if (parts.drop(2).any { it == "-p" || it == "--patch" }) {
            return InteractiveAddEngine.startPatch(repo, parts.drop(2))
        }
        val target = parts.drop(2).lastOrNull { !it.startsWith("-") }
            ?: return repo to listOf("usage: git add <path>")
        return if (target != "." && repo.files.none { it.name == target && !it.deleted }) {
            repo to listOf("fatal: pathspec '$target' did not match any files")
        } else {
            val updated = repo.files.map { if ((target == "." || it.name == target) && !it.deleted) it.copy(staged = true) else it }
            repo.copy(files = updated) to listOf("staged ${if (target == ".") "all files" else target}")
        }
    }

    private fun removeGitPath(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
        val cached = "--cached" in arguments
        val target = arguments.lastOrNull { !it.startsWith("-") }
            ?: return repo to listOf("usage: git rm [--cached] <path>")
        val updated = repo.files.mapNotNull { file ->
            if (file.name != target) {
                file
            } else if (cached) {
                file.copy(staged = false, tracked = false)
            } else {
                null
            }
        }
        return repo.copy(files = updated) to emptyList()
    }

    private fun moveGitPath(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
        val destination = arguments.lastOrNull() ?: return repo to listOf("usage: git mv <source> <destination>")
        val sources = arguments.dropLast(1)
        if (sources.isEmpty()) return repo to listOf("usage: git mv <source> <destination>")
        val destinationIsDirectory = sources.size > 1 || destination.endsWith("/")
        val updated = repo.files.map { file ->
            if (file.name in sources) {
                val target = if (destinationIsDirectory) {
                    destination.trimEnd('/') + "/" + file.name.substringAfterLast('/')
                } else {
                    destination
                }
                file.copy(name = target, staged = true)
            } else {
                file
            }
        }
        return repo.copy(files = updated) to emptyList()
    }

    private fun branch(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
        return when (arguments.firstOrNull()) {
            "-d", "-D", "--delete" -> {
                val branch = arguments.getOrNull(1)
                if (branch == null) repo to listOf("usage: git branch -d <branch>")
                else repo.copy(branches = repo.branches - branch) to listOf("Deleted branch $branch")
            }
            else -> {
                val branch = arguments.first()
                val base = arguments.getOrNull(1)
                val baseIndex = if (base == "HEAD~1" || base == "HEAD^") {
                    (repo.branches[repo.headBranch] ?: repo.commits.size) - 1
                } else {
                    repo.commits.size
                }.coerceAtLeast(0)
                if (repo.branches.containsKey(branch)) repo to listOf("fatal: a branch named '$branch' already exists")
                else repo.copy(branches = repo.branches + (branch to baseIndex)) to listOf("Created branch $branch")
            }
        }
    }

    private fun checkout(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
        return when {
            arguments.firstOrNull() == "-b" -> {
                val branch = arguments.getOrNull(1) ?: return repo to listOf("usage: git checkout -b <branch>")
                repo.copy(
                    headBranch = branch,
                    branches = repo.branches + (branch to repo.commits.size),
                ) to listOf("Switched to a new branch '$branch'")
            }
            arguments.firstOrNull() == "-B" -> {
                val branch = arguments.getOrNull(1) ?: return repo to listOf("usage: git checkout -B <branch>")
                repo.copy(
                    headBranch = branch,
                    branches = repo.branches + (branch to repo.commits.size),
                ) to listOf("Switched to branch '$branch'")
            }
            "--" in arguments -> {
                val target = arguments.last()
                val updated = repo.files.map { file ->
                    when (file.name) {
                        target -> file.copy(content = file.content.substringBefore("\nThese are changes you don't want to keep!"))
                        "file3" -> file
                        else -> file
                    }
                }.let { files ->
                    if (target == "file3" && files.none { it.name == "file3" }) files + GitFile("file3", tracked = true) else files
                }
                repo.copy(files = updated) to emptyList()
            }
            arguments.any { it == "file3" } -> {
                repo.copy(files = repo.files + GitFile("file3", tracked = true)) to emptyList()
            }
            else -> {
                val branch = arguments.first()
                val normalizedTag = branch.removePrefix("tags/").removePrefix("refs/tags/")
                when {
                    repo.branches.containsKey(branch) -> repo.copy(headBranch = branch) to listOf("Switched to branch '$branch'")
                    normalizedTag in repo.tags -> repo.copy(headBranch = "tags/$normalizedTag") to listOf("HEAD is now at $normalizedTag")
                    else -> repo to listOf("error: pathspec '$branch' did not match any branch")
                }
            }
        }
    }

    private fun reset(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
        return if ("--soft" in arguments) {
            repo.copy(
                commits = repo.commits.dropLast(1),
                files = repo.files.map { if (it.tracked) it.copy(staged = true) else it },
            ) to emptyList()
        } else {
            val target = arguments.last()
            repo.copy(files = repo.files.map { if (it.name == target) it.copy(staged = false) else it }) to emptyList()
        }
    }

    private fun merge(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
        val branch = arguments.lastOrNull().orEmpty()
        val squash = "--squash" in arguments
        val files = when {
            branch == "feature" && repo.files.none { it.name == "file2" } -> repo.files + GitFile("file2", tracked = true)
            branch == "long-feature-branch" && repo.files.none { it.name == "file3" } -> repo.files + GitFile("file3", staged = true)
            branch == "mybranch" -> repo.files.map {
                if (it.name == "poem.txt") it.copy(content = "Humpty Dumpty sat on a wall\nHumpty Dumpty had a great fall", staged = true)
                else it
            }
            else -> repo.files
        }
        return repo.copy(
            files = files,
            maintenanceActions = repo.maintenanceActions + if (squash) "merge-squash" else "merge",
        ) to emptyList()
    }

    private fun rebase(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
        val interactive = "-i" in arguments || "--interactive" in arguments
        val target = arguments.lastOrNull { it != "-i" && it != "--interactive" && !it.startsWith("-") }
        if (interactive && target == null) {
            return repo to listOf("fatal: No rebase upstream specified")
        }
        val originalCommits = repo.commits
        val commits = repo.commits
        val updatedBranches = when {
            "--onto" in arguments -> repo.branches + (repo.headBranch to (repo.branches["master"] ?: 0) + 1)
            arguments.isNotEmpty() -> repo.branches + (repo.headBranch to maxOf(repo.branches[repo.headBranch] ?: 0, repo.branches[arguments.last()] ?: 0))
            else -> repo.branches
        }
        val maintenanceActions = if ("--onto" in arguments) {
            repo.maintenanceActions + "rebase-onto"
        } else {
            repo.maintenanceActions
        }
        val output = if (interactive) interactiveRebaseConsoleLines(originalCommits, arguments, repo.headBranch) else emptyList()
        return repo.copy(commits = commits, branches = updatedBranches, maintenanceActions = maintenanceActions) to output
    }

    private fun interactiveRebaseConsoleLines(originalCommits: List<CommitNode>, arguments: List<String>, branch: String): List<String> {
        val target = arguments.lastOrNull { it != "-i" && it != "--interactive" }.orEmpty()
        return buildList {
            originalCommits.forEach { commit ->
                add("pick ${commit.id} ${commit.message}")
            }
            add("")
            add("# Rebase ${target.ifBlank { "HEAD" }} in progress; onto HEAD")
            add("# Commands:")
            add("# p, pick <commit> = use commit")
            add("# r, reword <commit> = use commit, but edit the commit message")
            add("# s, squash <commit> = use commit, but meld into previous commit")
            add("# d, drop <commit> = remove commit")
            add("Successfully rebased and updated refs/heads/$branch.")
        }
    }

    private fun cherryPick(repo: RepoState): Pair<RepoState, List<String>> {
        val files = if (repo.files.none { it.name == "README.md" }) {
            repo.files + GitFile("README.md", "Proper input instructions\n", tracked = true)
        } else {
            repo.files.map { if (it.name == "README.md") it.copy(tracked = true) else it }
        }
        return repo.copy(files = files, commits = listOf(CommitNode("${repo.commits.size + 1}", "Filled in README.md with proper input")) + repo.commits) to emptyList()
    }

    private fun commit(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
        val parsed = parseCommitArguments(arguments)
        if (parsed.error != null) {
            return repo to listOf(parsed.error)
        }

        val repoForCommit = if (parsed.stageAllTracked) {
            repo.copy(files = repo.files.map { file -> if (file.tracked) file.copy(staged = true) else file })
        } else {
            repo
        }

        val message = parsed.message ?: repo.commits.lastOrNull()?.message.orEmpty()
        val staged = repoForCommit.files.filter { it.staged }
        if (staged.isEmpty()) return repo to listOf("nothing to commit")

        val cleanedFiles = repoForCommit.files.map { file ->
            if (file.staged) file.copy(staged = false, tracked = true) else file
        }
        val nextCommits = if (parsed.amend && repo.commits.isNotEmpty()) {
            repo.commits.dropLast(1) + repo.commits.last().copy(message = message)
        } else {
            val nextId = "${repo.commits.size + 1}".padStart(7, '0')
            repo.commits + CommitNode(nextId, message)
        }

        return repoForCommit.copy(
            files = cleanedFiles,
            commits = nextCommits,
            branches = repo.branches + (repo.headBranch to nextCommits.size),
        ) to listOf("[${nextCommits.lastOrNull()?.id.orEmpty()}] $message")
    }

    private fun parseCommitArguments(arguments: List<String>): ParsedCommitArguments {
        var message: String? = null
        var stageAllTracked = false
        var amend = false
        var index = 0

        while (index < arguments.size) {
            val argument = arguments[index]
            when {
                argument == "-a" || argument == "--all" -> {
                    stageAllTracked = true
                }
                argument == "--amend" -> {
                    amend = true
                }
                argument == "--no-edit" -> {
                    // Keep the previous commit message when amending.
                }
                argument == "--date" -> {
                    if (arguments.getOrNull(index + 1) == null) {
                        return ParsedCommitArguments(error = "error: option '--date' requires a value")
                    }
                    index += 1
                }
                argument.startsWith("--date=") -> {
                    // The sandbox records commit structure, not timestamps.
                }
                argument == "-m" || argument == "--message" -> {
                    val next = arguments.getOrNull(index + 1)
                        ?: return ParsedCommitArguments(error = "error: commit message required. Use git commit -m \"message\"")
                    message = next
                    index += 1
                }
                argument.startsWith("--message=") -> {
                    message = argument.substringAfter('=')
                }
                argument.startsWith("-") && !argument.startsWith("--") && argument.length > 2 -> {
                    val shortFlags = argument.drop(1)
                    var shortIndex = 0
                    while (shortIndex < shortFlags.length) {
                        when (val flag = shortFlags[shortIndex]) {
                            'a' -> stageAllTracked = true
                            'm' -> {
                                val attachedValue = shortFlags.substring(shortIndex + 1)
                                if (attachedValue.isNotEmpty()) {
                                    message = attachedValue
                                } else {
                                    val next = arguments.getOrNull(index + 1)
                                        ?: return ParsedCommitArguments(error = "error: commit message required. Use git commit -m \"message\"")
                                    message = next
                                    index += 1
                                }
                                break
                            }
                            else -> return ParsedCommitArguments(error = "error: unsupported commit option '-$flag'")
                        }
                        shortIndex += 1
                    }
                }
                else -> return ParsedCommitArguments(error = "error: unsupported commit argument '$argument'")
            }
            index += 1
        }

        if (!amend && message.isNullOrBlank()) {
            return ParsedCommitArguments(error = "error: commit message required. Use git commit -m \"message\"")
        }

        return ParsedCommitArguments(message = message, stageAllTracked = stageAllTracked, amend = amend)
    }

    private fun statusLines(repo: RepoState): List<String> {
        val staged = repo.files.filter { it.staged }.map {
            when {
                it.deleted -> "deleted:    ${it.name}"
                it.tracked -> "modified:   ${it.name}"
                else -> "new file:   ${it.name}"
            }
        }
        val deleted = repo.files.filter { it.deleted && it.tracked && !it.staged }.map { "deleted:    ${it.name}" }
        val unstaged = repo.files.filterNot { it.staged || it.tracked || it.deleted }.map { "untracked:  ${it.name}" }
        return buildList {
            add("On branch ${repo.headBranch}")
            if (staged.isEmpty() && deleted.isEmpty() && unstaged.isEmpty()) {
                add("nothing to commit, working tree clean")
            } else {
                if (staged.isNotEmpty()) {
                    add("Changes to be committed:")
                    addAll(staged)
                }
                if (deleted.isNotEmpty()) {
                    add("Changes not staged for commit:")
                    addAll(deleted)
                }
                if (unstaged.isNotEmpty()) {
                    add("Untracked files:")
                    addAll(unstaged)
                }
            }
        }
    }

    private data class ParsedCommitArguments(
        val message: String? = null,
        val stageAllTracked: Boolean = false,
        val amend: Boolean = false,
        val error: String? = null,
    )
}
