package com.correx.core.kernel.orchestration

import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.EntryRole
import com.correx.core.events.events.ConceptPromotedEvent
import com.correx.core.events.events.ContextAssembledEvent
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.types.ContextEntryId
import com.correx.core.events.types.StageId
import com.correx.core.kernel.concept.ConceptCompilerProjection
import com.correx.core.kernel.concept.conceptClassKey
import com.correx.core.transitions.graph.StageConfig
import java.util.UUID

/**
 * Deterministic delivery of promoted capability-accretion concepts into a stage's context
 * (design 2026-07-12-acr-concept-compiler.md §"Delivery" — replaces the L3 top-k lottery as the
 * PRIMARY channel; L3 remains an instance-recall fallback). A [ConceptPromotedEvent] is the
 * authoritative record of a validated failure→fix class; injecting it as an L0/system block tells a
 * stage about a known resolution BEFORE it re-derives the same fix.
 *
 * Relevance filter (avoids the broadcast the design's verdict #1 rejected): only inject a concept a
 * stage could actually act on — a concept whose gate requires a capability ([GATE_REQUIRED_CAPABILITY])
 * the stage does not hold is skipped (no point telling a write-less verifier about a file_write fix).
 * Gates with no required capability (lint/review) are broadly relevant. Bounded to [limit] by
 * occurrences so context stays small. Pure over the promoted set → same input, same injection.
 */
internal fun selectPromotedConcepts(
    promoted: List<ConceptPromotedEvent>,
    allowedTools: Set<String>,
    contradicted: Set<String> = emptySet(),
    limit: Int = MAX_PROMOTED_CONCEPTS,
): List<ConceptPromotedEvent> =
    promoted
        // Honor a contradiction observed AFTER promotion (design 2026-07-12 §"Decay/contradiction"):
        // a concept whose class later failed to hold must not keep being delivered as a known fix.
        .filter { it.classKey.ifBlank { it.fingerprint } !in contradicted }
        .filter { concept -> GATE_REQUIRED_CAPABILITY[concept.gate]?.let { it in allowedTools } ?: true }
        .distinctBy { it.classKey.ifBlank { it.fingerprint } }
        .sortedByDescending { it.occurrences }
        .take(limit)

/** L0/system context entries for the concepts relevant to [stageConfig], read from the whole log. */
internal suspend fun SessionOrchestrator.promotedConceptEntries(stageConfig: StageConfig): List<ContextEntry> {
    val events = eventStore.allEvents().toList()
    val promoted = events.mapNotNull { it.payload as? ConceptPromotedEvent }
    if (promoted.isEmpty()) return emptyList()
    // Fold the same projection the compiler uses to find classes contradicted since promotion.
    val projection = ConceptCompilerProjection()
    val state = events.fold(projection.initial()) { s, e -> projection.apply(s, e) }
    val contradicted = state.clusters.values.filter { it.contradicted }.map { it.classKey }.toSet()
    return selectPromotedConcepts(promoted, stageConfig.effectiveAllowedTools, contradicted).map { concept ->
        // Deliver the FIX, not just a nag (design §"carry the fix"): when the accreted concept carries a
        // validated-fix ref, point the stage at the file that resolved this class before, so it applies a
        // known resolution instead of re-deriving one.
        val fixPointer = concept.fixPath?.let { " A prior validated fix modified: $it — mirror that resolution." }
            ?: ""
        val content = "Known ${concept.gate} failure class (accreted from prior validated runs). " +
            concept.conceptText + fixPointer
        ContextEntry(
            id = ContextEntryId(UUID.randomUUID().toString()),
            layer = ContextLayer.L0,
            content = content,
            sourceType = "promotedConcept",
            sourceId = concept.classKey.ifBlank { concept.fingerprint },
            tokenEstimate = estimateTokens(content),
            // #312: folded from the whole log including THIS run, so a concept promoted or
            // contradicted mid-run changes the set between stages. Mutable ⇒ USER.
            role = EntryRole.USER,
        )
    }
}

