GradleDependencyTreeParser.kt

package com.depanalyzer.parser.gradle

import com.depanalyzer.core.graph.DependencyNode

object GradleDependencyTreeParser {

    fun parse(output: String, verbose: Boolean = false): List<DependencyNode> {
        if (output.isEmpty()) return emptyList()

        val result = mutableListOf<DependencyNode>()
        val projects = extractProjects(output).ifEmpty {
            if (verbose) {
                System.err.println("[GradleDependencyTreeParser] No explicit project headers found, parsing entire output")
            }
            listOf("root" to output)
        }

        if (verbose) {
            System.err.println("[GradleDependencyTreeParser] Found ${projects.size} projects")
        }

        for ((projectName, projectContent) in projects) {
            val configurations = extractConfigurations(projectContent)
            if (verbose) {
                System.err.println("[GradleDependencyTreeParser] Project '$projectName': ${configurations.size} configurations")
            }

            for ((configName, configContent) in configurations) {
                val treeLines = parseTreeLines(configContent, configName)
                if (treeLines.isNotEmpty()) {
                    val nodes = buildHierarchy(treeLines)
                    result.addAll(nodes)
                }
            }
        }

        return deduplicateRoots(result)
    }

    private fun extractProjects(output: String): List<Pair<String, String>> {
        val projectRegex = Regex(
            pattern = """^(?:Root\s+)?project\s+'([^']*)'""",
            options = setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE)
        )
        val matches = projectRegex.findAll(output)
        val projects = mutableListOf<Pair<String, String>>()

        val positions = matches.map { it.range.first to it.groupValues[1] }.toList()

        for (i in positions.indices) {
            val startPos = positions[i].first
            val projectName = positions[i].second
            val endPos = if (i < positions.size - 1) positions[i + 1].first else output.length

            val projectContent = output.substring(startPos, endPos)
            projects.add(projectName to projectContent)
        }

