package solutions.tretter.githugandroid

internal object InteractiveAddEngine {
    private const val PatchHunkPrompt = "(1/1) Stage this hunk [y,n,q,a,d,s,e,p,P,?]?"

    fun start(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
        val targets = arguments.filterNot { it == "-i" || it == "--interactive" || it.startsWith("--") }
        val target = targets.lastOrNull()
        val candidates = interactiveAddCandidates(repo, target)
        return repo.copy(interactiveAddSession = InteractiveAddSession(target = target)) to interactiveAddConsoleLines(candidates)
    }

    fun startPatch(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
        val targets = arguments.filterNot { it == "-p" || it == "--patch" || it.startsWith("--") }
        val target = targets.lastOrNull()
        val patchFile = interactiveAddCandidates(repo, target).firstOrNull()
            ?: return repo to listOf("No changes.")
        return startPatchHunkSession(repo, patchFile.name)
    }

    fun handleInput(repo: RepoState, input: String): Pair<RepoState, List<String>> {
        val session = repo.interactiveAddSession ?: return repo to emptyList()
        val answer = input.trim()
        if (session.selectionAction == "patch-hunk") {
            return handlePatchHunkInput(repo, session, answer)
        }
        return if (session.awaitingUpdateSelection) {
            applyInteractiveAddUpdateSelection(repo, session, answer)
        } else {
            when (answer.lowercase()) {
                "1", "s", "status" -> repo to listOf("What now> $answer") + interactiveAddConsoleLines(interactiveAddCandidates(repo, session.target))
                "2", "u", "update" -> interactiveAddSelectionPrompt(repo, session, answer, "Update>>", "update")
                "3", "r", "revert" -> interactiveAddSelectionPrompt(repo, session, answer, "Revert>>", "revert")
                "4", "a", "add untracked", "add-untracked" -> interactiveAddSelectionPrompt(repo, session, answer, "Add untracked>>", "add-untracked")
                "5", "p", "patch" -> interactiveAddSelectionPrompt(repo, session, answer, "Patch update>>", "patch")
                "6", "d", "diff" -> interactiveAddSelectionPrompt(repo, session, answer, "Diff>>", "diff")
                "7", "q", "quit" -> repo.copy(interactiveAddSession = null) to listOf("What now> $answer", "Bye.")
                "8", "h", "help" -> repo to listOf("What now> $answer") + interactiveAddConsoleLines(interactiveAddCandidates(repo, session.target))
                else -> repo to listOf("What now> $answer", "Huh ($answer)?") + interactiveAddConsoleLines(interactiveAddCandidates(repo, session.target))
            }
        }
    }

    fun parsePatchHunkEditorInvocation(repo: RepoState, command: String): GitEditorInvocation? {
        val session = repo.interactiveAddSession ?: return null
        if (session.selectionAction != "patch-hunk") return null
        if (command.trim().lowercase() != "e") return null
        val target = session.target ?: return null
        val file = repo.files.firstOrNull { it.name == target && !it.deleted } ?: return null
        return GitEditorInvocation(
            command = command,
            kind = GitEditorCommandKind.PATCH_HUNK,
            title = "Edit Patch Hunk",
            initialContent = editablePatchHunkContent(file),
        )
    }

    fun applyPatchHunkEdit(repo: RepoState, content: String): Pair<RepoState, List<String>> {
        val session = repo.interactiveAddSession ?: return repo to listOf("No patch hunk is active.")
        val target = session.target ?: return repo.copy(interactiveAddSession = null) to listOf("No patch hunk is active.")
        if (session.selectionAction != "patch-hunk") return repo to listOf("No patch hunk is active.")
        if (content.isBlank()) return repo to listOf("Edited hunk was empty; patch was not applied.", PatchHunkPrompt)
        val updatedFiles = repo.files.map { file ->
            if (file.name == target && !file.deleted) file.copy(staged = true) else file
        }
        return repo.copy(files = updatedFiles, interactiveAddSession = null) to listOf(
            "$PatchHunkPrompt e",
            "Applied edited hunk.",
        )
    }

