ProjectAnalyzer.kt

package com.depanalyzer.core

import com.depanalyzer.cli.ProgressTracker
import com.depanalyzer.cli.VulnerabilitySourceMode
import com.depanalyzer.core.graph.ChainResolver
import com.depanalyzer.core.graph.DependencyGraphBuilder
import com.depanalyzer.parser.*
import com.depanalyzer.parser.gradle.GradleIntegration
import com.depanalyzer.parser.maven.MavenIntegration
import com.depanalyzer.parser.npm.NpmIntegration
import com.depanalyzer.parser.python.PythonIntegration
import com.depanalyzer.report.*
import com.depanalyzer.repository.*
import java.io.File
import java.nio.file.Path
import kotlin.io.path.name

class ProjectAnalyzer(
    private val repositoryClient: RepositoryClient = RepositoryClient(),
    private val ossIndexClient: OssIndexClient = OssIndexClient(),
    private val nvdClient: NvdClient = NvdClient(),
    private val projectDetector: ProjectDetector = ProjectDetector()
) {
    fun analyze(
        projectDir: Path,
        includeChains: Boolean = false,
        disableMaven: Boolean = false,
        disableGradle: Boolean = false,
        verbose: Boolean = false,
        treeMaxDepth: Int? = null,
        treeExpandMode: TreeExpandMode = TreeExpandMode.ALL,
        timeoutSeconds: Long = 1800L,
        vulnerabilitySourceMode: VulnerabilitySourceMode = VulnerabilitySourceMode.AUTO,
        showCommandOutput: Boolean = false,
        onPartialReport: ((DependencyReport) -> Unit)? = null
    ): DependencyReport {
        ProgressTracker.advanceProgress("Detección")
        val type = projectDetector.detect(projectDir)
        val dirFile = projectDir.toFile()

        ProgressTracker.logDetected("Proyecto detectado: $type")

        ProgressTracker.advanceProgress("Parseo")
        val (dependencies, rootNodes) = when (type) {
            ProjectType.MAVEN -> {
                ProgressTracker.logProcessing("Analizando proyecto Maven...")
                val mavenNodes = MavenIntegration.analyzeMavenProject(
                    projectDir = dirFile,
                    enableMaven = !disableMaven,
                    verbose = verbose,
                    timeoutSeconds = timeoutSeconds,
                    showCommandOutput = showCommandOutput
                )

                val parsedDeps = mavenNodes.flatMap { node ->
                    flattenNodeTree(node)
                }
                Pair(parsedDeps, mavenNodes)
            }

            ProjectType.GRADLE_GROOVY -> {
                ProgressTracker.logProcessing("Analizando proyecto Gradle (build.gradle)...")
                val gradleNodes = GradleIntegration.analyzeGradleProject(
                    projectDir = dirFile,
                    enableGradle = !disableGradle,
                    verbose = verbose,
                    timeoutSeconds = timeoutSeconds,
                    showCommandOutput = showCommandOutput
                )

                val parsedDeps = gradleNodes.flatMap { node ->
                    flattenNodeTree(node)
                }
                Pair(parsedDeps, gradleNodes)
            }

            ProjectType.GRADLE_KOTLIN -> {
                ProgressTracker.logProcessing("Analizando proyecto Gradle (build.gradle.kts)...")
                val gradleNodes = GradleIntegration.analyzeGradleProject(
                    projectDir = dirFile,
                    enableGradle = !disableGradle,
                    verbose = verbose,
                    timeoutSeconds = timeoutSeconds,
                    showCommandOutput = showCommandOutput
                )

                val parsedDeps = gradleNodes.flatMap { node ->
                    flattenNodeTree(node)
                }
                Pair(parsedDeps, gradleNodes)
            }

            ProjectType.NPM -> {
                ProgressTracker.logProcessing("Analizando proyecto Node.js (package.json)...")
                NpmIntegration.analyzeNpmProject(dirFile)
            }

            ProjectType.PYTHON_POETRY -> {
                ProgressTracker.logProcessing("Analizando proyecto Python (pyproject.toml/poetry.lock)...")
                PythonIntegration.analyzePoetryProject(dirFile)
            }

            ProjectType.PYTHON_REQUIREMENTS -> {
                ProgressTracker.logProcessing("Analizando proyecto Python (requirements.txt)...")
                PythonIntegration.analyzeRequirementsProject(dirFile)
            }
        }

        ProgressTracker.advanceProgress("Resolución de repos")
        val repositories = when (type) {
            ProjectType.MAVEN -> {
                val parser = PomDependencyParser()
                val pomFile = File(dirFile, "pom.xml")
                parser.repositories(pomFile)
            }

            ProjectType.GRADLE_GROOVY -> {
                val buildFile = File(dirFile, "build.gradle")
                GradleRepositoryParser().parse(buildFile)
            }

            ProjectType.GRADLE_KOTLIN -> {
                val buildFile = File(dirFile, "build.gradle.kts")
                GradleRepositoryParser().parse(buildFile)
            }

            ProjectType.NPM,
            ProjectType.PYTHON_POETRY,
            ProjectType.PYTHON_REQUIREMENTS -> {
                emptyList()
            }
        }

        val upToDate = mutableListOf<DependencyInfo>()
        val outdated = mutableListOf<OutdatedDependency>()
        val directDependencies = rootNodes.map { root ->
            ParsedDependency(
                groupId = root.groupId,
                artifactId = root.artifactId,
                version = root.version,
                scope = root.scope ?: "compile",
                section = if (root.isDependencyManagement) DependencySection.DEPENDENCY_MANAGEMENT else DependencySection.DEPENDENCIES,
                ecosystem = root.ecosystem
            )
        }.distinctBy { "${it.ecosystem}:${it.groupId}:${it.artifactId}:${it.version}" }

        val initialTree = buildDependencyTree(
            vulnerabilityMap = emptyMap(),
            outdatedMap = emptyList(),
            maxDepth = treeMaxDepth,
            expandMode = treeExpandMode,
            rootNodes = rootNodes
        )
        emitPartial(
            onPartialReport,
            DependencyReport(
                projectName = projectDir.name,
                dependencyTree = initialTree
            )
        )

        ProgressTracker.advanceProgress("Consulta de versiones")
        val distinctDependencies = dependencies.distinctBy { "${it.ecosystem}:${it.groupId}:${it.artifactId}" }
        distinctDependencies.forEachIndexed { index, dep ->
            val currentVersion = dep.version
            if (currentVersion != null && !isVariable(currentVersion)) {
                val latest = findLatestVersion(repositories, dep)
                if (latest != null && latest != currentVersion) {
                    outdated.add(
                        OutdatedDependency(
                            groupId = dep.groupId,
                            artifactId = dep.artifactId,
                            currentVersion = currentVersion,
                            latestVersion = latest,
                            ecosystem = dep.ecosystem
                        )
                    )
                } else {
                    upToDate.add(
                        DependencyInfo(
                            groupId = dep.groupId,
                            artifactId = dep.artifactId,
                            version = currentVersion,
                            ecosystem = dep.ecosystem
                        )
                    )
                }
            } else {
                upToDate.add(
                    DependencyInfo(
                        groupId = dep.groupId,
                        artifactId = dep.artifactId,
                        version = currentVersion ?: "unknown",
                        ecosystem = dep.ecosystem
                    )
                )
            }

            val shouldEmitProgressiveSnapshot = onPartialReport != null &&
                    ((index + 1) % 4 == 0 || index == distinctDependencies.lastIndex)
            if (shouldEmitProgressiveSnapshot) {
                val progressiveTree = buildDependencyTree(
                    vulnerabilityMap = emptyMap(),
                    outdatedMap = outdated,
                    maxDepth = treeMaxDepth,
                    expandMode = treeExpandMode,
                    rootNodes = rootNodes
                )
                emitPartial(
                    onPartialReport,
                    DependencyReport(
                        projectName = projectDir.name,
                        upToDate = upToDate.toList(),
                        outdated = outdated.toList(),
                        dependencyTree = progressiveTree
                    )
                )
            }
        }

        val versionedTree = buildDependencyTree(
            vulnerabilityMap = emptyMap(),
            outdatedMap = outdated,
            maxDepth = treeMaxDepth,
            expandMode = treeExpandMode,
            rootNodes = rootNodes
        )
        emitPartial(
            onPartialReport,
            DependencyReport(
                projectName = projectDir.name,
                upToDate = upToDate,
                outdated = outdated,
                dependencyTree = versionedTree
            )
        )

        ProgressTracker.advanceProgress("Árbol transitivo")
        // El árbol parcial ya se generó y emitió en `versionedTree` para la UI.

        ProgressTracker.logSecurity("Consultando vulnerabilidades...")
        ProgressTracker.advanceProgress("CVEs")
        val mavenDependencies = dependencies.filter { it.ecosystem == Ecosystem.MAVEN }
        val hasNonMaven = dependencies.any { it.ecosystem != Ecosystem.MAVEN }
        val vulnerabilityMap = when (vulnerabilitySourceMode) {
            VulnerabilitySourceMode.OSS_ONLY -> {
                try {
                    ossIndexClient.getVulnerabilities(dependencies, failOnError = true)
                } catch (e: Exception) {
                    throw IllegalStateException("[OSS] no se pudo obtener vulnerabilidades: ${e.message}", e)
                }
            }

            VulnerabilitySourceMode.NVD_ONLY -> {
                if (mavenDependencies.isEmpty()) {
                    ProgressTracker.logWarning("NVD solo aplica a dependencias Maven. CVE analysis skipped.")
                    emptyMap()
                } else {
                    try {
                        nvdClient.getVulnerabilities(mavenDependencies)
                    } catch (e: Exception) {
                        throw IllegalStateException("[NVD] no se pudo obtener vulnerabilidades: ${e.message}", e)
                    }
                }
            }

            VulnerabilitySourceMode.AUTO -> {
                val ossVulns = runCatching {
                    ossIndexClient.getVulnerabilities(dependencies, failOnError = true)
                }.getOrNull()
                if (ossVulns != null) {
                    val nvdVulns = if (mavenDependencies.isNotEmpty()) {
                        runCatching {
                            ProgressTracker.logSecurity("Enriqueciendo con datos de NVD...")
                            nvdClient.getVulnerabilities(mavenDependencies)
                        }.getOrElse {
                            if (verbose) {
                                System.err.println("  NVD enrichment failed: ${it.message}")
                            }
                            emptyMap()
                        }
                    } else {
                        if (hasNonMaven && verbose) {
                            System.err.println("  NVD enrichment skipped for non-Maven ecosystems")
                        }
                        emptyMap()
                    }
                    VulnerabilityMerger.mergeVulnerabilities(ossVulns, nvdVulns)
                } else {
                    runCatching {
                        if (mavenDependencies.isEmpty()) emptyMap() else nvdClient.getVulnerabilities(mavenDependencies)
                    }
                        .getOrElse {
                            ProgressTracker.logWarning("No se pudo consultar OSS ni NVD. Vulnerability analysis skipped.")
                            if (verbose) {
                                System.err.println("  Details OSS/NVD: ${it.message}")
                            }
                            emptyMap()
                        }
                }
            }
        }

        val (directVulnerable, transitiveVulnerable) = classifyVulnerabilities(
            dependencies = dependencies,
            directDependencies = directDependencies,
            vulnerabilityMap = vulnerabilityMap
        )

        val cveTree = buildDependencyTree(
            vulnerabilityMap = vulnerabilityMap,
            outdatedMap = outdated,
            maxDepth = treeMaxDepth,
            expandMode = treeExpandMode,
            rootNodes = rootNodes
        )
        emitPartial(
            onPartialReport,
            DependencyReport(
                projectName = projectDir.name,
                upToDate = upToDate,
                outdated = outdated,
                directVulnerable = directVulnerable,
                transitiveVulnerable = transitiveVulnerable,
                dependencyTree = cveTree
            )
        )

        val chains = if (includeChains) {
            buildVulnerabilityChains(dependencies, directDependencies, vulnerabilityMap)
        } else {
            emptyList()
        }

        ProgressTracker.logBuilding("Construyendo reporte final...")
        val dependencyTree = buildDependencyTree(
            vulnerabilityMap = vulnerabilityMap,
            outdatedMap = outdated,
            maxDepth = treeMaxDepth,
            expandMode = treeExpandMode,
            rootNodes = rootNodes
        )

        val report = DependencyReport(
            projectName = projectDir.name,
            upToDate = upToDate,
            outdated = outdated,
            directVulnerable = directVulnerable,
            transitiveVulnerable = transitiveVulnerable,
            vulnerabilityChains = chains,
            dependencyTree = dependencyTree
        )
        val finalReport = attachDirectSourceLocations(
            report = report,
            projectType = type,
            projectDir = dirFile,
            directDependencies = directDependencies
        )
        emitPartial(onPartialReport, finalReport)
        return finalReport
    }

    private fun emitPartial(
        onPartialReport: ((DependencyReport) -> Unit)?,
        report: DependencyReport
    ) {
        if (onPartialReport == null) return
        runCatching { onPartialReport(report) }
    }

    private fun buildDependencyTree(
        vulnerabilityMap: Map<String, List<Vulnerability>>,
        outdatedMap: List<OutdatedDependency>,
        maxDepth: Int?,
        expandMode: TreeExpandMode,
        rootNodes: List<com.depanalyzer.core.graph.DependencyNode>
    ): List<DependencyTreeNode>? {
        if (rootNodes.isEmpty()) {
            return null
        }

        val outdatedByCoordinate = outdatedMap.associateBy { "${it.groupId}:${it.artifactId}:${it.currentVersion}" }

        val builder = DependencyTreeBuilder(
            vulnerabilities = vulnerabilityMap,
            outdatedMap = outdatedByCoordinate
        )

        return builder.buildTree(rootNodes, maxDepth, expandMode).takeIf { it.isNotEmpty() }
    }

    private fun classifyVulnerabilities(
        dependencies: List<ParsedDependency>,
        directDependencies: List<ParsedDependency>,
        vulnerabilityMap: Map<String, List<Vulnerability>>
    ): Pair<List<VulnerableDependency>, List<VulnerableDependency>> {
        val direct = mutableListOf<VulnerableDependency>()
        val transitive = mutableListOf<VulnerableDependency>()

        val directCoordinates = directDependencies.map { "${it.groupId}:${it.artifactId}:${it.version}" }.toSet()

        vulnerabilityMap.forEach { (coordinates, vulnerabilities) ->
            val dep = dependencies.find { "${it.groupId}:${it.artifactId}:${it.version}" == coordinates }
                ?: return@forEach

            val vulnerableDep = VulnerableDependency(
                groupId = dep.groupId,
                artifactId = dep.artifactId,
                version = dep.version!!,
                vulnerabilities = vulnerabilities,
                dependencyChain = buildDependencyChain(coordinates, dependencies, directDependencies),
                ecosystem = dep.ecosystem
            )

            if (coordinates in directCoordinates) {
                direct.add(vulnerableDep)
            } else {
                transitive.add(vulnerableDep)
            }
        }

        return Pair(direct, transitive)
    }

    private fun buildDependencyChain(
        targetCoordinates: String,
        allDependencies: List<ParsedDependency>,
        directDependencies: List<ParsedDependency>
    ): List<String>? {
        if (directDependencies.any { "${it.groupId}:${it.artifactId}:${it.version}" == targetCoordinates }) {
            return null
        }

        val target = allDependencies.find { "${it.groupId}:${it.artifactId}:${it.version}" == targetCoordinates }
            ?: return null

        return listOf(target.groupId + ":" + target.artifactId)
    }

    private fun buildVulnerabilityChains(
        dependencies: List<ParsedDependency>,
        directDependencies: List<ParsedDependency>,
        vulnerabilityMap: Map<String, List<Vulnerability>>
    ): List<com.depanalyzer.core.graph.VulnerabilityChain> {
        val builder = DependencyGraphBuilder()
        val graph = builder.buildGraph(
            directDependencies = directDependencies,
            allDependencies = dependencies,
            vulnerabilities = vulnerabilityMap
        )

        return ChainResolver.resolveAllChains(graph, vulnerabilityMap)
    }

    private fun findLatestVersion(repos: List<ProjectRepository>, dependency: ParsedDependency): String? {
        return repositoryClient.getLatestVersion(dependency, repos)
    }

    private fun isVariable(version: String): Boolean {
        return version.startsWith("$") ||
                version.startsWith($$"${") ||
                version.any { it in "^~><=*!,| " }
    }

    private fun flattenNodeTree(node: com.depanalyzer.core.graph.DependencyNode): List<ParsedDependency> {
        val result = mutableListOf<ParsedDependency>()

        result.add(
            ParsedDependency(
                groupId = node.groupId,
                artifactId = node.artifactId,
                version = node.version,
                scope = node.scope ?: "compile",
                section = if (node.isDependencyManagement) DependencySection.DEPENDENCY_MANAGEMENT else DependencySection.DEPENDENCIES,
                ecosystem = node.ecosystem
            )
        )

        node.children.forEach { child ->
            result.addAll(flattenNodeTree(child))
        }

        return result
    }

    private fun attachDirectSourceLocations(
        report: DependencyReport,
        projectType: ProjectType,
        projectDir: File,
        directDependencies: List<ParsedDependency>
    ): DependencyReport {
        val buildFile = when (projectType) {
            ProjectType.MAVEN -> File(projectDir, "pom.xml")
            ProjectType.GRADLE_GROOVY -> File(projectDir, "build.gradle")
            ProjectType.GRADLE_KOTLIN -> File(projectDir, "build.gradle.kts")
            ProjectType.NPM -> File(projectDir, "package.json")
            ProjectType.PYTHON_POETRY -> File(projectDir, "pyproject.toml")
            ProjectType.PYTHON_REQUIREMENTS -> File(projectDir, "requirements.txt")
        }
        if (!buildFile.isFile) return report

        val lines = runCatching { buildFile.readLines() }.getOrElse { return report }
        val locations = directDependencies.associateNotNull { dependency ->
            val match = lines.withIndex().firstOrNull { (_, line) ->
                line.contains(dependency.artifactId, ignoreCase = dependency.ecosystem != Ecosystem.MAVEN)
            } ?: return@associateNotNull null
            val start = match.value.indexOf(
                string = dependency.artifactId,
                ignoreCase = dependency.ecosystem != Ecosystem.MAVEN
            )
            if (start < 0) return@associateNotNull null

            dependency.locationKey() to DependencySourceLocation(
                file = buildFile.name,
                line = match.index + 1,
                startColumn = start + 1,
                endColumn = start + dependency.artifactId.length + 1
            )
        }

        return report.copy(
            upToDate = report.upToDate.map { dependency ->
                dependency.copy(sourceLocation = locations[dependency.locationKey()])
            },
            outdated = report.outdated.map { dependency ->
                dependency.copy(sourceLocation = locations[dependency.locationKey()])
            },
            directVulnerable = report.directVulnerable.map { dependency ->
                dependency.copy(sourceLocation = locations[dependency.locationKey()])
            }
        )
    }

    private fun ParsedDependency.locationKey(): String = "$ecosystem:$groupId:$artifactId"
    private fun DependencyInfo.locationKey(): String = "$ecosystem:$groupId:$artifactId"
    private fun OutdatedDependency.locationKey(): String = "$ecosystem:$groupId:$artifactId"
    private fun VulnerableDependency.locationKey(): String = "$ecosystem:$groupId:$artifactId"

    private inline fun <T, K, V> Iterable<T>.associateNotNull(
        transform: (T) -> Pair<K, V>?
    ): Map<K, V> {
        val destination = LinkedHashMap<K, V>()
        for (element in this) {
            val pair = transform(element) ?: continue
            destination[pair.first] = pair.second
        }
        return destination
    }
}