/**
 * CodexExecutor - OpenAI Codex CLI 执行器
 * 参考: https://shipyard.build/blog/codex-cli-cheat-sheet/
 */
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { execFileSync } from 'child_process';
import { parse as parseToml } from 'smol-toml';
import { AgentType } from '../types/index.js';
import { which } from '../utils/index.js';
import { BaseExecutor, AgentCapability, } from './base.executor.js';
import { CommandBuilder, applyOverrides } from './command-builder.js';
import { extractImagePaths } from './image-utils.js';
import { AGENT_TOWER_MCP_IDENTITY_ENV_KEYS, AGENT_TOWER_MCP_SERVICE_ENV_KEYS, } from './execution-env.js';
/**
 * 将嵌套对象展平为 dotted path 键值对（递归到标量叶子）
 * { model_providers: { azure: { name: "A", query_params: { "api-version": "v1" } } } }
 * → [
 *     ["model_providers.azure.name", "A"],
 *     ["model_providers.azure.query_params.api-version", "v1"],
 *   ]
 */
function flattenObject(obj, prefix = '') {
    const entries = [];
    for (const [key, value] of Object.entries(obj)) {
        const fullPath = prefix ? `${prefix}.${key}` : key;
        if (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
            entries.push(...flattenObject(value, fullPath));
        }
        else {
            entries.push([fullPath, value]);
        }
    }
    return entries;
}
/**
 * 将 JS 值转为 TOML 字面量字符串（用于 -c 参数）
 * 对象转为 TOML inline table: { key = "value", key2 = 123 }
 */
function toTomlLiteral(value) {
    if (typeof value === 'string')
        return JSON.stringify(value);
    if (typeof value === 'boolean' || typeof value === 'number')
        return String(value);
    if (Array.isArray(value)) {
        return `[${value.map(v => toTomlLiteral(v)).join(', ')}]`;
    }
    if (value !== null && typeof value === 'object') {
        // TOML inline table: { key = "value", key2 = 123 }
        const pairs = Object.entries(value)
            .map(([k, v]) => `${k} = ${toTomlLiteral(v)}`)
            .join(', ');
        return `{${pairs}}`;
    }
    return String(value);
}
const AGENT_TOWER_MCP_ENV_KEYS = [
    ...AGENT_TOWER_MCP_IDENTITY_ENV_KEYS,
    ...AGENT_TOWER_MCP_SERVICE_ENV_KEYS,
];
const AGENT_TOWER_MCP_SERVER_NAMES = [
    'agent-tower',
    'agent-tower-dev',
];
function getCodexConfigDir() {
    return process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
}
/**
 * 读取 Codex config.toml 中实际声明的 mcp_servers 顶层 key。
 * 文件不存在、读取失败或解析失败时返回空集合（不抛错）。
 */
export function getCodexDeclaredMcpServerNames(configPath) {
    const filePath = configPath ?? path.join(getCodexConfigDir(), 'config.toml');
    try {
        const content = fs.readFileSync(filePath, 'utf-8');
        const parsed = parseToml(content);
        const mcpServers = parsed.mcp_servers;
        if (mcpServers && typeof mcpServers === 'object' && !Array.isArray(mcpServers)) {
            return new Set(Object.keys(mcpServers));
        }
    }
    catch {
        // config 不存在或解析失败 — 静默降级，不阻塞 Codex 启动
    }
    return new Set();
}
const CODEX_MCP_LIST_TIMEOUT_MS = 3000;
/**
 * 通过 `codex mcp list --json` 查询 Codex 运行时实际可见的 MCP server 名称。
 * 传入 provider settings 展平后的 `-c` 参数，确保 settings 注入的 MCP 也在列表中。
 * CLI 不存在、超时、exit 非 0、JSON 异常时返回 null 表示需要 fallback。
 */