    private fun interactiveAddSelectionPrompt(
        repo: RepoState,
        session: InteractiveAddSession,
        answer: String,
        prompt: String,
        action: String,
    ): Pair<RepoState, List<String>> {
        return repo.copy(
            interactiveAddSession = session.copy(
                awaitingUpdateSelection = true,
                selectionPrompt = prompt,
                selectionAction = action,
            ),
        ) to listOf("What now> $answer", prompt)
    }

    private fun applyInteractiveAddUpdateSelection(
        repo: RepoState,
        session: InteractiveAddSession,
        answer: String,
    ): Pair<RepoState, List<String>> {
        val candidates = interactiveAddCandidates(repo, session.target)
        val selectedNames = selectedInteractiveAddNames(candidates, answer)
        val prompt = session.selectionPrompt
        if (selectedNames.isEmpty()) {
            return repo to listOf("$prompt $answer", "No files selected.", prompt)
        }

        if (session.selectionAction == "patch" && selectedNames.size == 1) {
            return startPatchHunkSession(repo, selectedNames.single(), "$prompt $answer")
        }

        val updatedFiles = applyInteractiveAddSelectionAction(repo, selectedNames, session.selectionAction)
        val updatedRepo = repo.copy(
            files = updatedFiles,
            interactiveAddSession = session.copy(
                awaitingUpdateSelection = false,
                selectionPrompt = "Update>>",
                selectionAction = "update",
            ),
        )
        val summary = interactiveAddSelectionSummary(repo, updatedFiles, selectedNames, session.selectionAction)
        return updatedRepo to listOf(
            "$prompt $answer",
            summary,
        ) + interactiveAddConsoleLines(interactiveAddCandidates(updatedRepo, session.target))
    }

    private fun applyInteractiveAddSelectionAction(repo: RepoState, selectedNames: Set<String>, action: String): List<GitFile> {
        return when (action) {
            "revert" -> repo.files.mapNotNull { file ->
                if (file.name !in selectedNames || file.deleted) {
                    file
                } else if (file.tracked) {
                    file.copy(content = "", staged = false, deleted = false)
                } else {
                    null
                }
            }
            "diff" -> repo.files
            else -> repo.files.map { file ->
                if (file.name in selectedNames && !file.deleted) file.copy(staged = true) else file
            }
        }
    }

    private fun interactiveAddSelectionSummary(
        repo: RepoState,
        updatedFiles: List<GitFile>,
        selectedNames: Set<String>,
        action: String,
    ): String {
        return when (action) {
            "revert" -> "reverted ${selectedNames.size} path(s)"
            "diff" -> selectedNames.joinToString("\n") { "diff -- $it" }
            else -> {
                val stagedCount = updatedFiles.count { updatedFile ->
                    val before = repo.files.firstOrNull { it.name == updatedFile.name }
                    updatedFile.staged && before?.staged != true
                }
                "updated $stagedCount path(s)"
            }
        }
    }

    private fun startPatchHunkSession(repo: RepoState, target: String, prefixLine: String? = null): Pair<RepoState, List<String>> {
        val file = repo.files.firstOrNull { it.name == target && !it.deleted }
            ?: return repo to listOfNotNull(prefixLine, "No changes.")
        val session = InteractiveAddSession(
            target = target,
            awaitingUpdateSelection = false,
            selectionPrompt = PatchHunkPrompt,
            selectionAction = "patch-hunk",
        )
        val output = listOfNotNull(prefixLine) + patchHunkLines(file)
        return repo.copy(interactiveAddSession = session) to output
    }

