package com.correx.core.kernel.orchestration

import com.correx.core.artifacts.kind.KindContractTable
import com.correx.core.artifacts.kind.KindInference
import com.correx.core.events.events.FileWrittenEvent
import com.correx.core.events.events.ContractGateEvaluatedEvent
import com.correx.core.events.events.PlanCompileCheckedEvent
import com.correx.core.events.events.ContractAssertionResult
import com.correx.core.events.events.ArtifactCreatedEvent
import com.correx.core.events.events.BriefEchoMismatchEvent
import com.correx.core.events.events.BriefGroundingCheckedEvent
import com.correx.core.events.events.StaticAnalysisCompletedEvent
import com.correx.core.events.events.GroundingReference
import com.correx.core.events.events.ExecutionPlanLockedEvent
import com.correx.core.events.events.StageCheckpointFailedEvent
import com.correx.core.events.events.ToolInvocationRequestedEvent
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.transitions.execution.StageExecutionResult
import com.correx.core.transitions.graph.StageConfig
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.atomic.*

internal fun SessionOrchestrator.verifyProduces(
    sessionId: SessionId,
    stageId: StageId,
    stageConfig: StageConfig,
): StageExecutionResult {
    if (stageConfig.produces.isEmpty()) {
        if (stageConfig.allowedTools.isEmpty()) return StageExecutionResult.Success(emptyList())
        val ranTool = eventStore.read(sessionId).any {
            (it.payload as? ToolInvocationRequestedEvent)?.stageId == stageId
        }
        if (ranTool) return StageExecutionResult.Success(emptyList())
        log.error(
            "[Orchestrator] stage declared no artifacts and ran no tools " +
                    "session=${sessionId.value} stage=${stageId.value}",
        )
        return StageExecutionResult.Failure(
            "stage ${stageId.value} declared no artifacts and ran no tools",
            retryable = true,
            gate = "produces",
        )
    }
    val producedIds = stageConfig.produces.map { it.name }.toSet()
    val present = eventStore.read(sessionId)
        .mapNotNull { (it.payload as? ArtifactCreatedEvent)?.artifactId }
        .toSet()
    val missing = producedIds - present
    if (missing.isEmpty()) return StageExecutionResult.Success(emptyList())
    val missingIds = missing.joinToString(", ") { it.value }
    log.error(
        "[Orchestrator] stage missing produced artifacts " +
                "session=${sessionId.value} stage=${stageId.value} missing=$missingIds",
    )
    return StageExecutionResult.Failure(
        "stage ${stageId.value} did not produce declared artifacts: $missingIds",
        retryable = true,
        gate = "produces",
    )
}

/**
 * Stage-level plan checkpointing (BACKLOG §C-A2): make plan adherence observable and replayable
 * by emitting a first-class checkpoint event per stage, reconciling produced-vs-expected against
 * the confirmed (locked) plan. Only runs on plan-driven (phase-2) sessions — i.e. a log that
 * holds an [ExecutionPlanLockedEvent]; non-plan workflows emit nothing. The halt itself is owned
 * by [verifyProduces]; this method only EMITS — a [StageCheckpointFailedEvent] accompanies that
 * halt and surfaces *why* the plan diverged.
 */

internal suspend fun SessionOrchestrator.groundBriefReferences(
    sessionId: SessionId,
    stageId: StageId,
    stageConfig: StageConfig,
    effectives: RunEffectives,
): StageExecutionResult {
    if (stageConfig.metadata["groundReferences"] != "true") return StageExecutionResult.Success(emptyList())
    val workspaceRoot = effectives.policy?.workspaceRoot ?: return StageExecutionResult.Success(emptyList())
    val artifact = stageConfig.produces
        .filter { it.kind.llmEmitted }
        .firstNotNullOfOrNull { artifactContentCache["${sessionId.value}:${it.name.value}"] }
        ?: return StageExecutionResult.Success(emptyList())

    val references = BriefReferenceExtractor.fileReferences(artifact)
    if (references.isEmpty()) return StageExecutionResult.Success(emptyList())

    val results = references.map { ref ->
        GroundingReference(ref, worldProbe.exists(workspaceRoot.resolve(ref)))
    }
    emit(sessionId, BriefGroundingCheckedEvent(sessionId, stageId, stageId.value, results))

    val missing = results.filterNot { it.grounded }.map { it.reference }
    return if (missing.isEmpty()) {
        StageExecutionResult.Success(emptyList())
    } else {
        log.warn(
            "[Orchestrator] brief references nonexistent paths session={} stage={} missing={}",
            sessionId.value, stageId.value, missing.joinToString(", "),
        )
        StageExecutionResult.Failure(
            "stage ${stageId.value} references paths that do not exist in the workspace: " +
                "${missing.joinToString(", ")}. Reference only files you have read.",
            retryable = true,
            gate = "brief_grounding",
        )
    }
}

