/**
 * Shared RunViewModel — unified state interpretation for HUD and Parallel UI.
 */
const SECRET_KEY_NAMES = "apiKey|api_key|api-key|token|auth_token|auth-token|access_token|access-token|refresh_token|refresh-token|password|secret|client_secret|client-secret|authorization|bearer|private_key|private-key|key";
const SECRET_PATTERN = new RegExp([
    // Explicit key=value / key:"value" patterns
    `\\b(?:${SECRET_KEY_NAMES})\\s*[:=]\\s*["']?[^\\s"']{8,}["']?`,
    // Hex keys (32+ hex chars)
    `\\b[a-f0-9]{32,}\\b`,
    // Base64 keys (40+ base64 chars with optional padding)
    `\\b[A-Za-z0-9+/]{40,}=?\\b`,
    // GitHub personal access tokens
    `\\bghp_[A-Za-z0-9]{36}\\b`,
    // Railway tokens
    `\\brw_[A-Za-z0-9]+\\b`,
].join("|"), "gi");
export function sanitizeForDisplay(text) {
    return text.replace(SECRET_PATTERN, (match) => {
        const keyMatch = match.match(new RegExp(`\\b(?:${SECRET_KEY_NAMES})\\b`, "i"));
        const key = keyMatch?.[0] ?? "secret";
        return `${key}: ***REDACTED***`;
    });
}
export function parseRunStateResult(content) {
    try {
        const value = JSON.parse(content);
        if (!value || typeof value !== "object") {
            return { state: null, error: "invalid" };
        }
        if (typeof value.runId !== "string") {
            return { state: null, error: "invalid" };
        }
        if (!Array.isArray(value.nodes)) {
            return { state: null, error: "invalid" };
        }
        if (value.schemaVersion !== 1) {
            return { state: null, error: "oldSchema" };
        }
        return { state: value, error: "ok" };
    }
    catch {
        return { state: null, error: "corrupt" };
    }
}
export function getRunStateRecoveryHint(error, runId) {
    switch (error) {
        case "ok":
            return "";
        case "missing":
            return `No run state found. Start a new run with: omk run --run-id ${runId ?? "<run-id>"}`;
        case "invalid":
            return `Run state is invalid. Verify with: omk verify --run ${runId ?? "<run-id>"}`;
        case "corrupt":
            return `Run state file is corrupt. Check the state file or start a new run with: omk run --run-id ${runId ?? "<run-id>"}`;
        case "oldSchema":
            return `Run state schema is outdated or missing. Migrate or restart with: omk verify --run ${runId ?? "<run-id>"}`;
        default:
            return "";
    }
}
export function formatEtaMs(ms) {
    if (ms === undefined || ms < 0)
        return "--";
    const totalSeconds = Math.max(0, Math.round(ms / 1000));
    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;
    if (hours > 0)
        return `${hours}h ${minutes}m`;
    if (minutes > 0)
        return `${minutes}m ${seconds}s`;
    return `${seconds}s`;
}
export function renderRunSummary(vm) {
    const goal = vm.goalTitle ?? "--";
    const progress = `${vm.progress.settled}/${vm.progress.total}`;
    const health = vm.health.toUpperCase();
    const next = vm.nextAction ?? "--";
    const blocker = vm.blocker ? `${vm.blocker.reason} (${vm.blocker.nodeId})` : "none";
    return `Goal: ${goal} | Progress: ${progress} | Health: ${health} | Next: ${next} | Blocker: ${blocker}`;
}
export function buildRunViewModel(state, options = {}) {
    if (!state) {
        return {
            health: "warn",
            goalTitle: options.goalTitle ?? null,
            goalScore: null,
            activeNode: null,
            blocker: null,
            nextAction: null,
            progress: { percent: 0, done: 0, total: 0, running: 0, failed: 0, blocked: 0, skipped: 0, settled: 0 },
            eta: null,
            changedFiles: options.changedFiles ?? [],
            stateError: "missing",
            runId: null,
            workers: [],
            providerRouting: { attempts: 0, fallbackCount: 0, byProvider: {} },
            routeDecision: undefined,
        };
    }
    const nodes = state.nodes;
    const running = nodes.filter((n) => n.status === "running");
    const done = nodes.filter((n) => n.status === "done");
    const failed = nodes.filter((n) => n.status === "failed");
    const blocked = nodes.filter((n) => n.status === "blocked");
    const skipped = nodes.filter((n) => n.status === "skipped");
    const total = nodes.length;
    const settled = done.length + skipped.length + failed.length + blocked.length;
    const percent = total > 0 ? Math.round((settled / total) * 100) : 0;
    const providerRouting = computeProviderRouting(nodes);
    let health = "ok";
    if (failed.length > 0)
        health = "failed";
    else if (blocked.length > 0)
        health = "blocked";
    // Note: running with no pending is normal progress, not a warning.
    // Stale-worker detection is handled in the cockpit renderer via lastActivityAgeMs.
    const activeNode = running[0]
        ? {
            id: running[0].id,
            name: running[0].name || running[0].id,
            role: running[0].role || "unknown",
            thinking: running[0].thinking,
        }
        : null;
    const blockerNode = failed[0] ?? blocked[0];
    let blocker = null;
    if (blockerNode) {
        const lastEvidence = blockerNode.evidence?.length
            ? blockerNode.evidence[blockerNode.evidence.length - 1]
            : undefined;
        const isBlocked = blockerNode.status === "blocked";
        const retryCount = blockerNode.attempts?.length ?? blockerNode.retries ?? 0;
        const maxRetries = blockerNode.maxRetries;
        const recoverable = retryCount < maxRetries;
        const rawReason = isBlocked
            ? (blockerNode.blockedReason ?? "Node blocked")
            : (lastEvidence?.message ?? "Node failed");
        const reason = sanitizeForDisplay(rawReason);
        const nextAction = recoverable
            ? "Retrying..."
            : `omk goal continue ${state.goalId ?? state.runId}`;
        blocker = {
            nodeId: blockerNode.id,
            reason,
            nextAction,
            evidenceMessage: lastEvidence?.message ? sanitizeForDisplay(lastEvidence.message) : undefined,
            recoverable,
            retryCount,
            maxRetries,
            logHint: `Check .omk/runs/${state.runId}/logs/${blockerNode.id}.log`,
        };
    }
    const blockers = [
        ...failed.map((n) => {
            const lastEvidence = n.evidence?.length ? n.evidence[n.evidence.length - 1] : undefined;
            return {
                nodeId: n.id,
                reason: sanitizeForDisplay(lastEvidence?.message ?? "Node failed"),
                status: "failed",
            };
        }),
        ...blocked.map((n) => ({
            nodeId: n.id,
            reason: sanitizeForDisplay(n.blockedReason ?? "Node blocked"),
            status: "blocked",
        })),
    ];
    const nextAction = blocker?.nextAction ??
        (activeNode ? `Waiting for ${activeNode.name}` : settled === total ? "Run complete" : "Ready");
    const isLifecycleNode = (n) => n.id === "bootstrap" || n.id === "root-coordinator" || n.id === "review-merge";
    const now = Date.now();
    const lastActivityMs = state.lastActivityAt ? Date.parse(state.lastActivityAt) : 0;
    const nodeTelemetry = buildNodeTelemetryIndex(options.telemetry ?? []);
    const assignments = new Map();
    const capabilityAssignments = state.capabilityAssignments;
    const assignmentEntries = Array.isArray(capabilityAssignments)
        ? capabilityAssignments.map((assignment) => [assignment.nodeId, assignment])
        : Object.entries(capabilityAssignments ?? {});
    for (const [key, assignment] of assignmentEntries) {
        const nodeKey = assignment.nodeId ?? key;
        if (nodeKey)
            assignments.set(nodeKey, assignment);
        if (assignment.agent)
            assignments.set(assignment.agent, assignment);
        if (assignment.role)
            assignments.set(assignment.role, assignment);
    }
    const mapNodeToWorker = (n) => {
        const elapsed = n.durationMs ?? (n.startedAt ? now - new Date(n.startedAt).getTime() : 0);
        const lastEvidence = n.evidence?.length ? n.evidence[n.evidence.length - 1] : undefined;
        const stateValue = n.status === "pending" ? "idle" :
            n.status === "skipped" ? "skipped" :
                n.status ?? "idle";
        const telemetry = nodeTelemetry.get(n.id);
        const telemetryActivityMs = telemetry?.activityAt ? Date.parse(telemetry.activityAt) : 0;
        const telemetryHeartbeatMs = telemetry?.heartbeatAt ? Date.parse(telemetry.heartbeatAt) : 0;
        const stateHeartbeatMs = state.lastHeartbeatAt ? Date.parse(state.lastHeartbeatAt) : 0;
        const nodeStartedMs = n.startedAt ? Date.parse(n.startedAt) : 0;
        const nodeLastActivityMs = Math.max(lastActivityMs, telemetryActivityMs, nodeStartedMs);
        const nodeLastHeartbeatMs = Math.max(telemetryHeartbeatMs, stateHeartbeatMs, nodeStartedMs);
        const isChatIdle = n.id === "chat" && !n.thinking;
        const lastActivityAgeMs = n.status === "running" && nodeLastActivityMs > 0 && !isChatIdle
            ? now - nodeLastActivityMs
            : undefined;
        const lastHeartbeatAgeMs = n.status === "running" && nodeLastHeartbeatMs > 0 ? now - nodeLastHeartbeatMs : undefined;
        const thinking = n.status === "running" ? n.thinking : undefined;
        const phase = thinking ? sanitizeForDisplay(thinking) : telemetry?.phase ? sanitizeForDisplay(telemetry.phase) : undefined;
        const liveStatus = computeLiveStatus({
            state: stateValue,
            nodeId: n.id,
            hasPhase: Boolean(phase || thinking),
            lastActivityAgeMs,
            lastHeartbeatAgeMs,
            telemetryStatus: telemetry?.status,
        });
        return {
            id: n.id,
            label: options.workerLabels?.[n.id] ?? n.name ?? n.id,
            state: stateValue,
            liveStatus,
            elapsedMs: Math.max(0, elapsed),
            retryCount: n.attempts?.length ?? 0,
            currentNode: n.status === "running" ? (n.name || n.id) : undefined,
            lastEvidence: lastEvidence
                ? { gate: lastEvidence.gate, passed: lastEvidence.passed, message: lastEvidence.message ? sanitizeForDisplay(lastEvidence.message) : undefined }
                : undefined,
            thinking,
            phase,
            lastActivityAgeMs,
            lastHeartbeatAgeMs,
            assignment: assignments.get(n.id) ?? assignments.get(n.role),
        };
    };
    const workers = nodes.filter((n) => !isLifecycleNode(n)).map(mapNodeToWorker);
    const lifecycleNodes = nodes.filter(isLifecycleNode).map(mapNodeToWorker);
    return {
        health,
        goalTitle: options.goalTitle ?? null,
        goalScore: total > 0 ? Math.round((settled / total) * 100) : null,
        activeNode,
        blocker,
        nextAction,
        progress: {
            percent,
            done: done.length,
            total,
            running: running.length,
            failed: failed.length,
            blocked: blocked.length,
            skipped: skipped.length,
            settled,
        },
        eta: state.estimate?.estimatedRemainingMs != null ? formatEtaMs(state.estimate.estimatedRemainingMs) : null,
        etaConfidence: state.estimate?.confidence,
        changedFiles: options.changedFiles ?? [],
        stateError: "ok",
        runId: state.runId,
        workers,
        lifecycleNodes,
        blockers,
        startedAt: state.startedAt,
        lastActivityAt: state.lastActivityAt,
        iterationCount: state.iterationCount,
        maxIterations: state.maxIterations,
        providerRouting,
        routeDecision: state.routeDecision,
        teamRuntime: state.teamRuntime,
    };
}
function buildNodeTelemetryIndex(events) {
    const index = new Map();
    for (const event of events) {
        const nodeId = event.nodeId ?? event.laneId;
        if (!nodeId)
            continue;
        const current = index.get(nodeId) ?? {};
        if (event.type === "lane.activity" || event.type === "tool.started" || event.type === "tool.completed" || event.type === "provider.route") {
            current.activityAt = event.timestamp;
            const phase = event.data?.summary ?? event.data?.phase ?? event.data?.message;
            if (typeof phase === "string")
                current.phase = phase;
        }
        if (event.type === "lane.started") {
            current.activityAt = event.timestamp;
            current.heartbeatAt = event.timestamp;
            current.status = "running-active";
        }
        if (event.type === "lane.heartbeat") {
            current.heartbeatAt = event.timestamp;
        }
        if (event.type === "lane.completed" || event.type === "lane.failed" || event.type === "lane.stalled") {
            current.terminalAt = event.timestamp;
            current.status =
                event.type === "lane.completed" ? "done" :
                    event.type === "lane.failed" ? "failed" :
                        "stalled";
        }
        index.set(nodeId, current);
    }
    return index;
}
function computeLiveStatus(input) {
    if (input.state !== "running")
        return input.state;
    if (input.telemetryStatus === "stalled")
        return "stalled";
    if (input.nodeId === "chat" && !input.hasPhase)
        return "waiting-input";
    const heartbeatFresh = input.lastHeartbeatAgeMs == null || input.lastHeartbeatAgeMs <= 90_000;
    if (!heartbeatFresh)
        return "stalled";
    if (input.lastActivityAgeMs != null && input.lastActivityAgeMs <= 30_000)
        return "running-active";
    return "running-silent";
}
function computeProviderRouting(nodes) {
    const byProvider = {};
    let attempts = 0;
    let fallbackCount = 0;
    for (const node of nodes) {
        for (const attempt of node.attempts ?? []) {
            if (!attempt.provider && !attempt.requestedProvider && !attempt.fallbackFrom)
                continue;
            attempts += 1;
            const provider = attempt.provider ?? attempt.requestedProvider ?? "unknown";
            byProvider[provider] = (byProvider[provider] ?? 0) + 1;
            if (attempt.fallbackFrom)
                fallbackCount += 1;
        }
    }
    return { attempts, fallbackCount, byProvider };
}
