package com.correx.core.kernel.orchestration

import com.correx.core.context.model.ContextPack
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.ContextAssembledEvent
import com.correx.core.events.events.ContextManifestEntry
import com.correx.core.events.events.ContextTruncatedEvent
import com.correx.core.events.events.InitialIntentEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.transitions.graph.StageConfig
import kotlinx.datetime.Clock
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.atomic.*

internal fun SessionOrchestrator.error(
    stageId: StageId,
    path: String,
    throwable: Throwable,
): String = "[SessionOrchestrator] stage=${stageId.value}: " +
        "failed to load prompt '$path': ${throwable.message}"

/**
 * Strip an outer markdown code fence (```` ```json `` / ``` ``` ````) when the whole response
 * is a single fenced block. Conservative: only acts when the trimmed text both starts and ends
 * with a fence, so prose responses that merely contain an inline block are left untouched.
 */

internal fun SessionOrchestrator.resolveStagePromptText(stageId: StageId, stageConfig: StageConfig): String {
    stageConfig.metadata["promptInline"]?.trim()?.takeIf { it.isNotBlank() }?.let { return it }
    stageConfig.metadata["prompt"]?.let { path ->
        runCatching { promptResolver.resolve(path) }.getOrNull()
            ?.trim()?.takeIf { it.isNotBlank() }
            ?.let { return it }
    }
    return stageId.value
}

/**
 * The role's own prompt text (see [resolveStagePromptText]) is identical across every session
 * that runs the stage — it carries zero session-specific signal, so similarity against it can
 * only ever reflect generic lexical overlap with the role's boilerplate ("file_read", "ls",
 * "grep"...), never what the user actually asked for. The one thing that IS session-specific
 * is the initial user intent, so it's prepended here as the real anchor for the query (2026-07-08
 * finding: without it, the query matched a tool script's own symbol names over the files the
 * user's request actually named).
 */

internal fun SessionOrchestrator.repoKnowledgeQuery(sessionId: SessionId, stageId: StageId, stageConfig: StageConfig): String {
    val intent = eventStore.read(sessionId)
        .mapNotNull { it.payload as? InitialIntentEvent }
        .firstOrNull()?.intent?.trim()
    val stagePrompt = resolveStagePromptText(stageId, stageConfig)
    return if (intent.isNullOrBlank()) stagePrompt else "$intent\n\n$stagePrompt"
}

// --- event emission ---

// Surface context-budget overflow as an event on the workflow path. The builder already
// enforces the budget (drops oldest/compressible entries, pinning steering + event history);
// this records that it happened so replay and operators can see it, mirroring the router path.
internal suspend fun SessionOrchestrator.emitContextTruncationIfNeeded(
    sessionId: SessionId,
    stageId: StageId,
    contextPack: ContextPack,
) {
    val meta = contextPack.compressionMetadata
    if (meta.entriesDropped <= 0) return
    emit(
        sessionId,
        ContextTruncatedEvent(
            sessionId = sessionId,
            turnId = stageId.value,
            entriesDropped = meta.entriesDropped,
            truncatedLayers = meta.truncatedLayers.map { it.name },
            timestampMs = Clock.System.now().toEpochMilliseconds(),
        ),
    )
}

// #307: record the manifest of what got injected into the stage's initial context build — NOT
// the content (that's derivable/replayable, invariant #9, and already in CAS via the prompt
// artifact) — so the injected set is auditable from the event log alone.
internal suspend fun SessionOrchestrator.emitContextAssembled(
    sessionId: SessionId,
    stageId: StageId,
    contextPack: ContextPack,
) {
    val entries = contextPack.layers.values.flatten().map { entry ->
        ContextManifestEntry(
            sourceType = entry.sourceType,
            sourceId = entry.sourceId,
            tokenEstimate = entry.tokenEstimate,
            layer = entry.layer.name,
            role = entry.role.name,
        )
    }
    emit(
        sessionId,
        ContextAssembledEvent(
            sessionId = sessionId,
            stageId = stageId,
            contextPackId = contextPack.id.value,
            entries = entries,
            timestampMs = Clock.System.now().toEpochMilliseconds(),
        ),
    )
}

internal fun SessionOrchestrator.fallbackTokenEstimate(content: String): Int {
    return (content.length / 4).coerceAtLeast(1)
}

// --- stage-entry approval gate ---

/**
 * Emits [OrchestrationPausedEvent] + [ApprovalRequestedEvent] for a stage flagged
 * `requiresApproval`, awaits the user decision, and records it.
 * Returns `true` if the run should proceed into the stage, `false` if the approval was rejected.
 */