/**
 * Store 2 fix-confidence lifecycle (docs/plans/2026-07-21-acr-knowledge-accretion.md §Store 2): a
 * classKey below the hard-promotion threshold isn't silence — [ConceptCompilerProjection] already
 * tracks it as `unconfirmed` (validatedFixes 1..threshold-1) or `falsified` (contradicted). Reactively
 * matched against the CURRENT retry's own classKey (same normalization the compiler uses) and
 * delivered as a soft, one-shot hint (or steer-away) BEFORE the cluster earns hard promotion. Once
 * `classKey` is in [state.promoted][com.correx.core.kernel.concept.ConceptCompilerState.promoted] the
 * hard-promoted delivery ([promotedConceptEntries]) already covers it, so this is skipped to avoid
 * double delivery.
 *
 * Genuinely one-shot per retry occurrence (#306): the hint is keyed to the LATEST
 * [RetryAttemptedEvent] for this stage, and is only injected while that retry hasn't yet been
 * delivered — derived by folding prior [ContextAssembledEvent] manifests (sourceType="unconfirmedFix",
 * sourceId=classKey) recorded AFTER that retry's own position in the log. So a fresh contradicted
 * retry injects the steer-away exactly once (the first context build following it); every later
 * rebuild for the SAME retry occurrence — whether more tool rounds in this attempt or a subsequent
 * stage retry that hasn't reproduced the class again — sees the prior delivery and stays silent. A
 * later, NEW `RetryAttemptedEvent` of the same classKey (the class recurred) advances "latest" past
 * that delivery and earns one fresh injection of its own. No new mutable state: purely a fold over
 * existing events (invariant #9).
 */
internal suspend fun SessionOrchestrator.unconfirmedFixEntries(
    sessionEvents: List<StoredEvent>,
    stageId: StageId,
): List<ContextEntry> {
    val latestRetry = sessionEvents.lastOrNull { (it.payload as? RetryAttemptedEvent)?.stageId == stageId }
    val latest = latestRetry?.payload as? RetryAttemptedEvent
    val sig = latest?.failureReason?.lineSequence()?.firstOrNull()?.take(SIGNATURE_MAX)?.trim().orEmpty()
    val classKey = latest?.let { conceptClassKey(it.gate, sig) }
    val projection = ConceptCompilerProjection()
    val state = eventStore.allEvents().fold(projection.initial(), projection::apply)
    val cluster = classKey?.takeIf { it !in state.promoted }?.let { state.clusters[it] }
    val content = when {
        cluster == null -> null
        cluster.contradicted ->
            "## Steer away from a known dead end\nA prior attempt at this exact failure class " +
                "(${cluster.signature}) was tried before and a later failure showed it did NOT hold. " +
                "Do not repeat that approach — find a materially different fix."
        cluster.validatedFixes >= 1 ->
            "## Unconfirmed prior fix (validated ${cluster.validatedFixes}x, not yet settled)\n" +
                "This failure class (${cluster.signature}) was resolved before" +
                (cluster.fixPath?.let { " in `$it`" } ?: "") + " — worth trying first, but it hasn't " +
                "recurred enough times across sessions to be a certain fix here. Verify it actually applies."
        else -> null
    }
    val deliverable = deliverableUnconfirmedFix(content, classKey, latestRetry, sessionEvents)
    val entry = deliverable?.let { (text, key) ->
        ContextEntry(
            id = ContextEntryId(UUID.randomUUID().toString()),
            layer = ContextLayer.L1,
            content = text,
            sourceType = "unconfirmedFix",
            sourceId = key,
            tokenEstimate = estimateTokens(text),
            role = EntryRole.USER,
        )
    }
    return listOfNotNull(entry)
}

/**
 * (content, classKey) pair to deliver, or null if any of the one-shot preconditions fail: no
 * content derived, no classKey (no retry seen), no retry event to anchor the delivery check
 * against, or the classKey was already delivered for this retry occurrence. Split out of
 * [unconfirmedFixEntries] to keep that function's branching flat.
 */
private fun deliverableUnconfirmedFix(
    content: String?,
    classKey: String?,
    latestRetry: StoredEvent?,
    sessionEvents: List<StoredEvent>,
): Pair<String, String>? = content?.let { text ->
    classKey?.let { key ->
        latestRetry
            ?.takeUnless { unconfirmedFixAlreadyDelivered(sessionEvents, it.sessionSequence, key) }
            ?.let { text to key }
    }
}

/**
 * True when a prior [ContextAssembledEvent] manifest already recorded delivery of the
 * "unconfirmedFix" hint for [classKey] AFTER [afterSequence] (the triggering retry's own
 * position in the session log). Pure fold over recorded events — see #306.
 */
internal fun unconfirmedFixAlreadyDelivered(
    sessionEvents: List<StoredEvent>,
    afterSequence: Long,
    classKey: String,
): Boolean = sessionEvents.any { stored ->
    stored.sessionSequence > afterSequence &&
        (stored.payload as? ContextAssembledEvent)?.entries.orEmpty()
            .any { it.sourceType == "unconfirmedFix" && it.sourceId == classKey }
}

private const val SIGNATURE_MAX = 200
private const val MAX_PROMOTED_CONCEPTS = 3
