package com.correx.core.kernel.orchestration

// Context-entry builders kept top-level (not orchestrator members) for unit-testability
// and to stay under detekt's function-count threshold (same pattern as isBackEdge).

import com.correx.core.artifacts.kind.ArtifactKind
import com.correx.core.events.events.RepoKnowledgeHit
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.EntryRole
import com.correx.core.events.events.FailureTicketOpenedEvent
import com.correx.core.events.events.InitialIntentEvent
import com.correx.core.events.events.RefinementIterationEvent
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.ContextEntryId
import com.correx.core.events.types.StageId
import com.correx.core.sessions.BoundAgentInstructions
import com.correx.core.sessions.BoundProjectProfile
import com.correx.core.transitions.graph.WorkflowGraph
import java.util.UUID

fun buildRetryFeedbackEntry(events: List<StoredEvent>, stageId: StageId): ContextEntry? {
    val latest = events
        .mapNotNull { it.payload as? RetryAttemptedEvent }
        .lastOrNull { it.stageId == stageId } ?: return null
    val content = "## Retry feedback\n" +
        "Attempt ${latest.attemptNumber} of ${latest.maxAttempts} for stage '${stageId.value}'. " +
        "The previous attempt failed: ${latest.failureReason}\n" +
        "Address the failure cause directly. Do not repeat the identical approach."
    return ContextEntry(
        id = ContextEntryId(UUID.randomUUID().toString()),
        layer = ContextLayer.L1,
        content = content,
        sourceType = "retryFeedback",
        sourceId = stageId.value,
        tokenEstimate = content.length / 4,
        role = EntryRole.SYSTEM,
    )
}

/**
 * Feeds the open failure ticket into the recovery stage's context. The recovery stage was entered
 * by kernel routing (not a normal edge) precisely because an upstream write-less stage failed a gate
 * it lacked the capability to fix; without the ticket the recovery stage has no idea what to repair.
 * Surfaces the failing stage, the gate, and the captured gate output (evidence, recorded at
 * ticket-open per invariant #9) so recovery can act on recorded fact, not a re-observation.
 */
fun buildRecoveryTicketEntry(events: List<StoredEvent>, stageId: StageId): ContextEntry? {
    val ticket = events
        .mapNotNull { it.payload as? FailureTicketOpenedEvent }
        .lastOrNull { it.routeTo == stageId } ?: return null
    val content = if (ticket.escalated) {
        // Tier 2: the owner loop couldn't settle it — a cross-file contract dispute. The arbiter holds
        // the intent and reconciles ALL sides in one pass.
        val intent = events.mapNotNull { it.payload as? InitialIntentEvent }.lastOrNull()?.intent
        "## Contract arbitration ticket\n" +
            "Stage '${ticket.stageId.value}' keeps failing the '${ticket.gate}' gate even after the " +
            "file owners repaired their own layers — so this is NOT a bug in one file. The files named " +
            "in the gate output below disagree on a shared contract (a prop/type/API shape): each side " +
            "is internally consistent but they do not agree with each other, so fixing one re-breaks " +
            "the other. You are the arbiter. Read EVERY file named below, decide the single authoritative " +
            "shape" + (intent?.let { " that serves the intent" } ?: "") + ", then edit ALL of them in " +
            "ONE pass so they agree. Do not rewrite files wholesale — change only what the contract " +
            "requires. Then control returns to verification." +
            (intent?.let { "\n### Intent (authoritative)\n$it" } ?: "") +
            "\n### Gate output\n${ticket.evidence}"
    } else {
        "## Recovery ticket\n" +
            "Stage '${ticket.stageId.value}' failed the '${ticket.gate}' gate (${ticket.category}) and " +
            "lacks the '${ticket.requiredCapability}' capability to fix it, so the failure was routed to " +
            "you — you own the file(s) the gate names below. Make the SMALLEST change that clears the " +
            "specific error: read the current file, then patch only the offending lines (file_edit). Do " +
            "NOT rewrite the file from scratch — a full regeneration discards your prior working code and " +
            "usually reintroduces the same defect. Then control returns to verification.\n" +
            "### Gate output\n${ticket.evidence}"
    }
    return ContextEntry(
        id = ContextEntryId(UUID.randomUUID().toString()),
        layer = ContextLayer.L1,
        content = content,
        sourceType = "recoveryTicket",
        sourceId = stageId.value,
        tokenEstimate = content.length / 4,
        role = EntryRole.SYSTEM,
    )
}

/**
 * The remaining-delta checklist (stage-termination design 2026-07-11 §1a). A forward-looking,
 * shrinking list of the stage's *unmet* file-contract assertions — "what is left to make true",
 * not "what I have done". Computed by the kernel as contract-minus-current-filesystem-reality and
 * refreshed within the tool loop each time a write lands on a contract file, so the model watches
 * items disappear as it satisfies them; an empty list is the completion signal the stage otherwise
 * lacks. Reading files never changes it, which is what starves the re-read spiral.
 *
 * Pinned as a neverDrop entry by [com.correx.core.context.builder.DefaultContextPackBuilder] so the
 * budget/dedup passes can never evict it — its whole value is surviving the truncation that gave the
 * model amnesia in the first place. Returns null when nothing is failing (list empty ⇒ done).
 *
 * [items] are (target, assertionId, evidence) triples of the currently-failing assertions.
 */
