/**
 * Build output records from wake cycles, optionally enriched with
 * SDK transcript data.
 */
import { emptyRecoveryStats } from "./image-recovery.js";
import { buildSdkTrajectory, buildPriorContext, } from "./sdk-transcript.js";
import { buildSyntheticPiSystemMessage, parsePiCodingInitFromCycle, } from "./pi-log-metadata.js";
// ---------------------------------------------------------------------------
// Log-based trajectory (fallback when no SDK transcript available)
// ---------------------------------------------------------------------------
const TRAJECTORY_CATEGORIES = new Map([
    ["send", "context"],
    ["assistant", "assistant"],
    ["tool_call", "tool_call"],
    ["tool_result", "tool_result"],
]);
function buildTrajectoryEntry(entry) {
    // Thinking is logged as system with [thinking] prefix
    if (entry.category === "system" &&
        entry.message.startsWith("[thinking]")) {
        return {
            role: "thinking",
            content: entry.message.slice("[thinking]".length).trim(),
            timestamp: entry.timestamp,
        };
    }
    const role = TRAJECTORY_CATEGORIES.get(entry.category);
    if (!role)
        return null;
    const result = {
        role,
        content: entry.message,
        timestamp: entry.timestamp,
    };
    const metadata = {};
    if (entry.category === "tool_call") {
        metadata.name = entry.data.name ?? "";
        metadata.input = entry.data.input ?? {};
        if ("tool_use_id" in entry.data)
            metadata.tool_use_id = entry.data.tool_use_id;
        if ("parent_tool_use_id" in entry.data)
            metadata.parent_tool_use_id = entry.data.parent_tool_use_id;
        if ("subagent_type" in entry.data)
            metadata.subagent_type = entry.data.subagent_type;
    }
    else if (entry.category === "tool_result") {
        metadata.name = entry.data.name ?? "";
        if ("tool_use_id" in entry.data)
            metadata.tool_use_id = entry.data.tool_use_id;
    }
    if (Object.keys(metadata).length > 0) {
        result.metadata = metadata;
    }
    return result;
}
function buildLogTrajectory(cycle) {
    const trajectory = [];
    for (const entry of cycle.entries) {
        const t = buildTrajectoryEntry(entry);
        if (t)
            trajectory.push(t);
    }
    return trajectory;
}
// ---------------------------------------------------------------------------
// Prompt extraction
// ---------------------------------------------------------------------------
function extractPromptAndMeta(cycle) {
    const original = cycle.originalIncoming;
    if (original.length > 0) {
        const promptTexts = original.map((m) => m.data.text ?? m.message);
        const first = original[0];
        return {
            prompt: promptTexts.join("\n"),
            promptMeta: {
                channel: first.data.channel ?? "unknown",
                sender: first.data.sender ?? "",
                message_id: first.data.id ?? "",
                timestamp: first.timestamp,
                is_interrupt: false,
            },
        };
    }
    if (cycle.trigger === "heartbeat" || cycle.incomingMessages.length === 0) {
        return {
            prompt: "heartbeat",
            promptMeta: {
                channel: "heartbeat",
                sender: "",
                message_id: "",
                timestamp: cycle.wakeEntry?.timestamp ?? "",
                is_interrupt: false,
            },
        };
    }
    // All incoming are interrupts (heartbeat cycle that got interrupted)
    const allIncoming = cycle.incomingMessages;
    const promptTexts = allIncoming.map((m) => m.data.text ?? m.message);
    const first = allIncoming[0];
    return {
        prompt: promptTexts.join("\n"),
        promptMeta: {
            channel: first.data.channel ?? "unknown",
            sender: first.data.sender ?? "",
            message_id: first.data.id ?? "",
            timestamp: first.timestamp,
            is_interrupt: true,
        },
    };
}
// ---------------------------------------------------------------------------
// Interrupt metadata
// ---------------------------------------------------------------------------
function extractInterruptMeta(cycle) {
    const interruptMsgs = cycle.interruptIncoming;
    if (interruptMsgs.length === 0)
        return null;
    return interruptMsgs.map((im) => ({
        channel: im.data.channel ?? "unknown",
        sender: im.data.sender ?? "",
        message_id: im.data.id ?? "",
        timestamp: im.timestamp,
        text: im.data.text ?? im.message,
    }));
}
// ---------------------------------------------------------------------------
// Usage extraction
// ---------------------------------------------------------------------------
function extractUsage(cycle) {
    const result = cycle.resultEntry;
    if (!result?.data)
        return null;
    return {
        turns: result.data.turns,
        input_tokens: result.data.input_tokens,
        output_tokens: result.data.output_tokens,
        cost: result.data.cost,
    };
}
// ---------------------------------------------------------------------------
// System message extraction
// ---------------------------------------------------------------------------
function extractSystemMessage(cycle) {
    const sysEntry = cycle.systemInitEntry;
    if (sysEntry) {
        const prefix = "System message: ";
        const rawJson = sysEntry.message.slice(prefix.length);
        try {
            return JSON.parse(rawJson);
        }
        catch {
            return rawJson;
        }
    }
    const piInit = parsePiCodingInitFromCycle(cycle);
    if (piInit) {
        return buildSyntheticPiSystemMessage(piInit);
    }
    return null;
}
/**
 * Read `session_id` from a Claude SDK or Pi synthetic system init object.
 */
export function sessionIdFromSystemMessage(systemMessage) {
    if (systemMessage === null || typeof systemMessage !== "object") {
        return undefined;
    }
    const sid = systemMessage.session_id;
    return typeof sid === "string" && sid.length > 0 ? sid : undefined;
}
/**
 * When the output record has `system_message.session_id` but no top-level
 * `session_id`, copy it up so Viewer / export see a consistent field.
 */
