package com.correx.core.toolintent.rules

import com.correx.core.events.events.ToolCallObservation
import com.correx.core.events.risk.RiskAction
import com.correx.core.toolintent.ToolCallAssessment
import com.correx.core.toolintent.ToolCallAssessmentInput
import com.correx.core.toolintent.ToolCallRule
import com.correx.core.toolintent.maxAction
import com.correx.core.tools.contract.ParamRole
import com.correx.core.tools.contract.ToolCapability
import com.correx.core.validation.model.ValidationIssue
import com.correx.core.validation.model.ValidationSeverity
import java.net.URI

/**
 * Host allow/deny for network-capable tools. Dispatches on NETWORK_ACCESS.
 * Extracts hosts from URL-like string parameters (generic; no param-name
 * hardcoding). A denied host → BLOCK (terminal); with a non-empty allow-list,
 * any host not on it → PROMPT_USER; otherwise PROCEED. Suffix matches treat a
 * configured "example.com" as covering "api.example.com".
 */
@Suppress("ReturnCount")
class NetworkHostRule(
    allowedHosts: Set<String>,
    deniedHosts: Set<String>,
) : ToolCallRule {

    private val allowed: Set<String> = allowedHosts.map { it.lowercase() }.toSet()
    private val denied: Set<String> = deniedHosts.map { it.lowercase() }.toSet()

    override fun appliesTo(capabilities: Set<ToolCapability>): Boolean =
        ToolCapability.NETWORK_ACCESS in capabilities

    override fun assess(input: ToolCallAssessmentInput): ToolCallAssessment {
        val issues = mutableListOf<ValidationIssue>()
        val observations = mutableListOf<ToolCallObservation>()
        var disposition = RiskAction.PROCEED

        for (host in hosts(input)) {
            val denyHit = denied.any { host == it || host.endsWith(".$it") }
            // Per-session egress grants (folded from EgressHostsGrantedEvent via
            // EgressAllowlistProjection at the build site) union with the static allow-list: a host
            // granted to the session is allowed even if it is not in the static set. Empty = no
            // grants, which never narrows the static allow-list.
            val allowHit = EgressAllowlist.isAllowed(host, allowed, input.sessionEgressHosts)
            observations += ToolCallObservation(
                ruleCode = RULE_CODE,
                facts = mapOf("host" to host, "denied" to denyHit.toString(), "allowed" to allowHit.toString()),
            )
            when {
                denyHit -> {
                    issues += ValidationIssue(
                        code = "NETWORK_HOST_DENIED",
                        message = "Tool '${input.request.toolName}' targets denied host: $host",
                        severity = ValidationSeverity.ERROR,
                    )
                    disposition = maxAction(disposition, RiskAction.BLOCK)
                }
                !allowHit -> {
                    issues += ValidationIssue(
                        code = "NETWORK_HOST_NOT_ALLOWED",
                        message = "Tool '${input.request.toolName}' targets host outside allow-list: $host",
                        severity = ValidationSeverity.WARNING,
                    )
                    disposition = maxAction(disposition, RiskAction.PROMPT_USER)
                }
            }
        }
        return ToolCallAssessment(issues = issues, observations = observations, disposition = disposition)
    }

    private fun hosts(input: ToolCallAssessmentInput): List<String> {
        val declared = input.paramRoles.filterValues { it == ParamRole.NETWORK_TARGET }.keys
        val raws = if (declared.isNotEmpty()) {
            declared.flatMap { extractParamStrings(input.request.parameters[it]) }
        } else {
            input.request.parameters.values.filterIsInstance<String>()
        }
        return raws.mapNotNull { extractHost(it) }
    }

    private fun extractHost(raw: String): String? {
        if (!raw.contains("://")) return null
        return runCatching { URI(raw).host?.lowercase() }.getOrNull()
    }

    private companion object {
        const val RULE_CODE = "NETWORK_HOST"
    }
}
