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

/**
 * Reference-must-exist gate (anti-hallucination). Dispatches on FILE_READ: a read of a path that is
 * inside the workspace but does not exist is BLOCKED with "no such path", so an agent that
 * hallucinated a filename fails loud and self-corrects (list the directory, fix the name) instead of
 * proceeding on an empty/absent read. Out-of-workspace targets are [PathContainmentRule]'s concern
 * (it prompts), so this gate stays silent on them to avoid double-handling.
 *
 * Directory-enumeration tools ([ToolCapability.DIRECTORY_LIST]) are exempt: listing a not-yet-created
 * directory is a legitimate survey move (greenfield scaffolding, e.g. "does frontend/ exist yet?")
 * and the tool itself truthfully reports absent/empty. Hard-blocking it only makes weak models thrash
 * on a rejection loop, whereas a hallucinated file *read* still fails loud.
 */
class ReferenceExistsRule : ToolCallRule {

    override fun appliesTo(capabilities: Set<ToolCapability>): Boolean =
        ToolCapability.FILE_READ in capabilities && ToolCapability.DIRECTORY_LIST !in capabilities

    override fun assess(input: ToolCallAssessmentInput): ToolCallAssessment {
        val workspaceReal = input.probe.resolveReal(input.workspace.workspaceRoot)
        val root = input.workspace.workspaceRoot

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

        for (raw in candidatePathStrings(input.paramRoles, input.request.parameters)) {
            val candidate = Path.of(raw)
            val resolvedInput = if (candidate.isAbsolute) candidate else root.resolve(candidate)
            val exists = input.probe.exists(resolvedInput)
            val inWorkspace = input.probe.resolveReal(resolvedInput).startsWith(workspaceReal)

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

            if (inWorkspace && !exists) {
                issues += ValidationIssue(
                    code = RULE_CODE,
                    message = "Tool '${input.request.toolName}' references '$raw', which does not exist. " +
                        "Do NOT keep retrying the read. If you intend to create this file, call file_write " +
                        "directly — you cannot read a file that does not exist yet. Otherwise list the " +
                        "directory or fix the path; do not assume its contents.",
                    severity = ValidationSeverity.ERROR,
                )
                disposition = maxAction(disposition, RiskAction.BLOCK)
            }
        }

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

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