fun buildRemainingDeltaEntry(items: List<Triple<String, String, String>>): ContextEntry? {
    if (items.isEmpty()) return null
    val content = buildString {
        append("## Remaining to finish this stage\n")
        append(
            "Make each item below true, then call stage_complete. Items disappear from this list " +
                "as you satisfy them; when it is empty the stage is done. This list is recomputed " +
                "from the filesystem — re-reading files does NOT change it, only writing/editing the " +
                "named files does. Fix exactly these, do not re-explore.\n",
        )
        items.forEach { (target, assertionId, evidence) ->
            append("- [ ] ").append(target).append(" — ").append(assertionId)
            if (evidence.isNotBlank()) append(": ").append(evidence)
            append("\n")
        }
    }.trimEnd()
    return ContextEntry(
        id = ContextEntryId(UUID.randomUUID().toString()),
        layer = ContextLayer.L1,
        content = content,
        sourceType = "remainingDelta",
        sourceId = "remaining-delta",
        tokenEstimate = content.length / 4,
        role = EntryRole.SYSTEM,
    )
}

fun criticArtifactIds(
    events: List<StoredEvent>,
    graph: WorkflowGraph,
    stageId: StageId,
): Set<ArtifactId> {
    val cycle = events
        .mapNotNull { it.payload as? RefinementIterationEvent }
        .lastOrNull { it.cycleKey.endsWith("->${stageId.value}") } ?: return emptySet()
    val fromStage = StageId(cycle.cycleKey.substringBefore("->"))
    return graph.stages[fromStage]?.produces?.map { it.name }?.toSet() ?: emptySet()
}

fun buildArtifactKindVocabularyEntry(kinds: Collection<ArtifactKind>): ContextEntry {
    val listing = kinds.joinToString(", ") { k ->
        if (k.llmEmitted) "${k.id} (llm-emitted)" else k.id
    }
    val content = "## Available artifact kinds\n" +
        "Every stage's \"kind\" MUST be exactly one of: $listing\n" +
        "Stages that write or edit files use \"file_written\"; stages that run commands use " +
        "\"process_result\".\n" +
        "Do not invent kinds — a plan referencing an unknown kind fails to compile."
    return ContextEntry(
        id = ContextEntryId(UUID.randomUUID().toString()),
        layer = ContextLayer.L1,
        content = content,
        sourceType = "artifactKindVocabulary",
        sourceId = "artifact-kinds",
        tokenEstimate = content.length / 4,
        role = EntryRole.SYSTEM,
    )
}

fun buildRelevantFilesEntry(hits: List<RepoKnowledgeHit>): ContextEntry {
    val content = buildString {
        append("## Relevant files (retrieved by semantic similarity)\n")
        hits.map { it.text }.distinct().forEach { append("- ").append(it).append("\n") }
    }.trimEnd()
    return ContextEntry(
        id = ContextEntryId(UUID.randomUUID().toString()),
        layer = ContextLayer.L3,
        content = content,
        sourceType = "relevantFiles",
        sourceId = "repo-knowledge",
        tokenEstimate = content.length / 4,
        role = EntryRole.SYSTEM,
    )
}

fun buildProjectProfileEntry(profile: BoundProjectProfile): ContextEntry {
    val content = buildString {
        append("## Project profile\n")
        if (profile.about.isNotBlank()) append(profile.about).append("\n")
        if (profile.conventions.isNotEmpty()) {
            append("### Conventions\n")
            profile.conventions.forEach { append("- ").append(it).append("\n") }
        }
        if (profile.commands.isNotEmpty()) {
            append("### Commands\n")
            profile.commands.forEach { (name, cmd) -> append(name).append(": ").append(cmd).append("\n") }
        }
    }.trimEnd()
    return ContextEntry(
        id = ContextEntryId(UUID.randomUUID().toString()),
        layer = ContextLayer.L0,
        content = content,
        sourceType = "projectProfile",
        sourceId = "project-profile",
        tokenEstimate = content.length / 4,
        role = EntryRole.SYSTEM,
    )
}

// CLAUDE.md / AGENTS.md injected as L0 standing context (feat/backlog-burndown).
fun buildAgentInstructionsEntry(instructions: BoundAgentInstructions): ContextEntry {
    val content = instructions.content
    return ContextEntry(
        id = ContextEntryId(UUID.randomUUID().toString()),
        layer = ContextLayer.L0,
        content = content,
        sourceType = "agentInstructions",
        sourceId = "agent-instructions",
        tokenEstimate = content.length / 4,
        role = EntryRole.SYSTEM,
    )
}
