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.ToolCapability
import com.correx.core.validation.model.ValidationIssue
import com.correx.core.validation.model.ValidationSeverity
import java.nio.file.Path

/**
 * Read-before-write gate (anti-hallucination). Dispatches on FILE_WRITE, covering both file_write
 * and file_edit. A write to a path that exists on disk but was not file_read earlier this session
 * is BLOCKED — the classic "edited a file whose contents it never saw" failure. A brand-new file
 * (not on disk) passes: you cannot read what does not exist, and creating one is legitimate.
 *
 * The block message names the path; the orchestrator feeds the rationale back as a tool result, so
 * the agent reads the file and retries. Read paths and the write target are both resolved through
 * the probe (toRealPath), so `./Foo.kt` read then `Foo.kt` written match, and symlinks can't dodge it.
 */
class ReadBeforeWriteRule : ToolCallRule {

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

    override fun assess(input: ToolCallAssessmentInput): ToolCallAssessment {
        val root = input.workspace.workspaceRoot
        val readReal = input.session.reads.map { realOf(input, root, it) }.toSet()

        val issues = mutableListOf<ValidationIssue>()
        val observations = mutableListOf<ToolCallObservation>()
        var disposition = RiskAction.PROCEED

        for (raw in candidatePathStrings(input.paramRoles, input.request.parameters)) {
            val resolvedInput = resolveInput(root, raw)
            val exists = input.probe.exists(resolvedInput)
            val read = input.probe.resolveReal(resolvedInput) in readReal

            observations += ToolCallObservation(
                ruleCode = RULE_CODE,
                facts = mapOf("path" to raw, "exists" to exists.toString(), "read" to read.toString()),
            )

            if (exists && !read) {
                issues += ValidationIssue(
                    code = RULE_CODE,
                    message = "Tool '${input.request.toolName}' targets '$raw', which you have not " +
                        "read this session — call file_read on it before writing or editing.",
                    severity = ValidationSeverity.ERROR,
                )
                disposition = maxAction(disposition, RiskAction.BLOCK)
            }
        }

        return ToolCallAssessment(issues = issues, observations = observations, disposition = disposition)
    }

    private fun resolveInput(root: Path, raw: String): Path {
        val candidate = Path.of(raw)
        return if (candidate.isAbsolute) candidate else root.resolve(candidate)
    }

    private fun realOf(input: ToolCallAssessmentInput, root: Path, raw: String): Path =
        input.probe.resolveReal(resolveInput(root, raw))

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