package solutions.tretter.githugandroid

import android.content.Context
import android.system.Os
import java.io.File
import java.nio.file.Files

internal class GitProcessRunner(
    private val context: Context?,
    private val sandboxesRoot: File,
) {
    private companion object {
        val RequiredGitCommandAliases = listOf(
            "add",
            "branch",
            "checkout",
            "commit",
            "commit-tree",
            "config",
            "diff",
            "fetch",
            "index-pack",
            "merge",
            "merge-base",
            "pack-objects",
            "pull",
            "push",
            "read-tree",
            "rebase",
            "receive-pack",
            "rev-list",
            "rev-parse",
            "show-ref",
            "stash",
            "status",
            "symbolic-ref",
            "unpack-objects",
            "update-ref",
            "upload-pack",
        )
    }

    fun shellExecutable(): String {
        return when {
            context != null -> "/system/bin/sh"
            File("/bin/sh").exists() -> "/bin/sh"
            else -> "sh"
        }
    }

    fun runGit(
        binary: File,
        workingDir: File,
        arguments: List<String>,
        environment: Map<String, String> = emptyMap(),
    ): ProcessExecutionResult {
        return runProcess(binary, workingDir, arguments, environment)
    }

    fun runShellProcess(binary: File, workingDir: File, arguments: List<String>): ProcessExecutionResult {
        return try {
            val process = ProcessBuilder(listOf(binary.absolutePath) + arguments)
                .directory(workingDir)
                .redirectErrorStream(true)
                .apply {
                    environment()["HOME"] = workingDir.absolutePath
                    environment()["LC_ALL"] = "C"
                }
                .start()

            val output = process.inputStream.bufferedReader().readLines()
            val exit = process.waitFor()
            ProcessExecutionResult(exitCode = exit, outputLines = output)
        } catch (error: Exception) {
            ProcessExecutionResult(exitCode = -1, outputLines = listOf("Shell execution failed: ${error.message ?: error::class.java.simpleName}"))
        }
    }

    private fun runProcess(
        binary: File,
        workingDir: File,
        arguments: List<String>,
        extraEnvironment: Map<String, String> = emptyMap(),
    ): ProcessExecutionResult {
        return try {
            val gitExecPath = gitExecDirectory(binary)
            val process = ProcessBuilder(listOf(binary.absolutePath) + arguments)
                .directory(workingDir)
                .redirectErrorStream(true)
                .apply {
                    environment()["HOME"] = workingDir.absolutePath
                    environment()["GIT_EXEC_PATH"] = gitExecPath.absolutePath
                    environment()["PATH"] = listOfNotNull(
                        gitExecPath.absolutePath,
                        System.getenv("PATH")?.takeIf { it.isNotBlank() },
                    ).joinToString(File.pathSeparator)
                    environment()["GIT_CONFIG_NOSYSTEM"] = "1"
                    environment()["GIT_AUTHOR_NAME"] = "GitHug"
                    environment()["GIT_AUTHOR_EMAIL"] = "githug@example.com"
                    environment()["GIT_COMMITTER_NAME"] = "GitHug"
                    environment()["GIT_COMMITTER_EMAIL"] = "githug@example.com"
                    environment()["LC_ALL"] = "C"
                    environment().putAll(extraEnvironment)
                }
                .start()

            val output = process.inputStream.bufferedReader().readLines()
            val exit = process.waitFor()
            ProcessExecutionResult(exitCode = exit, outputLines = output)
        } catch (error: Exception) {
            ProcessExecutionResult(exitCode = -1, outputLines = listOf("Native Git execution failed: ${error.message ?: error::class.java.simpleName}"))
        }
    }

    private fun gitExecDirectory(binary: File): File {
        val directory = if (context != null) {
            File(context.filesDir, "git-exec")
        } else {
            File(binary.parentFile ?: binary.absoluteFile.parentFile ?: sandboxesRoot, "git-exec")
        }
        refreshGitExecDirectoryIfNeeded(directory, binary)
        directory.mkdirs()

        File(directory, "git").also { alias ->
            if (!alias.exists()) {
                createGitAlias(binary, alias)
            }
        }
        RequiredGitCommandAliases.forEach { command ->
            listOf("git-$command", command).forEach { aliasName ->
                val alias = File(directory, aliasName)
                if (!alias.exists()) {
                    createGitAlias(binary, alias)
                }
            }
        }
        return directory
    }

    private fun refreshGitExecDirectoryIfNeeded(directory: File, binary: File) {
        val fingerprint = buildString {
            append(binary.absolutePath)
            append('\n')
            append(binary.length())
            append('\n')
            append(binary.lastModified())
        }
        val stamp = File(directory, ".binary-fingerprint")
        val previousFingerprint = stamp.takeIf { it.isFile }?.readText()
        if (previousFingerprint != fingerprint) {
            directory.deleteRecursively()
        }
        directory.mkdirs()
        stamp.writeText(fingerprint)
    }

    private fun createGitAlias(binary: File, alias: File) {
        if (alias.exists() || Files.isSymbolicLink(alias.toPath())) {
            alias.delete()
        }
        if (context != null) {
            try {
                Os.symlink(binary.absolutePath, alias.absolutePath)
                alias.setExecutable(true, false)
                return
            } catch (_: Exception) {
                // Fall through to the portable options below.
            }
        }

        try {
            Files.createSymbolicLink(alias.toPath(), binary.toPath())
        } catch (_: Exception) {
            binary.copyTo(alias, overwrite = true)
        }
        alias.setExecutable(true, false)
    }
}

internal data class ProcessExecutionResult(
    val exitCode: Int,
    val outputLines: List<String>,
)
