GradleKotlinDependencyParser.kt

package com.depanalyzer.parser

import com.depanalyzer.repository.ProjectRepository
import java.io.File

class GradleKotlinDependencyParser(
    private val catalog: VersionCatalog = VersionCatalog(),
    private val repoParser: GradleRepositoryParser = GradleRepositoryParser()
) {
    fun parse(buildFile: File): List<ParsedGradleDependency> {
        require(buildFile.exists() && buildFile.isFile) { "Invalid build.gradle.kts path: ${buildFile.absolutePath}" }
        require(buildFile.name == "build.gradle.kts") { "Expected build.gradle.kts, got ${buildFile.name}" }

        val content = buildFile.readText()
        val dependenciesBody = extractDependenciesBody(content) ?: return emptyList()

        val cleanBody = stripBlockComments(dependenciesBody)
        val result = mutableListOf<ParsedGradleDependency>()
        cleanBody.lines().forEach { line ->
            parseDependencyLine(line)?.let(result::add)
        }

        return result
    }

    fun repositories(buildFile: File): List<ProjectRepository> {
        return repoParser.parse(buildFile)
    }

    private fun stripBlockComments(content: String): String {
        val blockCommentRegex = Regex("""/\*[\s\S]*?\*/""")
        return content.replace(blockCommentRegex, "")
    }

    private fun extractDependenciesBody(content: String): String? {
        val startRegex = Regex("""\bdependencies\s*\{""")
        val startMatch = startRegex.find(content) ?: return null
        val start = startMatch.range.first
        val openBrace = content.indexOf('{', start)
        if (openBrace == -1) return null

        var depth = 0
        var index = openBrace
        while (index < content.length) {
            when (content[index]) {
                '{' -> depth++
                '}' -> {
                    depth--
                    if (depth == 0) {
                        return content.substring(openBrace + 1, index)
                    }
                }
            }
            index++
        }
        return null
    }

    private fun parseDependencyLine(line: String): ParsedGradleDependency? {
        val noComment = line.substringBefore("//").trim()
        if (noComment.isBlank()) return null
        if (noComment.contains("project(")) return null

        // 1. String notation: implementation("group:artifact:version")
        val stringNotation = Regex("""^([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*['"]([^:'"]+):([^:'"]+):([^'"]+)['"]\s*\)$""")
        stringNotation.matchEntire(noComment)?.let { match ->
            return ParsedGradleDependency(
                groupId = match.groupValues[2],
                artifactId = match.groupValues[3],
                version = match.groupValues[4],
                configuration = match.groupValues[1]
            )
        }

        // 2. Catalog notation: implementation(libs.alias) or implementation(libs.alias.name)
        val catalogNotation = Regex("""^([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*libs\.([A-Za-z0-9._-]+)\s*\)$""")
        catalogNotation.matchEntire(noComment)?.let { match ->
            val configuration = match.groupValues[1]
            val aliasRaw = match.groupValues[2]
            // Aliases in libs.xxx.yyy are often libs.xxx-yyy in TOML
            val alias = aliasRaw.replace(".", "-")
            
            val libInfo = catalog.libraries[alias] ?: catalog.libraries[aliasRaw]
            if (libInfo != null) {
                val version = libInfo.version ?: libInfo.versionRef?.let { catalog.versions[it] }
                return ParsedGradleDependency(
                    groupId = libInfo.group,
                    artifactId = libInfo.name,
                    version = version,
                    configuration = configuration
                )
            }
        }

        return null
    }
}