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

/**
 * Stale-write gate. Blocks writing a file whose on-disk content has changed since the session read
 * it — the agent's view is stale (a concurrent/external edit). Only files that were *fully* read
 * baseline a hash; files the session has itself written this run are excluded, so the agent's own
 * edits never look stale. Catches the genuine "someone/something changed it under me" case without
 * tripping on normal read→edit flow. The block tells the agent to re-read before writing.
 */
class StaleWriteRule : ToolCallRule {

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

    override fun assess(input: ToolCallAssessmentInput): ToolCallAssessment {
        val readHashes = input.session.readHashes
        if (readHashes.isEmpty()) return ToolCallAssessment()

        val root = input.workspace.workspaceRoot
        val hashByReal = readHashes.entries.associate { realOf(input, root, it.key) to it.value }
        val writtenReal = input.session.writes.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 resolved = resolveInput(root, raw)
            val real = input.probe.resolveReal(resolved)
            val recorded = hashByReal[real]
            if (recorded == null || real in writtenReal) continue // not read in full, or self-edited
            val current = input.probe.contentHash(resolved)
            val stale = current != null && current != recorded
            observations += ToolCallObservation(
                ruleCode = RULE_CODE,
                facts = mapOf("path" to raw, "stale" to stale.toString()),
            )
            if (stale) {
                issues += ValidationIssue(
                    code = RULE_CODE,
                    message = "Tool '${input.request.toolName}' targets '$raw', which changed on disk " +
                        "since you read it — re-read it before writing so you don't clobber the change.",
                    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 = "STALE_WRITE"
    }
}
