package com.correx.core.kernel.orchestration

import com.correx.core.events.events.ReviewFindingsRaisedEvent
import com.correx.core.events.events.ReviewVerdict
import com.correx.core.events.events.ArtifactCreatedEvent
import com.correx.core.events.events.StaticAnalysisCompletedEvent
import com.correx.core.events.events.StaticAnalysisFinding
import com.correx.core.events.events.LspDiagnosticsCompletedEvent
import com.correx.core.events.events.ArtifactValidatedEvent
import com.correx.core.events.events.ArtifactValidatingEvent
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.BuildExpectation
import com.correx.core.transitions.graph.StageConfig
import com.correx.core.validation.model.ValidationReport
import com.correx.core.validation.model.ValidationSeverity
import java.nio.file.Path
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.atomic.*

internal suspend fun SessionOrchestrator.runStaticAnalysis(
    sessionId: SessionId,
    stageId: StageId,
    stageConfig: StageConfig,
    effectives: RunEffectives,
): StageExecutionResult {
    if (stageConfig.staticAnalysis.isEmpty()) return StageExecutionResult.Success(emptyList())
    val runner = staticAnalysisRunner
    val workspaceRoot = effectives.policy?.workspaceRoot
    if (runner == null || workspaceRoot == null) {
        log.warn(
            "[Orchestrator] stage declares static_analysis but no {} is wired — skipping " +
                "session={} stage={}",
            if (runner == null) "runner" else "workspace root",
            sessionId.value, stageId.value,
        )
        return StageExecutionResult.Success(emptyList())
    }

    val results = stageConfig.staticAnalysis.map { command ->
        val run = runner.run(workspaceRoot, command)
        StaticAnalysisFinding(
            command = command,
            exitCode = run.exitCode,
            summary = run.output.takeLast(STATIC_ANALYSIS_SUMMARY_CAP),
        ) to run.output
    }
    emit(sessionId, StaticAnalysisCompletedEvent(sessionId, stageId, results.map { it.first }))

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

    log.warn(
        "[Orchestrator] static analysis failed session={} stage={} commands={}",
        sessionId.value, stageId.value,
        failures.joinToString(", ") { it.first.command },
    )
    val detail = failures.joinToString("\n\n") { (finding, output) ->
        "$ ${finding.command}  (exit ${finding.exitCode})\n${output.takeLast(STATIC_ANALYSIS_FEEDBACK_CAP)}"
    }
    return StageExecutionResult.Failure(
        "stage ${stageId.value} did not pass static analysis. Fix these before review:\n\n$detail",
        retryable = true,
        gate = "static_analysis",
    )
}

@Suppress("ReturnCount")
internal suspend fun SessionOrchestrator.runLspDiagnostics(
    sessionId: SessionId,
    stageId: StageId,
    stageConfig: StageConfig,
    effectives: RunEffectives,
): StageExecutionResult {
    val runner = lspDiagnosticsRunner ?: return StageExecutionResult.Success(emptyList())
    val workspaceRoot = effectives.policy?.workspaceRoot ?: return StageExecutionResult.Success(emptyList())
    val stagePaths = stageWrittenPaths(sessionId, stageId)
    val paths = if (stagePaths.isNotEmpty()) {
        stagePaths
    } else if (stageConfig.autoBuildGate) {
        sessionWrittenPaths(sessionId)
    } else {
        emptyList()
    }
    if (paths.isEmpty()) return StageExecutionResult.Success(emptyList())

    val result = runner.pull(LspDiagnosticsRequest(workspaceRoot, paths))
    val plannedButUnwritten = stageConfig.writeManifest.filterNot { it in paths }
    val diagnostics = result.diagnostics.filter { diagnostic ->
        diagnostic.path in paths && plannedButUnwritten.none { planned ->
            diagnostic.message.contains(planned.substringAfterLast('/'), ignoreCase = true)
        }
    }
    emit(
        sessionId,
        LspDiagnosticsCompletedEvent(sessionId, stageId, result.server, diagnostics, result.skippedReason),
    )
    // Lint-class diagnostics (unused import, deprecated) are recorded above but never gate: a
    // tsconfig with noUnusedLocals promotes them to "error" severity, which no rewrite can clear.
    val errors = diagnostics.filter { it.severity.equals("error", ignoreCase = true) && !it.isLint }
    if (errors.isEmpty()) return StageExecutionResult.Success(emptyList())
    val detail = errors.joinToString("\n") {
        "- ${it.path}:${it.line + 1}:${it.character + 1} ${it.code.orEmpty()} ${it.message}".trim()
    }
    return StageExecutionResult.Failure(
        "stage ${stageId.value} has LSP diagnostics in files it wrote. Fix these before proceeding:\n$detail",
        retryable = true,
        gate = "lsp_diagnostics",
    )
}

