import { readFile, readdir, stat } from "fs/promises";
import { join } from "path";
import { style, header, status, label } from "../util/theme.js";
import { getProjectRoot, pathExists, readTextFile, getOmkPath, getRunPath, getRunsDir } from "../util/fs.js";
import { createDag } from "../orchestration/dag.js";
import { createStatePersister } from "../orchestration/state-persister.js";
import { checkEvidenceGates } from "../orchestration/evidence-gate.js";
import { createExecutor } from "../orchestration/executor.js";
import { createExecutableDagFromState, routeRunState } from "../orchestration/run-state.js";
import { createOmkSessionEnv } from "../util/session.js";
import { getOmkResourceSettings, getActiveRuntimePreset } from "../util/resource-profile.js";
import { listWorktrees } from "../util/worktree.js";
import { appendEvent } from "../util/events-logger.js";
import { t } from "../util/i18n.js";
import { createProviderBackedTaskRunner } from "../providers/provider-runtime.js";
export async function dagValidateCommand(filePath) {
    const root = getProjectRoot();
    const target = filePath ?? join(root, ".omk", "dag.json");
    if (!(await pathExists(target))) {
        throw new Error(`DAG definition not found: ${target}`);
    }
    const content = await readFile(target, "utf-8");
    let def;
    try {
        def = JSON.parse(content);
    }
    catch {
        throw new Error("Invalid JSON in DAG definition");
    }
    if (!def || typeof def !== "object" || !("nodes" in def)) {
        throw new Error("DAG definition must have a 'nodes' array");
    }
    try {
        createDag(def);
        console.log(status.ok("DAG is valid"));
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        throw new Error(`DAG validation failed: ${message}`);
    }
}
export async function dagShowCommand(runId) {
    const root = getProjectRoot();
    const persister = createStatePersister(getRunsDir(root));
    const state = await persister.load(runId);
    if (!state) {
        console.error(status.error(`Run not found: ${runId}`));
        process.exit(1);
    }
    console.log(header(`DAG Run: ${runId}`));
    console.log(label("Started", state.startedAt));
    if (state.completedAt) {
        console.log(label("Completed", state.completedAt));
    }
    console.log(label("Nodes", String(state.nodes.length)));
    console.log("");
    const statusColor = (s) => {
        switch (s) {
            case "done": return style.mint(s);
            case "running": return style.purple(s);
            case "failed": return style.pink(s);
            case "blocked": return style.skin(s);
            case "skipped": return style.gray(s);
            default: return style.blue(s);
        }
    };
    const rows = state.nodes.map((node) => [
        node.id,
        node.name,
        node.role,
        statusColor(node.status),
        node.dependsOn.join(", ") || "—",
        node.durationMs ? `${node.durationMs}ms` : "—",
        node.retries ? String(node.retries) : "—",
    ]);
    console.log(renderTable(["ID", "Name", "Role", "Status", "Deps", "Duration", "Retries"], rows));
    if (state.estimate) {
        console.log("");
        console.log(label("Progress", `${state.estimate.percentComplete}%`));
        console.log(label("ETA", state.estimate.estimatedCompletedAt ?? "—"));
        console.log(label("Confidence", state.estimate.confidence));
    }
    // Evidence summary
    const nodesWithEvidence = state.nodes.filter((n) => n.evidence && n.evidence.length > 0);
    if (nodesWithEvidence.length > 0) {
        console.log("");
        console.log(style.purpleBold("Evidence Gates"));
        for (const node of nodesWithEvidence) {
            for (const ev of node.evidence) {
                const icon = ev.passed ? "✓" : "✗";
                const color = ev.passed ? style.mint : style.pink;
                console.log(color(`  ${icon} ${node.id}: ${ev.gate}${ev.ref ? ` (${ev.ref})` : ""} — ${ev.message}`));
            }
        }
    }
}
export async function dagReplayCommand(runId, target, subtarget, options) {
    const root = getProjectRoot();
    const resolvedRunId = runId === "latest" ? await resolveLatestRunId(getRunsDir(root)) : runId;
    if (!resolvedRunId) {
        console.error(status.error(t("run.replayNotFound", runId)));
        process.exit(1);
    }
    const runDir = getRunPath(resolvedRunId, undefined, root);
    const persister = createStatePersister(getRunsDir(root));
    const state = await persister.load(resolvedRunId);
    if (!state) {
        console.error(status.error(t("run.replayNotFound", resolvedRunId)));
        process.exit(1);
    }
    // ── Evidence gate replay (backward compatible) ──
    if (target === "evidence" && subtarget === "gate") {
        console.log(header(`Replaying evidence gates for run: ${resolvedRunId}`));
        let passedCount = 0;
        let failedCount = 0;
        for (const node of state.nodes) {
            if (node.status !== "done" || !node.outputs || node.outputs.length === 0)
                continue;
            const gates = [];
            for (const output of node.outputs) {
                switch (output.gate) {
                    case "file-exists":
                        if (output.ref)
                            gates.push({ type: "file-exists", path: output.ref });
                        break;
                    case "test-pass":
                        gates.push({ type: "command-pass", command: output.ref ?? "npm test" });
                        break;
                    case "review-pass":
                    case "summary":
                        gates.push({ type: "summary-present", summaryMarker: output.ref ?? "## Summary" });
                        break;
                    default:
                        break;
                }
            }
            if (gates.length === 0)
                continue;
            const result = await checkEvidenceGates(gates, {
                cwd: root,
                stdout: "",
                nodeId: node.id,
            });
            for (const ev of result.evidence) {
                const icon = ev.passed ? "✓" : "✗";
                const color = ev.passed ? style.mint : style.pink;
                console.log(color(`  ${icon} ${node.id}: ${ev.gate}${ev.ref ? ` (${ev.ref})` : ""} — ${ev.message}`));
                if (ev.passed)
                    passedCount++;
                else
                    failedCount++;
            }
        }
        console.log("");
        console.log(style.mint(`Passed: ${passedCount}`));
        console.log(style.pink(`Failed: ${failedCount}`));
        if (failedCount > 0)
            process.exitCode = 1;
        return;
    }
    // ── DAG replay ──
    const [goalText, planText] = await Promise.all([
        readFile(join(runDir, "goal.md"), "utf-8")
            .then((c) => c.replace(/^# Goal\n\n?/, "").trim())
            .catch(() => ""),
        readFile(join(runDir, "plan.md"), "utf-8").catch(() => ""),
    ]);
    const flowMatch = planText.match(/Flow:\s*(.+)/m);
    const flow = flowMatch ? flowMatch[1].trim() : null;
    const workersMatch = planText.match(/Workers:\s*(\d+)/m);
    const workerCount = normalizeReplayWorkerCount(workersMatch?.[1]);
    // Determine which nodes to reset
    const nodesToReset = new Set();
    let mode = "full";
    if (options?.node) {
        mode = "node";
        const targetNode = state.nodes.find((n) => n.id === options.node);
        if (!targetNode) {
            console.error(status.error(t("run.replayNodeNotFound", options.node)));
            process.exit(1);
        }
        nodesToReset.add(options.node);
        addTransitiveDependents(state.nodes, options.node, nodesToReset);
    }
    else if (options?.fromFailure) {
        mode = "from-failure";
        const failedNodes = state.nodes.filter((n) => n.status === "failed" || n.status === "blocked");
        if (failedNodes.length === 0) {
            console.error(status.error(t("run.replayNoFailedNodes")));
            process.exit(1);
        }
        for (const node of failedNodes) {
            nodesToReset.add(node.id);
            addTransitiveDependents(state.nodes, node.id, nodesToReset);
        }
    }
    else {
        // Full replay: reset all non-bootstrap nodes
        for (const node of state.nodes) {
            if (node.id !== "bootstrap")
                nodesToReset.add(node.id);
        }
    }
    // Recover worktrees
    let worktreePaths = [];
    try {
        worktreePaths = await listWorktrees(resolvedRunId);
    }
    catch {
        // ignore
    }
    // Reset selected nodes
    for (const node of state.nodes) {
        if (!nodesToReset.has(node.id))
            continue;
        node.status = "pending";
        node.retries = 0;
        node.startedAt = undefined;
        node.completedAt = undefined;
        node.durationMs = undefined;
        node.attempts = undefined;
        node.evidence = undefined;
        node.blockedReason = undefined;
        node.thinking = undefined;
        const existingWt = worktreePaths.find((wt) => wt.endsWith(`/${node.id}`));
        if (existingWt) {
            node.worktree = existingWt;
        }
    }
    const statePath = join(runDir, "state.json");
    const routedState = routeRunState(state, workerCount);
    await persister.save(routedState);
    if (options?.dryRun) {
        console.log(header(`[DRY RUN] Replay DAG run: ${resolvedRunId}`));
        console.log(label("Mode", mode));
        if (mode === "node")
            console.log(label("Target node", options.node));
        console.log(label("Nodes reset", String(nodesToReset.size)));
        console.log("");
        const resetIds = state.nodes
            .filter((n) => nodesToReset.has(n.id))
            .map((n) => `  - ${n.id}: ${n.name} (${n.role})${n.worktree ? ` [worktree: ${n.worktree}]` : ""}`);
        console.log(style.cream("Nodes that would be replayed:"));
        console.log(resetIds.join("\n"));
        return;
    }
    // ── Reconstruct prompt ──
    const resources = await getOmkResourceSettings();
    const flowPath = flow ? getOmkPath(`flows/${flow}/SKILL.md`) : null;
    const kimiFlowPath = flow ? join(root, ".kimi/skills", `omk-flow-${flow}`, "SKILL.md") : null;
    let resolvedFlowPath = null;
    if (flowPath && (await pathExists(flowPath))) {
        resolvedFlowPath = flowPath;
    }
    else if (kimiFlowPath && (await pathExists(kimiFlowPath))) {
        resolvedFlowPath = kimiFlowPath;
    }
    const flowContent = resolvedFlowPath ? await readTextFile(resolvedFlowPath) : "";
    const promptLines = [
        `Flow: ${flow ?? "unknown"}`,
        `Goal: ${goalText}`,
        `Run ID: ${resolvedRunId}`,
        `Resource profile: ${resources.profile}`,
        `Worker budget: ${workerCount}`,
        ``,
        `🔁 REPLAY MODE: This is a forensic replay of a previous run.`,
    ];
    if (mode === "node") {
        promptLines.push(`Target node: ${options?.node} (and dependents)`);
    }
    else if (mode === "from-failure") {
        promptLines.push(`Target: failed/blocked nodes (and dependents)`);
    }
    else {
        promptLines.push(`Target: full replay`);
    }
    promptLines.push(``, t("run.planPrompt"), ``, `SKILLS & MCP USAGE (MANDATORY):`, `- Activate relevant skills from the routing hints for each node.`, `- Use MCP servers (omk-project, memory, quality-gate, etc.) when they fit the task.`, `- Prefer omk-project MCP tools for checkpoint, memory, and run-state operations.`, `- Use SearchWeb / FetchURL for external docs, official APIs, or citations.`, ``, flowContent);
    const promptText = promptLines.join("\n");
    // ── Set up DAG and executor ──
    const dag = createExecutableDagFromState(routedState);
    const executor = createExecutor({
        persister: createStatePersister(getRunsDir(root)),
        resumeFromState: routedState,
        eventRunDir: runDir,
    });
    // Wire event logging
    executor.onNodeStart?.((node) => {
        void appendEvent(runDir, { type: "node-start", runId: resolvedRunId, nodeId: node.id });
    });
    executor.onNodeComplete?.((node, result) => {
        void appendEvent(runDir, {
            type: "node-complete",
            runId: resolvedRunId,
            nodeId: node.id,
            data: { success: result.success, exitCode: result.exitCode },
        });
    });
    executor.onStateChange((s) => {
        void appendEvent(runDir, {
            type: "state-change",
            runId: resolvedRunId,
            data: {
                doneCount: s.nodes.filter((n) => n.status === "done").length,
                failedCount: s.nodes.filter((n) => n.status === "failed").length,
                blockedCount: s.nodes.filter((n) => n.status === "blocked").length,
            },
        });
    });
    // ── Create runner ──
    const agentFile = getOmkPath("agents/root.yaml");
    const providerPolicy = normalizeProviderPolicy(options?.provider);
    const activePreset = await getActiveRuntimePreset();
    const runner = await createProviderBackedTaskRunner({
        providerPolicy,
        deepseekPromptPrefix: buildReplayDeepSeekPromptPrefix({
            goalText,
            runId: resolvedRunId,
            mode,
            targetNode: options?.node,
        }),
        allowDeepSeekAdvisoryFileNodes: true,
        kimi: {
            cwd: root,
            timeout: 0,
            agentFile,
            promptPrefix: promptText,
            mcpScope: resources.mcpScope,
            skillsScope: resources.skillsScope,
            roleAgentFiles: true,
            mcpNames: activePreset?.mcpServers ?? [],
            skillNames: activePreset?.skills ?? [],
            hookNames: activePreset?.hooks ?? [],
            toolNames: [],
            env: {
                ...createOmkSessionEnv(root, resolvedRunId),
                OMK_RUN_ID: resolvedRunId,
                OMK_FLOW: flow ?? "",
                OMK_GOAL: goalText,
                OMK_WORKERS: String(workerCount),
                OMK_DAG_ROUTING: "1",
                OMK_DAG_STATE_PATH: statePath,
                OMK_MCP_SCOPE: resources.mcpScope,
                OMK_SKILLS_SCOPE: resources.skillsScope,
            },
        },
    });
    // ── Emit replay-start event ──
    await appendEvent(runDir, {
        type: "replay-start",
        runId: resolvedRunId,
        data: { mode, targetNode: options?.node },
    });
    console.log(header(`Replaying DAG run: ${resolvedRunId}`));
    console.log(label("Mode", mode));
    console.log(label("Workers", String(workerCount)));
    console.log(label("Provider policy", providerPolicy));
    console.log(label("Nodes reset", String(nodesToReset.size)));
    console.log("");
    const approvalPolicy = await loadApprovalPolicy(root);
    const result = await executor.execute(dag, runner, {
        runId: resolvedRunId,
        workers: workerCount,
        approvalPolicy,
    });
    // ── Emit replay-end event ──
    await appendEvent(runDir, {
        type: "replay-end",
        runId: resolvedRunId,
        data: { success: result.success },
    });
    console.log("");
    if (result.success) {
        console.log(status.ok("Replay complete"));
    }
    else {
        console.log(status.error("Replay failed"));
        process.exitCode = 1;
    }
    console.log(label("State", statePath));
}
// ── Helpers ──
export function normalizeReplayWorkerCount(value) {
    if (!value || !/^\d+$/.test(value))
        return 1;
    const parsed = Number.parseInt(value, 10);
    if (!Number.isFinite(parsed) || parsed < 1)
        return 1;
    return Math.min(parsed, 6);
}
async function resolveLatestRunId(runsDir) {
    try {
        const entries = await readdir(runsDir, { withFileTypes: true });
        const dirs = entries.filter((e) => e.isDirectory());
        if (dirs.length === 0)
            return null;
        const withMtime = await Promise.all(dirs.map(async (d) => {
            const st = await stat(join(runsDir, d.name, "state.json")).catch(() => null);
            return { name: d.name, mtime: st?.mtimeMs ?? 0 };
        }));
        withMtime.sort((a, b) => b.mtime - a.mtime);
        return withMtime[0].name;
    }
    catch {
        return null;
    }
}
function addTransitiveDependents(nodes, startId, out) {
    const dependents = new Map();
    for (const node of nodes) {
        for (const dep of node.dependsOn) {
            const arr = dependents.get(dep) ?? [];
            arr.push(node.id);
            dependents.set(dep, arr);
        }
    }
    const queue = [startId];
    const visited = new Set();
    while (queue.length > 0) {
        const current = queue.shift();
        if (visited.has(current))
            continue;
        visited.add(current);
        for (const d of dependents.get(current) ?? []) {
            if (!visited.has(d)) {
                out.add(d);
                queue.push(d);
            }
        }
    }
}
async function loadApprovalPolicy(root) {
    const configPath = join(root, ".omk", "config.toml");
    try {
        const content = await readFile(configPath, "utf-8");
        const match = content.match(/^approval_policy\s*=\s*["']([^"']+)["']/m);
        const value = match?.[1]?.trim().toLowerCase();
        if (value === "interactive" || value === "auto" || value === "yolo" || value === "block") {
            return value;
        }
    }
    catch {
        // ignore missing config
    }
    return "yolo";
}
function renderTable(headers, rows) {
    const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
    const pad = (str, width) => str + " ".repeat(Math.max(0, width - str.length));
    const headerLine = "  " + headers.map((h, i) => style.purpleBold(pad(h, colWidths[i]))).join("  ");
    const separator = "  " + colWidths.map((w) => "─".repeat(w)).join("  ");
    const body = rows.map((row) => "  " + row.map((cell, i) => pad(cell, colWidths[i])).join("  ")).join("\n");
    return [headerLine, separator, body].join("\n");
}
function normalizeProviderPolicy(value) {
    return value === "kimi" ? "kimi" : "auto";
}
function buildReplayDeepSeekPromptPrefix(input) {
    return [
        `Kimi DAG replay context.`,
        `Run ID: ${input.runId}`,
        `Replay mode: ${input.mode}`,
        input.targetNode ? `Target node: ${input.targetNode}` : "",
        `Goal: ${input.goalText}`,
        `DeepSeek is advisory/read-only unless the provider router selects a direct low-risk read node.`,
        `The configured authority provider keeps write, shell, merge, MCP, and final synthesis authority.`,
    ].filter(Boolean).join("\n");
}
