function createAgentRuntime(deps) {
  const {
    processEnv,
    CLAUDE_PATH,
    CODEX_PATH,
    MODEL_MAP,
    loadModelConfig,
    applyCustomTemplateToSettings,
    getDefaultCodexModel,
    loadCodexConfig,
    prepareCodexCustomRuntime,
    ccwebMcpServerArg,
    ccwebMcpServerArgs,
    internalMcpUrl,
    internalMcpToken,
    nodePath,
    wsSend,
    truncateObj,
    sanitizeToolInput,
    loadSession,
    saveSession,
    setRuntimeSessionId,
    getRuntimeSessionId,
    titleToolInstructions,
  } = deps;

  function readRuntimePositiveIntEnv(name, fallback, options = {}) {
    const raw = Number.parseInt(String(processEnv?.[name] || ''), 10);
    const min = Number.isFinite(options.min) ? options.min : 1;
    const max = Number.isFinite(options.max) ? options.max : Number.MAX_SAFE_INTEGER;
    if (!Number.isFinite(raw) || raw <= 0) return fallback;
    return Math.max(min, Math.min(max, raw));
  }

  const RUNTIME_FULL_TEXT_MAX_CHARS = readRuntimePositiveIntEnv(
    'CC_WEB_RUNTIME_FULL_TEXT_MAX_CHARS',
    256 * 1024,
    { min: 4096 },
  );
  const RUNTIME_TRUNCATED_HEAD = '[cc-web: 前文过长，已保留尾部以保护服务稳定性]\n';

  function keepTail(value, maxLen) {
    const text = String(value || '');
    if (!Number.isFinite(maxLen) || maxLen <= 0 || text.length <= maxLen) return text;
    const keep = Math.max(0, maxLen - RUNTIME_TRUNCATED_HEAD.length);
    return `${RUNTIME_TRUNCATED_HEAD}${text.slice(-keep)}`;
  }

  function appendCappedText(current, addition, maxLen = RUNTIME_FULL_TEXT_MAX_CHARS) {
    return keepTail(`${String(current || '')}${String(addition || '')}`, maxLen);
  }

  function tomlString(value) {
    return JSON.stringify(String(value || ''));
  }

  function tomlStringArray(values) {
    return `[${values.map((value) => tomlString(value)).join(',')}]`;
  }

  function tomlKeySegment(value) {
    const raw = String(value || '').trim();
    if (/^[A-Za-z0-9_-]+$/.test(raw)) return raw;
    return tomlString(raw);
  }

  function tomlValue(value) {
    if (Array.isArray(value)) return `[${value.map((item) => tomlValue(item)).join(',')}]`;
    if (value && typeof value === 'object') {
      return `{${Object.entries(value)
        .map(([key, item]) => `${tomlKeySegment(key)}=${tomlValue(item)}`)
        .join(',')}}`;
    }
    if (typeof value === 'boolean') return value ? 'true' : 'false';
    if (typeof value === 'number' && Number.isFinite(value)) return String(value);
    return tomlString(value);
  }

  function createCcwebMcpEnv(session, options = {}) {
    if (!ccwebMcpServerArg || !internalMcpUrl || !internalMcpToken || !session?.id) return null;
    const rawHopCount = Number.parseInt(String(options.mcpContext?.hopCount || 0), 10);
    const hopCount = Number.isFinite(rawHopCount) ? Math.max(0, rawHopCount) : 0;
    return {
      CC_WEB_MCP_URL: internalMcpUrl,
      CC_WEB_MCP_TOKEN: internalMcpToken,
      CC_WEB_SOURCE_SESSION_ID: session.id,
      CC_WEB_CROSS_HOP_COUNT: String(hopCount),
    };
  }

  function appendCcwebMcpConfig(args, mcpEnv) {
    if (!mcpEnv) return;
    const envVars = Object.keys(mcpEnv);
    const serverArgs = Array.isArray(ccwebMcpServerArgs) && ccwebMcpServerArgs.length > 0
      ? ccwebMcpServerArgs
      : [ccwebMcpServerArg];
    args.push(
      '-c', 'mcp_servers.ccweb.type="stdio"',
      '-c', `mcp_servers.ccweb.command=${tomlString(nodePath || 'node')}`,
      '-c', `mcp_servers.ccweb.args=${tomlStringArray(serverArgs)}`,
      '-c', `mcp_servers.ccweb.env_vars=${tomlStringArray(envVars)}`,
      '-c', 'mcp_servers.ccweb.startup_timeout_sec=10',
      '-c', 'mcp_servers.ccweb.tool_timeout_sec=60'
    );
  }

  function appendProjectMcpConfig(args, mcpServer) {
    const server = String(mcpServer?.server || mcpServer?.name || '').trim();
    const config = mcpServer?.config && typeof mcpServer.config === 'object' ? mcpServer.config : null;
    if (!server || !config) return;
    const prefix = `mcp_servers.${tomlKeySegment(server)}`;
    for (const key of ['type', 'command', 'args', 'env', 'env_vars', 'url', 'bearer_token_env_var', 'startup_timeout_sec', 'tool_timeout_sec']) {
      if (!Object.prototype.hasOwnProperty.call(config, key)) continue;
      args.push('-c', `${prefix}.${key}=${tomlValue(config[key])}`);
    }
  }

  function appendProjectMcpConfigs(args, mcpServers) {
    if (!Array.isArray(mcpServers)) return;
    for (const mcpServer of mcpServers) appendProjectMcpConfig(args, mcpServer);
  }

  function buildClaudeSpawnSpec(session, options = {}) {
    const hasAttachments = Array.isArray(options.attachments) && options.attachments.length > 0;
    const args = ['-p', '--output-format', 'stream-json', '--verbose'];
    if (hasAttachments) args.push('--input-format', 'stream-json');
    const permMode = session.permissionMode || 'yolo';
    switch (permMode) {
      case 'yolo':
        args.push('--dangerously-skip-permissions');
        break;
      case 'plan':
        args.push('--permission-mode', 'plan');
        break;
      case 'default':
        args.push('--permission-mode', 'default');
        break;
    }
    if (session.claudeSessionId) {
      args.push('--resume', session.claudeSessionId);
    }
    if (session.model) {
      args.push('--model', session.model);
    }

    const env = { ...processEnv };
    delete env.CLAUDECODE;
    delete env.CLAUDE_CODE;
    delete env.CC_WEB_PASSWORD;
    for (const k of Object.keys(env)) {
      if (k.startsWith('ANTHROPIC_')) delete env[k];
    }

    const modelCfg = loadModelConfig();
    if (modelCfg.mode === 'custom' && modelCfg.activeTemplate) {
      const tpl = (modelCfg.templates || []).find((t) => t.name === modelCfg.activeTemplate);
      if (tpl) applyCustomTemplateToSettings(tpl);
    }

    return {
      command: CLAUDE_PATH,
      args,
      env,
      cwd: session.cwd || processEnv.HOME || processEnv.USERPROFILE || process.cwd(),
      parser: 'claude',
      mode: permMode,
      resume: !!session.claudeSessionId,
    };
  }

  function buildCodexSpawnSpec(session, options = {}) {
    const codexConfig = loadCodexConfig();
    const runtimeConfig = prepareCodexCustomRuntime(codexConfig);
    if (runtimeConfig?.error) {
      return { error: runtimeConfig.error };
	    }
	    const runtimeId = getRuntimeSessionId(session);
    const args = ['exec'];
    args.push('--json', '--skip-git-repo-check');
    const ccwebMcpEnv = createCcwebMcpEnv(session, options);
    appendCcwebMcpConfig(args, ccwebMcpEnv);
    appendProjectMcpConfigs(args, options.projectMcpConfigs);
    if (titleToolInstructions) {
      args.push('-c', `developer_instructions=${tomlString(titleToolInstructions)}`);
    }

	    const permMode = session.permissionMode || 'yolo';
	    // `-s/--sandbox` is an option for `codex exec`, but not for `codex exec resume`.
	    // When resuming, it must appear before the `resume` subcommand, otherwise Codex CLI errors
	    // with: "unexpected argument '-s' found".
	    if (runtimeId && permMode === 'plan') {
	      args.push('-s', 'read-only');
	    }
	    if (runtimeId) args.push('resume');
	    switch (permMode) {
	      case 'yolo':
	        args.push('--dangerously-bypass-approvals-and-sandbox');
	        break;
	      case 'plan':
	        if (!runtimeId) args.push('-s', 'read-only');
	        break;
	      case 'default':
	      default:
	        args.push('--full-auto');
        break;
    }

    const effectiveModel = session.model || getDefaultCodexModel();
    if (effectiveModel) {
      const raw = String(effectiveModel).trim();
      // cc-web UI supports "gpt-5.4(high)" style selection, but Codex CLI expects:
      // - model: "gpt-5.4"
      // - reasoning effort: config key `model_reasoning_effort = "high"`
      const m = raw.match(/^(.*)\((low|medium|high|xhigh)\)\s*$/i);
      if (m) {
        const base = String(m[1] || '').trim();
        const lvl = String(m[2] || '').trim().toLowerCase();
        if (base) args.push('--model', base);
        // Use TOML string literal to avoid parsing ambiguity.
        args.push('-c', `model_reasoning_effort="${lvl}"`);
      } else {
        args.push('--model', raw);
      }
    }
    if (Array.isArray(options.attachments)) {
      for (const attachment of options.attachments) {
        if (attachment?.path) args.push('--image', attachment.path);
      }
    }
    if (runtimeId) {
      args.push(runtimeId, '-');
    } else {
      if (session.cwd) args.push('-C', session.cwd);
      args.push('-');
    }

    const env = { ...processEnv, ...(ccwebMcpEnv || {}) };
    delete env.CC_WEB_PASSWORD;
    delete env.CLAUDECODE;
    delete env.CLAUDE_CODE;
    if (runtimeConfig?.mode === 'custom') {
      env.CODEX_HOME = runtimeConfig.homeDir;
      env.OPENAI_API_KEY = runtimeConfig.apiKey;
      delete env.OPENAI_BASE_URL;
    }

    return {
      command: CODEX_PATH,
      args,
      env,
      cwd: session.cwd || processEnv.HOME || processEnv.USERPROFILE || process.cwd(),
      parser: 'codex',
      mode: permMode,
      resume: !!runtimeId,
    };
  }

  function codexToolName(item) {
    switch (item?.type) {
      case 'command_execution':
        return 'CommandExecution';
      case 'mcp_tool_call':
        return 'McpToolCall';
      case 'file_change':
        return 'FileChange';
      case 'reasoning':
        return 'Reasoning';
      default:
        return item?.type || 'CodexItem';
    }
  }

  function codexToolInput(item) {
    if (!item) return null;
    if (item.type === 'command_execution') return { command: item.command || '' };
    return truncateObj(item, 500);
  }

  function codexToolMeta(item) {
    if (!item) return null;
    switch (item.type) {
      case 'command_execution':
        return {
          kind: 'command_execution',
          title: 'Shell Command',
          subtitle: item.command || '',
          exitCode: typeof item.exit_code === 'number' ? item.exit_code : null,
          status: item.status || null,
        };
      case 'mcp_tool_call':
        return {
          kind: 'mcp_tool_call',
          title: 'MCP Tool',
          subtitle: item.tool_name || item.name || item.server_name || '',
          status: item.status || null,
        };
      case 'file_change':
        return {
          kind: 'file_change',
          title: 'File Change',
          subtitle: item.path || item.file_path || '',
          status: item.status || null,
        };
      case 'reasoning':
        return {
          kind: 'reasoning',
          title: 'Reasoning',
          subtitle: typeof item.text === 'string' ? item.text.slice(0, 120) : '',
          status: item.status || null,
        };
      default:
        return {
          kind: item.type || 'codex_item',
          title: codexToolName(item),
          subtitle: '',
          status: item.status || null,
        };
    }
  }

  function codexToolResult(item) {
    if (!item) return '';
    if (typeof item.aggregated_output === 'string' && item.aggregated_output) return item.aggregated_output;
    if (typeof item.text === 'string' && item.text) return item.text;
    return JSON.stringify(truncateObj(item, 1200));
  }

  function formatAgentMessageDividerTime(date = new Date()) {
    const pad = (value) => String(value).padStart(2, '0');
    return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
  }

  function createAgentMessageDivider() {
    const time = formatAgentMessageDividerTime();
    return `<div class="agent-message-divider" data-divider-time="${time}"><span>${time}</span></div>`;
  }

  function hasAgentMessageBoundaryAtEnd(text) {
    return /\n\s*(?:---|\*\*\*|___)\s*$/.test(text)
      || /(?:^|\n)\s*<div\s+class=["']agent-message-divider["'][\s\S]*?<\/div>\s*$/.test(text);
  }

  function hasAgentMessageBoundaryAtStart(text) {
    return /^\s*(?:---|\*\*\*|___)\s*\n/.test(text)
      || /^\s*<div\s+class=["']agent-message-divider["'][\s\S]*?<\/div>\s*/.test(text);
  }

  function sendRuntime(entry, sessionId, payload) {
    wsSend(entry.ws, { ...payload, sessionId });
  }

  function appendCodexAgentText(entry, text) {
    const nextText = String(text || '');
    if (!nextText) return '';

    const currentText = entry.fullText || '';
    const hasExistingText = /\S/.test(currentText);
    const hasVisualBoundary = hasAgentMessageBoundaryAtEnd(currentText) || hasAgentMessageBoundaryAtStart(nextText);
    const separator = hasExistingText && !hasVisualBoundary
      ? (/\n\s*$/.test(currentText) ? `\n${createAgentMessageDivider()}\n\n` : `\n\n${createAgentMessageDivider()}\n\n`)
      : '';
    const chunk = separator + nextText;
    entry.fullText = appendCappedText(entry.fullText || '', chunk);
    return chunk;
  }

  function ensureCodexToolCall(entry, item, sessionId) {
    let tc = entry.toolCalls.find((t) => t.id === item.id);
    if (tc) {
      tc.name = codexToolName(item);
      tc.kind = item.type || tc.kind || null;
      tc.meta = codexToolMeta(item) || tc.meta || null;
      if (tc.input == null) tc.input = codexToolInput(item);
      return tc;
    }
    tc = {
      name: codexToolName(item),
      id: item.id,
      kind: item.type || null,
      meta: codexToolMeta(item),
      input: codexToolInput(item),
      done: false,
    };
    entry.toolCalls.push(tc);
    sendRuntime(entry, sessionId, {
      type: 'tool_start',
      name: tc.name,
      toolUseId: item.id,
      input: tc.input,
      kind: tc.kind,
      meta: tc.meta,
    });
    return tc;
  }

  function processClaudeEvent(entry, event, sessionId) {
    if (!event || !event.type) return;

    switch (event.type) {
      case 'system':
        if (event.session_id) {
          const session = loadSession(sessionId);
          if (session) {
            session.claudeSessionId = event.session_id;
            saveSession(session);
          }
        }
        break;

      case 'assistant': {
        const content = event.message?.content;
        if (!Array.isArray(content)) break;

        for (const block of content) {
          if (block.type === 'text' && block.text) {
            entry.fullText = appendCappedText(entry.fullText || '', block.text);
            sendRuntime(entry, sessionId, { type: 'text_delta', text: block.text });
          } else if (block.type === 'tool_use') {
            const toolInput = sanitizeToolInput(block.name, block.input);
            const tc = { name: block.name, id: block.id, input: toolInput, done: false };
            entry.toolCalls.push(tc);
            sendRuntime(entry, sessionId, { type: 'tool_start', name: block.name, toolUseId: block.id, input: tc.input });
          } else if (block.type === 'tool_result') {
            const resultText = typeof block.content === 'string'
              ? block.content
              : Array.isArray(block.content)
                ? block.content.map((c) => c.text || '').join('\n')
                : JSON.stringify(block.content);
            const tc = entry.toolCalls.find((t) => t.id === block.tool_use_id);
            if (tc) {
              tc.done = true;
              tc.result = resultText.slice(0, 2000);
            }
            sendRuntime(entry, sessionId, { type: 'tool_end', toolUseId: block.tool_use_id, result: resultText.slice(0, 2000) });
          }
        }

        if (event.session_id) {
          const session = loadSession(sessionId);
          if (session && !session.claudeSessionId) {
            session.claudeSessionId = event.session_id;
            saveSession(session);
          }
        }
        break;
      }

      case 'result': {
        const session = loadSession(sessionId);
        if (session) {
          if (event.session_id) session.claudeSessionId = event.session_id;
          if (event.total_cost_usd) session.totalCost = (session.totalCost || 0) + event.total_cost_usd;
          saveSession(session);
        }
        entry.lastCost = event.total_cost_usd || null;
        if (entry.ws && event.total_cost_usd !== undefined) {
          sendRuntime(entry, sessionId, { type: 'cost', costUsd: session?.totalCost || 0 });
        }
        break;
      }
    }
  }

  function processCodexEvent(entry, event, sessionId) {
    if (!event || !event.type) return;

    switch (event.type) {
      case 'thread.started': {
        if (!event.thread_id) break;
        const session = loadSession(sessionId);
        if (session) {
          setRuntimeSessionId(session, event.thread_id);
          saveSession(session);
        }
        break;
      }

      case 'item.started': {
        const item = event.item;
        if (!item || !item.id || item.type === 'agent_message') break;
        ensureCodexToolCall(entry, item, sessionId);
        break;
      }

      case 'item.updated': {
        const item = event.item;
        if (!item || !item.id || item.type === 'agent_message') break;
        const tc = ensureCodexToolCall(entry, item, sessionId);
        const resultText = codexToolResult(item).slice(0, 2000);
        tc.done = false;
        tc.result = resultText;

        sendRuntime(entry, sessionId, {
          type: 'tool_update',
          toolUseId: item.id,
          name: tc.name,
          input: tc.input,
          result: resultText,
          kind: tc.kind,
          meta: tc.meta,
        });
        break;
      }

      case 'item.completed': {
        const item = event.item;
        if (!item || !item.id) break;
        if (item.type === 'agent_message') {
          if (item.text) {
            let parsedContent = null;
            try {
              parsedContent = JSON.parse(item.text);
            } catch {}

            if (parsedContent && Array.isArray(parsedContent)) {
              if (!entry.contentBlocks) entry.contentBlocks = [];
              entry.contentBlocks.push(...parsedContent);
              const textOnly = parsedContent.filter(b => b.type === 'text').map(b => b.text || '').join('');
              appendCodexAgentText(entry, textOnly);
              sendRuntime(entry, sessionId, { type: 'content_blocks', blocks: parsedContent });
            } else {
              const textChunk = appendCodexAgentText(entry, item.text);
              sendRuntime(entry, sessionId, { type: 'text_delta', text: textChunk });
            }
          }
          break;
        }
        const tc = ensureCodexToolCall(entry, item, sessionId);
        const resultText = codexToolResult(item).slice(0, 2000);
        tc.done = true;
        tc.result = resultText;

        sendRuntime(entry, sessionId, {
          type: 'tool_end',
          toolUseId: item.id,
          result: resultText,
          kind: tc.kind,
          meta: tc.meta,
        });
        break;
      }

      case 'turn.completed': {
        const usage = event.usage || null;
        entry.lastUsage = usage;
        const session = loadSession(sessionId);
        if (session && usage) {
          session.totalUsage = {
            inputTokens: (session.totalUsage?.inputTokens || 0) + (usage.input_tokens || 0),
            cachedInputTokens: (session.totalUsage?.cachedInputTokens || 0) + (usage.cached_input_tokens || 0),
            outputTokens: (session.totalUsage?.outputTokens || 0) + (usage.output_tokens || 0),
          };
          saveSession(session);
          sendRuntime(entry, sessionId, { type: 'usage', totalUsage: session.totalUsage });
        }
        break;
      }

      case 'turn.failed': {
        const message = event.error?.message || 'Codex 任务失败';
        entry.lastError = message;
        break;
      }

      case 'error':
        if (event.message) {
          if (/^Reconnecting\.\.\./.test(event.message)) {
            sendRuntime(entry, sessionId, { type: 'system_message', message: event.message });
          } else {
            entry.lastError = event.message;
          }
        }
        break;
    }
  }

  function processRuntimeEvent(entry, event, sessionId) {
    if (entry.agent === 'codex') processCodexEvent(entry, event, sessionId);
    else processClaudeEvent(entry, event, sessionId);
  }

  return {
    buildClaudeSpawnSpec,
    buildCodexSpawnSpec,
    processClaudeEvent,
    processCodexEvent,
    processRuntimeEvent,
  };
}

module.exports = { createAgentRuntime };
