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)
    private val repositoryInspector = GitRepositoryInspector(::requireNativeGit, ::runGit)
    private val editorWorkflow = GitEditorWorkflow(
        requireNativeGit = ::requireNativeGit,
        prepareLevel = ::prepareLevel,
        sandboxDir = ::sandboxDir,
        inspectSandbox = ::inspectSandbox,
        runGit = ::runGit,
        shellExecutable = processRunner::shellExecutable,
    )

    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 {
        return nativeGitBinary() != null && (context == null || NativeGitBridge.isAvailable())
    }

    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")
            if (context != null && !NativeGitBridge.isAvailable()) {
                append("\nNative bridge: unavailable")
            }
            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)
            }
        }
        File(sandbox, desired.currentDir).mkdirs()

        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, desired.currentDir).copy(currentDir = desired.currentDir)
    }

    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, currentRepo.currentDir).copy(currentDir = currentRepo.currentDir) to emptyList()
        val expandedTokens = if (tokens.firstOrNull() == "echo") {
            tokens
        } else {
            expandShellPathspecs(currentRepo, invocation.command)
        }

        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, result.first.currentDir).copy(currentDir = result.first.currentDir)
        return inspectedRepo 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("  invocation: init_git(argv), then cmd_main(argc, argv)")
            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, currentRepo.currentDir).copy(currentDir = currentRepo.currentDir) to listOf("Saved $path")
    }

    fun gitEditorInitialContent(level: Level, currentRepo: RepoState, invocation: GitEditorInvocation): String {
        return editorWorkflow.initialContent(level, currentRepo, invocation)
    }

    fun executeGitEditorCommand(
        level: Level,
        currentRepo: RepoState,
        invocation: GitEditorInvocation,
        message: String,
    ): Pair<RepoState, List<String>> {
        return editorWorkflow.execute(level, currentRepo, invocation, message)
    }

    private fun inspectSandbox(level: Level, currentDir: String = "."): RepoState =
        repositoryInspector.inspect(sandboxDir(level), currentDir)

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

    private fun requireNativeGit(): File {
        val binary = nativeGitBinary() ?: error(unavailableMessage())
        if (context != null && !NativeGitBridge.isAvailable()) {
            error(unavailableMessage())
        }
        return binary
    }

    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 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 String.isShellEnvironmentName(): Boolean {
        if (isEmpty()) return false
        if (first() != '_' && !first().isLetter()) return false
        return all { it == '_' || it.isLetterOrDigit() }
    }

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

}
