DepAnalyzerCli.kt

package com.depanalyzer.cli

import com.depanalyzer.core.ProjectAnalyzer
import com.depanalyzer.parser.*
import com.depanalyzer.parser.npm.NpmPackageParser
import com.depanalyzer.parser.python.PyprojectPoetryParser
import com.depanalyzer.parser.python.RequirementsParser
import com.depanalyzer.report.*
import com.depanalyzer.repository.NvdClient
import com.depanalyzer.repository.OssIndexClient
import com.depanalyzer.telemetry.TelemetryClient
import com.depanalyzer.telemetry.TelemetryConfig
import com.depanalyzer.telemetry.TelemetryEvent
import com.depanalyzer.tui.AnalyzeTuiApp
import com.depanalyzer.tui.TerminalCapabilities
import com.depanalyzer.tui.TerminalCapabilitiesDetector
import com.depanalyzer.update.*
import com.github.ajalt.clikt.core.*
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.optional
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.int
import com.github.ajalt.clikt.parameters.types.path
import com.github.ajalt.mordant.terminal.Terminal
import java.io.File
import java.nio.file.Path
import kotlin.io.path.writeText

class Depanalyzer : CliktCommand() {
    private val noTelemetry: Boolean by option(
        "--no-telemetry",
        help = "Disable anonymous usage telemetry"
    ).flag(default = false)

    override fun help(context: Context): String = "Analizador de Dependencias multi-ecosistema"
    override fun run() {
        if (noTelemetry) {
            TelemetryConfig.disable()
        }

        TelemetryClient.send(TelemetryEvent(eventType = "app_start"))
    }
}

data class AnalyzeExecutionRequest(
    val projectPath: Path,
    val includeChains: Boolean,
    val disableMaven: Boolean,
    val disableGradle: Boolean,
    val verbose: Boolean,
    val treeMaxDepth: Int?,
    val treeExpandMode: TreeExpandMode,
    val timeoutSeconds: Long,
    val vulnerabilitySourceMode: VulnerabilitySourceMode,
    val showCommandOutput: Boolean = false,
    val ossIndexToken: String?,
    val nvdApiKey: String?,
    val onPartialReport: ((DependencyReport) -> Unit)? = null
)

enum class VulnerabilitySourceMode {
    AUTO,
    OSS_ONLY,
    NVD_ONLY
}

data class TuiLaunchConfig(
    val initialStatus: String,
    val progressHint: String? = null,
    val scanProvider: ((DependencyReport) -> Unit) -> DependencyReport,
    val initialReport: DependencyReport? = null,
    val applyUpdates: ((List<UpdateSuggestion>) -> List<UpdateResult>)? = null
)

internal fun resolveVulnerabilitySourceModeFromFlags(
    forceOss: Boolean,
    forceNvd: Boolean
): VulnerabilitySourceMode? {
    if (forceOss && forceNvd) {
        return null
    }

    return when {
        forceOss -> VulnerabilitySourceMode.OSS_ONLY
        forceNvd -> VulnerabilitySourceMode.NVD_ONLY
        else -> VulnerabilitySourceMode.AUTO
    }
}

private fun defaultAnalyzeExecutor(request: AnalyzeExecutionRequest): DependencyReport {
    val analyzer = ProjectAnalyzer(
        ossIndexClient = OssIndexClient(token = request.ossIndexToken),
        nvdClient = NvdClient(apiKey = request.nvdApiKey)
    )

    return analyzer.analyze(
        request.projectPath,
        includeChains = request.includeChains,
        disableMaven = request.disableMaven,
        disableGradle = request.disableGradle,
        verbose = request.verbose,
        treeMaxDepth = request.treeMaxDepth,
        treeExpandMode = request.treeExpandMode,
        timeoutSeconds = request.timeoutSeconds,
        vulnerabilitySourceMode = request.vulnerabilitySourceMode,
        showCommandOutput = request.showCommandOutput,
        onPartialReport = request.onPartialReport
    )
}

private fun defaultJsonOutputPathProvider(@Suppress("UNUSED_PARAMETER") targetPath: Path): Path {
    return Path.of(System.getProperty("user.dir"), "dependency-report.json")
}

