package com.correx.core.transitions.graph

import com.correx.core.artifacts.kind.TypedArtifactSlot
import com.correx.core.events.types.ArtifactId
import com.correx.core.inference.GenerationConfig
import com.correx.core.inference.ModelCapability

data class StageConfig(
    val requiredCapabilities: Set<ModelCapability> = emptySet(),
    val modelId: String? = null,
    val tokenBudget: Int = 4096,
    val generationConfig: GenerationConfig = GenerationConfig(
        temperature = 0.7,
        topP = 1.0,
        maxTokens = 2048,
    ),
    val allowedTools: Set<String> = emptySet(),
    val maxRetries: Int = 3,
    val produces: List<TypedArtifactSlot> = emptyList(),
    val needs: Set<ArtifactId> = emptySet(),
    // Workspace-relative globs the stage is permitted to write (role-reliability §2
    // diff manifest). Empty = unrestricted within the workspace. A FILE_WRITE outside
    // the declared set is scope creep — the dominant local-implementer failure — and is
    // blocked by the plane-2 ManifestContainmentRule.
    val writeManifest: List<String> = emptyList(),
    // Operator-configured static-analysis commands (compiler/detekt/formatters) run as a
    // deterministic harness step after this stage produces its output (role-reliability §5).
    // A non-clean command fails the stage retryably with its output fed back, so only
    // static-clean output reaches the downstream reviewer. Empty = no static-first step.
    val staticAnalysis: List<String> = emptyList(),
    // Execution-gate scope for this stage (staged-verification §2). NONE = no build gate (default,
    // e.g. early scaffold/types stages); MODULE/PROJECT/TESTS resolve to the bound project profile's
    // typecheck/build/test command and run it as a deterministic Gate 4 after the stage produces.
    val buildExpectation: BuildExpectation = BuildExpectation.NONE,
    // Concrete (non-glob) workspace-relative paths this stage declares it will produce (Gate 2
    // Option A, staged-verification §3.2). The contract gate checks each declared file exists and
    // satisfies its inferred-kind contract, so a file the plan promised but the stage never wrote
    // FAILS the gate — catching missing files, which the runtime write-manifest alone (it only
    // inspects files that WERE written) cannot. Derived from the concrete entries of PlanStage.writes.
    val expectedFiles: List<String> = emptyList(),
    // Declared scope for this stage (architecture-conformance, design 2026-07-14): the
    // workspace-relative globs the stage's intent is confined to (e.g. a frontend-client stage
    // declares `frontend/**`). Distinct from [writeManifest], which is DERIVED from the concrete
    // `writes` and so cannot contradict them: `touches` is the architect's separate statement of
    // intent, and the plan compiler rejects a plan whose declared `writes` escape it (scope creep
    // caught at plan time, before any code is written). Also fed to the semantic reviewer so it can
    // judge goal-conformance against the declared scope. Empty = no scope constraint (default).
    val touches: List<String> = emptyList(),
    // Opt-in to the Gate 3 semantic (LLM) reviewer for this stage (staged-verification § Gate 3). When
    // true and a reviewer is wired, the reviewer reads this stage's produced files after the
    // deterministic funnel passes and raises advisory PR-comment findings; a high-confidence
    // correctness FAIL can block retryably until the review-block budget is spent. Default off.
    val semanticReview: Boolean = false,
    // Deterministic build-gate floor (staged-verification §Gate 4). Set by the compiler on the plan's
    // write-declaring plan stage when no stage declares an explicit [buildExpectation]. The LLM planner emits every
    // file-writing stage as the generic `file_written` kind (never a typed code kind) and rarely sets
    // build_expectation, so a plan that scaffolds real code would otherwise never hit an execution gate.
    // When this is set and NONE is declared, runExecutionGate promotes to a PROJECT build ONLY IF the
    // session actually wrote a code module (decided at run time from the FileWritten manifest + path→kind
    // inference), so a docs-only plan is left untouched. Explicit build_expectation always wins.
    val autoBuildGate: Boolean = false,
    val metadata: Map<String, String> = emptyMap(),
) {
    /**
     * The tools actually permitted at run time: the stage's declared [allowedTools] plus the
     * always-available read-only tools ([ALWAYS_AVAILABLE_READ_TOOLS]). A stage that grants any tool
     * can always inspect the workspace (read a file, list a dir) without the planner having to
     * remember to list them — this is why a write-less verifier could `shell ls -R` (flooding its
     * own context) instead of a scoped `list_dir`. A stage that declares NO tools stays toolless (a
     * pure-reasoning stage is left as designed — read tools would flip it into tool-calling mode).
     */
    val effectiveAllowedTools: Set<String>
        get() = if (allowedTools.isEmpty()) allowedTools else allowedTools + ALWAYS_AVAILABLE_READ_TOOLS

    companion object {
        /** Read-only tools every tool-granting stage may call regardless of its declared set.
         *  `tool_output` is deliberately NOT here — it can only retrieve output that has actually been
         *  spilled to CAS, so the orchestrator adds it on demand (once a spill has occurred) rather than
         *  advertising a hash-eating tool to every stage that will never spill (context noise that nudges
         *  models into reasoning about opaque hashes). */
        val ALWAYS_AVAILABLE_READ_TOOLS: Set<String> =
            setOf("file_read", "list_dir", "glob", "grep")
    }
}
