package solutions.tretter.githugandroid

import java.io.File

internal class GitRepositoryInspector(
    private val nativeGit: () -> File,
    private val runGit: (File, File, List<String>, Map<String, String>) -> ProcessExecutionResult,
) {
    fun inspect(sandbox: File, currentDir: String = "."): RepoState {
        val git = nativeGit()
        val workingDir = File(sandbox, currentDir).canonicalFile
            .takeIf { it.path == sandbox.canonicalPath || it.path.startsWith(sandbox.canonicalPath + File.separator) }
            ?: sandbox
        val repositoryRoot = generateSequence(workingDir) { directory ->
            directory.parentFile?.takeIf {
                it.path == sandbox.canonicalPath || it.path.startsWith(sandbox.canonicalPath + File.separator)
            }
        }.firstOrNull { File(it, ".git").exists() }
        val inspectionRoot = repositoryRoot ?: sandbox
        val filesOnDisk = inspectionRoot.walkTopDown()
            .filter { it.isFile && !it.relativeTo(inspectionRoot).path.startsWith(".git/") }
            .orEmpty()
            .toList()

        if (repositoryRoot == null) {
            return RepoState(
                initialized = false,
                files = filesOnDisk.map {
                    GitFile(name = it.relativeTo(inspectionRoot).path, content = it.readText())
                },
            )
        }

        val statusResult = run(git, inspectionRoot, listOf("status", "--porcelain"))
        val statusMap = mutableMapOf<String, Pair<Boolean, Boolean>>()
        val deletedStatusPaths = mutableSetOf<String>()
        statusResult.outputLines.forEach { line ->
            if (line.length < 4) return@forEach
            val x = line[0]
            val y = line[1]
            val path = line.substring(3).trim()
            val staged = x != ' ' && x != '?'
            val tracked = x != '?' || y != '?'
            statusMap[path] = staged to tracked
            if (x == 'D' || y == 'D') {
                deletedStatusPaths += path
            }
        }

        val logResult = run(git, inspectionRoot, listOf("log", "--pretty=format:%h\t%at\t%P\t%s"))
        val branchResult = run(git, inspectionRoot, listOf("branch", "--list"))
        val remoteBranchResult = run(git, inspectionRoot, listOf("branch", "-r", "--list"))
        val tagResult = run(git, inspectionRoot, listOf("tag", "--list"))
        val remoteResult = run(git, inspectionRoot, listOf("remote", "-v"))
        val headResult = run(git, inspectionRoot, listOf("branch", "--show-current"))
        val exactTagResult = run(git, inspectionRoot, listOf("describe", "--tags", "--exact-match"))
        val userNameResult = run(git, inspectionRoot, listOf("config", "--get", "user.name"))
        val userEmailResult = run(git, inspectionRoot, listOf("config", "--get", "user.email"))
        val stashResult = run(git, inspectionRoot, listOf("stash", "list", "--format=%gd"))
        val submodules = inspectSubmodules(git, inspectionRoot)
        val maintenanceActions = buildSet {
            if (File(inspectionRoot, ".git/objects/pack").listFiles()?.any { it.extension == "pack" } == true) {
                add("repack")
            }
        }
        val fetchHeadCount = File(inspectionRoot, ".git/FETCH_HEAD")
            .takeIf { it.isFile }
            ?.readLines()
            ?.count { it.isNotBlank() }
            ?: 0
        val config = buildMap {
            userNameResult.outputLines.firstOrNull()
                ?.takeIf { userNameResult.exitCode == 0 && it.isNotBlank() }
                ?.let { put("user.name", it) }
            userEmailResult.outputLines.firstOrNull()
                ?.takeIf { userEmailResult.exitCode == 0 && it.isNotBlank() }
                ?.let { put("user.email", it) }
        }

        val commits = if (logResult.exitCode == 0) {
            logResult.outputLines.filter { it.isNotBlank() }.mapNotNull { line ->
                val parts = line.split('\t', limit = 4)
                if (parts.isEmpty()) {
                    null
                } else {
                    val parentHashes = parts.getOrNull(2).orEmpty().split(Regex("\\s+")).filter { it.isNotBlank() }
                    CommitNode(
                        id = parts[0],
                        authorTimestampSeconds = parts.getOrNull(1)?.toLongOrNull(),
                        parentCount = parentHashes.size,
                        message = parts.getOrElse(3) { "" },
                    )
                }
            }
        } else {
            emptyList()
        }

        val branches = branchResult.outputLines
            .map { it.removePrefix("*").trim() }
            .filter { it.isNotBlank() && !it.startsWith("(") }
            .associateWith { branch ->
                run(git, inspectionRoot, listOf("rev-list", "--count", branch))
                    .outputLines
                    .firstOrNull()
                    ?.toIntOrNull()
                    ?: 0
            }
        val localBranchHeads = branches.keys.associateWith { branch ->
            run(git, inspectionRoot, listOf("rev-parse", "--verify", "refs/heads/$branch"))
                .outputLines
                .firstOrNull()
                .orEmpty()
        }
        val localTagHeads = tagResult.outputLines
            .filter { it.isNotBlank() }
            .associateWith { tag ->
                run(git, inspectionRoot, listOf("rev-list", "-n", "1", tag))
                    .outputLines
                    .firstOrNull()
                    .orEmpty()
            }
        val remoteRefs = inspectRemoteRefs(git, inspectionRoot, remoteResult.outputLines)
        val headBranch = headResult.outputLines.firstOrNull()
            ?.ifBlank { null }
            ?: exactTagResult.outputLines.firstOrNull()
                ?.takeIf { exactTagResult.exitCode == 0 && it.isNotBlank() }
                ?.let { "tags/$it" }
            ?: "DETACHED"

        return RepoState(
            initialized = true,
            files = filesOnDisk.map { file ->
                val relativePath = file.relativeTo(inspectionRoot).path
                val (staged, tracked) = statusMap[relativePath] ?: (false to true)
                GitFile(
                    name = relativePath,
                    content = file.readText(),
                    staged = staged,
                    tracked = tracked,
                )
            } + deletedStatusPaths
                .filterNot { deletedPath -> filesOnDisk.any { it.relativeTo(inspectionRoot).path == deletedPath } }
                .map { deletedPath ->
                    val (staged, tracked) = statusMap[deletedPath] ?: (false to true)
                    GitFile(
                        name = deletedPath,
                        staged = staged,
                        tracked = tracked,
                        deleted = true,
                    )
                },
            commits = commits,
            headBranch = headBranch,
            branches = branches,
            tags = tagResult.outputLines.filter { it.isNotBlank() },
            remotes = remoteResult.outputLines.mapNotNull { line ->
                val parts = line.trim().split(Regex("\\s+"))
                if (parts.size >= 2) parts[0] to parts[1] else null
            }.toMap(),
            config = config,
            stashes = stashResult.outputLines.filter { it.isNotBlank() },
            fetchedBranches = remoteBranchResult.outputLines
                .map { it.removePrefix("*").trim() }
                .filter { it.isNotBlank() && " -> " !in it }
                .toSet(),
            fetchHeadCount = fetchHeadCount,
            pushedBranches = remoteRefs.heads
                .filter { remoteHead -> localBranchHeads[remoteHead.branch] == remoteHead.hash }
                .map { remoteHead -> "${remoteHead.remote}/${remoteHead.branch}" }
                .toSet(),
            pushedTags = remoteRefs.tags
                .filter { remoteTag -> localTagHeads[remoteTag.tag] == remoteTag.hash }
                .map { it.tag }
                .toSet(),
            submodules = submodules,
            maintenanceActions = maintenanceActions,
        )
    }

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

    private fun inspectRemoteRefs(git: File, inspectionRoot: File, remoteLines: List<String>): RemoteRefs {
        val remotes = remoteLines.mapNotNull { line ->
            val parts = line.trim().split(Regex("\\s+"))
            if (parts.size >= 2) parts[0] to parts[1] else null
        }.toMap()
        val heads = mutableSetOf<RemoteHead>()
        val tags = mutableSetOf<RemoteTag>()

        remotes.forEach remote@{ (remoteName, remoteUrl) ->
            val output = run(git, inspectionRoot, listOf("ls-remote", remoteUrl))
            if (output.exitCode != 0) return@remote
            output.outputLines.forEach { line ->
                val parts = line.trim().split(Regex("\\s+"))
                if (parts.size < 2) return@forEach
                val hash = parts[0]
                val ref = parts[1].removeSuffix("^{}")
                when {
                    ref.startsWith("refs/heads/") -> {
                        heads += RemoteHead(remoteName, ref.removePrefix("refs/heads/"), hash)
                    }
                    ref.startsWith("refs/tags/") -> {
                        tags += RemoteTag(ref.removePrefix("refs/tags/"), hash)
                    }
                }
            }
        }

        return RemoteRefs(heads = heads, tags = tags)
    }

    private fun inspectSubmodules(git: File, inspectionRoot: File): Map<String, String> {
        val gitmodules = File(inspectionRoot, ".gitmodules")
        if (!gitmodules.isFile) return emptyMap()

        val output = run(git, inspectionRoot, listOf("config", "--file", gitmodules.absolutePath, "--get-regexp", "submodule\\..*\\.(path|url)"))
        if (output.exitCode != 0) return emptyMap()

        val entries = mutableMapOf<String, MutableMap<String, String>>()
        output.outputLines.forEach { line ->
            val key = line.substringBefore(' ')
            val value = line.substringAfter(' ', missingDelimiterValue = "").trim()
            val parts = key.split('.')
            if (parts.size != 3 || parts[0] != "submodule") return@forEach
            entries.getOrPut(parts[1]) { mutableMapOf() }[parts[2]] = value
        }

        return entries.values.mapNotNull { values ->
            val path = values["path"]?.trimEnd('/')
            val url = values["url"]
            if (path != null && url != null) path to url else null
        }.toMap()
    }

    private data class RemoteRefs(
        val heads: Set<RemoteHead> = emptySet(),
        val tags: Set<RemoteTag> = emptySet(),
    )

    private data class RemoteHead(
        val remote: String,
        val branch: String,
        val hash: String,
    )

    private data class RemoteTag(
        val tag: String,
        val hash: String,
    )
}
