package com.correx.core.kernel.orchestration

import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.model.ApprovalDecision
import com.correx.core.inference.Tokenizer
import com.correx.core.journal.DefaultDecisionJournalRepository
import com.correx.core.artifacts.kind.ArtifactKindRegistry
import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ArtifactContentStoredEvent
import com.correx.core.events.events.ClarificationAnswer
import com.correx.core.events.events.ClarificationAnsweredEvent
import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.SteeringNoteAddedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.TransitionExecutedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.orchestration.OrchestrationState
import com.correx.core.events.types.ApprovalDecisionId
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.ClarificationRequestId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
import com.correx.core.kernel.execution.WorkflowResult
import com.correx.core.kernel.orchestration.subagent.InSessionSubagentRunner
import com.correx.core.kernel.orchestration.subagent.SubagentRunner
import com.correx.core.kernel.retry.RetryCoordinator
import com.correx.core.sessions.Session
import com.correx.core.transitions.graph.WorkflowGraph
import kotlinx.datetime.Clock
import org.slf4j.LoggerFactory
import java.util.UUID
import java.util.concurrent.*
import java.util.concurrent.atomic.*

@SuppressWarnings(
    "ReturnCount",
    "NestedBlockDepth",
)
private val log = LoggerFactory.getLogger(DefaultSessionOrchestrator::class.java)

// Own, small budget for the retry-agency recovery route: how many times a single stage may be
// routed to a recovery stage before the run fails terminally. Deliberately smaller than the
// per-gate retry budget — recovery is a purpose-built repair-then-reverify loop, not open-ended.
// RECOVERY_ROUTE_BUDGET / INTENT_ROUTE_BUDGET now in OrchestrationTuning.

// Tier-2 escalation budget: how many NO-PROGRESS routes the arbiter (recovery-stage-as-intent-holder)
// gets after the tier-1 owner loop exhausts, before the run fails terminally. Independent of the owner
// budget (keyed with INTENT_BUDGET_SUFFIX) so escalation is a genuine second chance, not a shared pool.

// Transition ids for the failure-ticket loop. A ticket routes the failing gate's control to the
// repair owner (TICKET_ROUTE); the owner, once done, hands control straight back to the gate that
// opened the ticket (TICKET_RETURN). Both are kernel-driven, not plan edges — see ticketReturnMove.
internal val TICKET_ROUTE = TransitionId("ticket-route")
internal val TICKET_RETURN = TransitionId("ticket-return")

// Shortest evidence token treated as a file reference — guards against matching a written path's
// suffix on a trivially short token (an extension fragment, a one-char name).
internal const val MIN_EVIDENCE_TOKEN = 5

// File-like tokens in gate evidence (tsc/build output): "src/App.tsx", "frontend/x.ts:12:5".
internal val EVIDENCE_PATH_RE = Regex("""[\w./@-]+\.[A-Za-z0-9]+""")

// Deterministic gate id → capability the failing stage must hold to change the failure condition.
// A gate not listed here is not eligible for recovery routing (retries in place as before).
// Gate id for a repeated missing build prerequisite (design #163/#170). Its own separate handling
// (bounded bootstrap turn / recovery routing) is triggered off this id before the normal retry path.
internal const val WORKSPACE_PRECONDITION_GATE = "workspace_precondition"

// Gate id for a provably-stuck stage failure loop (Vikunja #78). Detected by repeatedToolFailureLoop
// when the SAME tool-failure signature recurs past the tuning limit; routed straight to recovery
// (never retried in place) by the step handler before the normal retry path.
internal const val STAGE_LOOP_BREAK_GATE = "stage_loop_break"

// Gate id for the same-fingerprint loop-breaker firing INSIDE the recovery stage itself (Vikunja
// #309) — a file recovery keeps rewriting without ever clearing its diagnostic. Distinct from
// STAGE_LOOP_BREAK_GATE (which fires on repeated raw TOOL failures pre-recovery): this one is keyed
// on (path, diagnostic) persistence, since recovery's file_edit calls themselves typically succeed.
internal const val RECOVERY_LOOP_BREAK_GATE = "recovery_loop_break"

