/**
 * engine/runtimes/codex.js - OpenAI Codex CLI runtime adapter.
 *
 * Implements the runtime contract documented in engine/runtimes/claude.js.
 * Codex-specific behavior stays here: binary resolution, `codex exec` argv,
 * prompt construction, JSONL parsing, session persistence, skill roots, and
 * error normalization.
 */

const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync, execSync } = require('child_process');
const { FAILURE_CLASS, safeWrite, ts, resolveEngineCacheDir } = require('../shared');

const ENGINE_DIR = __dirname.replace(/[\\/]runtimes$/, '');
const MINIONS_DIR = path.resolve(ENGINE_DIR, '..');
const _CACHE_DIR = resolveEngineCacheDir(ENGINE_DIR);
const isWin = process.platform === 'win32';
const RUNTIME_NAME = 'codex';

const CAPS_FILE = path.join(_CACHE_DIR, 'codex-caps.json');
const MODELS_CACHE = path.join(_CACHE_DIR, 'codex-models.json');
const CAPS_SCHEMA_VERSION = 2;

const PLATFORM_PACKAGE_BY_TRIPLE = Object.freeze({
  'x86_64-unknown-linux-musl': '@openai/codex-linux-x64',
  'aarch64-unknown-linux-musl': '@openai/codex-linux-arm64',
  'x86_64-apple-darwin': '@openai/codex-darwin-x64',
  'aarch64-apple-darwin': '@openai/codex-darwin-arm64',
  'x86_64-pc-windows-msvc': '@openai/codex-win32-x64',
  'aarch64-pc-windows-msvc': '@openai/codex-win32-arm64',
});

function _safeJson(p) {
  try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
}

function _safeWriteJson(p, obj) {
  try { fs.writeFileSync(p, JSON.stringify(obj, null, 2)); } catch { /* best effort */ }
}

function _execSyncCapture(cmd, env, timeoutMs = 10000) {
  return execSync(cmd, { encoding: 'utf8', env, timeout: timeoutMs, windowsHide: true });
}

function _execFileCapture(bin, args, { env = process.env, timeoutMs = 10000, native = true } = {}) {
  const execBin = native ? bin : process.execPath;
  const execArgs = native ? args : [bin, ...args];
  return execFileSync(execBin, execArgs, { encoding: 'utf8', env, timeout: timeoutMs, windowsHide: true });
}

function _codexBinaryName(platform = process.platform) {
  return platform === 'win32' ? 'codex.exe' : 'codex';
}

function _nativeVendorHit(vendorRoot, triple = _platformTriple(process.platform, process.arch)) {
  if (!vendorRoot || !triple) return null;
  const binName = _codexBinaryName();
  const packageBin = path.join(vendorRoot, triple, 'bin', binName);
  if (fs.existsSync(packageBin)) return { bin: packageBin, source: 'npm-platform-vendor' };
  const legacyBin = path.join(vendorRoot, triple, 'codex', binName);
  if (fs.existsSync(legacyBin)) return { bin: legacyBin, source: 'npm-vendor' };
  return null;
}