/**
 * True if the most recent LSP diagnostics run for [stageId] was skipped (e.g. tsserver failed to
 * initialize) rather than actually run. A skipped run emits empty diagnostics, which the LSP gate
 * treats as clean — so the execution gate must not trust MODULE→LSP delegation when this holds, and
 * falls through to the real build command. Pure projection over recorded events (invariant #9).
 */
internal fun SessionOrchestrator.lspDiagnosticsSkipped(sessionId: SessionId, stageId: StageId): Boolean =
    eventStore.read(sessionId)
        .mapNotNull { it.payload as? LspDiagnosticsCompletedEvent }
        .lastOrNull { it.stageId == stageId }
        ?.skippedReason != null

internal fun SessionOrchestrator.sessionWrittenPaths(sessionId: SessionId): List<String> =
    eventStore.read(sessionId)
        .mapNotNull { it.payload as? com.correx.core.events.events.FileWrittenEvent }
        .filter { it.postImageHash != null }
        .map { it.path }
        .distinct()

/**
 * Gate 4 — execution (staged-verification §1/§2). A stage's [StageConfig.buildExpectation]
 * (none/module/project/tests) resolves to the bound project profile's typecheck/build/test
 * command and runs it as a deterministic build gate. NONE runs nothing (early stages where the
 * project isn't yet runnable). A profile missing the alias skips with a warning — the vocabulary
 * degrades safely rather than failing when the operator hasn't configured commands. A non-clean
 * run fails the stage retryably with the build output fed back (recorded as a
 * [StaticAnalysisCompletedEvent], invariant #9, so replay never re-runs it).
 */

@Suppress("ReturnCount")
internal suspend fun SessionOrchestrator.runExecutionGate(
    sessionId: SessionId,
    stageId: StageId,
    stageConfig: StageConfig,
    effectives: RunEffectives,
    profileCommands: Map<String, String>,
): StageExecutionResult {
    // An explicit build_expectation resolves directly. Otherwise, if the compiler flagged this
    // (terminal) stage as auto-gateable and the session actually wrote a code module, promote to a
    // PROJECT build — the LLM planner never sets build_expectation on its file_written stages, so
    // this is the only path that gates a scaffold's compilability. Code-ness is judged here (not at
    // compile time) from the real FileWritten manifest, so a docs-only plan is left alone.
    val expectation: BuildExpectation? = when {
        stageConfig.buildExpectation != BuildExpectation.NONE -> stageConfig.buildExpectation
        stageConfig.autoBuildGate && sessionProducedBuildTarget(sessionId) -> BuildExpectation.PROJECT
        else -> null
    }
    // LSP pull diagnostics are the compiler-front-end/typecheck gate. Keep PROJECT bundlers and
    // TESTS as real commands, but do not invoke the profile's flat `typecheck` alias as well —
    // UNLESS the LSP run for this stage was skipped (no tsserver, init failure): a skipped check
    // verified nothing, and empty diagnostics then read as "clean", so fall through to the real
    // build command instead of trusting a gate that never ran.
    // ponytail: no unit test — first test on this path needs a full orchestrator + bound-profile +
    // LSP-runner fixture (none exists); validated by live QA re-run instead. Add if it regresses.
    if (expectation == BuildExpectation.MODULE && lspDiagnosticsRunner != null &&
        !lspDiagnosticsSkipped(sessionId, stageId)
    ) {
        return StageExecutionResult.Success(emptyList())
    }
    val alias = expectation?.commandAlias ?: return StageExecutionResult.Success(emptyList())
    val runner = staticAnalysisRunner
    val workspaceRoot = effectives.policy?.workspaceRoot
    if (runner == null || workspaceRoot == null) return StageExecutionResult.Success(emptyList())
    val toolchain = stageProducedToolchain(sessionId, stageId)?.profileKey
    val command = expectation.commandFor(profileCommands, toolchain)
    if (command.isNullOrBlank()) {
        log.warn(
            "[Orchestrator] stage {} needs a {} build gate but project profile has no '{}'{} " +
                "command — skipping execution gate",
            stageId.value, expectation, alias,
            toolchain?.let { " for toolchain '$it'" }.orEmpty(),
        )
        return StageExecutionResult.Success(emptyList())
    }

    runSetupCommand(sessionId, stageId, workspaceRoot, profileCommands, runner)?.let { return it }

    val run = runner.run(workspaceRoot, command)
    val finding = StaticAnalysisFinding(command, run.exitCode, run.output.takeLast(STATIC_ANALYSIS_SUMMARY_CAP))
    emit(sessionId, StaticAnalysisCompletedEvent(sessionId, stageId, listOf(finding)))
    if (finding.clean) {
        // Known-good workspace invariant (design 2026-07-15 seam 2): bind this green result to the
        // workspace state key so the next stage can trust it only while the workspace is unchanged.
        recordWorkspaceVerification(sessionId, stageId, expectation, command, passed = true)
        return StageExecutionResult.Success(emptyList())
    }

    log.warn(
        "[Orchestrator] execution gate failed session={} stage={} expectation={} command={}",
        sessionId.value, stageId.value, expectation, command,
    )
    return StageExecutionResult.Failure(
        "stage ${stageId.value} did not pass its $expectation build gate. " +
            "Fix these before proceeding:\n\n$ $command  (exit ${run.exitCode})\n" +
            run.output.takeLast(STATIC_ANALYSIS_FEEDBACK_CAP),
        retryable = true,
        gate = "execution",
    )
}