private fun defaultTuiRunner(config: TuiLaunchConfig, capabilities: TerminalCapabilities): DependencyReport? {
    return AnalyzeTuiApp(terminal = Terminal(ansiLevel = capabilities.ansiLevel))
        .runAsync(
            initialStatus = config.initialStatus,
            progressHint = config.progressHint,
            scanProvider = config.scanProvider,
            initialReport = config.initialReport,
            applyUpdates = config.applyUpdates
        )
}

abstract class BaseAnalyzeCommand(
    commandName: String,
    private val forceTui: Boolean,
    private val analyzeExecutor: (AnalyzeExecutionRequest) -> DependencyReport,
    private val jsonOutputPathProvider: (Path) -> Path,
    private val terminalCapabilitiesDetector: TerminalCapabilitiesDetector,
    private val tuiRunner: (TuiLaunchConfig, TerminalCapabilities) -> DependencyReport?
) : CliktCommand(name = commandName) {
    private val telemetryCommandName: String = commandName
    private val path: Path? by argument(help = "Ruta al directorio del proyecto (default: directorio actual)")
        .path(mustExist = true, canBeFile = false)
        .optional()
    private val output: String? by option("-o", "--output", help = "Formato de salida (json a archivo)")
    private val outputFile: String? by option(
        "--output-file",
        help = "Ruta del reporte JSON; use '-' para stdout"
    )
    private val quiet: Boolean by option(
        "--quiet",
        help = "Suprime progreso y mensajes informativos"
    ).flag(default = false)
    private val noColor: Boolean by option("--no-color", help = "Desactiva el color en la consola").flag()
    private val tui: Boolean by option("--tui", help = "Activa la interfaz TUI interactiva").flag()
    private val ossToken: String? by option("--oss-token", help = "Token de autenticación para OSS Index API")
    private val nvdToken: String? by option("--nvd-token", help = "API key para NVD API")
    private val oss: Boolean by option("--oss", help = "Fuerza uso de OSS Index (sin fallback)").flag()
    private val nvd: Boolean by option("--nvd", help = "Fuerza uso de NVD (sin fallback)").flag()
    private val verbose: Boolean by option(
        "-v",
        "--verbose",
        help = "Modo detallado - muestra estructura completa del modelo"
    ).flag()
    private val showChains: Boolean by option(
        "--show-chains",
        help = "Muestra cadenas de vulnerabilidades (paths desde directas a vulnerables)"
    ).flag()
    private val chainDetail: Boolean by option(
        "--chain-detail",
        help = "Muestra detalles completos de cadenas (requiere --show-chains)"
    ).flag()
    private val offline: Boolean by option(
        "--offline",
        help = "Deshabilita Maven dependency:tree. Usa análisis estático (más rápido, menos preciso)"
    ).flag()
    private val dynamic: Boolean by option(
        "--dynamic",
        help = "Fuerza análisis dinámico (más preciso, más lento). Por defecto: análisis estático"
    ).flag(default = false)
    private val disableMaven: Boolean by option(
        "--disable-maven",
        help = "Fuerza el análisis estático desactivando Maven"
    ).flag()
    private val disableGradle: Boolean by option(
        "--disable-gradle",
        help = "Desactiva Gradle dependency tree execution, usa análisis estático de build.gradle"
    ).flag()
    private val ascii: Boolean by option(
        "--ascii",
        help = "Usa caracteres ASCII en lugar de Unicode para el árbol de dependencias"
    ).flag()
    private val treeDepth: Int? by option(
        "--tree-depth",
        help = "Limita la profundidad del árbol de dependencias a N niveles"
    ).int()
    private val treeExpand: String? by option(
        "--tree-expand",
        help = "Modo de expansión del árbol: collapsed, critical, high, medium, all (default: all)"
    )
    private val timeout: Int? by option(
        "--timeout",
        help = "Timeout en segundos para descarga de dependencias (default: 1800s = 30 min)"
    ).int()
    private val commandOutput: Boolean by option(
        "--command-output",
        help = "Muestra salida detallada de comandos Gradle/Maven durante el analisis dinamico"
    ).flag()
    private val failOnCritical: Boolean by option(
        "--fail-on-critical",
        help = "Retorna exit code 1 si se detectan CVEs críticos"
    ).flag()

    override fun run() {
        val targetPath = path ?: Path.of(".")
        val tuiRequested = forceTui || tui
        val startTime = System.currentTimeMillis()
        val token = getOssTokenFromCliOrEnv()
        val nvdApiKey = getNvdTokenFromCliOrEnv()
        val sourceMode = resolveVulnerabilitySourceMode() ?: return

        trackCommandAndFlagFeatures()

        val expandMode = when (treeExpand?.lowercase()) {
            "collapsed" -> TreeExpandMode.COLLAPSED
            "critical" -> TreeExpandMode.CRITICAL
            "high" -> TreeExpandMode.HIGH
            "medium" -> TreeExpandMode.MEDIUM
            "all", null -> TreeExpandMode.ALL
            else -> {
                echo(
                    "Error: modo de expansión desconocido '$treeExpand'. Use: collapsed, critical, high, medium, all",
                    err = true
                )
                return
            }
        }
        val timeoutSeconds = timeout?.toLong() ?: 1800L
        val capabilities = terminalCapabilitiesDetector.detect(noColor = noColor)
        val interactiveTui = tuiRequested && capabilities.supportsInteractiveTui

        ProgressTracker.setMuted(interactiveTui || quiet || outputFile == "-")

        try {
            if (!interactiveTui) {
                ProgressTracker.logStart("Iniciando análisis en $targetPath...")
                ProgressTracker.startProgress(
                    listOf(
                        "Detección",
                        "Parseo",
                        "Resolución de repos",
                        "Consulta de versiones",
                        "Árbol transitivo",
                        "CVEs",
                        "Reporte"
                    )
                )
            }

            if (!interactiveTui && sourceMode == VulnerabilitySourceMode.NVD_ONLY && nvdApiKey == null) {
                echo("Advertencia: --nvd sin token/API key puede estar muy limitado (~50 req/hora)", err = true)
            }

            if (interactiveTui) {
                val dynamicForcedMessage = buildDynamicForcedMessage()
                val initialDirectReport = buildInitialDirectReport(targetPath)
                val updateApplier = buildTuiUpdateApplier(targetPath)
                val tuiRequest = AnalyzeExecutionRequest(
                    projectPath = targetPath,
                    includeChains = true,
                    disableMaven = false,
                    disableGradle = false,
                    verbose = verbose,
                    treeMaxDepth = treeDepth,
                    treeExpandMode = expandMode,
                    timeoutSeconds = timeoutSeconds,
                    vulnerabilitySourceMode = sourceMode,
                    showCommandOutput = commandOutput,
                    ossIndexToken = token,
                    nvdApiKey = nvdApiKey
                )

                val tuiReport = tuiRunner(
                    TuiLaunchConfig(
                        initialStatus = "Iniciando escaneo dinámico...",
                        progressHint = dynamicForcedMessage,
                        initialReport = initialDirectReport,
                        applyUpdates = updateApplier,
                        scanProvider = { onPartialReport ->
                            analyzeExecutor(tuiRequest.copy(onPartialReport = onPartialReport))
                        }
                    ),
                    capabilities
                )

                if (failOnCritical && tuiReport != null && hasCriticalVulnerability(tuiReport)) {
                    throw ProgramResult(1)
                }
                return
            }

            if (tuiRequested && !capabilities.supportsInteractiveTui) {
                echo("Advertencia: TUI no disponible en este entorno (sin TTY o CI). Se usa salida CLI.", err = true)
            }

            val standardRequest = AnalyzeExecutionRequest(
                projectPath = targetPath,
                includeChains = showChains,
                disableMaven = !dynamic || offline || disableMaven,
                disableGradle = !dynamic || disableGradle,
                verbose = verbose,
                treeMaxDepth = treeDepth,
                treeExpandMode = expandMode,
                timeoutSeconds = timeoutSeconds,
                vulnerabilitySourceMode = sourceMode,
                showCommandOutput = commandOutput,
                ossIndexToken = token,
                nvdApiKey = nvdApiKey
            )

            val report = try {
                analyzeExecutor(standardRequest)
            } catch (e: Exception) {
                sendErrorEvent(e)
                echo("Error durante el análisis: ${e.message}", err = true)
                throw ProgramResult(2)
            }

            if (!tuiRequested) {
                ProgressTracker.advanceProgress("Reporte")
            }
            val totalDuration = System.currentTimeMillis() - startTime
            if (!tuiRequested) {
                ProgressTracker.logSeparator()
                ProgressTracker.completeProgress()
                ProgressTracker.logSuccess("Análisis completado", totalDuration)
            }

            renderCliOutput(targetPath, report, capabilities)

            if (failOnCritical && hasCriticalVulnerability(report)) {
                throw ProgramResult(1)
            }
        } catch (e: ProgramResult) {
            throw e
        } catch (e: Exception) {
            sendErrorEvent(e)
            throw e
        } finally {
            val durationMs = System.currentTimeMillis() - startTime
            TelemetryClient.send(
                TelemetryEvent(
                    eventType = "scan_run",
                    durationMs = durationMs
                )
            )
            TelemetryClient.flush(timeoutMs = 2500L)
            ProgressTracker.setMuted(false)
            ProgressTracker.setListener(null)
        }
    }

    private fun trackCommandAndFlagFeatures() {
        TelemetryClient.send(
            TelemetryEvent(
                eventType = "feature_used",
                feature = "${telemetryCommandName}_command"
            )
        )

        if (tui) trackFeature("flag_tui")
        if (output != null) trackFeature("flag_output_${output!!.lowercase()}")
        if (outputFile != null) trackFeature("flag_output_file")
        if (quiet) trackFeature("flag_quiet")
        if (verbose) trackFeature("flag_verbose")
        if (showChains) trackFeature("flag_show_chains")
        if (chainDetail) trackFeature("flag_chain_detail")
        if (offline) trackFeature("flag_offline")
        if (dynamic) trackFeature("flag_dynamic")
        if (disableMaven) trackFeature("flag_disable_maven")
        if (disableGradle) trackFeature("flag_disable_gradle")
        if (ascii) trackFeature("flag_ascii")
        if (treeDepth != null) trackFeature("flag_tree_depth")
        if (treeExpand != null) trackFeature("flag_tree_expand")
        if (timeout != null) trackFeature("flag_timeout")
        if (oss) trackFeature("flag_oss")
        if (nvd) trackFeature("flag_nvd")
        if (commandOutput) trackFeature("flag_command_output")
        if (failOnCritical) trackFeature("flag_fail_on_critical")
    }

    private fun trackFeature(feature: String) {
        TelemetryClient.send(TelemetryEvent(eventType = "feature_used", feature = feature))
    }

    private fun sendErrorEvent(error: Throwable) {
        TelemetryClient.send(
            TelemetryEvent(
                eventType = "error",
                errorType = error.javaClass.simpleName,
                errorMessage = error.message?.take(200)
            )
        )
    }

    private fun buildDynamicForcedMessage(): String {
        val ignoredFlags = mutableListOf<String>()
        if (offline) ignoredFlags += "--offline"
        if (disableMaven) ignoredFlags += "--disable-maven"
        if (disableGradle) ignoredFlags += "--disable-gradle"

        return if (ignoredFlags.isEmpty()) {
            "Modo TUI: análisis dinámico habilitado para dependencias transitivas"
        } else {
            "Modo TUI: análisis dinámico forzado, ignorando ${ignoredFlags.joinToString(", ")}"
        }
    }

    private fun buildInitialDirectReport(projectPath: Path): DependencyReport? {
        return runCatching {
            val projectType = ProjectDetector().detect(projectPath)
            val projectDir = projectPath.toFile()
            val directNodes = when (projectType) {
                ProjectType.MAVEN -> {
                    val pom = File(projectDir, "pom.xml")
                    PomDependencyParser()
                        .parse(pom)
                        .filter { it.section == DependencySection.DEPENDENCIES }
                        .distinctBy { "${it.groupId}:${it.artifactId}" }
                        .map { dep ->
                            DependencyTreeNode(
                                groupId = dep.groupId,
                                artifactId = dep.artifactId,
                                currentVersion = dep.version ?: "unknown",
                                isDirectDependency = true,
                                scope = dep.scope
                            )
                        }
                }

                ProjectType.GRADLE_GROOVY -> {
                    val buildFile = File(projectDir, "build.gradle")
                    GradleGroovyDependencyParser()
                        .parse(buildFile)
                        .distinctBy { "${it.groupId}:${it.artifactId}" }
                        .map { dep ->
                            DependencyTreeNode(
                                groupId = dep.groupId,
                                artifactId = dep.artifactId,
                                currentVersion = dep.version ?: "unknown",
                                isDirectDependency = true,
                                scope = dep.configuration
                            )
                        }
                }

                ProjectType.GRADLE_KOTLIN -> {
                    val buildFile = File(projectDir, "build.gradle.kts")
                    GradleKotlinDependencyParser()
                        .parse(buildFile)
                        .distinctBy { "${it.groupId}:${it.artifactId}" }
                        .map { dep ->
                            DependencyTreeNode(
                                groupId = dep.groupId,
                                artifactId = dep.artifactId,
                                currentVersion = dep.version ?: "unknown",
                                isDirectDependency = true,
                                scope = dep.configuration
                            )
                        }
                }

                ProjectType.NPM -> {
                    val packageFile = File(projectDir, "package.json")
                    NpmPackageParser()
                        .parse(packageFile)
                        .distinctBy { "${it.groupId}:${it.artifactId}" }
                        .map { dep ->
                            DependencyTreeNode(
                                groupId = dep.groupId,
                                artifactId = dep.artifactId,
                                currentVersion = dep.version ?: "unknown",
                                isDirectDependency = true,
                                scope = dep.scope,
                                ecosystem = dep.ecosystem
                            )
                        }
                }

                ProjectType.PYTHON_POETRY -> {
                    val pyprojectFile = File(projectDir, "pyproject.toml")
                    if (!pyprojectFile.exists()) {
                        emptyList()
                    } else {
                        PyprojectPoetryParser()
                            .parse(pyprojectFile)
                            .distinctBy { "${it.groupId}:${it.artifactId}" }
                            .map { dep ->
                                DependencyTreeNode(
                                    groupId = dep.groupId,
                                    artifactId = dep.artifactId,
                                    currentVersion = dep.version ?: "unknown",
                                    isDirectDependency = true,
                                    scope = dep.scope,
                                    ecosystem = dep.ecosystem
                                )
                            }
                    }
                }

                ProjectType.PYTHON_REQUIREMENTS -> {
                    val requirementsFile = File(projectDir, "requirements.txt")
                    if (!requirementsFile.exists()) {
                        emptyList()
                    } else {
                        RequirementsParser()
                            .parse(requirementsFile)
                            .distinctBy { "${it.groupId}:${it.artifactId}" }
                            .map { dep ->
                                DependencyTreeNode(
                                    groupId = dep.groupId,
                                    artifactId = dep.artifactId,
                                    currentVersion = dep.version ?: "unknown",
                                    isDirectDependency = true,
                                    scope = dep.scope,
                                    ecosystem = dep.ecosystem
                                )
                            }
                    }
                }
            }

            DependencyReport(
                projectName = projectPath.fileName?.toString() ?: projectPath.toString(),
                dependencyTree = directNodes
            )
        }.getOrNull()
    }

    private fun buildTuiUpdateApplier(projectPath: Path): ((List<UpdateSuggestion>) -> List<UpdateResult>)? {
        val projectType = runCatching { ProjectDetector().detect(projectPath) }.getOrNull() ?: return null
        val buildFile = resolveBuildFile(projectPath, projectType)
        if (!buildFile.exists()) return null

        val updater = updaterForProjectType(projectType)
        return { suggestions ->
            if (suggestions.isEmpty()) {
                emptyList()
            } else {
                runCatching { BuildFileBackup.ensureBackup(buildFile) }
                suggestions.map { suggestion ->
                    val directSuggestion = suggestion.copy(targetType = UpdateTargetType.DIRECT)
                    val result = runCatching { updater.applyUpdate(buildFile, directSuggestion) }
                    if (result.isSuccess) {
                        val applied = result.getOrDefault(false)
                        UpdateResult(
                            suggestion = directSuggestion,
                            applied = applied,
                            note = if (applied) "aplicada" else "sin coincidencia editable"
                        )
                    } else {
                        UpdateResult(
                            suggestion = directSuggestion,
                            applied = false,
                            note = "error: ${result.exceptionOrNull()?.message ?: "desconocido"}"
                        )
                    }
                }
            }
        }
    }

    private fun resolveBuildFile(projectPath: Path, projectType: ProjectType): File {
        val projectDir = projectPath.toFile()
        return 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")
        }
    }

    private fun updaterForProjectType(projectType: ProjectType): BuildFileUpdater {
        return when (projectType) {
            ProjectType.MAVEN -> PomBuildFileUpdater()
            ProjectType.GRADLE_GROOVY -> GradleGroovyBuildFileUpdater()
            ProjectType.GRADLE_KOTLIN -> GradleKotlinBuildFileUpdater()
            ProjectType.NPM -> NpmPackageJsonBuildFileUpdater()
            ProjectType.PYTHON_POETRY -> PyprojectBuildFileUpdater()
            ProjectType.PYTHON_REQUIREMENTS -> RequirementsBuildFileUpdater()
        }
    }

    private fun renderCliOutput(targetPath: Path, report: DependencyReport, capabilities: TerminalCapabilities) {
        if (output?.lowercase() == "json") {
            val generator = ReportGenerator()
            val json = if (verbose) generator.toJsonVerbose(report) else generator.toJson(report)
            if (outputFile == "-") {
                echo(json)
                return
            }

            val outputPath = outputFile
                ?.let(Path::of)
                ?: jsonOutputPathProvider(targetPath)
            outputPath.writeText(json)
            if (!quiet) {
                echo("Reporte JSON exportado a: $outputPath")
            }
            return
        }

        val renderer = ConsoleRenderer(
            noColor = noColor || capabilities.ansiLevel == com.github.ajalt.mordant.rendering.AnsiLevel.NONE,
            useAscii = ascii,
            treeMaxDepth = treeDepth,
            ansiLevel = capabilities.ansiLevel
        )

        if (verbose) {
            renderer.renderVerbose(report, showChains = showChains, detailedChains = chainDetail)
        } else {
            renderer.render(report, showChains = showChains, detailedChains = chainDetail)
        }
    }

    private fun hasCriticalVulnerability(report: DependencyReport): Boolean {
        return (report.directVulnerable + report.transitiveVulnerable)
            .flatMap { it.vulnerabilities }
            .any { it.severity == VulnerabilitySeverity.CRITICAL }
    }

    private fun getOssTokenFromCliOrEnv(): String? {
        return (ossToken ?: System.getenv("OSS_INDEX_TOKEN"))
            ?.trim()
            ?.takeUnless { it.isBlank() }
    }

    private fun getNvdTokenFromCliOrEnv(): String? {
        return nvdToken ?: System.getenv("NVD_API_KEY")
    }

    private fun resolveVulnerabilitySourceMode(): VulnerabilitySourceMode? {
        val resolved = resolveVulnerabilitySourceModeFromFlags(forceOss = oss, forceNvd = nvd)
        if (resolved == null) {
            echo("Error: --oss y --nvd son mutuamente excluyentes", err = true)
            return null
        }

        return resolved
    }
}