        return projects
    }

    private fun extractConfigurations(projectContent: String): List<Pair<String, String>> {
        val configRegex = Regex("""(?m)^([A-Za-z][A-Za-z0-9-]*)\s*-\s*""")
        val matches = configRegex.findAll(projectContent)
        val configurations = mutableListOf<Pair<String, String>>()

        val positions = matches.map { it.range.first to it.groupValues[1] }.toList()

        for (i in positions.indices) {
            val startPos = positions[i].first
            val configName = positions[i].second
            val endPos = if (i < positions.size - 1) positions[i + 1].first else projectContent.length

            val configContent = projectContent.substring(startPos, endPos)
            configurations.add(configName to configContent)
        }

        return configurations
    }

    private fun parseTreeLines(content: String, configName: String): List<TreeLine> {
        val result = mutableListOf<TreeLine>()
        val scope = mapConfigurationToScope(configName)

        content.split("\n")
            .filter { it.isNotEmpty() && (it.contains("---") || it.contains(":")) }
            .forEach { line ->
                parseTreeLine(line, scope)?.let { result.add(it) }
            }

        return result
    }

    private fun parseTreeLine(line: String, scope: String): TreeLine? {
        if (line.contains("(c)")) {
            return null
        }

        val depth = calculateDepth(line)

        // Extract the dependency coordinate
        val coordinateRegex = Regex("""([a-zA-Z0-9\-_.]+):([a-zA-Z0-9\-_.]+):([a-zA-Z0-9\-_.]+)""")
        val match = coordinateRegex.find(line) ?: return null

        val groupId = match.groupValues[1]
        val artifactId = match.groupValues[2]
        val version = match.groupValues[3]

        // Check for annotations
        val isExcluded = line.contains("(*)") || line.contains("(n)") || line.contains("(c)")
        val isDirect = depth == 0

        return TreeLine(
            depth = depth,
            line = line.trim(),
            groupId = groupId,
            artifactId = artifactId,
            version = version,
            scope = scope,
            isDirect = isDirect,
            isExcluded = isExcluded
        )
    }

    private fun calculateDepth(line: String): Int {
        var depth = 0
        for (char in line) {
            if (char in "| +\\" || char == '-') {
                if (char != '-') depth++
                if (char == '\\' || char == '+') break
            } else if (char != ' ') {
                break
            }
        }
        return maxOf(0, depth / 4)  // Rough estimate: every 4 chars per level
    }

    private fun buildHierarchy(lines: List<TreeLine>): List<DependencyNode> {
        if (lines.isEmpty()) return emptyList()

        val result = mutableListOf<DependencyNode>()
        val stack = mutableListOf<Pair<DependencyNode, Int>>()  // Node + depth

        for (line in lines) {
            val node = DependencyNode(
                id = "${line.groupId}:${line.artifactId}:${line.version}",
                groupId = line.groupId,
                artifactId = line.artifactId,
                version = line.version,
                parent = null,
                children = mutableListOf(),
                scope = line.scope,
                isDependencyManagement = false
            )

            // Pop stack until we find the parent at a shallower depth
            while (stack.isNotEmpty() && stack.last().second >= line.depth) {
                stack.removeAt(stack.size - 1)
            }

            if (stack.isEmpty()) {
                // This is a root node
                result.add(node)
            } else {
                // Add as child to the node at the top of the stack
                val parent = stack.last().first
                val nodeWithParent = node.copy(parent = parent)
                parent.addChild(nodeWithParent)
                stack.add(nodeWithParent to line.depth)
                continue
            }

            stack.add(node to line.depth)
        }

        return result
    }

    private fun deduplicateRoots(nodes: List<DependencyNode>): List<DependencyNode> {
        if (nodes.isEmpty()) return emptyList()

        val deduped = linkedMapOf<String, DependencyNode>()
        for (node in nodes) {
            val existing = deduped[node.coordinate]
            deduped[node.coordinate] = if (existing == null) {
                copyTree(node, parent = null)
            } else {
                mergeTrees(existing, node, parent = null)
            }
        }
        return deduped.values.toList()
    }

    private fun mergeTrees(base: DependencyNode, incoming: DependencyNode, parent: DependencyNode?): DependencyNode {
        val baseChildrenByCoordinate = base.children.associateBy { it.coordinate }.toMutableMap()
        for (incomingChild in incoming.children) {
            val existing = baseChildrenByCoordinate[incomingChild.coordinate]
            baseChildrenByCoordinate[incomingChild.coordinate] = if (existing == null) {
                copyTree(incomingChild, parent = null)
            } else {
                mergeTrees(existing, incomingChild, parent = null)
            }
        }

        val mergedChildren = baseChildrenByCoordinate.values.sortedBy { it.coordinate }.toMutableList()
        val chosenScope = pickPreferredScope(base.scope, incoming.scope)

        val merged = DependencyNode(
            id = base.id,
            groupId = base.groupId,
            artifactId = base.artifactId,
            version = base.version,
            parent = parent,
            children = mutableListOf(),
            vulnerabilities = base.vulnerabilities,
            scope = chosenScope,
            isDependencyManagement = base.isDependencyManagement || incoming.isDependencyManagement
        )

        val childrenWithParent = mergedChildren.map { child ->
            setParentRecursively(child, merged)
        }
        merged.children.addAll(childrenWithParent)
        return merged
    }

    private fun copyTree(node: DependencyNode, parent: DependencyNode?): DependencyNode {
        val copy = DependencyNode(
            id = node.id,
            groupId = node.groupId,
            artifactId = node.artifactId,
            version = node.version,
            parent = parent,
            children = mutableListOf(),
            vulnerabilities = node.vulnerabilities,
            scope = node.scope,
            isDependencyManagement = node.isDependencyManagement
        )
        copy.children.addAll(node.children.map { child -> copyTree(child, copy) })
        return copy
    }

    private fun setParentRecursively(node: DependencyNode, parent: DependencyNode): DependencyNode {
        return copyTree(node, parent)
    }

    private fun pickPreferredScope(primary: String?, secondary: String?): String? {
        val score = mapOf(
            "compile" to 4,
            "runtime" to 3,
            "provided" to 2,
            "test" to 1
        )

        val left = primary?.lowercase()
        val right = secondary?.lowercase()
        if (left == null) return secondary
        if (right == null) return primary

        return if ((score[left] ?: 0) >= (score[right] ?: 0)) primary else secondary
    }

    private fun mapConfigurationToScope(configName: String): String {
        return when {
            configName.contains("compile", ignoreCase = true) && !configName.contains("test", ignoreCase = true) ->
                "compile"

            configName.contains("runtime", ignoreCase = true) && !configName.contains("test", ignoreCase = true) ->
                "runtime"

            configName.contains("test", ignoreCase = true) -> "test"
            configName.contains("provided", ignoreCase = true) -> "provided"
            else -> "compile"  // default
        }
    }

    /**
     * Represents a parsed line from the dependency tree.
     */
    private data class TreeLine(
        val depth: Int,
        val line: String,
        val groupId: String,
        val artifactId: String,
        val version: String,
        val scope: String,
        val isDirect: Boolean = false,
        val isExcluded: Boolean = false
    )
}