import { composeThinkingAndContent, extractContentFromMessage, extractThinkingFromMessage, resolveFinalAssistantText, } from "./tui-formatters.js";
function extractTextBlocksAndSignals(message) {
    if (!message || typeof message !== "object") {
        return { textBlocks: [], sawNonTextContentBlocks: false };
    }
    const record = message;
    const content = record.content;
    if (typeof content === "string") {
        const text = content.trim();
        return {
            textBlocks: text ? [text] : [],
            sawNonTextContentBlocks: false,
        };
    }
    if (!Array.isArray(content)) {
        return { textBlocks: [], sawNonTextContentBlocks: false };
    }
    const textBlocks = [];
    let sawNonTextContentBlocks = false;
    for (const block of content) {
        if (!block || typeof block !== "object") {
            continue;
        }
        const rec = block;
        if (rec.type === "text" && typeof rec.text === "string") {
            const text = rec.text.trim();
            if (text) {
                textBlocks.push(text);
            }
            continue;
        }
        if (typeof rec.type === "string" && rec.type !== "thinking") {
            sawNonTextContentBlocks = true;
        }
    }
    return { textBlocks, sawNonTextContentBlocks };
}
function isDroppedBoundaryTextBlockSubset(params) {
    const { streamedTextBlocks, finalTextBlocks } = params;
    if (finalTextBlocks.length === 0 || finalTextBlocks.length >= streamedTextBlocks.length) {
        return false;
    }
    const prefixMatches = finalTextBlocks.every((block, index) => streamedTextBlocks[index] === block);
    if (prefixMatches) {
        return true;
    }
    const suffixStart = streamedTextBlocks.length - finalTextBlocks.length;
    return finalTextBlocks.every((block, index) => streamedTextBlocks[suffixStart + index] === block);
}
export class TuiStreamAssembler {
    runs = new Map();
    getOrCreateRun(runId) {
        let state = this.runs.get(runId);
        if (!state) {
            state = {
                thinkingText: "",
                contentText: "",
                contentBlocks: [],
                sawNonTextContentBlocks: false,
                displayText: "",
            };
            this.runs.set(runId, state);
        }
        return state;
    }
    updateRunState(state, message, showThinking) {
        const thinkingText = extractThinkingFromMessage(message);
        const contentText = extractContentFromMessage(message);
        const { textBlocks, sawNonTextContentBlocks } = extractTextBlocksAndSignals(message);
        if (thinkingText) {
            state.thinkingText = thinkingText;
        }
        if (contentText) {
            state.contentText = contentText;
            state.contentBlocks = textBlocks.length > 0 ? textBlocks : [contentText];
        }
        if (sawNonTextContentBlocks) {
            state.sawNonTextContentBlocks = true;
        }
        const displayText = composeThinkingAndContent({
            thinkingText: state.thinkingText,
            contentText: state.contentText,
            showThinking,
        });
        state.displayText = displayText;
    }
    ingestDelta(runId, message, showThinking) {
        const state = this.getOrCreateRun(runId);
        const previousDisplayText = state.displayText;
        this.updateRunState(state, message, showThinking);
        if (!state.displayText || state.displayText === previousDisplayText) {
            return null;
        }
        return state.displayText;
    }
    finalize(runId, message, showThinking) {
        const state = this.getOrCreateRun(runId);
        const streamedDisplayText = state.displayText;
        const streamedTextBlocks = [...state.contentBlocks];
        const streamedSawNonTextContentBlocks = state.sawNonTextContentBlocks;
        this.updateRunState(state, message, showThinking);
        const finalComposed = state.displayText;
        const shouldKeepStreamedText = streamedSawNonTextContentBlocks &&
            isDroppedBoundaryTextBlockSubset({
                streamedTextBlocks,
                finalTextBlocks: state.contentBlocks,
            });
        const finalText = resolveFinalAssistantText({
            finalText: shouldKeepStreamedText ? streamedDisplayText : finalComposed,
            streamedText: streamedDisplayText,
        });
        this.runs.delete(runId);
        return finalText;
    }
    drop(runId) {
        this.runs.delete(runId);
    }
}
