package com.correx.core.kernel.orchestration

import com.correx.core.events.events.CritiqueFinding
import com.correx.core.events.events.CritiqueFindingsRecordedEvent
import com.correx.core.events.events.CritiqueOutcome
import com.correx.core.events.events.CritiqueOutcomeCorrelatedEvent
import com.correx.core.events.events.CritiqueVerdict
import com.correx.core.events.types.SessionId

/**
 * Decides how each prior critique finding turned out across a review loop — the producer half of
 * critique calibration (BACKLOG §B-§6 / pipeline-addenda A3). Pure over the recorded review
 * iterations, so it is replay-deterministic and unit-testable; the orchestrator runs it once at
 * loop resolution (see completeWorkflow / failWorkflow) and emits the results, which the
 * `:core:critique` projection folds into per-(model, role) precision.
 *
 * Rules, per critic (grouped by role + modelHash) and per finding id:
 *  - **UPHELD** — the finding was raised in some iteration and is gone by the final review: the
 *    implementer fixed it between rounds, so the critic was right and the finding was actioned.
 *  - **DISMISSED** — the finding persists into the final review AND that review APPROVED: the
 *    reviewer signed off without it being addressed, i.e. treated it as non-blocking.
 *  - **INCONCLUSIVE** — the finding is still open at a non-approved terminal (loop exhausted): no
 *    signal either way.
 *
 * Findings carry a stable [CritiqueFinding.id] so the same logical finding is tracked across
 * iterations.
 */
object CritiqueOutcomeCorrelator {

    fun correlate(
        sessionId: SessionId,
        reviews: List<CritiqueFindingsRecordedEvent>,
    ): List<CritiqueOutcomeCorrelatedEvent> =
        reviews
            .groupBy { it.role to it.modelHash }
            .flatMap { (_, group) -> correlateCritic(sessionId, group) }

    private fun correlateCritic(
        sessionId: SessionId,
        critic: List<CritiqueFindingsRecordedEvent>,
    ): List<CritiqueOutcomeCorrelatedEvent> {
        val ordered = critic.sortedBy { it.iteration }
        val finalReview = ordered.last()
        val finalIds = finalReview.findings.mapTo(mutableSetOf()) { it.id }

        // Latest appearance per finding id (later iterations overwrite), used for severity and the
        // owning critic's role/modelHash.
        val latest = LinkedHashMap<String, Pair<CritiqueFindingsRecordedEvent, CritiqueFinding>>()
        for (review in ordered) {
            for (finding in review.findings) {
                latest[finding.id] = review to finding
            }
        }

        return latest.map { (id, appearance) ->
            val (review, finding) = appearance
            val outcome = when {
                id !in finalIds -> CritiqueOutcome.UPHELD
                finalReview.verdict == CritiqueVerdict.APPROVED -> CritiqueOutcome.DISMISSED
                else -> CritiqueOutcome.INCONCLUSIVE
            }
            CritiqueOutcomeCorrelatedEvent(
                sessionId = sessionId,
                findingId = id,
                role = review.role,
                modelHash = review.modelHash,
                severity = finding.severity,
                outcome = outcome,
            )
        }
    }
}
