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

/** Local single-quote escaping (mirrors ShellExecutor.String.shellEscape). */
private fun String.shellEscape(): String = "'" + this.replace("'", "'\\''") + "'"

/**
 * BloodHound AD attack-path analysis wrapper.
 *
 *  - upload : collect Active Directory data via bloodhound-python (Termux) or parse a
 *             SharpHound .zip dump.
 *  - query  : runs Cypher-style analysis. Because Android cannot host a Neo4j instance,
 *             queries are processed against the local JSON dump and emitted as a textual
 *             attack-path report.
 */
data class BloodhoundResult(
    val operation: String,
    val queryType: String?,
    val dumpFile: String?,
    val report: String,
    val attackPaths: List<String>,
)

class BloodhoundTool : BaseTool() {

    private val requiresTermux: Boolean = true

    override val metadata = ToolMetadata(
        name = "bloodhound_termux",
        displayName = "BloodHound (AD)",
        description = "Active Directory attack-path analysis — collect with bloodhound-python or query a SharpHound dump",
        category = ToolCategory.LATERAL_MOVEMENT,
        requiresRoot = false,
        parameters = listOf(
            ToolParam(
                "operation", ParamType.ENUM, "Operation to perform",
                defaultValue = "upload",
                enumValues = listOf("upload", "query"),
            ),
            ToolParam("dump_file", ParamType.STRING, "Path to BloodHound .zip/.json dump (query mode)", required = false, defaultValue = ""),
            ToolParam(
                "query_type", ParamType.ENUM, "Cypher-style query (query mode)",
                required = false, defaultValue = "shortest_paths",
                enumValues = listOf("shortest_paths", "high_value_targets", "kerberoastable", "as_rep_roastable"),
            ),
            ToolParam("target", ParamType.STRING, "Domain controller / LDAP host (upload mode)", required = false, defaultValue = ""),
            ToolParam("domain", ParamType.STRING, "AD domain (upload mode)", required = false, defaultValue = ""),
            ToolParam("username", ParamType.STRING, "AD username (upload mode)", required = false, defaultValue = ""),
            ToolParam("password", ParamType.STRING, "AD password (upload mode)", required = false, defaultValue = ""),
        ),
        chainSuggestions = listOf("cme_termux", "rubeus_termux"),
    )

    override fun validate(params: Map<String, Any>): List<String> {
        val errors = super.validate(params).toMutableList()
        val op = param(params, "operation") ?: "upload"
        if (op !in listOf("upload", "query")) errors.add("'operation' must be 'upload' or 'query'")
        if (op == "query") {
            param(params, "dump_file")?.takeIf { it.isNotBlank() }
                ?: errors.add("'dump_file' is required for query mode")
        }
        if (op == "upload") {
            param(params, "target")?.takeIf { it.isNotBlank() }
                ?: errors.add("'target' is required for upload mode")
        }
        return errors
    }

    override suspend fun execute(params: Map<String, Any>, emit: suspend (ToolEvent) -> Unit) {
        val operation = param(params, "operation") ?: "upload"
        val dumpFile = param(params, "dump_file") ?: ""
        val queryType = param(params, "query_type") ?: "shortest_paths"
        val target = param(params, "target") ?: ""
        val domain = param(params, "domain") ?: ""
        val username = param(params, "username") ?: ""
        val password = param(params, "password") ?: ""

        if (operation == "upload") {
            if (target.isBlank()) {
                emit(ToolEvent.Error("target is required for upload mode"))
                emit(ToolEvent.Done)
                return
            }
            emit(ToolEvent.Log("Collecting AD data from $target via bloodhound-python (Termux)..."))
            val args = buildList {
                add("-d"); add(domain.ifBlank { "domain.local" })
                add("-u"); add(username.ifBlank { "guest" })
                add("-p"); add(password.ifBlank { "" })
                add("-ns"); add(target)
                add("--collection-method"); add("All")
                add("-c"); add("All")
            }
            emit(ToolEvent.Log("▶ bloodhound-python ${args.joinToString(" ")}"))
            withContext(Dispatchers.IO) {
                try {
                    shellExecutor?.executeTermuxAsRoot("bloodhound-python", args, "bloodhound", emit)
                } catch (e: Exception) {
                    emit(ToolEvent.Error("BloodHound upload failed: ${e.message}"))
                }
            }
            emit(ToolEvent.Log("Collection complete. Use operation='query' with dump_file to analyse the .zip output."))
            emit(ToolEvent.Result(BloodhoundResult("upload", null, null, "Collected from $target", emptyList())))
            emit(ToolEvent.ChainSuggestion("cme_termux", mapOf("target" to target, "protocol" to "smb")))
            emit(ToolEvent.Done)
            return
        }

        // query mode — process the JSON dump locally (no Neo4j on Android).
        if (dumpFile.isBlank()) {
            emit(ToolEvent.Error("dump_file is required for query mode"))
            emit(ToolEvent.Done)
            return
        }
        emit(ToolEvent.Log("Analysing dump '$dumpFile' for query '$queryType' (Neo4j unavailable on Android — JSON processed locally)..."))

        val (report, paths) = analyseDump(dumpFile, queryType, emit)
        emit(ToolEvent.Log(report))
        paths.forEach { emit(ToolEvent.Log("  → $it")) }

        emit(ToolEvent.Result(BloodhoundResult("query", queryType, dumpFile, report, paths)))
        emit(ToolEvent.ChainSuggestion("cme_termux", mapOf("target" to target, "protocol" to "smb")))
        emit(ToolEvent.Done)
    }

