package com.correx.apps.server.memory

import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.RepoKnowledgeHit
import com.correx.core.events.events.RepoKnowledgeHitsFilteredEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.inference.Embedder
import com.correx.core.kernel.orchestration.RepoKnowledgeRetriever
import com.correx.core.talkie.l3.L3MemoryStore
import com.correx.core.talkie.l3.L3Query
import kotlinx.coroutines.CancellationException
import kotlinx.datetime.Clock
import org.slf4j.LoggerFactory
import java.util.UUID

private val log = LoggerFactory.getLogger(L3RepoKnowledgeRetriever::class.java)

private const val RETRIEVAL_OVERSAMPLE_FACTOR = 4

// Real embedder scores for genuinely relevant repo-map hits cluster 0.68-0.80 (2026-07-03
// live grounding); below this floor a hit is just the nearest thing in a small corpus, not
// actually related to the query — rendering it as "relevant" is the same poisoning failure
// mode as the markdown-in-L3 bug, just via low-signal cosine similarity instead of topic drift.
//
// ponytail: 0.5 was too low. Docs embed a terse *symbol-list* descriptor ("path: module X;
// symbols: a,b") while queries embed *prose* intent — an asymmetric prose↔symbols comparison
// that collapses ALL scores into a ~0.5 noise band (2026-07-21 session 459: top junk hits at
// 0.54/0.53, real relevance never reached). At 0.5 those noise-winners cleared the bar and
// SUPPRESSED the deterministic repo-map floor (repoEntriesOrMapFloor). 0.6 sits above the
// observed noise ceiling (~0.55) and below the real-signal floor (0.68): noise → empty →
// fall back to the repo map. Calibration knob — retune if the embedder model changes.
private const val MIN_SIMILARITY_SCORE = 0.6f

class L3RepoKnowledgeRetriever(
    private val embedder: Embedder,
    private val l3MemoryStore: L3MemoryStore,
    private val repoRoot: String,
    private val eventStore: EventStore? = null,
) : RepoKnowledgeRetriever {
    override suspend fun retrieve(sessionId: SessionId, query: String, k: Int): List<RepoKnowledgeHit> {
        val vector = embedder.embed(query)
        val candidates = l3MemoryStore.query(L3Query(vector = vector, k = k * RETRIEVAL_OVERSAMPLE_FACTOR))
            // Versioned trailing delimiter keeps sibling repo roots isolated and stale semantic
            // documents out after a repo-map descriptor format change.
            .filter { it.entry.turnId.startsWith(repoMapEmbeddingPrefix(repoRoot)) }
        val (kept, dropped) = candidates.partition { it.score >= MIN_SIMILARITY_SCORE }
        if (dropped.isNotEmpty()) recordDropped(sessionId, query, dropped.map { it.toHit() })
        return kept.take(k).map { it.toHit() }
    }

    private suspend fun recordDropped(sessionId: SessionId, query: String, dropped: List<RepoKnowledgeHit>) {
        val store = eventStore ?: return
        runCatching {
            store.append(
                NewEvent(
                    metadata = EventMetadata(
                        eventId = EventId(UUID.randomUUID().toString()),
                        sessionId = sessionId,
                        timestamp = Clock.System.now(),
                        schemaVersion = 1,
                        causationId = null,
                        correlationId = null,
                    ),
                    payload = RepoKnowledgeHitsFilteredEvent(
                        sessionId = sessionId,
                        query = query,
                        threshold = MIN_SIMILARITY_SCORE,
                        dropped = dropped,
                    ),
                ),
            )
        }.exceptionOrNull()?.let { e ->
            if (e is CancellationException) throw e
            log.warn("failed to record filtered repo-knowledge hits for {}: {}", sessionId.value, e.message)
        }
    }

    private fun com.correx.core.talkie.l3.L3Hit.toHit() =
        RepoKnowledgeHit(path = entry.text.substringBefore(":"), text = entry.text, score = score)
}
