package com.correx.apps.server.memory

import com.correx.core.config.ProjectConfig
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.RepoMapComputedEvent
import com.correx.core.events.events.SteeringNoteAddedEvent
import com.correx.core.events.events.WorkspaceStateObservedEvent
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.journal.DefaultDecisionJournalRepository
import com.correx.core.journal.ProjectMemoryDistiller
import com.correx.core.router.l3.L3MemoryEntry
import com.correx.core.router.l3.L3MemoryStore
import com.correx.core.router.l3.L3Query
import kotlinx.coroutines.CancellationException
import kotlinx.datetime.Clock
import org.slf4j.LoggerFactory
import java.util.UUID

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

// Over-fetch factor: project lines share the L3 store with chat turns, so query wider
// than memoryK and post-filter by the repo-scoped turnId tag.
private const val OVERFETCH = 4

/**
 * Cross-session, repo-scoped memory. Project lines are tagged in the L3 store via
 * `turnId = "project:<repoRoot>"` (no schema change). On session end the decision journal
 * is distilled and persisted; on session start prior-session project lines are retrieved and
 * seeded into the new session as a recorded steering note (invariant #9: the L3 read is
 * recorded as an event, never re-observed at replay), so the decision-journal pinning surfaces
 * them to every stage. All operations are gated on [ProjectConfig.enabled].
 */
