import path from 'node:path';
import fsp from 'node:fs/promises';
import { parseShellEnvValue } from '../codex-lb/codex-lb-env.js';
import { ensureDir, exists, nowIso, projectRoot, readJson, readText, writeJsonAtomic } from '../fsx.js';
import { sha256File, imageDimensions } from '../wiki-image/image-hash.js';
import { detectImagegenCapability } from '../imagegen/imagegen-capability.js';
import { validateGptImage2Request } from '../imagegen/gpt-image-2-request-validator.js';
import { withResponsesRetry } from '../responses-retry-policy.js';
import { discoverCodexAppGeneratedImage } from './codex-app-generated-image-discovery.js';
import { writeImageArtifactPathContract } from '../image/image-artifact-path-contract.js';
import { registerImageArtifact } from '../image/image-artifact-registry.js';
const DEFAULT_OPENAI_IMAGE_EDITS_ENDPOINT = 'https://api.openai.com/v1/images/edits';
export function buildCalloutPrompt(sourceScreenId, context = {}) {
    return [
        'Review this UI screenshot as a senior product design lead.',
        `Screenshot source id: ${sourceScreenId}.`,
        'Output must be a new image artifact, not prose.',
        'Text-only response is invalid.',
        'Use the screenshot as the reference image input and preserve original-resolution visual evidence as much as the host allows.',
        'Overlay numbered callouts on concrete visible UI regions.',
        'Each callout must include P0/P1/P2/P3 severity labels.',
        'Mark visual hierarchy, contrast, alignment, density, affordance, and eye-flow arrows.',
        'Include a compact corrected mini-comp or before/after strip for the highest-impact fix.',
        'Use only visible evidence and the provided route context.',
        'Do not invent product requirements.',
        context?.target ? `Target surface: ${context.target}.` : ''
    ].filter(Boolean).join(' ');
}
export async function detectCodexAppImagegenCapability(opts = {}) {
    const capability = await detectImagegenCapability(opts).catch(() => null);
    const codexApp = capability?.codex_app || {
        available: false,
        detector: 'capability_detection_failed',
        raw: null
    };
    const available = codexApp.available === true;
    return {
        schema: 'sks.codex-app-imagegen-capability.v1',
        ok: true,
        available,
        status: available ? 'available' : 'integration_optional',
        detector: codexApp.detector || 'codex_features_list',
        raw: codexApp.raw || null
    };
}
export function createCodexAppImagegenAdapter(opts = {}) {
    const available = opts.available === true || process.env.SKS_CODEX_APP_IMAGEGEN_AVAILABLE === '1';
    return {
        surface: 'codex_app_imagegen',
        model: 'gpt-image-2',
        available,
        async generateCalloutReview(input) {
            // Manual attach wins; otherwise auto-discover the most recent Codex App
            // GUI $imagegen output from ~/.codex/generated_images so the route does not
            // require the user to pass SKS_CODEX_APP_IMAGEGEN_OUTPUT by hand.
            const manualOutput = opts.outputImagePath || process.env.SKS_CODEX_APP_IMAGEGEN_OUTPUT || null;
            const discovery = !manualOutput && opts.autoDiscoverGeneratedImage !== false
                ? await discoverCodexAppGeneratedImage({
                    codexHome: opts.codexHome,
                    env: opts.env,
                    sinceMs: typeof opts.generatedImageSinceMs === 'number' ? opts.generatedImageSinceMs : null,
                    maxAgeMs: opts.generatedImageMaxAgeMs,
                    nowMs: typeof opts.nowMs === 'number' ? opts.nowMs : Date.now()
                }).catch(() => null)
                : null;
            const suppliedOutput = manualOutput || discovery?.selected?.path || null;
            if (!input?.output_dir && !suppliedOutput) {
                const blocker = available ? 'imagegen_request_output_dir_missing' : 'imagegen_capability_missing';
                return {
                    ok: false,
                    status: 'blocked',
                    generated_image_path: null,
                    output_id: null,
                    blocker,
                    provider: 'codex_app_imagegen',
                    latency_ms: null
                };
            }
            await ensureDir(input.output_dir);
            const requestArtifact = path.join(input.output_dir, 'image-ux-gpt-image-2-request.json');
            const responseArtifact = path.join(input.output_dir, 'image-ux-gpt-image-2-response.json');
            const validation = await validateGptImage2Request({
                provider: 'codex_app_imagegen',
                endpoint: 'Codex App $imagegen',
                model: 'gpt-image-2',
                prompt: input.prompt,
                source_image_path: input.source_image_path,
                output_dir: input.output_dir,
                params: { size: 'auto' },
                privacy: input.privacy
            });
            await writeJsonAtomic(requestArtifact, {
                schema: 'sks.image-ux-gpt-image-2-request.v1',
                created_at: nowIso(),
                provider: 'codex_app_imagegen',
                endpoint: 'Codex App $imagegen',
                model: 'gpt-image-2',
                source_screen_id: input.source_screen_id,
                source_image_path: path.resolve(input.source_image_path),
                prompt: input.prompt,
                validation,
                unsupported_parameters_omitted: ['input_fidelity'],
                privacy: input.privacy
            });
            if (!validation.ok) {
                await writeJsonAtomic(responseArtifact, {
                    schema: 'sks.image-ux-gpt-image-2-response.v1',
                    created_at: nowIso(),
                    provider: 'codex_app_imagegen',
                    model: 'gpt-image-2',
                    ok: false,
                    status: 'blocked',
                    blocker: 'gpt_image_2_request_validation_failed',
                    validation_blockers: validation.blockers,
                    local_only: true
                });
                return { ok: false, status: 'blocked', generated_image_path: null, output_id: null, blocker: 'gpt_image_2_request_validation_failed', provider: 'codex_app_imagegen', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: null };
            }
            if (suppliedOutput && await exists(path.resolve(suppliedOutput))) {
                const dest = path.join(input.output_dir, path.basename(String(suppliedOutput)));
                await fsp.copyFile(path.resolve(suppliedOutput), dest);
                const meta = await generatedImageMetadata(process.cwd(), dest, {
                    source_screen_id: input.source_screen_id,
                    provider_surface: 'codex_app_imagegen',
                    output_id: opts.outputId || null,
                    real_generated: true
                });
                const outputSource = manualOutput ? 'manual_attach' : 'auto_discovered_generated_images';
                const imageContract = await writeGeneratedImagePathContract(input, dest, 'codex_app_imagegen').catch(() => null);
                await writeJsonAtomic(responseArtifact, {
                    schema: 'sks.image-ux-gpt-image-2-response.v1',
                    created_at: nowIso(),
                    provider: 'codex_app_imagegen',
                    model: 'gpt-image-2',
                    ok: true,
                    status: 'generated',
                    output_image_path: dest,
                    output_image_sha256: meta.sha256,
                    output_id: meta.output_id,
                    output_source: outputSource,
                    image_artifact_path_contract: imageContract?.artifact_path || null,
                    discovered_from: discovery?.selected?.path || null,
                    discovery: discovery ? { candidates_considered: discovery.candidates_considered, since_ms: discovery.since_ms, max_age_ms: discovery.max_age_ms } : null,
                    local_only: true
                });
                return {
                    ok: true,
                    status: 'generated',
                    generated_image_path: dest,
                    output_id: opts.outputId || null,
                    blocker: null,
                    provider: 'codex_app_imagegen',
                    output_source: outputSource,
                    request_artifact: requestArtifact,
                    response_artifact: responseArtifact,
                    image_artifact_path_contract: imageContract?.artifact_path || null,
                    latency_ms: null
                };
            }
            await writeJsonAtomic(responseArtifact, {
                schema: 'sks.image-ux-gpt-image-2-response.v1',
                created_at: nowIso(),
                provider: 'codex_app_imagegen',
                model: 'gpt-image-2',
                ok: false,
                status: 'blocked',
                blocker: available ? 'codex_app_imagegen_output_missing' : 'imagegen_capability_missing',
                setup_guidance: available
                    ? 'Codex App image generation is available, but SKS found no fresh generated image. In Codex App run $imagegen/gpt-image-2 to generate the annotated review image (SKS auto-discovers the newest output from ~/.codex/generated_images), or attach it explicitly with SKS_CODEX_APP_IMAGEGEN_OUTPUT.'
                    : 'Codex App image generation was not detected. Run in Codex App with $imagegen/gpt-image-2. For a separate non-Codex API task, explicitly enable the OpenAI Images API fallback and set OPENAI_API_KEY.',
                generated_images_dir: discovery?.generated_images_dir || null,
                discovery_rejected_reason: discovery?.rejected_reason || null,
                local_only: true
            });
            return {
                ok: false,
                status: 'blocked',
                generated_image_path: null,
                output_id: null,
                blocker: available ? 'codex_app_imagegen_output_missing' : 'imagegen_capability_missing',
                provider: 'codex_app_imagegen',
                request_artifact: requestArtifact,
                response_artifact: responseArtifact,
                latency_ms: null
            };
        }
    };
}
export function createFakeImagegenAdapter(opts = {}) {
    return {
        surface: 'fake_imagegen_adapter',
        model: 'gpt-image-2',
        available: opts.available !== false,
        async generateCalloutReview(input) {
            const started = Date.now();
            await ensureDir(input.output_dir);
            const requestArtifact = path.join(input.output_dir, 'image-ux-gpt-image-2-request.json');
            const responseArtifact = path.join(input.output_dir, 'image-ux-gpt-image-2-response.json');
            const validation = await validateGptImage2Request({
                provider: 'fake_imagegen_adapter',
                endpoint: 'local hermetic fixture',
                model: 'gpt-image-2',
                prompt: input.prompt,
                source_image_path: input.source_image_path,
                output_dir: input.output_dir,
                params: { size: 'auto' },
                privacy: input.privacy
            });
            await writeJsonAtomic(requestArtifact, {
                schema: 'sks.image-ux-gpt-image-2-request.v1',
                created_at: nowIso(),
                provider: 'fake_imagegen_adapter',
                endpoint: 'local hermetic fixture',
                model: 'gpt-image-2',
                source_screen_id: input.source_screen_id,
                source_image_path: path.resolve(input.source_image_path),
                prompt: input.prompt,
                validation,
                fake_adapter: true,
                source: 'mock_like_fixture',
                real_generation_claim_allowed: false,
                unsupported_parameters_omitted: ['input_fidelity'],
                privacy: input.privacy
            });
            if (!validation.ok) {
                await writeJsonAtomic(responseArtifact, {
                    schema: 'sks.image-ux-gpt-image-2-response.v1',
                    created_at: nowIso(),
                    provider: 'fake_imagegen_adapter',
                    model: 'gpt-image-2',
                    ok: false,
                    status: 'blocked',
                    blocker: 'gpt_image_2_request_validation_failed',
                    validation_blockers: validation.blockers,
                    local_only: true
                });
                return { ok: false, status: 'blocked', generated_image_path: null, output_id: null, blocker: 'gpt_image_2_request_validation_failed', provider: 'fake_imagegen_adapter', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
            }
            const sourcePath = path.resolve(input.source_image_path);
            const out = path.join(input.output_dir, `fake-gpt-image-2-callout-${input.source_screen_id || 'screen'}.png`);
            await fsp.copyFile(sourcePath, out);
            const meta = await generatedImageMetadata(process.cwd(), out, {
                source_screen_id: input.source_screen_id,
                provider_surface: 'fake_imagegen_adapter',
                output_id: `fake-${Date.now()}`,
                real_generated: false,
                mock: true
            });
            const imageContract = await writeGeneratedImagePathContract(input, out, 'fake_imagegen_adapter').catch(() => null);
            await writeJsonAtomic(responseArtifact, {
                schema: 'sks.image-ux-gpt-image-2-response.v1',
                created_at: nowIso(),
                provider: 'fake_imagegen_adapter',
                model: 'gpt-image-2',
                ok: true,
                status: 'generated',
                output_image_path: out,
                output_image_sha256: meta.sha256,
                output_id: meta.output_id,
                image_artifact_path_contract: imageContract?.artifact_path || null,
                dimensions: { width: meta.width, height: meta.height, format: meta.format },
                latency_ms: Date.now() - started,
                fake_adapter: true,
                source: 'mock_like_fixture',
                real_generated: false,
                mock: true,
                local_only: true
            });
            return { ok: true, status: 'generated', generated_image_path: out, output_id: meta.output_id, blocker: null, provider: 'fake_imagegen_adapter', request_artifact: requestArtifact, response_artifact: responseArtifact, image_artifact_path_contract: imageContract?.artifact_path || null, latency_ms: Date.now() - started };
        }
    };
}
export function createOpenAIImagesApiAdapter(opts = {}) {
    const apiKey = opts.apiKey || process.env.OPENAI_API_KEY || null;
    const allowCodexLbApiFallback = opts.allowCodexLbApiFallback === true || process.env.SKS_IMAGEGEN_ALLOW_CODEX_LB_API_FALLBACK === '1';
    const codexLb = allowCodexLbApiFallback && opts.codexLb?.available === true ? opts.codexLb : null;
    return {
        surface: 'openai_images_api',
        model: 'gpt-image-2',
        available: Boolean(apiKey || codexLb),
        async generateCalloutReview(input) {
            const started = Date.now();
            await ensureDir(input.output_dir);
            const requestArtifact = path.join(input.output_dir, 'image-ux-gpt-image-2-request.json');
            const responseArtifact = path.join(input.output_dir, 'image-ux-gpt-image-2-response.json');
            const sourcePath = path.resolve(input.source_image_path);
            const sourceSha = await sha256File(sourcePath);
            const auth = await resolveImagesApiAuth({ ...opts, apiKey, codexLb });
            const useResponsesImageTool = auth.auth_source === 'CODEX_LB_API_KEY' && Boolean(auth.responses_endpoint);
            const effectiveEndpoint = useResponsesImageTool ? auth.responses_endpoint : auth.endpoint;
            const validation = await validateGptImage2Request({
                provider: 'openai_images_api',
                endpoint: effectiveEndpoint,
                model: 'gpt-image-2',
                prompt: input.prompt,
                source_image_path: sourcePath,
                output_dir: input.output_dir,
                params: { size: 'auto' },
                privacy: input.privacy
            });
            await writeJsonAtomic(requestArtifact, {
                schema: 'sks.image-ux-gpt-image-2-request.v1',
                created_at: nowIso(),
                provider: useResponsesImageTool ? 'openai_responses_image_generation' : 'openai_images_api',
                endpoint: effectiveEndpoint,
                auth_source: auth.auth_source,
                model: 'gpt-image-2',
                responses_model: useResponsesImageTool ? responsesImagegenModel(opts) : null,
                source_screen_id: input.source_screen_id,
                source_image_path: sourcePath,
                source_screenshot_sha256: sourceSha,
                prompt: input.prompt,
                validation,
                image_input_fidelity_note: 'high_fidelity_automatic',
                unsupported_parameters_omitted: ['input_fidelity'],
                privacy: input.privacy
            });
            if (!validation.ok) {
                await writeJsonAtomic(responseArtifact, {
                    schema: 'sks.image-ux-gpt-image-2-response.v1',
                    created_at: nowIso(),
                    provider: 'openai_images_api',
                    model: 'gpt-image-2',
                    ok: false,
                    status: 'blocked',
                    blocker: 'gpt_image_2_request_validation_failed',
                    validation_blockers: validation.blockers,
                    local_only: true
                });
                return { ok: false, status: 'blocked', generated_image_path: null, output_id: null, blocker: 'gpt_image_2_request_validation_failed', provider: 'openai_images_api', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
            }
            if (!auth.apiKey) {
                const blocked = {
                    schema: 'sks.image-ux-gpt-image-2-response.v1',
                    created_at: nowIso(),
                    provider: 'openai_images_api',
                    model: 'gpt-image-2',
                    ok: false,
                    status: 'blocked',
                    blocker: auth.blocker,
                    setup_guidance: auth.auth_source === 'CODEX_LB_API_KEY'
                        ? 'CODEX_LB_API_KEY is only a non-Codex API fallback when explicitly enabled; it does not satisfy Codex App $imagegen evidence. Attach a real Codex App $imagegen output image for full SKS verification.'
                        : 'Set OPENAI_API_KEY only for an explicit non-Codex Images API fallback, or attach a real Codex App $imagegen output image for full SKS verification.',
                    local_only: true
                };
                await writeJsonAtomic(responseArtifact, blocked);
                return { ok: false, status: 'blocked', generated_image_path: null, output_id: null, blocker: auth.blocker, provider: 'openai_images_api', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
            }
            try {
                if (useResponsesImageTool) {
                    const imageDataUrl = `data:${mimeForPath(sourcePath)};base64,${await fsp.readFile(sourcePath, 'base64')}`;
                    const { result: attemptResult, attempts, retry_log } = await withResponsesRetry(async () => {
                        const response = await fetchWithTimeout(effectiveEndpoint, {
                            method: 'POST',
                            headers: { authorization: `Bearer ${auth.apiKey}`, 'content-type': 'application/json' },
                            body: JSON.stringify({
                                model: responsesImagegenModel(opts),
                                input: [{
                                        role: 'user',
                                        content: [
                                            { type: 'input_text', text: input.prompt },
                                            { type: 'input_image', image_url: imageDataUrl }
                                        ]
                                    }],
                                tools: [{ type: 'image_generation', action: 'edit', size: 'auto', ...imagegenQualityParam(opts) }],
                                tool_choice: { type: 'image_generation' }
                            })
                        }, imagegenFetchTimeoutMs(opts));
                        const payload = await readResponsePayload(response, imagegenFetchTimeoutMs(opts));
                        // Retry on transient HTTP status OR an SSE/JSON server_error/rate_limit payload.
                        return { value: { response, payload }, status: response.ok ? null : response.status, code: payloadRetryCode(payload) };
                    }, imagegenRetryOptions(opts));
                    const { response, payload } = attemptResult;
                    if (!response.ok) {
                        await writeJsonAtomic(responseArtifact, redactedImagegenResponse(payload, false, Date.now() - started, 'openai_responses_image_generation', { attempts, retry_log }));
                        return { ok: false, status: 'blocked', generated_image_path: null, output_id: null, blocker: imagegenErrorKind(payload), provider: 'openai_responses_image_generation', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
                    }
                    if (payload?.error) {
                        await writeJsonAtomic(responseArtifact, redactedImagegenResponse(payload, false, Date.now() - started, 'openai_responses_image_generation'));
                        return { ok: false, status: 'blocked', generated_image_path: null, output_id: null, blocker: imagegenErrorKind(payload), provider: 'openai_responses_image_generation', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
                    }
                    const generated = findResponsesImageGenerationOutput(payload);
                    if (!generated?.b64) {
                        await writeJsonAtomic(responseArtifact, redactedImagegenResponse({ ...payload, blocker: 'missing_b64_image_output' }, false, Date.now() - started, 'openai_responses_image_generation'));
                        return { ok: false, status: 'blocked', generated_image_path: null, output_id: generated?.id || null, blocker: 'missing_b64_image_output', provider: 'openai_responses_image_generation', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
                    }
                    const out = path.join(input.output_dir, `gpt-image-2-callout-${Date.now()}.png`);
                    await fsp.writeFile(out, Buffer.from(String(generated.b64), 'base64'));
                    const meta = await generatedImageMetadata(process.cwd(), out, {
                        source_screen_id: input.source_screen_id,
                        provider_surface: 'openai_responses_image_generation',
                        output_id: generated.id || payload?.id || null,
                        real_generated: true
                    });
                    const imageContract = await writeGeneratedImagePathContract(input, out, 'openai_responses_image_generation').catch(() => null);
                    await writeJsonAtomic(responseArtifact, {
                        schema: 'sks.image-ux-gpt-image-2-response.v1',
                        created_at: nowIso(),
                        provider: 'openai_responses_image_generation',
                        model: 'gpt-image-2',
                        responses_model: responsesImagegenModel(opts),
                        auth_source: auth.auth_source,
                        ok: true,
                        status: 'generated',
                        output_image_path: out,
                        output_image_sha256: meta.sha256,
                        output_id: meta.output_id,
                        image_artifact_path_contract: imageContract?.artifact_path || null,
                        dimensions: { width: meta.width, height: meta.height, format: meta.format },
                        latency_ms: Date.now() - started,
                        token_cost_metadata: payload?.usage || null,
                        local_only: true
                    });
                    return { ok: true, status: 'generated', generated_image_path: out, output_id: meta.output_id, blocker: null, provider: 'openai_responses_image_generation', request_artifact: requestArtifact, response_artifact: responseArtifact, image_artifact_path_contract: imageContract?.artifact_path || null, latency_ms: Date.now() - started };
                }
                const sourceBytes = await fsp.readFile(sourcePath);
                const qualityParam = imagegenQualityParam(opts);
                const { result: attemptResult, attempts, retry_log } = await withResponsesRetry(async () => {
                    const form = new FormData();
                    form.append('model', 'gpt-image-2');
                    form.append('prompt', input.prompt);
                    if (qualityParam.quality)
                        form.append('quality', String(qualityParam.quality));
                    form.append('image', new Blob([sourceBytes], { type: mimeForPath(sourcePath) }), path.basename(sourcePath));
                    const response = await fetchWithTimeout(auth.endpoint, {
                        method: 'POST',
                        headers: { authorization: `Bearer ${auth.apiKey}` },
                        body: form
                    }, imagegenFetchTimeoutMs(opts));
                    const payload = await readResponsePayload(response, imagegenFetchTimeoutMs(opts));
                    return { value: { response, payload }, status: response.ok ? null : response.status, code: payloadRetryCode(payload) };
                }, imagegenRetryOptions(opts));
                const { response, payload } = attemptResult;
                if (!response.ok) {
                    await writeJsonAtomic(responseArtifact, redactedImagegenResponse(payload, false, Date.now() - started, 'openai_images_api', { attempts, retry_log }));
                    return { ok: false, status: 'blocked', generated_image_path: null, output_id: null, blocker: imagegenErrorKind(payload), provider: 'openai_images_api', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
                }
                const image = Array.isArray(payload?.data) ? payload.data[0] : null;
                const b64 = image?.b64_json || image?.b64 || null;
                if (!b64) {
                    await writeJsonAtomic(responseArtifact, redactedImagegenResponse({ ...payload, blocker: 'missing_b64_image_output' }, false, Date.now() - started));
                    return { ok: false, status: 'blocked', generated_image_path: null, output_id: image?.id || null, blocker: 'missing_b64_image_output', provider: 'openai_images_api', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
                }
                const out = path.join(input.output_dir, `gpt-image-2-callout-${Date.now()}.png`);
                await fsp.writeFile(out, Buffer.from(String(b64), 'base64'));
                const meta = await generatedImageMetadata(process.cwd(), out, {
                    source_screen_id: input.source_screen_id,
                    provider_surface: 'openai_images_api',
                    output_id: image?.id || payload?.id || null,
                    real_generated: true
                });
                const imageContract = await writeGeneratedImagePathContract(input, out, 'openai_images_api').catch(() => null);
                await writeJsonAtomic(responseArtifact, {
                    schema: 'sks.image-ux-gpt-image-2-response.v1',
                    created_at: nowIso(),
                    provider: 'openai_images_api',
                    model: 'gpt-image-2',
                    auth_source: auth.auth_source,
                    ok: true,
                    status: 'generated',
                    output_image_path: out,
                    output_image_sha256: meta.sha256,
                    output_id: meta.output_id,
                    image_artifact_path_contract: imageContract?.artifact_path || null,
                    dimensions: { width: meta.width, height: meta.height, format: meta.format },
                    latency_ms: Date.now() - started,
                    token_cost_metadata: payload?.usage || null,
                    local_only: true
                });
                return { ok: true, status: 'generated', generated_image_path: out, output_id: meta.output_id, blocker: null, provider: 'openai_images_api', request_artifact: requestArtifact, response_artifact: responseArtifact, image_artifact_path_contract: imageContract?.artifact_path || null, latency_ms: Date.now() - started };
            }
            catch (err) {
                const provider = useResponsesImageTool ? 'openai_responses_image_generation' : 'openai_images_api';
                const payload = { error: { message: err instanceof Error ? err.message : String(err) } };
                const response = redactedImagegenResponse(payload, false, Date.now() - started, provider);
                await writeJsonAtomic(responseArtifact, response);
                return { ok: false, status: 'blocked', generated_image_path: null, output_id: null, blocker: imagegenErrorKind(payload), provider, request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
            }
        }
    };
}
async function writeGeneratedImagePathContract(input, outputPath, provider) {
    const root = await resolveImageArtifactRoot(input);
    if (input.mission_id) {
        await registerImageArtifact(root, {
            missionId: input.mission_id,
            id: `${provider}-${input.source_screen_id || 'screen'}`,
            kind: 'generated_image',
            filePath: outputPath,
            route: '$Image-UX-Review',
            stage: provider
        });
    }
    return writeImageArtifactPathContract(root, {
        missionId: input.mission_id || 'unassigned',
        images: [{
                id: `${provider}-${input.source_screen_id || 'screen'}`,
                kind: 'generated_image',
                filePath: outputPath,
                route: '$Image-UX-Review',
                stage: provider
            }],
        artifactPath: path.join(input.output_dir, 'image-artifact-path-contract.json')
    });
}
async function resolveImageArtifactRoot(input) {
    const cwdRoot = await projectRoot(process.cwd()).catch(() => process.cwd());
    const resolvedCwd = path.resolve(process.cwd());
    if (path.resolve(cwdRoot) !== resolvedCwd)
        return cwdRoot;
    return projectRoot(input.output_dir || process.cwd()).catch(() => cwdRoot);
}
export async function generateGptImage2CalloutReview(input, opts = {}) {
    if (opts.fake === true || process.env.SKS_TEST_FAKE_IMAGEGEN === '1') {
        return createFakeImagegenAdapter(opts.fakeAdapter || {}).generateCalloutReview(input);
    }
    const capability = await detectImagegenCapability(opts.capability || {}).catch(() => null);
    // Auto-enable the OpenAI direct-key API fallback whenever an OPENAI_API_KEY is
    // available, so a missing/failed Codex App $imagegen surface still produces a
    // real image instead of a hard block. Explicit opt-out wins: pass
    // allowApiFallback:false or SKS_IMAGEGEN_ALLOW_API_FALLBACK=0.
    const openAiKeyPresent = Boolean(opts.openai?.apiKey || process.env.OPENAI_API_KEY);
    const explicitDisableApiFallback = opts.allowApiFallback === false || process.env.SKS_IMAGEGEN_ALLOW_API_FALLBACK === '0';
    // codex-lb imagegen is a direct API fallback, not Codex App imagegen evidence.
    // It must be explicitly enabled by the caller or environment.
    const explicitDisableCodexLbFallback = opts.allowCodexLbApiFallback === false || process.env.SKS_IMAGEGEN_ALLOW_CODEX_LB_API_FALLBACK === '0';
    const allowCodexLbApiFallback = !explicitDisableCodexLbFallback && (opts.allowCodexLbApiFallback === true
        || process.env.SKS_IMAGEGEN_ALLOW_CODEX_LB_API_FALLBACK === '1');
    const allowApiFallback = !explicitDisableApiFallback && (opts.allowApiFallback === true
        || process.env.SKS_IMAGEGEN_ALLOW_API_FALLBACK === '1'
        || openAiKeyPresent
        || allowCodexLbApiFallback);
    const openaiOptions = {
        ...(opts.openai || {}),
        codexLb: allowCodexLbApiFallback ? opts.openai?.codexLb || capability?.codex_lb || null : null,
        allowCodexLbApiFallback
    };
    const codexAdapter = createCodexAppImagegenAdapter({
        ...(opts.codexApp || {}),
        available: opts.codexApp?.available === true || capability?.codex_app?.available === true
    });
    const codexResult = await codexAdapter.generateCalloutReview(input);
    if (codexResult.ok || !allowApiFallback)
        return codexResult;
    return createOpenAIImagesApiAdapter(openaiOptions).generateCalloutReview(input);
}
export function imagegenCapabilityBlocker(surface = 'Codex App $imagegen') {
    return {
        schema: 'sks.image-ux-imagegen-blocker.v1',
        status: 'blocked',
        blocker: 'imagegen_capability_missing',
        surface,
        model: 'gpt-image-2',
        guidance: 'Run the request in Codex App with $imagegen/gpt-image-2 and attach the generated annotated review image path. OPENAI_API_KEY or CODEX_LB_API_KEY may be used only for an explicitly requested non-Codex API fallback, and that fallback does not satisfy Codex App imagegen evidence. SKS must not fabricate or substitute a text-only review.'
    };
}
async function resolveImagesApiAuth(opts = {}) {
    const openAiKey = String(opts.apiKey || process.env.OPENAI_API_KEY || '').trim();
    if (openAiKey) {
        return {
            apiKey: openAiKey,
            auth_source: 'OPENAI_API_KEY',
            endpoint: imageEditsEndpoint(opts.baseUrl || 'https://api.openai.com/v1'),
            responses_endpoint: responsesEndpoint(opts.baseUrl || 'https://api.openai.com/v1'),
            blocker: null
        };
    }
    const codexLb = opts.codexLb?.available === true ? opts.codexLb : null;
    if (!codexLb) {
        return {
            apiKey: null,
            auth_source: null,
            endpoint: imageEditsEndpoint(opts.baseUrl || 'https://api.openai.com/v1'),
            responses_endpoint: responsesEndpoint(opts.baseUrl || 'https://api.openai.com/v1'),
            blocker: 'openai_api_key_missing'
        };
    }
    const envKey = codexLb.env_key || 'CODEX_LB_API_KEY';
    const envPath = codexLb.env_path || opts.codexLbEnvPath || '';
    const envText = envPath ? await readText(envPath, '').catch(() => '') : '';
    const codexLbKey = String(opts.codexLbApiKey || process.env[envKey] || parseShellEnvValue(envText, envKey) || '').trim();
    return {
        apiKey: codexLbKey || null,
        auth_source: envKey,
        endpoint: imageEditsEndpoint(codexLb.base_url || opts.baseUrl || ''),
        responses_endpoint: responsesEndpoint(codexLb.base_url || opts.baseUrl || ''),
        blocker: codexLbKey ? null : 'codex_lb_api_key_missing'
    };
}
function imageEditsEndpoint(baseUrl = '') {
    const base = String(baseUrl || DEFAULT_OPENAI_IMAGE_EDITS_ENDPOINT).trim().replace(/\/+$/, '');
    return /\/images\/edits$/i.test(base) ? base : `${base}/images/edits`;
}
function responsesEndpoint(baseUrl = '') {
    const base = String(baseUrl || 'https://api.openai.com/v1').trim().replace(/\/+$/, '');
    return /\/responses$/i.test(base) ? base : `${base}/responses`;
}
function responsesImagegenModel(opts = {}) {
    return String(opts.responsesModel || process.env.SKS_IMAGEGEN_RESPONSES_MODEL || process.env.OPENAI_MODEL || 'gpt-5.5');
}
function imagegenFetchTimeoutMs(opts = {}) {
    const value = Number(opts.fetchTimeoutMs || process.env.SKS_IMAGEGEN_FETCH_TIMEOUT_MS || 90000);
    return Number.isFinite(value) && value > 0 ? value : 90000;
}
async function fetchWithTimeout(url, init, timeoutMs) {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(new Error(`imagegen_fetch_timeout_${timeoutMs}ms`)), timeoutMs);
    try {
        return await fetch(url, { ...init, signal: controller.signal });
    }
    finally {
        clearTimeout(timeout);
    }
}
async function readResponsePayload(response, timeoutMs = 90000) {
    const text = await textWithTimeout(response, timeoutMs);
    if (!text)
        return {};
    try {
        return JSON.parse(text);
    }
    catch {
        const sse = parseSsePayload(text);
        if (sse)
            return sse;
        return { error: { message: text.slice(0, 2000) } };
    }
}
async function textWithTimeout(response, timeoutMs) {
    let timeout = null;
    try {
        return await Promise.race([
            response.text(),
            new Promise((_, reject) => {
                timeout = setTimeout(() => reject(new Error(`imagegen_response_read_timeout_${timeoutMs}ms`)), timeoutMs);
            })
        ]);
    }
    finally {
        if (timeout)
            clearTimeout(timeout);
    }
}
function parseSsePayload(text) {
    const events = [];
    for (const line of text.split(/\r?\n/)) {
        if (!line.startsWith('data:'))
            continue;
        const data = line.slice('data:'.length).trim();
        if (!data || data === '[DONE]')
            continue;
        try {
            events.push(JSON.parse(data));
        }
        catch { }
    }
    const failed = events.find((event) => event?.type === 'response.failed' || event?.response?.status === 'failed');
    if (failed) {
        return {
            object: 'response.sse',
            status: 'failed',
            error: failed?.response?.error || failed?.error || { message: 'responses_sse_failed' },
            events: events.map((event) => event?.type || null)
        };
    }
    const completed = [...events].reverse().find((event) => event?.type === 'response.completed' && event?.response);
    if (completed?.response)
        return completed.response;
    const imageEvent = [...events].reverse().find((event) => /image_generation_call/.test(String(event?.type || '')) && (event?.result || event?.item?.result || event?.b64_json));
    if (imageEvent) {
        return {
            object: 'response.sse',
            status: 'completed',
            output: [{
                    id: imageEvent?.item?.id || imageEvent?.id || null,
                    type: 'image_generation_call',
                    status: imageEvent?.status || imageEvent?.item?.status || 'completed',
                    result: imageEvent?.result || imageEvent?.item?.result || imageEvent?.b64_json || null
                }],
            events: events.map((event) => event?.type || null)
        };
    }
    return events.length ? { object: 'response.sse', status: 'unknown', events: events.map((event) => event?.type || null) } : null;
}
function findResponsesImageGenerationOutput(payload) {
    for (const output of Array.isArray(payload?.output) ? payload.output : []) {
        if (String(output?.type || '') === 'image_generation_call') {
            const b64 = typeof output?.result === 'string' ? output.result : output?.result?.b64_json || output?.b64_json || null;
            if (b64)
                return { b64: String(b64), id: output?.id || payload?.id || null };
        }
    }
    return null;
}
export async function generatedImageMetadata(root, imagePath, opts = {}) {
    const absolute = path.resolve(root, imagePath);
    const dims = await imageDimensions(absolute);
    return {
        id: opts.id || `generated-review-${(await sha256File(absolute)).slice(0, 10)}`,
        path: path.relative(root, absolute).split(path.sep).join('/'),
        sha256: await sha256File(absolute),
        width: dims.width,
        height: dims.height,
        format: dims.format,
        source_screen_id: opts.source_screen_id || null,
        provider_model: 'gpt-image-2',
        provider_surface: opts.provider_surface || 'codex_app_imagegen',
        requested_fidelity: 'high_fidelity_automatic',
        image_input_fidelity_note: 'high_fidelity_automatic',
        privacy: 'local-only',
        output_id: opts.output_id || null,
        created_at: opts.created_at || nowIso(),
        real_generated: opts.real_generated === true,
        mock: opts.mock === true,
        callout_extraction_required: true,
        source: opts.mock ? 'mock_fixture' : 'real_gpt_image_2_callout'
    };
}
function mimeForPath(file) {
    const ext = path.extname(file).toLowerCase();
    if (ext === '.jpg' || ext === '.jpeg')
        return 'image/jpeg';
    if (ext === '.webp')
        return 'image/webp';
    return 'image/png';
}
function redactedImagegenResponse(payload, ok, latencyMs, provider = 'openai_images_api', retry = {}) {
    return {
        schema: 'sks.image-ux-gpt-image-2-response.v1',
        created_at: nowIso(),
        provider,
        model: 'gpt-image-2',
        ok,
        status: ok ? 'generated' : 'blocked',
        blocker: ok ? null : imagegenErrorKind(payload),
        redacted_error: payload?.error?.message ? String(payload.error.message).replace(/sk-[A-Za-z0-9_-]{16,}/g, '[REDACTED_OPENAI_KEY]') : null,
        payload_summary: summarizeImagegenPayload(payload),
        latency_ms: latencyMs,
        attempts: retry.attempts ?? null,
        retry_log: retry.retry_log ?? null,
        local_only: true
    };
}
// Classify a parsed image-API/Responses payload into a retryable error code so a
// 200-with-server_error SSE body or an `error.code` of rate_limit/overloaded is
// retried, not just non-2xx HTTP statuses. Returns null when not retryable.
function payloadRetryCode(payload) {
    if (!payload)
        return null;
    const status = String(payload?.status || '');
    const errorType = String(payload?.error?.type || payload?.error?.code || '');
    const haystack = `${status} ${errorType} ${JSON.stringify(payload?.error || '')}`.toLowerCase();
    if (/rate[_ -]?limit|too many requests|429/.test(haystack))
        return 'rate_limit_exceeded';
    if (/overloaded|proxy_overloaded|server[_ -]?error|temporarily unavailable|unavailable|5\d\d/.test(haystack))
        return 'server_error';
    if (/timeout|timed out|aborted/.test(haystack))
        return 'ETIMEDOUT';
    if (status === 'failed' && /server|overload|unavailable|rate/.test(haystack))
        return 'server_error';
    return null;
}
// gpt-image-2 supports an optional `quality` (low|medium|high|auto). Default to
// 'high' for review callouts so legibility holds; allow override/disable.
function imagegenQualityParam(opts = {}) {
    const raw = String(opts.quality || process.env.SKS_IMAGEGEN_QUALITY || 'high').trim().toLowerCase();
    if (raw === 'none' || raw === 'off' || raw === '')
        return {};
    return ['low', 'medium', 'high', 'auto'].includes(raw) ? { quality: raw } : { quality: 'high' };
}
// Wire imagegen fetches into the centralized responses retry policy: exponential
// backoff on 429/5xx/timeout and transient network errors, classifying a thrown
// fetch error (abort/timeout/network) into a retryable code.
function imagegenRetryOptions(opts = {}) {
    return {
        sleep: opts.retrySleep,
        classifyError: (err) => {
            const code = String(err?.code
                || err?.name
                || err?.message
                || '');
            if (/AbortError|timeout|abort/i.test(code))
                return { code: 'ETIMEDOUT', status: null };
            if (/ECONNRESET|EAI_AGAIN|ETIMEDOUT|ENOTFOUND|ECONNREFUSED|fetch failed/i.test(code))
                return { code: 'ECONNRESET', status: null };
            return { code: 'request_failed', status: null };
        }
    };
}
function summarizeImagegenPayload(payload) {
    const outputs = Array.isArray(payload?.output) ? payload.output : [];
    return {
        id: payload?.id || null,
        object: payload?.object || null,
        status: payload?.status || null,
        model: payload?.model || null,
        error_type: payload?.error?.type || null,
        error_code: payload?.error?.code || null,
        output_count: outputs.length,
        output: outputs.slice(0, 8).map((output) => ({
            id: output?.id || null,
            type: output?.type || null,
            status: output?.status || null,
            action: output?.action || null,
            role: output?.role || null,
            result_present: typeof output?.result === 'string' || Boolean(output?.result?.b64_json || output?.b64_json),
            result_kind: typeof output?.result,
            result_chars: typeof output?.result === 'string' ? output.result.length : null,
            content_types: Array.isArray(output?.content) ? output.content.map((item) => item?.type || null) : []
        })),
        output_text: outputs.flatMap((output) => Array.isArray(output?.content) ? output.content : [])
            .filter((item) => typeof item?.text === 'string')
            .map((item) => item.text.slice(0, 500))
            .slice(0, 3)
    };
}
function imagegenErrorKind(payload) {
    const text = JSON.stringify(payload || {});
    if (/moderation|safety|policy/i.test(text))
        return 'imagegen_moderation_blocked';
    if (/rate_limit|overloaded|proxy_overloaded|429/i.test(text))
        return 'imagegen_remote_rate_limited';
    if (/timeout|AbortError|aborted/i.test(text))
        return 'imagegen_remote_timeout';
    if (/api[_ -]?key|auth|401/i.test(text))
        return 'openai_api_key_missing_or_invalid';
    return payload?.blocker || 'openai_images_api_error';
}
//# sourceMappingURL=imagegen-adapter.js.map