import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { basename, extname, join } from 'node:path';
import { Codex } from '@openai/codex-sdk';
import { appendOutputStyleToPrompt, summarizeResult } from './agent-runtime-types.js';
import { startCodexFunctionToolBridge } from './codex-function-tool-bridge.js';
/**
 * Runtime backed by OpenAI's Codex SDK.
 * Spawns the bundled Codex CLI and consumes its structured JSONL event stream.
 */
export class CodexRuntime {
    config;
    abortController = null;
    constructor(config = {}) {
        this.config = config;
    }
    async execute(prompt, workingDir, options) {
        this.abortController = new AbortController();
        const output = [];
        const cwd = workingDir || process.cwd();
        const startTime = Date.now();
        const d = (msg) => console.log(`[codex] ${msg}`);
        const agentDir = options?.configDir;
        const codexHome = agentDir ? join(agentDir, '.codex') : undefined;
        if (codexHome)
            prepareCodexHome(codexHome, agentDir);
        const skillNames = normalizeSkillNames(options?.skills, codexHome);
        const sandboxMode = normalizeSandboxMode(this.config.permissionMode);
        const approvalPolicy = normalizeApprovalPolicy(this.config.permissionMode);
        let functionToolBridge;
        d(`starting | cwd=${cwd} model=${this.config.model ?? 'default'} sandboxMode=${sandboxMode} approvalPolicy=${approvalPolicy} maxTurns=${options?.maxTurns ?? '∞'} mcpServers=${Object.keys(options?.mcpServers ?? {}).join(',') || '-'} functionTools=${options?.functionTools?.map((tool) => tool.name).join(',') || '-'} skills=${skillNames.join(',') || '-'} codexHome=${codexHome ?? 'default'} sandboxDirs=${options?.sandboxDirs?.join(',') ?? '-'}`);
        d(`prompt: ${prompt.slice(0, 300)}${prompt.length > 300 ? '...' : ''}`);
        try {
            functionToolBridge = await startCodexFunctionToolBridge(options?.functionTools, d);
            const mcpServers = withFunctionToolBridge(options?.mcpServers, functionToolBridge);
            const configOverrides = buildCodexConfig(this.config, mcpServers, skillNames);
            const codex = new Codex({
                apiKey: this.config.apiKey,
                baseUrl: shouldUseCodexOpenAIBaseUrl(this.config) ? this.config.baseURL : undefined,
                config: configOverrides,
                env: buildEnv(this.config, codexHome),
            });
            const threadOptions = {
                model: this.config.model,
                sandboxMode,
                approvalPolicy,
                workingDirectory: cwd,
                skipGitRepoCheck: true,
                additionalDirectories: options?.sandboxDirs,
                networkAccessEnabled: true,
                webSearchMode: 'live',
            };
            const thread = options?.resumeSessionId
                ? codex.resumeThread(options.resumeSessionId, threadOptions)
                : codex.startThread(threadOptions);
            const { events } = await thread.runStreamed(appendOutputStyleToPrompt(prompt, options?.outputStyle), {
                signal: this.abortController.signal,
            });
            let resultText = '';
            let tokenCount = 0;
            let usage;
            let error;
            let sessionId = options?.resumeSessionId ?? thread.id ?? undefined;
            const emittedItemLines = new Set();
            for await (const event of events) {
                if (event.type === 'thread.started') {
                    sessionId = event.thread_id;
                    options?.onEvent?.({ type: 'session', sessionId });
                }
                logEventDebug(event, d);
                const mappedEvents = mapRuntimeEvents(event);
                const mappedEventLines = new Set(mappedEvents.flatMap((runtimeEvent) => (runtimeEvent.type === 'tool_use' ? [runtimeEvent.line] : [])));
                for (const runtimeEvent of mappedEvents) {
                    options?.onEvent?.(runtimeEvent);
                }
                for (const line of formatEventLines(event)) {
                    if (!line || emittedItemLines.has(line))
                        continue;
                    emittedItemLines.add(line);
                    output.push(line);
                    if (mappedEventLines.has(line))
                        continue;
                    options?.onEvent?.({ type: 'output', line });
                }
                if (event.type === 'item.completed' && event.item.type === 'agent_message') {
                    resultText = event.item.text;
                }
                else if (event.type === 'turn.completed') {
                    tokenCount = countUsageTokens(event.usage);
                    usage = normalizeUsage(event.usage);
                    const usageLine = formatUsageLine(event);
                    output.push(usageLine);
                    options?.onEvent?.({ type: 'output', line: usageLine });
                }
                else if (event.type === 'turn.failed') {
                    error = event.error.message;
                }
                else if (event.type === 'error') {
                    error = event.message;
                }
            }
            const elapsed = Date.now() - startTime;
            if (error) {
                d(`failed ${elapsed}ms | tokens=${tokenCount} | ${error}`);
                return {
                    success: false,
                    summary: 'Codex execution failed',
                    artifacts: [],
                    error,
                    output,
                    sessionId,
                    usage,
                };
            }
            const text = resultText || lastAgentMessageText(output) || output.at(-1) || '';
            d(`done ${elapsed}ms | tokens=${tokenCount}`);
            return {
                success: true,
                summary: summarizeResult(text),
                artifacts: [],
                output,
                sessionId,
                usage,
            };
        }
        catch (err) {
            const elapsed = Date.now() - startTime;
            const message = err instanceof Error ? err.message : String(err);
            d(`failed ${elapsed}ms | ${message}`);
            if (err instanceof Error && err.stack)
                console.error(err.stack);
            return { success: false, summary: 'Codex execution failed', artifacts: [], error: message, output, sessionId: options?.resumeSessionId };
        }
        finally {
            try {
                await functionToolBridge?.close();
            }
            catch (err) {
                const message = err instanceof Error ? err.message : String(err);
                d(`function tool bridge close failed | ${message}`);
            }
            this.abortController = null;
        }
    }
    stop() {
        this.abortController?.abort();
    }
}
function buildEnv(config, codexHome) {
    return removeUndefined({
        ...process.env,
        CODEX_HOME: codexHome || process.env.CODEX_HOME,
        CODEX_API_KEY: config.apiKey || process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY,
        OPENAI_API_KEY: config.apiKey || process.env.OPENAI_API_KEY,
        CODEX_INTERNAL_ORIGINATOR_OVERRIDE: process.env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE || 'agent-spaces/server',
    });
}
function buildCodexConfig(runtimeConfig, rawMcpServers, skillNames) {
    const config = {};
    Object.assign(config, buildCodexProviderConfig(runtimeConfig));
    if (runtimeConfig.thinkingEnabled !== false) {
        config.model_reasoning_effort = runtimeConfig.thinkingEffort ?? 'medium';
    }
    const mcpServers = normalizeMcpServers(rawMcpServers);
    if (mcpServers)
        config.mcp_servers = mcpServers;
    if (skillNames.length > 0) {
        config.skills = { enabled: skillNames };
    }
    return removeUndefined(config);
}
function withFunctionToolBridge(mcpServers, bridge) {
    if (!bridge)
        return mcpServers;
    return {
        ...(mcpServers ?? {}),
        [bridge.name]: { url: bridge.url },
    };
}
function buildCodexProviderConfig(runtimeConfig) {
    if (!runtimeConfig.baseURL || shouldUseCodexOpenAIBaseUrl(runtimeConfig))
        return {};
    const providerName = sanitizeCodexProviderName(runtimeConfig.provider);
    return {
        model_provider: providerName,
        model_providers: {
            [providerName]: removeUndefined({
                name: providerName,
                base_url: runtimeConfig.baseURL,
                wire_api: normalizeCodexWireApi(runtimeConfig.provider),
            }),
        },
    };
}
function shouldUseCodexOpenAIBaseUrl(runtimeConfig) {
    return !runtimeConfig.provider || runtimeConfig.provider === 'openai-chat-completions';
}
function sanitizeCodexProviderName(provider) {
    const raw = String(provider || 'custom').trim().toLowerCase();
    return raw.replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'custom';
}
function normalizeCodexWireApi(provider) {
    if (provider === 'openai-responses')
        return 'responses';
    if (provider === 'openai-chat-completions')
        return 'chat';
    return undefined;
}
function normalizeMcpServers(servers) {
    if (!servers || Object.keys(servers).length === 0)
        return undefined;
    return Object.fromEntries(Object.entries(servers).flatMap(([name, server]) => {
        const normalized = normalizeMcpServer(server);
        return normalized ? [[name, normalized]] : [];
    }));
}
function normalizeMcpServer(server) {
    if (!server || typeof server !== 'object' || Array.isArray(server))
        return null;
    const record = server;
    if (typeof record.command === 'string') {
        return removeUndefined({
            command: record.command,
            args: Array.isArray(record.args) ? record.args : undefined,
            env: isRecord(record.env) ? record.env : undefined,
        });
    }
    if (typeof record.url === 'string') {
        return removeUndefined({
            url: record.url,
            headers: isRecord(record.headers) ? record.headers : undefined,
            bearer_token_env_var: typeof record.bearer_token_env_var === 'string'
                ? record.bearer_token_env_var
                : typeof record.bearerTokenEnvVar === 'string'
                    ? record.bearerTokenEnvVar
                    : undefined,
        });
    }
    return record;
}
function normalizeSandboxMode(permissionMode) {
    if (permissionMode === 'plan')
        return 'read-only';
    if (permissionMode === 'default' || permissionMode === 'acceptEdits' || permissionMode === 'auto') {
        return 'workspace-write';
    }
    return 'danger-full-access';
}
function normalizeApprovalPolicy(permissionMode) {
    if (permissionMode === 'default' || permissionMode === 'acceptEdits' || permissionMode === 'auto') {
        return 'on-request';
    }
    if (permissionMode === 'plan')
        return 'untrusted';
    return 'never';
}
function prepareCodexHome(codexHome, agentDir) {
    mkdirSync(codexHome, { recursive: true });
    const skillsDir = join(codexHome, 'skills');
    rmSync(skillsDir, { recursive: true, force: true });
    mkdirSync(skillsDir, { recursive: true });
    if (!agentDir)
        return;
    const sourceSkillsDir = join(agentDir, 'skills');
    if (!existsSync(sourceSkillsDir))
        return;
    for (const file of readdirSync(sourceSkillsDir)) {
        if (extname(file).toLowerCase() !== '.md')
            continue;
        const skillName = sanitizeSkillName(file);
        const targetSkillDir = join(skillsDir, skillName);
        mkdirSync(targetSkillDir, { recursive: true });
        const sourceFile = join(sourceSkillsDir, file);
        const targetFile = join(targetSkillDir, 'SKILL.md');
        writeFileSync(targetFile, ensureCodexSkillFrontmatter(skillName, sourceFile), 'utf-8');
    }
}
function ensureCodexSkillFrontmatter(skillName, sourceFile) {
    const content = readFileSync(sourceFile, 'utf-8');
    if (/^---\r?\n[\s\S]*?\r?\n---\r?\n/.test(content))
        return content;
    const title = basename(sourceFile).replace(/\.md$/i, '');
    return [
        '---',
        `name: ${skillName}`,
        `description: ${title}`,
        '---',
        '',
        content,
    ].join('\n');
}
function normalizeSkillNames(skills, codexHome) {
    if (!Array.isArray(skills))
        return [];
    return skills
        .map(sanitizeSkillName)
        .filter((skill) => Boolean(skill) && hasSkillContent(skill, codexHome));
}
function hasSkillContent(skill, codexHome) {
    if (!codexHome)
        return true;
    const skillFile = join(codexHome, 'skills', skill, 'SKILL.md');
    return existsSync(skillFile) && statSync(skillFile).size > 0;
}
function sanitizeSkillName(name) {
    const raw = basename(name).replace(/\.md$/i, '').trim();
    return raw.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
}
function mapRuntimeEvents(event) {
    if (event.type === 'item.started') {
        const toolUse = mapToolUse(event.item);
        return toolUse ? [toolUse] : [];
    }
    if (event.type === 'item.completed') {
        const toolResult = mapToolResult(event.item);
        return toolResult ? [toolResult] : [];
    }
    return [];
}
function mapToolUse(item) {
    switch (item.type) {
        case 'command_execution':
            return {
                type: 'tool_use',
                id: item.id,
                name: 'Bash',
                input: { command: item.command },
                line: formatCommandLine(item),
            };
        case 'mcp_tool_call':
            return {
                type: 'tool_use',
                id: item.id,
                name: `${item.server}.${item.tool}`,
                input: item.arguments,
                line: formatMcpToolCallLine(item),
            };
        case 'web_search':
            return {
                type: 'tool_use',
                id: item.id,
                name: 'WebSearch',
                input: { query: item.query },
                line: `Tool: WebSearch query=${JSON.stringify(truncate(item.query, 140))}`,
            };
        default:
            return null;
    }
}
function mapToolResult(item) {
    switch (item.type) {
        case 'command_execution':
            return {
                type: 'tool_result',
                toolUseId: item.id,
                result: {
                    status: item.status,
                    exit_code: item.exit_code,
                    output: item.aggregated_output,
                },
            };
        case 'mcp_tool_call':
            return {
                type: 'tool_result',
                toolUseId: item.id,
                result: item.error ?? item.result ?? { status: item.status },
            };
        default:
            return null;
    }
}
function formatEventLines(event) {
    switch (event.type) {
        case 'thread.started':
            return [`Codex initialized with thread ${event.thread_id}`];
        case 'item.started':
        case 'item.updated':
        case 'item.completed': {
            const line = formatItemLine(event.item);
            return line ? [line] : [];
        }
        case 'turn.failed':
            return [event.error.message];
        case 'error':
            return [event.message];
        default:
            return [];
    }
}
function formatItemLine(item) {
    switch (item.type) {
        case 'agent_message':
            return item.text;
        case 'reasoning':
            return `[Reasoning] ${item.text}`;
        case 'command_execution':
            return formatCommandLine(item);
        case 'file_change':
            return formatFileChangeLine(item);
        case 'mcp_tool_call':
            return formatMcpToolCallLine(item);
        case 'web_search':
            return `Tool: WebSearch query=${JSON.stringify(truncate(item.query, 140))}`;
        case 'todo_list':
            return formatTodoListLine(item);
        case 'error':
            return item.message;
    }
}
function formatCommandLine(item) {
    return `Tool: Bash command=${JSON.stringify(truncate(item.command, 180))}`;
}
function formatFileChangeLine(item) {
    const changes = item.changes
        .map((change) => `${change.kind}:${change.path}`)
        .join(', ');
    return `Tool: ApplyPatch ${changes || item.status}`;
}
function formatMcpToolCallLine(item) {
    const summary = summarizeToolInput(item.arguments);
    const toolName = `${item.server}.${item.tool}`;
    return summary ? `Tool: ${toolName} ${summary}` : `Tool: ${toolName}`;
}
function formatTodoListLine(item) {
    const counts = item.items.reduce((acc, todo) => {
        acc.total += 1;
        if (todo.completed)
            acc.completed += 1;
        return acc;
    }, { total: 0, completed: 0 });
    return `Todo: ${counts.completed}/${counts.total} completed`;
}
function formatUsageLine(event) {
    const usage = event.usage;
    const total = countUsageTokens(usage);
    return `[Usage] tokens=${total} input=${usage.input_tokens} output=${usage.output_tokens} reasoning=${usage.reasoning_output_tokens}`;
}
function logEventDebug(event, d) {
    switch (event.type) {
        case 'item.started':
            logItemDebug('item started', event.item, d);
            return;
        case 'item.updated':
            logItemDebug('item updated', event.item, d);
            return;
        case 'item.completed':
            logItemDebug('item completed', event.item, d);
            return;
        case 'turn.completed':
            d(`turn completed | tokens=${countUsageTokens(event.usage)}`);
            return;
        case 'turn.failed':
            d(`turn failed | ${truncate(event.error.message, 240)}`);
            return;
        case 'error':
            d(`stream error | ${truncate(event.message, 240)}`);
            return;
        default:
            return;
    }
}
function logItemDebug(prefix, item, d) {
    switch (item.type) {
        case 'command_execution':
            d(`${prefix} | command=${truncate(item.command, 180)} status=${item.status}`);
            return;
        case 'file_change':
            d(`${prefix} | file_change=${item.changes.map((change) => `${change.kind}:${change.path}`).join(',') || '-'} status=${item.status}`);
            return;
        case 'mcp_tool_call':
            d(`${prefix} | mcp=${item.server}.${item.tool} status=${item.status}`);
            return;
        case 'agent_message':
            d(`${prefix} | agent_message=${truncate(item.text, 180)}`);
            return;
        case 'reasoning':
            d(`${prefix} | reasoning=${truncate(item.text, 180)}`);
            return;
        case 'web_search':
            d(`${prefix} | web_search=${truncate(item.query, 180)}`);
            return;
        case 'todo_list':
            d(`${prefix} | todo_list=${item.items.length}`);
            return;
        case 'error':
            d(`${prefix} | error=${truncate(item.message, 180)}`);
            return;
    }
}
function lastAgentMessageText(output) {
    return [...output].reverse().find((line) => {
        return line.trim() && !isToolLikeLine(line) && !/^\[.*\]/.test(line.trim());
    }) ?? '';
}
function isToolLikeLine(line) {
    return /^(Tool:|Todo:|Codex initialized\b)/i.test(line.trim());
}
function summarizeToolInput(input) {
    if (!input || typeof input !== 'object' || Array.isArray(input))
        return '';
    const record = input;
    const keys = ['file_path', 'path', 'command', 'pattern', 'query', 'description', 'prompt'];
    const details = keys.flatMap((key) => {
        const value = record[key];
        if (typeof value !== 'string' || !value.trim())
            return [];
        return `${key}=${JSON.stringify(truncate(value.trim(), 140))}`;
    });
    if (details.length > 0)
        return details.join(' ');
    return JSON.stringify(redactLargeInput(record));
}
function redactLargeInput(input) {
    return Object.fromEntries(Object.entries(input).map(([key, value]) => {
        if (typeof value === 'string')
            return [key, truncate(value, 140)];
        return [key, value];
    }));
}
function truncate(value, max) {
    return value.length > max ? `${value.slice(0, max - 3)}...` : value;
}
function countUsageTokens(usage) {
    return usage.input_tokens
        + usage.cached_input_tokens
        + usage.output_tokens
        + usage.reasoning_output_tokens;
}
function normalizeUsage(usage) {
    return {
        inputTokens: usage.input_tokens,
        outputTokens: usage.output_tokens,
        cachedInputTokens: usage.cached_input_tokens,
        reasoningTokens: usage.reasoning_output_tokens,
        totalTokens: countUsageTokens(usage),
    };
}
function isRecord(value) {
    return Boolean(value && typeof value === 'object' && !Array.isArray(value));
}
function removeUndefined(record) {
    return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
}
//# sourceMappingURL=codex-runtime.js.map