import { promises as fs } from "node:fs";
import os from "node:os";
import path from "node:path";
import { z } from "zod";
import { mapCodexRolloutToolCall } from "./codex/tool-call-mapper.js";
import { extractCodexTerminalSessionId, nonEmptyString } from "./tool-call-mapper-utils.js";
const MAX_ROLLOUT_SEARCH_DEPTH = 4;
function resolveCodexSessionRoot() {
    if (process.env.CODEX_SESSION_DIR) {
        return process.env.CODEX_SESSION_DIR;
    }
    const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
    return path.join(codexHome, "sessions");
}
async function findRolloutFile(threadId, root) {
    const stack = [{ dir: root, depth: 0 }];
    while (stack.length > 0) {
        const { dir, depth } = stack.pop();
        let entries;
        try {
            entries = await fs.readdir(dir, { withFileTypes: true });
        }
        catch {
            continue;
        }
        for (const entry of entries) {
            const entryPath = path.join(dir, entry.name);
            if (entry.isFile()) {
                const matchesThread = entry.name.includes(threadId);
                const matchesPrefix = entry.name.startsWith("rollout-");
                const matchesExtension = entry.name.endsWith(".json") || entry.name.endsWith(".jsonl");
                if (matchesThread && matchesPrefix && matchesExtension) {
                    return entryPath;
                }
            }
            else if (entry.isDirectory() && depth < MAX_ROLLOUT_SEARCH_DEPTH) {
                stack.push({ dir: entryPath, depth: depth + 1 });
            }
        }
    }
    return null;
}
const RolloutContentItemSchema = z.union([
    z.object({ type: z.literal("input_text"), text: z.string() }),
    z.object({ type: z.literal("output_text"), text: z.string() }),
    z.object({ type: z.literal("reasoning_text"), text: z.string() }),
    z.object({ type: z.literal("text"), text: z.string() }),
    z
        .object({
        type: z.string(),
        text: z.string().optional(),
        message: z.string().optional(),
    })
        .passthrough(),
]);
const RolloutContentArraySchema = z.array(RolloutContentItemSchema);
function extractContentTextByType(content, itemType) {
    const parsed = RolloutContentArraySchema.safeParse(content);
    if (!parsed.success) {
        return "";
    }
    return parsed.data
        .filter((item) => item.type === itemType)
        .map((item) => item.text)
        .join("\n")
        .trim();
}
const RolloutResponseMessagePayloadSchema = z.object({
    type: z.literal("message"),
    role: z.enum(["user", "assistant"]).optional(),
    content: z.unknown().optional(),
});
const RolloutResponseReasoningPayloadSchema = z.object({
    type: z.literal("reasoning"),
    content: z.unknown().optional(),
    summary: z.array(z.object({ text: z.string().optional() })).optional(),
    text: z.string().optional(),
});
const RolloutResponseFunctionCallPayloadSchema = z.object({
    type: z.literal("function_call"),
    name: z.string().optional(),
    call_id: z.string().optional(),
    arguments: z.unknown().optional(),
});
const RolloutResponseCustomToolCallPayloadSchema = z
    .object({
    type: z.literal("custom_tool_call"),
    name: z.string().optional(),
    call_id: z.string().optional(),
    arguments: z.unknown().optional(),
    input: z.unknown().optional(),
})
    .passthrough();
const RolloutResponseFunctionCallOutputPayloadSchema = z
    .object({
    type: z.literal("function_call_output"),
    call_id: z.string().optional(),
    output: z.unknown().optional(),
})
    .passthrough();
const RolloutResponseCustomToolCallOutputPayloadSchema = z
    .object({
    type: z.literal("custom_tool_call_output"),
    call_id: z.string().optional(),
    output: z.unknown().optional(),
})
    .passthrough();
const RolloutImageGenerationPayloadSchema = z
    .object({
    type: z.union([z.literal("image_generation_call"), z.literal("image_generation_end")]),
    result: z.string().optional(),
    savedPath: z.string().optional(),
    saved_path: z.string().optional(),
    path: z.string().optional(),
})
    .passthrough();