class ProjectMemoryService(
    private val config: ProjectConfig,
    private val embedder: Embedder,
    private val l3MemoryStore: L3MemoryStore,
    private val journalRepository: DefaultDecisionJournalRepository,
    private val eventStore: EventStore,
    private val distiller: ProjectMemoryDistiller = ProjectMemoryDistiller(),
    private val indexer: RepoMapIndexerPort = RepoMapIndexer(),
    private val workspaceStateProbe: WorkspaceStateProbe = RealWorkspaceStateProbe(),
) {
    private fun tag(repoRoot: String) = "project:$repoRoot"

    /** Resolved repo-root key: configured [ProjectConfig.root], else the working dir. */
    fun repoRoot(): String = config.root.ifBlank { System.getProperty("user.dir") ?: "." }

    /**
     * Walk [repoRoot] and record the ranked file/symbol map as a [RepoMapComputedEvent] once
     * per session. The full map is recorded (the log); only a top-K slice is injected into
     * context downstream. Replay reads the recorded event, never re-scanning (invariant #9).
     */
    suspend fun indexAndRecord(sessionId: SessionId, repoRoot: String) {
        if (!config.enabled) return
        val stateKey = eventStore.read(sessionId)
            .mapNotNull { it.payload as? WorkspaceStateObservedEvent }
            .lastOrNull()?.stateKey
        if (stateKey != null && l3MemoryStore.existsByTurnIdPrefix("repomap:$repoRoot:$stateKey")) {
            log.debug("repo-map already indexed for {}@{} — skipping", repoRoot, stateKey)
            return
        }
        runCatching {
            val entries = indexer.index(java.nio.file.Path.of(repoRoot))
            if (entries.isEmpty()) return
            eventStore.append(
                NewEvent(
                    metadata = EventMetadata(
                        eventId = EventId(UUID.randomUUID().toString()),
                        sessionId = sessionId,
                        timestamp = Clock.System.now(),
                        schemaVersion = 1,
                        causationId = null,
                        correlationId = null,
                    ),
                    payload = RepoMapComputedEvent(
                        sessionId = sessionId,
                        repoRoot = repoRoot,
                        entries = entries,
                        stateKey = stateKey,
                        computedAt = Clock.System.now(),
                    ),
                ),
            )
            val tag = if (stateKey != null) "repomap:$repoRoot:$stateKey" else "repomap:$repoRoot:"
            entries.forEach { entry ->
                runCatching {
                    val text = entry.path + if (entry.symbols.isEmpty()) "" else ": ${entry.symbols.joinToString(", ")}"
                    l3MemoryStore.store(
                        L3MemoryEntry(
                            id = UUID.randomUUID().toString(),
                            sessionId = sessionId,
                            turnId = tag,
                            text = text,
                            vector = embedder.embed(text),
                            timestampMs = Clock.System.now().toEpochMilliseconds(),
                        ),
                    )
                }.exceptionOrNull()?.let { e ->
                    if (e is CancellationException) throw e
                    log.warn("repo-map embedding failed for {}: {}", entry.path, e.message)
                }
            }
        }.exceptionOrNull()?.let { e ->
            if (e is CancellationException) throw e
            log.warn("repo-map indexing failed for {}: {}", repoRoot, e.message)
        }
    }

    /**
     * Observe the workspace content state and record it as a [WorkspaceStateObservedEvent]
     * (invariant #9: environment observations are recorded as events, never re-observed).
     */
    suspend fun observeAndRecord(sessionId: SessionId, repoRoot: String) {
        if (!config.enabled) return
        val state = workspaceStateProbe.observe(repoRoot) ?: run {
            log.debug("workspace state unavailable for {} — skipping observation", repoRoot)
            return
        }
        runCatching {
            eventStore.append(
                NewEvent(
                    metadata = EventMetadata(
                        eventId = EventId(UUID.randomUUID().toString()),
                        sessionId = sessionId,
                        timestamp = Clock.System.now(),
                        schemaVersion = 1,
                        causationId = null,
                        correlationId = null,
                    ),
                    payload = WorkspaceStateObservedEvent(
                        sessionId = sessionId,
                        workspaceRoot = repoRoot,
                        stateKey = state.stateKey,
                        source = state.source,
                        branch = state.branch,
                        dirty = state.dirty,
                    ),
                ),
            )
        }.exceptionOrNull()?.let { e ->
            if (e is CancellationException) throw e
            log.warn("workspace state observation failed for {}: {}", repoRoot, e.message)
        }
    }

    /** Distil the session's decision journal into durable, repo-tagged L3 entries. */
    suspend fun persist(sessionId: SessionId, repoRoot: String) {
        if (!config.enabled) return
        val lines = distiller.distill(journalRepository.getJournal(sessionId), repoRoot)
        lines.forEach { line ->
            runCatching {
                val vector = embedder.embed(line)
                l3MemoryStore.store(
                    L3MemoryEntry(
                        id = UUID.randomUUID().toString(),
                        sessionId = sessionId,
                        turnId = tag(repoRoot),
                        text = line,
                        vector = vector,
                        timestampMs = Clock.System.now().toEpochMilliseconds(),
                    ),
                )
            }.exceptionOrNull()?.let { e ->
                if (e is CancellationException) throw e
                log.warn("project memory persist failed for {}: {}", repoRoot, e.message)
            }
        }
    }

    /**
     * Retrieve prior-session project lines for [repoRoot] and seed them into [sessionId] as a
     * steering note. Returns the lines seeded (empty if disabled or none found).
     */
    suspend fun retrieveAndSeed(sessionId: SessionId, repoRoot: String): List<String> {
        if (!config.enabled) return emptyList()
        val lines = runCatching {
            val vector = embedder.embed(repoRoot)
            l3MemoryStore.query(L3Query(vector = vector, k = config.memoryK * OVERFETCH))
                .map { it.entry }
                .filter { it.turnId == tag(repoRoot) && it.sessionId != sessionId }
                .map { it.text }
                .distinct()
                .take(config.memoryK)
        }.getOrElse { e ->
            if (e is CancellationException) throw e
            log.warn("project memory retrieval failed for {}: {}", repoRoot, e.message)
            emptyList()
        }
        if (lines.isEmpty()) return emptyList()

        eventStore.append(
            NewEvent(
                metadata = EventMetadata(
                    eventId = EventId(UUID.randomUUID().toString()),
                    sessionId = sessionId,
                    timestamp = Clock.System.now(),
                    schemaVersion = 1,
                    causationId = null,
                    correlationId = null,
                ),
                payload = SteeringNoteAddedEvent(
                    sessionId = sessionId,
                    content = "Project memory (prior sessions):\n" + lines.joinToString("\n"),
                ),
            ),
        )
        return lines
    }
}
