import chalk from "chalk";
import kleur from "kleur";
import fs from "node:fs/promises";
import { renderMarkdownAnsi } from "./markdownRenderer.js";
import { formatFinishLine } from "../oracle/finishLine.js";
import { sessionStore, wait } from "../sessionStore.js";
import { formatTokenCount, formatTokenValue } from "../oracle/runUtils.js";
import { resumeBrowserSession } from "../browser/reattach.js";
import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "../browser/artifacts.js";
import { estimateTokenCount } from "../browser/utils.js";
import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost, } from "./sessionTable.js";
import { abbreviateResponseId, buildResponseOwnerIndex, resolveSessionLineage, } from "./sessionLineage.js";
import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
import { formatBrowserModelSelectionEvidence, formatSessionBrowserModelWithRequestedKey, resolveSessionBrowserModelDisplayName, } from "../browser/modelDisplay.js";
const isTty = () => Boolean(process.stdout.isTTY);
const dim = (text) => (isTty() ? kleur.dim(text) : text);
export const MAX_RENDER_BYTES = 200_000;
function isProcessAlive(pid) {
    if (!pid)
        return false;
    try {
        process.kill(pid, 0);
        return true;
    }
    catch (error) {
        const code = error instanceof Error ? error.code : undefined;
        if (code === "ESRCH" || code === "EINVAL") {
            return false;
        }
        if (code === "EPERM") {
            return true;
        }
        return true;
    }
}
function formatBytes(bytes) {
    if (bytes < 1024)
        return `${bytes} B`;
    if (bytes < 1024 * 1024)
        return `${(bytes / 1024).toFixed(1)} KB`;
    return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
function isDeepResearchBrowserSession(metadata) {
    return metadata.mode === "browser" && metadata.browser?.config?.researchMode === "deep";
}
const DEEP_RESEARCH_TOOL_CALL_MARKERS = [
    "called tool",
    "used tool",
    "użyto narzędzia",
    "narzędzie wywołane",
];
function isDeepResearchToolCallPlaceholder(answerText, outputTokens) {
    const lines = answerText
        .toLowerCase()
        .split(/\r?\n/)
        .map((line) => line.replace(/\s+/g, " ").trim())
        .filter(Boolean);
    if (!lines[0] || !DEEP_RESEARCH_TOOL_CALL_MARKERS.includes(lines[0])) {
        return false;
    }
    if (lines.length === 1) {
        return outputTokens == null || outputTokens <= 8;
    }
    const wrapper = lines.slice(1).join(" ");
    const structuralSignals = [
        wrapper.includes("deep research app"),
        /\bcall tool\b/.test(wrapper),
        /\brequest\s*\{/.test(wrapper),
        /\bresponse\s*\{/.test(wrapper),
        /\bsession[_ ]id\b/.test(wrapper),
    ].filter(Boolean).length;
    return wrapper.includes("deep research app") && structuralSignals >= 2;
}
export function isDeepResearchPlaceholderCapture(metadata, logText) {
    if (/\[reattach\][^\n]*\nAnswer:/i.test(logText)) {
        return false;
    }
    const answer = trimBeforeFirstAnswer(logText).replace(/^Answer:\s*/i, "");
    const modelUsage = metadata.models?.find((run) => run.model === metadata.model)?.usage;
    const outputTokens = metadata.usage?.outputTokens ?? modelUsage?.outputTokens;
    return isDeepResearchToolCallPlaceholder(answer, outputTokens);
}
async function writeReattachAnswer(sessionId, result, replaceExistingLog) {
    const body = result.answerMarkdown || result.answerText;
    if (replaceExistingLog) {
        const paths = await sessionStore.getPaths(sessionId);
        await fs.writeFile(paths.log, `[reattach] replaced incomplete Deep Research capture from existing Chrome tab\nAnswer:\n${body}\n`, "utf8");
        return;
    }
    const logWriter = sessionStore.createLogWriter(sessionId);
    logWriter.logLine("[reattach] captured assistant response from existing Chrome tab");
    logWriter.logLine("Answer:");
    logWriter.logLine(body);
    logWriter.stream.end();
}
async function saveReattachBrowserArtifacts(sessionId, metadata, result) {
    const body = result.answerMarkdown || result.answerText;
    const conversationUrl = metadata.browser?.runtime?.tabUrl;
    const logger = ((message) => console.log(dim(message)));
    const reportArtifact = isDeepResearchBrowserSession(metadata)
        ? await saveDeepResearchReportArtifact({
            sessionId,
            reportMarkdown: body,
            conversationUrl,
            logger,
        }).catch(() => null)
        : null;
    const prompt = (await readStoredPrompt(sessionId)) ?? metadata.promptPreview ?? "";
    const transcriptArtifact = await saveBrowserTranscriptArtifact({
        sessionId,
        prompt,
        answerMarkdown: body,
        conversationUrl,
        artifacts: appendArtifacts(undefined, [reportArtifact]),
        logger,
    }).catch(() => null);
    return appendArtifacts(metadata.artifacts, [reportArtifact, transcriptArtifact]);
}
const CLEANUP_TIP = 'Tip: Run "oracle session --clear --hours 24" to prune cached runs (add --all to wipe everything).';
export async function showStatus({ hours, includeAll, limit, showExamples = false, modelFilter, }) {
    const metas = await sessionStore.listSessions();
    const { entries, truncated, total } = sessionStore.filterSessions(metas, {
        hours,
        includeAll,
        limit,
    });
    const filteredEntries = modelFilter
        ? entries.filter((entry) => matchesModel(entry, modelFilter))
        : entries;
    const richTty = process.stdout.isTTY && chalk.level > 0;
    const responseOwners = buildResponseOwnerIndex(metas);
    if (!filteredEntries.length) {
        console.log(CLEANUP_TIP);
        if (showExamples) {
            printStatusExamples();
        }
        return;
    }
    console.log(chalk.bold("Recent Sessions"));
    console.log(formatSessionTableHeader(richTty));
    const treeRows = buildStatusTreeRows(filteredEntries, responseOwners);
    for (const row of treeRows) {
        const line = formatSessionTableRow(row.entry, { rich: richTty, displaySlug: row.displaySlug });
        const detachedParent = row.detachedParentLabel != null
            ? richTty
                ? chalk.gray(` <- ${row.detachedParentLabel}`)
                : ` <- ${row.detachedParentLabel}`
            : "";
        console.log(`${line}${detachedParent}`);
    }
    if (truncated) {
        const sessionsDir = sessionStore.sessionsDir();
        console.log(chalk.yellow(`Showing ${entries.length} of ${total} sessions from the requested range. Run "oracle session --clear" or delete entries in ${sessionsDir} to free space, or rerun with --status-limit/--status-all.`));
    }
    if (showExamples) {
        printStatusExamples();
    }
}
export async function attachSession(sessionId, options) {
    let metadata = await sessionStore.readSession(sessionId);
    if (!metadata) {
        console.error(chalk.red(`No session found with ID ${sessionId}`));
        process.exitCode = 1;
        return;
    }
    if (metadata.mode === "browser" && metadata.status === "running" && !metadata.browser?.runtime) {
        await wait(250);
        const refreshed = await sessionStore.readSession(sessionId);
        if (refreshed) {
            metadata = refreshed;
        }
    }
    const normalizedModelFilter = options?.model?.trim().toLowerCase();
    if (normalizedModelFilter) {
        const availableModels = metadata.models?.map((model) => model.model.toLowerCase()) ??
            (metadata.model ? [metadata.model.toLowerCase()] : []);
        if (!availableModels.includes(normalizedModelFilter)) {
            console.error(chalk.red(`Model "${options?.model}" not found in session ${sessionId}.`));
            process.exitCode = 1;
            return;
        }
    }
    const initialStatus = metadata.status;
    const wantsRender = Boolean(options?.renderMarkdown);
    const isVerbose = Boolean(process.env.ORACLE_VERBOSE_RENDER);
    const runtime = metadata.browser?.runtime;
    const controllerAlive = isProcessAlive(runtime?.controllerPid);
    const workerAlive = isProcessAlive(metadata.lifecycle?.workerPid);
    const hasChromeDisconnect = metadata.response?.incompleteReason === "chrome-disconnected";
    const hasIncompleteCapture = metadata.response?.incompleteReason === "incomplete-capture";
    const statusAllowsReattach = metadata.status === "running" ||
        (metadata.status === "error" && (hasChromeDisconnect || hasIncompleteCapture));
    const hasFallbackSessionInfo = Boolean(runtime?.chromePort ||
        runtime?.chromeBrowserWSEndpoint ||
        runtime?.chromeProfileRoot ||
        runtime?.tabUrl ||
        runtime?.conversationId);
    const deepResearchPlaceholderCapture = isDeepResearchBrowserSession(metadata) &&
        hasFallbackSessionInfo &&
        isDeepResearchPlaceholderCapture(metadata, await sessionStore.readLog(sessionId).catch(() => ""));
    const completedDeepResearchPlaceholder = metadata.status === "completed" && deepResearchPlaceholderCapture;
    const hasRecoverableConversation = hasRecoverableChatGptConversation(runtime);
    const hasLiveChromeFallback = Boolean((metadata.status === "running" || hasIncompleteCapture || completedDeepResearchPlaceholder) &&
        (runtime?.chromePort || runtime?.chromeBrowserWSEndpoint || runtime?.chromeProfileRoot));
    const canReattach = (statusAllowsReattach || completedDeepResearchPlaceholder) &&
        metadata.mode === "browser" &&
        hasFallbackSessionInfo &&
        !workerAlive &&
        (hasRecoverableConversation ||
            runtime?.promptSubmitted ||
            hasLiveChromeFallback ||
            completedDeepResearchPlaceholder) &&
        (hasChromeDisconnect ||
            hasIncompleteCapture ||
            completedDeepResearchPlaceholder ||
            (runtime?.controllerPid && !controllerAlive));
    if (canReattach) {
        const portInfo = runtime?.chromePort ? `port ${runtime.chromePort}` : "unknown port";
        const urlInfo = runtime?.tabUrl ? `url=${runtime.tabUrl}` : "url=unknown";
        console.log(chalk.yellow(`Attempting to reattach to the existing Chrome session (${portInfo}, ${urlInfo})...`));
        try {
            const result = await resumeBrowserSession(runtime, metadata.browser?.config, Object.assign(((message) => {
                if (message) {
                    console.log(dim(message));
                }
            }), { verbose: true }), { promptPreview: metadata.promptPreview });
            const outputTokens = estimateTokenCount(result.answerMarkdown);
            const artifacts = await saveReattachBrowserArtifacts(sessionId, metadata, result);
            await writeReattachAnswer(sessionId, result, completedDeepResearchPlaceholder ||
                (hasIncompleteCapture && deepResearchPlaceholderCapture));
            if (metadata.model) {
                await sessionStore.updateModelRun(metadata.id, metadata.model, {
                    status: "completed",
                    usage: {
                        inputTokens: 0,
                        outputTokens,
                        reasoningTokens: 0,
                        totalTokens: outputTokens,
                    },
                    completedAt: new Date().toISOString(),
                });
            }
            await sessionStore.updateSession(sessionId, {
                status: "completed",
                completedAt: new Date().toISOString(),
                usage: {
                    inputTokens: 0,
                    outputTokens,
                    reasoningTokens: 0,
                    totalTokens: outputTokens,
                },
                errorMessage: undefined,
                browser: {
                    config: metadata.browser?.config,
                    runtime,
                    modelSelection: metadata.browser?.modelSelection,
                    warnings: metadata.browser?.warnings,
                },
                artifacts,
                response: { status: "completed" },
                error: undefined,
                transport: undefined,
            });
            console.log(chalk.green("Reattach succeeded; session marked completed."));
            metadata = (await sessionStore.readSession(sessionId)) ?? metadata;
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            console.log(chalk.red(`Reattach failed: ${message}`));
            if (completedDeepResearchPlaceholder) {
                if (metadata.model) {
                    await sessionStore.updateModelRun(metadata.id, metadata.model, {
                        status: "error",
                        response: { status: "incomplete", incompleteReason: "incomplete-capture" },
                        error: {
                            category: "browser-automation",
                            message: `Deep Research capture incomplete: ${message}`,
                        },
                    });
                }
                await sessionStore.updateSession(sessionId, {
                    status: "error",
                    errorMessage: `Deep Research capture incomplete: ${message}`,
                    response: { status: "incomplete", incompleteReason: "incomplete-capture" },
                    error: {
                        category: "browser-automation",
                        message: `Deep Research capture incomplete: ${message}`,
                    },
                });
                metadata = (await sessionStore.readSession(sessionId)) ?? metadata;
            }
        }
    }
    if (!options?.suppressMetadata) {
        const reattachLine = buildReattachLine(metadata);
        if (reattachLine) {
            console.log(chalk.blue(reattachLine));
        }
        const chainLine = await buildSessionChainLine(metadata);
        if (chainLine) {
            console.log(dim(`Chain: ${chainLine}`));
        }
        console.log(`Created: ${metadata.createdAt}`);
        console.log(`Status: ${metadata.status}`);
        if (metadata.lifecycle) {
            const attached = metadata.lifecycle.attached ? "attached" : "detached";
            console.log(`Execution: ${formatSessionExecutionLabel(metadata)} (${attached})`);
            console.log(`Reattach: ${metadata.lifecycle.reattachCommand}`);
        }
        if (metadata.models && metadata.models.length > 0) {
            console.log("Models:");
            for (const run of metadata.models) {
                const usage = run.usage
                    ? ` tok=${formatTokenCount(run.usage.outputTokens ?? 0)}/${formatTokenCount(run.usage.totalTokens ?? 0)}`
                    : "";
                const modelLabel = (metadata.mode ?? metadata.options?.mode) === "browser"
                    ? formatSessionBrowserModelWithRequestedKey(metadata, run.model)
                    : run.model;
                console.log(`- ${chalk.cyan(modelLabel)} — ${run.status}${usage}`);
            }
        }
        else if (metadata.model) {
            const modelLabel = (metadata.mode ?? metadata.options?.mode) === "browser"
                ? formatSessionBrowserModelWithRequestedKey(metadata)
                : metadata.model;
            console.log(`Model: ${modelLabel}`);
        }
        const browserEvidence = formatBrowserEvidence(metadata);
        if (browserEvidence) {
            console.log("Browser evidence:");
            for (const line of browserEvidence) {
                console.log(dim(`- ${line}`));
            }
        }
        if (metadata.artifacts && metadata.artifacts.length > 0) {
            console.log("Artifacts:");
            for (const artifact of metadata.artifacts) {
                const label = artifact.label ?? artifact.kind;
                const size = artifact.sizeBytes ? ` (${formatBytes(artifact.sizeBytes)})` : "";
                const checksum = artifact.sha256 ? ` sha256=${artifact.sha256.slice(0, 12)}…` : "";
                const validation = artifact.validation
                    ? ` validation=${artifact.validation.ok ? "ok" : (artifact.validation.error ?? "failed")}`
                    : "";
                const transfer = artifact.transfer?.status ? ` transfer=${artifact.transfer.status}` : "";
                console.log(`- ${chalk.cyan(label)} — ${artifact.path}${size}${checksum}${validation}${transfer}`);
            }
        }
        const responseSummary = formatResponseMetadata(metadata.response);
        if (responseSummary) {
            console.log(dim(`Response: ${responseSummary}`));
        }
        const transportSummary = formatTransportMetadata(metadata.transport);
        if (transportSummary) {
            console.log(dim(`Transport: ${transportSummary}`));
        }
        const userErrorSummary = formatUserErrorMetadata(metadata.error);
        if (userErrorSummary) {
            console.log(dim(`User error: ${userErrorSummary}`));
        }
    }
    const shouldTrimIntro = initialStatus === "completed" || initialStatus === "partial" || initialStatus === "error";
    if (options?.renderPrompt !== false) {
        const prompt = await readStoredPrompt(sessionId);
        if (prompt) {
            console.log(chalk.bold("Prompt:"));
            console.log(renderMarkdownAnsi(prompt));
            console.log(dim("---"));
        }
    }
    if (shouldTrimIntro) {
        const fullLog = await buildSessionLogForDisplay(sessionId, metadata, normalizedModelFilter);
        const trimmed = trimBeforeFirstAnswer(fullLog);
        const size = Buffer.byteLength(trimmed, "utf8");
        const canRender = wantsRender && isTty() && size <= MAX_RENDER_BYTES;
        if (wantsRender && size > MAX_RENDER_BYTES) {
            const msg = `Render skipped (log too large: ${size} bytes > ${MAX_RENDER_BYTES}). Showing raw text.`;
            console.log(dim(msg));
            if (isVerbose) {
                console.log(dim(`Verbose: renderMarkdown=true tty=${isTty()} size=${size}`));
            }
        }
        else if (wantsRender && !isTty()) {
            const msg = "Render requested but stdout is not a TTY; showing raw text.";
            console.log(dim(msg));
            if (isVerbose) {
                console.log(dim(`Verbose: renderMarkdown=true tty=${isTty()} size=${size}`));
            }
        }
        if (canRender) {
            if (isVerbose) {
                console.log(dim(`Verbose: rendering markdown (size=${size}, tty=${isTty()})`));
            }
            process.stdout.write(renderMarkdownAnsi(trimmed));
        }
        else {
            process.stdout.write(trimmed);
        }
        const summary = formatCompletionSummary(metadata, { includeSlug: true });
        if (summary) {
            console.log(`\n${chalk.green.bold(summary)}`);
        }
        if (options?.propagateFailure && metadata.status === "error") {
            process.exitCode = 1;
        }
        return;
    }
    if (wantsRender) {
        console.log(dim("Render will apply after completion; streaming raw text meanwhile..."));
        if (isVerbose) {
            console.log(dim(`Verbose: streaming phase renderMarkdown=true tty=${isTty()}`));
        }
    }
    const liveRenderState = wantsRender && isTty()
        ? {
            pending: "",
            inFence: false,
            inTable: false,
            renderedBytes: 0,
            fallback: false,
            noticedFallback: false,
        }
        : null;
    let lastLength = 0;
    const renderLiveChunk = (chunk) => {
        if (!liveRenderState || chunk.length === 0) {
            process.stdout.write(chunk);
            return;
        }
        if (liveRenderState.fallback) {
            process.stdout.write(chunk);
            return;
        }
        liveRenderState.pending += chunk;
        const { chunks, remainder } = extractRenderableChunks(liveRenderState.pending, liveRenderState);
        liveRenderState.pending = remainder;
        for (const candidate of chunks) {
            const projected = liveRenderState.renderedBytes + Buffer.byteLength(candidate, "utf8");
            if (projected > MAX_RENDER_BYTES) {
                if (!liveRenderState.noticedFallback) {
                    console.log(dim(`Render skipped (log too large: > ${MAX_RENDER_BYTES} bytes). Showing raw text.`));
                    liveRenderState.noticedFallback = true;
                }
                liveRenderState.fallback = true;
                process.stdout.write(candidate + liveRenderState.pending);
                liveRenderState.pending = "";
                return;
            }
            process.stdout.write(renderMarkdownAnsi(candidate));
            liveRenderState.renderedBytes += Buffer.byteLength(candidate, "utf8");
        }
    };
    const flushRemainder = () => {
        if (!liveRenderState || liveRenderState.fallback) {
            return;
        }
        if (liveRenderState.pending.length === 0) {
            return;
        }
        const text = liveRenderState.pending;
        liveRenderState.pending = "";
        const projected = liveRenderState.renderedBytes + Buffer.byteLength(text, "utf8");
        if (projected > MAX_RENDER_BYTES) {
            if (!liveRenderState.noticedFallback) {
                console.log(dim(`Render skipped (log too large: > ${MAX_RENDER_BYTES} bytes). Showing raw text.`));
            }
            process.stdout.write(text);
            liveRenderState.fallback = true;
            return;
        }
        process.stdout.write(renderMarkdownAnsi(text));
    };
    const printNew = async () => {
        const text = await buildSessionLogForDisplay(sessionId, metadata, normalizedModelFilter);
        const nextChunk = text.slice(lastLength);
        if (nextChunk.length > 0) {
            renderLiveChunk(nextChunk);
            lastLength = text.length;
        }
    };
    await printNew();
    // biome-ignore lint/nursery/noUnnecessaryConditions: deliberate infinite poll
    while (true) {
        const latest = await sessionStore.readSession(sessionId);
        if (!latest) {
            break;
        }
        if (latest.status === "completed" || latest.status === "partial" || latest.status === "error") {
            await printNew();
            flushRemainder();
            if (!options?.suppressMetadata) {
                if (latest.status === "error" && latest.errorMessage) {
                    console.log("\nResult:");
                    console.log(`Session failed: ${latest.errorMessage}`);
                }
                if ((latest.status === "completed" || latest.status === "partial") && latest.usage) {
                    const summary = formatCompletionSummary(latest, { includeSlug: true });
                    if (summary) {
                        const color = latest.status === "partial" ? chalk.yellow.bold : chalk.green.bold;
                        console.log(`\n${color(summary)}`);
                    }
                    else {
                        const usage = latest.usage;
                        console.log(`\nFinished (tok i/o/r/t: ${usage.inputTokens}/${usage.outputTokens}/${usage.reasoningTokens}/${usage.totalTokens})`);
                    }
                }
            }
            if (options?.propagateFailure && latest.status === "error") {
                process.exitCode = 1;
            }
            break;
        }
        const controllerPid = latest.lifecycle?.workerPid ?? latest.browser?.runtime?.controllerPid;
        if (latest.lifecycle?.detached && controllerPid && !isProcessAlive(controllerPid)) {
            const settled = await sessionStore.readSession(sessionId);
            if (!settled) {
                break;
            }
            if (settled.status === "completed" || settled.status === "partial") {
                continue;
            }
            await printNew();
            flushRemainder();
            const message = settled.status === "error"
                ? (settled.errorMessage ?? "Detached worker failed.")
                : "Detached worker exited before the session reached a terminal state.";
            const failure = {
                category: "internal",
                message,
            };
            if (settled.model) {
                await sessionStore.updateModelRun(settled.id, settled.model, {
                    status: "error",
                    completedAt: new Date().toISOString(),
                    response: { status: "incomplete", incompleteReason: "incomplete-capture" },
                    error: failure,
                });
            }
            await sessionStore.updateSession(settled.id, {
                status: "error",
                completedAt: new Date().toISOString(),
                errorMessage: message,
                response: { status: "incomplete", incompleteReason: "incomplete-capture" },
                error: failure,
            });
            console.log(chalk.yellow(`${message} Reattach via: ${settled.lifecycle?.reattachCommand}`));
            if (options?.propagateFailure) {
                process.exitCode = 1;
            }
            break;
        }
        await wait(1000);
        await printNew();
    }
}
export function formatResponseMetadata(metadata) {
    if (!metadata) {
        return null;
    }
    const parts = [];
    if (metadata.responseId) {
        parts.push(`response=${metadata.responseId}`);
    }
    if (metadata.requestId) {
        parts.push(`request=${metadata.requestId}`);
    }
    if (metadata.status) {
        parts.push(`status=${metadata.status}`);
    }
    if (metadata.incompleteReason) {
        parts.push(`incomplete=${metadata.incompleteReason}`);
    }
    return parts.length > 0 ? parts.join(" | ") : null;
}
export function formatTransportMetadata(metadata) {
    if (!metadata?.reason) {
        return null;
    }
    const reasonLabels = {
        "client-timeout": "client timeout (deadline exceeded)",
        "connection-lost": "connection lost before completion",
        "client-abort": "request aborted locally",
        unknown: "unknown transport failure",
    };
    const label = reasonLabels[metadata.reason] ?? "transport error";
    return `${metadata.reason} — ${label}`;
}
export function formatUserErrorMetadata(metadata) {
    if (!metadata) {
        return null;
    }
    const parts = [];
    if (metadata.category) {
        parts.push(metadata.category);
    }
    if (metadata.message) {
        parts.push(`message=${metadata.message}`);
    }
    if (metadata.details && Object.keys(metadata.details).length > 0) {
        parts.push(`details=${JSON.stringify(metadata.details)}`);
    }
    return parts.length > 0 ? parts.join(" | ") : null;
}
export function formatBrowserEvidence(metadata) {
    const browser = metadata.browser;
    if (!browser?.modelSelection && (!browser?.warnings || browser.warnings.length === 0)) {
        return null;
    }
    const lines = [];
    const evidence = browser.modelSelection;
    if (evidence) {
        lines.push(`model ${formatBrowserModelSelectionEvidence(evidence, metadata.model)}`);
    }
    for (const warning of browser.warnings ?? []) {
        lines.push(`warning ${warning.code}: ${warning.message}`);
    }
    return lines.length > 0 ? lines : null;
}
export function buildReattachLine(metadata) {
    if (!metadata.id) {
        return null;
    }
    const referenceTime = metadata.startedAt ?? metadata.createdAt;
    if (!referenceTime) {
        return null;
    }
    const elapsedLabel = formatRelativeDuration(referenceTime);
    if (!elapsedLabel) {
        return null;
    }
    if (metadata.status === "running") {
        return `Session ${metadata.id} reattached, request started ${elapsedLabel} ago.`;
    }
    return null;
}
export function trimBeforeFirstAnswer(logText) {
    const marker = "Answer:";
    const index = logText.indexOf(marker);
    if (index === -1) {
        return logText;
    }
    const laterIndex = logText.lastIndexOf(marker);
    const reattachIndex = logText.indexOf("[reattach]", index + marker.length);
    if (laterIndex > index && reattachIndex > index && reattachIndex < laterIndex) {
        const firstCapture = logText.slice(index + marker.length, reattachIndex);
        if (isDeepResearchToolCallPlaceholder(firstCapture)) {
            return logText.slice(laterIndex);
        }
    }
    return logText.slice(index);
}
function formatRelativeDuration(referenceIso) {
    const timestamp = Date.parse(referenceIso);
    if (Number.isNaN(timestamp)) {
        return null;
    }
    const diffMs = Date.now() - timestamp;
    if (diffMs < 0) {
        return null;
    }
    const seconds = Math.max(1, Math.round(diffMs / 1000));
    if (seconds < 60) {
        return `${seconds}s`;
    }
    const minutes = Math.floor(seconds / 60);
    const remainingSeconds = seconds % 60;
    if (minutes < 60) {
        return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
    }
    const hours = Math.floor(minutes / 60);
    const remainingMinutes = minutes % 60;
    if (hours < 24) {
        const parts = [`${hours}h`];
        if (remainingMinutes > 0) {
            parts.push(`${remainingMinutes}m`);
        }
        return parts.join(" ");
    }
    const days = Math.floor(hours / 24);
    const remainingHours = hours % 24;
    const parts = [`${days}d`];
    if (remainingHours > 0) {
        parts.push(`${remainingHours}h`);
    }
    if (remainingMinutes > 0 && days === 0) {
        parts.push(`${remainingMinutes}m`);
    }
    return parts.join(" ");
}
function printStatusExamples() {
    console.log("");
    console.log(chalk.bold("Usage Examples"));
    console.log(`${chalk.bold("  oracle status --hours 72 --limit 50")}`);
    console.log(dim("    Show 72h of history capped at 50 entries."));
    console.log(`${chalk.bold("  oracle status --clear --hours 168")}`);
    console.log(dim("    Delete sessions older than 7 days (use --all to wipe everything)."));
    console.log(`${chalk.bold("  oracle session <session-id>")}`);
    console.log(dim("    Attach to a specific running/completed session to stream its output."));
    console.log(dim(CLEANUP_TIP));
}
function matchesModel(entry, filter) {
    const normalized = filter.trim().toLowerCase();
    if (!normalized) {
        return true;
    }
    const models = entry.models?.map((model) => model.model.toLowerCase()) ??
        (entry.model ? [entry.model.toLowerCase()] : []);
    return models.includes(normalized);
}
function formatLineageParentLabel(lineage) {
    if (!lineage?.parentSessionId) {
        return undefined;
    }
    return lineage.parentResponseId
        ? `${lineage.parentSessionId} (${abbreviateResponseId(lineage.parentResponseId)})`
        : lineage.parentSessionId;
}
function buildStatusTreeRows(entries, responseOwners) {
    const entryById = new Map(entries.map((entry) => [entry.id, entry]));
    const orderIndex = new Map(entries.map((entry, index) => [entry.id, index]));
    const lineageById = new Map();
    const childMap = new Map();
    for (const entry of entries) {
        const lineage = resolveSessionLineage(entry, responseOwners);
        lineageById.set(entry.id, lineage);
        const parentId = lineage?.parentSessionId;
        if (parentId && parentId !== entry.id && entryById.has(parentId)) {
            const siblings = childMap.get(parentId) ?? [];
            siblings.push(entry);
            childMap.set(parentId, siblings);
        }
    }
    for (const siblings of childMap.values()) {
        siblings.sort((a, b) => (orderIndex.get(a.id) ?? 0) - (orderIndex.get(b.id) ?? 0));
    }
    const rows = [];
    const visited = new Set();
    const walkChild = (entry, ancestorHasMore, isLast) => {
        if (visited.has(entry.id)) {
            return;
        }
        visited.add(entry.id);
        const children = childMap.get(entry.id) ?? [];
        const nodeBranch = isLast ? "└─ " : "├─ ";
        const prefix = `${ancestorHasMore.map((hasMore) => (hasMore ? "│  " : "   ")).join("")}${nodeBranch}`;
        rows.push({ entry, displaySlug: `${prefix}${entry.id}` });
        children.forEach((child, index) => {
            walkChild(child, [...ancestorHasMore, !isLast], index === children.length - 1);
        });
    };
    const walkRoot = (entry) => {
        if (visited.has(entry.id)) {
            return;
        }
        visited.add(entry.id);
        const lineage = lineageById.get(entry.id);
        const hiddenParent = lineage?.parentSessionId && !entryById.has(lineage.parentSessionId)
            ? formatLineageParentLabel(lineage)
            : undefined;
        const children = childMap.get(entry.id) ?? [];
        rows.push({ entry, displaySlug: entry.id, detachedParentLabel: hiddenParent });
        children.forEach((child, index) => {
            walkChild(child, [], index === children.length - 1);
        });
    };
    const roots = entries.filter((entry) => {
        const parentId = lineageById.get(entry.id)?.parentSessionId;
        return !(parentId && parentId !== entry.id && entryById.has(parentId));
    });
    roots.forEach((entry) => {
        walkRoot(entry);
    });
    entries.forEach((entry) => {
        walkRoot(entry);
    });
    return rows;
}
async function buildSessionChainLine(metadata) {
    const lineageWithoutLookup = resolveSessionLineage(metadata);
    if (!lineageWithoutLookup) {
        return `root -> ${metadata.id}`;
    }
    if (lineageWithoutLookup.parentSessionId) {
        return `${formatLineageParentLabel(lineageWithoutLookup)} -> ${metadata.id}`;
    }
    if (!lineageWithoutLookup.parentResponseId) {
        return `root -> ${metadata.id}`;
    }
    const sessions = await sessionStore.listSessions().catch(() => []);
    const responseOwners = buildResponseOwnerIndex(sessions);
    const lineage = resolveSessionLineage(metadata, responseOwners) ?? lineageWithoutLookup;
    if (lineage.parentSessionId) {
        return `${formatLineageParentLabel(lineage)} -> ${metadata.id}`;
    }
    if (!lineage.parentResponseId) {
        return `root -> ${metadata.id}`;
    }
    return `${abbreviateResponseId(lineage.parentResponseId)} -> ${metadata.id}`;
}
async function buildSessionLogForDisplay(sessionId, fallbackMeta, modelFilter) {
    const normalizedFilter = modelFilter?.trim().toLowerCase();
    const freshMetadata = (await sessionStore.readSession(sessionId)) ?? fallbackMeta;
    const models = freshMetadata.models ?? fallbackMeta.models ?? [];
    if (models.length === 0) {
        if (normalizedFilter) {
            return await sessionStore.readModelLog(sessionId, modelFilter);
        }
        return await sessionStore.readLog(sessionId);
    }
    const candidates = normalizedFilter
        ? models.filter((model) => model.model.toLowerCase() === normalizedFilter)
        : models;
    if (candidates.length === 0) {
        return "";
    }
    const sections = [];
    let hasContent = false;
    for (const model of candidates) {
        const body = (await sessionStore.readModelLog(sessionId, model.model)) ?? "";
        if (body.trim().length > 0) {
            hasContent = true;
        }
        sections.push(`=== ${model.model} ===\n${body}`.trimEnd());
    }
    if (!hasContent) {
        // Fallback for runs that recorded output only in the session log (e.g., browser runs without per-model logs).
        return await sessionStore.readLog(sessionId);
    }
    return sections.join("\n\n");
}
function extractRenderableChunks(text, state) {
    const chunks = [];
    let buffer = "";
    const lines = text.split(/(\n)/);
    for (let i = 0; i < lines.length; i += 1) {
        const segment = lines[i];
        if (segment === "\n") {
            buffer += segment;
            // Detect code fences
            const prev = lines[i - 1] ?? "";
            const fenceMatch = prev.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
            if (!state.inFence && fenceMatch) {
                state.inFence = true;
                state.fenceDelimiter = fenceMatch[2];
            }
            else if (state.inFence && state.fenceDelimiter && prev.startsWith(state.fenceDelimiter)) {
                state.inFence = false;
                state.fenceDelimiter = undefined;
            }
            const trimmed = prev.trim();
            if (!state.inFence) {
                if (!state.inTable && trimmed.startsWith("|") && trimmed.includes("|")) {
                    state.inTable = true;
                }
                if (state.inTable && trimmed === "") {
                    state.inTable = false;
                }
            }
            const safeBreak = !state.inFence && !state.inTable && trimmed === "";
            if (safeBreak) {
                chunks.push(buffer);
                buffer = "";
            }
            continue;
        }
        buffer += segment;
    }
    return { chunks, remainder: buffer };
}
export function formatCompletionSummary(metadata, options = {}) {
    if (!metadata.usage || metadata.elapsedMs == null) {
        return null;
    }
    const modeLabel = (metadata.mode ?? metadata.options?.mode) === "browser"
        ? `${resolveSessionBrowserModelDisplayName(metadata)}[browser]`
        : (metadata.model ?? "n/a");
    const usage = metadata.usage;
    const cost = resolveSessionCost(metadata);
    const tokensDisplay = [
        usage.inputTokens ?? 0,
        usage.outputTokens ?? 0,
        usage.reasoningTokens ?? 0,
        usage.totalTokens ?? 0,
    ]
        .map((value, index) => formatTokenValue(value, {
        input_tokens: usage.inputTokens,
        output_tokens: usage.outputTokens,
        reasoning_tokens: usage.reasoningTokens,
        total_tokens: usage.totalTokens,
    }, index))
        .join("/");
    const tokensPart = (() => {
        const parts = tokensDisplay.split("/");
        if (parts.length !== 4)
            return tokensDisplay;
        return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
    })();
    const filesCount = metadata.options?.file?.length ?? 0;
    const filesPart = filesCount > 0 ? `files=${filesCount}` : null;
    const slugPart = options.includeSlug ? `slug=${metadata.id}` : null;
    const { line1, line2 } = formatFinishLine({
        elapsedMs: metadata.elapsedMs,
        model: modeLabel,
        costUsd: cost ?? null,
        tokensPart,
        detailParts: [filesPart, slugPart],
    });
    return line2 ? `${line1} | ${line2}` : line1;
}
async function readStoredPrompt(sessionId) {
    const request = await sessionStore.readRequest(sessionId);
    if (request?.prompt && request.prompt.trim().length > 0) {
        return request.prompt;
    }
    const meta = await sessionStore.readSession(sessionId);
    if (meta?.options?.prompt && meta.options.prompt.trim().length > 0) {
        return meta.options.prompt;
    }
    return null;
}
