import { logEvent } from "./logger.js";
import { classifyUpstreamError, classifyUpstreamErrorCode } from "./errorClassify.js";
import { compressReferenceB64ForOAuth } from "./referenceImageCompress.js";
import { detectImageMimeFromB64 } from "./refs.js";
import { errInfo } from "./errInfo.js";
import { setJobPhase } from "./inflight.js";
import { requireRuntimeContext } from "./runtimeContext.js";
import { parseJson, parseStream, safeDiagnosticLabel, } from "./responsesParse.js";
import { imageToolChoice, imageToolChoiceKind, tools, toolTypes, } from "./responsesTools.js";
import { emptyResponseError } from "./responsesErrors.js";
import { retryPromptOnlyJsonImage } from "./responsesFallback.js";
import { AUTO_PROMPT_FIDELITY_SUFFIX, DIRECT_PROMPT_FIDELITY_SUFFIX, EDIT_DEVELOPER_PROMPT, EDIT_NO_SEARCH_DEVELOPER_PROMPT, GENERATE_DEVELOPER_PROMPT, GENERATE_NO_SEARCH_DEVELOPER_PROMPT, MULTIMODE_DEVELOPER_PROMPT, MULTIMODE_NO_SEARCH_DEVELOPER_PROMPT, buildEditTextPrompt, buildMultimodeSequencePrompt, buildUserTextPrompt, waitForOAuthReady, } from "./oauthProxy.js";
const RESPONSES_ERROR_MARKER = "ima2ResponsesError";
function makeError(message, { status = 500, code = "RESPONSES_IMAGE_ERROR", cause, ...rest } = {}) {
    const err = new Error(message);
    err.status = status;
    err.code = code;
    if (cause)
        err.cause = cause;
    Object.assign(err, rest);
    Object.defineProperty(err, RESPONSES_ERROR_MARKER, { value: true });
    return err;
}
function parseOpenAIErrorBody(text) {
    try {
        const parsed = JSON.parse(text);
        const error = parsed?.error || {};
        return {
            message: typeof error.message === "string" && error.message ? error.message : "OpenAI request failed",
            code: safeDiagnosticLabel(error.code),
            type: safeDiagnosticLabel(error.type),
            param: safeDiagnosticLabel(error.param),
        };
    }
    catch {
        return null;
    }
}
function normalizedCode(upstream) {
    const byCode = classifyUpstreamErrorCode(upstream?.code);
    if (byCode !== "UNKNOWN")
        return byCode;
    const byType = classifyUpstreamErrorCode(upstream?.type);
    if (byType !== "UNKNOWN")
        return byType;
    const byMessage = classifyUpstreamError(upstream?.message);
    return byMessage !== "UNKNOWN" ? byMessage : "RESPONSES_IMAGE_ERROR";
}
function safeUpstreamClientMessage(upstream, status) {
    const code = normalizedCode(upstream);
    if (code === "AUTH_API_KEY_INVALID")
        return "API key is invalid or unavailable.";
    if (code === "MODERATION_REFUSED")
        return "OpenAI refused the image request for safety reasons.";
    if (code === "INVALID_REQUEST")
        return "OpenAI rejected the image request parameters.";
    if (status === 401 || status === 403)
        return "OpenAI authentication failed.";
    if (status === 429)
        return "OpenAI rate limited the image request.";
    return "OpenAI rejected the image request.";
}
function safeBaseUrl(value) {
    try {
        const parsed = new URL(value);
        parsed.username = "";
        parsed.password = "";
        return parsed.toString().replace(/\/$/, "");
    }
    catch {
        return value.replace(/\/$/, "");
    }
}
function apiAuthorizationHeader(apiKey) {
    const key = typeof apiKey === "string" ? apiKey.trim() : "";
    if (!key) {
        throw makeError("API key is required for API provider image generation", {
            status: 401,
            code: "API_KEY_REQUIRED",
        });
    }
    if (/[\u0000-\u001f\u007f]/.test(key)) {
        throw makeError("API key contains invalid characters.", {
            status: 401,
            code: "AUTH_API_KEY_INVALID",
        });
    }
    return `Bearer ${key}`;
}
function isKnownResponsesError(value) {
    return Boolean(value &&
        typeof value === "object" &&
        value.ima2ResponsesError === true);
}
async function getEndpoint(ctx, provider, _scope) {
    if (provider === "api") {
        return {
            url: "https://api.openai.com/v1/responses",
            headers: {
                "Content-Type": "application/json",
                Accept: "text/event-stream",
                Authorization: apiAuthorizationHeader(ctx.apiKey),
            },
        };
    }
    await waitForOAuthReady(ctx);
    const port = ctx?.config?.oauth?.proxyPort || 10531;
    return {
        url: `${safeBaseUrl(ctx?.oauthUrl || `http://127.0.0.1:${port}`)}/v1/responses`,
        headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
    };
}
function normalizeRef(ref) {
    const b64 = typeof ref === "string" ? ref : ref?.b64;
    const detectedMime = typeof ref === "object" && ref?.detectedMime
        ? ref.detectedMime
        : detectImageMimeFromB64(b64);
    const declaredMime = typeof ref === "object" ? ref?.declaredMime : null;
    const mime = ["image/png", "image/jpeg", "image/webp"].includes(detectedMime)
        ? detectedMime
        : ["image/png", "image/jpeg", "image/webp"].includes(declaredMime)
            ? declaredMime
            : "image/png";
    return { type: "input_image", image_url: `data:${mime};base64,${b64}` };
}
function combineAbortSignals(signals) {
    if (signals.length === 1)
        return signals[0];
    const controller = new AbortController();
    for (const signal of signals) {
        if (signal.aborted) {
            controller.abort();
            break;
        }
        signal.addEventListener("abort", () => controller.abort(), { once: true });
    }
    return controller.signal;
}
async function postResponses({ ctx, provider, scope, payload, requestId, maxImages = 1, signal = null, onPartialImage = null, onFinalImage = null, }) {
    const { url, headers } = await getEndpoint(ctx, provider, scope);
    const timeoutMs = ctx?.config?.oauth?.generationTimeoutMs || 400 * 1000;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    const fetchSignal = signal
        ? combineAbortSignals([controller.signal, signal])
        : controller.signal;
    try {
        const res = await fetch(url, {
            method: "POST",
            headers: headers,
            signal: fetchSignal,
            body: JSON.stringify(payload),
        });
        logEvent(scope, "response", { requestId, provider, status: res.status, contentType: res.headers.get("content-type") });
        if (!res.ok) {
            const text = await res.text();
            const upstream = parseOpenAIErrorBody(text);
            if (res.status >= 400 && res.status < 500 && upstream?.message) {
                throw makeError(safeUpstreamClientMessage(upstream, res.status), {
                    status: res.status,
                    code: normalizedCode(upstream),
                    upstreamBodyChars: text.length,
                    upstreamCode: upstream.code,
                    upstreamType: upstream.type,
                    upstreamParam: upstream.param,
                    upstreamMessageRedacted: true,
                });
            }
            throw makeError(`${provider === "api" ? "OpenAI API" : "OAuth proxy"} returned ${res.status}`, {
                status: res.status,
                upstreamBodyChars: text.length,
            });
        }
        if (requestId)
            setJobPhase(requestId, "streaming");
        const contentType = res.headers.get("content-type") || "";
        return contentType.includes("text/event-stream")
            ? await parseStream(res, { requestId, scope, maxImages, onPartialImage, onFinalImage })
            : await parseJson(res, maxImages);
    }
    catch (e) {
        const err = errInfo(e);
        if (err.name === "AbortError") {
            if (signal?.aborted) {
                throw makeError("Generation canceled", {
                    status: 499,
                    code: "GENERATION_CANCELED",
                    cause: err.raw,
                });
            }
            throw makeError("Responses image generation timed out", { status: 504, code: "RESPONSES_IMAGE_TIMEOUT", cause: err.raw });
        }
        if (isKnownResponsesError(err.raw))
            throw err.raw;
        throw makeError("Responses request failed before receiving a response", {
            status: 502,
            code: "NETWORK_FAILED",
            errorName: err.name,
            upstreamMessageRedacted: true,
        });
    }
    finally {
        clearTimeout(timer);
    }
}
export async function generateViaResponses(provider, prompt, quality, size, moderation = "low", references = [], requestId = null, mode = "auto", ctxRaw = {}, options = {}) {
    const ctx = requireRuntimeContext(ctxRaw);
    const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
    const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
    const requestTools = tools(webSearchEnabled, { quality, size, moderation, ...(options.partialImages ? { partial_images: options.partialImages } : {}) });
    const toolChoice = imageToolChoice(options.forceImageToolChoice ?? ctx.config?.oauth?.forceImageToolChoice !== false);
    const toolChoiceKind = imageToolChoiceKind(toolChoice);
    const referenceInputs = references.map(normalizeRef);
    const userContent = referenceInputs.length
        ? [...referenceInputs, { type: "input_text", text: buildUserTextPrompt(prompt, mode, { webSearchEnabled }) }]
        : buildUserTextPrompt(prompt, mode, { webSearchEnabled });
    const result = await postResponses({
        ctx,
        provider,
        scope: provider === "api" ? "api-generate" : "oauth",
        requestId,
        maxImages: 1,
        signal: options.signal,
        onPartialImage: options.onPartialImage,
        onFinalImage: options.onFinalImage,
        payload: {
            model,
            input: [
                { role: "developer", content: webSearchEnabled ? GENERATE_DEVELOPER_PROMPT : GENERATE_NO_SEARCH_DEVELOPER_PROMPT },
                { role: "user", content: userContent },
            ],
            tools: requestTools,
            tool_choice: toolChoice,
            reasoning: { effort: options.reasoningEffort || "low" },
            stream: true,
        },
    });
    const image = result.images[0];
    if (!image?.b64) {
        if (options.allowPromptOnlyOAuthFallback === true) {
            const fallback = await retryPromptOnlyJsonImage({
                postResponses,
                ctx,
                provider,
                prompt,
                mode,
                model,
                quality,
                size,
                moderation,
                requestId,
                signal: options.signal,
                initial: result,
                referencesDroppedOnRetry: referenceInputs.length > 0,
                webSearchDroppedOnRetry: webSearchEnabled,
                reasoningEffort: options.reasoningEffort,
            });
            if (fallback)
                return fallback;
        }
        throw emptyResponseError("No image data received from Responses API", result, {
            provider,
            model,
            quality,
            size,
            moderation,
            webSearchEnabled,
            refsCount: referenceInputs.length,
            inputImageCount: referenceInputs.length,
            promptChars: typeof prompt === "string" ? prompt.length : 0,
            toolTypes: toolTypes(requestTools),
            toolChoiceKind,
        });
    }
    return { b64: image.b64, usage: result.usage, webSearchCalls: result.webSearchCalls, revisedPrompt: image.revisedPrompt, text: result.text };
}
export async function generateMultimodeViaResponses(provider, prompt, quality, size, moderation = "low", references = [], requestId = null, mode = "auto", ctxRaw = {}, options = {}) {
    const ctx = requireRuntimeContext(ctxRaw);
    const maxImages = Math.min(8, Math.max(1, Math.trunc(Number(options.maxImages) || 1)));
    const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
    const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
    const requestTools = tools(webSearchEnabled, { quality, size, moderation, ...(options.partialImages ? { partial_images: options.partialImages } : {}) });
    const userText = buildMultimodeSequencePrompt(mode === "direct"
        ? `${prompt}${DIRECT_PROMPT_FIDELITY_SUFFIX}`
        : `${prompt}${webSearchEnabled ? "" : ""}${AUTO_PROMPT_FIDELITY_SUFFIX}`, maxImages, { webSearchEnabled });
    const referenceInputs = references.map(normalizeRef);
    const userContent = referenceInputs.length
        ? [...referenceInputs, { type: "input_text", text: userText }]
        : userText;
    return await postResponses({
        ctx,
        provider,
        scope: provider === "api" ? "api-multimode" : "oauth-multimode",
        requestId,
        maxImages,
        signal: options.signal,
        onPartialImage: options.onPartialImage,
        onFinalImage: options.onFinalImage,
        payload: {
            model,
            input: [
                { role: "developer", content: webSearchEnabled ? MULTIMODE_DEVELOPER_PROMPT : MULTIMODE_NO_SEARCH_DEVELOPER_PROMPT },
                { role: "user", content: userContent },
            ],
            tools: requestTools,
            tool_choice: "required",
            reasoning: { effort: options.reasoningEffort || "low" },
            stream: true,
        },
    });
}
export async function editViaResponses(provider, prompt, imageB64, quality, size, moderation = "low", mode = "auto", ctxRaw = {}, requestId = null, options = {}) {
    const ctx = requireRuntimeContext(ctxRaw);
    const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
    const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
    const requestTools = tools(webSearchEnabled, { quality, size, moderation });
    const toolChoice = imageToolChoice(options.forceImageToolChoice ?? ctx.config?.oauth?.forceImageToolChoice !== false);
    const toolChoiceKind = imageToolChoiceKind(toolChoice);
    const imageForRequest = await compressReferenceB64ForOAuth(imageB64, {
        maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
        force: true,
    });
    const referenceImages = await Promise.all((Array.isArray(options.references) ? options.references : []).map((ref) => compressReferenceB64ForOAuth(typeof ref === "string" ? ref : ref?.b64, {
        maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
        force: true,
    })));
    const maskContent = typeof options.mask === "string" && options.mask.length > 0
        ? [
            { type: "input_image", image_url: `data:image/png;base64,${options.mask}` },
            { type: "input_text", text: "The previous image is an edit mask guide. Use it as prompt guidance for where the edit should apply; it is not a visible final image element." },
        ]
        : [];
    const userContent = [
        { type: "input_image", image_url: `data:image/jpeg;base64,${imageForRequest.b64}` },
        ...referenceImages.map(({ b64 }) => ({ type: "input_image", image_url: `data:image/jpeg;base64,${b64}` })),
        ...maskContent,
        { type: "input_text", text: buildEditTextPrompt(prompt, mode, { webSearchEnabled }) },
    ];
    const result = await postResponses({
        ctx,
        provider,
        scope: provider === "api" ? "api-edit" : "oauth-edit",
        requestId,
        maxImages: 1,
        signal: options.signal,
        payload: {
            model,
            input: [
                { role: "developer", content: webSearchEnabled ? EDIT_DEVELOPER_PROMPT : EDIT_NO_SEARCH_DEVELOPER_PROMPT },
                { role: "user", content: userContent },
            ],
            tools: requestTools,
            tool_choice: toolChoice,
            reasoning: { effort: options.reasoningEffort || "low" },
            stream: true,
        },
    });
    const image = result.images[0];
    if (!image?.b64) {
        throw emptyResponseError("No image data received from Responses edit", result, {
            provider,
            model,
            quality,
            size,
            moderation,
            webSearchEnabled,
            refsCount: referenceImages.length,
            inputImageCount: 1 + referenceImages.length + (maskContent.length ? 1 : 0),
            promptChars: typeof prompt === "string" ? prompt.length : 0,
            toolTypes: toolTypes(requestTools),
            toolChoiceKind,
        });
    }
    return { b64: image.b64, usage: result.usage, revisedPrompt: image.revisedPrompt, webSearchCalls: result.webSearchCalls };
}
