PoetryLockParser.kt

package com.depanalyzer.parser.python

import com.depanalyzer.core.graph.DependencyNode
import com.depanalyzer.parser.Ecosystem
import com.depanalyzer.parser.ParsedDependency
import java.io.File

class PoetryLockParser {
    fun parse(poetryLockFile: File, directDependencies: List<ParsedDependency>): List<DependencyNode> {
        if (!poetryLockFile.exists() || !poetryLockFile.isFile) return emptyList()
        if (poetryLockFile.name != "poetry.lock") return emptyList()

        val content = poetryLockFile.readText()
        val blocks = extractPackageBlocks(content)
        if (blocks.isEmpty()) return emptyList()

        data class LockPackage(val name: String, val version: String, val dependencies: Set<String>)

        val packageMap = mutableMapOf<String, LockPackage>()
        blocks.forEach { block ->
            val name = Regex("""(?m)^name\s*=\s*['\"]([^'\"]+)['\"]""").find(block)
                ?.groupValues?.get(1)
                ?.trim()
                ?.lowercase()
                ?.replace('_', '-')
                ?: return@forEach
            val version = Regex("""(?m)^version\s*=\s*['\"]([^'\"]+)['\"]""").find(block)
                ?.groupValues?.get(1)
                ?.trim()
                ?: return@forEach

            val dependencies = extractDependencies(block)
            packageMap[name] = LockPackage(name, version, dependencies)
        }

        if (packageMap.isEmpty()) return emptyList()

        val scopeByName = directDependencies.associate { dep ->
            dep.artifactId.lowercase().replace('_', '-') to dep.scope
        }

        val rootNames = if (directDependencies.isNotEmpty()) {
            directDependencies.map { it.artifactId.lowercase().replace('_', '-') }.toSet()
        } else {
            packageMap.keys
        }

        fun buildNode(name: String, visiting: MutableSet<String>): DependencyNode? {
            val lockPackage = packageMap[name] ?: return null
            val key = "$name@${lockPackage.version}"
            if (!visiting.add(key)) return null

            val node = DependencyNode(
                id = "pypi:${lockPackage.name}:${lockPackage.version}",
                groupId = "pypi",
                artifactId = lockPackage.name,
                version = lockPackage.version,
                scope = scopeByName[lockPackage.name] ?: "main",
                ecosystem = Ecosystem.PYPI
            )

            lockPackage.dependencies.forEach { dependencyName ->
                buildNode(dependencyName, visiting)?.let(node::addChild)
            }

            visiting.remove(key)
            return node
        }

        return rootNames.mapNotNull { buildNode(it, mutableSetOf()) }
    }

    private fun extractPackageBlocks(content: String): List<String> {
        val parts = content.split("[[package]]")
        return parts.drop(1).map { it.trim() }.filter { it.isNotBlank() }
    }

    private fun extractDependencies(block: String): Set<String> {
        val dependencies = mutableSetOf<String>()
        var inDependenciesSection = false

        block.lines().forEach { rawLine ->
            val line = rawLine.trim()
            if (line.isBlank()) return@forEach

            if (line.startsWith("[") && line.endsWith("]")) {
                inDependenciesSection = line == "[package.dependencies]"
                return@forEach
            }

            if (!inDependenciesSection) return@forEach

            val depName = line.substringBefore('=').trim().trim('"', '\'').lowercase().replace('_', '-')
            if (depName.isNotBlank()) {
                dependencies += depName
            }
        }

        return dependencies
    }
}