package com.correx.core.kernel.orchestration

import com.correx.core.events.events.BriefEcho

/**
 * The divergence between an original brief and a planner's echo of it.
 *
 * - [droppedFiles] / [droppedSymbols] / [droppedAcceptanceCriteria]: present in the original brief,
 *   missing from the echo — the planner failed to carry them forward.
 * - [inventedFiles]: file paths in the echo not in the original — hallucinated paths.
 *
 * [hasMismatch] is conservative: see [BriefEchoComparator.compare].
 */
internal data class BriefEchoDiff(
    val droppedFiles: Set<String>,
    val droppedSymbols: Set<String>,
    val droppedAcceptanceCriteria: Set<String>,
    val inventedFiles: Set<String>,
) {
    /**
     * A mismatch that should halt the stage. Only dropping a *required acceptance criterion* or
     * *inventing a file path* qualifies: those are correctness-bearing. Merely dropping a file or
     * symbol the plan may legitimately not echo is tolerated and does NOT halt.
     */
    val hasMismatch: Boolean
        get() = droppedAcceptanceCriteria.isNotEmpty() || inventedFiles.isNotEmpty()
}

/**
 * Diffs a planner's [BriefEcho] against the original brief's. Pure and deterministic.
 *
 * Diff semantics:
 * - `dropped* = original.X - echo.X` (the planner failed to carry something forward).
 * - `inventedFiles = echo.referencedFiles - original.referencedFiles` (hallucinated paths).
 *
 * Comparison is case-insensitive and trimmed for acceptance criteria and symbols (free-text the
 * model may rephrase in casing/whitespace), and exact for file paths (a path is a path).
 */
internal object BriefEchoComparator {

    fun compare(original: BriefEcho, echo: BriefEcho): BriefEchoDiff {
        val droppedFiles = original.referencedFiles - echo.referencedFiles
        val inventedFiles = echo.referencedFiles - original.referencedFiles
        return BriefEchoDiff(
            droppedFiles = droppedFiles,
            droppedSymbols = normalizedDrop(original.symbols, echo.symbols),
            droppedAcceptanceCriteria = normalizedDrop(original.acceptanceCriteria, echo.acceptanceCriteria),
            inventedFiles = inventedFiles,
        )
    }

    /** Original entries whose normalized form is absent from the echo, returned in original form. */
    private fun normalizedDrop(original: Set<String>, echo: Set<String>): Set<String> {
        val echoKeys = echo.mapTo(HashSet()) { it.normalize() }
        return original.filterTo(LinkedHashSet()) { it.normalize() !in echoKeys }
    }

    private fun String.normalize(): String = trim().lowercase()
}
