PyprojectBuildFileUpdater.kt

package com.depanalyzer.update

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

class PyprojectBuildFileUpdater : BuildFileUpdater {
    override fun applyUpdate(buildFile: File, suggestion: UpdateSuggestion): Boolean {
        if (!InputSafety.isSafeVersion(suggestion.newVersion)) return false
        if (!buildFile.exists() || !buildFile.isFile || buildFile.name != "pyproject.toml") return false

        val packageName = suggestion.artifactId
        val lines = buildFile.readLines().toMutableList()

        val updatedDirect = updateExistingDependency(lines, packageName, suggestion.newVersion)
        if (updatedDirect) {
            buildFile.writeText(lines.joinToString("\n") + "\n")
            return true
        }

        if (suggestion.targetType == UpdateTargetType.TRANSITIVE_OVERRIDE) {
            val inserted = insertInPoetryMainSection(lines, packageName, suggestion.newVersion)
            if (inserted) {
                buildFile.writeText(lines.joinToString("\n") + "\n")
                return true
            }
        }

        return false
    }

    private fun updateExistingDependency(lines: MutableList<String>, packageName: String, newVersion: String): Boolean {
        var section = ""
        for (index in lines.indices) {
            val line = lines[index]
            val clean = line.substringBefore('#').trim()
            if (clean.startsWith("[") && clean.endsWith("]")) {
                section = clean.removePrefix("[").removeSuffix("]")
                continue
            }

            val inPoetryDeps = section == "tool.poetry.dependencies" ||
                    (section.startsWith("tool.poetry.group.") && section.endsWith(".dependencies"))
            if (!inPoetryDeps) continue

            val key = clean.substringBefore('=', "").trim().trim('"', '\'')
            if (!key.equals(packageName, ignoreCase = true)) continue

            lines[index] = updateLineValue(line, newVersion)
            return true
        }

        return false
    }

    private fun updateLineValue(line: String, newVersion: String): String {
        val comment = if (line.contains('#')) line.substringAfter('#') else ""
        val beforeComment = line.substringBefore('#')

        val quotedValueRegex = Regex("""^\s*([^=]+)=\s*['\"]([^'\"]+)['\"]\s*$""")
        quotedValueRegex.matchEntire(beforeComment)?.let { match ->
            val keyPart = match.groupValues[1].trim()
            val currentValue = match.groupValues[2].trim()
            val replacement = preservePrefix(currentValue, newVersion)
            return "$keyPart = \"$replacement\"" + if (comment.isNotBlank()) " #$comment" else ""
        }

        val inlineVersionRegex = Regex("""(version\s*=\s*['\"])([^'\"]+)(['\"])""")
        if (inlineVersionRegex.containsMatchIn(beforeComment)) {
            val replaced = inlineVersionRegex.replace(beforeComment) { match ->
                val currentValue = match.groupValues[2].trim()
                val replacement = preservePrefix(currentValue, newVersion)
                "${match.groupValues[1]}$replacement${match.groupValues[3]}"
            }
            return replaced + if (comment.isNotBlank()) " #$comment" else ""
        }

        return line
    }

    private fun insertInPoetryMainSection(lines: MutableList<String>, packageName: String, newVersion: String): Boolean {
        var sectionStart = -1
        var insertIndex = -1

        for (index in lines.indices) {
            val clean = lines[index].substringBefore('#').trim()
            if (clean == "[tool.poetry.dependencies]") {
                sectionStart = index
                insertIndex = index + 1
                continue
            }

            if (sectionStart != -1) {
                if (clean.startsWith("[") && clean.endsWith("]")) {
                    break
                }
                insertIndex = index + 1
            }
        }

        if (sectionStart == -1 || insertIndex == -1) return false

        val newLine = "$packageName = \"$newVersion\""
        lines.add(insertIndex, newLine)
        return true
    }

    private fun preservePrefix(currentSpec: String, newVersion: String): String {
        val trimmed = currentSpec.trim()
        return when {
            trimmed.startsWith("^") -> "^$newVersion"
            trimmed.startsWith("~") -> "~$newVersion"
            trimmed.startsWith(">=") -> ">=$newVersion"
            trimmed.startsWith("<=") -> "<=$newVersion"
            trimmed.startsWith(">") -> ">$newVersion"
            trimmed.startsWith("<") -> "<$newVersion"
            trimmed.startsWith("=") -> "=$newVersion"
            else -> newVersion
        }
    }
}