/**
 * Run the operator-declared `setup` command (profile alias [SETUP_COMMAND_ALIAS]) before a build
 * gate, when one is configured. A build like `tsc && vite build` fails spuriously if the project
 * was never prepared (deps uninstalled, codegen not run) — the funnel reads that as a code failure
 * and routes into futile repair. The operator names the fix in their profile (`setup = "npm ci"`,
 * `"cargo fetch"`, `"./gradlew dependencies"`, …); the kernel stays ecosystem-agnostic. Runs in the
 * workspace root, recorded as a [StaticAnalysisCompletedEvent] (invariant #9). Returns a
 * [StageExecutionResult.Failure] when setup itself fails (a real, actionable error), else null (no
 * setup configured, or it succeeded — proceed to the build).
 */

internal suspend fun SessionOrchestrator.runSetupCommand(
    sessionId: SessionId,
    stageId: StageId,
    workspaceRoot: Path,
    profileCommands: Map<String, String>,
    runner: StaticAnalysisRunner,
): StageExecutionResult? {
    val setup = profileCommands[SETUP_COMMAND_ALIAS]?.takeIf { it.isNotBlank() } ?: return null
    log.info(
        "[Orchestrator] running setup command before build gate session={} stage={} command='{}'",
        sessionId.value, stageId.value, setup,
    )
    val run = runner.run(workspaceRoot, setup)
    val finding = StaticAnalysisFinding(setup, run.exitCode, run.output.takeLast(STATIC_ANALYSIS_SUMMARY_CAP))
    emit(sessionId, StaticAnalysisCompletedEvent(sessionId, stageId, listOf(finding)))
    if (finding.clean) return null
    return StageExecutionResult.Failure(
        "stage ${stageId.value} setup command failed before its build gate. " +
            "Fix these before proceeding:\n\n$ $setup  (exit ${run.exitCode})\n" +
            run.output.takeLast(STATIC_ANALYSIS_FEEDBACK_CAP),
        retryable = true,
        gate = "execution",
    )
}

/**
 * Gate 3 — semantic (LLM) review (staged-verification § Gate 3). For a stage that opts in
 * (`semanticReview`), the injected [SemanticReviewer] reads the files the stage actually wrote
 * (the FileWritten manifest — never blind context) and reviews them against the stage objective,
 * returning PR-comment-style findings. Runs last, so its input is static-clean (the deterministic
 * funnel already passed) and its findings exclude anything the cheap gates catch.
 *
 * The reviewer is advisory: every finding is recorded in a [ReviewFindingsRaisedEvent] and
 * surfaces to the operator regardless of verdict. A [ReviewVerdict.FAIL] blocks the stage
 * retryably ONLY when it carries a high-confidence *correctness* finding and the review-block
 * budget ([REVIEW_BLOCK_RETRY_CAP]) is not yet spent — so a stuck reviewer can never trap the
 * stage; after the budget, findings surface without blocking. Suboptimal/opinion findings never
 * block. Recorded as an environment observation (invariant #9); replay never re-runs the reviewer.
 */