export function queryCodexMcpServerNames(configOverrideArgs = []) {
    try {
        const args = [...configOverrideArgs, 'mcp', 'list', '--json'];
        const stdout = execFileSync('codex', args, {
            timeout: CODEX_MCP_LIST_TIMEOUT_MS,
            encoding: 'utf-8',
            stdio: ['ignore', 'pipe', 'ignore'],
        });
        const parsed = JSON.parse(stdout);
        if (!Array.isArray(parsed))
            return null;
        for (const item of parsed) {
            if (!item || typeof item !== 'object' || typeof item.name !== 'string') {
                return null;
            }
        }
        return new Set(parsed.map((s) => s.name));
    }
    catch {
        return null;
    }
}
/**
 * 检测 Codex 已声明的 MCP server 名称。
 * 主路径：`codex mcp list --json`（能反映 config + `-c` 注入的完整 MCP 视图）。
 * Fallback：直接读取 config.toml（CLI 不可用时）。
 */
export function detectDeclaredMcpServers(configOverrideArgs = [], fallbackConfigPath) {
    const cliResult = queryCodexMcpServerNames(configOverrideArgs);
    if (cliResult !== null)
        return cliResult;
    return getCodexDeclaredMcpServerNames(fallbackConfigPath);
}
export function buildAgentTowerMcpEnvConfigOverrides(env, declaredMcpServers) {
    const args = [];
    for (const serverName of AGENT_TOWER_MCP_SERVER_NAMES) {
        if (!declaredMcpServers.has(serverName))
            continue;
        for (const key of AGENT_TOWER_MCP_ENV_KEYS) {
            const value = env.get(key);
            if (value) {
                args.push('-c', `mcp_servers.${serverName}.env.${key}=${toTomlLiteral(value)}`);
            }
        }
    }
    return args;
}
/**
 * 获取基础命令
 */
