ReportGenerator.kt

package com.depanalyzer.report

import tools.jackson.databind.SerializationFeature
import tools.jackson.databind.json.JsonMapper
import tools.jackson.module.kotlin.KotlinModule
import com.depanalyzer.parser.ProjectType
import com.depanalyzer.update.UpdateSuggestion

class ReportGenerator {
    private val jsonMapper = JsonMapper.builder()
        .addModule(KotlinModule.Builder().build())
        .enable(SerializationFeature.INDENT_OUTPUT)
        .build()

    fun toJson(report: DependencyReport): String {
        return jsonMapper.writeValueAsString(report)
    }

    fun toJsonVerbose(report: DependencyReport): String {
        return jsonMapper.writeValueAsString(report)
    }

    fun toJsonUpdatePlan(
        projectType: ProjectType,
        buildFile: String,
        suggestions: List<UpdateSuggestion>
    ): String {
        val payload = mapOf(
            "schemaVersion" to "1.0",
            "projectType" to projectType.name,
            "buildFile" to buildFile,
            "suggestions" to suggestions.map { suggestion ->
                mapOf(
                    "id" to suggestion.suggestionId,
                    "groupId" to suggestion.groupId,
                    "artifactId" to suggestion.artifactId,
                    "currentVersion" to suggestion.currentVersion,
                    "newVersion" to suggestion.newVersion,
                    "reason" to suggestion.reason.name,
                    "targetType" to suggestion.targetType.name,
                    "viaDirectCoordinate" to suggestion.viaDirectCoordinate,
                    "ecosystem" to suggestion.ecosystem.name
                )
            }
        )
        return jsonMapper.writeValueAsString(payload)
    }

    fun toText(report: DependencyReport): String {
        val sb = StringBuilder()
        sb.appendLine("====================================================")
        sb.appendLine("Análisis de Dependencias: ${report.projectName}")
        sb.appendLine("====================================================")
        sb.appendLine()

        if (report.directVulnerable.isNotEmpty() || report.transitiveVulnerable.isNotEmpty()) {
            sb.appendLine("VULNERABILIDADES DETECTADAS")
            sb.appendLine("---------------------------")

            if (report.directVulnerable.isNotEmpty()) {
                sb.appendLine("[Directas]")
                report.directVulnerable.forEach { dep ->
                    sb.appendLine("  - ${dep.groupId}:${dep.artifactId}:${dep.version}")
                    dep.vulnerabilities.forEach { v ->
                        val desc = v.description ?: "No description available"
                        sb.appendLine("    * [${v.severity}] ${v.cveId}: $desc")
                    }
                }
                sb.appendLine()
            }

            if (report.transitiveVulnerable.isNotEmpty()) {
                sb.appendLine("[Transitivas]")
                report.transitiveVulnerable.forEach { dep ->
                    sb.appendLine("  - ${dep.groupId}:${dep.artifactId}:${dep.version}")
                    if (dep.dependencyChain != null) {
                        sb.appendLine("    Ruta: ${dep.dependencyChain.joinToString(" -> ")}")
                    }
                    dep.vulnerabilities.forEach { v ->
                        val desc = v.description ?: "No description available"
                        sb.appendLine("    * [${v.severity}] ${v.cveId}: $desc")
                    }
                }
                sb.appendLine()
            }
        }

        if (report.outdated.isNotEmpty()) {
            sb.appendLine("DEPENDENCIAS DESACTUALIZADAS")
            sb.appendLine("----------------------------")
            report.outdated.forEach { dep ->
                sb.appendLine("  - ${dep.groupId}:${dep.artifactId}: ${dep.currentVersion} -> ${dep.latestVersion}")
            }
            sb.appendLine()
        }

        sb.appendLine("RESUMEN")
        sb.appendLine("-------")
        sb.appendLine("  Al día: ${report.upToDate.size}")
        sb.appendLine("  Desactualizadas: ${report.outdated.size}")
        sb.appendLine("  Vulnerabilidades directas: ${report.directVulnerable.size}")
        sb.appendLine("  Vulnerabilidades transitivas: ${report.transitiveVulnerable.size}")
        sb.appendLine("====================================================")

        return sb.toString()
    }

    private fun renderTreeNode(sb: StringBuilder, node: DependencyTreeNode, level: Int, useAscii: Boolean) {
        val indent = "  ".repeat(level)
        val prefix = if (useAscii) {
            if (level == 0) "" else "|"
        } else {
            if (level == 0) "" else "│"
        }

        val marker = if (node.isDirectDependency) {
            if (useAscii) "[DIRECT]" else "🔴"
        } else {
            if (useAscii) "[TRANSITIVE]" else "🟡"
        }

        val nodeLabel = "${node.groupId}:${node.artifactId}:${node.currentVersion}"
        sb.appendLine("$indent$prefix$marker $nodeLabel")

        if (node.latestVersion != null) {
            val updateMarker = if (useAscii) "[UPDATE]" else "⬆️"
            val updateIndent = if (level == 0) "" else "  ".repeat(level) + "|  "
            sb.appendLine("$updateIndent$updateMarker Disponible: ${node.latestVersion}")
        }

        node.vulnerabilities.forEach { vuln ->
            val vulnMarker = when (vuln.severity) {
                VulnerabilitySeverity.CRITICAL -> if (useAscii) "[CRITICAL]" else "🔴"
                VulnerabilitySeverity.HIGH -> if (useAscii) "[HIGH]" else "🟠"
                VulnerabilitySeverity.MEDIUM -> if (useAscii) "[MEDIUM]" else "🟡"
                VulnerabilitySeverity.LOW -> if (useAscii) "[LOW]" else "🟢"
                VulnerabilitySeverity.UNKNOWN -> if (useAscii) "[UNKNOWN]" else "⚪"
            }
            val cvssStr = vuln.cvssScore?.let { " (${it})" } ?: ""
            val vulnIndent = if (level == 0) "" else "  ".repeat(level) + "|  "
            sb.appendLine("$vulnIndent$vulnMarker [${vuln.cveId}] ${vuln.severity}$cvssStr")
        }

        node.children.forEach { child ->
            renderTreeNode(sb, child, level + 1, useAscii)
        }
    }
}