package com.correx.apps.server.memory

import com.correx.core.events.events.PossibleContradictionFlaggedEvent
import com.correx.core.events.events.RelatedDecision
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.inference.Embedder
import com.correx.core.router.l3.L3MemoryStore
import com.correx.core.router.l3.L3Query

/**
 * Display-only architect contradiction surfacing (BACKLOG §B-§4). When the architect records a
 * decision, this embeds the decision text, retrieves semantically-near PRIOR decisions/ADRs from
 * L3, and — if any clear the similarity threshold — returns a [PossibleContradictionFlaggedEvent]
 * for the operator to eyeball. v1 is purely informational: there is no LLM judge and the caller
 * emits the flag without ever halting or failing the stage.
 *
 * Namespace convention: distilled decision-journal lines are persisted into L3 by
 * [ProjectMemoryService] under `turnId = "project:<repoRoot>"` (trailing-`:` delimiter). This is
 * the only decision-bearing L3 namespace that exists today, so [decisionNamespacePrefix] defaults
 * to `"project:"` — a `startsWith` prefix, matching the trailing-`:` delimiter convention used by
 * [L3RepoKnowledgeRetriever]'s `"repomap:<repoRoot>:"` filter. Hits are also constrained to PRIOR
 * sessions (`entry.sessionId != sessionId`) so the architect never flags its own in-flight run.
 */
class ArchitectContradictionChecker(
    private val embedder: Embedder,
    private val l3MemoryStore: L3MemoryStore,
    private val k: Int = DEFAULT_K,
    private val scoreThreshold: Double = DEFAULT_SCORE_THRESHOLD,
    private val decisionNamespacePrefix: String = DEFAULT_DECISION_NAMESPACE_PREFIX,
) {
    /**
     * @return a [PossibleContradictionFlaggedEvent] listing related prior decisions, or null when
     * none clear the threshold. The CALLER emits it (display-only) — this never halts.
     */
    suspend fun check(
        sessionId: SessionId,
        stageId: StageId,
        decisionText: String,
    ): PossibleContradictionFlaggedEvent? {
        if (decisionText.isBlank()) return null
        val vector = embedder.embed(decisionText)
        val related = l3MemoryStore.query(L3Query(vector = vector, k = k * RETRIEVAL_OVERSAMPLE_FACTOR))
            .filter { it.entry.turnId.startsWith(decisionNamespacePrefix) }
            .filter { it.entry.sessionId != sessionId }
            .filter { it.score >= scoreThreshold }
            .take(k)
            .map {
                RelatedDecision(
                    summary = it.entry.text,
                    score = it.score.toDouble(),
                    source = it.entry.turnId,
                )
            }
        if (related.isEmpty()) return null
        return PossibleContradictionFlaggedEvent(
            sessionId = sessionId,
            stageId = stageId,
            decisionSummary = decisionText,
            related = related,
        )
    }

    companion object {
        const val DEFAULT_K = 5
        const val DEFAULT_SCORE_THRESHOLD = 0.75
        const val DEFAULT_DECISION_NAMESPACE_PREFIX = "project:"
        private const val RETRIEVAL_OVERSAMPLE_FACTOR = 4
    }
}
