package com.correx.core.toolintent

import com.correx.core.events.events.SessionWorkingTaskEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.ToolExecutionCompletedEvent
import com.correx.core.events.events.ToolInvocationRequestedEvent
import com.correx.core.events.types.SessionId
import com.correx.core.sessions.projections.Projection
import com.correx.core.toolintent.rules.candidatePathStrings
import com.correx.core.tools.contract.ToolCapability

/**
 * What a session has done to the workspace this run, plus the task it is working — the single
 * read-model every anti-hallucination gate consumes (plane-2 rules via the threaded
 * [ToolCallAssessmentInput.session]; tool gates via a provider port). [pendingReads] is
 * fold-internal (reads awaiting their completion event) and not meant for consumers.
 */
data class SessionContext(
    val reads: Set<String> = emptySet(),
    val writes: Set<String> = emptySet(),
    val activeTask: ActiveTask? = null,
    // Path -> content hash captured when the file was last fully read this session; the stale-write
    // gate compares it against the file's current hash. Only whole-file reads contribute.
    val readHashes: Map<String, String> = emptyMap(),
    val pendingReads: Map<String, List<String>> = emptyMap(),
)

/** The task this session has claimed and is working, with its declared write scope. */
data class ActiveTask(val taskId: String, val scope: List<String>)

/**
 * Folds one session's tool events into a [SessionContext]:
 * - **reads** — completed invocations whose recorded capability includes FILE_READ (path from the
 *   request args; a read that failed never completes, so it never counts).
 * - **writes** — the resolved paths the orchestrator already records on each completion's
 *   `receipt.affectedEntities` (via FileAffectingTool), so writes need no param-parsing here.
 * - **activeTask** — set when the session claims a task; null until a SessionWorkingTaskEvent lands.
 */
class SessionContextProjection(private val sessionId: SessionId) : Projection<SessionContext> {

    override fun initial(): SessionContext = SessionContext()

    override fun apply(state: SessionContext, event: StoredEvent): SessionContext =
        when (val payload = event.payload) {
            is ToolInvocationRequestedEvent -> recordPendingRead(state, payload)
            is ToolExecutionCompletedEvent -> recordCompletion(state, payload)
            is SessionWorkingTaskEvent ->
                if (payload.sessionId == sessionId) {
                    state.copy(activeTask = ActiveTask(payload.taskId.value, payload.affectedPaths))
                } else {
                    state
                }
            else -> state
        }

    private fun recordPendingRead(state: SessionContext, payload: ToolInvocationRequestedEvent): SessionContext {
        if (payload.sessionId != sessionId || ToolCapability.FILE_READ !in payload.capabilities) return state
        val paths = candidatePathStrings(emptyMap(), payload.request.parameters)
        if (paths.isEmpty()) return state
        return state.copy(pendingReads = state.pendingReads + (payload.invocationId.value to paths))
    }

    private fun recordCompletion(state: SessionContext, payload: ToolExecutionCompletedEvent): SessionContext {
        if (payload.sessionId != sessionId) return state
        val justRead = state.pendingReads[payload.invocationId.value]
        val hash = payload.receipt.structuredOutput["contentHash"] as? String
        val newHashes = if (justRead != null && hash != null) {
            state.readHashes + justRead.associateWith { hash }
        } else {
            state.readHashes
        }
        return state.copy(
            reads = if (justRead != null) state.reads + justRead else state.reads,
            writes = state.writes + payload.receipt.affectedEntities,
            readHashes = newHashes,
            pendingReads = state.pendingReads - payload.invocationId.value,
        )
    }
}
