GradleGroovyBuildFileUpdater.kt

package com.depanalyzer.update

import com.depanalyzer.security.InputSafety
import java.io.File

class GradleGroovyBuildFileUpdater : BuildFileUpdater {
    override fun applyUpdate(buildFile: File, suggestion: UpdateSuggestion): Boolean {
        if (!InputSafety.isSafeVersion(suggestion.newVersion)) return false

        return when (suggestion.targetType) {
            UpdateTargetType.DIRECT -> applyDirectUpdate(buildFile, suggestion)
            UpdateTargetType.TRANSITIVE_OVERRIDE -> applyTransitiveOverride(buildFile, suggestion)
        }
    }

    private fun applyDirectUpdate(buildFile: File, suggestion: UpdateSuggestion): Boolean {
        val originalContent = buildFile.readText()
        var content = originalContent

        val stringNotation = Regex(
            """(['"])\Q${suggestion.groupId}\E:\Q${suggestion.artifactId}\E:([^'"]+)\1"""
        )
        val stringUpdated = replaceVersion(content, stringNotation, suggestion)
        if (stringUpdated != content) {
            buildFile.writeText(stringUpdated)
            return true
        }

        extractVersionToken(content, stringNotation)?.let { token ->
            val variableUpdated = replaceVariableVersion(content, token, suggestion)
            if (variableUpdated != content) {
                buildFile.writeText(variableUpdated)
                return true
            }
        }

        val mapNotation = Regex(
            """group\s*:\s*['"]\Q${suggestion.groupId}\E['"]\s*,\s*name\s*:\s*['"]\Q${suggestion.artifactId}\E['"]\s*,\s*version\s*:\s*(['"]?)([^,'")\s]+)\1"""
        )
        content = replaceVersion(content, mapNotation, suggestion)
        if (content != originalContent) {
            buildFile.writeText(content)
            return true
        }

        extractVersionToken(originalContent, mapNotation)?.let { token ->
            val variableUpdated = replaceVariableVersion(originalContent, token, suggestion)
            if (variableUpdated != originalContent) {
                buildFile.writeText(variableUpdated)
                return true
            }
        }

        return false
    }

    private fun applyTransitiveOverride(buildFile: File, suggestion: UpdateSuggestion): Boolean {
        val content = buildFile.readText()
        val updatedExisting = updateExistingConstraint(content, suggestion)
        if (updatedExisting != content) {
            buildFile.writeText(updatedExisting)
            return true
        }

        val dependenciesRange = findDependenciesBlockRange(content) ?: return false
        val dependencyLine = "implementation '${suggestion.groupId}:${suggestion.artifactId}:${suggestion.newVersion}'"
        val constraintsRegex = Regex("""constraints\s*\{([\s\S]*?)\}""")

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

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

    private fun updateExistingConstraint(content: String, suggestion: UpdateSuggestion): String {
        val regex = Regex("""(['"])\Q${suggestion.groupId}\E:\Q${suggestion.artifactId}\E:([^'"]+)\1""")
        return regex.replace(content) { match ->
            if (match.groupValues[2].trim() == suggestion.newVersion) match.value
            else "${match.groupValues[1]}${suggestion.groupId}:${suggestion.artifactId}:${suggestion.newVersion}${match.groupValues[1]}"
        }
    }

    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 replaceVersion(content: String, regex: Regex, suggestion: UpdateSuggestion): String {
        return regex.replace(content) { match ->
            val currentVersion = match.groupValues[2].trim()
            if (currentVersion == suggestion.currentVersion) {
                match.value.replace(currentVersion, suggestion.newVersion)
            } else {
                match.value
            }
        }
    }

    private fun extractVersionToken(content: String, regex: Regex): String? {
        val match = regex.find(content) ?: return null
        val token = match.groupValues[2].trim()
        return token.takeIf { it.isVariableToken() }
    }

    private fun replaceVariableVersion(content: String, token: String, suggestion: UpdateSuggestion): String {
        val variableName = token.toVariableName() ?: return content
        if (variableName.isBlank()) return content

        val patterns = listOf(
            Regex("""(\bdef\s+\Q$variableName\E\s*=\s*['"])([^'"]+)(['"])"""),
            Regex("""(\bext\.\Q$variableName\E\s*=\s*['"])([^'"]+)(['"])"""),
            Regex("""(^\s*\Q$variableName\E\s*=\s*['"])([^'"]+)(['"])""", setOf(RegexOption.MULTILINE))
        )

        for (pattern in patterns) {
            val updated = pattern.replace(content) { match ->
                if (match.groupValues[2].trim() == suggestion.currentVersion) {
                    "${match.groupValues[1]}${suggestion.newVersion}${match.groupValues[3]}"
                } else {
                    match.value
                }
            }
            if (updated != content) return updated
        }

        return content
    }

    private fun String.isVariableToken(): Boolean {
        return startsWith("\${") || startsWith("$") || startsWith("ext.") || matches(Regex("[A-Za-z_][A-Za-z0-9_]*"))
    }

    private fun String.toVariableName(): String? {
        return when {
            startsWith("\${") && endsWith("}") -> removePrefix("\${").removeSuffix("}")
            startsWith("$") -> removePrefix("$")
            startsWith("ext.") -> removePrefix("ext.")
            matches(Regex("[A-Za-z_][A-Za-z0-9_]*")) -> this
            else -> null
        }
    }
}