function _resolvePlatformPackageDir(pkgDir, triple = _platformTriple(process.platform, process.arch)) {
  const platformPackage = PLATFORM_PACKAGE_BY_TRIPLE[triple];
  if (!platformPackage || !pkgDir) return null;

  const candidates = [];
  try {
    const packageJson = require.resolve(`${platformPackage}/package.json`, { paths: [pkgDir, path.dirname(pkgDir), MINIONS_DIR] });
    candidates.push(path.dirname(packageJson));
  } catch {}

  const scopedName = platformPackage.replace(/^@openai\//, '');
  candidates.push(
    path.join(path.dirname(pkgDir), scopedName),
    path.join(path.dirname(path.dirname(pkgDir)), '@openai', scopedName),
  );

  for (const candidate of candidates) {
    try {
      if (candidate && fs.existsSync(path.join(candidate, 'package.json'))) return candidate;
    } catch {}
  }
  return null;
}

function _resolveNativeCodexBinaryForPackage(pkgDir) {
  if (!pkgDir) return null;
  const localVendor = _nativeVendorHit(path.join(pkgDir, 'vendor'));
  if (localVendor) return localVendor;

  const platformPkg = _resolvePlatformPackageDir(pkgDir);
  if (!platformPkg) return null;
  return _nativeVendorHit(path.join(platformPkg, 'vendor'));
}

function _looksLikeCodexPackageDir(p) {
  if (!p) return false;
  try {
    return fs.statSync(p).isDirectory()
      && fs.existsSync(path.join(p, 'package.json'))
      && fs.existsSync(path.join(p, 'bin', 'codex.js'));
  } catch {
    return false;
  }
}

function _probeCodexPackage(pkgDir) {
  if (!pkgDir) return null;
  let stat = null;
  try { stat = fs.statSync(pkgDir); } catch { return null; }
  if (stat.isFile()) {
    const packageRoot = _resolveNpmPackageFromBin(pkgDir);
    if (packageRoot) {
      const nativeHit = _resolveNativeCodexBinaryForPackage(packageRoot);
      if (!nativeHit) return null;
      if (_isNodeLoadable(pkgDir)) return { bin: pkgDir, native: false, source: 'npm-package' };
      if (path.resolve(pkgDir) === path.resolve(nativeHit.bin)) return { bin: pkgDir, native: true, source: nativeHit.source };
      return null;
    }
    if (!_isNodeLoadable(pkgDir)) return { bin: pkgDir, native: true, source: 'direct' };
    return null;
  }

  const nativeHit = _resolveNativeCodexBinaryForPackage(pkgDir);
  const binJs = path.join(pkgDir, 'bin', 'codex.js');
  if (fs.existsSync(binJs)) {
    if (!nativeHit) return null;
    return { bin: binJs, native: false, source: 'npm-package' };
  }

  if (nativeHit) return { bin: nativeHit.bin, native: true, source: nativeHit.source };
  return null;
}

function _platformTriple(platform = process.platform, arch = process.arch) {
  if (platform === 'darwin' && arch === 'arm64') return 'aarch64-apple-darwin';
  if (platform === 'darwin' && arch === 'x64') return 'x86_64-apple-darwin';
  if ((platform === 'linux' || platform === 'android') && arch === 'arm64') return 'aarch64-unknown-linux-musl';
  if ((platform === 'linux' || platform === 'android') && arch === 'x64') return 'x86_64-unknown-linux-musl';
  if (platform === 'win32' && arch === 'arm64') return 'aarch64-pc-windows-msvc';
  if (platform === 'win32' && arch === 'x64') return 'x86_64-pc-windows-msvc';
  return null;
}

function _normalizeWhichPath(p) {
  if (!p) return null;
  return isWin ? p : p.replace(/^\/([a-zA-Z])\//, (_, d) => d.toUpperCase() + ':/').replace(/\//g, path.sep);
}

function _isNodeLoadable(file) {
  const ext = path.extname(file).toLowerCase();
  return ext === '.js' || ext === '.mjs' || ext === '.cjs';
}

function _resolveNpmPackageFromBin(binPath) {
  if (!binPath) return null;
  let real = binPath;
  try { real = fs.realpathSync(binPath); } catch {}
  const normalized = real.replace(/\\/g, '/');
  const marker = '/node_modules/@openai/codex/';
  const idx = normalized.indexOf(marker);
  if (idx >= 0) {
    const root = normalized.slice(0, idx + marker.length - 1);
    return root.split('/').join(path.sep);
  }

  const dir = path.dirname(real);
  const candidates = [
    path.join(dir, 'node_modules', '@openai', 'codex'),
    path.join(dir, '..', 'lib', 'node_modules', '@openai', 'codex'),
  ];
  for (const c of candidates) {
    try { if (fs.existsSync(c)) return c; } catch {}
  }
  return null;
}

function _resolvePathHit(binPath) {
  if (!binPath) return null;
  const normalized = _normalizeWhichPath(binPath);
  if (!normalized || !fs.existsSync(normalized)) return null;

  const pkgDir = _resolveNpmPackageFromBin(normalized);
  if (pkgDir) return _probeCodexPackage(pkgDir);

  if (isWin) {
    const ext = path.extname(normalized).toLowerCase();
    if (ext === '.exe') return { bin: normalized, native: true, source: 'path' };
    if (_isNodeLoadable(normalized)) return { bin: normalized, native: false, source: 'path-js' };
    return null;
  }
  return { bin: normalized, native: !_isNodeLoadable(normalized), source: _isNodeLoadable(normalized) ? 'path-js' : 'path' };
}

function _findPathCodex(env) {
  try {
    const cmd = isWin ? 'where codex 2>NUL' : 'which codex 2>/dev/null';
    const raw = _execSyncCapture(cmd, env);
    for (const line of raw.split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
      const hit = _resolvePathHit(line);
      if (hit) return hit;
    }
  } catch { /* PATH probe is optional */ }
  return null;
}

function _findKnownNpmCodex(env) {
  const prefixes = [
    env.npm_config_prefix ? path.join(env.npm_config_prefix, 'node_modules', '@openai', 'codex') : '',
    env.APPDATA ? path.join(env.APPDATA, 'npm', 'node_modules', '@openai', 'codex') : '',
    '/usr/local/lib/node_modules/@openai/codex',
    '/usr/lib/node_modules/@openai/codex',
    '/opt/homebrew/lib/node_modules/@openai/codex',
    path.join(path.dirname(process.execPath), '..', 'lib', 'node_modules', '@openai', 'codex'),
    path.join(path.dirname(process.execPath), 'node_modules', '@openai', 'codex'),
    path.join(MINIONS_DIR, 'node_modules', '@openai', 'codex'),
  ].filter(Boolean);
  for (const pkg of prefixes) {
    const found = _probeCodexPackage(pkg);
    if (found) return found;
  }
  try {
    const globalRoot = _execSyncCapture('npm root -g', env).trim();
    const found = _probeCodexPackage(path.join(globalRoot, '@openai', 'codex'));
    if (found) return found;
  } catch { /* optional */ }
  return null;
}

function _isCachedBinUsable(cached) {
  if (!cached || typeof cached !== 'object') return false;
  if (cached.version !== CAPS_SCHEMA_VERSION) return false;
  if (typeof cached.codexBin !== 'string' || !cached.codexBin) return false;
  try { if (!fs.existsSync(cached.codexBin)) return false; } catch { return false; }
  if (cached.codexIsNative === false && !_isNodeLoadable(cached.codexBin)) return false;
  if (cached.codexIsNative === false) {
    const pkgDir = _resolveNpmPackageFromBin(cached.codexBin);
    if (pkgDir && !_resolveNativeCodexBinaryForPackage(pkgDir)) return false;
  }
  return true;
}

function resolveBinary({ env = process.env, config = null } = {}) {
  const overridePath = config?.codex?.binary && config.codex.binary !== 'codex'
    ? config.codex.binary
    : null;
  if (overridePath && fs.existsSync(overridePath)) {
    const pkgHit = _probeCodexPackage(overridePath);
    if (pkgHit) return { bin: pkgHit.bin, native: pkgHit.native, leadingArgs: [] };
    if (_resolveNpmPackageFromBin(overridePath) || _looksLikeCodexPackageDir(overridePath)) return null;
    return { bin: overridePath, native: !_isNodeLoadable(overridePath), leadingArgs: [] };
  }

  const cached = _safeJson(CAPS_FILE);
  if (_isCachedBinUsable(cached)) {
    return { bin: cached.codexBin, native: !!cached.codexIsNative, leadingArgs: [] };
  }

  const found = _findPathCodex(env) || _findKnownNpmCodex(env);
  if (!found) return null;

  _safeWriteJson(CAPS_FILE, {
    version: CAPS_SCHEMA_VERSION,
    codexBin: found.bin,
    codexIsNative: !!found.native,
    source: found.source || 'unknown',
    resolvedAt: new Date().toISOString(),
  });
  return { bin: found.bin, native: !!found.native, leadingArgs: [] };
}

function resolveModel(input) {
  if (input == null || input === '') return undefined;
  return String(input);
}

function modelLooksFamiliar(model) {
  if (!model) return true;
  const m = String(model).toLowerCase();
  if (m.startsWith('gpt-')) return true;
  if (m.startsWith('o3') || m.startsWith('o4')) return true;
  if (m.includes('codex')) return true;
  const catalog = module.exports._readCatalogIds();
  if (catalog && catalog.has(m)) return true;
  return false;
}

function _readCatalogIds() {
  const cached = _safeJson(MODELS_CACHE);
  const list = cached && Array.isArray(cached.models) ? cached.models : null;
  if (!list || list.length === 0) return null;
  const ids = new Set();
  for (const m of list) {
    const id = m && (m.id || m.name);
    if (id) ids.add(String(id).toLowerCase());
  }
  return ids.size > 0 ? ids : null;
}

function _mapEffort(level) {
  if (level == null || level === '') return undefined;
  const s = String(level);
  if (s === 'max') return 'xhigh';
  return s;
}

function _normalizeConfigOverrides(configOverrides) {
  if (!configOverrides) return [];
  if (Array.isArray(configOverrides)) return configOverrides.filter(Boolean).map(String);
  if (typeof configOverrides === 'object') {
    return Object.entries(configOverrides)
      .filter(([k, v]) => k && v !== undefined)
      .map(([k, v]) => `${k}=${typeof v === 'string' ? JSON.stringify(v) : JSON.stringify(v)}`);
  }
  return [];
}

function buildArgs(opts = {}) {
  const {
    model,
    effort,
    sessionId,
    outputLastMessage,
    sandbox = 'workspace-write',
    skipGitRepoCheck = true,
    ignoreRules = true,
    configOverrides,
  } = opts;

  const args = ['exec'];
  if (sessionId) args.push('resume', String(sessionId));

  args.push('--json', '--color', 'never');
  if (model) args.push('--model', String(model));
  if (sandbox) args.push('--sandbox', String(sandbox));
  if (skipGitRepoCheck === true) args.push('--skip-git-repo-check');
  if (ignoreRules === true) args.push('--ignore-rules');
  if (outputLastMessage) args.push('--output-last-message', String(outputLastMessage));

  const mappedEffort = _mapEffort(effort);
  if (mappedEffort) args.push('--config', `model_reasoning_effort="${mappedEffort}"`);

  for (const override of _normalizeConfigOverrides(configOverrides)) {
    args.push('--config', override);
  }

  args.push('-');
  return args;
}

function buildSpawnFlags(opts = {}) {
  const flags = ['--runtime', RUNTIME_NAME];
  if (opts.maxTurns != null) flags.push('--max-turns', String(opts.maxTurns));
  if (opts.model) flags.push('--model', String(opts.model));
  if (opts.allowedTools) flags.push('--allowedTools', String(opts.allowedTools));
  if (module.exports.capabilities.effortLevels && opts.effort) flags.push('--effort', String(opts.effort));
  if (module.exports.capabilities.sessionResume && opts.sessionId) flags.push('--resume', String(opts.sessionId));
  return flags;
}

function buildPrompt(promptText, sysPromptText, opts = {}) {
  const user = promptText == null ? '' : String(promptText);
  if (opts && opts.sessionId) return user;
  if (sysPromptText == null || sysPromptText === '') return user;
  return `<system>\n${String(sysPromptText)}\n</system>\n\n${user}`;
}

function getUserAssetDirs({ homeDir = os.homedir() } = {}) {
  return [
    path.join(homeDir, '.codex'),
    path.join(homeDir, '.agents'),
  ];
}

function getSkillRoots({ homeDir = os.homedir(), project = null } = {}) {
  const roots = [
    { dir: path.join(homeDir, '.agents', 'skills'), scope: 'agent-skill' },
    { dir: path.join('/etc', 'codex', 'skills'), scope: 'codex-admin' },
  ];
  if (project?.localPath) {
    const projectName = project.name || path.basename(project.localPath);
    roots.push({ dir: path.join(project.localPath, '.agents', 'skills'), scope: 'project', projectName });
  }
  return roots;
}

function getSkillWriteTargets({ homeDir = os.homedir(), project = null } = {}) {
  const targets = { personal: path.join(homeDir, '.agents', 'skills') };
  if (project?.localPath) targets.project = path.join(project.localPath, '.agents', 'skills');
  return targets;
}

function getResumeSessionId({ agentId, branchName, agentsDir, maxAgeMs = 2 * 60 * 60 * 1000, logger = console } = {}) {
  if (!agentId || agentId.startsWith('temp-') || !agentsDir) return null;
  const sessionPath = path.join(agentsDir, agentId, 'session.json');
  try {
    const sessionFile = _safeJson(sessionPath);
    if (!sessionFile?.sessionId || !sessionFile.savedAt) return null;
    if (sessionFile.runtime && sessionFile.runtime !== RUNTIME_NAME) {
      if (logger && typeof logger.info === 'function') {
        logger.info(`Skipping resume for ${agentId}: runtime mismatch (session: ${sessionFile.runtime}, current: ${RUNTIME_NAME}) - clearing session.json`);
      }
      try { fs.unlinkSync(sessionPath); } catch {}
      return null;
    }
    const sessionAge = Date.now() - new Date(sessionFile.savedAt).getTime();
    const sameBranch = branchName && sessionFile.branch && sessionFile.branch === branchName;
    if (sessionAge < maxAgeMs && sameBranch) return sessionFile.sessionId;
  } catch (e) {
    if (logger && typeof logger.warn === 'function') logger.warn('session resume lookup: ' + e.message);
  }
  return null;
}

function saveSession({ agentId, dispatchId, branch, sessionId, agentsDir, now = ts, writeJson = safeWrite, logger = console } = {}) {
  if (!sessionId || !agentId || agentId.startsWith('temp-') || !agentsDir) return false;
  try {
    writeJson(path.join(agentsDir, agentId, 'session.json'), {
      sessionId,
      dispatchId,
      savedAt: typeof now === 'function' ? now() : new Date().toISOString(),
      branch: branch || null,
      runtime: RUNTIME_NAME,
    });
    return true;
  } catch (err) {
    if (logger && typeof logger.warn === 'function') logger.warn(`Session save: ${err.message}`);
    return false;
  }
}

function detectPermissionGate(outputChunk) {
  const lower = String(outputChunk || '').toLowerCase();
  return /\b(approve|approval|allow|permission|trust this|continue\?)\b/.test(lower);
}

function getPromptDeliveryMode() {
  return 'stdin';
}

function usesSystemPromptFile() {
  return false;
}

function _pickString(...values) {
  for (const value of values) {
    if (typeof value === 'string' && value) return value;
  }
  return '';
}

function _eventType(obj) {
  return String(obj?.type || obj?.event || obj?.event_type || obj?.data?.type || '');
}

function _extractSessionId(obj) {
  return _pickString(
    obj?.session_id,
    obj?.sessionId,
    obj?.conversation_id,
    obj?.conversationId,
    obj?.thread_id,
    obj?.threadId,
    obj?.data?.session_id,
    obj?.data?.sessionId,
    obj?.data?.conversation_id,
    obj?.data?.thread_id,
  );
}

function _extractModel(obj) {
  return _pickString(obj?.model, obj?.data?.model, obj?.metadata?.model, obj?.response?.model);
}

function _extractText(obj) {
  const type = _eventType(obj);
  const item = obj?.item || obj?.data?.item;
  if (item && typeof item === 'object') {
    const itemType = String(item.type || '');
    if (/agent_message|assistant_message|message|output_text/i.test(itemType)) {
      const text = _pickString(item.text, item.content, item.message, item.output);
      if (text) return text;
    }
  }
  if (/agent_message|assistant_message|message|output_text|task_complete|final/i.test(type)) {
    return _pickString(
      obj?.text,
      obj?.message,
      obj?.content,
      obj?.summary,
      obj?.final_message,
      obj?.finalMessage,
      obj?.data?.text,
      obj?.data?.message,
      obj?.data?.content,
      obj?.data?.summary,
      obj?.data?.final_message,
      obj?.data?.finalMessage,
    );
  }
  if (obj?.type === 'result' || obj?.type === 'turn.completed') {
    return _pickString(obj?.output, obj?.text, obj?.message, obj?.data?.output, obj?.data?.text, obj?.data?.message);
  }
  return '';
}

function _extractDelta(obj) {
  const type = _eventType(obj);
  if (!/delta|chunk/i.test(type)) return '';
  return _pickString(
    obj?.delta,
    obj?.text_delta,
    obj?.content_delta,
    obj?.data?.delta,
    obj?.data?.text_delta,
    obj?.data?.content_delta,
    obj?.data?.deltaContent,
  );
}

function _extractToolUse(obj) {
  const type = _eventType(obj);
  const item = obj?.item || obj?.data?.item;
  const itemType = String(item?.type || '');
  if (!/tool|function|command|web_search/i.test(type) && !/tool|command|web_search/i.test(itemType)) return null;
  const data = item || obj.data || obj;
  const name = _pickString(
    data.tool_name,
    data.toolName,
    data.name,
    data.command,
    data.tool,
    data.server && data.tool ? `${data.server}.${data.tool}` : '',
    data.function?.name,
  );
  if (!name) return null;
  const input = data.arguments || data.args || data.input || data.function?.arguments || {};
  return { name, input };
}

function _usageFrom(obj) {
  const source = obj?.usage || obj?.data?.usage || obj?.response?.usage || null;
  if (!source || typeof source !== 'object') return null;
  return {
    costUsd: source.cost_usd ?? source.costUsd ?? null,
    inputTokens: source.input_tokens ?? source.inputTokens ?? source.prompt_tokens ?? null,
    outputTokens: source.output_tokens ?? source.outputTokens ?? source.completion_tokens ?? null,
    cacheRead: source.cached_input_tokens ?? source.cache_read_input_tokens ?? source.cacheReadInputTokens ?? null,
    cacheCreation: source.cache_creation_input_tokens ?? source.cacheCreationInputTokens ?? null,
    durationMs: source.duration_ms ?? source.durationMs ?? obj?.duration_ms ?? obj?.durationMs ?? 0,
    numTurns: source.num_turns ?? source.numTurns ?? null,
  };
}

function parseOutput(raw, { maxTextLength = 0 } = {}) {
  const safeRaw = raw == null ? '' : String(raw);
  if (!safeRaw) return { text: '', usage: null, sessionId: null, model: null };

  let text = '';
  let deltaText = '';
  let usage = null;
  let sessionId = null;
  let model = null;

  for (const rawLine of safeRaw.split('\n')) {
    const line = rawLine.trim();
    if (!line) continue;
    if (!line.startsWith('{')) {
      text = line;
      continue;
    }
    let obj;
    try { obj = JSON.parse(line); } catch { continue; }
    const id = _extractSessionId(obj);
    if (id) sessionId = id;
    const m = _extractModel(obj);
    if (m && !model) model = m;
    const u = _usageFrom(obj);
    if (u) usage = u;
    const delta = _extractDelta(obj);
    if (delta) deltaText += delta;
    const eventText = _extractText(obj);
    if (eventText) {
      text = eventText;
      deltaText = '';
    }
  }

  const finalText = text || deltaText;
  return {
    text: maxTextLength && finalText.length > maxTextLength ? finalText.slice(-maxTextLength) : finalText,
    usage,
    sessionId,
    model,
  };
}

function parseStreamChunk(line) {
  const trimmed = line == null ? '' : String(line).trim();
  if (!trimmed || !trimmed.startsWith('{')) return null;
  try { return JSON.parse(trimmed); } catch { return null; }
}

function _collectErrorSignal(rawOutput) {
  const text = rawOutput == null ? '' : String(rawOutput);
  if (!text) return '';
  const signals = [];
  let sawJsonLine = false;
  for (const rawLine of text.split('\n')) {
    const line = rawLine.trim();
    if (!line) continue;
    if (!line.startsWith('{')) {
      signals.push(line);
      continue;
    }
    let obj;
    try { obj = JSON.parse(line); } catch { continue; }
    sawJsonLine = true;
    const type = _eventType(obj).toLowerCase();
    const isError = type.includes('error') || obj.error != null || obj.data?.error != null || obj.is_error === true;
    if (!isError) continue;
    for (const value of [
      obj.error,
      obj.message,
      obj.stderr,
      obj.data?.error,
      obj.data?.message,
      obj.data?.stderr,
    ]) {
      if (typeof value === 'string' && value.trim()) signals.push(value.trim());
      else if (value && typeof value === 'object' && typeof value.message === 'string') signals.push(value.message);
    }
  }
  if (signals.length > 0) return signals.join('\n');
  return sawJsonLine ? '' : text;
}

function _extractInvalidModelName(text) {
  if (!text) return null;
  const patterns = [
    /(?:unknown|invalid)\s+model(?:\s+id)?\s*[:=]?\s*['"`]?([A-Za-z0-9._\/-]+)['"`]?/i,
    /model\s+['"`]([^'"`]+)['"`]\s+(?:not\s+found|is\s+invalid|is\s+unknown|invalid)/i,
    /model\s+(?:not\s+found|invalid|unknown)\s*[:=]?\s*['"`]?([A-Za-z0-9._\/-]+)['"`]?/i,
    /"model"\s*:\s*"([^"]+)"/i,
  ];
  for (const re of patterns) {
    const m = text.match(re);
    if (m && m[1]) return m[1];
  }
  return null;
}

function parseError(rawOutput) {
  const text = _collectErrorSignal(rawOutput);
  if (!text) return { message: '', code: null, retriable: true };
  const lower = text.toLowerCase();

  const hasAuth = /not authenticated|login required|please.*log.*in|api key.*invalid|invalid api key|\bunauthorized\b|\b401\b|\b403\b/i.test(text);
  if (hasAuth) {
    return { message: 'Codex authentication failed - run `codex login` or configure an OpenAI API key, then try again.', code: 'auth-failure', retriable: false };
  }
  if (/context window|context.*length.*exceeded|token limit|prompt.*too long|conversation.*too long/i.test(text)) {
    return { message: 'Codex context window exhausted', code: 'context-limit', retriable: false };
  }
  if (/missing optional dependency\s+@openai\/codex-|reinstall codex|unsupported platform/i.test(text)) {
    return { message: 'Codex installation is incomplete for this platform. Reinstall `@openai/codex` or install the native Codex CLI, then restart Minions.', code: 'config-error', retriable: false };
  }
  if (/unknown\s+model|model\s+not\s+found|invalid\s+model|model_not_found|400[^a-z]+(?:bad\s+request|invalid|model)/i.test(text)) {
    const name = _extractInvalidModelName(text) || 'unknown';
    return { message: `Model "${name}" not available for runtime codex. Configure a valid model in Settings -> Engine.`, code: 'model-unavailable', retriable: false };
  }
  if (/overloaded|service[_ ]unavailable|temporarily unavailable|\b503\b|rate limit|too many requests|\b429\b/i.test(text)) {
    return { message: text, code: 'model-unavailable', retriable: true };
  }
  if (/sandbox|permission denied|operation not permitted|not allowed|approval required|read-only/i.test(lower)) {
    return { message: text, code: 'permission-blocked', retriable: false };
  }
  if (/internal error|panic|uncaught|codex.*crashed|fatal: codex/i.test(lower)) {
    return { message: text, code: 'crash', retriable: true };
  }
  return { message: '', code: null, retriable: true };
}

function _runtimeFailureClass(code) {
  if (code === 'config-error') return FAILURE_CLASS.CONFIG_ERROR;
  if (code === 'auth-failure' || code === 'permission-blocked') return FAILURE_CLASS.PERMISSION_BLOCKED;
  if (code === 'context-limit') return FAILURE_CLASS.OUT_OF_CONTEXT;
  if (code === 'model-unavailable') return FAILURE_CLASS.MODEL_UNAVAILABLE;
  if (code === 'crash') return FAILURE_CLASS.SPAWN_ERROR;
  return null;
}

function classifyFailure({ code, stdout = '', stderr = '', fallback } = {}) {
  if (code === 78) return { failureClass: FAILURE_CLASS.CONFIG_ERROR, retryable: false, message: 'Codex configuration error' };
  const parsed = parseError(`${stdout || ''}\n${stderr || ''}`);
  const runtimeClass = parsed.code ? _runtimeFailureClass(parsed.code) : null;
  if (runtimeClass) return { failureClass: runtimeClass, retryable: parsed.retriable !== false, message: parsed.message || '' };
  const fallbackClass = typeof fallback === 'function' ? fallback(code, stdout, stderr) : FAILURE_CLASS.UNKNOWN;
  return { failureClass: fallbackClass, retryable: parsed.retriable !== false, message: parsed.message || '' };
}

function _normalizeModelEntry(entry) {
  if (!entry) return null;
  if (typeof entry === 'string') return { id: entry, name: entry, provider: 'openai' };
  if (typeof entry !== 'object') return null;
  const id = entry.id || entry.name || entry.model || entry.slug;
  if (!id) return null;
  return {
    id: String(id),
    name: String(entry.name || entry.display_name || entry.label || id),
    provider: String(entry.provider || entry.provider_id || entry.vendor || 'openai'),
  };
}

function _extractModelsFromCatalog(catalog) {
  if (!catalog || typeof catalog !== 'object') return null;
  const candidates = [
    catalog.models,
    catalog.data,
    catalog.catalog,
    catalog.available_models,
    catalog.availableModels,
  ];
  for (const c of candidates) {
    if (Array.isArray(c)) {
      const models = c.map(_normalizeModelEntry).filter(Boolean);
      return models.length > 0 ? models : null;
    }
  }
  const values = Object.values(catalog).filter(v => v && typeof v === 'object');
  const models = values.map(_normalizeModelEntry).filter(Boolean);
  return models.length > 0 ? models : null;
}

async function listModels({ env = process.env, timeoutMs = 10000 } = {}) {
  const resolved = resolveBinary({ env });
  if (!resolved) return null;
  let raw = '';
  try {
    raw = _execFileCapture(resolved.bin, [...(resolved.leadingArgs || []), 'debug', 'models', '--bundled'], {
      env,
      timeoutMs,
      native: resolved.native,
    });
  } catch {
    return null;
  }
  try {
    const parsed = JSON.parse(raw);
    return _extractModelsFromCatalog(parsed);
  } catch {
    return null;
  }
}

function createStreamConsumer(ctx) {
  let deltaText = '';
  let terminalText = '';

  function consume(obj) {
    if (!obj || typeof obj !== 'object') return;
    const id = _extractSessionId(obj);
    if (id) ctx.setSessionId(id);

    const u = _usageFrom(obj);
    if (u) ctx.setUsage(u);

    const type = _eventType(obj);
    if (/reasoning|thinking/i.test(type)) ctx.notifyThinking();

    const toolUse = _extractToolUse(obj);
    if (toolUse) ctx.pushToolUse(toolUse.name, toolUse.input);

    const delta = _extractDelta(obj);
    if (delta) {
      deltaText += delta;
      ctx.pushText(deltaText);
    }

    const eventText = _extractText(obj);
    if (eventText) {
      terminalText = eventText;
      deltaText = '';
      if (/complete|completed|final|result/i.test(type)) ctx.setText(eventText);
      else ctx.pushText(eventText);
    }
  }

  function reset() {
    deltaText = '';
    terminalText = '';
  }

  return { consume, reset };
}

const capabilities = {
  streaming: true,
  sessionResume: true,
  midRunSessionId: false,
  systemPromptFile: false,
  effortLevels: true,
  costTracking: false,
  modelShorthands: false,
  modelDiscovery: true,
  promptViaArg: false,
  budgetCap: false,
  bareMode: false,
  fallbackModel: false,
  sessionPersistenceControl: false,
  resumePromptCarryover: true,
  streamConsumer: true,
};

const INSTALL_HINT = 'install Codex CLI with `npm install -g @openai/codex` or `brew install --cask codex`, then run `codex login`.';
const PERMISSION_BYPASS_FLAGS = ['--sandbox'];

module.exports = {
  name: RUNTIME_NAME,
  capabilities,
  resolveBinary,
  capsFile: CAPS_FILE,
  listModels,
  modelsCache: MODELS_CACHE,
  spawnScript: path.join(ENGINE_DIR, 'spawn-agent.js'),
  installHint: INSTALL_HINT,
  buildSpawnFlags,
  buildArgs,
  buildPrompt,
  getUserAssetDirs,
  getSkillRoots,
  getSkillWriteTargets,
  getResumeSessionId,
  saveSession,
  detectPermissionGate,
  getPromptDeliveryMode,
  usesSystemPromptFile,
  classifyFailure,
  resolveModel,
  modelLooksFamiliar,
  parseOutput,
  parseStreamChunk,
  parseError,
  createStreamConsumer,
  permissionBypassFlags: PERMISSION_BYPASS_FLAGS,
  _platformTriple,
  _probeCodexPackage,
  _resolvePathHit,
  _isCachedBinUsable,
  _resolveNativeCodexBinaryForPackage,
  _mapEffort,
  _normalizeConfigOverrides,
  _extractModelsFromCatalog,
  _readCatalogIds,
  _extractInvalidModelName,
  CAPS_SCHEMA_VERSION,
};
