/**
 * Codex Proxy — Anthropic Messages API → OpenAI API
 * Claude Code에서 OpenAI 호환 모델을 백엔드로 사용하는 로컬 프록시
 *
 * chatgpt-pro: Codex Responses API (chatgpt.com/backend-api/codex/responses)
 * openai/antigravity/custom: Chat Completions API (api.openai.com/v1/chat/completions)
 */
import fs from 'fs';
import http from 'http';
import crypto from 'crypto';
import { spawn } from 'child_process';
import { getApiKeyFromConfig, findCodexCredentials, getAuthInfo } from './gpt/auth.js';
import { readGlobalConfig, getAntigravityApiKey } from './config/GlobalConfigManager.js';
import { CHATGPT_BASE_URL } from './gpt/constants.js';
// ─── Constants ───────────────────────────────────────────────
const DEFAULT_TARGET_URL = 'https://api.openai.com';
const CODEX_RESPONSES_URL = `${CHATGPT_BASE_URL}/codex/responses`;
// ─── System Prompt Extraction ────────────────────────────────
function extractSystemText(system) {
    if (!system)
        return '';
    if (typeof system === 'string')
        return system;
    return system.filter(b => b.type === 'text' && b.text).map(b => b.text).join('\n');
}
function translateContentPart(block) {
    if (block.type === 'text')
        return { type: 'text', text: block.text || '' };
    if (block.type === 'image' && block.source?.data) {
        const media = block.source.media_type || 'image/png';
        return { type: 'image_url', image_url: { url: `data:${media};base64,${block.source.data}` } };
    }
    return null;
}
// ─── Assistant Message Translation ───────────────────────────
function translateAssistantMsg(msg) {
    if (typeof msg.content === 'string') {
        return { role: 'assistant', content: msg.content };
    }
    const blocks = msg.content;
    const text = blocks.filter(b => b.type === 'text').map(b => b.text || '').join('');
    const tools = blocks.filter(b => b.type === 'tool_use');
    const result = { role: 'assistant', content: text || null };
    if (tools.length > 0) {
        result.tool_calls = tools.map(t => ({
            id: t.id || `call_${crypto.randomUUID()}`,
            type: 'function',
            function: { name: t.name || '', arguments: JSON.stringify(t.input || {}) },
        }));
    }
    return result;
}
// ─── User Message Translation ────────────────────────────────
function translateUserMsg(msg, result) {
    if (typeof msg.content === 'string') {
        result.push({ role: 'user', content: msg.content });
        return;
    }
    const blocks = msg.content;
    const toolResults = blocks.filter(b => b.type === 'tool_result');
    const others = blocks.filter(b => b.type !== 'tool_result' && b.type !== 'thinking');
    for (const tr of toolResults) {
        const text = typeof tr.content === 'string'
            ? tr.content
            : Array.isArray(tr.content)
                ? tr.content.filter(b => b.type === 'text').map(b => b.text || '').join('')
                : '';
        result.push({ role: 'tool', tool_call_id: tr.tool_use_id || '', content: text });
    }
    if (others.length === 0)
        return;
    const parts = others
        .map(b => translateContentPart(b))
        .filter((p) => p !== null);
    if (parts.length === 1 && parts[0].type === 'text') {
        result.push({ role: 'user', content: parts[0].text || '' });
    }
    else if (parts.length > 0) {
        result.push({ role: 'user', content: parts });
    }
}
// ─── Full Message Array Translation ──────────────────────────
function buildOMessages(system, messages) {
    const result = [];
    const sysText = extractSystemText(system);
    if (sysText)
        result.push({ role: 'system', content: sysText });
    for (const msg of messages) {
        if (msg.role === 'assistant') {
            result.push(translateAssistantMsg(msg));
        }
        else {
            translateUserMsg(msg, result);
        }
    }
    return result;
}
// ─── Tool Definition Translation ─────────────────────────────
function translateTools(tools) {
    if (!tools?.length)
        return undefined;
    return tools.map(t => ({
        type: 'function',
        function: { name: t.name, description: t.description || '', parameters: t.input_schema },
    }));
}
function translateToolChoice(choice) {
    if (!choice)
        return undefined;
    if (choice.type === 'auto')
        return 'auto';
    if (choice.type === 'any')
        return 'required';
    if (choice.type === 'tool' && choice.name) {
        return { type: 'function', function: { name: choice.name } };
    }
    return undefined;
}
// ─── Build OpenAI Request Body ───────────────────────────────
function buildORequest(req, defaultModel) {
    const body = {
        model: defaultModel || req.model,
        messages: buildOMessages(req.system, req.messages),
        max_tokens: req.max_tokens,
        stream: req.stream ?? false,
    };
    if (req.temperature !== undefined)
        body.temperature = req.temperature;
    if (req.top_p !== undefined)
        body.top_p = req.top_p;
    if (req.stop_sequences)
        body.stop = req.stop_sequences;
    const tools = translateTools(req.tools);
    if (tools)
        body.tools = tools;
    const tc = translateToolChoice(req.tool_choice);
    if (tc !== undefined)
        body.tool_choice = tc;
    if (req.stream)
        body.stream_options = { include_usage: true };
    return body;
}
// ─── Finish Reason Mapping ───────────────────────────────────
function mapFinishReason(reason) {
    if (reason === 'stop')
        return 'end_turn';
    if (reason === 'tool_calls')
        return 'tool_use';
    if (reason === 'length')
        return 'max_tokens';
    return 'end_turn';
}
// ─── Build Anthropic Response (Non-Streaming) ────────────────
function buildAResponse(oResp, model) {
    const choices = oResp.choices || [];
    const choice = choices[0] || {};
    const msg = choice.message || {};
    const usage = oResp.usage || {};
    const content = [];
    if (msg.content)
        content.push({ type: 'text', text: msg.content });
    const tcs = msg.tool_calls;
    if (tcs) {
        for (const tc of tcs) {
            const fn = tc.function;
            let input = {};
            try {
                input = JSON.parse(fn.arguments || '{}');
            }
            catch { /* use empty */ }
            content.push({ type: 'tool_use', id: tc.id, name: fn.name, input });
        }
    }
    return {
        id: `msg_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`,
        type: 'message',
        role: 'assistant',
        content,
        model,
        stop_reason: mapFinishReason(choice.finish_reason),
        stop_sequence: null,
        usage: { input_tokens: usage.prompt_tokens || 0, output_tokens: usage.completion_tokens || 0 },
    };
}
// ─── SSE Helpers ─────────────────────────────────────────────
function writeSSE(res, event, data) {
    res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
}
function newStreamState(model) {
    return {
        messageId: `msg_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`,
        model,
        nextBlockIndex: 0,
        textBlockIndex: -1,
        textBlockOpen: false,
        toolBlockMap: new Map(),
        finishReason: null,
        inputTokens: 0,
        outputTokens: 0,
    };
}
// ─── Stream Chunk Processing ─────────────────────────────────
function processChunk(res, s, chunk) {
    if (chunk.usage) {
        s.inputTokens = chunk.usage.prompt_tokens ?? s.inputTokens;
        s.outputTokens = chunk.usage.completion_tokens ?? s.outputTokens;
    }
    const choice = chunk.choices?.[0];
    if (!choice)
        return;
    if (choice.delta?.content) {
        if (s.textBlockIndex < 0) {
            s.textBlockIndex = s.nextBlockIndex++;
            writeSSE(res, 'content_block_start', {
                type: 'content_block_start', index: s.textBlockIndex,
                content_block: { type: 'text', text: '' },
            });
            s.textBlockOpen = true;
        }
        writeSSE(res, 'content_block_delta', {
            type: 'content_block_delta', index: s.textBlockIndex,
            delta: { type: 'text_delta', text: choice.delta.content },
        });
    }
    if (choice.delta?.tool_calls) {
        for (const tc of choice.delta.tool_calls) {
            processToolDelta(res, s, tc);
        }
    }
    if (choice.finish_reason)
        s.finishReason = choice.finish_reason;
}
function processToolDelta(res, s, tc) {
    const idx = tc.index;
    if (!s.toolBlockMap.has(idx)) {
        if (s.textBlockOpen) {
            writeSSE(res, 'content_block_stop', { type: 'content_block_stop', index: s.textBlockIndex });
            s.textBlockOpen = false;
        }
        const blockIdx = s.nextBlockIndex++;
        s.toolBlockMap.set(idx, blockIdx);
        writeSSE(res, 'content_block_start', {
            type: 'content_block_start', index: blockIdx,
            content_block: {
                type: 'tool_use',
                id: tc.id || `call_${crypto.randomUUID()}`,
                name: tc.function?.name || '',
                input: {},
            },
        });
    }
    if (tc.function?.arguments) {
        writeSSE(res, 'content_block_delta', {
            type: 'content_block_delta', index: s.toolBlockMap.get(idx),
            delta: { type: 'input_json_delta', partial_json: tc.function.arguments },
        });
    }
}
// ─── Close All Stream Blocks ─────────────────────────────────
function closeStream(res, s) {
    if (s.textBlockOpen) {
        writeSSE(res, 'content_block_stop', { type: 'content_block_stop', index: s.textBlockIndex });
    }
    for (const [, blockIdx] of s.toolBlockMap) {
        writeSSE(res, 'content_block_stop', { type: 'content_block_stop', index: blockIdx });
    }
    writeSSE(res, 'message_delta', {
        type: 'message_delta',
        delta: { stop_reason: mapFinishReason(s.finishReason), stop_sequence: null },
        usage: { output_tokens: s.outputTokens },
    });
    writeSSE(res, 'message_stop', { type: 'message_stop' });
}
// ─── Streaming Response Handler ──────────────────────────────
async function handleStream(fetchRes, httpRes, model) {
    const s = newStreamState(model);
    httpRes.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
    });
    writeSSE(httpRes, 'message_start', {
        type: 'message_start',
        message: {
            id: s.messageId, type: 'message', role: 'assistant', content: [],
            model: s.model, stop_reason: null, stop_sequence: null,
            usage: { input_tokens: s.inputTokens, output_tokens: 0 },
        },
    });
    const reader = fetchRes.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    try {
        let streamDone = false;
        while (!streamDone) {
            const read = await reader.read();
            if (read.done)
                break;
            buffer += decoder.decode(read.value, { stream: true });
            const lines = buffer.split('\n');
            buffer = lines.pop() || '';
            for (const line of lines) {
                if (!line.startsWith('data: '))
                    continue;
                const data = line.slice(6).trim();
                if (data === '[DONE]') {
                    streamDone = true;
                    break;
                }
                try {
                    processChunk(httpRes, s, JSON.parse(data));
                }
                catch { /* skip malformed chunk */ }
            }
        }
    }
    catch (err) {
        try {
            writeSSE(httpRes, 'error', {
                type: 'error', error: { type: 'api_error', message: err.message },
            });
        }
        catch { /* response may be closed */ }
    }
    closeStream(httpRes, s);
    httpRes.end();
}
// ─── HTTP Helpers ────────────────────────────────────────────
function collectBody(req) {
    return new Promise((resolve, reject) => {
        const chunks = [];
        req.on('data', (c) => chunks.push(c));
        req.on('end', () => resolve(Buffer.concat(chunks).toString()));
        req.on('error', reject);
    });
}
function sendError(res, status, message) {
    res.writeHead(status, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ type: 'error', error: { type: 'api_error', message } }));
}
// ─── Config 읽기 ─────────────────────────────────────────────
export function getProxySettings() {
    const config = readGlobalConfig();
    return config.codexProxy || {};
}
// ─── 인증 소스 확인 (동기, 시작/상태 표시용) ────────────────
export function checkAuthSource() {
    if (process.env.CODEX_PROXY_API_KEY)
        return { source: 'env' };
    const settings = getProxySettings();
    if (settings.apiKey)
        return { source: 'apikey' };
    if (settings.provider === 'antigravity' && getAntigravityApiKey())
        return { source: 'apikey' };
    const codex = findCodexCredentials();
    if (codex)
        return { source: 'codex-cli' };
    const apiKey = process.env.OPENAI_API_KEY || getApiKeyFromConfig();
    if (apiKey)
        return { source: 'apikey' };
    return null;
}
async function resolveRequestAuth() {
    const settings = getProxySettings();
    const baseUrl = process.env.CODEX_PROXY_TARGET_URL || settings.targetUrl || DEFAULT_TARGET_URL;
    // 1. 환경변수 최우선
    const proxyKey = process.env.CODEX_PROXY_API_KEY;
    if (proxyKey)
        return { token: proxyKey, baseUrl, provider: 'openai-compat' };
    // 2. Config의 커스텀 API key
    if (settings.apiKey)
        return { token: settings.apiKey, baseUrl, provider: 'openai-compat' };
    // 3. Antigravity provider → credentials.antigravity.apiKey
    if (settings.provider === 'antigravity') {
        const antigravityKey = getAntigravityApiKey();
        if (antigravityKey)
            return { token: antigravityKey, baseUrl, provider: 'openai-compat' };
    }
    // 4. GPT 인증 (codex-cli OAuth → apikey 순서, 토큰 자동 갱신)
    const auth = await getAuthInfo();
    const token = auth.accessToken || auth.apiKey;
    if (!token)
        throw new Error('인증 정보 없음. vibe codex --setup 실행 필요');
    // codex-cli OAuth → ChatGPT Pro (Codex Responses API)
    if (auth.type === 'codex-cli' && auth.accessToken) {
        return {
            token: auth.accessToken,
            baseUrl: CHATGPT_BASE_URL,
            provider: 'chatgpt-pro',
            accountId: auth.accountId,
        };
    }
    return { token, baseUrl, provider: 'openai-compat' };
}
function buildCodexInput(messages) {
    const input = [];
    for (const msg of messages) {
        if (msg.role === 'assistant') {
            if (typeof msg.content === 'string') {
                input.push({
                    type: 'message', role: 'assistant',
                    content: [{ type: 'output_text', text: msg.content }],
                });
            }
            else {
                const texts = msg.content.filter(b => b.type === 'text');
                const tools = msg.content.filter(b => b.type === 'tool_use');
                if (texts.length > 0) {
                    input.push({
                        type: 'message', role: 'assistant',
                        content: [{ type: 'output_text', text: texts.map(t => t.text || '').join('') }],
                    });
                }
                for (const t of tools) {
                    input.push({
                        type: 'function_call',
                        call_id: t.id || `call_${crypto.randomUUID()}`,
                        name: t.name || '',
                        arguments: JSON.stringify(t.input || {}),
                    });
                }
            }
        }
        else {
            // user messages
            if (typeof msg.content === 'string') {
                input.push({
                    type: 'message', role: 'user',
                    content: [{ type: 'input_text', text: msg.content }],
                });
            }
            else {
                const toolResults = msg.content.filter(b => b.type === 'tool_result');
                const others = msg.content.filter(b => b.type !== 'tool_result' && b.type !== 'thinking');
                for (const tr of toolResults) {
                    const text = typeof tr.content === 'string'
                        ? tr.content
                        : Array.isArray(tr.content)
                            ? tr.content.filter(b => b.type === 'text').map(b => b.text || '').join('')
                            : '';
                    input.push({
                        type: 'function_call_output',
                        call_id: tr.tool_use_id || '',
                        output: text,
                    });
                }
                if (others.length > 0) {
                    const text = others
                        .filter(b => b.type === 'text')
                        .map(b => b.text || '').join('');
                    if (text) {
                        input.push({
                            type: 'message', role: 'user',
                            content: [{ type: 'input_text', text }],
                        });
                    }
                }
            }
        }
    }
    return input;
}
function buildCodexTools(tools) {
    if (!tools?.length)
        return undefined;
    return tools.map(t => ({
        type: 'function',
        name: t.name,
        description: t.description || '',
        parameters: t.input_schema,
    }));
}
function buildCodexRequest(req, defaultModel) {
    const sysText = extractSystemText(req.system);
    const body = {
        model: defaultModel || req.model,
        instructions: sysText || 'You are a helpful assistant.',
        input: buildCodexInput(req.messages),
        store: false,
        stream: true,
    };
    if (req.temperature !== undefined)
        body.temperature = req.temperature;
    if (req.top_p !== undefined)
        body.top_p = req.top_p;
    const tools = buildCodexTools(req.tools);
    if (tools)
        body.tools = tools;
    return body;
}
function buildCodexHeaders(token, accountId) {
    const headers = {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
        'OpenAI-Beta': 'responses=experimental',
        'originator': 'codex_cli_rs',
        'session_id': crypto.randomUUID(),
        'User-Agent': 'codex-tui/0.120.0',
    };
    if (accountId)
        headers['chatgpt-account-id'] = accountId;
    return headers;
}
function newCodexStreamState(model) {
    return {
        messageId: `msg_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`,
        model,
        nextBlockIndex: 0,
        textBlockIndex: -1,
        textBlockOpen: false,
        toolBlockMap: new Map(),
        finishReason: 'end_turn',
        inputTokens: 0,
        outputTokens: 0,
    };
}
function processCodexEvent(res, s, eventType, data) {
    switch (eventType) {
        case 'response.output_text.delta': {
            const delta = data.delta;
            if (!delta)
                break;
            if (s.textBlockIndex < 0) {
                s.textBlockIndex = s.nextBlockIndex++;
                writeSSE(res, 'content_block_start', {
                    type: 'content_block_start', index: s.textBlockIndex,
                    content_block: { type: 'text', text: '' },
                });
                s.textBlockOpen = true;
            }
            writeSSE(res, 'content_block_delta', {
                type: 'content_block_delta', index: s.textBlockIndex,
                delta: { type: 'text_delta', text: delta },
            });
            break;
        }
        case 'response.output_item.added': {
            const item = data.item;
            if (!item)
                break;
            if (item.type === 'function_call') {
                if (s.textBlockOpen) {
                    writeSSE(res, 'content_block_stop', { type: 'content_block_stop', index: s.textBlockIndex });
                    s.textBlockOpen = false;
                }
                const blockIdx = s.nextBlockIndex++;
                const callId = item.call_id || `call_${crypto.randomUUID()}`;
                s.toolBlockMap.set(callId, blockIdx);
                s.finishReason = 'tool_use';
                writeSSE(res, 'content_block_start', {
                    type: 'content_block_start', index: blockIdx,
                    content_block: {
                        type: 'tool_use', id: callId,
                        name: item.name || '', input: {},
                    },
                });
            }
            break;
        }
        case 'response.function_call_arguments.delta': {
            const delta = data.delta;
            const callId = data.call_id || data.item_id;
            if (!delta || !callId)
                break;
            const blockIdx = s.toolBlockMap.get(callId);
            if (blockIdx !== undefined) {
                writeSSE(res, 'content_block_delta', {
                    type: 'content_block_delta', index: blockIdx,
                    delta: { type: 'input_json_delta', partial_json: delta },
                });
            }
            break;
        }
        case 'response.completed': {
            const response = data.response;
            if (response?.usage) {
                const usage = response.usage;
                s.inputTokens = usage.input_tokens ?? 0;
                s.outputTokens = usage.output_tokens ?? 0;
            }
            break;
        }
    }
}
function closeCodexStream(res, s) {
    if (s.textBlockOpen) {
        writeSSE(res, 'content_block_stop', { type: 'content_block_stop', index: s.textBlockIndex });
    }
    for (const [, blockIdx] of s.toolBlockMap) {
        writeSSE(res, 'content_block_stop', { type: 'content_block_stop', index: blockIdx });
    }
    writeSSE(res, 'message_delta', {
        type: 'message_delta',
        delta: { stop_reason: mapFinishReason(s.finishReason === 'tool_use' ? 'tool_calls' : 'stop'), stop_sequence: null },
        usage: { output_tokens: s.outputTokens },
    });
    writeSSE(res, 'message_stop', { type: 'message_stop' });
}
async function handleCodexStream(fetchRes, httpRes, model) {
    const s = newCodexStreamState(model);
    httpRes.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
    });
    writeSSE(httpRes, 'message_start', {
        type: 'message_start',
        message: {
            id: s.messageId, type: 'message', role: 'assistant', content: [],
            model: s.model, stop_reason: null, stop_sequence: null,
            usage: { input_tokens: 0, output_tokens: 0 },
        },
    });
    const reader = fetchRes.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    let currentEvent = '';
    try {
        let done = false;
        while (!done) {
            const read = await reader.read();
            if (read.done)
                break;
            buffer += decoder.decode(read.value, { stream: true });
            const lines = buffer.split('\n');
            buffer = lines.pop() || '';
            for (const line of lines) {
                if (line.startsWith('event: ')) {
                    currentEvent = line.slice(7).trim();
                }
                else if (line.startsWith('data: ')) {
                    const raw = line.slice(6).trim();
                    if (currentEvent === 'response.completed')
                        done = true;
                    try {
                        const data = JSON.parse(raw);
                        processCodexEvent(httpRes, s, currentEvent, data);
                    }
                    catch { /* skip */ }
                    if (done) {
                        // parse completed event before breaking
                        try {
                            const data = JSON.parse(raw);
                            processCodexEvent(httpRes, s, 'response.completed', data);
                        }
                        catch { /* skip */ }
                    }
                }
            }
        }
    }
    catch (err) {
        try {
            writeSSE(httpRes, 'error', {
                type: 'error', error: { type: 'api_error', message: err.message },
            });
        }
        catch { /* response may be closed */ }
    }
    closeCodexStream(httpRes, s);
    httpRes.end();
}
// ─── Codex Responses 비스트리밍 수집 ────────────────────────
async function collectCodexResponse(fetchRes, model) {
    const reader = fetchRes.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    let currentEvent = '';
    let textContent = '';
    const toolCalls = [];
    let currentToolId = '';
    let currentToolArgs = '';
    let inputTokens = 0;
    let outputTokens = 0;
    let stopReason = 'end_turn';
    while (true) {
        const read = await reader.read();
        if (read.done)
            break;
        buffer += decoder.decode(read.value, { stream: true });
        const lines = buffer.split('\n');
        buffer = lines.pop() || '';
        for (const line of lines) {
            if (line.startsWith('event: ')) {
                currentEvent = line.slice(7).trim();
            }
            else if (line.startsWith('data: ')) {
                const raw = line.slice(6).trim();
                try {
                    const data = JSON.parse(raw);
                    switch (currentEvent) {
                        case 'response.output_text.delta':
                            textContent += data.delta || '';
                            break;
                        case 'response.output_item.added': {
                            const item = data.item;
                            if (item?.type === 'function_call') {
                                if (currentToolId) {
                                    toolCalls.push({ id: currentToolId, name: '', args: currentToolArgs });
                                }
                                currentToolId = item.call_id || '';
                                currentToolArgs = '';
                                stopReason = 'tool_use';
                                toolCalls.push({ id: currentToolId, name: item.name || '', args: '' });
                            }
                            break;
                        }
                        case 'response.function_call_arguments.delta':
                            currentToolArgs += data.delta || '';
                            if (toolCalls.length > 0) {
                                toolCalls[toolCalls.length - 1].args = currentToolArgs;
                            }
                            break;
                        case 'response.completed': {
                            const resp = data.response;
                            if (resp?.usage) {
                                const u = resp.usage;
                                inputTokens = u.input_tokens ?? 0;
                                outputTokens = u.output_tokens ?? 0;
                            }
                            break;
                        }
                    }
                }
                catch { /* skip */ }
            }
        }
    }
    const content = [];
    if (textContent)
        content.push({ type: 'text', text: textContent });
    for (const tc of toolCalls) {
        let input = {};
        try {
            input = JSON.parse(tc.args || '{}');
        }
        catch { /* use empty */ }
        content.push({ type: 'tool_use', id: tc.id, name: tc.name, input });
    }
    return {
        id: `msg_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`,
        type: 'message',
        role: 'assistant',
        content,
        model,
        stop_reason: stopReason === 'tool_use' ? 'tool_use' : 'end_turn',
        stop_sequence: null,
        usage: { input_tokens: inputTokens, output_tokens: outputTokens },
    };
}
// ─── Messages Endpoint Handler ───────────────────────────────
async function handleMessages(req, res, config) {
    const body = await collectBody(req);
    let aReq;
    try {
        aReq = JSON.parse(body);
    }
    catch {
        sendError(res, 400, 'Invalid JSON');
        return;
    }
    let auth;
    try {
        auth = await resolveRequestAuth();
    }
    catch (err) {
        sendError(res, 401, err.message);
        return;
    }
    if (auth.provider === 'chatgpt-pro') {
        // ─── Codex Responses API 경로 ──────────────────────────
        const codexBody = buildCodexRequest(aReq, config.defaultModel);
        const headers = buildCodexHeaders(auth.token, auth.accountId);
        const fetchRes = await fetch(CODEX_RESPONSES_URL, {
            method: 'POST',
            headers,
            body: JSON.stringify(codexBody),
        });
        if (!fetchRes.ok) {
            const errText = await fetchRes.text();
            sendError(res, fetchRes.status, `Codex error: ${errText.slice(0, 500)}`);
            return;
        }
        if (aReq.stream) {
            await handleCodexStream(fetchRes, res, aReq.model);
        }
        else {
            const aResp = await collectCodexResponse(fetchRes, aReq.model);
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify(aResp));
        }
    }
    else {
        // ─── Chat Completions API 경로 (기존) ──────────────────
        const oBody = buildORequest(aReq, config.defaultModel);
        const targetUrl = `${auth.baseUrl}/v1/chat/completions`;
        const fetchRes = await fetch(targetUrl, {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${auth.token}`,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(oBody),
        });
        if (!fetchRes.ok) {
            const errText = await fetchRes.text();
            sendError(res, fetchRes.status, `OpenAI error: ${errText.slice(0, 500)}`);
            return;
        }
        if (aReq.stream) {
            await handleStream(fetchRes, res, aReq.model);
        }
        else {
            const oResp = await fetchRes.json();
            const aResp = buildAResponse(oResp, aReq.model);
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify(aResp));
        }
    }
}
// ─── HTTP Server ─────────────────────────────────────────────
export function createProxyServer(config) {
    return http.createServer(async (req, res) => {
        res.setHeader('Access-Control-Allow-Origin', '*');
        res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS, GET');
        res.setHeader('Access-Control-Allow-Headers', '*');
        if (req.method === 'OPTIONS') {
            res.writeHead(204);
            res.end();
            return;
        }
        const url = req.url || '';
        if (url.startsWith('/v1/messages') && req.method === 'POST') {
            try {
                await handleMessages(req, res, config);
            }
            catch (err) {
                sendError(res, 500, err.message);
            }
        }
        else if (url === '/health') {
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ status: 'ok', port: config.port }));
        }
        else if (url === '/v1/models' && req.method === 'GET') {
            // Claude Code 모델 검증용 — 설정된 모델을 반환
            const model = config.defaultModel || 'gpt-5.5';
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({
                object: 'list',
                data: [
                    { id: model, object: 'model', created: Date.now(), owned_by: 'codex-proxy' },
                ],
            }));
        }
        else {
            // 알 수 없는 엔드포인트 로그
            console.error(`[proxy] unhandled: ${req.method} ${url}`);
            sendError(res, 404, `Unknown endpoint: ${url}`);
        }
    });
}
// ─── 기본 모델 결정 ────────────────────────────────────────
function resolveDefaultModel(configModel) {
    // config에 모델이 지정되어 있고 'latest'가 아니면 그대로 사용
    if (configModel && configModel !== 'latest')
        return configModel;
    // models_cache.json에서 최신(첫 번째) 모델 자동 선택
    const codexHome = process.env.CODEX_HOME || `${process.env.HOME || process.env.USERPROFILE}/.codex`;
    try {
        const raw = fs.readFileSync(`${codexHome}/models_cache.json`, 'utf-8');
        const cache = JSON.parse(raw);
        const first = cache.models?.[0]?.slug;
        if (first)
            return first;
    }
    catch { /* no cache */ }
    return 'gpt-4o';
}
function resolveModelSlots(defaultModel) {
    // models_cache.json에서 사용 가능한 모델 목록 조회
    const codexHome = process.env.CODEX_HOME || `${process.env.HOME || process.env.USERPROFILE}/.codex`;
    let models = [];
    try {
        const raw = fs.readFileSync(`${codexHome}/models_cache.json`, 'utf-8');
        const cache = JSON.parse(raw);
        models = (cache.models || []).map(m => m.slug).filter(Boolean);
    }
    catch { /* no cache */ }
    // Opus → 선택된 모델 (최상위)
    const opus = defaultModel;
    // Sonnet → codex 코딩 모델 (spark 제외, 정확도 우선)
    const codexModel = models.find(m => m.includes('codex') && !m.includes('mini') && !m.includes('spark') && m !== defaultModel)
        || models.find(m => m.includes('codex') && !m.includes('mini') && !m.includes('spark'))
        || defaultModel;
    // Haiku → codex-spark (고속), 없으면 mini
    const miniModel = models.find(m => m.includes('codex-spark'))
        || models.find(m => m.includes('mini'))
        || defaultModel;
    return { opus, sonnet: codexModel, haiku: miniModel };
}
// ─── 세션 실행 (프록시 + Claude Code 원샷) ──────────────────
export function launchSession(model, claudeArgs = []) {
    const authSource = checkAuthSource();
    if (!authSource) {
        console.error('인증 정보 없음.');
        console.error('  ChatGPT Pro: npm i -g @openai/codex && codex login');
        console.error('  Antigravity: ANTIGRAVITY_API_KEY 또는 CODEX_PROXY_API_KEY 설정');
        console.error('  OpenAI:      vibe gpt key <key> 또는 OPENAI_API_KEY 설정');
        process.exit(1);
    }
    const settings = getProxySettings();
    const defaultModel = model || process.env.CODEX_PROXY_MODEL || resolveDefaultModel(settings.model);
    const modelSlots = resolveModelSlots(defaultModel);
    const config = { port: 0, defaultModel };
    const server = createProxyServer(config);
    server.listen(0, '127.0.0.1', () => {
        const addr = server.address();
        const port = addr.port;
        const authLabel = AUTH_SOURCE_LABELS[authSource.source];
        console.log(`Codex Proxy :${port} | ${authLabel} | opus=${modelSlots.opus} sonnet=${modelSlots.sonnet} haiku=${modelSlots.haiku}`);
        const child = spawn('claude', ['--dangerously-skip-permissions', ...claudeArgs], {
            stdio: 'inherit',
            env: {
                ...process.env,
                ANTHROPIC_BASE_URL: `http://localhost:${port}`,
                ANTHROPIC_AUTH_TOKEN: 'codex-proxy',
                ANTHROPIC_API_KEY: 'codex-proxy',
                API_TIMEOUT_MS: '3000000',
                ANTHROPIC_DEFAULT_OPUS_MODEL: modelSlots.opus,
                ANTHROPIC_DEFAULT_SONNET_MODEL: modelSlots.sonnet,
                ANTHROPIC_DEFAULT_HAIKU_MODEL: modelSlots.haiku,
            },
        });
        child.on('error', (err) => {
            console.error(`claude 실행 실패: ${err.message}`);
            console.error('Claude Code 설치: npm i -g @anthropic-ai/claude-code');
            server.close();
            process.exit(1);
        });
        child.on('exit', (code) => {
            server.close();
            process.exit(code ?? 0);
        });
    });
    process.on('SIGINT', () => { server.close(); });
    process.on('SIGTERM', () => { server.close(); process.exit(0); });
}
const AUTH_SOURCE_LABELS = {
    'codex-cli': 'Codex CLI (ChatGPT Pro)',
    'apikey': 'API Key',
    'env': 'CODEX_PROXY_API_KEY',
};
// ─── 셸 함수 생성 ───────────────────────────────────────────
export function generateShellFunction(model) {
    if (model) {
        return `codex-cc() { CODEX_PROXY_MODEL="${model}" vibe codex "$@"; }`;
    }
    return `codex-cc() { vibe codex "$@"; }`;
}
//# sourceMappingURL=codex-proxy.js.map