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.talkie.l3.L3MemoryStore
import com.correx.core.talkie.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:<workspaceRoot>"`. The exact tag is derived
 * from the session's recorded workspace binding so a decision can never cross workspace boundaries.
 * 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,
) {
    /**
     * @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,
        workspaceRoot: 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 == projectMemoryTag(workspaceRoot) }
            .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
        private const val RETRIEVAL_OVERSAMPLE_FACTOR = 4

        fun projectMemoryTag(workspaceRoot: String): String = "project:$workspaceRoot"
    }
}