    private fun handlePatchHunkInput(repo: RepoState, session: InteractiveAddSession, answer: String): Pair<RepoState, List<String>> {
        val target = session.target ?: return repo.copy(interactiveAddSession = null) to listOf("No changes.")
        return when (answer.lowercase()) {
            "y", "a" -> {
                val updatedFiles = repo.files.map { file ->
                    if (file.name == target && !file.deleted) file.copy(staged = true) else file
                }
                repo.copy(files = updatedFiles, interactiveAddSession = null) to listOf("$PatchHunkPrompt $answer")
            }
            "n", "d" -> repo.copy(interactiveAddSession = null) to listOf("$PatchHunkPrompt $answer")
            "q" -> repo.copy(interactiveAddSession = null) to listOf("$PatchHunkPrompt $answer", "Quit")
            "?" -> repo to listOf(
                "$PatchHunkPrompt $answer",
                "y - stage this hunk",
                "n - do not stage this hunk",
                "q - quit; do not stage this hunk or any remaining ones",
                "a - stage this hunk and all later hunks in the file",
                "d - do not stage this hunk or any later hunks in the file",
                "s - split the current hunk into smaller hunks",
                "e - manually edit the current hunk",
                "p - print the current hunk",
                "? - print help",
                PatchHunkPrompt,
            )
            "p" -> {
                val file = repo.files.firstOrNull { it.name == target && !it.deleted }
                if (file == null) {
                    repo.copy(interactiveAddSession = null) to listOf("No changes.")
                } else {
                    repo to listOf("$PatchHunkPrompt $answer") + patchHunkLines(file)
                }
            }
            "s" -> repo to listOf("$PatchHunkPrompt $answer", "Sorry, cannot split this hunk", PatchHunkPrompt)
            "e" -> repo to listOf("$PatchHunkPrompt $answer", "Opening patch editor")
            else -> repo to listOf("$PatchHunkPrompt $answer", "Unknown command '$answer'.", PatchHunkPrompt)
        }
    }

    private fun patchHunkLines(file: GitFile): List<String> {
        return patchDiffHeaderLines(file) + patchHunkBodyLines(file) + PatchHunkPrompt
    }

    private fun editablePatchHunkContent(file: GitFile): String {
        return buildList {
            add("# Manual hunk edit mode -- see bottom for a quick guide.")
            addAll(patchHunkBodyLines(file))
            add("# ---")
            add("# To remove '-' lines, make them ' ' lines (context).")
            add("# To remove '+' lines, delete them.")
            add("# Lines starting with # will be removed.")
            add("# If the patch applies cleanly, the edited hunk will immediately be marked for staging.")
            add("# If it does not apply cleanly, you will be given an opportunity to")
            add("# edit again.  If all lines of the hunk are removed, then the edit is")
            add("# aborted and the hunk is left unchanged.")
        }.joinToString("\n")
    }

    private fun patchDiffHeaderLines(file: GitFile): List<String> {
        return listOf(
            "diff --git a/${file.name} b/${file.name}",
            "index 0000000..0000001 100644",
            "--- a/${file.name}",
            "+++ b/${file.name}",
        )
    }

    private fun patchHunkBodyLines(file: GitFile): List<String> {
        val lines = file.content.lines()
        val nonEmptyLines = lines.dropLastWhile { it.isEmpty() }
        val addedCount = nonEmptyLines.size.coerceAtLeast(1)
        return buildList {
            add("@@ -1 +1,$addedCount @@")
            if (nonEmptyLines.isEmpty()) {
                add("+")
            } else {
                nonEmptyLines.forEach { line -> add("+$line") }
            }
        }
    }

    private fun interactiveAddConsoleLines(candidates: List<GitFile>): List<String> {
        return buildList {
            add("           staged     unstaged path")
            candidates.forEachIndexed { index, file ->
                val staged = if (file.staged) "unchanged" else "+0/-0"
                val unstaged = when {
                    file.tracked -> "+1/-0"
                    else -> "+0/-0"
                }
                add("${index + 1}: ${staged.padEnd(10)} ${unstaged.padEnd(9)} ${file.name}")
            }
            if (candidates.isEmpty()) {
                add("No changes.")
            }
            add("*** Commands ***")
            add("  1: status   2: update   3: revert   4: add untracked")
            add("  5: patch    6: diff     7: quit     8: help")
            add("What now>")
        }
    }

    private fun interactiveAddCandidates(repo: RepoState, target: String?): List<GitFile> {
        return repo.files.filter { file ->
            !file.deleted && (target == null || target == "." || file.name == target || file.name.startsWith(target.trimEnd('/') + "/"))
        }
    }

    private fun selectedInteractiveAddNames(candidates: List<GitFile>, answer: String): Set<String> {
        if (answer == "*") return candidates.map { it.name }.toSet()
        return answer.split(Regex("[,\\s]+"))
            .mapNotNull { token ->
                token.toIntOrNull()
                    ?.takeIf { it in 1..candidates.size }
                    ?.let { candidates[it - 1].name }
            }
            .toSet()
    }
}
