package com.optix.android.ui.screens.lanmap

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.gson.Gson
import com.optix.android.domain.model.HostResult
import com.optix.android.domain.model.PortResult
import com.optix.android.domain.model.Session
import com.optix.android.domain.model.ToolCategory
import com.optix.android.domain.model.ToolEvent
import com.optix.android.domain.model.ToolMetadata
import com.optix.android.domain.model.ToolChain
import com.optix.android.domain.model.Workflow
import com.optix.android.domain.model.WorkflowStep
import com.optix.android.domain.repository.ChainRepository
import com.optix.android.domain.repository.NodeRepository
import com.optix.android.domain.repository.SettingsRepository
import com.optix.android.domain.repository.ToolRepository
import com.optix.android.domain.repository.WorkflowRepository
import com.optix.android.domain.model.ExportFormat
import com.optix.android.domain.usecase.RunToolUseCase
import com.optix.android.domain.usecase.SessionExporter
import com.optix.android.domain.usecase.SessionUseCase
import com.optix.android.domain.usecase.BootstrapCapabilities
import com.optix.android.domain.usecase.BootstrapStep
import com.optix.android.tools.engine.EnvironmentManager
import com.optix.android.util.NetworkUtils
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

data class ToolReadiness(
    val termuxInstalled: Boolean = false,
    val rootAvailable: Boolean = false,
    val readyCount: Int = 0,
    val needsTermuxCount: Int = 0,
    val needsRootCount: Int = 0,
)

sealed class BootstrapStatus {
    data object Idle : BootstrapStatus()
    data object Running : BootstrapStatus()
    data class TermuxMissing(val message: String) : BootstrapStatus()
    data class Done(
        val installed: List<String> = emptyList(),
        val failed: List<String> = emptyList(),
        val logs: List<String> = emptyList(),
    ) : BootstrapStatus()
}

data class LanMapUiState(
    val nodes: List<HostResult> = emptyList(),
    val scanning: Boolean = false,
    val selectedNode: HostResult? = null,
    val activeTargetIp: String? = null,
    val portScanProgress: Map<String, Pair<Int, Int>> = emptyMap(),
    val portScanning: Set<String> = emptySet(),
    val selfIp: String? = null,
    val gatewayIp: String? = null,
    val discoveryMode: String = "LAN",
    val selectedTool: ToolMetadata? = null,
    val params: Map<String, String> = emptyMap(),
    val results: List<Any> = emptyList(),
    val logs: List<String> = emptyList(),
    val isRunning: Boolean = false,
    val progress: Pair<Int, Int>? = null,
    val error: String? = null,
    val sessionSaved: Boolean = false,
    val favorites: Set<String> = emptySet(),
    val pendingInstall: Pair<String, String>? = null,
    val suggestedChain: Pair<String, Map<String, Any>>? = null,
    val globalConsoleLogs: List<String> = emptyList(),
    val recentSessions: List<Session> = emptyList(),
    val toolReadiness: ToolReadiness = ToolReadiness(),
    val exportResult: String? = null,
    val workflows: List<Workflow> = emptyList(),
    val workflowValidationError: String? = null,
    val activeChain: ToolChain? = null,
    val chainStepIndex: Int = 0,
    val chainStatus: String? = null,
    val pendingEthicalAck: ToolMetadata? = null,
    val bootstrapStatus: BootstrapStatus = BootstrapStatus.Idle,
)

