import { readdir, readFile, stat as fsStat } from "fs/promises";
import { join } from "path";
import { execFile, execSync } from "child_process";
import { promisify } from "util";
import { getOmkPath, pathExists, getProjectRoot, getRunPath, getRunsDir } from "../util/fs.js";
import { getProviderUsage } from "../util/provider-usage.js";
import { buildUsageViewModel } from "../util/usage-view-model.js";
import { getOmkResourceSettings } from "../util/resource-profile.js";
import { formatBytes } from "../util/output-buffer.js";
import { formatOmkVersionFooter } from "../util/version.js";
import { t } from "../util/i18n.js";
import { parseRunStateResult, buildRunViewModel, renderRunSummary, sanitizeForDisplay, } from "../util/run-view-model.js";
import { style, status, panel, gauge, stat, omkHudHeader, gradient, getSystemUsage, separator, padEndAnsi, sanitizeTerminalText, } from "../util/theme.js";
import { loadTodos } from "../util/todo-sync.js";
import { readSessionMeta } from "../util/session.js";
const execFileAsync = promisify(execFile);
function formatUptime(seconds) {
    const h = Math.floor(seconds / 3600);
    const m = Math.floor((seconds % 3600) / 60);
    const s = seconds % 60;
    return `${h}h ${m}m ${s}s`;
}
function getDiskUsage() {
    try {
        const raw = execSync("df -k .", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"], timeout: 5000 });
        const line = raw.trim().split("\n").pop();
        if (!line)
            return null;
        const parts = line.trim().split(/\s+/);
        if (parts.length < 6)
            return null;
        const total = parseInt(parts[1], 10) * 1024;
        const used = parseInt(parts[2], 10) * 1024;
        const percent = Math.round((used / total) * 100);
        return { used, total, percent };
    }
    catch {
        return null;
    }
}
export function parseGitStatusPorcelain(output) {
    return output
        .split(/\r?\n/)
        .map((line) => line.trimEnd())
        .filter(Boolean)
        .map((line) => {
        const statusCode = line.slice(0, 2);
        const rawPath = line.slice(3).trim();
        const renamedPath = rawPath.includes(" -> ") ? rawPath.split(" -> ").pop() ?? rawPath : rawPath;
        return {
            status: statusCode.trim() || statusCode,
            path: renamedPath.replace(/^"|"$/g, ""),
        };
    });
}
async function getGitChanges(root) {
    try {
        const { stdout } = await execFileAsync("git", ["status", "--porcelain=v1", "--untracked-files=normal"], {
            cwd: root,
            timeout: 5000,
            maxBuffer: 1024 * 1024,
            encoding: "utf-8",
        });
        return parseGitStatusPorcelain(String(stdout));
    }
    catch {
        return null;
    }
}
function lineWidth(line) {
    return sanitizeTerminalText(line).length;
}
function blockWidth(block) {
    return Math.max(0, ...block.split("\n").map(lineWidth));
}
function truncateText(value, maxLength) {
    const clean = sanitizeTerminalText(value).replace(/\s+/g, " ").trim();
    const chars = [...clean];
    if (chars.length <= maxLength)
        return clean;
    return `${chars.slice(0, Math.max(1, maxLength - 1)).join("")}…`;
}
function statusRank(statusValue) {
    switch (statusValue) {
        case "running": return 0;
        case "pending": return 1;
        case "blocked": return 2;
        case "failed": return 3;
        case "skipped": return 4;
        case "done": return 5;
        default: return 6;
    }
}
function todoMarker(statusValue) {
    switch (statusValue) {
        case "running": return style.purpleBold("▶");
        case "done": return style.mintBold("✓");
        case "failed": return style.red("✕");
        case "blocked": return style.orange("■");
        case "skipped": return style.gray("⊘");
        default: return style.gray("□");
    }
}
function gitMarker(changeStatus) {
    const normalized = changeStatus.replace(/\s/g, "");
    if (normalized === "??")
        return style.blue("?");
    if (normalized.includes("D"))
        return style.red("D");
    if (normalized.includes("A"))
        return style.mint("A");
    if (normalized.includes("R"))
        return style.purple("R");
    return style.orange("M");
}
function truncateLine(line, maxWidth) {
    const clean = sanitizeTerminalText(line);
    if (clean.length <= maxWidth)
        return line;
    const visible = clean.slice(0, Math.max(1, maxWidth - 1));
    return visible + "…";
}
function healthColor(health) {
    switch (health) {
        case "ok": return style.mint;
        case "warn": return style.orange;
        case "blocked": return style.orange;
        case "failed": return style.red;
        default: return style.gray;
    }
}
function formatProviderCounts(metrics) {
    return Object.entries(metrics.byProvider)
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([provider, count]) => `${provider}=${count}`)
        .join(", ");
}
function formatProviderMetricLine(metrics) {
    if (metrics.attempts <= 0)
        return null;
    const counts = formatProviderCounts(metrics);
    const fallback = metrics.fallbackCount > 0 ? ` · fallback ${metrics.fallbackCount}` : "";
    return `${metrics.attempts} attempt${metrics.attempts === 1 ? "" : "s"}${counts ? ` · ${counts}` : ""}${fallback}`;
}
function todoItemMarker(statusValue) {
    switch (statusValue) {
        case "in_progress": return style.purpleBold("▶");
        case "done": return style.mintBold("✓");
        case "failed": return style.red("✕");
        case "blocked": return style.orange("■");
        case "skipped": return style.gray("⊘");
        case "pending": return style.gray("□");
        default: return style.gray("□");
    }
}
function sessionStatusColor(statusValue) {
    switch (statusValue) {
        case "active": return style.mint;
        case "completed": return style.mintBold;
        case "failed": return style.red;
        case "idle": return style.gray;
        default: return style.gray;
    }
}
function stateErrorRecovery(error, runId) {
    switch (error) {
        case "corrupt": return `omk summary-show ${runId ?? "<run-id>"}`;
        case "missing": return `omk run --run-id ${runId ?? "<id>"}`;
        case "invalid":
        case "oldSchema":
            return `omk verify --run ${runId ?? "<run-id>"}`;
        default:
            return "";
    }
}
async function loadGoalData(goalId) {
    const goalsBase = getOmkPath("goals");
    const goalPath = join(goalsBase, goalId, "goal.json");
    const evidencePath = join(goalsBase, goalId, "evidence.json");
    try {
        const goalContent = await readFile(goalPath, "utf-8");
        const goal = JSON.parse(goalContent);
        if (!goal || typeof goal !== "object" || !goal.title)
            return null;
        let evidence = [];
        try {
            const evContent = await readFile(evidencePath, "utf-8");
            evidence = JSON.parse(evContent);
        }
        catch {
            // no evidence file
        }
        const criteria = (goal.successCriteria ?? []).filter((c) => c.requirement === "required");
        const requiredTotal = criteria.length;
        const requiredPassed = criteria.filter((c) => evidence.some((e) => e.criterionId === c.id && e.passed)).length;
        const firstUnmet = criteria.find((c) => !evidence.some((e) => e.criterionId === c.id && e.passed));
        const latestEvidenceAt = evidence.length > 0
            ? evidence.sort((a, b) => b.checkedAt.localeCompare(a.checkedAt))[0].checkedAt
            : undefined;
        return {
            title: goal.title,
            status: goal.status,
            requiredTotal,
            requiredPassed,
            firstUnmet: firstUnmet ? { id: firstUnmet.id, description: firstUnmet.description } : undefined,
            latestEvidenceAt,
        };
    }
    catch {
        return null;
    }
}
function buildSummaryBar(vm, stateError, _goalTitle, width) {
    if (stateError !== "ok") {
        const warning = `⚠ Latest Run state is ${stateError}. Run: ${stateErrorRecovery(stateError, vm.runId)}`;
        return truncateLine(warning, width);
    }
    const summary = renderRunSummary(vm);
    return truncateLine(summary, width);
}
function buildStateErrorPanel(stateError, runId) {
    const recovery = stateErrorRecovery(stateError, runId);
    const lines = [
        "",
        `  ${style.orangeBold("⚠ State:")} ${style.orange(stateError)}`,
        `  ${style.gray("Suggested recovery:")}`,
        recovery ? `    ${style.cream(recovery)}` : "",
        "",
    ].filter(Boolean);
    return panel(lines, gradient("Run State Warning"));
}
export function buildHudSidebar(state, changes, options = {}) {
    const maxTodos = options.maxTodos ?? 9;
    const maxFiles = options.maxFiles ?? 12;
    const lines = ["", `  ${style.pinkBold("Right Rail")}`];
    const vm = options.viewModel ?? (state ? buildRunViewModel(state) : null);
    const nodes = [...(state?.nodes ?? [])].sort((a, b) => {
        const rank = statusRank(a.status) - statusRank(b.status);
        return rank !== 0 ? rank : a.id.localeCompare(b.id);
    });
    if (vm) {
        lines.push(`  ${style.gray("run")} ${style.creamBold(truncateText(vm.runId ?? "--", 34))}`);
        lines.push(`  ${style.gray("progress")} ${style.mintBold(`${vm.progress.settled}/${vm.progress.total}`)} ${style.gray(`settled, ${vm.progress.running} active`)}`);
        if (vm.progress.skipped > 0) {
            lines.push(`  ${style.gray("skipped")} ${style.gray(`${vm.progress.skipped}`)}`);
        }
        if (vm.health !== "ok") {
            lines.push(`  ${style.gray("health")} ${healthColor(vm.health)(vm.health.toUpperCase())}`);
        }
        const providerLine = formatProviderMetricLine(vm.providerRouting);
        if (providerLine) {
            lines.push(`  ${style.gray("provider")} ${style.cream(providerLine)}`);
        }
        lines.push("");
    }
    else if (state) {
        const done = nodes.filter((node) => node.status === "done").length;
        const skipped = nodes.filter((node) => node.status === "skipped").length;
        const failed = nodes.filter((node) => node.status === "failed").length;
        const blocked = nodes.filter((node) => node.status === "blocked").length;
        const active = nodes.filter((node) => node.status === "running").length;
        lines.push(`  ${style.gray("run")} ${style.creamBold(truncateText(state.runId, 34))}`);
        lines.push(`  ${style.gray("progress")} ${style.mintBold(`${done + skipped + failed + blocked}/${nodes.length}`)} ${style.gray(`settled, ${active} active`)}`);
        lines.push("");
    }
    if (state?.routeDecision) {
        lines.push(`  ${style.gray("route")} ${style.cream(truncateText(state.routeDecision.intent, 28))}`);
        lines.push(`  ${style.gray("mode")} ${style.cream(state.routeDecision.mode)}`);
        lines.push("");
    }
    const capabilityEntries = Object.entries(state?.capabilityAssignments ?? {});
    if (capabilityEntries.length > 0) {
        lines.push(`  ${style.pinkBold("CAPABILITY")}`);
        for (const [nodeId, assignment] of capabilityEntries.slice(0, 4)) {
            const prefix = truncateText(sanitizeForDisplay(nodeId), 12);
            if (assignment.skills?.length)
                lines.push(`  ${style.gray(prefix)} ${style.cream(truncateText(`skills:${assignment.skills.join(",")}`, 42))}`);
            if (assignment.hooks?.length)
                lines.push(`  ${style.gray(prefix)} ${style.cream(truncateText(`hooks:${assignment.hooks.join(",")}`, 42))}`);
            if (assignment.mcpServers?.length)
                lines.push(`  ${style.gray(prefix)} ${style.cream(truncateText(`mcp:${assignment.mcpServers.join(",")}`, 42))}`);
        }
        lines.push("");
    }
    lines.push(`  ${style.pinkBold("TODO")}`);
    if (options.todos) {
        const todos = [...options.todos].sort((a, b) => statusRank(a.status) - statusRank(b.status));
        if (todos.length === 0) {
            lines.push(`  ${style.gray("TODO 없음")}`);
        }
        else {
            for (const todo of todos.slice(0, maxTodos)) {
                const role = todo.role ? style.gray(`[${truncateText(sanitizeForDisplay(todo.role), 9)}]`) : "";
                const name = truncateText(sanitizeForDisplay(todo.title), 30);
                lines.push(`  ${todoItemMarker(todo.status)} ${role} ${name}`.replace(/\s+/g, " ").trimEnd());
            }
            if (todos.length > maxTodos) {
                lines.push(`  ${style.gray(`… ${todos.length - maxTodos} more todos`)}`);
            }
        }
    }
    else if (nodes.length === 0) {
        lines.push(`  ${style.gray(t("hud.noActiveRun"))}`);
        lines.push(`  ${style.gray(t("hud.runToSeeTodos"))}`);
    }
    else {
        for (const node of nodes.slice(0, maxTodos)) {
            const role = style.gray(`[${truncateText(sanitizeForDisplay(node.role), 9)}]`);
            const name = truncateText(sanitizeForDisplay(node.name || node.id), 30);
            lines.push(`  ${todoMarker(node.status)} ${role} ${name}`);
        }
        if (nodes.length > maxTodos) {
            lines.push(`  ${style.gray(`… ${nodes.length - maxTodos} more tasks`)}`);
        }
    }
    if (vm && vm.workers.length > 0) {
        lines.push("", `  ${style.pinkBold("AGENTS")}`);
        for (const worker of vm.workers.slice(0, Math.max(1, Math.min(5, maxTodos)))) {
            const stateTag = worker.state === "running"
                ? style.purple("▶")
                : worker.state === "done"
                    ? style.mint("✓")
                    : worker.state === "failed"
                        ? style.red("✕")
                        : worker.state === "skipped"
                            ? style.gray("⊘")
                            : style.gray("□");
            const live = worker.liveStatus && worker.liveStatus !== worker.state ? style.gray(` ${worker.liveStatus}`) : "";
            lines.push(`  ${stateTag}${live} ${truncateText(sanitizeForDisplay(worker.label), 32)}`);
        }
    }
    lines.push("", `  ${style.pinkBold("Changed Files")} ${style.gray(`(${changes.length})`)}`);
    if (changes.length === 0) {
        lines.push(`  ${style.mint("✓")} ${style.gray("clean worktree")}`);
    }
    else {
        for (const change of changes.slice(0, maxFiles)) {
            lines.push(`  ${gitMarker(change.status)} ${truncateText(change.path, 38)}`);
        }
        if (changes.length > maxFiles) {
            lines.push(`  ${style.gray(`… ${changes.length - maxFiles} more files`)}`);
        }
    }
    lines.push("");
    const contentLines = options.maxWidth
        ? lines.map((l) => truncateLine(l, Math.max(1, options.maxWidth - 4)))
        : lines;
    return panel(contentLines, gradient("TODO / Changed Files"));
}
export function renderHudColumns(mainPanels, sidebar, terminalWidth = defaultHudTerminalWidth()) {
    const left = mainPanels.join("\n\n");
    const leftLines = left.split("\n");
    const rightLines = sidebar.split("\n");
    const leftWidth = blockWidth(left);
    const rightWidth = blockWidth(sidebar);
    const gap = 3;
    if (terminalWidth < 100 || leftWidth + gap + rightWidth > terminalWidth) {
        return `${left}\n\n${sidebar}`;
    }
    const height = Math.max(leftLines.length, rightLines.length);
    const output = [];
    for (let i = 0; i < height; i += 1) {
        const leftLine = leftLines[i] ?? "";
        const rightLine = rightLines[i] ?? "";
        output.push(`${padEndAnsi(leftLine, leftWidth)}${" ".repeat(gap)}${rightLine}`.trimEnd());
    }
    return output.join("\n");
}
function defaultHudTerminalWidth() {
    const stdoutWidth = process.stdout.columns;
    if (typeof stdoutWidth === "number" && stdoutWidth > 0)
        return stdoutWidth;
    const envWidth = Number.parseInt(process.env.COLUMNS ?? "", 10);
    return Number.isFinite(envWidth) && envWidth > 0 ? envWidth : 120;
}
export function renderHudColumnsWithDetectedWidth(mainPanels, sidebar) {
    return renderHudColumns(mainPanels, sidebar, defaultHudTerminalWidth());
}
function normalizeRefreshMs(refreshMs) {
    if (refreshMs === undefined)
        return 2_000;
    if (!Number.isFinite(refreshMs))
        return 2_000;
    return Math.min(60_000, Math.max(250, Math.round(refreshMs)));
}
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
function renderFooter(refreshMs) {
    const footerLines = [
        separator(50),
        style.mint(t("hud.hint")) + "  " + style.gray("omk chat") + " " + t("hud.interactive") + "  |  " + style.gray("omk plan") + " " + t("hud.plan") + "  |  " + style.gray("omk run") + " " + t("hud.execute") + "  |  " + style.gray("omk merge") + " " + t("hud.merge"),
    ];
    if (refreshMs) {
        footerLines.push(style.gray(t("hud.liveRefresh", refreshMs)));
    }
    footerLines.push(style.gray("  " + formatOmkVersionFooter()), separator(50));
    return footerLines.join("\n");
}
function clearScreen() {
    process.stdout.write("\x1b[2J\x1b[H");
}
function enterAlternateScreen() {
    process.stdout.write("\x1b[?1049h");
}
function leaveAlternateScreen() {
    process.stdout.write("\x1b[?1049l");
}
function runCandidateScore(candidate) {
    let score = 0;
    if (candidate.schemaVersion === 1)
        score += 1;
    if (candidate.name === "latest")
        score -= 8;
    return score;
}
export function selectLatestRunName(candidates) {
    const valid = candidates.filter((candidate) => candidate.hasState || candidate.hasGoal || candidate.hasPlan);
    if (valid.length === 0)
        return null;
    return [...valid]
        .sort((a, b) => {
        const activity = (b.stateUpdatedAtMs ?? b.mtimeMs ?? 0) - (a.stateUpdatedAtMs ?? a.mtimeMs ?? 0);
        if (activity !== 0)
            return activity;
        const mtime = b.mtimeMs - a.mtimeMs;
        if (mtime !== 0)
            return mtime;
        const score = runCandidateScore(b) - runCandidateScore(a);
        if (score !== 0)
            return score;
        return b.name.localeCompare(a.name);
    })[0].name;
}
export async function listRunCandidates(runsDir) {
    const entries = await readdir(runsDir, { withFileTypes: true });
    const dirs = entries.filter((entry) => entry.isDirectory());
    return Promise.all(dirs.map(async (entry) => {
        const runDir = join(runsDir, entry.name);
        const info = await fsStat(runDir).catch(() => null);
        const statePath = getRunPath(entry.name, "state.json");
        const [hasState, hasGoal, hasPlan] = await Promise.all([
            pathExists(statePath),
            pathExists(getRunPath(entry.name, "goal.md")),
            pathExists(getRunPath(entry.name, "plan.md")),
        ]);
        let schemaVersion;
        let stateUpdatedAtMs = info?.mtimeMs ?? 0;
        if (hasState) {
            try {
                const stateContent = await readFile(statePath, "utf-8");
                const parsed = JSON.parse(stateContent);
                if (parsed && typeof parsed === "object") {
                    if (typeof parsed.schemaVersion === "number") {
                        schemaVersion = parsed.schemaVersion;
                    }
                    if (typeof parsed.updatedAt === "string") {
                        const parsedTime = Date.parse(parsed.updatedAt);
                        if (!Number.isNaN(parsedTime)) {
                            stateUpdatedAtMs = parsedTime;
                        }
                    }
                }
            }
            catch {
                // ignore parse errors; fall back to directory mtime
            }
            // If state.json exists but updatedAt is missing, use the file's own mtime
            // so long-running runs with an old directory mtime still rank correctly.
            if (stateUpdatedAtMs === (info?.mtimeMs ?? 0)) {
                try {
                    const stateStat = await fsStat(statePath);
                    stateUpdatedAtMs = stateStat.mtimeMs;
                }
                catch {
                    // ignore
                }
            }
        }
        return {
            name: entry.name,
            mtimeMs: info?.mtimeMs ?? 0,
            stateUpdatedAtMs,
            hasState,
            hasGoal,
            hasPlan,
            schemaVersion,
        };
    }));
}
// ── System panel cache (avoids redundant fs/shell calls within refresh interval) ──
let cachedSystemPanel;
let cachedSystemPanelTime = 0;
async function buildSystemPanel(options = {}) {
    const now = Date.now();
    const refreshMs = options.systemRefreshMs ?? 5000;
    if (cachedSystemPanel && now - cachedSystemPanelTime < refreshMs) {
        return cachedSystemPanel;
    }
    const usage = getSystemUsage();
    const disk = options.showDisk !== false ? getDiskUsage() : null;
    const resources = await getOmkResourceSettings();
    const sysLines = [
        "",
        gauge("CPU Load", usage.cpuPercent, 100, 20),
        gauge("Memory  ", usage.memPercent, 100, 20),
        disk ? gauge("Disk    ", disk.percent, 100, 20) : "",
        "",
        stat("Load Avg", usage.loadAvg.map((v) => v.toFixed(2)).join(", "), ""),
        stat("Memory", `${usage.memUsedGB} / ${usage.memTotalGB}`, " GB"),
        options.showHeap !== false ? stat("Heap", `${usage.heapUsedMB} / ${usage.heapTotalMB}`, " MB") : "",
        options.showHeap !== false ? stat("Heap Ext", `${usage.heapExternalMB}`, " MB") : "",
        stat("Event Loop", `${usage.eventLoopLagMs.toFixed(1)}`, " ms"),
        stat("OMK Buffer", formatBytes(resources.shellMaxBufferBytes), ""),
        disk ? stat("Disk", `${(disk.used / 1024 / 1024 / 1024).toFixed(1)} / ${(disk.total / 1024 / 1024 / 1024).toFixed(1)}`, " GB") : "",
        options.showUptime !== false ? stat("Uptime", formatUptime(usage.uptimeSeconds), "") : "",
        "",
    ].filter(Boolean);
    const result = panel(sysLines, gradient("System Usage"));
    cachedSystemPanel = result;
    cachedSystemPanelTime = now;
    return result;
}
async function buildProviderUsagePanel(providerUsage, fetchQuota = true) {
    const usage = providerUsage ?? await getProviderUsage({ fetchQuota });
    const LIMIT_HOURS = 5;
    const limitSeconds = LIMIT_HOURS * 3600;
    const vm = buildUsageViewModel(usage);
    const fiveHourGaugePercent = vm.fiveHour.percent ??
        Math.min(100, Math.round((usage.totalSecondsLast5Hours / limitSeconds) * 100));
    const weekGaugePercent = vm.weekly.percent ??
        Math.min(100, Math.round((usage.totalSecondsWeek / (limitSeconds * 7)) * 100));
    const loginLine = vm.source === "missingAuth"
        ? stat("OAuth Login", "login required; showing local sessions only", "")
        : stat("OAuth Login", `${vm.accountLabel} (${vm.authStatus})`, "");
    const planLines = [
        "",
        gauge("5h Window", fiveHourGaugePercent, 100, 20),
        gauge("This Week", weekGaugePercent, 100, 20),
        "",
        loginLine,
        stat("5h Usage", vm.fiveHour.label, ""),
        stat("5h Sessions", `${usage.sessionCountLast5Hours}`, ""),
        stat("Today Used", vm.today.label, ""),
        stat("Today Sessions", `${vm.today.sessionCount}`, ""),
        stat("Week Usage", vm.weekly.label, ""),
        stat("Week Sessions", `${usage.sessionCountWeek}`, ""),
        vm.error ? stat("Quota Source", `local fallback (${vm.error})`, "") : "",
        "",
    ].filter(Boolean);
    return panel(planLines, gradient("Provider Usage"));
}
async function buildProjectStatusPanel(gitChangesResult) {
    const root = getProjectRoot();
    const gitChanges = gitChangesResult ?? [];
    const projLines = [""];
    projLines.push(`  ${style.purple("🌿 Git")}      ${gitChangesResult === null ? status.warn("unavailable") : gitChanges.length === 0 ? status.ok("clean") : status.warn(`${gitChanges.length} changes`)}`);
    const [omkExists, agentsMdExists, designMdExists] = await Promise.all([
        pathExists(join(root, ".omk")),
        pathExists(join(root, "AGENTS.md")),
        pathExists(join(root, "DESIGN.md")),
    ]);
    projLines.push(`  ${style.purple("📁 OMK")}      ${omkExists ? status.ok("initialized") : status.warn("omk init needed")}`);
    projLines.push(`  ${style.purple("📝 AGENTS")}   ${agentsMdExists ? status.ok("exists") : status.warn("missing")}`);
    projLines.push(`  ${style.purple("🎨 DESIGN")}   ${designMdExists ? status.ok("exists") : status.info("optional")}`);
    projLines.push("");
    return panel(projLines, gradient("Project Status"));
}
function workerLiveness(ageMs) {
    if (ageMs === undefined)
        return { label: "active", color: style.mint };
    if (ageMs > 180_000)
        return { label: "stale", color: style.red };
    if (ageMs > 90_000)
        return { label: "quiet", color: style.orange };
    if (ageMs > 30_000)
        return { label: "slow", color: style.cream };
    return { label: "active", color: style.mint };
}
function formatDurationMs(ms) {
    const totalSeconds = Math.max(0, Math.round(ms / 1000));
    const h = Math.floor(totalSeconds / 3600);
    const m = Math.floor((totalSeconds % 3600) / 60);
    const s = totalSeconds % 60;
    if (h > 0)
        return `${h}h ${m}m`;
    if (m > 0)
        return `${m}m ${s}s`;
    return `${s}s`;
}
async function buildLatestRunPanel(options, vm, stateError, _gitChanges, maxWidth, sessionMeta, todos) {
    const runsDir = getRunsDir();
    let runLines = [];
    if (await pathExists(runsDir)) {
        let latestRunName = options?.runId ?? null;
        if (!latestRunName) {
            const runCandidates = await listRunCandidates(runsDir);
            latestRunName = selectLatestRunName(runCandidates);
        }
        if (latestRunName) {
            let goalTitle = null;
            let goalData = null;
            if (vm.runId && stateError === "ok") {
                try {
                    const stateContent = await readFile(getRunPath(latestRunName, "state.json"), "utf-8");
                    const { state } = parseRunStateResult(stateContent);
                    if (state?.goalId) {
                        goalData = await loadGoalData(state.goalId);
                        if (goalData)
                            goalTitle = goalData.title;
                    }
                    if (!goalData && state?.goalSnapshot?.title) {
                        goalTitle = state.goalSnapshot.title;
                    }
                }
                catch {
                    // ignore
                }
            }
            const [goalMd, plan] = await Promise.all([
                readFile(getRunPath(latestRunName, "goal.md"), "utf-8").catch(() => null),
                readFile(getRunPath(latestRunName, "plan.md"), "utf-8").catch(() => null),
            ]);
            if (!goalTitle && goalMd) {
                goalTitle = goalMd.replace(/# Goal\n\n?/, "").split("\n")[0].trim();
            }
            if (!goalTitle)
                goalTitle = "N/A";
            // ── Top summary bar: Run / Goal / Health / Progress / ETA / Stale count ──
            const staleWorkers = vm.workers.filter((w) => w.state === "running" && (w.lastActivityAgeMs ?? 0) > 90_000).length;
            const chatBadge = sessionMeta?.type === "chat" ? ` ${style.cream("💬 Chat")}` : "";
            const providerLine = formatProviderMetricLine(vm.providerRouting);
            runLines = [
                "",
                `  ${style.gray("Run:")}    ${style.creamBold(latestRunName)}${chatBadge}`,
                `  ${style.gray("Goal:")}    ${style.cream(goalTitle)}`,
                `  ${style.gray("Health:")}  ${healthColor(vm.health)(vm.health.toUpperCase())}`,
                `  ${style.gray("Progress:")} ${style.mintBold(`${vm.progress.settled}/${vm.progress.total}`)} ${style.gray(`(${vm.progress.percent}%)`)}`,
            ];
            if (providerLine) {
                runLines.push(`  ${style.gray("Provider:")} ${style.cream(providerLine)}`);
            }
            if (vm.eta) {
                runLines.push(`  ${style.gray("ETA:")}     ${style.cream(vm.eta)}`);
            }
            if (staleWorkers > 0) {
                runLines.push(`  ${style.gray("Stale:")}   ${style.redBold(`${staleWorkers} worker${staleWorkers > 1 ? "s" : ""}`)}`);
            }
            if (sessionMeta) {
                const statusColor = sessionStatusColor(sessionMeta.status);
                runLines.push(`  ${style.gray("Session:")} ${statusColor(sessionMeta.status.toUpperCase())}`);
                if (sessionMeta.todoCount > 0) {
                    runLines.push(`  ${style.gray("Todos:")}   ${style.mintBold(`${sessionMeta.todoDoneCount}/${sessionMeta.todoCount}`)} ${style.gray("completed")}`);
                }
            }
            runLines.push("");
            if (goalData) {
                if (goalData.status) {
                    runLines.push(`  ${style.gray("Goal Status:")} ${style.creamBold(goalData.status)}`);
                }
                if (goalData.requiredTotal > 0) {
                    runLines.push(`  ${style.gray("Criteria:")}   ${style.mintBold(`${goalData.requiredPassed}/${goalData.requiredTotal}`)} ${style.gray("required passed")}`);
                }
                if (goalData.firstUnmet) {
                    runLines.push(`  ${style.gray("Next Up:")}    ${style.orange(truncateText(goalData.firstUnmet.description, 50))}`);
                }
                runLines.push("");
            }
            else if (vm.goalTitle) {
                if (vm.goalScore != null) {
                    runLines.push(`  ${style.gray("Score:")}     ${style.mintBold(`${vm.goalScore}%`)}`);
                }
                runLines.push("");
            }
            if (vm.teamRuntime) {
                const team = vm.teamRuntime;
                const presentWindows = team.windows.filter((w) => w.status === "present").length;
                const missingWindows = team.windows.filter((w) => w.status === "missing").length;
                runLines.push(`  ${style.pinkBold("Team Runtime")}`);
                runLines.push(`    ${style.gray("session")} ${style.cream(team.session)} ${style.gray("status")} ${healthColor(missingWindows > 0 ? "warn" : "ok")(team.status)}`);
                runLines.push(`    ${style.gray("windows")} ${style.mintBold(`${presentWindows}/${team.windows.length}`)} ${style.gray(`present · workers ${team.workerCount} · reviewer ${team.reviewerCount}`)}`);
                if (team.coordinatorPanes > 0) {
                    runLines.push(`    ${style.gray("coordinator panes")} ${style.mint(String(team.coordinatorPanes))}`);
                }
                if (missingWindows > 0) {
                    runLines.push(`    ${style.orange(`${missingWindows} expected window(s) missing`)}`);
                }
                runLines.push("");
            }
            // ── Worker table ──
            if (vm.workers.length > 0) {
                runLines.push(`  ${style.pinkBold("Workers")}`);
                for (const w of vm.workers) {
                    const live = workerLiveness(w.lastActivityAgeMs);
                    const stateTag = w.state === "running"
                        ? `${live.color("▶")} ${live.color(w.state)}`
                        : w.state === "done"
                            ? `${style.mintBold("✓")} ${style.mint(w.state)}`
                            : w.state === "failed"
                                ? `${style.red("✕")} ${style.red(w.state)}`
                                : w.state === "skipped"
                                    ? `${style.gray("⊘")} ${style.gray(w.state)}`
                                    : `${style.gray("□")} ${style.gray(w.state)}`;
                    const elapsed = w.elapsedMs > 0 ? style.gray(` · ${formatDurationMs(w.elapsedMs)}`) : "";
                    const activity = w.lastActivityAgeMs != null
                        ? style.gray(` · activity ${formatDurationMs(w.lastActivityAgeMs)} ago`)
                        : "";
                    const phaseLine = w.phase ? style.gray(` · ${truncateText(w.phase, 50)}`) : "";
                    runLines.push(`    ${stateTag}${elapsed}${activity}${phaseLine}`);
                    runLines.push(`      ${style.gray(truncateText(w.label, 40))}`);
                }
                runLines.push("");
            }
            // ── Blockers / Next Action ──
            if (vm.blocker) {
                runLines.push(`  ${style.redBold("Blocker:")} ${style.red(vm.blocker.reason)}`);
                runLines.push(`  ${style.gray("Action:")}  ${style.cream(vm.blocker.nextAction)}`);
                runLines.push("");
            }
            else if (vm.nextAction && vm.nextAction !== "Ready") {
                runLines.push(`  ${style.gray("Next:")}    ${style.cream(vm.nextAction)}`);
                runLines.push("");
            }
            if (plan) {
                runLines.push(`  ${style.gray("Plan:")}    ${style.mint("✔ generated")}`);
                runLines.push("");
            }
            // ── TODO mini-section ──
            if (!options.compact && todos && todos.length > 0) {
                runLines.push(`  ${style.pinkBold("TODOs")}`);
                for (const todo of todos.slice(0, 5)) {
                    const marker = todoItemMarker(todo.status);
                    const title = truncateText(todo.title, 50);
                    runLines.push(`    ${marker} ${title}`);
                }
                if (todos.length > 5) {
                    runLines.push(`    ${style.gray(`… ${todos.length - 5} more`)}`);
                }
                runLines.push("");
            }
        }
    }
    if (runLines.length === 0) {
        runLines = ["", `  ${style.gray(t("hud.noRunHistory"))}`, ""];
    }
    if (maxWidth) {
        const contentMaxWidth = Math.max(1, maxWidth - 4);
        runLines = runLines.map((l) => truncateLine(l, contentMaxWidth));
    }
    return panel(runLines, gradient("Latest Run"));
}
async function renderCompactDashboard(options) {
    const width = options.terminalWidth ?? defaultHudTerminalWidth();
    const effectiveWidth = Math.max(40, width - 4);
    const root = getProjectRoot();
    const gitChangesResult = await getGitChanges(root);
    const gitChanges = gitChangesResult ?? [];
    const runsDir = getRunsDir();
    let vm = buildRunViewModel(null);
    let stateError = "missing";
    let latestRunName = options.runId ?? null;
    if (await pathExists(runsDir)) {
        if (!latestRunName) {
            const candidates = await listRunCandidates(runsDir);
            latestRunName = selectLatestRunName(candidates);
        }
        if (latestRunName) {
            const stateContent = await readFile(getRunPath(latestRunName, "state.json"), "utf-8").catch(() => null);
            if (stateContent) {
                const result = parseRunStateResult(stateContent);
                stateError = result.error;
                vm = buildRunViewModel(result.state, { changedFiles: gitChanges.map((c) => c.path) });
            }
        }
    }
    let sessionMeta = null;
    let todos = null;
    if (latestRunName) {
        sessionMeta = await readSessionMeta(latestRunName).catch(() => null);
        todos = await loadTodos(latestRunName).catch(() => null);
    }
    const goalTitle = vm.goalTitle ?? null;
    const output = [];
    output.push(omkHudHeader(options.runId));
    const summary = buildSummaryBar(vm, stateError, goalTitle, effectiveWidth);
    output.push(summary);
    output.push("");
    const systemPanel = await buildSystemPanel(options);
    output.push(systemPanel);
    const runPanel = await buildLatestRunPanel(options, vm, stateError, gitChanges, effectiveWidth, sessionMeta, todos);
    output.push(runPanel);
    output.push("");
    if (stateError !== "ok" && latestRunName) {
        output.push(buildStateErrorPanel(stateError, latestRunName));
        output.push("");
    }
    const sidebar = buildHudSidebar(null, gitChanges, { viewModel: vm, maxWidth: effectiveWidth, todos });
    output.push(sidebar);
    output.push("");
    output.push(renderFooter(options.footerRefreshMs));
    output.push("");
    return output.join("\n");
}
async function renderMediumDashboard(options) {
    const width = options.terminalWidth ?? defaultHudTerminalWidth();
    const root = getProjectRoot();
    const gitChangesResult = await getGitChanges(root);
    const gitChanges = gitChangesResult ?? [];
    const mainPanels = [];
    const output = [];
    output.push(omkHudHeader(options.runId));
    const runsDir = getRunsDir();
    let vm = buildRunViewModel(null);
    let stateError = "missing";
    let latestRunName = options.runId ?? null;
    if (await pathExists(runsDir)) {
        if (!latestRunName) {
            const candidates = await listRunCandidates(runsDir);
            latestRunName = selectLatestRunName(candidates);
        }
        if (latestRunName) {
            const stateContent = await readFile(getRunPath(latestRunName, "state.json"), "utf-8").catch(() => null);
            if (stateContent) {
                const result = parseRunStateResult(stateContent);
                stateError = result.error;
                vm = buildRunViewModel(result.state, { changedFiles: gitChanges.map((c) => c.path) });
            }
        }
    }
    let sessionMeta = null;
    let todos = null;
    if (latestRunName) {
        sessionMeta = await readSessionMeta(latestRunName).catch(() => null);
        todos = await loadTodos(latestRunName).catch(() => null);
    }
    const goalTitle = vm.goalTitle ?? null;
    const summary = buildSummaryBar(vm, stateError, goalTitle, width - 4);
    output.push(summary);
    output.push("");
    const projPanel = await buildProjectStatusPanel(gitChangesResult);
    mainPanels.push(projPanel);
    const runPanel = await buildLatestRunPanel(options, vm, stateError, gitChanges, undefined, sessionMeta, todos);
    mainPanels.push(runPanel);
    if (stateError !== "ok" && latestRunName) {
        mainPanels.push(buildStateErrorPanel(stateError, latestRunName));
    }
    const sidebar = buildHudSidebar(null, gitChanges, { viewModel: vm, todos });
    output.push(renderHudColumns(mainPanels, sidebar, width));
    output.push("");
    output.push(renderFooter(options.footerRefreshMs));
    output.push("");
    return output.join("\n");
}
async function renderFullDashboard(options) {
    const width = options.terminalWidth ?? defaultHudTerminalWidth();
    const root = getProjectRoot();
    const gitChangesResult = await getGitChanges(root);
    const gitChanges = gitChangesResult ?? [];
    const mainPanels = [];
    const output = [];
    output.push(omkHudHeader(options.runId));
    const runsDir = getRunsDir();
    let vm = buildRunViewModel(null);
    let stateError = "missing";
    let latestRunName = options.runId ?? null;
    if (await pathExists(runsDir)) {
        if (!latestRunName) {
            const candidates = await listRunCandidates(runsDir);
            latestRunName = selectLatestRunName(candidates);
        }
        if (latestRunName) {
            const stateContent = await readFile(getRunPath(latestRunName, "state.json"), "utf-8").catch(() => null);
            if (stateContent) {
                const result = parseRunStateResult(stateContent);
                stateError = result.error;
                vm = buildRunViewModel(result.state, { changedFiles: gitChanges.map((c) => c.path) });
            }
        }
    }
    let sessionMeta = null;
    let todos = null;
    if (latestRunName) {
        sessionMeta = await readSessionMeta(latestRunName).catch(() => null);
        todos = await loadTodos(latestRunName).catch(() => null);
    }
    const goalTitle = vm.goalTitle ?? null;
    const summary = buildSummaryBar(vm, stateError, goalTitle, width - 4);
    output.push(summary);
    output.push("");
    mainPanels.push(await buildSystemPanel(options));
    mainPanels.push(await buildProviderUsagePanel(options.providerUsage, options.fetchQuota ?? true));
    mainPanels.push(await buildProjectStatusPanel(gitChangesResult));
    const runPanel = await buildLatestRunPanel(options, vm, stateError, gitChanges, undefined, sessionMeta, todos);
    mainPanels.push(runPanel);
    if (stateError !== "ok" && latestRunName) {
        mainPanels.push(buildStateErrorPanel(stateError, latestRunName));
    }
    const sidebar = buildHudSidebar(null, gitChanges, { viewModel: vm, todos });
    output.push(renderHudColumns(mainPanels, sidebar, width));
    output.push("");
    output.push(renderFooter(options.footerRefreshMs));
    output.push("");
    return output.join("\n");
}
async function renderSectionDashboard(options) {
    const width = options.terminalWidth ?? defaultHudTerminalWidth();
    const root = getProjectRoot();
    const gitChangesResult = await getGitChanges(root);
    const gitChanges = gitChangesResult ?? [];
    const output = [];
    output.push(omkHudHeader(options.runId));
    const runsDir = getRunsDir();
    let vm = buildRunViewModel(null);
    let stateError = "missing";
    let latestRunName = options.runId ?? null;
    if (await pathExists(runsDir)) {
        if (!latestRunName) {
            const candidates = await listRunCandidates(runsDir);
            latestRunName = selectLatestRunName(candidates);
        }
        if (latestRunName) {
            const stateContent = await readFile(getRunPath(latestRunName, "state.json"), "utf-8").catch(() => null);
            if (stateContent) {
                const result = parseRunStateResult(stateContent);
                stateError = result.error;
                vm = buildRunViewModel(result.state, { changedFiles: gitChanges.map((c) => c.path) });
            }
        }
    }
    let sessionMeta = null;
    let todos = null;
    if (latestRunName) {
        sessionMeta = await readSessionMeta(latestRunName).catch(() => null);
        todos = await loadTodos(latestRunName).catch(() => null);
    }
    const goalTitle = vm.goalTitle ?? null;
    const summary = buildSummaryBar(vm, stateError, goalTitle, width - 4);
    output.push(summary);
    output.push("");
    switch (options.section) {
        case "run": {
            const runPanel = await buildLatestRunPanel(options, vm, stateError, gitChanges, undefined, sessionMeta, todos);
            output.push(runPanel);
            output.push("");
            if (stateError !== "ok" && latestRunName) {
                output.push(buildStateErrorPanel(stateError, latestRunName));
                output.push("");
            }
            const sidebar = buildHudSidebar(null, gitChanges, { viewModel: vm, todos });
            output.push(sidebar);
            break;
        }
        case "project": {
            const projPanel = await buildProjectStatusPanel(gitChangesResult);
            output.push(projPanel);
            break;
        }
        case "resources": {
            output.push(await buildSystemPanel(options));
            output.push("");
            output.push(await buildProviderUsagePanel(options.providerUsage, options.fetchQuota ?? true));
            break;
        }
    }
    output.push("");
    output.push(renderFooter(options.footerRefreshMs));
    output.push("");
    return output.join("\n");
}
export async function renderHudDashboard(options = {}) {
    const width = options.terminalWidth ?? defaultHudTerminalWidth();
    if (options.section) {
        return renderSectionDashboard(options);
    }
    if (options.compact || width < 90) {
        return renderCompactDashboard(options);
    }
    if (width < 120) {
        return renderMediumDashboard(options);
    }
    return renderFullDashboard(options);
}
export async function hudCommand(options = {}) {
    const refreshMs = normalizeRefreshMs(options.refreshMs);
    if (!options.watch) {
        console.log(await renderHudDashboard(options));
        return;
    }
    let stopped = false;
    const stop = () => {
        stopped = true;
    };
    process.once("SIGINT", stop);
    process.once("SIGTERM", stop);
    let cachedUsage;
    let lastUsageRefreshMs = 0;
    const usageRefreshMs = Math.max(60_000, refreshMs);
    const useAlternateScreen = options.alternateScreen ?? false;
    const shouldClear = !(options.noClear ?? false) && (options.clear ?? true);
    if (useAlternateScreen) {
        enterAlternateScreen();
    }
    try {
        let lastFrame = "";
        while (!stopped) {
            const now = Date.now();
            if (!cachedUsage || now - lastUsageRefreshMs >= usageRefreshMs) {
                cachedUsage = await getProviderUsage();
                lastUsageRefreshMs = now;
            }
            const frame = await renderHudDashboard({ ...options, providerUsage: cachedUsage, footerRefreshMs: refreshMs });
            if (frame !== lastFrame) {
                lastFrame = frame;
                if (shouldClear)
                    clearScreen();
                process.stdout.write(frame + "\n");
            }
            if (stopped)
                break;
            await sleep(refreshMs);
        }
    }
    finally {
        process.off("SIGINT", stop);
        process.off("SIGTERM", stop);
        if (useAlternateScreen) {
            leaveAlternateScreen();
        }
    }
    process.stdout.write("\n");
}