function getBaseCommand() {
    return 'codex';
}
export class CodexExecutor extends BaseExecutor {
    agentType = AgentType.CODEX;
    displayName = 'Codex';
    config;
    constructor(config = {}) {
        super();
        this.config = config;
        this.cmdOverrides = config.cmd;
    }
    /**
     * 获取可用性信息
     */
    async getAvailabilityInfo() {
        // 检查 codex 命令是否存在
        const codexPath = await which('codex');
        if (!codexPath) {
            return { type: 'NOT_FOUND', error: 'Codex CLI not installed' };
        }
        const codexDir = getCodexConfigDir();
        const configPath = path.join(codexDir, 'config.toml');
        const authPath = path.join(codexDir, 'auth.json');
        // 检查认证文件
        if (fs.existsSync(authPath)) {
            try {
                const stats = fs.statSync(authPath);
                const timestamp = Math.floor(stats.mtimeMs / 1000);
                return {
                    type: 'LOGIN_DETECTED',
                    lastAuthTimestamp: timestamp,
                };
            }
            catch {
                return { type: 'INSTALLATION_FOUND' };
            }
        }
        // 仅当 provider 显式依赖 profile 时，才要求本地 config.toml 存在。
        // 其他场景下，Codex 也可能通过 provider 的 settings/env 在运行时注入配置。
        if (this.config.profile && !fs.existsSync(configPath)) {
            return {
                type: 'NOT_FOUND',
                error: 'Codex profile requires ~/.codex/config.toml',
            };
        }
        return { type: 'INSTALLATION_FOUND' };
    }
    /**
     * 获取 Agent 能力
     */
    getCapabilities() {
        return [AgentCapability.SESSION_FORK];
    }
    /**
     * 获取默认 MCP 配置路径
     */
    getDefaultMcpConfigPath() {
        return path.join(getCodexConfigDir(), 'config.toml');
    }
    /**
     * 构建 TOML 配置覆盖参数（--profile 和 -c）
     */
    buildConfigOverrides() {
        const args = [];
        // --profile
        if (this.config.profile) {
            args.push('--profile', this.config.profile);
        }
        // settings (TOML) → -c 参数
        if (this.config.settings) {
            const parsed = parseToml(this.config.settings);
            for (const [key, value] of flattenObject(parsed)) {
                args.push('-c', `${key}=${toTomlLiteral(value)}`);
            }
        }
        return args;
    }
    /**
     * 构建命令
     */
    buildCommandBuilder() {
        let builder = CommandBuilder.new(getBaseCommand());
        // 配置覆盖（--profile、-c 参数）— 放在最前面
        const configArgs = this.buildConfigOverrides();
        if (configArgs.length > 0) {
            builder.extendParams(configArgs);
        }
        // 模型选择
        if (this.config.model) {
            builder.extendParams(['--model', this.config.model]);
        }
        // 跳过所有确认提示并在无沙箱环境下执行命令（危险模式，类似 Claude Code 的 --dangerously-skip-permissions）
        if (this.config.dangerouslyBypassApprovalsAndSandbox) {
            builder.extendParams(['--dangerously-bypass-approvals-and-sandbox']);
        }
        // 实时搜索
        if (this.config.liveSearch) {
            builder.extendParams(['--search', 'live']);
        }
        // 应用覆盖
        return applyOverrides(builder, this.cmdOverrides);
    }
    /**
     * 启动新会话
     */
    async spawn(config) {
        const commandBuilder = this.buildCommandBuilder();
        const configOverrideArgs = this.buildConfigOverrides();
        const declaredMcpServers = detectDeclaredMcpServers(configOverrideArgs, this.getDefaultMcpConfigPath() ?? undefined);
        commandBuilder.extendParams(buildAgentTowerMcpEnvConfigOverrides(config.env, declaredMcpServers));
        // Codex 使用 exec 子命令进行非交互式执行
        commandBuilder.extendParams(['exec', '--json', '--skip-git-repo-check']);
        // 组合 prompt
        const prompt = this.combinePrompt(config.prompt);
        // 检测并提取图片路径（-i <FILE>... 是 variadic 参数，会贪婪消费后续参数，
        // 必须用 -- 分隔，防止 prompt 被当成文件路径）
        const { textPrompt, imagePaths } = await extractImagePaths(prompt);
        for (const imgPath of imagePaths) {
            commandBuilder.extendParams(['-i', imgPath]);
        }
        if (imagePaths.length > 0) {
            commandBuilder.extendParams(['--']);
        }
        const commandParts = commandBuilder.buildInitial();
        const newConfig = { ...config, prompt: textPrompt };
        return this.spawnInternal(newConfig, commandParts);
    }
    /**
     * 继续现有会话
     */
    async spawnFollowUp(config, sessionId, resetToMessageId) {
        const commandBuilder = this.buildCommandBuilder();
        const configOverrideArgs = this.buildConfigOverrides();
        const declaredMcpServers = detectDeclaredMcpServers(configOverrideArgs, this.getDefaultMcpConfigPath() ?? undefined);
        commandBuilder.extendParams(buildAgentTowerMcpEnvConfigOverrides(config.env, declaredMcpServers));
        // Codex 使用 exec resume 命令继续会话（支持 JSON 输出）
        const additionalArgs = ['exec', 'resume', '--json', '--skip-git-repo-check'];
        if (sessionId) {
            additionalArgs.push(sessionId);
        }
        // 注意：Codex 的 exec resume 不支持 --from 参数
        // resetToMessageId 参数暂时忽略
        // 组合 prompt
        const prompt = this.combinePrompt(config.prompt);
        // 检测并提取图片路径（-i <FILE>... 是 variadic 参数，会贪婪消费后续参数，
        // 必须用 -- 分隔，防止 prompt 被当成文件路径）
        const { textPrompt, imagePaths } = await extractImagePaths(prompt);
        for (const imgPath of imagePaths) {
            additionalArgs.push('-i', imgPath);
        }
        if (imagePaths.length > 0) {
            additionalArgs.push('--');
        }
        const commandParts = commandBuilder.buildFollowUp(additionalArgs);
        const newConfig = { ...config, prompt: textPrompt };
        return this.spawnInternal(newConfig, commandParts);
    }
    /**
     * 组合 prompt
     */
    combinePrompt(prompt) {
        if (this.config.appendPrompt) {
            return `${prompt}${this.config.appendPrompt}`;
        }
        return prompt;
    }
}
//# sourceMappingURL=codex.executor.js.map