TuiState.kt

package com.depanalyzer.tui

import com.depanalyzer.report.VulnerabilitySeverity
import com.depanalyzer.update.UpdateSuggestion

enum class TuiTab {
    DETAIL,
    TREE;

    fun label(): String = when (this) {
        DETAIL -> "Detalle"
        TREE -> "Arbol transitivo"
    }

    fun next(): TuiTab = when (this) {
        DETAIL -> TREE
        TREE -> DETAIL
    }

    fun previous(): TuiTab = when (this) {
        DETAIL -> TREE
        TREE -> DETAIL
    }
}

enum class TuiQuickFilter {
    DIRECT,
    CVE,
    OUTDATED,
    TRANSITIVE,
    ALL;

    fun label(): String = when (this) {
        DIRECT -> "Dependencias"
        ALL -> "Todas"
        CVE -> "Solo CVE"
        OUTDATED -> "Solo desact."
        TRANSITIVE -> "Solo transitivas"
    }

    fun next(): TuiQuickFilter = when (this) {
        DIRECT -> CVE
        CVE -> OUTDATED
        OUTDATED -> TRANSITIVE
        TRANSITIVE -> ALL
        ALL -> DIRECT
    }
}

data class TuiSummary(
    val projectName: String,
    val outdatedCount: Int,
    val vulnerableCount: Int,
    val totalEntries: Int
)

data class TuiVulnerability(
    val cveId: String,
    val severity: VulnerabilitySeverity,
    val cvssScore: Double?,
    val description: String?
)

data class TuiDependencyEntry(
    val coordinate: String,
    val currentVersion: String,
    val latestVersion: String? = null,
    val vulnerabilityCount: Int = 0,
    val outdatedCount: Int = 0,
    val maxSeverity: VulnerabilitySeverity? = null,
    val source: String = "report",
    val vulnerabilities: List<TuiVulnerability> = emptyList(),
    val chainPreview: List<String> = emptyList(),
    val transitiveTreeLines: List<String> = emptyList(),
    val updateSuggestion: UpdateSuggestion? = null
)

data class TuiState(
    val entries: List<TuiDependencyEntry>,
    val summary: TuiSummary,
    val cursor: Int = 0,
    val scrollOffset: Int = 0,
    val activeFilter: TuiQuickFilter = TuiQuickFilter.DIRECT,
    val activeTab: TuiTab = TuiTab.DETAIL,
    val statusLine: String = "Listo",
    val confirmationPrompt: String? = null,
    val isTreeTabEnabled: Boolean = true,
    val treeUnavailableMessage: String? = null,
    val pendingUpdates: Map<String, UpdateSuggestion> = emptyMap(),
    val isLoading: Boolean = false,
    val loadingMessage: String = "",
    val loadingFrame: Int = 0,
    val loadError: String? = null
) {
    val filteredIndexes: List<Int>
        get() {
            return entries.mapIndexedNotNull { index, entry ->
                if (matchesQuickFilter(entry)) index else null
            }
        }

    val selectedEntry: TuiDependencyEntry?
        get() {
            if (filteredIndexes.isEmpty()) return null
            val selectedIndex = filteredIndexes[cursor.coerceIn(0, filteredIndexes.lastIndex)]
            return entries.getOrNull(selectedIndex)
        }

    fun moveCursor(delta: Int): TuiState {
        if (filteredIndexes.isEmpty()) return copy(cursor = 0)
        return copy(cursor = (cursor + delta).coerceIn(0, filteredIndexes.lastIndex))
    }

    fun ensureCursorBounds(): TuiState {
        if (filteredIndexes.isEmpty()) return copy(cursor = 0)
        return copy(cursor = cursor.coerceIn(0, filteredIndexes.lastIndex))
    }

    fun cycleFilter(): TuiState {
        return copy(activeFilter = activeFilter.next(), cursor = 0, scrollOffset = 0)
    }

    fun nextTab(): TuiState = copy(activeTab = activeTab.next())

    fun previousTab(): TuiState = copy(activeTab = activeTab.previous())

    fun ensureScrollVisible(windowSize: Int): TuiState {
        if (filteredIndexes.isEmpty()) {
            return copy(scrollOffset = 0)
        }

        if (windowSize <= 0) {
            return copy(scrollOffset = 0)
        }

        val boundedCursor = cursor.coerceIn(0, filteredIndexes.lastIndex)
        val maxScroll = (filteredIndexes.size - windowSize).coerceAtLeast(0)

        val adjustedOffset = when {
            boundedCursor < scrollOffset -> boundedCursor
            boundedCursor >= scrollOffset + windowSize -> boundedCursor - windowSize + 1
            else -> scrollOffset
        }.coerceIn(0, maxScroll)

        return copy(cursor = boundedCursor, scrollOffset = adjustedOffset)
    }

    private fun matchesQuickFilter(entry: TuiDependencyEntry): Boolean {
        return when (activeFilter) {
            TuiQuickFilter.DIRECT -> !entry.source.equals("transitive", ignoreCase = true)
            TuiQuickFilter.ALL -> true
            TuiQuickFilter.CVE -> entry.vulnerabilityCount > 0
            TuiQuickFilter.OUTDATED -> entry.latestVersion != null
            TuiQuickFilter.TRANSITIVE -> entry.transitiveTreeLines.any { it.startsWith("  +") }
        }
    }
}