package com.optix.android.tools.exploit

import com.optix.android.domain.model.ParamType
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.ToolParam
import com.optix.android.tools.registry.BaseTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext

/**
 * Lightweight LPE finding model — kept local to this file (NOT in the domain model
 * per project conventions). Used to structure privilege-escalation enumeration results.
 */
data class LPEFinding(
    val title: String,
    val description: String,
    val severity: String,        // "low" | "medium" | "high" | "critical"
    val evidence: String,
    val recommendation: String = "",
)

data class LpeScanResult(
    val mode: String,            // "local" | "remote"
    val target: String?,
    val findings: List<LPEFinding>,
    val rawSummary: String,
)

/**
 * LinPEAS-style Linux privilege escalation enumeration.
 *
 *  - local  : reads /proc, /sys, suid binaries, cron, etc. directly via shell
 *             (requiresRoot = false — we gather whatever the shell can read)
 *  - remote : runs PEAS-like checks over SSH (requiresRoot = true — runs through
 *             Termux as root to reach the ssh binary cleanly)
 *
 * Structured findings are emitted as a single [ToolEvent.Result] carrying an
 * [LpeScanResult].
 */
class LinPEASTool : BaseTool() {

    /** Documented Termux requirement (ShellExecutor enforces availability at runtime). */
    private val requiresTermux: Boolean = false

    override val metadata = ToolMetadata(
        name = "linpeas_termux",
        displayName = "LinPEAS (PrivEsc)",
        description = "Linux privilege escalation enumeration — local device analysis or remote SSH PEAS-style checks",
        category = ToolCategory.LPE,
        requiresRoot = false,     // local mode; remote mode enforces root via executeTermuxAsRoot
        parameters = listOf(
            ToolParam(
                "mode", ParamType.ENUM, "Execution mode",
                defaultValue = "local",
                enumValues = listOf("local", "remote"),
            ),
            ToolParam("target", ParamType.STRING, "Target host (required for remote mode)", required = false, defaultValue = ""),
            ToolParam("ssh_user", ParamType.STRING, "SSH username (remote mode)", required = false, defaultValue = ""),
        ),
        chainSuggestions = listOf("metasploit_termux", "hydra_termux"),
    )

    override fun validate(params: Map<String, Any>): List<String> {
        val errors = super.validate(params).toMutableList()
        val mode = param(params, "mode") ?: "local"
        if (mode != "local" && mode != "remote") {
            errors.add("'mode' must be 'local' or 'remote'")
        }
        if (mode == "remote") {
            param(params, "target")?.takeIf { it.isNotBlank() }
                ?: errors.add("'target' is required for remote mode")
            // Termux availability is enforced at runtime by ShellExecutor.executeTermuxAsRoot.
        }
        return errors
    }

    override suspend fun execute(params: Map<String, Any>, emit: suspend (ToolEvent) -> Unit) {
        val mode = param(params, "mode") ?: "local"
        val target = param(params, "target") ?: ""
        val sshUser = param(params, "ssh_user") ?: ""

        val findings = mutableListOf<LPEFinding>()
        var raw = StringBuilder()

        when (mode) {
            "remote" -> {
                if (target.isBlank()) {
                    emit(ToolEvent.Error("target is required for remote mode"))
                    emit(ToolEvent.Done)
                    return
                }
                emit(ToolEvent.Log("Running remote PEAS-style enumeration over SSH -> $sshUser@$target", com.optix.android.domain.model.LogLevel.WARN))
                val cmds = remotePeasCommands()
                val remoteScript = cmds.joinToString("; ")
                val userPart = if (sshUser.isBlank()) target else "$sshUser@$target"
                // Pass through Termux as root so the ssh binary is reachable.
                shellExecutor?.executeTermuxAsRoot(
                    "ssh",
                    listOf("-o", "StrictHostKeyChecking=no", "-o", "BatchMode=yes", userPart, "'" + remoteScript.replace("'", "'\\''") + "'"),
                    "openssh",
                    emit,
                )
                emit(ToolEvent.Log("Remote enumeration dispatched. Configure passwordless SSH (key) for BatchMode to succeed."))
                findings.add(
                    LPEFinding(
                        title = "Remote enumeration dispatched",
                        description = "PEAS checks were executed over SSH against $userPart.",
                        severity = "low",
                        evidence = remoteScript,
                        recommendation = "Review the emitted logs for misconfigured sudo, SUID, cron and writable paths.",
                    ),
                )
            }
            else -> {
                emit(ToolEvent.Log("Running local Linux privesc enumeration on this device..."))
                val checks = localPeasCommands()
                withContext(Dispatchers.IO) {
                    for (check in checks) {
                        if (!currentCoroutineContext().isActive) break
                        emit(ToolEvent.Log("» ${check.label}"))
                        val out = runLocal(check.cmd)
                        raw.append("==== ${check.label} ====\n").append(out).append("\n")
                        findings.addAll(parseLocal(check, out))
                    }
                }
            }
        }

        val summary = buildString {
            append("LinPEAS ($mode) — ${findings.size} finding(s)\n")
            findings.forEachIndexed { i, f ->
                append("${i + 1}. [${f.severity.uppercase()}] ${f.title}\n")
            }
        }
        emit(ToolEvent.Log(summary))

        if (findings.isNotEmpty()) {
            emit(ToolEvent.Result(LpeScanResult(mode = mode, target = target.ifBlank { null }, findings = findings, rawSummary = raw.toString())))
        }
        emit(ToolEvent.ChainSuggestion("metasploit_termux", mapOf("rhosts" to (target.ifBlank { "127.0.0.1" }))))
        emit(ToolEvent.Done)
    }

