GradleKotlinBuildFileUpdater.kt

package com.depanalyzer.update

import com.depanalyzer.parser.LibraryInfo
import com.depanalyzer.parser.VersionCatalogParser
import com.depanalyzer.security.InputSafety
import java.io.File

class GradleKotlinBuildFileUpdater(
    private val catalogParser: VersionCatalogParser = VersionCatalogParser()
) : BuildFileUpdater {
    override fun applyUpdate(buildFile: File, suggestion: UpdateSuggestion): Boolean {
        if (!InputSafety.isSafeVersion(suggestion.newVersion)) return false

        if (suggestion.targetType == UpdateTargetType.TRANSITIVE_OVERRIDE) {
            return applyTransitiveOverride(buildFile, suggestion)
        }

        val content = buildFile.readText()
        val stringNotation = Regex(
            """(['"])\Q${suggestion.groupId}\E:\Q${suggestion.artifactId}\E:([^'"]+)\1"""
        )

        val replaced = stringNotation.replace(content) { match ->
            val current = match.groupValues[2].trim()
            if (current == suggestion.currentVersion) {
                match.value.replace(current, suggestion.newVersion)
            } else {
                match.value
            }
        }

        if (replaced != content) {
            buildFile.writeText(replaced)
            return true
        }

        return updateVersionCatalog(buildFile, content, suggestion)
    }

    private fun applyTransitiveOverride(buildFile: File, suggestion: UpdateSuggestion): Boolean {
        val content = buildFile.readText()
        val directUpdated = applyUpdate(buildFile, suggestion.copy(targetType = UpdateTargetType.DIRECT))
        if (directUpdated) return true

        val refreshed = buildFile.readText()
        val existingConstraintRegex = Regex(
            """implementation\s*\(\s*['"]\Q${suggestion.groupId}\E:\Q${suggestion.artifactId}\E:([^'"]+)['"]\s*\)"""
        )
        val updatedExisting = existingConstraintRegex.replace(refreshed) { match ->
            if (match.groupValues[1].trim() == suggestion.newVersion) match.value
            else "implementation(\"${suggestion.groupId}:${suggestion.artifactId}:${suggestion.newVersion}\")"
        }
        if (updatedExisting != refreshed) {
            buildFile.writeText(updatedExisting)
            return true
        }

        val dependenciesRange = findDependenciesBlockRange(refreshed) ?: return false
        val dependenciesContent = refreshed.substring(dependenciesRange.first, dependenciesRange.last + 1)
        val line = "implementation(\"${suggestion.groupId}:${suggestion.artifactId}:${suggestion.newVersion}\")"
        val constraintsRegex = Regex("""constraints\s*\{([\s\S]*?)\}""")

        val newDependenciesContent = if (constraintsRegex.containsMatchIn(dependenciesContent)) {
            val match = constraintsRegex.find(dependenciesContent)!!
            val replacement = run {
                val body = match.groupValues[1]
                "constraints {$body\n        $line\n    }"
            }
            dependenciesContent.replaceRange(match.range, replacement)
        } else {
            dependenciesContent.replaceRange(
                dependenciesContent.lastIndex,
                dependenciesContent.lastIndex + 1,
                "\n    constraints {\n        $line\n    }\n}"
            )
        }

        val updated =
            refreshed.replaceRange(dependenciesRange.first, dependenciesRange.last + 1, newDependenciesContent)
        if (updated == refreshed) return false
        buildFile.writeText(updated)
        return true
    }

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

        var depth = 0
        for (index in openBrace until content.length) {
            when (content[index]) {
                '{' -> depth++
                '}' -> {
                    depth--
                    if (depth == 0) return openBrace..index
                }
            }
        }
        return null
    }

    private fun updateVersionCatalog(buildFile: File, buildContent: String, suggestion: UpdateSuggestion): Boolean {
        val catalogFile = File(buildFile.parentFile, "gradle/libs.versions.toml")
        if (!catalogFile.exists()) return false

        val catalog = catalogParser.parse(catalogFile)
        val matchingAliases = catalog.libraries.filterValues { lib ->
            lib.group == suggestion.groupId && lib.name == suggestion.artifactId
        }
        if (matchingAliases.isEmpty()) return false

        val usedAliases = matchingAliases.filterKeys { alias -> isAliasUsed(buildContent, alias) }
        if (usedAliases.isEmpty()) return false

        var toml = catalogFile.readText()
        var changed = false
        for ((alias, libInfo) in usedAliases) {
            val updated = updateTomlForAlias(toml, alias, libInfo, suggestion)
            if (updated != toml) {
                toml = updated
                changed = true
            }
        }

        if (!changed) return false
        catalogFile.writeText(toml)
        return true
    }

    private fun isAliasUsed(buildContent: String, alias: String): Boolean {
        val dotAlias = alias.replace('-', '.')
        return Regex("""\blibs\.\Q$alias\E\b""").containsMatchIn(buildContent) ||
                Regex("""\blibs\.\Q$dotAlias\E\b""").containsMatchIn(buildContent)
    }

    private fun updateTomlForAlias(
        toml: String,
        alias: String,
        libInfo: LibraryInfo,
        suggestion: UpdateSuggestion
    ): String {
        libInfo.versionRef?.let { ref ->
            val versionsSectionUpdated = updateVersionInVersionsSection(toml, ref, suggestion)
            if (versionsSectionUpdated != toml) return versionsSectionUpdated
        }

        if (!libInfo.version.isNullOrBlank()) {
            val inlineMapUpdated = updateInlineLibraryVersion(toml, alias, suggestion)
            if (inlineMapUpdated != toml) return inlineMapUpdated

            val stringNotationUpdated = updateStringLibraryVersion(toml, alias, suggestion)
            if (stringNotationUpdated != toml) return stringNotationUpdated
        }

        return toml
    }

    private fun updateVersionInVersionsSection(toml: String, key: String, suggestion: UpdateSuggestion): String {
        val sectionRegex = Regex("""(\[versions]\s*[\s\S]*?)(?=\n\[[^]]+]|$)""")
        val sectionMatch = sectionRegex.find(toml) ?: return toml
        val section = sectionMatch.groupValues[1]

        val lineRegex = Regex("""(^\s*\Q$key\E\s*=\s*['"])([^'"]+)(['"].*$)""", setOf(RegexOption.MULTILINE))
        val updatedSection = lineRegex.replace(section) { match ->
            if (match.groupValues[2].trim() == suggestion.currentVersion) {
                "${match.groupValues[1]}${suggestion.newVersion}${match.groupValues[3]}"
            } else {
                match.value
            }
        }

        if (updatedSection == section) return toml
        return toml.replaceRange(sectionMatch.range, updatedSection)
    }

    private fun updateInlineLibraryVersion(toml: String, alias: String, suggestion: UpdateSuggestion): String {
        val lineRegex = Regex(
            """(^\s*\Q$alias\E\s*=\s*\{[^\n]*?version\s*=\s*['"])([^'"]+)(['"][^\n]*}\s*$)""",
            setOf(RegexOption.MULTILINE)
        )

        return lineRegex.replace(toml) { match ->
            if (match.groupValues[2].trim() == suggestion.currentVersion) {
                "${match.groupValues[1]}${suggestion.newVersion}${match.groupValues[3]}"
            } else {
                match.value
            }
        }
    }

    private fun updateStringLibraryVersion(toml: String, alias: String, suggestion: UpdateSuggestion): String {
        val lineRegex = Regex(
            """(^\s*\Q$alias\E\s*=\s*['"][^:'"]+:[^:'"]+:)([^'"]+)(['"].*$)""",
            setOf(RegexOption.MULTILINE)
        )

        return lineRegex.replace(toml) { match ->
            if (match.groupValues[2].trim() == suggestion.currentVersion) {
                "${match.groupValues[1]}${suggestion.newVersion}${match.groupValues[3]}"
            } else {
                match.value
            }
        }
    }
}