/**
 * SDK transcript loading, segmentation, prior context extraction,
 * and cycle matching.
 */
import fs from "node:fs";
import readline from "node:readline";
import { discoverTranscriptSessionFiles } from "./transcript-discovery.js";
import { recoverImagesInContent, emptyRecoveryStats } from "./image-recovery.js";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const BASE64_PLACEHOLDER = "<base64-image>";
function str(val) {
    return typeof val === "string" ? val : "";
}
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") {
                parts.push(str(block.text));
            }
        }
        return parts.join("\n");
    }
    return "";
}
function redactBase64(content) {
    if (!Array.isArray(content))
        return content;
    const out = [];
    for (const block of content) {
        if (block !== null && typeof block === "object") {
            const b = block;
            if (b.type === "image") {
                const source = b.source;
                if (source?.type === "base64") {
                    out.push({
                        type: "image",
                        source: {
                            type: "base64",
                            media_type: source.media_type ?? "image/png",
                            data: BASE64_PLACEHOLDER,
                        },
                    });
                    continue;
                }
            }
            if (b.type === "tool_result") {
                out.push({
                    ...b,
                    content: redactBase64(b.content),
                });
                continue;
            }
        }
        out.push(block);
    }
    return out;
}
/**
 * Process image content: recover from archive when a sidecar index is
 * provided, otherwise fall back to redacting base64 data.
 */
async function processImageContent(content, sidecarIndex, stats, cache) {
    if (sidecarIndex) {
        return recoverImagesInContent(content, sidecarIndex, stats, cache);
    }
    return redactBase64(content);
}
// ---------------------------------------------------------------------------
// Load SDK transcripts (per-session)
// ---------------------------------------------------------------------------
/**
 * Normalize a raw JSONL line from Claude SDK or Pi Coding Agent transcripts
 * into the SDKMessage shape expected by segmentation and conversion.
 */
export function normalizeTranscriptLine(parsed) {
    const rawType = parsed.type;
    if (typeof rawType !== "string")
        return null;
    if (rawType === "message" && parsed.message !== null && typeof parsed.message === "object") {
        const inner = parsed.message;
        const role = inner.role;
        const ts = str(parsed.timestamp);
        if (role === "user" || role === "assistant") {
            return {
                type: role,
                timestamp: ts,
                message: {
                    ...inner,
                    content: normalizePiMessageContent(inner.content),
                },
            };
        }
        if (role === "toolResult") {
            const toolCallId = str(inner.toolCallId ?? inner.tool_use_id);
            const toolName = str(inner.toolName ?? inner.name);
            const isError = inner.isError === true;
            const blocks = normalizePiToolResultContent(inner.content, toolCallId, toolName, isError);
            return {
                type: "user",
                timestamp: ts,
                message: { role: "user", content: blocks },
            };
        }
        return null;
    }
    if (rawType === "compaction") {
        const summary = str(parsed.summary);
        return {
            type: "system",
            subtype: "compact_boundary",
            timestamp: str(parsed.timestamp),
            message: summary ? { role: "system", content: summary } : undefined,
            compactMetadata: {
                trigger: "pi_compaction",
                preTokens: typeof parsed.tokensBefore === "number" ? parsed.tokensBefore : undefined,
            },
        };
    }
    if (rawType === "user" || rawType === "assistant" || rawType === "system") {
        return parsed;
    }
    return null;
}
function normalizePiMessageContent(content) {
    if (!Array.isArray(content))
        return content;
    const out = [];
    for (const block of content) {
        if (block === null || typeof block !== "object") {
            out.push(block);
            continue;
        }
        const b = block;
        if (b.type === "toolCall") {
            out.push({
                type: "tool_use",
                id: b.id,
                name: b.name,
                input: b.arguments ?? b.input ?? {},
            });
        }
        else {
            out.push(block);
        }
    }
    return out;
}
function normalizePiToolResultContent(content, toolCallId, toolName, isError) {
    // Preserve array/string content verbatim so downstream image recovery
    // (see processImageContent → recoverImagesInContent) can walk image blocks
    // and `[Screenshot: archived=...]` text placeholders. Only fall back to
    // JSON.stringify for non-array, non-string shapes.
    const resultContent = Array.isArray(content) || typeof content === "string"
        ? content
        : JSON.stringify(content);
    const block = {
        type: "tool_result",
        tool_use_id: toolCallId,
        content: resultContent,
        is_error: isError,
    };
    if (toolName)
        block.name = toolName;
    return [block];
}
function isObjectRecord(value) {
    return typeof value === "object" && value !== null;
}
function messageTimestamp(message) {
    if (typeof message.timestamp === "string")
        return message.timestamp;
    if (typeof message.timestamp === "number")
        return new Date(message.timestamp).toISOString();
    return "";
}
/**
 * Load SDK transcripts grouped by session ID.  Each entry represents one
 * session's messages in chronological order.
 */