@HiltViewModel
class LanMapViewModel @Inject constructor(
    private val runToolUseCase: RunToolUseCase,
    private val sessionUseCase: SessionUseCase,
    private val nodeRepository: NodeRepository,
    private val toolRepository: ToolRepository,
    private val settingsRepository: SettingsRepository,
    private val environmentManager: EnvironmentManager,
    private val networkUtils: NetworkUtils,
    private val gson: Gson,
    private val sessionExporter: SessionExporter,
    private val workflowRepository: WorkflowRepository,
    private val chainRepository: ChainRepository,
    private val bootstrapUseCase: BootstrapCapabilities,
) : ViewModel() {

    private val _uiState = MutableStateFlow(LanMapUiState())
    val uiState: StateFlow<LanMapUiState> = _uiState.asStateFlow()

    val toolsByCategory: Map<ToolCategory, List<ToolMetadata>> = ToolCategory.entries
        .associateWith { toolRepository.getToolsByCategory(it) }

    private var runJob: Job? = null
    private var startTime = 0L
    private var lastScanSeenNodeIds: Set<String> = emptySet()
    private var ethicalAckValue: Boolean = false

    init {
        settingsRepository.getAppSettings()
            .onEach { settings -> _uiState.update { it.copy(favorites = settings.favorites) } }
            .launchIn(viewModelScope)

        sessionUseCase.getAll()
            .onEach { sessions -> _uiState.update { it.copy(recentSessions = sessions.take(30)) } }
            .launchIn(viewModelScope)

        workflowRepository.getAllWorkflows()
            .onEach { workflows -> _uiState.update { it.copy(workflows = workflows) } }
            .launchIn(viewModelScope)

        settingsRepository.hasEthicalAck()
            .onEach { acked ->
                ethicalAckValue = acked
                if (acked) _uiState.update { it.copy(pendingEthicalAck = null) }
            }
            .launchIn(viewModelScope)

        nodeRepository.observeNodes()
            .onEach { nodes ->
                _uiState.update { state ->
                    val selectedKey = state.selectedNode?.stableNodeKey()
                    state.copy(
                        nodes = nodes,
                        selectedNode = if (selectedKey != null) nodes.firstOrNull { it.stableNodeKey() == selectedKey } else state.selectedNode,
                    )
                }
            }
            .launchIn(viewModelScope)

        viewModelScope.launch {
            refreshToolReadiness()
        }

        observeChains()
    }

    private fun globalLog(msg: String) {
        _uiState.update { state ->
            state.copy(globalConsoleLogs = (state.globalConsoleLogs + msg).takeLast(500))
        }
    }

    fun clearGlobalLogs() {
        _uiState.update { it.copy(globalConsoleLogs = emptyList()) }
    }

    fun refreshToolReadiness() {
        viewModelScope.launch {
            val allTools = toolRepository.getAllTools()
            val termuxInstalled = environmentManager.isTermuxInstalled()
            val rootAvailable = environmentManager.isRootAvailable()
            val termuxTools = allTools.filter { it.needsTermux() }
            val rootTools = allTools.filter { it.requiresRoot }
            val readyCount = allTools.count { tool ->
                (!tool.needsTermux() || termuxInstalled) && (!tool.requiresRoot || rootAvailable)
            }
            _uiState.update {
                it.copy(
                    toolReadiness = ToolReadiness(
                        termuxInstalled = termuxInstalled,
                        rootAvailable = rootAvailable,
                        readyCount = readyCount,
                        needsTermuxCount = termuxTools.size,
                        needsRootCount = rootTools.size,
                    ),
                )
            }
        }
    }

    fun exportNodeMap(): String {
        val nodes = _uiState.value.nodes
        return gson.toJson(
            mapOf(
                "generatedAt" to System.currentTimeMillis(),
                "activeTargetIp" to _uiState.value.activeTargetIp,
                "discoveryMode" to _uiState.value.discoveryMode,
                "nodeCount" to nodes.size,
                "nodes" to nodes,
            )
        )
    }

    fun exportSessions(format: ExportFormat, sessions: List<Session> = _uiState.value.recentSessions) {
        val data = sessions.ifEmpty { return }
        val output = when (format) {
            ExportFormat.JSON -> sessionExporter.toJson(data)
            ExportFormat.CSV -> sessionExporter.toCsv(data)
            ExportFormat.REPORT -> sessionExporter.toReport(data)
        }
        _uiState.update { it.copy(exportResult = output) }
    }

    fun clearExport() {
        _uiState.update { it.copy(exportResult = null) }
    }

    fun scanLan() {
        if (_uiState.value.scanning) return
        val net = networkUtils.getLocalNetworkState()
        if (net.subnet == null || !net.isWifiConnected) {
            scanWifiFallback(net)
            return
        }

        _uiState.update {
            it.copy(
                scanning = true,
                selectedNode = null,
                selfIp = net.ipAddress,
                gatewayIp = net.gatewayIp,
                discoveryMode = "LAN",
            )
        }
        lastScanSeenNodeIds = emptySet()
        globalLog("[LAN] Scanning ${net.subnet}...")

        viewModelScope.launch {
            runToolUseCase("lan_scanner", mapOf("subnet" to net.subnet, "timeout_ms" to "400"))
                .collectLatest { event ->
                    when (event) {
                        is ToolEvent.Result -> {
                            val host = event.data as? HostResult ?: return@collectLatest
                            handleDiscoveredNode(host, source = "LAN")
                        }
                        is ToolEvent.Done -> {
                            nodeRepository.markMissingNodes(lastScanSeenNodeIds)
                            _uiState.update { it.copy(scanning = false) }
                            globalLog("[LAN] Scan complete - ${_uiState.value.nodes.count { !it.status.equals("STALE", true) }} active hosts")
                        }
                        is ToolEvent.Error -> {
                            _uiState.update { it.copy(scanning = false) }
                            globalLog("[LAN] Error: ${event.message}")
                        }
                        else -> Unit
                    }
                }
        }
    }

    private fun scanWifiFallback(net: NetworkUtils.NetworkState) {
        _uiState.update {
            it.copy(
                scanning = true,
                selectedNode = null,
                selfIp = net.connectedSsid ?: "DEVICE",
                gatewayIp = null,
                discoveryMode = "WIFI",
            )
        }
        globalLog("[WIFI] LAN unavailable - loading nearby WiFi networks...")

        viewModelScope.launch {
            val wifiNodes = networkUtils.scanNearbyWifiNetworks().map { ap ->
                HostResult(
                    ip = ap.bssid ?: ap.ssid,
                    hostname = ap.ssid,
                    status = if (ap.isConnected) "CONNECTED" else "AVAILABLE",
                    macAddress = ap.bssid,
                    deviceType = "WiFi Network",
                    alias = ap.ssid,
                    transport = "wifi",
                    security = ap.security,
                    signalLevelDbm = ap.signalLevelDbm,
                    note = if (ap.isConnected) "Current WiFi network" else "Nearby access point",
                ).normalizedNode()
            }

            wifiNodes.forEach { node ->
                lastScanSeenNodeIds = lastScanSeenNodeIds + node.stableNodeKey()
                nodeRepository.upsertNodes(listOf(node))
            }
            _uiState.update { state ->
                state.copy(
                    nodes = mergeNodes(state.nodes, wifiNodes),
                    scanning = false,
                )
            }

            if (wifiNodes.isEmpty()) {
                globalLog(
                    if (networkUtils.hasWifiScanPermission()) "[WIFI] No nearby networks found."
                    else "[WIFI] Location permission is needed for WiFi discovery."
                )
            } else {
                globalLog("[WIFI] Found ${wifiNodes.size} nearby WiFi networks")
            }
        }
    }

    fun selectNode(node: HostResult?) {
        _uiState.update { it.copy(selectedNode = node) }
        if (node != null) {
            if (!node.transport.equals("wifi", ignoreCase = true)) {
                _uiState.update { it.copy(activeTargetIp = node.ip) }
            }
            val tool = _uiState.value.selectedTool
            if (tool != null) {
                _uiState.update { state ->
                    val updated = state.params.toMutableMap()
                    tool.parameters.forEach { p ->
                        val n = p.name.lowercase()
                        when {
                            n.contains("target") || n.contains("host") || n == "ip" || (n.contains("ip") && !n.contains("subnet")) -> {
                                updated[p.name] = node.ip
                            }
                            n.contains("mac") || n.contains("bssid") -> {
                                updated[p.name] = node.macAddress ?: ""
                            }
                            n.contains("port") -> {
                                updated[p.name] = node.openPorts.joinToString(",")
                            }
                        }
                    }
                    state.copy(params = updated)
                }
            }
        }
    }

    fun renameNode(ip: String, alias: String) {
        val trimmed = alias.trim().ifBlank { null }
        _uiState.update { state ->
            val updatedNodes = state.nodes.map { node ->
                if (node.ip == ip) {
                    node.copy(alias = trimmed, lastSeenAt = System.currentTimeMillis())
                } else {
                    node
                }
            }
            state.copy(
                nodes = updatedNodes,
                selectedNode = updatedNodes.find { it.stableNodeKey() == state.selectedNode?.stableNodeKey() },
            )
        }
        viewModelScope.launch {
            val current = _uiState.value.nodes.find { it.ip == ip }
            if (current != null) nodeRepository.upsertNodes(listOf(current))
        }
    }

    fun quickPortScan(host: HostResult) {
        if (host.ip in _uiState.value.portScanning) return
        _uiState.update { it.copy(portScanning = it.portScanning + host.ip) }
        globalLog("[PORT] Starting fast scan on ${host.ip} (1-1024)...")

        viewModelScope.launch {
            val openPorts = mutableListOf<Int>()
            runToolUseCase(
                "port_scanner",
                mapOf("target" to host.ip, "start_port" to "1", "end_port" to "1024", "timeout_ms" to "150"),
            ).collectLatest { event ->
                when (event) {
                    is ToolEvent.Result -> {
                        val port = event.data as? PortResult ?: return@collectLatest
                        if (port.state == "open") {
                            openPorts.add(port.port)
                            globalLog("[PORT] ${host.ip}:${port.port} open")
                        }
                    }
                    is ToolEvent.Progress -> _uiState.update {
                        it.copy(portScanProgress = it.portScanProgress + (host.ip to (event.current to event.total)))
                    }
                    is ToolEvent.Done, is ToolEvent.Error -> {
                        globalLog(
                            if (event is ToolEvent.Error) "[PORT] Error: ${event.message}"
                            else if (openPorts.isEmpty()) "[PORT] Done - no common ports found on ${host.ip}. Try a full scan."
                            else "[PORT] Done - ${openPorts.size} open ports on ${host.ip}"
                        )
                        _uiState.update { state ->
                            val updated = state.nodes.map { node ->
                                if (node.stableNodeKey() == host.stableNodeKey()) node.copy(openPorts = openPorts, lastSeenAt = System.currentTimeMillis()) else node
                            }
                            state.copy(
                                nodes = updated,
                                portScanning = state.portScanning - host.ip,
                                portScanProgress = state.portScanProgress - host.ip,
                                selectedNode = if (state.selectedNode?.stableNodeKey() == host.stableNodeKey()) {
                                    updated.find { it.stableNodeKey() == host.stableNodeKey() }
                                } else state.selectedNode,
                            )
                        }
                        viewModelScope.launch {
                            _uiState.value.nodes.find { it.stableNodeKey() == host.stableNodeKey() }?.let { node ->
                                nodeRepository.upsertNodes(listOf(node))
                            }
                        }
                    }
                    else -> Unit
                }
            }
        }
    }

    fun setActiveTarget(node: HostResult) = setActiveTarget(node.ip, node.transport)

    fun setActiveTarget(ip: String, transport: String = "lan") {
        if (transport.equals("wifi", ignoreCase = true)) {
            _uiState.update { it.copy(activeTargetIp = null) }
            globalLog("[TARGET] WiFi nodes are not valid IP targets.")
            return
        }

        _uiState.update { it.copy(activeTargetIp = ip) }
        val tool = _uiState.value.selectedTool ?: return
        val targetKeys = tool.parameters
            .filter { p ->
                val n = p.name.lowercase()
                n.contains("target") || n.contains("host") || n == "ip" || (n.contains("ip") && !n.contains("subnet"))
            }
            .map { it.name }

        if (targetKeys.isNotEmpty()) {
            _uiState.update { state ->
                val updated = state.params.toMutableMap()
                targetKeys.forEach { key -> updated[key] = ip }
                state.copy(params = updated)
            }
        }
    }

    fun clearActiveTarget() {
        _uiState.update { it.copy(activeTargetIp = null) }
    }

    fun selectTool(toolName: String) {
        val meta = toolRepository.getToolMetadata(toolName) ?: return
        val net = networkUtils.getLocalNetworkState()
        val activeTarget = _uiState.value.activeTargetIp
        val selectedNode = _uiState.value.selectedNode

        val defaults = meta.parameters.associate { p ->
            val n = p.name.lowercase()
            val dynamicValue = when {
                selectedNode != null && (n.contains("target") || n.contains("host") || n == "ip" || (n.contains("ip") && !n.contains("subnet"))) -> selectedNode.ip
                selectedNode != null && (n.contains("mac") || n.contains("bssid")) -> selectedNode.macAddress ?: ""
                selectedNode != null && n.contains("port") -> selectedNode.openPorts.joinToString(",")
                activeTarget != null && (n.contains("target") || n.contains("host") || n == "ip") -> activeTarget
                n.contains("subnet") && net.subnet != null -> net.subnet
                else -> p.defaultValue
            }
            p.name to (dynamicValue ?: "")
        }

        _uiState.update {
            it.copy(
                selectedTool = meta,
                params = defaults,
                results = emptyList(),
                logs = emptyList(),
                error = null,
                progress = null,
                sessionSaved = false,
            )
        }
    }

    fun updateParam(key: String, value: String) {
        _uiState.update { it.copy(params = it.params + (key to value)) }
    }

    fun runTool() {
        val tool = _uiState.value.selectedTool ?: return
        if (_uiState.value.isRunning) return

        // Pre-execution ethical-use confirmation gate (one-time per session)
        if (_uiState.value.pendingEthicalAck == null && !ethicalAckValue) {
            _uiState.update { it.copy(pendingEthicalAck = tool) }
            return
        }

        proceedRunTool()
    }

    /** Invoked after the ethical-use acknowledgement has been given. */
    private fun proceedRunTool() {
        val tool = _uiState.value.selectedTool ?: return
        if (_uiState.value.isRunning) return

        _uiState.update {
            it.copy(isRunning = true, results = emptyList(), logs = emptyList(), error = null, progress = null, sessionSaved = false, suggestedChain = null)
        }
        startTime = System.currentTimeMillis()

        runJob = viewModelScope.launch {
            try {
                val params: Map<String, Any> = _uiState.value.params
                runToolUseCase(tool.name, params).collectLatest { event ->
                    when (event) {
                        is ToolEvent.Log -> {
                            _uiState.update { it.copy(logs = it.logs + event.message) }
                            globalLog(event.message)
                        }
                        is ToolEvent.Result -> {
                            _uiState.update { it.copy(results = it.results + event.data) }
                        }
                        is ToolEvent.Progress -> _uiState.update { it.copy(progress = event.current to event.total) }
                        is ToolEvent.Error -> {
                            _uiState.update { it.copy(error = event.message, logs = it.logs + "[ERROR] ${event.message}") }
                            globalLog("[${tool.name}] ERROR: ${event.message}")
                        }
                        is ToolEvent.InstallRequired -> _uiState.update {
                            it.copy(
                                isRunning = false,
                                pendingInstall = event.toolName to event.aptPackage,
                                logs = it.logs + "[WARNING] '${event.toolName}' not found - install '${event.aptPackage}' via Termux?",
                            )
                        }
                        is ToolEvent.Done -> {
                            if (tool.name == "port_scanner") {
                                syncPortsToNode()
                            }
                            globalLog("[${tool.name}] Done - ${_uiState.value.results.size} results")
                            _uiState.update { it.copy(isRunning = false, progress = null) }
                            autoSaveSession(tool.name)
                        }
                        is ToolEvent.ChainSuggestion -> {
                            if (toolRepository.getToolMetadata(event.nextTool) != null) {
                                _uiState.update { it.copy(suggestedChain = event.nextTool to event.params) }
                                globalLog("[CHAIN] Suggested next tool: ${event.nextTool}")
                            }
                        }
                        is ToolEvent.TermuxMissing -> {
                            _uiState.update {
                                it.copy(
                                    isRunning = false,
                                    pendingInstall = event.toolName to event.aptPackage,
                                    logs = it.logs + "[WARNING] Termux environment missing or configured incorrectly for '${event.toolName}'"
                                )
                            }
                        }
                        is ToolEvent.CapabilityMissing -> {
                            _uiState.update {
                                it.copy(
                                    isRunning = false,
                                    error = "${event.toolName}: ${event.reason}",
                                    logs = it.logs + "[CAPABILITY] ${event.toolName}: ${event.reason}"
                                )
                            }
                        }
                    }
                }
            } catch (t: Throwable) {
                val message = t.message ?: "Unexpected tool failure"
                _uiState.update { it.copy(error = message, isRunning = false, progress = null, logs = it.logs + "[ERROR] $message") }
                globalLog("[${tool.name}] ERROR: $message")
            }
        }
    }

    fun cancelTool() {
        runJob?.cancel()
        _uiState.update { it.copy(isRunning = false, progress = null) }
    }

    /** Accept the pre-execution ethical-use confirmation and proceed with the pending run. */
    fun confirmEthicalAck() {
        viewModelScope.launch { settingsRepository.setEthicalAck(true) }
        val pending = _uiState.value.pendingEthicalAck
        _uiState.update { it.copy(pendingEthicalAck = null) }
        if (pending != null) {
            proceedRunTool()
        }
    }

    /** Dismiss the confirmation dialog without running the tool. */
    fun dismissEthicalAck() {
        _uiState.update { it.copy(pendingEthicalAck = null) }
    }

    fun confirmInstall() {
        val tool = _uiState.value.selectedTool ?: return
        if (_uiState.value.pendingEthicalAck == null && !ethicalAckValue) {
            _uiState.update { it.copy(pendingEthicalAck = tool) }
            return
        }
        _uiState.update { it.copy(pendingInstall = null, isRunning = true, logs = it.logs + "[INFO] Installing and retrying...") }
        runJob = viewModelScope.launch {
            try {
                val params: Map<String, Any> = _uiState.value.params + ("__auto_install" to "true")
                runToolUseCase(tool.name, params).collectLatest { event ->
                    when (event) {
                        is ToolEvent.Log -> _uiState.update { it.copy(logs = it.logs + event.message) }
                        is ToolEvent.Result -> _uiState.update { it.copy(results = it.results + event.data) }
                        is ToolEvent.Progress -> _uiState.update { it.copy(progress = event.current to event.total) }
                        is ToolEvent.Error -> _uiState.update { it.copy(error = event.message, isRunning = false) }
                        is ToolEvent.InstallRequired -> _uiState.update { it.copy(isRunning = false, pendingInstall = event.toolName to event.aptPackage) }
                        is ToolEvent.Done -> _uiState.update { it.copy(isRunning = false, progress = null) }
                        is ToolEvent.ChainSuggestion -> {
                            if (toolRepository.getToolMetadata(event.nextTool) != null) {
                                _uiState.update { it.copy(suggestedChain = event.nextTool to event.params) }
                            }
                        }
                        is ToolEvent.TermuxMissing -> _uiState.update { it.copy(isRunning = false, pendingInstall = event.toolName to event.aptPackage) }
                        is ToolEvent.CapabilityMissing -> _uiState.update { it.copy(isRunning = false, error = "${event.toolName}: ${event.reason}") }
                    }
                }
            } catch (t: Throwable) {
                _uiState.update { it.copy(error = t.message ?: "Unexpected install failure", isRunning = false, progress = null) }
            }
        }
    }

    fun dismissInstall() {
        _uiState.update { it.copy(pendingInstall = null) }
    }

    /** Triggers one-time capability bootstrapping (root-check + Termux package install). */
    fun bootstrapCapabilities() {
        if (_uiState.value.bootstrapStatus is BootstrapStatus.Running) return
        _uiState.update { it.copy(bootstrapStatus = BootstrapStatus.Running) }

        viewModelScope.launch {
            val installed = mutableListOf<String>()
            val failed = mutableListOf<String>()
            val logs = mutableListOf<String>()
            bootstrapUseCase().collect { step ->
                when (step) {
                    is BootstrapStep.CapabilityCheck ->
                        logs.add("[SETUP] Termux=${step.termuxInstalled}  Root=${step.rootAvailable}")
                    is BootstrapStep.TermuxMissing -> {
                        logs.add("[SETUP] ${step.message}")
                        _uiState.update { it.copy(bootstrapStatus = BootstrapStatus.TermuxMissing(step.message)) }
                    }
                    is BootstrapStep.Installing -> logs.add("[SETUP] Installing ${step.packageName}...")
                    is BootstrapStep.PackageInstalled -> installed.add(step.packageName)
                    is BootstrapStep.PackageFailed -> failed.add(step.packageName)
                    is BootstrapStep.Log -> logs.add(step.message)
                    is BootstrapStep.Finished -> {
                        logs.add(
                            "[SETUP] Done — installed: ${installed.size}, failed: ${failed.size}"
                        )
                        _uiState.update {
                            it.copy(
                                bootstrapStatus = BootstrapStatus.Done(
                                    installed = installed.toList(),
                                    failed = failed.toList(),
                                    logs = logs.toList(),
                                )
                            )
                        }
                        refreshToolReadiness()
                    }
                }
            }
        }
    }

    fun dismissBootstrap() {
        _uiState.update { it.copy(bootstrapStatus = BootstrapStatus.Idle) }
    }

    /**
     * Launch a contextual follow-up tool pre-targeted at an entity found in a result card.
     * Reuses the existing selectTool + updateParam + runTool machinery so execution logic
     * is not duplicated.
     */
    fun runAction(toolSlug: String, params: Map<String, String>) {
        if (toolRepository.getToolMetadata(toolSlug) == null) return
        selectTool(toolSlug)
        params.forEach { (k, v) -> updateParam(k, v) }
        _uiState.update { it.copy(suggestedChain = null) }
        runTool()
    }

    /** Run the tool suggested by the last run's ChainSuggestion, prefilling its params. */
    fun runSuggestedChain() {
        val chain = _uiState.value.suggestedChain ?: return
        selectTool(chain.first)
        chain.second.forEach { (k, v) -> updateParam(k, v.toString()) }
        _uiState.update { it.copy(suggestedChain = null) }
        runTool()
    }

    fun clearSuggestedChain() {
        _uiState.update { it.copy(suggestedChain = null) }
    }

    // ------------------------------------------------------------------
    // AI-orchestrated multi-step chains
    // ------------------------------------------------------------------

    private var chainJob: Job? = null

    /**
     * Observe chains forwarded from the AI assistant (via [ChainRepository]) and
     * enqueue them for sequential execution. Each step runs to completion before
     * the next starts; later steps receive updated params from prior results
     * (e.g. discovered IPs / open ports surfaced by earlier steps).
     */
    private fun observeChains() {
        chainRepository.pendingChain
            .onEach { chain -> enqueueChain(chain) }
            .launchIn(viewModelScope)
    }

    /** Enqueue a chain for sequential execution. Ignored while another chain runs. */
    fun enqueueChain(chain: ToolChain) {
        if (_uiState.value.activeChain != null) {
            globalLog("[CHAIN] A chain is already running — ignoring new chain '${chain.name}'")
            return
        }
        if (chain.steps.isEmpty()) {
            globalLog("[CHAIN] Received an empty chain — nothing to run.")
            return
        }
        globalLog("[CHAIN] Enqueuing '${chain.name}' (${chain.steps.size} steps)")
        _uiState.update {
            it.copy(activeChain = chain, chainStepIndex = 0, chainStatus = "Starting…")
        }
        chainJob = viewModelScope.launch { runChainSequentially(chain) }
    }

    private suspend fun runChainSequentially(chain: ToolChain) {
        var inheritedParams: Map<String, String> = emptyMap()
        try {
            chain.steps.forEachIndexed { index, step ->
                _uiState.update {
                    it.copy(
                        chainStepIndex = index,
                        chainStatus = "Step ${index + 1}/${chain.steps.size}: ${step.toolSlug}",
                    )
                }
                // Merge prior-step results with the step's own params (own params win).
                val mergedParams = (inheritedParams + step.params).toMap()
                globalLog("[CHAIN] Running step ${index + 1}: ${step.toolSlug}")

                val results = runStep(step.toolSlug, mergedParams)
                // Surface discovered hosts / open ports for subsequent steps.
                inheritedParams = deriveInheritedParams(results)
            }
            _uiState.update {
                it.copy(chainStatus = "Chain '${chain.name}' complete", chainStepIndex = chain.steps.size)
            }
            globalLog("[CHAIN] Finished '${chain.name}'")
        } catch (t: Throwable) {
            val message = t.message ?: "Chain step failed"
            _uiState.update { it.copy(chainStatus = "Chain failed at step ${_uiState.value.chainStepIndex + 1}: $message") }
            globalLog("[CHAIN] Error: $message")
        } finally {
            // Keep the chain visible briefly then clear so UI can re-trigger.
            _uiState.update { it.copy(activeChain = null, chainStepIndex = 0, chainStatus = null) }
        }
    }

    /** Run a single chain step and collect all result events. Returns the raw results. */
    private suspend fun runStep(
        toolSlug: String,
        params: Map<String, String>,
    ): List<Any> {
        val results = mutableListOf<Any>()
        runToolUseCase(toolSlug, params).collectLatest { event ->
            when (event) {
                is ToolEvent.Result -> {
                    results.add(event.data)
                    _uiState.update { it.copy(results = it.results + event.data) }
                }
                is ToolEvent.Log -> {
                    _uiState.update { it.copy(logs = it.logs + event.message) }
                    globalLog(event.message)
                }
                is ToolEvent.Progress -> _uiState.update { it.copy(progress = event.current to event.total) }
                is ToolEvent.Error -> globalLog("[CHAIN][${toolSlug}] ERROR: ${event.message}")
                is ToolEvent.Done -> { /* step finished */ }
                else -> Unit
            }
        }
        return results
    }

    /**
     * Derive params that later chain steps can reuse. Pulls the first discovered
     * host IP and any open ports from prior results so a downstream port-scan or
     * vuln step can target what the earlier discovery step actually found.
     */
    private fun deriveInheritedParams(results: List<Any>): Map<String, String> {
        val hosts = results.filterIsInstance<HostResult>()
        val ports = results.filterIsInstance<PortResult>().filter { it.state == "open" }.map { it.port }
        val params = mutableMapOf<String, String>()
        hosts.firstOrNull()?.let { params["target"] = it.ip }
        if (ports.isNotEmpty()) params["ports"] = ports.joinToString(",")
        return params
    }

    fun cancelChain() {
        chainJob?.cancel()
        _uiState.update { it.copy(activeChain = null, chainStepIndex = 0, chainStatus = null) }
        globalLog("[CHAIN] Cancelled by user")
    }

    /**
     * Build a single-step workflow from the currently selected tool + params.
     * Returns null when no tool is selected.
     */
    fun currentStepFromSelection(): WorkflowStep? {
        val tool = _uiState.value.selectedTool ?: return null
        return WorkflowStep(toolSlug = tool.name, params = _uiState.value.params)
    }

    /**
     * Build steps from the most recent ChainSuggestion (the suggested follow-up tool).
     * Returns null when there is no pending suggestion.
     */
    fun stepsFromSuggestedChain(): List<WorkflowStep>? {
        val chain = _uiState.value.suggestedChain ?: return null
        val params = chain.second.mapValues { it.value.toString() }
        return listOf(WorkflowStep(toolSlug = chain.first, params = params))
    }

    /**
     * Persist a workflow. Validates that every step references a registered tool slug.
     * Returns the new workflow id, or -1 when validation fails (error stored in UI state).
     */
    fun saveWorkflow(name: String, steps: List<WorkflowStep>): Long {
        val trimmed = name.trim()
        val invalid = steps.firstOrNull { toolRepository.getToolMetadata(it.toolSlug) == null }
        if (trimmed.isBlank()) {
            _uiState.update { it.copy(workflowValidationError = "Workflow name cannot be empty") }
            return -1
        }
        if (steps.isEmpty()) {
            _uiState.update { it.copy(workflowValidationError = "Workflow must contain at least one step") }
            return -1
        }
        if (invalid != null) {
            _uiState.update { it.copy(workflowValidationError = "Unknown tool: ${invalid.toolSlug}") }
            return -1
        }
        _uiState.update { it.copy(workflowValidationError = null) }
        var id = -1L
        viewModelScope.launch {
            id = workflowRepository.saveWorkflow(
                Workflow(name = trimmed, steps = steps, createdAt = System.currentTimeMillis())
            )
            globalLog("[WORKFLOW] Saved \"$trimmed\" (${steps.size} steps)")
        }
        return id
    }

    fun deleteWorkflow(id: Long) {
        viewModelScope.launch { workflowRepository.deleteWorkflow(id) }
    }

    /**
     * Run a saved workflow: execute each step sequentially, reusing the existing
     * tool-run path. Each step's params are merged over the current selection so that
     * values like target IP are carried forward into the run.
     */
    fun runWorkflow(id: Long) {
        val workflow = _uiState.value.workflows.firstOrNull { it.id == id } ?: return
        if (_uiState.value.isRunning) return
        _uiState.update {
            it.copy(isRunning = true, results = emptyList(), logs = emptyList(), error = null, progress = null, sessionSaved = false, suggestedChain = null)
        }
        startTime = System.currentTimeMillis()
        globalLog("[WORKFLOW] Running \"${workflow.name}\" (${workflow.steps.size} steps)")

        runJob = viewModelScope.launch {
            try {
                workflow.steps.forEachIndexed { index, step ->
                    globalLog("[WORKFLOW] Step ${index + 1}/${workflow.steps.size}: ${step.toolSlug}")
                    runToolUseCase(step.toolSlug, step.params).collectLatest { event ->
                        when (event) {
                            is ToolEvent.Log -> {
                                _uiState.update { it.copy(logs = it.logs + event.message) }
                                globalLog(event.message)
                            }
                            is ToolEvent.Result -> _uiState.update { it.copy(results = it.results + event.data) }
                            is ToolEvent.Progress -> _uiState.update { it.copy(progress = event.current to event.total) }
                            is ToolEvent.Error -> {
                                _uiState.update { it.copy(error = event.message, logs = it.logs + "[ERROR] ${event.message}") }
                                globalLog("[WORKFLOW] ERROR: ${event.message}")
                            }
                            is ToolEvent.InstallRequired -> _uiState.update {
                                it.copy(
                                    isRunning = false,
                                    pendingInstall = event.toolName to event.aptPackage,
                                    logs = it.logs + "[WARNING] '${event.toolName}' not found - install '${event.aptPackage}' via Termux?",
                                )
                            }
                            is ToolEvent.TermuxMissing -> _uiState.update {
                                it.copy(
                                    isRunning = false,
                                    pendingInstall = event.toolName to event.aptPackage,
                                    logs = it.logs + "[WARNING] Termux environment missing or configured incorrectly for '${event.toolName}'",
                                )
                            }
                            is ToolEvent.CapabilityMissing -> _uiState.update {
                                it.copy(
                                    isRunning = false,
                                    error = "${event.toolName}: ${event.reason}",
                                    logs = it.logs + "[CAPABILITY] ${event.toolName}: ${event.reason}",
                                )
                            }
                            is ToolEvent.ChainSuggestion -> Unit
                            is ToolEvent.Done -> Unit
                        }
                    }
                }
                globalLog("[WORKFLOW] \"${workflow.name}\" complete - ${_uiState.value.results.size} results")
                _uiState.update { it.copy(isRunning = false, progress = null) }
            } catch (t: Throwable) {
                val message = t.message ?: "Unexpected workflow failure"
                _uiState.update { it.copy(error = message, isRunning = false, progress = null, logs = it.logs + "[ERROR] $message") }
                globalLog("[WORKFLOW] ERROR: $message")
            }
        }
    }

    fun toggleFavorite(toolId: String) {
        viewModelScope.launch { settingsRepository.toggleFavorite(toolId) }
    }

    private fun handleDiscoveredNode(host: HostResult, source: String) {
        val now = System.currentTimeMillis()
        val normalized = host.normalizedNode(now)
        lastScanSeenNodeIds = lastScanSeenNodeIds + normalized.stableNodeKey()
        viewModelScope.launch { nodeRepository.upsertNodes(listOf(normalized)) }
        mergeNode(normalized)
        globalLog("[$source] Found ${normalized.ip}${normalized.hostname?.let { " ($it)" } ?: ""}")
    }

    private fun mergeNode(node: HostResult) {
        _uiState.update { state ->
            val updated = mergeNodes(state.nodes, listOf(node))
            state.copy(
                nodes = updated,
                selectedNode = if (state.selectedNode?.stableNodeKey() == node.stableNodeKey()) {
                    updated.firstOrNull { it.stableNodeKey() == node.stableNodeKey() }
                } else state.selectedNode,
            )
        }
    }

    private fun mergeNodes(existing: List<HostResult>, incoming: List<HostResult>): List<HostResult> {
        val merged = existing.associateBy { it.stableNodeKey() }.toMutableMap()
        incoming.forEach { node -> merged[node.stableNodeKey()] = node }
        return merged.values.sortedWith(compareByDescending<HostResult> { it.lastSeenAt }.thenBy { it.ip })
    }

    private fun syncPortsToNode() {
        val targetKey = _uiState.value.params["target"]?.lowercase().orEmpty()
        val openPorts = _uiState.value.results
            .filterIsInstance<PortResult>()
            .filter { it.state == "open" }
            .map { it.port }
        if (targetKey.isBlank() || openPorts.isEmpty()) return

        val updated = _uiState.value.nodes.map { node ->
            if (node.ip.equals(targetKey, ignoreCase = true) || node.stableNodeKey() == targetKey) {
                node.copy(openPorts = openPorts, lastSeenAt = System.currentTimeMillis())
            } else {
                node
            }
        }
        _uiState.update { it.copy(nodes = updated) }
        viewModelScope.launch {
            updated.firstOrNull { it.ip.equals(targetKey, ignoreCase = true) || it.stableNodeKey() == targetKey }?.let { nodeRepository.upsertNodes(listOf(it)) }
        }
    }

    private suspend fun autoSaveSession(toolName: String) {
        val state = _uiState.value
        if (state.results.isEmpty()) return
        sessionUseCase.save(
            Session(
                toolName = state.selectedTool?.displayName ?: toolName,
                toolSlug = toolName,
                params = state.params,
                results = state.results.map { gson.toJson(it) },
                logs = state.logs,
                durationMs = System.currentTimeMillis() - startTime,
            )
        )
        _uiState.update { it.copy(sessionSaved = true) }
    }

    private fun HostResult.normalizedNode(now: Long = System.currentTimeMillis()): HostResult {
        val key = stableNodeKey()
        val existing = _uiState.value.nodes.firstOrNull { it.stableNodeKey() == key }
        return copy(
            nodeId = key,
            alias = alias ?: existing?.alias,
            firstSeenAt = existing?.firstSeenAt?.takeIf { it > 0 } ?: firstSeenAt.takeIf { it > 0 } ?: now,
            lastSeenAt = now,
            seenCount = (existing?.seenCount ?: seenCount).coerceAtLeast(0) + 1,
        )
    }

    private fun HostResult.stableNodeKey(): String = nodeId
        ?: when {
            !macAddress.isNullOrBlank() -> "${transport.lowercase()}:${macAddress.lowercase()}"
            transport.equals("wifi", ignoreCase = true) -> "wifi:${ip.lowercase()}"
            else -> "lan:${ip.lowercase()}"
        }

    private fun ToolMetadata.needsTermux(): Boolean =
        name.contains("termux", ignoreCase = true) || description.contains("termux", ignoreCase = true)
}