/**
 * Brief echo gate (plan-pipeline-addenda §A1): for a stage that opted in (`brief_echo`),
 * diff the model's restatement artifact against the original brief. Both are already in the
 * artifact content cache (recorded via ArtifactCreatedEvent), so the diff is a pure function
 * of recorded data — deterministically recomputable on replay without re-probing any
 * environment (invariants #8, #9). Emits [BriefEchoMismatchEvent] on mismatch as a domain
 * signal, then fails the stage (retryable) so the pipeline cannot reach plan generation on a
 * misread brief.
 *
 * Brief-source selection among `needs`: prefers the artifact named "analysis"; if absent,
 * falls back to the single needs artifact present. When there are multiple needs and none is
 * named "analysis", the gate skips (ambiguous brief) and returns Success.
 */

internal suspend fun SessionOrchestrator.checkBriefEcho(
    sessionId: SessionId,
    stageId: StageId,
    stageConfig: StageConfig,
): StageExecutionResult {
    if (stageConfig.metadata["briefEcho"] != "true") return StageExecutionResult.Success(emptyList())

    val echoSlot = stageConfig.produces.firstOrNull { it.kind.llmEmitted }
        ?: return StageExecutionResult.Success(emptyList())
    val echo = artifactContentCache["${sessionId.value}:${echoSlot.name.value}"]
        ?: return StageExecutionResult.Success(emptyList())

    val brief = when {
        stageConfig.needs.any { it.value == "analysis" } ->
            artifactContentCache["${sessionId.value}:analysis"]
        stageConfig.needs.size == 1 ->
            artifactContentCache["${sessionId.value}:${stageConfig.needs.single().value}"]
        else -> null // ambiguous — skip
    } ?: return StageExecutionResult.Success(emptyList())

    val div = BriefEchoDiff.compare(brief, echo)

    return if (div.isMismatch) {
        emit(
            sessionId,
            BriefEchoMismatchEvent(
                sessionId = sessionId,
                stageId = stageId,
                uncoveredRequirements = div.uncoveredRequirements,
                hallucinatedFiles = div.hallucinatedFiles,
                hallucinatedSymbols = div.hallucinatedSymbols,
            ),
        )
        log.warn(
            "[Orchestrator] brief echo mismatch session={} stage={} uncovered={} hallucinatedFiles={}",
            sessionId.value, stageId.value,
            div.uncoveredRequirements.joinToString(", "),
            div.hallucinatedFiles.joinToString(", "),
        )
        StageExecutionResult.Failure(
            "stage ${stageId.value} restated the brief unfaithfully — " +
                "dropped requirements: [${div.uncoveredRequirements.joinToString("; ")}]; " +
                "files not in the brief: [${div.hallucinatedFiles.joinToString(", ")}]. " +
                "Restate the brief exactly; do not drop requirements or invent files.",
            retryable = true,
            gate = "brief_echo",
        )
    } else {
        StageExecutionResult.Success(emptyList())
    }
}

/**
 * Post-validation stage gates, run in order; the first failure short-circuits and is returned.
 * Cheap deterministic checks run first (produces presence, brief grounding, brief echo), then
 * the heavier process-spawning static-analysis step last, so it only runs once a stage is
 * otherwise sound.
 */

internal suspend fun SessionOrchestrator.runPostStageGates(
    sessionId: SessionId,
    stageId: StageId,
    stageConfig: StageConfig,
    effectives: RunEffectives,
    profileCommands: Map<String, String>,
): StageExecutionResult {
    val produced = verifyProduces(sessionId, stageId, stageConfig)
    if (produced is StageExecutionResult.Failure) return produced
    // Stage-level plan checkpoint (C-A2): once a stage has produced its declared artifacts,
    // record the checkpoint before the heavier brief/static gates run.
    emitStageCheckpoint(sessionId, stageId, stageConfig)
    val gates: List<suspend () -> StageExecutionResult> = listOf(
        { groundBriefReferences(sessionId, stageId, stageConfig, effectives) },
        { checkBriefEcho(sessionId, stageId, stageConfig) },
        { runContractGate(sessionId, stageId, stageConfig, effectives) },
        { runPlanCompileGate(sessionId, stageId, stageConfig) },
        { runStaticAnalysis(sessionId, stageId, stageConfig, effectives) },
        { runLspDiagnostics(sessionId, stageId, stageConfig, effectives) },
        { runExecutionGate(sessionId, stageId, stageConfig, effectives, profileCommands) },
        { runReviewGate(sessionId, stageId, stageConfig, effectives) },
    )
    for (gate in gates) {
        val result = gate()
        if (result is StageExecutionResult.Failure) return result
    }
    return StageExecutionResult.Success(emptyList())
}