export async function loadSdkTranscripts(transcriptDir, sessionIds) {
    const discovered = discoverTranscriptSessionFiles(transcriptDir, sessionIds);
    const bySession = new Map();
    for (const { sessionId, label, files } of discovered) {
        if (!bySession.has(sessionId))
            bySession.set(sessionId, []);
        const msgs = bySession.get(sessionId) ?? [];
        let count = 0;
        for (const filePath of files) {
            if (!fs.existsSync(filePath))
                continue;
            const rl = readline.createInterface({
                input: fs.createReadStream(filePath, { encoding: "utf-8" }),
                crlfDelay: Infinity,
            });
            for await (const raw of rl) {
                const trimmed = raw.trim();
                if (!trimmed)
                    continue;
                try {
                    const parsed = JSON.parse(trimmed);
                    const normalized = normalizeTranscriptLine(parsed);
                    if (normalized) {
                        msgs.push(normalized);
                        count++;
                    }
                }
                catch {
                    // skip malformed
                }
            }
        }
        console.log(`  SDK transcript [${label}/${sessionId.slice(0, 8)}]: ${count} messages`);
    }
    const result = [];
    for (const [sessionId, messages] of bySession) {
        messages.sort((a, b) => messageTimestamp(a).localeCompare(messageTimestamp(b)));
        result.push({ sessionId, messages });
    }
    const total = result.reduce((s, r) => s + r.messages.length, 0);
    console.log(`  SDK transcript: ${result.length} session(s), ${total} total messages`);
    return result;
}
// ---------------------------------------------------------------------------
// Segment SDK messages into session turns
// ---------------------------------------------------------------------------
/**
 * Match `[Wake Event]` / `[Session Switch]` at the start of any text block.
 * VisionClaw emits the marker as the first line of a dedicated header text
 * block, but a reset-summary text block may be prepended (see
 * `engines/{claude,pi}/session.ts#prependResetSummary` and the Claude session
 * reset path). Checking each block's leading text matches the agent's
 * structural prompt while still rejecting markers that appear mid-text
 * (e.g. an assistant quoting "[Wake Event]" inside prose).
 */
function detectTurnStart(text) {
    if (text.startsWith("[Wake Event]"))
        return "wake_event";
    if (text.startsWith("[Session Switch]"))
        return "session_switch";
    if (text.startsWith("Previous session summary after refresh"))
        return "wake_event";
    return null;
}
function detectTurnStartInContent(content) {
    if (typeof content === "string")
        return detectTurnStart(content);
    if (Array.isArray(content)) {
        for (const block of content) {
            if (block !== null &&
                typeof block === "object" &&
                block.type === "text") {
                const text = str(block.text);
                const found = detectTurnStart(text);
                if (found)
                    return found;
            }
        }
    }
    return null;
}
function detectTurnBoundary(msg) {
    if (!isUserMessage(msg))
        return null;
    const piMessage = getPiMessage(msg);
    const content = piMessage ? piMessage.content : msg.message?.content;
    return detectTurnStartInContent(content);
}
/**
 * Segment a single session's messages into session turns.
 * Each turn starts at a [Wake Event] or [Session Switch] user message
 * and extends until the next such boundary (or end of messages).
 */