    // ------------------------------------------------------------------
    // Local command set
    // ------------------------------------------------------------------

    private data class PeasCheck(val label: String, val cmd: String)

    private fun localPeasCommands(): List<PeasCheck> = listOf(
        PeasCheck("Current identity", "id; whoami"),
        PeasCheck("Sudo permissions", "sudo -n -l 2>/dev/null || echo 'sudo -l unavailable'"),
        PeasCheck("SUID binaries", "find / -perm -4000 -type f 2>/dev/null"),
        PeasCheck("Cron jobs", "ls -la /etc/cron* 2>/dev/null; cat /etc/crontab 2>/dev/null"),
        PeasCheck("Passwd/shadow", "ls -la /etc/passwd /etc/shadow 2>/dev/null"),
        PeasCheck("Writable paths", "find / -writable -type d 2>/dev/null | grep -v -E '/(proc|sys|dev)/' | head -n 40"),
        PeasCheck("Kernel version", "uname -a"),
        PeasCheck("ENV / capabilities", "env 2>/dev/null; grep -R 'Cap' /proc/self/status 2>/dev/null"),
        PeasCheck("Interesting services", "ls -la /etc/init.d/ 2>/dev/null; service --status-all 2>/dev/null"),
    )

    private fun remotePeasCommands(): List<String> = listOf(
        "id",
        "sudo -n -l 2>/dev/null",
        "find / -perm -4000 -type f 2>/dev/null",
        "ls -la /etc/cron* 2>/dev/null; cat /etc/crontab 2>/dev/null",
        "ls -la /etc/passwd /etc/shadow 2>/dev/null",
        "uname -a",
        "env",
    )

    // ------------------------------------------------------------------
    // Parsing heuristics
    // ------------------------------------------------------------------

    private fun parseLocal(check: PeasCheck, out: String): List<LPEFinding> {
        val items = mutableListOf<LPEFinding>()
        when (check.label) {
            "Current identity" -> {
                if (out.contains("uid=0(") || out.contains("root")) {
                    items.add(LPEFinding("Already running as root", "Current shell is root — no escalation needed.", "critical", out.trim()))
                }
            }
            "Sudo permissions" -> {
                if (out.contains("NOPASSWD") || out.contains("(ALL)") && !out.contains("unavailable")) {
                    items.add(LPEFinding("Passwordless sudo", "User can run commands as root without a password.", "high", out.trim(), "Abuse sudo to spawn a root shell."))
                }
            }
            "SUID binaries" -> {
                val interesting = out.lines().filter { line ->
                    line.contains(Regex("/(nmap|vim|bash|sh|find|cp|mv|nc|netcat|less|more|awk|perl|python|ruby|tar|zip|docker|pkexec|env|screen|watch)$"))
                }
                if (interesting.isNotEmpty()) {
                    items.add(LPEFinding("Interesting SUID binary", "A potentially exploitable SUID binary was found.", "high", interesting.joinToString("\n"), "Try GTFOBins techniques to escalate via the SUID binary."))
                }
            }
            "Passwd/shadow" -> {
                if (out.contains("passwd -rw") || out.contains("-rw- 1 root root /etc/passwd")) {
                    items.add(LPEFinding("Writable /etc/passwd", "/etc/passwd is writable — add a root user.", "critical", out.trim(), "echo 'root2::0:0::/root:/bin/sh' >> /etc/passwd"))
                }
            }
            "Cron jobs" -> {
                if (out.contains("root") && out.lines().any { it.contains(" ") && !it.startsWith("#") }) {
                    items.add(LPEFinding("System cron present", "Review cron entries for writable scripts or misconfigurations.", "medium", out.trim(), "Check for cron jobs running world-writable scripts."))
                }
            }
        }
        return items
    }

    private suspend fun runLocal(cmd: String): String = try {
        val process = ProcessBuilder("sh", "-c", cmd)
            .redirectErrorStream(true)
            .start()
        val reader = process.inputStream.bufferedReader()
        val sb = StringBuilder()
        while (true) {
            if (!currentCoroutineContext().isActive) { process.destroy(); break }
            val line = reader.readLine() ?: break
            sb.appendLine(line)
        }
        process.waitFor()
        sb.toString()
    } catch (e: Exception) {
        "Error: ${e.message}"
    }
}