const RolloutEventAgentReasoningPayloadSchema = z.object({
    type: z.literal("agent_reasoning"),
    text: z.string().optional(),
});
const RolloutEventAgentMessagePayloadSchema = z.object({
    type: z.literal("agent_message"),
    message: z
        .union([
        z.string(),
        z
            .object({
            role: z.string().optional(),
            message: z.string().optional(),
            text: z.string().optional(),
        })
            .passthrough(),
    ])
        .optional(),
});
const RolloutEventUserMessagePayloadSchema = z.object({
    type: z.literal("user_message"),
    message: z
        .union([
        z.string(),
        z
            .object({
            role: z.string().optional(),
            message: z.string().optional(),
            text: z.string().optional(),
        })
            .passthrough(),
    ])
        .optional(),
});
const RolloutMessageContentSchema = z.union([
    z.string().transform((content) => content.trim()),
    z
        .array(z
        .object({
        text: z.string().optional(),
        message: z.string().optional(),
    })
        .passthrough())
        .transform((content) => content
        .map((block) => block.text ?? block.message ?? "")
        .map((text) => text.trim())
        .filter(Boolean)
        .join("\n")
        .trim()),
]);
function extractMessageText(content) {
    const parsed = RolloutMessageContentSchema.safeParse(content);
    if (!parsed.success) {
        return "";
    }
    return parsed.data;
}
const RolloutEventMessageTextSchema = z.union([
    z.string(),
    z
        .object({
        message: z.string().optional(),
        text: z.string().optional(),
    })
        .passthrough()
        .transform((message) => message.message ?? message.text ?? ""),
]);
function extractEventMessageText(message) {
    const parsed = RolloutEventMessageTextSchema.safeParse(message);
    if (!parsed.success) {
        return "";
    }
    return parsed.data;
}
function isSyntheticRolloutUserMessage(text) {
    const normalized = text.trim();
    if (!normalized) {
        return false;
    }
    const lower = normalized.toLowerCase();
    if (lower.startsWith("# agents.md instructions for") && lower.includes("<instructions>")) {
        return true;
    }
    if (lower.startsWith("<environment_context>")) {
        return true;
    }
    return false;
}
function extractReasoningText(payload) {
    if (Array.isArray(payload.summary)) {
        const text = payload.summary
            .map((item) => item.text ?? "")
            .filter(Boolean)
            .join("\n")
            .trim();
        if (text) {
            return text;
        }
    }
    const contentText = extractContentTextByType(payload.content, "reasoning_text");
    if (contentText) {
        return contentText;
    }
    if (typeof payload.text === "string") {
        return payload.text;
    }
    return "";
}
function parseJsonLikeString(value) {
    try {
        return JSON.parse(value);
    }
    catch {
        return value;
    }
}
function parseJsonLikeValue(value) {
    if (typeof value === "string") {
        return parseJsonLikeString(value);
    }
    return value;
}
function normalizeGeneratedImageSource(payload) {
    const savedPath = nonEmptyString(payload.savedPath) ??
        nonEmptyString(payload.saved_path) ??
        nonEmptyString(payload.path) ??
        null;
    if (savedPath) {
        return savedPath;
    }
    const result = nonEmptyString(payload.result);
    if (!result) {
        return null;
    }
    return result.startsWith("data:") ? result : `data:image/png;base64,${result}`;
}
function markdownImageSource(source) {
    if (/^(https?:|data:|blob:)/i.test(source)) {
        return source;
    }
    // Convert backslashes to forward slashes so markdown parsers don't treat
    // sequences like `\.` as escapes — that silently drops the separator and
    // turns Windows paths like `C:\Users\me\.codex\foo.png` into unfetchable
    // garbage. Windows file APIs accept `/` everywhere.
    const normalized = source.replace(/\\/g, "/");
    return `<${normalized.replace(/>/g, "%3E")}>`;
}
function mapGeneratedImagePayloadToTimeline(payload) {
    const source = normalizeGeneratedImageSource(payload);
    return source
        ? {
            kind: "timeline",
            item: {
                type: "assistant_message",
                text: `![Generated image](${markdownImageSource(source)})`,
            },
        }
        : { kind: "ignore" };
}
function readTerminalSessionId(value) {
    if (typeof value === "number" && Number.isFinite(value)) {
        return String(value);
    }
    if (typeof value === "string") {
        return nonEmptyString(value.trim());
    }
    return undefined;
}
function mapCodexTerminalInteractionToToolCall(params) {
    const processId = nonEmptyString(params.processId);
    const callId = processId
        ? `terminal-session-${processId}`
        : (nonEmptyString(params.fallbackCallId) ?? "terminal-interaction");
    const label = nonEmptyString(params.command);
    return {
        type: "tool_call",
        callId,
        name: "terminal",
        status: "completed",
        error: null,
        detail: {
            type: "plain_text",
            ...(label ? { label } : {}),
            icon: "square_terminal",
        },
        metadata: processId ? { processId } : undefined,
    };
}
function buildTerminalCommandBySessionId(parsedRecords) {
    const outputsByCallId = parsedRecords
        .filter((record) => record.kind === "output")
        .reduce((map, record) => map.set(record.callId, record.output), new Map());
    const commandsBySessionId = new Map();
    for (const record of parsedRecords) {
        if (record.kind !== "call" || record.name === "write_stdin") {
            continue;
        }
        const mapped = mapCodexRolloutToolCall({
            callId: record.callId ?? null,
            name: record.name,
            input: record.input ?? null,
            output: record.callId ? (outputsByCallId.get(record.callId) ?? null) : null,
        });
        if (!mapped || mapped.detail.type !== "shell") {
            continue;
        }
        const sessionId = extractCodexTerminalSessionId(mapped.detail.output);
        if (!sessionId) {
            continue;
        }
        commandsBySessionId.set(sessionId, mapped.detail.command);
    }
    return commandsBySessionId;
}
function readOutputPayloadValue(payload) {
    if (payload.output !== undefined) {
        return parseJsonLikeValue(payload.output);
    }
    const { type: _type, call_id: _callId, ...rest } = payload;
    return Object.keys(rest).length > 0 ? rest : undefined;
}
const FunctionCallInputNormalizationSchema = z.union([
    z
        .object({ cmd: z.string() })
        .transform((input) => ({ name: "Bash", input: { command: input.cmd } })),
    z
        .object({ command: z.array(z.string()) })
        .transform((input) => ({ name: "Bash", input: { command: input.command[2] ?? "" } })),
    z.unknown().transform((input) => ({ name: "unknown", input })),
]);
const RolloutResponseRecordSchema = z.union([
    RolloutResponseMessagePayloadSchema.transform((payload) => {
        const text = extractMessageText(payload.content);
        const itemType = payload.role === "assistant" ? "assistant_message" : "user_message";
        const shouldEmit = text.length > 0 && (itemType !== "user_message" || !isSyntheticRolloutUserMessage(text));
        return shouldEmit ? { kind: "timeline", item: { type: itemType, text } } : { kind: "ignore" };
    }),
    RolloutResponseReasoningPayloadSchema.transform((payload) => {
        const text = extractReasoningText(payload);
        return text.length > 0
            ? { kind: "timeline", item: { type: "reasoning", text } }
            : { kind: "ignore" };
    }),
    z
        .union([RolloutResponseFunctionCallPayloadSchema, RolloutResponseCustomToolCallPayloadSchema])
        .transform((payload) => {
        const rawName = payload.name ?? "unknown";
        const rawInput = payload.arguments ?? ("input" in payload ? payload.input : undefined);
        const parsedArguments = parseJsonLikeValue(rawInput);
        const normalized = rawName === "exec_command" || rawName === "shell"
            ? FunctionCallInputNormalizationSchema.parse(parsedArguments)
            : { name: rawName, input: parsedArguments };
        return {
            kind: "call",
            name: normalized.name,
            callId: payload.call_id,
            input: normalized.input,
        };
    }),
    z
        .union([
        RolloutResponseFunctionCallOutputPayloadSchema,
        RolloutResponseCustomToolCallOutputPayloadSchema,
    ])
        .transform((payload) => {
        if (!payload.call_id) {
            return { kind: "ignore" };
        }
        const output = readOutputPayloadValue(payload);
        return output !== undefined
            ? { kind: "output", callId: payload.call_id, output }
            : { kind: "ignore" };
    }),
    RolloutImageGenerationPayloadSchema.transform(mapGeneratedImagePayloadToTimeline),
    z.unknown().transform(() => ({ kind: "ignore" })),
]);
const RolloutEventRecordSchema = z.union([
    RolloutImageGenerationPayloadSchema.transform(mapGeneratedImagePayloadToTimeline),
    RolloutEventAgentReasoningPayloadSchema.transform((payload) => payload.text
        ? { kind: "timeline", item: { type: "reasoning", text: payload.text } }
        : { kind: "ignore" }),
    RolloutEventAgentMessagePayloadSchema.transform((payload) => {
        const text = extractEventMessageText(payload.message);
        return text.length > 0
            ? { kind: "timeline", item: { type: "assistant_message", text } }
            : { kind: "ignore" };
    }),
    RolloutEventUserMessagePayloadSchema.transform((payload) => {
        const text = extractEventMessageText(payload.message);
        const shouldEmit = text.length > 0 && !isSyntheticRolloutUserMessage(text);
        return shouldEmit
            ? { kind: "timeline", item: { type: "user_message", text } }
            : { kind: "ignore" };
    }),
    z.unknown().transform(() => ({ kind: "ignore" })),
]);
const RolloutRecordSchema = z
    .object({
    type: z.enum(["response_item", "event_msg"]),
    payload: z.unknown().optional(),
    item: z.unknown().optional(),
    msg: z.unknown().optional(),
})
    .transform((entry) => entry.type === "response_item"
    ? RolloutResponseRecordSchema.parse(entry.payload ?? entry.item)
    : RolloutEventRecordSchema.parse(entry.payload ?? entry.msg));