export function segmentSdkSessionTurns(sessionId, messages) {
    // First pass: find turn boundary indices and compact_boundary indices
    const turnBoundaries = [];
    const boundaryIndices = [];
    for (let i = 0; i < messages.length; i++) {
        const msg = messages[i];
        if (msg.type === "system" &&
            (msg.subtype === "compact_boundary" || msg.subtype === "compaction")) {
            boundaryIndices.push(i);
        }
        else {
            const turnType = detectTurnBoundary(msg);
            if (turnType) {
                turnBoundaries.push({ index: i, startType: turnType });
            }
        }
    }
    // Second pass: build turns with prior context
    const turns = [];
    for (let ti = 0; ti < turnBoundaries.length; ti++) {
        const { index: bi, startType } = turnBoundaries[ti];
        const nextBi = ti + 1 < turnBoundaries.length ? turnBoundaries[ti + 1].index : messages.length;
        const turnMessages = messages.slice(bi, nextBi);
        // Prior context: from most recent compact_boundary to this turn start.
        let prior = [];
        const bp = bisectRight(boundaryIndices, bi);
        if (bp > 0) {
            const lastBoundary = boundaryIndices[bp - 1];
            prior = messages.slice(lastBoundary, bi);
        }
        else if (bi > 0) {
            prior = messages.slice(0, bi);
        }
        turns.push({
            sessionId,
            startType,
            timestamp: messageTimestamp(messages[bi]),
            messages: turnMessages,
            priorContext: prior,
        });
    }
    return turns;
}
function bisectRight(arr, target) {
    let lo = 0;
    let hi = arr.length;
    while (lo < hi) {
        const mid = (lo + hi) >>> 1;
        if (arr[mid] <= target)
            lo = mid + 1;
        else
            hi = mid;
    }
    return lo;
}
// ---------------------------------------------------------------------------
// Build prior context
// ---------------------------------------------------------------------------
export async function buildPriorContext(turn, sidecarIndex, stats, cache) {
    if (turn.priorContext.length === 0)
        return null;
    const hasCompactBoundary = turn.priorContext.some((m) => m.type === "system" && m.subtype === "compact_boundary");
    let compactionSummary = null;
    const messagesToConvert = [];
    for (const msg of turn.priorContext) {
        if (msg.type === "system" &&
            (msg.subtype === "compact_boundary" || msg.subtype === "compaction")) {
            if (compactionSummary === null) {
                const fromMessage = extractTextFromContent(msg.message?.content);
                if (fromMessage) {
                    compactionSummary = fromMessage;
                }
            }
            continue;
        }
        if (msg.type === "queue-operation" || msg.type === "progress")
            continue;
        if (isUserMessage(msg) &&
            compactionSummary === null &&
            messagesToConvert.length === 0) {
            const text = getMessageText(msg);
            if (text.includes("continued from a previous conversation")) {
                compactionSummary = text;
                continue;
            }
        }
        messagesToConvert.push(msg);
    }
    const priorMessages = await convertSdkMessages(messagesToConvert, sidecarIndex, stats, cache);
    if (compactionSummary === null && priorMessages.length === 0) {
        if (hasCompactBoundary) {
            return {
                compaction_summary: null,
                compacted_fresh_start: true,
                prior_messages_count: 0,
                prior_messages: [],
            };
        }
        return null;
    }
    return {
        compaction_summary: compactionSummary,
        prior_messages_count: priorMessages.length,
        prior_messages: priorMessages,
    };
}
// ---------------------------------------------------------------------------
// Convert raw SDK messages to clean trajectory entries
// ---------------------------------------------------------------------------
function getPiMessage(msg) {
    if (msg.type !== "message" || !isObjectRecord(msg.message))
        return null;
    return msg.message;
}
function isUserMessage(msg) {
    if (msg.type === "user")
        return true;
    const piMessage = getPiMessage(msg);
    return piMessage?.role === "user";
}
function getMessageText(msg) {
    const piMessage = getPiMessage(msg);
    if (piMessage)
        return extractTextFromContent(piMessage.content);
    return extractTextFromContent(msg.message?.content);
}
async function convertPiCodingMessage(msg, sidecarIndex, stats, cache) {
    const piMessage = getPiMessage(msg);
    if (!piMessage)
        return [];
    const role = piMessage.role;
    const ts = messageTimestamp(msg);
    const content = piMessage.content;
    if (role === "user") {
        const text = extractTextFromContent(content);
        return detectTurnStartInContent(content)
            ? [{ role: "context", content: text, timestamp: ts }]
            : [{ role: "user_message", content: text, timestamp: ts }];
    }
    if (role === "assistant") {
        if (!Array.isArray(content))
            return [];
        const entries = [];
        for (const block of content) {
            if (!isObjectRecord(block))
                continue;
            if (block.type === "thinking") {
                entries.push({ role: "thinking", content: str(block.thinking), timestamp: ts });
            }
            else if (block.type === "text") {
                entries.push({ role: "assistant", content: str(block.text), timestamp: ts });
            }
            else if (block.type === "toolCall") {
                entries.push({
                    role: "tool_call",
                    content: str(block.name),
                    timestamp: ts,
                    metadata: {
                        name: str(block.name),
                        input: block.arguments ?? {},
                        tool_use_id: str(block.id),
                    },
                });
            }
        }
        return entries;
    }
    if (role === "toolResult") {
        let resultContent = content;
        if (Array.isArray(resultContent)) {
            resultContent = await processImageContent(resultContent, sidecarIndex, stats, cache);
        }
        const resultText = typeof resultContent === "string"
            ? resultContent
            : JSON.stringify(resultContent);
        return [{
                role: "tool_result",
                content: resultText,
                timestamp: ts,
                metadata: {
                    name: str(piMessage.toolName),
                    tool_use_id: str(piMessage.toolCallId),
                    is_error: piMessage.isError === true,
                },
            }];
    }
    return [];
}
export async function convertSdkMessages(messages, sidecarIndex = null, stats = emptyRecoveryStats(), cache) {
    const entries = [];
    for (const msg of messages) {
        if (msg.type === "message") {
            entries.push(...await convertPiCodingMessage(msg, sidecarIndex, stats, cache));
            continue;
        }
        const mtype = msg.type ?? "";
        const ts = messageTimestamp(msg);
        if (mtype === "user") {
            const content = msg.message?.content;
            if (typeof content === "string") {
                if (detectTurnStart(content)) {
                    entries.push({ role: "context", content, timestamp: ts });
                }
                else {
                    entries.push({ role: "user_message", content, timestamp: ts });
                }
            }
            else if (Array.isArray(content)) {
                const hasTurnStart = detectTurnStartInContent(content) !== null;
                if (hasTurnStart) {
                    const processed = await processImageContent(content, sidecarIndex, stats, cache);
                    const textParts = processed
                        .filter((b) => b.type === "text")
                        .map((b) => str(b.text));
                    const hasImage = content.some((b) => b !== null &&
                        typeof b === "object" &&
                        b.type === "image");
                    let ctxText = textParts.join("\n");
                    if (hasImage && !sidecarIndex)
                        ctxText += " [+image:base64]";
                    const entry = { role: "context", content: ctxText, timestamp: ts };
                    if (hasImage && sidecarIndex) {
                        const imageBlocks = processed.filter((b) => b !== null &&
                            typeof b === "object" &&
                            b.type === "image");
                        if (imageBlocks.length > 0) {
                            entry.metadata = { images: imageBlocks };
                        }
                    }
                    entries.push(entry);
                }
                else {
                    for (const block of content) {
                        if (block !== null &&
                            typeof block === "object" &&
                            block.type === "tool_result") {
                            const b = block;
                            let resultContent = b.content;
                            if (Array.isArray(resultContent)) {
                                resultContent = await processImageContent(resultContent, sidecarIndex, stats, cache);
                            }
                            const resultText = typeof resultContent === "string"
                                ? resultContent
                                : JSON.stringify(resultContent);
                            const meta = {
                                tool_use_id: str(b.tool_use_id),
                            };
                            if (b.name)
                                meta.name = str(b.name);
                            if ("is_error" in b)
                                meta.is_error = b.is_error;
                            entries.push({
                                role: "tool_result",
                                content: resultText,
                                timestamp: ts,
                                metadata: meta,
                            });
                        }
                    }
                }
            }
        }
        else if (mtype === "assistant") {
            const contentBlocks = msg.message?.content;
            if (!Array.isArray(contentBlocks))
                continue;
            for (const block of contentBlocks) {
                if (block === null || typeof block !== "object")
                    continue;
                const b = block;
                const btype = b.type;
                if (btype === "thinking") {
                    entries.push({
                        role: "thinking",
                        content: str(b.thinking),
                        timestamp: ts,
                    });
                }
                else if (btype === "text") {
                    entries.push({
                        role: "assistant",
                        content: str(b.text),
                        timestamp: ts,
                    });
                }
                else if (btype === "tool_use") {
                    entries.push({
                        role: "tool_call",
                        content: str(b.name),
                        timestamp: ts,
                        metadata: {
                            name: str(b.name),
                            input: b.input ?? {},
                            tool_use_id: str(b.id),
                        },
                    });
                }
            }
        }
    }
    return entries;
}
export async function buildSdkTrajectory(turn, sidecarIndex = null, stats = emptyRecoveryStats(), cache) {
    return convertSdkMessages(turn.messages, sidecarIndex, stats, cache);
}
export function buildSdkTurnIndex(allTurns) {
    const exact = new Map();
    const bySession = new Map();
    const wakeTurns = [];
    for (const t of allTurns) {
        exact.set(`${t.sessionId}:${t.timestamp}`, t);
        let sessionList = bySession.get(t.sessionId);
        if (!sessionList) {
            sessionList = [];
            bySession.set(t.sessionId, sessionList);
        }
        sessionList.push(t);
        if (t.startType === "wake_event") {
            wakeTurns.push(t);
        }
    }
    return { exact, bySession, wakeTurns };
}
function localToUtcApprox(localTs) {
    try {
        const dt = new Date(localTs);
        if (isNaN(dt.getTime()))
            return "";
        const iso = dt.toISOString();
        return iso.replace(/(\.\d{3})\d*Z$/, "$1Z");
    }
    catch {
        return "";
    }
}
/**
 * Match a specific session turn within a log wake cycle to an SDK session turn.
 *
 * @param logTimestamp - The log-side timestamp (local timezone) of the
 *   system-init entry for this session turn.
 * @param sessionId - The SDK session UUID.
 * @param index - The pre-built SDK turn index.
 */