/**
 * Gate 2 — contract (design 2026-07-06-staged-verification-and-review.md §1). For a stage that
 * produces file_written artifacts, project the files it actually wrote (Option B: runtime
 * manifest), infer each file's kind from its path, derive the [KindContractTable] assertions
 * (universal floor for unknown kinds), and evaluate the FS/TEXT ones deterministically. Every
 * verdict is recorded in a [ContractGateEvaluatedEvent] (invariant #9), so replay reads it back
 * and never re-inspects the filesystem.
 *
 * A failed assertion fails the stage retryably with the failing assertion ids + evidence fed
 * back verbatim — failing-test-name granular hand-back, so the implementer fixes exactly the
 * empty/missing/malformed file rather than read-looping. COMPILER assertions are recorded as
 * skipped here; the static-analysis command gate is their authoritative check.
 */
/**
 * Evaluate the stage's file contract against current filesystem reality and record the verdict
 * in a [ContractGateEvaluatedEvent] (invariant #9). Returns the per-assertion results, or null
 * when the gate does not apply (no evaluator, no workspace root, no file_written produces, or no
 * paths). Shared by the post-stage [runContractGate] and the within-loop remaining-delta refresh
 * ([buildRemainingDeltaEntry]) so both read the same contract-minus-reality computation from a
 * single evaluation + single event emission.
 */

internal suspend fun SessionOrchestrator.evaluateStageContract(
    sessionId: SessionId,
    stageId: StageId,
    stageConfig: StageConfig,
    effectives: RunEffectives,
): List<ContractAssertionResult>? {
    val evaluator = contractAssertionEvaluator ?: return null
    val workspaceRoot = effectives.policy?.workspaceRoot ?: return null
    if (stageConfig.produces.none { it.kind.id == "file_written" }) return null

    // Option A ∪ B: the files the stage actually wrote (runtime manifest) unioned with the
    // concrete files the plan declared it would produce (expectedFiles). A declared file that
    // was never written survives here and fails file_exists — that is the missing-file catch.
    val paths = (stageWrittenPaths(sessionId, stageId) + stageConfig.expectedFiles).distinct()
    if (paths.isEmpty()) return null

    val assertions = paths.flatMap { path ->
        KindContractTable.assertionsFor(KindInference.kindFor(path) ?: "", path)
    }
    val results = assertions.map { a ->
        val v = evaluator.evaluate(workspaceRoot, a)
        ContractAssertionResult(
            assertionId = a.id,
            target = a.target,
            layer = a.layer.name,
            evaluator = a.evaluator.name,
            passed = v.passed,
            skipped = v.skipped,
            evidence = v.evidence.takeLast(CONTRACT_EVIDENCE_CAP),
        )
    }
    emit(sessionId, ContractGateEvaluatedEvent(sessionId, stageId, results))
    return results
}

/** The currently-failing assertions as (target, assertionId, evidence) triples for the checklist. */
internal fun SessionOrchestrator.contractFailureItems(results: List<ContractAssertionResult>): List<Triple<String, String, String>> =
    results.filterNot { it.passed }.map { Triple(it.target, it.assertionId, it.evidence) }

internal suspend fun SessionOrchestrator.runContractGate(
    sessionId: SessionId,
    stageId: StageId,
    stageConfig: StageConfig,
    effectives: RunEffectives,
): StageExecutionResult {
    val results = evaluateStageContract(sessionId, stageId, stageConfig, effectives)
        ?: return StageExecutionResult.Success(emptyList())

    val failures = results.filterNot { it.passed }
    if (failures.isEmpty()) return StageExecutionResult.Success(emptyList())

    log.warn(
        "[Orchestrator] contract gate failed session={} stage={} assertions={}",
        sessionId.value, stageId.value,
        failures.joinToString(", ") { "${it.assertionId}(${it.target})" },
    )
    val detail = failures.joinToString("\n") { "- ${it.assertionId} on ${it.target}: ${it.evidence}" }
    return StageExecutionResult.Failure(
        "stage ${stageId.value} did not satisfy its file contract. Fix these before review:\n$detail",
        retryable = true,
        gate = "contract",
    )
}

