package com.optix.android.tools.osint

import com.optix.android.domain.model.OsintResult
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.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject

// Shared URL-encoding helper for this file.
private fun String.urlEncode(): String = java.net.URLEncoder.encode(this, "UTF-8")

/**
 * Shodan host lookup — returns open ports, services, banners, geolocation and
 * known vulnerabilities for a single IP. Requires a Shodan API key.
 *
 * Docs: https://api.shodan.io/shodan/host/{ip}?key={apiKey}
 */
class ShodanHostTool(private val client: OkHttpClient) : BaseTool() {

    override val metadata = ToolMetadata(
        name = "shodan_host",
        displayName = "Shodan Host Lookup",
        description = "Look up an IP on Shodan: open ports, banners, services, geolocation, ISP and known CVEs. Requires a Shodan API key.",
        category = ToolCategory.OSINT,
        parameters = listOf(
            ToolParam("ip", ParamType.STRING, "Target IP address (e.g. 8.8.8.8)"),
            ToolParam("api_key", ParamType.STRING, "Shodan API key (add it in Settings)"),
        ),
        apiKeyRef = "shodan",
        chainSuggestions = listOf("shodan_search", "nmap_termux", "http_inspector"),
    )

    override suspend fun execute(params: Map<String, Any>, emit: suspend (ToolEvent) -> Unit) {
        val ip = paramRequired(params, "ip")
        val apiKey = param(params, "api_key")
        if (apiKey.isNullOrBlank()) {
            emit(ToolEvent.Log("⚠️ Shodan API key is missing. Add it in Settings → API Keys (Shodan)."))
            emit(ToolEvent.Error("Shodan API key required. Open Settings and add your Shodan API key."))
            emit(ToolEvent.Done)
            return
        }

        val url = "https://api.shodan.io/shodan/host/${ip.urlEncode()}?key=${apiKey.urlEncode()}"
        emit(ToolEvent.Log("[Shodan] Looking up host $ip..."))

        withContext(Dispatchers.IO) {
            val request = Request.Builder().url(url).header("User-Agent", "OptixV4").build()
            runCatching {
                client.newCall(request).execute().use { response ->
                    val body = response.body?.string().orEmpty()
                    when (response.code) {
                        401, 403 -> {
                            emit(ToolEvent.Error("Shodan rejected the API key (${response.code}). Check it in Settings."))
                            return@use
                        }
                        404 -> {
                            emit(ToolEvent.Error("Shodan has no data for $ip (HTTP 404)."))
                            return@use
                        }
                        429 -> {
                            emit(ToolEvent.Error("Shodan rate limit reached (429). Upgrade your plan or retry later."))
                            return@use
                        }
                    }
                    if (body.isBlank()) {
                        emit(ToolEvent.Error("Empty response from Shodan (HTTP ${response.code})."))
                        return@use
                    }
                    val json = JSONObject(body)
                    if (json.has("error")) {
                        emit(ToolEvent.Error("Shodan error: ${json.optString("error")}"))
                        return@use
                    }

                    emit(ToolEvent.Result(OsintResult("IP", json.optString("ip_str", ip), "Shodan")))
                    json.optString("org").takeIf { it.isNotBlank() }
                        ?.let { emit(ToolEvent.Result(OsintResult("Organization", it, "Shodan"))) }
                    json.optString("os").takeIf { it.isNotBlank() }
                        ?.let { emit(ToolEvent.Result(OsintResult("OS", it, "Shodan"))) }
                    json.optString("isp").takeIf { it.isNotBlank() }
                        ?.let { emit(ToolEvent.Result(OsintResult("ISP", it, "Shodan"))) }
                    json.optString("country_name").takeIf { it.isNotBlank() }
                        ?.let { emit(ToolEvent.Result(OsintResult("Country", it, "Shodan"))) }
                    json.optString("city").takeIf { it.isNotBlank() }
                        ?.let { emit(ToolEvent.Result(OsintResult("City", it, "Shodan"))) }
                    json.optString("last_update").takeIf { it.isNotBlank() }
                        ?.let { emit(ToolEvent.Result(OsintResult("Last Update", it, "Shodan"))) }

                    val ports = json.optJSONArray("ports")
                    if (ports != null) {
                        val portList = (0 until ports.length()).map { ports.getInt(it) }.sorted()
                        emit(ToolEvent.Result(OsintResult("Open Ports", portList.joinToString(", "), "Shodan")))
                    }

                    val hostnames = json.optJSONArray("hostnames")
                    if (hostnames != null && hostnames.length() > 0) {
                        val names = (0 until hostnames.length()).map { hostnames.getString(it) }
                        emit(ToolEvent.Result(OsintResult("Hostnames", names.joinToString(", "), "Shodan")))
                    }

                    val vulns = json.optJSONObject("vulns")
                    if (vulns != null) {
                        val cves = mutableListOf<String>()
                        vulns.keys()?.forEach { cves.add(it) }
                        emit(ToolEvent.Result(OsintResult("Vulnerabilities", cves.joinToString(", "), "Shodan")))
                    }

                    val tags = json.optJSONArray("tags")
                    if (tags != null && tags.length() > 0) {
                        val tagList = (0 until tags.length()).map { tags.getString(it) }
                        emit(ToolEvent.Result(OsintResult("Tags", tagList.joinToString(", "), "Shodan")))
                    }

                    emit(ToolEvent.Log("[Shodan] Host lookup complete for $ip."))
                }
            }.onFailure { emit(ToolEvent.Error("Shodan request failed: ${it.message}")) }
        }

        emit(ToolEvent.ChainSuggestion("nmap_termux", mapOf("target" to ip)))
        emit(ToolEvent.Done)
    }
}