internal val GATE_REQUIRED_CAPABILITY: Map<String, String> = mapOf(
    "execution" to "file_write",
    "contract" to "file_write",
    "static_analysis" to "file_write",
    WORKSPACE_PRECONDITION_GATE to "file_write",
)

// ponytail: trimmed to the single non-redundant sentence after the 2026-07-16 audition
// (docs/qa/QA-stage-prompt-audition-results-2026-07-16.md) found the full block performance-neutral —
// the read-before-write / verify-before-complete / use-exact-feedback nudges just restate behavior the
// gates already enforce, and run 3 proved prose doesn't stop failure loops (deterministic defenses do).
// Only the scaffolding-scope boundary encodes a decision no gate makes. Delete outright if a future
// multi-fixture audit still shows no clarity value.
internal const val CURATED_STAGE_OPERATING_GUIDANCE = """## Operating guidance
When the authoritative intent plainly requires project setup, creating its manifest, configuration,
or entry file is in scope; do not spend turns re-deciding that settled boundary."""

// Deterministic failure category from the gate id (no LLM — keeps routing off the untrusted path).
internal fun ticketCategory(gate: String): String = when (gate) {
    "plan_compile" -> "planning"
    else -> "implementation"
}

class DefaultSessionOrchestrator(
    internal val repositories: OrchestratorRepositories,
    engines: OrchestratorEngines,
    internal val retryCoordinator: RetryCoordinator,
    artifactStore: ArtifactStore,
    tokenizer: Tokenizer? = null,
    decisionJournalRepository: DefaultDecisionJournalRepository,
    internal val compactionService: JournalCompactionService? = null,
    artifactKindRegistry: ArtifactKindRegistry? = null,
    repoKnowledgeRetriever: RepoKnowledgeRetriever? = null,
    readyTaskCounter: ReadyTaskCounter? = null,
    taskClaimCoordinator: TaskClaimCoordinator? = null,
    tuning: OrchestrationTuning = OrchestrationTuning(),
) : SessionOrchestrator(repositories, engines, artifactStore, decisionJournalRepository, artifactKindRegistry = artifactKindRegistry, repoKnowledgeRetriever = repoKnowledgeRetriever, readyTaskCounter = readyTaskCounter, taskClaimCoordinator = taskClaimCoordinator, tuning = tuning), ApprovalGateway {
    override val tokenizer: Tokenizer? = tokenizer
    override val cancellations: ConcurrentHashMap<SessionId, AtomicBoolean> =
        ConcurrentHashMap<SessionId, AtomicBoolean>()

    // Hybrid-exhaustion salvage judge (T6/T7): consulted only when the "review" gate exhausts its
    // per-gate retry budget. Null = deterministic allow-one-reset-then-fail fallback (see
    // decideGateExhaustion) so the feature degrades safely without inference.
    internal val salvageJudge: SalvageJudge? = engines.salvageJudge

    // One-shot post-failure diagnostic (#294): consulted once per terminal-failure fingerprint just
    // before a run goes terminal. Null = deterministic degrade (fail terminally, see terminalOrDiagnose).
    internal val postFailureDiagnoser: PostFailureDiagnoser? = engines.postFailureDiagnoser

    override val subagentRunner: SubagentRunner = InSessionSubagentRunner(
        executeStage = { sid, stg, graph, session, cfg ->
            executeStage(sid, stg, graph, session, cfg, effectivesFor(cfg))
        },
    )

    override suspend fun run(
        sessionId: SessionId,
        graph: WorkflowGraph,
        config: OrchestrationConfig,
    ): WorkflowResult = runFrom(sessionId, graph, config, graph.start)

    /**
     * Runs [graph] entering at [startStage] instead of [graph].start. Used by the freestyle
     * return-to-architect loop to re-enter just the plan-producing stage (with grounding feedback
     * already in its L1 context) without redoing discovery/analyst. [startStage] must be in [graph].
     */
    suspend fun runFrom(
        sessionId: SessionId,
        graph: WorkflowGraph,
        config: OrchestrationConfig,
        startStage: StageId,
    ): WorkflowResult {
        require(graph.stages.containsKey(startStage)) {
            "startStage '${startStage.value}' is not a stage of workflow '${graph.id}'"
        }
        log.debug("[Orchestrator] session={} workflow={} start={}", sessionId.value, graph.id, startStage.value)
        emitWorkflowStarted(sessionId, graph, config, startStage)

        val base = ExecutionContext(graph, sessionId, 0, startStage, config, null, null)
        val enriched = base.enrich()

        // Execute the start stage before entering the step loop
        return when (val result = enterStage(enriched, startStage)) {
            is StepResult.Continue -> step(result.ctx)
            is StepResult.Terminal -> result.result
        }
    }

    /** Returns the cached validated artifact content for the given session + artifact id, or null if absent. */
    fun validatedArtifactContent(sessionId: SessionId, artifactId: ArtifactId): String? =
        artifactContentCache["${sessionId.value}:${artifactId.value}"]

    /**
     * Rebuilds [artifactContentCache] from durable events after a server restart.
     * Scans all events for [ArtifactContentStoredEvent], which records the
     * slot-name -> CAS content-hash mapping at write time, and re-fetches the
     * stored bytes from [artifactStore] by **content hash** (not slot name).
     * Must be called before [resume] so transition conditions and `needs`-injection
     * that read artifact content work correctly after a cold start.
     */
    suspend fun rehydrate(sessionId: SessionId) {
        repositories.eventStore.readFrom(sessionId, fromSequence = 0L).forEach { event ->
            val p = event.payload as? ArtifactContentStoredEvent ?: return@forEach
            val key = "${sessionId.value}:${p.artifactId.value}"
            artifactStore.get(p.contentHash)
                ?.toString(Charsets.UTF_8)
                ?.let { artifactContentCache[key] = it }
        }
    }

    suspend fun resume(
        sessionId: SessionId,
        graph: WorkflowGraph,
        config: OrchestrationConfig,
    ): WorkflowResult {
        val stageId = orchestrationRepository.getState(sessionId).currentStageId
            ?: return WorkflowResult.Failed(sessionId, "resume: no currentStageId", retryExhausted = false)
        if (!graph.stages.containsKey(stageId)) {
            // Terminal pseudo-stage ("done") or a stage from another graph — nothing to
            // re-enter. Treat as already completed instead of throwing inside enterStage.
            log.info(
                "[Orchestrator] resume: session={} stage={} not in graph '{}' — already terminal",
                sessionId.value, stageId.value, graph.id,
            )
            return WorkflowResult.Completed(sessionId, stageId)
        }
        log.info("[Orchestrator] resuming session={} workflow={} stage={}", sessionId.value, graph.id, stageId.value)
        val base = ExecutionContext(graph, sessionId, 0, stageId, config, null, null)
        val enriched = base.enrich()
        return when (val result = enterStage(enriched, stageId)) {
            is StepResult.Continue -> step(result.ctx.copy(currentStageId = stageId))
            is StepResult.Terminal -> result.result
        }
    }

    override suspend fun cancel(sessionId: SessionId) {
        cancellations.getOrPut(sessionId) { AtomicBoolean(false) }.set(true)
    }

    override suspend fun submitApprovalDecision(requestId: ApprovalRequestId, decision: ApprovalDecision) {
        val deferred = pendingApprovals[requestId]
        if (deferred != null) {
            deferred.complete(decision)
            return
        }
        // No live orchestrator coroutine for this request — server restarted while approval was pending.
        // Record the decision and emit resume to unblock the session in the event store.
        log.warn(
            "submitApprovalDecision: no live deferred for requestId={} (server restart?), emitting events directly",
            requestId.value,
        )
        val sessionId = decision.contextSnapshot.identity.sessionId
        val stageId = decision.contextSnapshot.identity.stageId
            ?: orchestrationRepository.getState(sessionId).currentStageId
            ?: return
        emit(sessionId, ApprovalDecisionResolvedEvent(
            decisionId = ApprovalDecisionId(UUID.randomUUID().toString()),
            requestId = requestId,
            outcome = decision.outcome ?: ApprovalOutcome.REJECTED,
            status = ApprovalStatus.COMPLETED,
            tier = decision.tier,
            resolutionTimestamp = Clock.System.now(),
            reason = decision.reason,
            userSteering = decision.userSteering,
        ))
        emit(sessionId, OrchestrationResumedEvent(sessionId, stageId))
    }

    /**
     * Delivers the operator's answers to a pending clarification raised by a stage. The waiting
     * stage (parked in [requestClarificationIfNeeded]) unparks and the run advances to the next
     * stage with the answers in context. If the server restarted while the clarification was pending there is no live
     * coroutine to complete, so the answers are recorded directly and the session resumed.
     */
    suspend fun submitClarification(
        sessionId: SessionId,
        stageId: StageId,
        requestId: ClarificationRequestId,
        answers: List<ClarificationAnswer>,
    ) {
        val deferred = pendingClarifications[requestId]
        if (deferred != null) {
            deferred.complete(answers)
            return
        }
        log.warn(
            "submitClarification: no live deferred for requestId={} (server restart?), recording directly",
            requestId.value,
        )
        emit(sessionId, ClarificationAnsweredEvent(requestId, sessionId, stageId, answers))
        emit(sessionId, OrchestrationResumedEvent(sessionId, stageId))
    }

    /**
     * Records free-form operator steering for a session that is mid-stage — independent of any
     * approval gate. Unlike [submitApprovalDecision]/[submitClarification] this never completes a
     * pending deferred: the running stage is not paused waiting on it. It only appends a
     * [com.correx.core.events.events.SteeringNoteAddedEvent], which [buildSteeringNoteEntries] folds
     * into context as an advisory `steeringNote` entry on the *next* context turn (invariants #3/#7 —
     * advisory only, never a state mutation). Reuses the existing SteeringNoteAddedEvent/fold rather
     * than a parallel event type, since that mechanism already implements exactly this semantics
     * (see TalkieFacade's STEERING chat mode, which emits the same event).
     */
    suspend fun submitSteering(sessionId: SessionId, text: String) {
        if (text.isBlank()) return
        val currentStageId = runCatching { orchestrationRepository.getState(sessionId).currentStageId }
            .getOrNull()
        emit(
            sessionId,
            SteeringNoteAddedEvent(
                sessionId = sessionId,
                content = text,
                stageId = currentStageId,
            ),
        )
    }

    private fun ExecutionContext.enrich() = EnrichedExecutionContext(
        graph, sessionId, stageCount, currentStageId, config,
        session = repositories.sessionRepository.getSession(sessionId),
        state = orchestrationRepository.getState(sessionId),
    )
}

// A back-edge re-enters any stage already visited this run (e.g. reviewer→implementer). The start
// stage is visited via WorkflowStarted rather than TransitionExecuted, so it must participate too.
// Top-level (not a member) to keep the orchestrator off the TooManyFunctions threshold.
internal fun isBackEdge(events: List<StoredEvent>, target: StageId): Boolean =
    events.any {
        when (val payload = it.payload) {
            is WorkflowStartedEvent -> payload.startStageId == target
            is TransitionExecutedEvent -> payload.to == target
            else -> false
        }
    }

internal sealed class StepResult {
    data class Continue(val ctx: EnrichedExecutionContext) : StepResult()
    data class Terminal(val result: WorkflowResult) : StepResult()
}

internal data class ExecutionContext(
    val graph: WorkflowGraph,
    val sessionId: SessionId,
    val stageCount: Int,
    val currentStageId: StageId,
    val config: OrchestrationConfig,
    val session: Session?,
    val state: OrchestrationState?,
)

internal data class EnrichedExecutionContext(
    val graph: WorkflowGraph,
    val sessionId: SessionId,
    val stageCount: Int,
    val currentStageId: StageId,
    val config: OrchestrationConfig,
    val session: Session,
    val state: OrchestrationState,
)
