import { setJobPhase } from "./inflight.js";
import { logEvent } from "./logger.js";
function createState() {
    return {
        images: [],
        eventTypes: {},
        outputItemSummary: [],
        usage: null,
        textOutput: "",
        finalTextOutput: null,
        webSearchCalls: 0,
        eventCount: 0,
        extraIgnored: 0,
        chunkCount: 0,
        bytesRead: 0,
        maxChunkBytes: 0,
        lfBoundaryCount: 0,
        crlfBoundaryCount: 0,
        parseSkipCount: 0,
        finalBufferChars: 0,
        sawDoneSentinel: false,
        sawResponseCompleted: false,
        imageCallSeen: false,
        imageCallCompleted: false,
        imageCallFailed: false,
        imageResultCount: 0,
        webSearchCallSeen: false,
        messageOutputSeen: false,
    };
}
const MAX_DIAGNOSTIC_LABEL_CHARS = 120;
const UNSAFE_DIAGNOSTIC_LABEL = /(bearer\s+|sk-[a-z0-9_-]{4,}|data:image\/|https?:\/\/|[a-z][a-z0-9+.-]*:\/\/|@|[\r\n])/i;
const SAFE_DIAGNOSTIC_LABEL = /^[A-Za-z0-9_.:[\]-]+$/;
export function safeDiagnosticLabel(value, fallback = null) {
    if (typeof value !== "string" || value.length === 0)
        return fallback;
    const trimmed = value.slice(0, MAX_DIAGNOSTIC_LABEL_CHARS);
    if (UNSAFE_DIAGNOSTIC_LABEL.test(trimmed))
        return "_redacted";
    if (!SAFE_DIAGNOSTIC_LABEL.test(trimmed))
        return "_redacted";
    return trimmed;
}
function extractSseData(block) {
    let eventData = "";
    for (const rawLine of block.split(/\r?\n/)) {
        const line = rawLine.replace(/\r$/, "");
        if (line.startsWith("data:"))
            eventData += line.slice(5).trimStart();
    }
    return eventData;
}
function nextSseBlock(buffer) {
    const match = /\r?\n\r?\n/.exec(buffer);
    if (!match)
        return null;
    return {
        block: buffer.slice(0, match.index),
        rest: buffer.slice(match.index + match[0].length),
        delimiter: match[0],
    };
}
function extractPartialImage(data) {
    if (typeof data?.type !== "string" || !data.type.includes("partial"))
        return null;
    const item = data.item || {};
    const b64 = data.partial_image_b64 ||
        data.partial_image ||
        data.image ||
        data.result ||
        item.partial_image_b64 ||
        item.partial_image ||
        item.image ||
        item.result;
    if (typeof b64 !== "string" || b64.length === 0)
        return null;
    const index = typeof data.partial_image_index === "number" && Number.isFinite(data.partial_image_index)
        ? data.partial_image_index
        : typeof data.index === "number" && Number.isFinite(data.index)
            ? data.index
            : typeof item.partial_image_index === "number" && Number.isFinite(item.partial_image_index)
                ? item.partial_image_index
                : typeof item.index === "number" && Number.isFinite(item.index)
                    ? item.index
                    : null;
    return { b64, index };
}
function extractTextDelta(data) {
    if (data.type === "response.output_text.delta" && typeof data.delta === "string")
        return data.delta;
    return null;
}
function extractFinalText(data) {
    if (data.type === "response.output_text.done" && typeof data.text === "string")
        return cleanTextOutput(data.text);
    if (data.type === "response.output_item.done" && data.item?.type === "message") {
        return extractJsonItemText(data.item);
    }
    return null;
}
function extractJsonItemText(item) {
    if (item.type === "output_text" && typeof item.text === "string")
        return cleanTextOutput(item.text);
    if (!Array.isArray(item.content))
        return null;
    const text = item.content
        .filter((part) => part.type === "output_text" && typeof part.text === "string")
        .map((part) => part.text)
        .join("\n\n");
    return cleanTextOutput(text);
}
function cleanTextOutput(value) {
    const trimmed = value.trim();
    return trimmed ? trimmed.slice(0, 4_000) : null;
}
function summarizeItem(eventType, item) {
    const result = typeof item.result === "string" ? item.result : "";
    const revised = typeof item.revised_prompt === "string" ? item.revised_prompt : "";
    return {
        eventType: safeDiagnosticLabel(eventType, "_unknown") || "_unknown",
        itemType: safeDiagnosticLabel(item.type),
        status: safeDiagnosticLabel(item.status),
        hasResult: result.length > 0,
        resultChars: result.length,
        revisedPromptChars: revised.length,
        hasError: Boolean(item.error),
        errorCode: safeDiagnosticLabel(item.error?.code),
        errorType: safeDiagnosticLabel(item.error?.type),
        errorParam: safeDiagnosticLabel(item.error?.param),
    };
}
function recordOutputItem(state, eventType, item) {
    if (!item)
        return;
    const summary = summarizeItem(eventType, item);
    state.outputItemSummary.push(summary);
    if (item.type === "image_generation_call") {
        state.imageCallSeen = true;
        state.imageCallCompleted = state.imageCallCompleted || eventType === "response.output_item.done" || item.status === "completed";
        state.imageCallFailed = state.imageCallFailed || item.status === "failed" || Boolean(item.error);
        if (summary.hasResult)
            state.imageResultCount++;
    }
    if (item.type === "web_search_call")
        state.webSearchCallSeen = true;
    if (item.type === "message")
        state.messageOutputSeen = true;
}
function diagnosticsFromState(state) {
    const outputTextChars = (state.finalTextOutput ?? cleanTextOutput(state.textOutput) ?? "").length;
    return {
        eventTypes: state.eventTypes,
        streamStats: {
            chunkCount: state.chunkCount,
            bytesRead: state.bytesRead,
            maxChunkBytes: state.maxChunkBytes,
            lfBoundaryCount: state.lfBoundaryCount,
            crlfBoundaryCount: state.crlfBoundaryCount,
            parseSkipCount: state.parseSkipCount,
            finalBufferChars: state.finalBufferChars,
            sawDoneSentinel: state.sawDoneSentinel,
            sawResponseCompleted: state.sawResponseCompleted,
        },
        outputItemSummary: state.outputItemSummary,
        imageCallSeen: state.imageCallSeen,
        imageCallCompleted: state.imageCallCompleted,
        imageCallFailed: state.imageCallFailed,
        imageResultCount: state.imageResultCount,
        webSearchCallSeen: state.webSearchCallSeen,
        messageOutputSeen: state.messageOutputSeen,
        outputTextChars,
    };
}
function resultFromState(state) {
    const text = state.finalTextOutput ?? cleanTextOutput(state.textOutput);
    return {
        images: state.images,
        usage: state.usage,
        webSearchCalls: state.webSearchCalls,
        eventCount: state.eventCount,
        eventTypes: state.eventTypes,
        extraIgnored: state.extraIgnored,
        text,
        diagnostics: diagnosticsFromState(state),
    };
}
function makeStreamError(message, code, eventCount, eventType) {
    const err = new Error(message);
    err.code = "RESPONSES_STREAM_ERROR";
    err.upstreamCode = code;
    err.status = 502;
    err.eventCount = eventCount;
    err.eventType = eventType;
    Object.defineProperty(err, "ima2ResponsesError", { value: true });
    return err;
}
async function appendFinalImageFromItem(state, item, maxImages, requestId, onFinalImage) {
    if (item.type !== "image_generation_call" || typeof item.result !== "string" || !item.result)
        return;
    if (state.images.some((image) => image.b64 === item.result))
        return;
    if (state.images.length < maxImages) {
        const image = {
            b64: item.result,
            revisedPrompt: typeof item.revised_prompt === "string" ? item.revised_prompt : null,
        };
        const index = state.images.length;
        state.images.push(image);
        if (requestId)
            setJobPhase(requestId, "decoding");
        await onFinalImage?.(image, index);
    }
    else {
        state.extraIgnored++;
    }
}
export async function parseStream(res, { requestId, scope, maxImages = 1, onPartialImage = null, onFinalImage = null, }) {
    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    const state = createState();
    let buffer = "";
    while (true) {
        const { done, value } = await reader.read();
        if (done)
            break;
        state.chunkCount++;
        state.bytesRead += value.byteLength;
        state.maxChunkBytes = Math.max(state.maxChunkBytes, value.byteLength);
        buffer += decoder.decode(value, { stream: true });
        let next;
        while ((next = nextSseBlock(buffer)) !== null) {
            const block = next.block;
            buffer = next.rest;
            if (next.delimiter.includes("\r\n"))
                state.crlfBoundaryCount++;
            else
                state.lfBoundaryCount++;
            const eventData = extractSseData(block);
            if (eventData === "[DONE]") {
                state.sawDoneSentinel = true;
                continue;
            }
            if (!eventData)
                continue;
            let data;
            try {
                data = JSON.parse(eventData);
            }
            catch {
                state.parseSkipCount++;
                continue;
            }
            state.eventCount++;
            const eventType = safeDiagnosticLabel(data.type, "_unknown") || "_unknown";
            state.eventTypes[eventType] = (state.eventTypes[eventType] || 0) + 1;
            const delta = extractTextDelta(data);
            if (delta)
                state.textOutput += delta;
            const finalText = extractFinalText(data);
            if (finalText)
                state.finalTextOutput = finalText;
            if (finalText)
                state.messageOutputSeen = true;
            const partial = extractPartialImage(data);
            if (partial && typeof onPartialImage === "function")
                onPartialImage(partial);
            if (data.type === "response.output_item.done")
                recordOutputItem(state, data.type, data.item);
            if (data.type === "response.output_item.done" && data.item?.type === "image_generation_call") {
                await appendFinalImageFromItem(state, data.item, maxImages, requestId, onFinalImage);
            }
            if (data.type === "response.output_item.done" && data.item?.type === "web_search_call")
                state.webSearchCalls++;
            if (data.type === "response.completed") {
                state.sawResponseCompleted = true;
                state.usage = data.response?.usage || null;
                for (const item of data.response?.output || []) {
                    recordOutputItem(state, data.type, item);
                    await appendFinalImageFromItem(state, item, maxImages, requestId, onFinalImage);
                }
                const wsNum = data.response?.tool_usage?.web_search?.num_requests;
                if (typeof wsNum === "number" && wsNum > state.webSearchCalls)
                    state.webSearchCalls = wsNum;
            }
            if (data.type === "error") {
                throw makeStreamError("Responses stream returned an error", safeDiagnosticLabel(data.error?.code, "RESPONSES_STREAM_ERROR") || "RESPONSES_STREAM_ERROR", state.eventCount, eventType);
            }
        }
    }
    state.finalBufferChars = buffer.length;
    logEvent(scope, "stream_end", {
        requestId,
        events: state.eventCount,
        imageCount: state.images.length,
        webSearchCalls: state.webSearchCalls,
        imageCallSeen: state.imageCallSeen,
        messageOutputSeen: state.messageOutputSeen,
        bytesRead: state.bytesRead,
        parseSkipCount: state.parseSkipCount,
    });
    return resultFromState(state);
}
export async function parseJson(res, maxImages) {
    const json = await res.json();
    const state = createState();
    state.usage = json.usage || null;
    for (const item of json.output || []) {
        state.eventCount++;
        state.eventTypes["json.output"] = (state.eventTypes["json.output"] || 0) + 1;
        recordOutputItem(state, "json.output", item);
        if (item.type === "image_generation_call" && item.result && state.images.length < maxImages) {
            state.images.push({
                b64: item.result,
                revisedPrompt: typeof item.revised_prompt === "string" ? item.revised_prompt : null,
            });
        }
        if (item.type === "web_search_call")
            state.webSearchCalls++;
        const itemText = extractJsonItemText(item);
        if (itemText)
            state.textOutput += `${state.textOutput ? "\n\n" : ""}${itemText}`;
    }
    return resultFromState(state);
}