internal suspend fun SessionOrchestrator.runReviewGate(
    sessionId: SessionId,
    stageId: StageId,
    stageConfig: StageConfig,
    effectives: RunEffectives,
): StageExecutionResult {
    if (!stageConfig.semanticReview) return StageExecutionResult.Success(emptyList())
    val reviewer = semanticReviewer ?: return StageExecutionResult.Success(emptyList())
    val workspaceRoot = effectives.policy?.workspaceRoot ?: return StageExecutionResult.Success(emptyList())
    val files = stageWrittenPaths(sessionId, stageId)
    if (files.isEmpty()) return StageExecutionResult.Success(emptyList())

    // Goal-conformance framing (architecture-conformance 2026-07-14): lead the reviewer with the
    // authoritative intent and the stage's declared scope so it can judge whether the files serve
    // the goal (and stay within scope), not just whether they are internally defect-free — the
    // deterministic gates already cover mechanical correctness. Absent intent/scope degrade to the
    // prior upstream-artifact objective.
    val intentHeader = initialIntent(sessionId)?.takeIf { it.isNotBlank() }
        ?.let { "## Authoritative intent (the goal these files must serve)\n$it\n\n" } ?: ""
    val scopeHeader = stageConfig.touches.takeIf { it.isNotEmpty() }
        ?.let { "## This stage's declared scope\n${it.joinToString(", ")}\n\n" } ?: ""
    val objective = (
        intentHeader + scopeHeader +
            stageConfig.needs
                .mapNotNull { artifactContentCache["${sessionId.value}:${it.value}"] }
                .joinToString("\n\n")
                .ifBlank { "Stage '${stageId.value}' produced the files below; review them for defects." }
        ).take(REVIEW_OBJECTIVE_CAP)

    val outcome = reviewer.review(sessionId, stageId, workspaceRoot, files, objective)

    // A FAIL blocks only on a high-confidence correctness finding, and only while the review-block
    // budget remains — count prior review blocks for this stage from the log (replay-safe).
    val priorBlocks = eventStore.read(sessionId)
        .mapNotNull { it.payload as? ReviewFindingsRaisedEvent }
        .count { it.stageId == stageId && it.blocked }
    val blockingFinding = outcome.findings.firstOrNull {
        it.correctness && it.confidence >= tuning.reviewBlockMinConfidence
    }
    val shouldBlock = outcome.verdict == ReviewVerdict.FAIL &&
        blockingFinding != null &&
        priorBlocks < tuning.reviewBlockRetryCap

    emit(sessionId, ReviewFindingsRaisedEvent(sessionId, stageId, outcome.verdict, outcome.findings, blocked = shouldBlock))

    if (!shouldBlock) {
        if (outcome.findings.isNotEmpty()) {
            log.info(
                "[Orchestrator] semantic review advisory session={} stage={} verdict={} findings={}",
                sessionId.value, stageId.value, outcome.verdict, outcome.findings.size,
            )
        }
        return StageExecutionResult.Success(emptyList())
    }

    log.warn(
        "[Orchestrator] semantic review blocked session={} stage={} finding={}",
        sessionId.value, stageId.value, blockingFinding?.target,
    )
    val detail = outcome.findings
        .filter { it.correctness }
        .joinToString("\n") { "- [${it.severity}] ${it.target}: ${it.message}" +
            (it.suggestedFix?.let { fix -> "\n  fix: $fix" } ?: "") }
    return StageExecutionResult.Failure(
        "stage ${stageId.value} failed semantic review — fix these correctness issues:\n$detail",
        retryable = true,
        gate = "review",
    )
}

internal suspend fun SessionOrchestrator.emitProcessResultEvents(
    sessionId: SessionId,
    stageId: StageId,
    stageConfig: StageConfig,
) {
    // Emit artifact lifecycle events for process_result slots on failure branches.
    // Content was already stored in CAS + cache by dispatchToolCalls for every outcome.
    stageConfig.produces.filter { it.kind.id == "process_result" }.forEach { slot ->
        emitAll(
            sessionId,
            listOf(
                ArtifactCreatedEvent(slot.name, sessionId, stageId, schemaVersion = 1),
                ArtifactValidatingEvent(slot.name, sessionId, stageId),
                ArtifactValidatedEvent(slot.name, sessionId, stageId),
            ),
        )
    }
}

// Collapse a validation report into a concise, actionable reason. This string becomes the
// RetryAttemptedEvent.failureReason that the retry-feedback entry feeds back to the model, so a
// weak model is told *what* was wrong (e.g. "required property 'summary' missing") instead of a
// bare "validation failed" it can't act on. Capped to keep the feedback prompt small.
internal fun SessionOrchestrator.summarizeRejection(report: ValidationReport): String {
    val errors = report.sections.flatMap { section ->
        section.issues
            .filter { it.severity == ValidationSeverity.ERROR }
            .map { it.message }
    }.distinct()
    return if (errors.isEmpty()) {
        "validation failed"
    } else {
        "validation failed: " + errors.take(tuning.maxFeedbackIssues).joinToString("; ")
    }
}