/**
 * Plan-compile gate: for a stage that produces an `execution_plan` artifact (the freestyle
 * architect), compile its produced plan JSON as a post-stage gate. A compile failure fails the
 * stage retryably with the compiler error fed back, so the existing retry-feedback loop hands it
 * to the architect and the model self-corrects — rather than the post-planning compiler rejecting
 * a broken plan with no re-plan path (which parked the session in ACTIVE forever). The verdict is
 * recorded in a [PlanCompileCheckedEvent] (invariant #9) so replay reads it back and never
 * re-compiles.
 */

internal suspend fun SessionOrchestrator.runPlanCompileGate(
    sessionId: SessionId,
    stageId: StageId,
    stageConfig: StageConfig,
): StageExecutionResult {
    val check = planCompilationCheck ?: return StageExecutionResult.Success(emptyList())
    val planSlot = stageConfig.produces.firstOrNull { it.kind.id == "execution_plan" }
        ?: return StageExecutionResult.Success(emptyList())
    val json = artifactContentCache["${sessionId.value}:${planSlot.name.value}"]
        ?: return StageExecutionResult.Success(emptyList())

    val error = check.check(json, "freestyle-${sessionId.value}")
    emit(sessionId, PlanCompileCheckedEvent(sessionId, stageId, passed = error == null, error = error))
    if (error == null) return StageExecutionResult.Success(emptyList())

    log.warn(
        "[Orchestrator] plan-compile gate failed session={} stage={}: {}",
        sessionId.value, stageId.value, error,
    )
    return StageExecutionResult.Failure(
        "stage ${stageId.value} produced an execution_plan that does not compile. " +
            "Fix the plan before it can be locked:\n$error",
        retryable = true,
        gate = "plan_compile",
    )
}

/**
 * The workspace-relative paths this stage actually wrote, projected from events (invariant #1):
 * ToolInvocationRequested → this stage's invocation ids, FileWrittenEvent → the paths written
 * under them. Pure projection, replay-safe.
 */
/**
 * True if the session has committed to a buildable project — it wrote either a real code module
 * (a path whose inferred kind carries the `imports_resolve` COMPILER assertion: TS/React/Kotlin
 * source) OR a build manifest ([BUILD_MANIFEST_KINDS]: package.json, a Gradle module) that
 * declares a toolchain. Read from the recorded FileWritten manifest (invariant #9: replay-safe,
 * no filesystem access), it is the run-time signal that warrants the auto build gate — the
 * planner's declared kinds (uniformly `file_written`) never reveal this, but the written paths do.
 *
 * The manifest case is what catches the degenerate scaffold that writes only `package.json` and
 * no sources: `npm run build` then fails for lack of an entry, so the gate rejects the empty
 * "project" instead of letting it reach COMPLETED. A docs-only plan writes neither, so it is left
 * alone.
 */

internal fun SessionOrchestrator.sessionProducedBuildTarget(sessionId: SessionId): Boolean =
    eventStore.read(sessionId)
        .mapNotNull { it.payload as? FileWrittenEvent }
        .filter { it.postImageHash != null }
        .map { it.path }
        .distinct()
        .any { path ->
            val kind = KindInference.kindFor(path) ?: return@any false
            kind in BUILD_MANIFEST_KINDS ||
                KindContractTable.assertionsFor(kind, path).any { it.id == "imports_resolve" }
        }

internal fun SessionOrchestrator.stageWrittenPaths(sessionId: SessionId, stageId: StageId): List<String> {
    val events = eventStore.read(sessionId)
    val invocationIds = events.mapNotNull { it.payload as? ToolInvocationRequestedEvent }
        .filter { it.stageId == stageId }
        .map { it.invocationId }
        .toSet()
    return events.mapNotNull { it.payload as? FileWrittenEvent }
        .filter { it.invocationId in invocationIds && it.postImageHash != null }
        .map { it.path }
        .distinct()
}

/**
 * Static-first reviewer gate (role-reliability §5): for a stage that declares `static_analysis`
 * commands, run them (compiler / detekt / formatters) against its just-produced output in the
 * workspace root. Every result is recorded in a [StaticAnalysisCompletedEvent] — an environment
 * observation captured at run time (invariant #9), so replay reads it back and never re-runs.
 *
 * A non-clean command fails the stage retryably with its output fed back verbatim (§2: compiler/
 * test output is ground truth, not LLM noise), so the producing stage fixes the issue and only
 * static-clean output ever reaches the downstream LLM reviewer — its context thereby excludes
 * anything static tools already catch, with no parsing or context plumbing.
 */
