import { containsTerminalControlPayloadFinal, containsTerminalControlPayloadPrefix, classifyTerminalControlPayloadPrefixRisk, shouldHoldTerminalControlPayloadPrefix, shouldHoldTerminalTextualToolInvocationPrefix, } from "../provider-turn/terminal-output-classifier.js";
export const DEFAULT_SAFE_TEXT_STREAM_GATE_MAX_BUFFERED_CHARS = 1_024;
const DEFAULT_SAFE_TEXT_STREAM_GATE_MAX_OPEN_FENCE_BUFFERED_CHARS = 8_192;
const DEFAULT_SAFE_TEXT_STREAM_GATE_MAX_AMBIGUOUS_RUNTIME_JSON_CHARS = 8_192;
const DEFAULT_SAFE_TEXT_STREAM_GATE_MAX_STANDALONE_JSON_CHARS = 128 * 1_024;
const DEFAULT_SAFE_TEXT_STREAM_GATE_STRUCTURED_PREVIEW_MIN_CHARS = 160;
export function createSafeTextStreamGate(input) {
    const mode = input.mode ??
        (input.allowTextPreview === false ? "structured-decision-preview" : "respond-preview");
    const allowTextPreview = mode !== "structured-decision-preview";
    const maxBufferedChars = input.maxBufferedChars ?? DEFAULT_SAFE_TEXT_STREAM_GATE_MAX_BUFFERED_CHARS;
    const maxOpenFenceBufferedChars = input.maxOpenFenceBufferedChars ?? DEFAULT_SAFE_TEXT_STREAM_GATE_MAX_OPEN_FENCE_BUFFERED_CHARS;
    const maxAmbiguousRuntimeJsonChars = input.maxAmbiguousRuntimeJsonChars ??
        DEFAULT_SAFE_TEXT_STREAM_GATE_MAX_AMBIGUOUS_RUNTIME_JSON_CHARS;
    const maxStandaloneJsonChars = input.maxStandaloneJsonChars ?? DEFAULT_SAFE_TEXT_STREAM_GATE_MAX_STANDALONE_JSON_CHARS;
    const structuredPreviewMinEmitChars = input.structuredPreviewMinEmitChars ??
        DEFAULT_SAFE_TEXT_STREAM_GATE_STRUCTURED_PREVIEW_MIN_CHARS;
    const toolNames = input.toolNames ?? [];
    let blocked = false;
    let pendingText = "";
    let structuredPreviewOpened = false;
    const pendingEmit = (length = pendingText.length) => {
        if (length <= 0) {
            return { kind: "hold" };
        }
        const pending = pendingText.slice(0, length);
        pendingText = pendingText.slice(length);
        return pending ? { kind: "emit", text: pending } : { kind: "hold" };
    };
    const suppressPending = (length = pendingText.length) => {
        const deltaLength = Math.min(Math.max(0, length), pendingText.length);
        pendingText = pendingText.slice(Math.max(0, length));
        return deltaLength > 0
            ? { deltaLength, kind: "suppress", reasonCode: "structured-preview-disabled" }
            : { kind: "hold" };
    };
    const block = () => {
        blocked = true;
        return { kind: "block", reasonCode: "terminal-control-payload" };
    };
    const previewEmitOrSuppress = (length = pendingText.length, final = false) => {
        if (allowTextPreview) {
            return pendingEmit(length);
        }
        if (mode === "structured-decision-preview" &&
            (structuredPreviewOpened || length >= structuredPreviewMinEmitChars)) {
            structuredPreviewOpened = true;
            return pendingEmit(length);
        }
        return final ? suppressPending(length) : { kind: "hold" };
    };
    const evaluate = (final) => {
        if (blocked) {
            return { kind: "hold" };
        }
        const unclosedFenceStart = findUnclosedMarkdownCodeFenceStart(pendingText);
        if (unclosedFenceStart !== undefined) {
            const beforeFence = pendingText.slice(0, unclosedFenceStart);
            if (beforeFence) {
                const beforeFenceResult = evaluateSegments(beforeFence, final);
                if (beforeFenceResult.kind === "block") {
                    return block();
                }
                if (beforeFenceResult.kind === "hold") {
                    return beforeFenceResult;
                }
            }
            const openFenceLength = pendingText.length - unclosedFenceStart;
            if (openFenceLength >= maxOpenFenceBufferedChars) {
                return block();
            }
            if (final) {
                const fenceText = pendingText.slice(unclosedFenceStart);
                if (containsTerminalControlPayloadFinal(fenceText, toolNames, {
                    ignoreMarkdownCodeFences: false,
                })) {
                    return block();
                }
                return previewEmitOrSuppress(undefined, final);
            }
            if (unclosedFenceStart > 0) {
                return previewEmitOrSuppress(unclosedFenceStart, final);
            }
            return { kind: "hold" };
        }
        const segmentResult = evaluateSegments(pendingText, final);
        if (segmentResult.kind === "block") {
            return block();
        }
        if (segmentResult.kind === "hold") {
            return segmentResult;
        }
        return previewEmitOrSuppress(undefined, final);
    };
    const evaluateSegments = (rawText, final) => {
        const segments = segmentCandidates(rawText);
        if (segments.length === 0) {
            return final ? { kind: "emit" } : { kind: "hold" };
        }
        if (segments.some(({ text }) => containsTerminalControlPayloadFinal(text, toolNames))) {
            return { kind: "block" };
        }
        const openStandaloneJsonBlocks = findOpenStandaloneJsonBlocks(rawText);
        if (final) {
            for (const block of openStandaloneJsonBlocks) {
                const prefixRisk = classifyTerminalControlPayloadPrefixRisk(block.text, toolNames);
                if (containsTerminalControlPayloadPrefix(block.text, toolNames) ||
                    prefixRisk === "strong-control-prefix" ||
                    prefixRisk === "ambiguous-runtime-kind-json-prefix" ||
                    prefixRisk === "ambiguous-provider-native-json-prefix") {
                    return { kind: "block" };
                }
            }
        }
        if (!final) {
            for (const segment of segments) {
                if (containsTerminalControlPayloadPrefix(segment.text, toolNames)) {
                    return segment.rawLength >= maxBufferedChars ? { kind: "block" } : { kind: "hold" };
                }
            }
            for (const segment of segments) {
                const prefixRisk = classifyTerminalControlPayloadPrefixRisk(segment.text, toolNames);
                if (prefixRisk === "strong-control-prefix") {
                    return segment.rawLength >= maxBufferedChars ? { kind: "block" } : { kind: "hold" };
                }
                if (prefixRisk === "ambiguous-runtime-kind-json-prefix") {
                    return segment.rawLength >= maxAmbiguousRuntimeJsonChars
                        ? { kind: "block" }
                        : { kind: "hold" };
                }
                if (prefixRisk === "ambiguous-provider-native-json-prefix") {
                    return segment.rawLength >= maxAmbiguousRuntimeJsonChars
                        ? { kind: "block" }
                        : { kind: "hold" };
                }
                if (prefixRisk === "standalone-open-json-prefix") {
                    return segment.rawLength >= maxStandaloneJsonChars ? { kind: "block" } : { kind: "hold" };
                }
                if (shouldHoldTerminalControlPayloadPrefix(segment.text)) {
                    return segment.rawLength >= maxBufferedChars ? { kind: "block" } : { kind: "hold" };
                }
                if (shouldHoldTerminalTextualToolInvocationPrefix(segment.text, toolNames, {
                    holdSingleCharacterPrefix: mode === "structured-decision-preview",
                })) {
                    return segment.rawLength >= maxBufferedChars ? { kind: "block" } : { kind: "hold" };
                }
            }
        }
        return { kind: "emit" };
    };
    return {
        flush: () => evaluate(true),
        push: (delta) => {
            if (blocked) {
                return { kind: "hold" };
            }
            pendingText += delta;
            return evaluate(false);
        },
    };
}
function segmentCandidates(rawText) {
    const fullText = rawText.trimStart();
    const currentLineRaw = rawText.slice(rawText.lastIndexOf("\n") + 1);
    const currentLineText = currentLineRaw.trimStart();
    const segments = [
        { rawLength: rawText.length, text: fullText },
        { rawLength: currentLineRaw.length, text: currentLineText },
        ...standaloneJsonCandidateSegments(rawText),
    ];
    const seen = new Set();
    return segments.filter((segment) => {
        if (!segment.text || seen.has(segment.text)) {
            return false;
        }
        seen.add(segment.text);
        return true;
    });
}
function findOpenStandaloneJsonBlocks(text) {
    return standaloneJsonCandidateSegments(text)
        .map((segment) => scanOpenStandaloneJsonBlock(segment.text, text.length - segment.rawLength))
        .filter((block) => block !== undefined);
}
function standaloneJsonCandidateSegments(rawText) {
    const segments = [];
    let inFence = false;
    let lineStart = 0;
    while (lineStart <= rawText.length) {
        const nextLineBreak = rawText.indexOf("\n", lineStart);
        const lineEnd = nextLineBreak === -1 ? rawText.length : nextLineBreak;
        const line = rawText.slice(lineStart, lineEnd);
        const leadingWhitespaceLength = line.length - line.trimStart().length;
        const contentStart = lineStart + leadingWhitespaceLength;
        const trimmedLine = line.trimStart();
        if (trimmedLine.startsWith("```")) {
            inFence = !inFence;
        }
        else if (!inFence) {
            const openingChar = rawText[contentStart];
            if (openingChar === "{" || openingChar === "[") {
                segments.push({
                    rawLength: rawText.length - contentStart,
                    text: rawText.slice(contentStart),
                });
            }
        }
        if (nextLineBreak === -1) {
            break;
        }
        lineStart = nextLineBreak + 1;
    }
    return segments;
}
function scanOpenStandaloneJsonBlock(text, startIndex) {
    const openingChar = text[0];
    if (openingChar !== "{" && openingChar !== "[") {
        return undefined;
    }
    let arrayDepth = 0;
    let escaped = false;
    let inString = false;
    let objectDepth = 0;
    for (let index = 0; index < text.length; index += 1) {
        const char = text[index];
        if (escaped) {
            escaped = false;
            continue;
        }
        if (inString && char === "\\") {
            escaped = true;
            continue;
        }
        if (char === '"') {
            inString = !inString;
            continue;
        }
        if (inString) {
            continue;
        }
        if (char === "{") {
            objectDepth += 1;
        }
        else if (char === "}") {
            objectDepth -= 1;
        }
        else if (char === "[") {
            arrayDepth += 1;
        }
        else if (char === "]") {
            arrayDepth -= 1;
        }
        if (index > 0 && objectDepth <= 0 && arrayDepth <= 0) {
            return undefined;
        }
    }
    const depth = objectDepth + arrayDepth;
    return depth > 0
        ? {
            depth,
            escaped,
            inString,
            openingChar,
            startIndex,
            text,
        }
        : undefined;
}
function findUnclosedMarkdownCodeFenceStart(value) {
    let searchIndex = 0;
    let openFenceStart;
    while (searchIndex < value.length) {
        const markerIndex = value.indexOf("```", searchIndex);
        if (markerIndex === -1) {
            break;
        }
        openFenceStart = openFenceStart === undefined ? markerIndex : undefined;
        searchIndex = markerIndex + 3;
    }
    return openFenceStart;
}
//# sourceMappingURL=safe-text-stream-gate.js.map