package com.correx.core.kernel.orchestration

import com.correx.core.artifacts.ArtifactState
import com.correx.core.events.events.CritiqueFindingsRecordedEvent
import com.correx.core.events.events.CritiqueOutcomeCorrelatedEvent
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.TransitionExecutedEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.kernel.execution.WorkflowResult
import com.correx.core.transitions.evaluation.EvaluationContext
import com.correx.core.transitions.graph.WorkflowGraph
import com.correx.core.transitions.resolution.TransitionDecision
import kotlinx.datetime.Clock
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.atomic.*

// A provider 4xx client error (e.g. llama.cpp 400 "invalid_request") is a deterministic
// request-construction failure — retrying re-sends the identical bad request and exhausts the
// budget for nothing (F-002). Treat 4xx as terminal, except 408 (timeout) and 429 (rate-limit)
// which are transient. Everything else (network/5xx/unknown) stays retryable.
internal fun SessionOrchestrator.isRetryableFailure(reason: String): Boolean {
    val status = Regex("""returned\s+(\d{3})""").find(reason)?.groupValues?.get(1)?.toIntOrNull()
        ?: return true
    return status !in 400..499 || status == HTTP_TIMEOUT || status == HTTP_TOO_MANY_REQUESTS
}

// --- transition helpers ---

internal fun SessionOrchestrator.resolveTransition(
    graph: WorkflowGraph,
    sessionId: SessionId,
    currentStageId: StageId,
    artifacts: Map<ArtifactId, ArtifactState> = emptyMap(),
    artifactContent: Map<ArtifactId, String> = emptyMap(),
): TransitionDecision {
    val ctx = EvaluationContext(
        sessionId = sessionId,
        currentStage = currentStageId,
        artifacts = artifacts,
        artifactContent = artifactContent,
        readyTaskCount = readyTaskCounter?.count(sessionId) ?: 0,
    )
    return transitionResolver.resolve(graph, ctx)
}

internal fun SessionOrchestrator.isTerminal(graph: WorkflowGraph, stageId: StageId): Boolean =
    graph.transitions.none { it.from == stageId }

internal suspend fun SessionOrchestrator.advanceStage(
    sessionId: SessionId,
    fromStageId: StageId,
    decision: TransitionDecision.Move,
): StageId {
    emit(
        sessionId,
        TransitionExecutedEvent(sessionId, fromStageId, decision.to, decision.transitionId, decision.redirectId),
    )
    return decision.to
}

// --- terminal state helpers ---

internal suspend fun SessionOrchestrator.emitWorkflowStarted(
    sessionId: SessionId,
    graph: WorkflowGraph,
    config: OrchestrationConfig,
    startStage: StageId = graph.start,
) {
    emit(
        sessionId,
        WorkflowStartedEvent(
            sessionId = sessionId,
            workflowId = graph.id,
            startStageId = startStage,
            retryPolicy = config.retryPolicy,
        ),
    )
}

internal suspend fun SessionOrchestrator.completeWorkflow(
    sessionId: SessionId,
    terminalStageId: StageId,
    stageCount: Int,
    workflowId: String,
): WorkflowResult.Completed {
    log.debug(
        "[Orchestrator] COMPLETED session={} terminalStage={} stages={}",
        sessionId.value, terminalStageId.value, stageCount,
    )
    correlateCritiqueOutcomes(sessionId)
    emit(sessionId, WorkflowCompletedEvent(sessionId, terminalStageId, stageCount, workflowId))
    cancellations.remove(sessionId)
    evictArtifactContentCache(sessionId)
    return WorkflowResult.Completed(sessionId, terminalStageId)
}

