package com.kawomi.githugandroid

import androidx.compose.runtime.saveable.listSaver

enum class PlayMode(val label: String) {
    CLI_ONLY("CLI ONLY"),
    VISUAL("VISUAL"),
}

data class GitFile(
    val name: String,
    val content: String = "",
    val staged: Boolean = false,
    val tracked: Boolean = false,
)

data class CommitNode(
    val id: String,
    val message: String,
)

data class RepoState(
    val initialized: Boolean = false,
    val files: List<GitFile> = emptyList(),
    val commits: List<CommitNode> = emptyList(),
    val headBranch: String = "master",
    val branches: Map<String, Int> = emptyMap(),
)

data class Level(
    val id: String,
    val title: String,
    val description: String,
    val hints: List<String>,
    val commandSuggestions: List<String>,
    val validator: (RepoState) -> Boolean,
    val setup: () -> RepoState,
)

fun sampleLevels(): List<Level> = listOf(
    Level(
        id = "init",
        title = "Init",
        description = "A new directory, git_hug, has been created. Initialize an empty repository in it.",
        hints = listOf("Use git init to create a new repository.", "Try `git init` in the command area."),
        commandSuggestions = listOf("git init", "git status"),
        validator = { it.initialized },
        setup = { RepoState() },
    ),
    Level(
        id = "add",
        title = "Add",
        description = "There is a file in your folder called README; add it to your staging area.",
        hints = listOf("You want to stage README.", "Use `git add README`."),
        commandSuggestions = listOf("git status", "git add README", "ls"),
        validator = { repo -> repo.files.any { it.name == "README" && it.staged } },
        setup = { RepoState(initialized = true, files = listOf(GitFile("README")), branches = mapOf("master" to 0)) },
    ),
    Level(
        id = "commit",
        title = "Commit",
        description = "The README file has been added to your staging area, now commit it.",
        hints = listOf("You must include a message when you commit.", "Use `git commit -m \"message\"`."),
        commandSuggestions = listOf("git status", "git commit -m \"Initial commit\"", "git log"),
        validator = { repo -> repo.commits.isNotEmpty() },
        setup = { RepoState(initialized = true, files = listOf(GitFile("README", staged = true)), branches = mapOf("master" to 0)) },
    ),
)

val RepoStateSaver = listSaver<RepoState, Any>(
    save = { state ->
        listOf(
            state.initialized,
            state.headBranch,
            state.files.flatMap { listOf(it.name, it.content, it.staged.toString(), it.tracked.toString()) },
            state.commits.flatMap { listOf(it.id, it.message) },
            state.branches.flatMap { listOf(it.key, it.value.toString()) },
        )
    },
    restore = { saved ->
        val initialized = saved[0] as Boolean
        val headBranch = saved[1] as String
        val fileParts = saved[2] as List<*>
        val commitParts = saved[3] as List<*>
        val branchParts = saved[4] as List<*>
        RepoState(
            initialized = initialized,
            headBranch = headBranch,
            files = fileParts.chunked(4).map {
                GitFile(
                    name = it[0] as String,
                    content = it[1] as String,
                    staged = (it[2] as String).toBoolean(),
                    tracked = (it[3] as String).toBoolean(),
                )
            },
            commits = commitParts.chunked(2).map {
                CommitNode(it[0] as String, it[1] as String)
            },
            branches = branchParts.chunked(2).associate { (it[0] as String) to (it[1] as String).toInt() }
        )
    }
)

object GitSandboxEngine {
    fun commandReferenceLines(): List<String> = listOf(
        "Available sandbox commands:",
        "  git init",
        "  git status",
        "  git add <file>",
        "  git add .",
        "  git commit -m \"message\"",
        "  git commit -am \"message\"",
        "  git log",
        "  git branch <name>",
        "  git checkout <name>",
        "  ls",
        "  touch <file>",
        "  help",
        "  git help",
    )

    fun execute(repo: RepoState, command: String): Pair<RepoState, List<String>> {
        val parts = tokenizeCommand(command)
        if (parts.isEmpty()) return repo to emptyList()
        return when {
            parts[0] == "help" || parts[0] == "?" -> repo to commandReferenceLines()
            parts[0] == "touch" && parts.size >= 2 -> {
                val name = parts[1]
                if (repo.files.any { it.name == name }) repo to listOf("$name already exists")
                else repo.copy(files = repo.files + GitFile(name = name)) to emptyList()
            }
            parts[0] == "ls" -> repo to repo.files.map { it.name }.ifEmpty { listOf() }
            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 commandReferenceLines()
            parts.size >= 2 && parts[1] == "status" -> repo to statusLines(repo)
            parts.size >= 3 && parts[1] == "add" -> {
                val target = parts[2]
                if (target != "." && repo.files.none { it.name == target }) {
                    repo to listOf("fatal: pathspec '$target' did not match any files")
                } else {
                    val updated = repo.files.map { if (target == "." || it.name == target) it.copy(staged = true) else it }
                    repo.copy(files = updated) to listOf("staged ${if (target == ".") "all files" else target}")
                }
            }
            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] == "branch" -> {
                val branch = parts[2]
                if (repo.branches.containsKey(branch)) repo to listOf("fatal: a branch named '$branch' already exists")
                else repo.copy(branches = repo.branches + (branch to repo.commits.size)) to listOf("Created branch $branch")
            }
            parts.size >= 3 && parts[1] == "checkout" -> {
                val branch = parts[2]
                if (!repo.branches.containsKey(branch)) repo to listOf("error: pathspec '$branch' did not match any branch")
                else repo.copy(headBranch = branch) to listOf("Switched to branch '$branch'")
            }
            else -> repo to listOf("Unsupported git command in MVP sandbox: ${parts.drop(1).joinToString(" ")}")
        }
    }

    fun tokenizeCommand(command: String): List<String> {
        val result = mutableListOf<String>()
        val current = StringBuilder()
        var quoteChar: Char? = null
        var escaping = false

        command.forEach { char ->
            when {
                escaping -> {
                    current.append(char)
                    escaping = false
                }
                char == '\\' && quoteChar != '\'' -> {
                    escaping = true
                }
                quoteChar != null -> {
                    if (char == quoteChar) {
                        quoteChar = null
                    } else {
                        current.append(char)
                    }
                }
                char == '"' || char == '\'' -> {
                    quoteChar = char
                }
                char.isWhitespace() -> {
                    if (current.isNotEmpty()) {
                        result += current.toString()
                        current.clear()
                    }
                }
                else -> current.append(char)
            }
        }

        if (escaping) {
            current.append('\\')
        }
        if (current.isNotEmpty()) {
            result += current.toString()
        }

        return result
    }

    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.orEmpty()
        val staged = repoForCommit.files.filter { it.staged }
        if (staged.isEmpty()) return repo to listOf("nothing to commit")

        val nextId = "${repo.commits.size + 1}".padStart(7, '0')
        val cleanedFiles = repoForCommit.files.map { file ->
            if (file.staged) file.copy(staged = false, tracked = true) else file
        }

        return repoForCommit.copy(
            files = cleanedFiles,
            commits = repo.commits + CommitNode(nextId, message),
            branches = repo.branches + (repo.headBranch to (repo.commits.size + 1)),
        ) to listOf("[$nextId] $message")
    }

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

        while (index < arguments.size) {
            val argument = arguments[index]
            when {
                argument == "-a" || argument == "--all" -> {
                    stageAllTracked = true
                }
                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 (message.isNullOrBlank()) {
            return ParsedCommitArguments(error = "error: commit message required. Use git commit -m \"message\"")
        }

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

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

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