function parseJsonRolloutTimeline(parsed) {
    if (!parsed || typeof parsed !== "object") {
        return null;
    }
    const items = parsed.items;
    if (!Array.isArray(items)) {
        return null;
    }
    const timeline = [];
    for (const entry of items) {
        if (!entry || typeof entry !== "object") {
            continue;
        }
        const messagePayloadResult = RolloutResponseMessagePayloadSchema.safeParse(entry);
        if (messagePayloadResult.success) {
            const payload = messagePayloadResult.data;
            const text = extractMessageText(payload.content);
            if (!text) {
                continue;
            }
            if (payload.role === "assistant") {
                timeline.push({ type: "assistant_message", text });
            }
            else if (payload.role === "user") {
                if (!isSyntheticRolloutUserMessage(text)) {
                    timeline.push({ type: "user_message", text });
                }
            }
            continue;
        }
        const reasoningPayloadResult = RolloutResponseReasoningPayloadSchema.safeParse(entry);
        if (reasoningPayloadResult.success) {
            const text = extractReasoningText(reasoningPayloadResult.data);
            if (text) {
                timeline.push({ type: "reasoning", text });
            }
            continue;
        }
    }
    return timeline;
}
function timelineTextFingerprint(item) {
    switch (item.type) {
        case "user_message":
        case "assistant_message":
        case "reasoning":
            return `${item.type}\u0000${item.text}`;
        default:
            return null;
    }
}
function generatedImageSourceFromMarkdown(text) {
    const match = text.match(/^!\[Generated image\]\((.*)\)$/);
    return match?.[1] ?? null;
}
function shouldReplaceGeneratedImage(previous, current) {
    if (previous.type !== "assistant_message" || current.type !== "assistant_message") {
        return false;
    }
    const previousSource = generatedImageSourceFromMarkdown(previous.text);
    const currentSource = generatedImageSourceFromMarkdown(current.text);
    if (!previousSource || !currentSource) {
        return false;
    }
    return previousSource.startsWith("data:") && !currentSource.startsWith("data:");
}
function shouldSkipGeneratedImage(previous, current) {
    if (previous.type !== "assistant_message" || current.type !== "assistant_message") {
        return false;
    }
    const previousSource = generatedImageSourceFromMarkdown(previous.text);
    const currentSource = generatedImageSourceFromMarkdown(current.text);
    if (!previousSource || !currentSource) {
        return false;
    }
    return !previousSource.startsWith("data:") && currentSource.startsWith("data:");
}
function dedupeMirroredTextTimelineItems(timeline) {
    const deduped = [];
    for (const item of timeline) {
        const prev = deduped[deduped.length - 1];
        if (!prev) {
            deduped.push(item);
            continue;
        }
        if (shouldReplaceGeneratedImage(prev, item)) {
            deduped[deduped.length - 1] = item;
            continue;
        }
        if (shouldSkipGeneratedImage(prev, item)) {
            continue;
        }
        const currentFingerprint = timelineTextFingerprint(item);
        const previousFingerprint = timelineTextFingerprint(prev);
        if (currentFingerprint && previousFingerprint && currentFingerprint === previousFingerprint) {
            continue;
        }
        deduped.push(item);
    }
    return deduped;
}
export async function parseRolloutFile(filePath) {
    const content = await fs.readFile(filePath, "utf8");
    const trimmed = content.trim();
    if (!trimmed) {
        return [];
    }
    try {
        const parsed = JSON.parse(trimmed);
        const jsonTimeline = parseJsonRolloutTimeline(parsed);
        if (jsonTimeline) {
            return dedupeMirroredTextTimelineItems(jsonTimeline);
        }
    }
    catch {
        // Fall back to JSONL parsing.
    }
    const lines = trimmed
        .split(/\r?\n/)
        .map((line) => line.trim())
        .filter(Boolean);
    const parsedRecords = lines
        .map((line) => {
        try {
            return JSON.parse(line);
        }
        catch {
            return null;
        }
    })
        .filter((record) => record !== null)
        .map((record) => RolloutRecordSchema.safeParse(record))
        .filter((result) => result.success)
        .map((result) => result.data);
    const outputsByCallId = parsedRecords
        .filter((record) => record.kind === "output")
        .reduce((map, record) => map.set(record.callId, record.output), new Map());
    const terminalCommandsBySessionId = buildTerminalCommandBySessionId(parsedRecords);
    const timeline = parsedRecords.flatMap((record) => record.kind === "timeline"
        ? [record.item]
        : record.kind === "call"
            ? (() => {
                if (record.name === "write_stdin") {
                    const input = record.input && typeof record.input === "object"
                        ? record.input
                        : null;
                    const sessionId = readTerminalSessionId(input?.session_id) ?? readTerminalSessionId(input?.sessionId);
                    return [
                        mapCodexTerminalInteractionToToolCall({
                            processId: sessionId,
                            fallbackCallId: record.callId,
                            command: sessionId ? terminalCommandsBySessionId.get(sessionId) : undefined,
                        }),
                    ];
                }
                const mapped = mapCodexRolloutToolCall({
                    callId: record.callId ?? null,
                    name: record.name,
                    input: record.input ?? null,
                    output: record.callId ? (outputsByCallId.get(record.callId) ?? null) : null,
                });
                return mapped ? [mapped] : [];
            })()
            : []);
    return dedupeMirroredTextTimelineItems(timeline);
}
export async function loadCodexPersistedTimeline(sessionId, options, logger) {
    const rolloutPath = options?.rolloutPath ?? null;
    if (rolloutPath) {
        try {
            const stat = await fs.stat(rolloutPath);
            if (stat.isFile()) {
                const timeline = await parseRolloutFile(rolloutPath);
                if (timeline.length > 0) {
                    return timeline;
                }
            }
        }
        catch {
            // Fall back to session root scan.
        }
    }
    try {
        const preferredRoot = options?.sessionRoot ?? resolveCodexSessionRoot();
        const fallbackRoot = resolveCodexSessionRoot();
        let rolloutFile = null;
        if (preferredRoot) {
            rolloutFile = await findRolloutFile(sessionId, preferredRoot);
        }
        if (!rolloutFile && fallbackRoot && fallbackRoot !== preferredRoot) {
            rolloutFile = await findRolloutFile(sessionId, fallbackRoot);
        }
        if (!rolloutFile) {
            return [];
        }
        return await parseRolloutFile(rolloutFile);
    }
    catch (error) {
        logger?.warn({ err: error, sessionId }, "Failed to load persisted timeline");
        return [];
    }
}
//# sourceMappingURL=codex-rollout-timeline.js.map