    /**
     * Process a BloodHound JSON dump with a Termux python helper (the standard `json`
     * module is always available in Termux). Extracts entities matching [queryType].
     */
    private suspend fun analyseDump(dumpFile: String, queryType: String, emit: suspend (ToolEvent) -> Unit): Pair<String, List<String>> {
        val cypher = cypherFor(queryType)
        emit(ToolEvent.Log("Cypher equivalent: $cypher"))

        val pyScript = buildPythonScript(dumpFile, queryType)
        // Write the script with Kotlin (avoids fragile shell quoting of embedded Python).
        val scriptPath = "/sdcard/Optix/optix_bh_query.py"
        try {
            java.io.File(scriptPath).writeText(pyScript)
        } catch (e: Exception) {
            emit(ToolEvent.Error("Could not write analysis script: ${e.message}"))
            return "BloodHound query aborted: ${e.message}" to emptyList()
        }

        val out = StringBuilder()
        var failed = false
        withContext(Dispatchers.IO) {
            try {
                // Single shell command string for `su -c`. `su -c` treats the whole 2nd
                // argument as ONE command, so the run-helper invocation must be self-contained.
                val cmd = "/data/data/com.termux/files/usr/bin/python3 ${scriptPath.shellEscape()}"
                val process = ProcessBuilder("su", "-c", cmd)
                    .redirectErrorStream(true)
                    .start()
                val reader = process.inputStream.bufferedReader()
                while (true) {
                    if (!currentCoroutineContext().isActive) { process.destroy(); break }
                    val line = reader.readLine() ?: break
                    out.appendLine(line)
                }
                val exit = process.waitFor()
                if (exit != 0 && out.isEmpty()) {
                    failed = true
                    emit(ToolEvent.Error("BloodHound analysis exited with code $exit. Is python3 installed in Termux?"))
                }
            } catch (e: Exception) {
                failed = true
                out.appendLine("Analysis error: ${e.message}")
            }
        }
        val text = out.toString()
        val paths = text.lines().filter { it.isNotBlank() && !it.startsWith("__COUNT__") }
        val report = "BloodHound '$queryType' analysis:\n$text".trimEnd()
        return report to paths.take(200)
    }

    private fun buildPythonScript(dumpFile: String, queryType: String): String {
        val safeDump = dumpFile.replace("'", "\\'")
        val safeQuery = queryType.replace("'", "\\'")
        return """
import json, glob, os, zipfile

dump = '$safeDump'
q = '$safeQuery'
files = []
if os.path.isdir(dump):
    files = glob.glob(os.path.join(dump, '*.json'))
elif dump.endswith('.zip'):
    z = zipfile.ZipFile(dump)
    for n in z.namelist():
        if n.endswith('.json'):
            try:
                d = json.loads(z.read(n).decode('utf-8', 'ignore'))
                files.extend(d if isinstance(d, list) else [])
            except Exception:
                pass
else:
    files = [dump]

nodes = []
for f in files:
    try:
        with open(f) as fh:
            data = json.load(fh)
        if isinstance(data, list):
            nodes.extend(data)
        elif isinstance(data, dict) and 'data' in data:
            nodes.extend(data['data'])
    except Exception:
        pass

out = []
for n in nodes:
    props = n.get('properties', {}) or n.get('props', {}) or {}
    name = props.get('name', props.get('objectid', 'unknown'))
    if q == 'kerberoastable' and props.get('hasspn') and props.get('kerberoastable'):
        out.append('Kerberoastable: %s' % name)
    elif q == 'as_rep_roastable' and props.get('dontreqpreauth'):
        out.append('AS-REP roastable: %s' % name)
    elif q == 'high_value_targets' and (props.get('highvalue') or props.get('admincount')):
        out.append('High value: %s' % name)
    elif q == 'shortest_paths' and (props.get('name') or props.get('objectid')):
        out.append('Node: %s' % name)

if not out:
    print('No matching entities found in dump.')
else:
    for line in out[:200]:
        print(line)
print('__COUNT__%d' % len(out))
""".trimIndent()
    }

    private fun cypherFor(queryType: String): String = when (queryType) {
        "high_value_targets" -> "MATCH (n) WHERE n.highvalue=true OR n.admincount=true RETURN n"
        "kerberoastable" -> "MATCH (n:User) WHERE n.hasspn=true RETURN n"
        "as_rep_roastable" -> "MATCH (n:User) WHERE n.dontreqpreauth=true RETURN n"
        else -> "MATCH p=shortestPath((a)-[*1..]->(b)) RETURN p"
    }
}