function fuzzyMatchTurn(logTimestamp, candidates, maxDeltaMs) {
    const targetMs = new Date(logTimestamp).getTime();
    if (isNaN(targetMs))
        return undefined;
    let best;
    let bestDelta = maxDeltaMs;
    for (const st of candidates) {
        const stMs = new Date(st.timestamp.replace("Z", "+00:00")).getTime();
        if (isNaN(stMs))
            continue;
        const delta = Math.abs(stMs - targetMs);
        if (delta < bestDelta) {
            bestDelta = delta;
            best = st;
        }
    }
    return best;
}
export function matchSdkTurn(logTimestamp, sessionId, index, options) {
    const utcTs = localToUtcApprox(logTimestamp);
    if (utcTs) {
        const exact = index.exact.get(`${sessionId}:${utcTs}`);
        if (exact)
            return exact;
    }
    const sessionTurns = index.bySession.get(sessionId);
    if (sessionTurns) {
        const matched = fuzzyMatchTurn(logTimestamp, sessionTurns, 5000);
        if (matched)
            return matched;
    }
    if (options?.crossSessionWakeFallback) {
        const wakeTs = options.wakeTimestamp ?? logTimestamp;
        return matchSdkTurnByWakeTime(wakeTs, index, options.maxCrossSessionDeltaMs ?? 30_000);
    }
    return undefined;
}
/**
 * Match a log wake cycle to any SDK/Pi transcript turn by timestamp alone.
 * Used when Pi logs reference a different session UUID than the transcript file.
 */
export function matchSdkTurnByWakeTime(logTimestamp, index, maxDeltaMs = 30_000) {
    return fuzzyMatchTurn(logTimestamp, index.wakeTurns, maxDeltaMs);
}
//# sourceMappingURL=sdk-transcript.js.map