package com.correx.infrastructure.tools.web

import com.correx.core.approvals.Tier
import com.correx.core.events.events.ToolRequest
import com.correx.core.events.types.ToolInvocationId
import com.correx.core.tools.contract.Tool
import com.correx.core.tools.contract.ToolCapability
import com.correx.core.tools.contract.ToolExecutor
import com.correx.core.tools.contract.ToolResult
import com.correx.core.tools.contract.ValidationResult
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.isSuccess
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import java.net.URLEncoder

/**
 * Searches the self-hosted SearXNG instance (research-workflow-spec §3). Local-first: no API keys,
 * no third-party telemetry. Search hits only the one configured [searxngBaseUrl], so unlike
 * [WebFetchTool] there is no arbitrary-host egress to police — it is [Tier.T1].
 *
 * Returns the top [maxResults] results as a readable list, with the result URLs in metadata so the
 * fetch stage can act on them (and the operator can approve the source list, not each URL).
 */
class WebSearchTool(
    private val httpClient: HttpClient,
    private val searxngBaseUrl: String,
    private val maxResults: Int = DEFAULT_MAX_RESULTS,
) : Tool, ToolExecutor {

    private val json = Json { ignoreUnknownKeys = true }

    override val name: String = "web_search"
    override val description: String = "Search the web (local SearXNG); returns titles, URLs, and snippets."
    override val tier: Tier = Tier.T1
    override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.NETWORK_ACCESS)
    override val parametersSchema: JsonObject = buildJsonObject {
        put("type", "object")
        putJsonObject("properties") {
            putJsonObject("query") {
                put("type", "string")
                put("description", "Search query.")
            }
        }
        put("required", buildJsonArray { add(JsonPrimitive("query")) })
    }

    override fun validateRequest(request: ToolRequest): ValidationResult {
        val query = request.parameters["query"] as? String
        return if (query.isNullOrBlank()) {
            ValidationResult.Invalid("Missing 'query' parameter.")
        } else {
            ValidationResult.Valid
        }
    }

    override suspend fun execute(request: ToolRequest): ToolResult {
        val invalid = validateRequest(request) as? ValidationResult.Invalid
        if (invalid != null) return ToolResult.Failure(request.invocationId, invalid.reason, recoverable = false)
        val query = request.parameters["query"] as String
        return runCatching { search(request.invocationId, query) }
            .getOrElse { ToolResult.Failure(request.invocationId, "search failed: ${it.message}", recoverable = true) }
    }

    private suspend fun search(invocationId: ToolInvocationId, query: String): ToolResult {
        val url = "${searxngBaseUrl.trimEnd('/')}/search?q=${URLEncoder.encode(query, "UTF-8")}&format=json"
        val response = httpClient.get(url)
        if (!response.status.isSuccess()) {
            return ToolResult.Failure(invocationId, "SearXNG HTTP ${response.status.value}", recoverable = true)
        }
        val results = json.decodeFromString<SearxResponse>(response.bodyAsText()).results.take(maxResults)
        return ToolResult.Success(
            invocationId = invocationId,
            output = render(query, results),
            metadata = mapOf(
                "query" to query,
                "result_count" to results.size.toString(),
                "urls" to results.joinToString("\n") { it.url },
            ),
        )
    }

    private fun render(query: String, results: List<SearxResult>): String {
        if (results.isEmpty()) return "No results for: $query"
        return results.mapIndexed { index, r ->
            buildString {
                append("${index + 1}. ${r.title.ifBlank { r.url }}\n")
                append("   ${r.url}")
                if (r.content.isNotBlank()) append("\n   ${r.content.trim()}")
            }
        }.joinToString("\n\n")
    }

    @Serializable
    private data class SearxResponse(val results: List<SearxResult> = emptyList())

    @Serializable
    private data class SearxResult(
        val url: String = "",
        val title: String = "",
        val content: String = "",
    )

    companion object {
        const val DEFAULT_MAX_RESULTS = 8
    }
}