export function applyTopLevelSessionId(record) {
    if (typeof record.session_id === "string" && record.session_id.length > 0) {
        return;
    }
    const fromSystem = sessionIdFromSystemMessage(record.system_message);
    if (fromSystem) {
        record.session_id = fromSystem;
    }
}
/** Copy the log wake entry timestamp onto the output record. */
export function applyWakeCycleId(record, wakeTimestamp) {
    if (wakeTimestamp.length > 0) {
        record.wake_cycle_id = wakeTimestamp;
    }
}
/**
 * Set top-level and system_message session_id to the matched transcript session.
 */
export function alignRecordSessionId(record, sessionId) {
    if (!sessionId)
        return;
    record.session_id = sessionId;
    if (record.system_message !== null && typeof record.system_message === "object") {
        record.system_message.session_id = sessionId;
    }
}
// ---------------------------------------------------------------------------
// Public: build a complete output record
// ---------------------------------------------------------------------------
/**
 * Merge interrupt messages from log entries into an SDK-based trajectory.
 * The SDK transcript doesn't capture injected interrupts, so we insert them
 * at the correct position based on timestamps.
 */
function mergeInterruptsIntoTrajectory(trajectory, cycle) {
    const interrupts = cycle.interruptIncoming;
    if (interrupts.length === 0)
        return trajectory;
    const interruptEntries = interrupts.map((im) => ({
        role: "user_message",
        content: im.data.text ?? im.message.replace("[interrupt] ", ""),
        timestamp: im.timestamp,
        metadata: {
            channel: im.data.channel ?? "unknown",
            sender: im.data.sender ?? "",
            is_interrupt: true,
        },
    }));
    const merged = [...trajectory, ...interruptEntries];
    merged.sort((a, b) => {
        const ta = new Date(a.timestamp).getTime();
        const tb = new Date(b.timestamp).getTime();
        return ta - tb;
    });
    return merged;
}
export async function buildRecord(cycle, sdkTurn, sidecarIndex = null, stats = emptyRecoveryStats(), cache) {
    const { prompt, promptMeta } = extractPromptAndMeta(cycle);
    let trajectory = sdkTurn !== undefined
        ? await buildSdkTrajectory(sdkTurn, sidecarIndex, stats, cache)
        : buildLogTrajectory(cycle);
    if (sdkTurn !== undefined && cycle.hasInterrupts) {
        trajectory = mergeInterruptsIntoTrajectory(trajectory, cycle);
    }
    const priorContext = sdkTurn !== undefined ? await buildPriorContext(sdkTurn, sidecarIndex, stats, cache) : null;
    // Extract dynamic system prompt logged at runtime (if available)
    const systemPromptEntry = cycle.systemPromptEntry;
    const systemPrompt = systemPromptEntry?.message ?? null;
    const record = {
        system_message: extractSystemMessage(cycle),
        system_prompt: systemPrompt,
        prior_context: priorContext,
        prompt,
        prompt_meta: promptMeta,
        interrupts: extractInterruptMeta(cycle),
        trajectory,
        usage: extractUsage(cycle),
        complete: cycle.hasResult,
        has_interrupts: cycle.hasInterrupts,
        trajectory_source: sdkTurn !== undefined ? "sdk_transcript" : "visionclaw_logs",
    };
    if (sdkTurn) {
        alignRecordSessionId(record, sdkTurn.sessionId);
    }
    else {
        applyTopLevelSessionId(record);
    }
    return record;
}
/**
 * Build a record for a session-switch turn (not the primary wake-event turn).
 * These have no log-level prompt/interrupts — the "prompt" is the session
 * switch context itself, extracted from the SDK turn's first message.
 */
export async function buildSessionSwitchRecord(sdkTurn, sidecarIndex = null, stats = emptyRecoveryStats(), cache) {
    const trajectory = await buildSdkTrajectory(sdkTurn, sidecarIndex, stats, cache);
    const priorContext = await buildPriorContext(sdkTurn, sidecarIndex, stats, cache);
    // Extract prompt from the first message (which should be the [Session Switch] text)
    let prompt = "session_switch";
    if (sdkTurn.messages.length > 0) {
        const first = sdkTurn.messages[0];
        if (first.type === "user") {
            const text = extractTextFromContent(first.message?.content);
            if (text)
                prompt = text;
        }
        else if (first.type === "message" && first.message?.role === "user") {
            const text = extractTextFromContent(first.message.content);
            if (text)
                prompt = text;
        }
    }
    // Extract system message from the SDK turn's init message (if present)
    let systemMessage = null;
    const initMsg = sdkTurn.messages.find((m) => m.type === "system" && m.subtype === "init");
    if (initMsg) {
        systemMessage = initMsg;
    }
    const record = {
        system_message: systemMessage,
        system_prompt: null,
        prior_context: priorContext,
        prompt,
        prompt_meta: {
            channel: "session_switch",
            sender: "",
            message_id: "",
            timestamp: sdkTurn.timestamp,
            is_interrupt: false,
        },
        interrupts: null,
        trajectory,
        usage: null,
        complete: true,
        has_interrupts: false,
        trajectory_source: "sdk_transcript",
    };
    alignRecordSessionId(record, sdkTurn.sessionId);
    return record;
}
function extractTextFromContent(content) {
    if (typeof content === "string")
        return content;
    if (Array.isArray(content)) {
        const parts = [];
        for (const block of content) {
            if (block !== null &&
                typeof block === "object" &&
                block.type === "text") {
                const text = block.text;
                if (typeof text === "string")
                    parts.push(text);
            }
        }
        return parts.join("\n");
    }
    return "";
}
//# sourceMappingURL=record-builder.js.map