class Analyze(
    analyzeExecutor: (AnalyzeExecutionRequest) -> DependencyReport = ::defaultAnalyzeExecutor,
    jsonOutputPathProvider: (Path) -> Path = ::defaultJsonOutputPathProvider,
    terminalCapabilitiesDetector: TerminalCapabilitiesDetector = TerminalCapabilitiesDetector(),
    tuiRunner: (TuiLaunchConfig, TerminalCapabilities) -> DependencyReport? = ::defaultTuiRunner
) : BaseAnalyzeCommand(
    commandName = "analyze",
    forceTui = false,
    analyzeExecutor = analyzeExecutor,
    jsonOutputPathProvider = jsonOutputPathProvider,
    terminalCapabilitiesDetector = terminalCapabilitiesDetector,
    tuiRunner = tuiRunner
) {
    override fun help(context: Context): String = "Analiza las dependencias de un proyecto"
}

class Tui(
    analyzeExecutor: (AnalyzeExecutionRequest) -> DependencyReport = ::defaultAnalyzeExecutor,
    jsonOutputPathProvider: (Path) -> Path = ::defaultJsonOutputPathProvider,
    terminalCapabilitiesDetector: TerminalCapabilitiesDetector = TerminalCapabilitiesDetector(),
    tuiRunner: (TuiLaunchConfig, TerminalCapabilities) -> DependencyReport? = ::defaultTuiRunner
) : BaseAnalyzeCommand(
    commandName = "tui",
    forceTui = true,
    analyzeExecutor = analyzeExecutor,
    jsonOutputPathProvider = jsonOutputPathProvider,
    terminalCapabilitiesDetector = terminalCapabilitiesDetector,
    tuiRunner = tuiRunner
) {
    override fun help(context: Context): String = "Abre la interfaz TUI interactiva"
}

fun main(args: Array<String>) = Depanalyzer()
    .subcommands(Analyze(), Tui(), Update())
    .main(args)