internal suspend fun SessionOrchestrator.failWorkflow(
    sessionId: SessionId,
    stageId: StageId,
    reason: String,
    retryExhausted: Boolean,
): WorkflowResult.Failed {
    log.warn(
        "[Orchestrator] FAILED session={} stage={} reason={} retryExhausted={}",
        sessionId.value, stageId.value, reason, retryExhausted,
    )
    // failWorkflow IS the clean-termination path: whatever goes wrong while recording it
    // (event-store hiccup, correlation lookup, or any other transient failure) must never be
    // allowed to escape as a raw exception — that would blow past this function's own
    // WorkflowFailedEvent (correct stageId/reason/retryExhausted) and land in the server's
    // top-level catch-all, which fabricates its own event with the wrong stage (graph.start)
    // and a garbage reason (the escaping throwable's message/toString). Record what we can and
    // still return a clean Failed result either way.
    // Critique correlation is best-effort bookkeeping; if it throws we log and press on, because
    // the terminal WorkflowFailedEvent below MUST still be recorded — skipping it would leave the
    // event log with no terminal marker for the session (breaks replay-completeness, invariants
    // #1/#8) and let the server catch-all fabricate a wrong one.
    runCatching { correlateCritiqueOutcomes(sessionId) }.onFailure { e ->
        log.error(
            "[Orchestrator] failWorkflow: critique correlation failed session={} stage={}",
            sessionId.value, stageId.value, e,
        )
    }
    // Last-ditch: the one unrecoverable case. If emitting the terminal event itself throws
    // (e.g. a serialization edge), we cannot record the failure at all — log it loudly with the
    // full throwable so it is never silent, then still return a clean Failed result.
    runCatching {
        emit(sessionId, WorkflowFailedEvent(sessionId, stageId, reason, retryExhausted))
    }.onFailure { e ->
        log.error(
            "[Orchestrator] failWorkflow: FAILED to record terminal WorkflowFailedEvent — event " +
                "log has no terminal marker for session={} stage={}",
            sessionId.value, stageId.value, e,
        )
    }
    cancellations.remove(sessionId)
    evictArtifactContentCache(sessionId)
    return WorkflowResult.Failed(sessionId, reason, retryExhausted)
}

/**
 * At loop resolution, correlate any recorded critique findings (§B-§6) into outcome events that
 * feed the critic-calibration projection. A no-op when no [CritiqueFindingsRecordedEvent]s were
 * recorded (the LLM-side finding emission is a separate, model-gated activation), and idempotent
 * — it skips when outcomes already exist — so it is safe on every terminal path.
 */

internal suspend fun SessionOrchestrator.correlateCritiqueOutcomes(sessionId: SessionId) {
    val events = eventStore.read(sessionId)
    if (events.any { it.payload is CritiqueOutcomeCorrelatedEvent }) return
    val reviews = events.mapNotNull { it.payload as? CritiqueFindingsRecordedEvent }
    if (reviews.isEmpty()) return
    CritiqueOutcomeCorrelator.correlate(sessionId, reviews).forEach { emit(sessionId, it) }
}

internal suspend fun SessionOrchestrator.emit(sessionId: SessionId, payload: EventPayload) {
    log.debug("[session {}] emitting event, payload: {}", sessionId.value.take(7), payload)
    eventStore.append(
        NewEvent(
            metadata = EventMetadata(
                eventId = EventId(UUID.randomUUID().toString()),
                sessionId = sessionId,
                timestamp = Clock.System.now(),
                schemaVersion = 1,
                causationId = null,
                correlationId = null,
            ),
            payload = payload,
        ),
    )
}

/** Appends [payloads] as one transaction (one flow-publish pass) for events that belong together. */
internal suspend fun SessionOrchestrator.emitAll(sessionId: SessionId, payloads: List<EventPayload>) {
    if (payloads.size <= 1) {
        payloads.firstOrNull()?.let { emit(sessionId, it) }
        return
    }
    eventStore.appendAll(
        payloads.map { payload ->
            NewEvent(
                metadata = EventMetadata(
                    eventId = EventId(UUID.randomUUID().toString()),
                    sessionId = sessionId,
                    timestamp = Clock.System.now(),
                    schemaVersion = 1,
                    causationId = null,
                    correlationId = null,
                ),
                payload = payload,
            )
        },
    )
}
