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.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject

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

/**
 * OpenCTI threat-intelligence connector. Pushes/pulls STIX indicators from an
 * OpenCTI instance over its GraphQL API.
 *
 *   operation = "search" → query indicators by free-text search
 *   operation = "push"   → create a new Indicator from the provided query
 *
 * Docs: https://docs.opencti.io/latest/development/graphql/
 */
class OpenCTIIntegration(private val client: OkHttpClient) : BaseTool() {

    override val metadata = ToolMetadata(
        name = "opencti_integration",
        displayName = "OpenCTI Connector",
        description = "Push or pull threat-intel indicators from an OpenCTI instance via its GraphQL API.",
        category = ToolCategory.THREAT_INTEL,
        parameters = listOf(
            ToolParam(
                "operation", ParamType.ENUM, "Operation to perform", defaultValue = "search",
                enumValues = listOf("search", "push"),
            ),
            ToolParam("query", ParamType.STRING, "Search term (search) or indicator value/pattern (push)"),
            ToolParam("opencti_url", ParamType.STRING, "Base URL of the OpenCTI instance (e.g. https://opencti.example.com)"),
            ToolParam("api_key", ParamType.STRING, "OpenCTI API key (Bearer token)"),
        ),
        apiKeyRef = "opencti",
        chainSuggestions = listOf("shodan_search", "hibp_check"),
    )

    override suspend fun execute(params: Map<String, Any>, emit: suspend (ToolEvent) -> Unit) {
        val operation = (param(params, "operation") ?: "search").lowercase()
        val query = paramRequired(params, "query")
        val baseUrl = paramRequired(params, "opencti_url").trimEnd('/')
        val apiKey = paramRequired(params, "api_key")

        val graphqlUrl = "$baseUrl/graphql"
        emit(ToolEvent.Log("[OpenCTI] $operation on $baseUrl"))

        val (gqlQuery, variables) = when (operation) {
            "push" -> buildPush(query)
            else -> buildSearch(query)
        }

        val payload = JSONObject().apply {
            put("query", gqlQuery)
            put("variables", variables)
        }.toString()

        withContext(Dispatchers.IO) {
            val request = Request.Builder()
                .url(graphqlUrl)
                .post(payload.toRequestBody(JSON))
                .header("Authorization", "Bearer $apiKey")
                .header("Content-Type", "application/json")
                .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("OpenCTI rejected the API key (${response.code}). Check it in Settings."))
                            return@use
                        }
                    }
                    if (body.isBlank()) {
                        emit(ToolEvent.Error("Empty response from OpenCTI (HTTP ${response.code})."))
                        return@use
                    }
                    val json = JSONObject(body)
                    val errors = json.optJSONArray("errors")
                    if (errors != null && errors.length() > 0) {
                        val detail = errors.optJSONObject(0)?.optString("message") ?: errors.getString(0)
                        emit(ToolEvent.Error("OpenCTI GraphQL error: $detail"))
                        return@use
                    }

                    val data = json.optJSONObject("data")
                    if (data == null) {
                        emit(ToolEvent.Error("OpenCTI returned no data."))
                        return@use
                    }

                    if (operation == "push") {
                        val created = data.optJSONObject("indicatorAdd")
                        val id = created?.optString("id") ?: "unknown"
                        emit(ToolEvent.Result(OsintResult("Pushed Indicator ID", id, "OpenCTI")))
                        emit(ToolEvent.Log("[OpenCTI] Indicator pushed (id=$id)."))
                    } else {
                        val indicators = data.optJSONObject("indicators")
                        val edges = indicators?.optJSONArray("edges")
                        val count = edges?.length() ?: 0
                        emit(ToolEvent.Result(OsintResult("Indicators Found", count.toString(), "OpenCTI")))
                        val limit = minOf(count, 25)
                        for (i in 0 until limit) {
                            val node = edges?.optJSONObject(i)?.optJSONObject("node") ?: continue
                            val name = node.optString("name", "?")
                            val pattern = node.optString("pattern", "")
                            val score = node.optInt("x_opencti_score", -1)
                            val label = buildString {
                                append(name)
                                if (score >= 0) append(" (score=$score)")
                                if (pattern.isNotBlank()) append(" — $pattern")
                            }
                            emit(ToolEvent.Result(OsintResult("Indicator #$i", label, "OpenCTI")))
                        }
                        emit(ToolEvent.Log("[OpenCTI] Search complete — $count indicator(s)."))
                    }
                }
            }.onFailure { emit(ToolEvent.Error("OpenCTI request failed: ${it.message}")) }
        }

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

    private fun buildSearch(query: String): Pair<String, JSONObject> {
        val gql = """
            query SearchIndicators(${'$'}q: String) {
              indicators(search: ${'$'}q, first: 50) {
                edges {
                  node {
                    id
                    name
                    pattern
                    pattern_type
                    x_opencti_score
                    valid_from
                  }
                }
              }
            }
        """.trimIndent()
        val variables = JSONObject().put("q", query)
        return gql to variables
    }

    private fun buildPush(query: String): Pair<String, JSONObject> {
        val gql = """
            mutation CreateIndicator(${'$'}input: IndicatorAddInput!) {
              indicatorAdd(input: ${'$'}input) {
                id
              }
            }
        """.trimIndent()
        val input = JSONObject().apply {
            put("name", query)
            put("pattern", "[ipv4-addr:value = '$query']")
            put("pattern_type", "stix")
            put("x_opencti_score", 50)
            put("valid_from", java.time.OffsetDateTime.now().toString())
        }
        val variables = JSONObject().put("input", input)
        return gql to variables
    }

    companion object {
        private val JSON = "application/json; charset=utf-8".toMediaType()
    }
}