/**
 * Shodan search — runs a Shodan search query across the entire Shodan database
 * and returns matching hosts. Requires a Shodan API key.
 *
 * Docs: https://api.shodan.io/shodan/host/search?key={apiKey}&query={query}
 */
class ShodanSearchTool(private val client: OkHttpClient) : BaseTool() {

    override val metadata = ToolMetadata(
        name = "shodan_search",
        displayName = "Shodan Search",
        description = "Search the Shodan database with a filter query (e.g. 'apache country:\"US\"'). Requires a Shodan API key.",
        category = ToolCategory.OSINT,
        parameters = listOf(
            ToolParam("query", ParamType.STRING, "Shodan search query (e.g. 'port:22 country:\"DE\"')"),
            ToolParam("api_key", ParamType.STRING, "Shodan API key (add it in Settings)"),
        ),
        apiKeyRef = "shodan",
        chainSuggestions = listOf("shodan_host", "nmap_termux", "http_inspector"),
    )

    override suspend fun execute(params: Map<String, Any>, emit: suspend (ToolEvent) -> Unit) {
        val query = paramRequired(params, "query")
        val apiKey = param(params, "api_key")
        if (apiKey.isNullOrBlank()) {
            emit(ToolEvent.Log("⚠️ Shodan API key is missing. Add it in Settings → API Keys (Shodan)."))
            emit(ToolEvent.Error("Shodan API key required. Open Settings and add your Shodan API key."))
            emit(ToolEvent.Done)
            return
        }

        val url = "https://api.shodan.io/shodan/host/search?key=${apiKey.urlEncode()}&query=${query.urlEncode()}"
        emit(ToolEvent.Log("[Shodan] Searching: $query"))

        withContext(Dispatchers.IO) {
            val request = Request.Builder().url(url).header("User-Agent", "OptixV4").build()
            runCatching {
                client.newCall(request).execute().use { response ->
                    val body = response.body?.string().orEmpty()
                    when (response.code) {
                        401, 403 -> {
                            emit(ToolEvent.Error("Shodan rejected the API key (${response.code}). Check it in Settings."))
                            return@use
                        }
                        429 -> {
                            emit(ToolEvent.Error("Shodan rate limit reached (429). Upgrade your plan or retry later."))
                            return@use
                        }
                    }
                    if (body.isBlank()) {
                        emit(ToolEvent.Error("Empty response from Shodan (HTTP ${response.code})."))
                        return@use
                    }
                    val json = JSONObject(body)
                    if (json.has("error")) {
                        emit(ToolEvent.Error("Shodan error: ${json.optString("error")}"))
                        return@use
                    }

                    val total = json.optInt("total", 0)
                    emit(ToolEvent.Result(OsintResult("Total Matches", total.toString(), "Shodan")))
                    emit(ToolEvent.Log("[Shodan] $total total match(es). Showing first results."))

                    val matches = json.optJSONArray("matches")
                    val limit = minOf(matches?.length() ?: 0, 25)
                    for (i in 0 until limit) {
                        val m = matches?.optJSONObject(i) ?: continue
                        val ip = m.optString("ip_str", "?")
                        val port = m.optInt("port", -1)
                        val org = m.optString("org", "")
                        val country = m.optJSONObject("location")?.optString("country_name", "") ?: ""
                        val label = buildString {
                            append(ip)
                            if (port >= 0) append(":$port")
                            if (org.isNotBlank()) append(" ($org)")
                            if (country.isNotBlank()) append(" - $country")
                        }
                        emit(ToolEvent.Result(OsintResult("Match #$i", label, "Shodan")))
                    }
                    emit(ToolEvent.Log("[Shodan] Search complete."))
                }
            }.onFailure { emit(ToolEvent.Error("Shodan request failed: ${it.message}")) }
        }

        emit(ToolEvent.ChainSuggestion("shodan_host", mapOf("ip" to query)))
        emit(ToolEvent.Done)
    }
}
