import { spawn } from "child_process";
import { EventEmitter } from "events";
import { toProviderNativeReasoningEffort } from "../config.mjs";
import {
  assertWorkspacePolicyCompatibility,
  buildWorkspacePolicyContext,
  normalizeAssetReadablePaths,
} from "../workspace-policy.mjs";

function normalizeApprovalPolicy(policy, dangerousMode) {
  if (typeof policy === "string" && policy.trim()) {
    const normalized = policy.trim().toLowerCase();
    if (normalized === "suggested") return "on-request";
    if (normalized === "plan") return "on-request";
    if (normalized === "auto-approve") return "never";
    if (normalized === "bypasspermissions") return "never";
    return normalized;
  }
  return dangerousMode === true ? "never" : null;
}

function normalizeSandboxMode(mode, dangerousMode) {
  if (typeof mode === "string" && mode.trim()) {
    const normalized = mode.trim().toLowerCase();
    if (normalized === "workspacewrite" || normalized === "workspace-write") return "workspace-write";
    if (normalized === "readonly" || normalized === "read-only") return "read-only";
    if (normalized === "dangerfullaccess" || normalized === "danger-full-access") {
      return "danger-full-access";
    }
    return normalized;
  }
  return dangerousMode === true ? "danger-full-access" : null;
}

function isPlainObject(value) {
  return value && typeof value === "object" && !Array.isArray(value);
}

function normalizeCodexUserInputAnswers(questions, rawAnswers) {
  if (!isPlainObject(rawAnswers)) {
    throw new Error("Codex user input response requires an answers object");
  }
  if (!Array.isArray(questions) || questions.length === 0) {
    throw new Error("Codex user input request is missing questions");
  }

  const normalized = {};
  const expectedIds = new Set();

  for (const question of questions) {
    if (!isPlainObject(question) || typeof question.id !== "string" || !question.id.trim()) {
      throw new Error("Codex user input request has an invalid question id");
    }
    const questionId = question.id;
    expectedIds.add(questionId);
    const answer = rawAnswers[questionId];
    if (!isPlainObject(answer)) {
      throw new Error(`Missing answer payload for Codex question "${questionId}"`);
    }
    if (!Array.isArray(answer.answers) || answer.answers.length === 0) {
      throw new Error(`Codex question "${questionId}" requires at least one answer`);
    }
    const values = answer.answers.map((entry) => {
      if (typeof entry !== "string" || !entry.trim()) {
        throw new Error(`Codex question "${questionId}" includes an invalid answer value`);
      }
      return entry.trim();
    });
    normalized[questionId] = { answers: values };
  }

  for (const key of Object.keys(rawAnswers)) {
    if (!expectedIds.has(key)) {
      throw new Error(`Unexpected Codex user input answer key: ${key}`);
    }
  }

  return normalized;
}

function normalizeCodexElicitationResponse(params, rawResponse) {
  if (!isPlainObject(rawResponse)) {
    throw new Error("Codex MCP elicitation response requires a response object");
  }
  const action = typeof rawResponse.action === "string" ? rawResponse.action.trim() : "";
  if (!["accept", "decline", "cancel"].includes(action)) {
    throw new Error("Codex MCP elicitation response requires action accept, decline, or cancel");
  }

  const response = {
    action,
    content: rawResponse.content ?? null,
    _meta: rawResponse._meta ?? null,
  };

  if (params?.mode === "form" && action === "accept") {
    if (!isPlainObject(response.content)) {
      throw new Error("Codex MCP form elicitation requires object content when accepting");
    }
  }

  if (action !== "accept") {
    response.content = null;
  }

  return response;
}

function codexPendingControlKindToSubtype(kind) {
  if (kind === "user_input") return "user_input";
  if (kind === "mcp_elicitation") return "mcp_elicitation";
  if (kind === "command") return "command";
  if (kind === "patch") return "patch";
  if (kind === "permissions") return "permissions";
  throw new Error(`Unsupported Codex pending control kind: ${kind}`);
}

function buildUserInput(text) {
  return [{ type: "text", text }];
}

function normalizePendingMessage(payload) {
  if (typeof payload === "string") {
    return {
      text: payload,
      rawText: payload,
      isSlashCommand: false,
      slashCommandName: null,
    };
  }
  const text = typeof payload?.text === "string" ? payload.text : "";
  const rawText = typeof payload?.rawText === "string" ? payload.rawText : text;
  return {
    text,
    rawText,
    isSlashCommand: payload?.isSlashCommand === true,
    slashCommandName: typeof payload?.slashCommandName === "string" && payload.slashCommandName.trim()
      ? payload.slashCommandName.trim().toLowerCase()
      : null,
  };
}

function ensureReasoningState(reasoningStateMap, itemId) {
  if (!(reasoningStateMap instanceof Map) || typeof itemId !== "string" || !itemId) {
    return null;
  }
  const existing = reasoningStateMap.get(itemId);
  if (isPlainObject(existing)) {
    existing.sawDelta = existing.sawDelta === true;
    existing.summaryParts = Array.isArray(existing.summaryParts) ? existing.summaryParts : [];
    existing.emittedCompleteText = typeof existing.emittedCompleteText === "string" ? existing.emittedCompleteText : "";
    return existing;
  }
  const state = { sawDelta: false, summaryParts: [], emittedCompleteText: "" };
  reasoningStateMap.set(itemId, state);
  return state;
}

function extractReasoningText(value, seen = new Set()) {
  if (typeof value === "string") {
    return value;
  }
  if (Array.isArray(value)) {
    return value
      .map((entry) => extractReasoningText(entry, seen).trim())
      .filter(Boolean)
      .join("\n");
  }
  if (!isPlainObject(value)) {
    return "";
  }
  if (seen.has(value)) {
    return "";
  }
  seen.add(value);

  if (typeof value.text === "string") {
    return value.text;
  }

  const candidateKeys = [
    "delta",
    "content",
    "summary",
    "part",
    "summaryPart",
    "summary_part",
    "parts",
    "value",
  ];
  return candidateKeys
    .map((key) => extractReasoningText(value[key], seen).trim())
    .filter(Boolean)
    .join("\n");
}

function extractReasoningSummaryPartText(params) {
  if (!isPlainObject(params)) return "";
  const directCandidates = [
    params.summaryPart,
    params.summary_part,
    params.part,
    params.summary,
    params.content,
    params.text,
  ];
  for (const candidate of directCandidates) {
    const extracted = extractReasoningText(candidate).trim();
    if (extracted) {
      return extracted;
    }
  }
  return extractReasoningText(params).trim();
}

function extractCompletedReasoningText(item, state) {
  if (!isPlainObject(item)) return "";

  if (typeof item.text === "string" && item.text.trim()) {
    return item.text.trim();
  }

  const summaryText = extractReasoningText(item.summary).trim();
  if (summaryText) {
    return summaryText;
  }

  const accumulatedSummaryText = extractReasoningText(state?.summaryParts || []).trim();
  if (accumulatedSummaryText) {
    return accumulatedSummaryText;
  }

  return extractReasoningText(item.content).trim();
}

export class CodexAppServerAdapter {
  constructor(options = {}) {
    this.command = options.command || "codex";
    this.getConfiguredCommandFn = typeof options.getConfiguredCommand === "function"
      ? options.getConfiguredCommand
      : null;
    this.spawnFn = options.spawnFn || spawn;
    this.providerId = "codex";
    this.capabilities = {
      interactiveSession: true,
      resume: true,
      permissionResponses: true,
      historyImport: false,
      interrupt: true,
      workspacePolicy: true,
    };
  }

  getConfiguredCommand() {
    if (!this.getConfiguredCommandFn) return null;
    const value = this.getConfiguredCommandFn();
    return typeof value === "string" && value.trim() ? value.trim() : null;
  }

  resolveCommand() {
    return this.getConfiguredCommand() || this.command;
  }

  startSession({
    sessionId,
    cwd,
    agentId,
    resumeSessionId,
    fork = false,
    model,
    approvalPolicy,
    sandboxMode,
    dangerousMode,
    searchEnabled,
    imageInputEnabled,
    native,
    workspacePolicy,
    assetReadablePaths,
    runtimeEnv,
  }) {
    const normalizedSandboxMode = normalizeSandboxMode(sandboxMode, dangerousMode);
    assertWorkspacePolicyCompatibility({
      provider: this.providerId,
      workspacePolicy,
      sandboxMode: normalizedSandboxMode,
    });
    const extraArgs = Array.isArray(native?.args) ? native.args : [];
    const mergedEnv =
      runtimeEnv && typeof runtimeEnv === "object" && !Array.isArray(runtimeEnv)
        ? { ...process.env, ...runtimeEnv }
        : { ...process.env };
    const proc = this.spawnFn(this.resolveCommand(), [
      "app-server",
      "--listen",
      "stdio://",
      ...extraArgs,
    ], {
      cwd,
      stdio: ["pipe", "pipe", "pipe"],
      env: mergedEnv,
    });

    return new CodexAppServerSession(sessionId, proc, {
      cwd,
      agentId,
      resumeSessionId,
      fork,
      model,
      approvalPolicy,
      sandboxMode: normalizedSandboxMode,
      dangerousMode,
      searchEnabled,
      imageInputEnabled,
      native,
      workspacePolicy,
      assetReadablePaths,
    });
  }

  sendMessage(session, payload) {
    session.send(payload);
  }

  respondToControl(session, payload) {
    session.respondToControl(payload);
  }

  stopSession(session) {
    session.stop();
  }

  interruptSession(session) {
    session.interrupt();
  }

  killSession(session) {
    session.kill();
  }

  supportsInteractiveSession() {
    return true;
  }

  supportsResume() {
    return true;
  }

  supportsPermissionResponses() {
    return true;
  }
}

export class CodexAppServerSession extends EventEmitter {
  constructor(id, proc, options) {
    super();
    this.id = id;
    this.proc = proc;
    this.cwd = options.cwd;
    this.agentId = options.agentId;
    this.provider = "codex";
    this.transport = "app-server";
    this.capabilities = {
      interactiveSession: true,
      resume: true,
      permissionResponses: true,
      historyImport: false,
      interrupt: true,
      workspacePolicy: true,
    };
    this.model = options.model || null;
    this.reasoningEffort = options.reasoningEffort || null;
    this.reasoningEffortSource = options.reasoningEffortSource || null;
    this.approvalPolicy = normalizeApprovalPolicy(options.approvalPolicy, options.dangerousMode);
    this.sandboxMode = normalizeSandboxMode(options.sandboxMode, options.dangerousMode);
    this.dangerousMode = options.dangerousMode === true;
    this.searchEnabled = options.searchEnabled === true;
    this.imageInputEnabled = options.imageInputEnabled === true;
    this.native = options.native || { args: [], configOverrides: {}, commands: [], profile: null };
    this.workspacePolicy = options.workspacePolicy || null;
    this.assetReadablePaths = normalizeAssetReadablePaths(options.assetReadablePaths || []);
    this.resumeSessionId = options.resumeSessionId || null;
    this.fork = options.fork === true;
    this.threadId = null;
    this.providerResumeId = null;
    this.activeTurnId = null;
    this.alive = true;
    this._ready = false;
    this._stdoutBuffer = "";
    this._nextRequestId = 1;
    this._pendingRequests = new Map();
    this._pendingMessages = [];
    this._pendingControls = new Map();
    this._agentMessageState = new Map();
    this._reasoningState = new Map();
    this._sending = false;
    this._steerWaitTurnId = null;
    this._awaitingTurnStart = false;

    proc.stdout.on("data", (chunk) => this.consumeStdout(chunk.toString()));
    proc.stderr.on("data", (chunk) => {
      this.emit("event", {
        type: "stderr",
        provider: this.provider,
        text: chunk.toString(),
      });
    });
    proc.on("exit", (code, signal) => {
      this.alive = false;
      for (const pending of this._pendingRequests.values()) {
        pending.reject(new Error(`Codex app-server exited before response (code=${code}, signal=${signal || "none"})`));
      }
      this._pendingRequests.clear();
      this.emit("exit", code, signal);
    });
    proc.on("error", (err) => {
      this.alive = false;
      this.emit("error", err);
    });

    this.initialize().catch((err) => this.emit("error", err));
  }

  async initialize() {
    await this.request("initialize", {
      clientInfo: {
        name: "oysterun",
        title: "Oysterun Host",
        version: "0.1.0",
      },
      capabilities: {
        experimentalApi: true,
      },
    });
    this.notify("initialized", {});

    if (this.resumeSessionId && this.fork === true) {
      const result = await this.request("thread/fork", this.buildThreadForkParams(this.resumeSessionId));
      this.captureThread(result?.thread || null);
    } else if (this.resumeSessionId) {
      const result = await this.request("thread/resume", this.buildThreadResumeParams(this.resumeSessionId));
      this.captureThread(result?.thread || null);
    }

    this._ready = true;
    this.emitReady();
    this.drainPendingMessages();
  }

  buildThreadConfig() {
    const config = isPlainObject(this.native?.configOverrides)
      ? { ...this.native.configOverrides }
      : {};
    const translatedReasoningEffort = toProviderNativeReasoningEffort("codex", this.reasoningEffort, this.model);
    if (translatedReasoningEffort && this.reasoningEffortSource !== "native") {
      config.model_reasoning_effort = translatedReasoningEffort;
    }
    if (!Object.prototype.hasOwnProperty.call(config, "model_reasoning_summary")) {
      // Ask Codex to emit a readable reasoning summary instead of relying on encrypted-only
      // reasoning payloads that collapse to the "(reasoning)" placeholder in Oysterun.
      config.model_reasoning_summary = "detailed";
    }
    if (!Object.prototype.hasOwnProperty.call(config, "model_supports_reasoning_summaries")) {
      config.model_supports_reasoning_summaries = true;
    }
    if (this.native?.profile) {
      config.profile = this.native.profile;
    }
    config.web_search = this.searchEnabled === true ? "live" : "disabled";
    const tools = isPlainObject(config.tools) ? { ...config.tools } : {};
    tools.view_image = this.imageInputEnabled === true;
    config.tools = tools;
    return config;
  }

  buildThreadStartParams() {
    return {
      model: this.model || undefined,
      cwd: this.cwd,
      approvalPolicy: this.approvalPolicy || undefined,
      sandbox: this.sandboxMode || undefined,
      serviceName: "oysterun",
      config: this.buildThreadConfig(),
      persistExtendedHistory: true,
    };
  }

  buildThreadResumeParams(threadId) {
    return {
      threadId,
      model: this.model || undefined,
      cwd: this.cwd,
      approvalPolicy: this.approvalPolicy || undefined,
      sandbox: this.sandboxMode || undefined,
      config: this.buildThreadConfig(),
      persistExtendedHistory: true,
    };
  }

  buildThreadForkParams(threadId) {
    return {
      threadId,
      model: this.model || undefined,
      cwd: this.cwd,
      approvalPolicy: this.approvalPolicy || undefined,
      sandbox: this.sandboxMode || undefined,
      config: this.buildThreadConfig(),
      persistExtendedHistory: true,
    };
  }

  buildSandboxPolicy() {
    assertWorkspacePolicyCompatibility({
      provider: this.provider,
      workspacePolicy: this.workspacePolicy,
      sandboxMode: this.sandboxMode,
    });
    const policyContext = buildWorkspacePolicyContext({
      workspacePolicy: this.workspacePolicy,
      assetReadablePaths: this.assetReadablePaths,
    });
    if (this.sandboxMode === "danger-full-access") {
      return { type: "dangerFullAccess" };
    }
    if (this.sandboxMode === "read-only") {
      return {
        type: "readOnly",
        access: {
          type: "restricted",
          includePlatformDefaults: true,
          readableRoots: policyContext.readablePaths,
        },
        networkAccess: false,
      };
    }
    if (this.sandboxMode === "workspace-write") {
      return {
        type: "workspaceWrite",
        writableRoots: policyContext.writableRoots,
        permissionProfile: {
          type: "restricted",
          includePlatformDefaults: true,
          readableRoots: policyContext.readablePaths,
        },
        networkAccess: false,
      };
    }
    return null;
  }

  buildTurnStartParams(text) {
    return {
      threadId: this.threadId,
      input: buildUserInput(text),
      model: this.model || undefined,
      cwd: this.cwd,
      approvalPolicy: this.approvalPolicy || undefined,
      sandboxPolicy: this.buildSandboxPolicy() || undefined,
    };
  }

  captureThread(thread) {
    const nextThreadId = thread?.id || null;
    if (typeof nextThreadId === "string" && nextThreadId) {
      this.threadId = nextThreadId;
      this.providerResumeId = nextThreadId;
    }
  }

  emitReady() {
    this.emit("event", {
      type: "session.ready",
      provider: this.provider,
      session_id: this.id,
      thread_id: this.threadId,
      model: this.model,
      approval_policy: this.approvalPolicy,
      sandbox_mode: this.sandboxMode,
      provider_resume_id: this.providerResumeId,
      transport: this.transport,
    });
  }

  consumeStdout(text) {
    this._stdoutBuffer += text;
    const lines = this._stdoutBuffer.split("\n");
    this._stdoutBuffer = lines.pop() ?? "";
    for (const line of lines) {
      if (!line.trim()) continue;
      let msg;
      try {
        msg = JSON.parse(line);
      } catch {
        this.emit("event", {
          type: "session.notice",
          provider: this.provider,
          subtype: "raw_stdout",
          text: line,
        });
        continue;
      }
      this.handleMessage(msg);
    }
  }

  handleMessage(msg) {
    if (msg.id !== undefined && (Object.prototype.hasOwnProperty.call(msg, "result") || Object.prototype.hasOwnProperty.call(msg, "error"))) {
      const pending = this._pendingRequests.get(String(msg.id));
      if (!pending) return;
      this._pendingRequests.delete(String(msg.id));
      if (msg.error) {
        pending.reject(new Error(msg.error.message || JSON.stringify(msg.error)));
        return;
      }
      pending.resolve(msg.result);
      return;
    }

    if (typeof msg.method !== "string") return;

    if (msg.id !== undefined) {
      this.handleServerRequest(msg);
      return;
    }

    this.handleNotification(msg.method, msg.params || {});
  }

  handleServerRequest(msg) {
    const requestId = String(msg.id);
    const params = msg.params || {};

    if (msg.method === "item/commandExecution/requestApproval") {
      this._pendingControls.set(requestId, { kind: "command", params });
      this.emit("event", {
        type: "control.request",
        provider: this.provider,
        request_id: requestId,
        subtype: "command",
        payload: params,
      });
      return;
    }

    if (msg.method === "item/fileChange/requestApproval") {
      this._pendingControls.set(requestId, { kind: "patch", params });
      this.emit("event", {
        type: "control.request",
        provider: this.provider,
        request_id: requestId,
        subtype: "patch",
        payload: params,
      });
      return;
    }

    if (msg.method === "item/permissions/requestApproval") {
      this._pendingControls.set(requestId, { kind: "permissions", params });
      this.emit("event", {
        type: "control.request",
        provider: this.provider,
        request_id: requestId,
        subtype: "permissions",
        payload: params,
      });
      return;
    }

    if (msg.method === "item/tool/requestUserInput") {
      this._pendingControls.set(requestId, { kind: "user_input", params });
      this.emit("event", {
        type: "control.request",
        provider: this.provider,
        request_id: requestId,
        subtype: "user_input",
        payload: params,
      });
      return;
    }

    if (msg.method === "mcpServer/elicitation/request") {
      this._pendingControls.set(requestId, { kind: "mcp_elicitation", params });
      this.emit("event", {
        type: "control.request",
        provider: this.provider,
        request_id: requestId,
        subtype: "mcp_elicitation",
        payload: params,
      });
      return;
    }

    this.writeJson({
      jsonrpc: "2.0",
      id: msg.id,
      error: {
        code: -32601,
        message: `Unsupported app-server request: ${msg.method}`,
      },
    });
    this.emit("event", {
      type: "runtime.error",
      provider: this.provider,
      error: `Unsupported app-server request: ${msg.method}`,
    });
  }

  handleNotification(method, params) {
    if (method === "thread/started") {
      this.captureThread(params.thread || null);
      if (this._ready) {
        this.emitReady();
      }
      return;
    }

    if (method === "thread/status/changed") {
      this.emit("event", {
        type: "session.notice",
        provider: this.provider,
        subtype: "thread.status_changed",
        payload: params,
      });
      return;
    }

    if (method === "turn/started") {
      const turnId = params?.turn?.id || null;
      if (typeof turnId === "string" && turnId) {
        this.activeTurnId = turnId;
        this._steerWaitTurnId = null;
      }
      this._awaitingTurnStart = false;
      this.emit("event", {
        type: "session.notice",
        provider: this.provider,
        subtype: "turn.started",
        payload: params,
      });
      return;
    }

    if (method === "turn/completed") {
      const turn = params?.turn || {};
      if (turn.id && turn.id === this.activeTurnId) {
        this.activeTurnId = null;
      }
      this._awaitingTurnStart = false;
      this._steerWaitTurnId = null;
      this.emit("event", {
        type: "turn.completed",
        provider: this.provider,
        thread_id: this.threadId,
        request_id: null,
        usage: turn.usage || null,
        status: turn.status || null,
        error: turn.error?.message || null,
      });
      if (turn.status === "failed" && turn.error?.message) {
        this.emit("event", {
          type: "runtime.error",
          provider: this.provider,
          error: turn.error.message,
        });
      }
      this.drainPendingMessages();
      return;
    }

    if (method === "thread/compacted") {
      this._awaitingTurnStart = false;
      this.emit("event", {
        type: "session.notice",
        provider: this.provider,
        subtype: "compact_boundary",
        payload: params,
      });
      return;
    }

    if (method === "item/started") {
      for (const event of this.normalizeItemStarted(params.item || null)) {
        this.emit("event", event);
      }
      return;
    }

    if (method === "item/completed") {
      for (const event of this.normalizeItemCompleted(params.item || null)) {
        this.emit("event", event);
      }
      return;
    }

    if (method === "item/agentMessage/delta") {
      const itemId = params.itemId || params.item_id || null;
      if (typeof itemId === "string" && itemId) {
        const state = this._agentMessageState.get(itemId) || { sawDelta: false };
        state.sawDelta = true;
        this._agentMessageState.set(itemId, state);
      }
      this.emit("event", {
        type: "message.assistant",
        provider: this.provider,
        text: typeof params.delta === "string" ? params.delta : "",
        delta: true,
      });
      return;
    }

    if (method === "item/commandExecution/outputDelta") {
      this.emit("event", {
        type: "tool.output",
        provider: this.provider,
        call_id: params.itemId || params.item_id || null,
        stream: null,
        text: typeof params.delta === "string" ? params.delta : "",
      });
      return;
    }

    if (method === "item/fileChange/outputDelta") {
      this.emit("event", {
        type: "tool.output",
        provider: this.provider,
        call_id: params.itemId || params.item_id || null,
        stream: null,
        text: typeof params.delta === "string" ? params.delta : "",
      });
      return;
    }

    if (method === "serverRequest/resolved") {
      const requestId = params.requestId !== undefined ? String(params.requestId) : null;
      if (requestId) {
        this._pendingControls.delete(requestId);
      }
      this.drainPendingMessages();
      return;
    }

    if (method === "item/reasoning/textDelta") {
      const itemId = params.itemId || params.item_id || null;
      const text = typeof params.delta === "string" ? params.delta : "";
      if (!text) {
        return;
      }
      if (typeof itemId === "string" && itemId) {
        const state = ensureReasoningState(this._reasoningState, itemId);
        if (state) {
          state.sawDelta = true;
        }
      }
      this.emit("event", {
        type: "message.thinking",
        provider: this.provider,
        text,
        delta: true,
        redacted: false,
      });
      return;
    }

    if (method === "item/reasoning/summaryTextDelta") {
      const itemId = params.itemId || params.item_id || null;
      const text = typeof params.delta === "string" ? params.delta : "";
      if (!text) {
        return;
      }
      if (typeof itemId === "string" && itemId) {
        const state = ensureReasoningState(this._reasoningState, itemId);
        if (state) {
          state.sawDelta = true;
        }
      }
      this.emit("event", {
        type: "message.thinking",
        provider: this.provider,
        text,
        delta: true,
        summary: true,
        redacted: false,
      });
      return;
    }

    if (method === "item/reasoning/summaryPartAdded") {
      const itemId = params.itemId || params.item_id || null;
      const text = extractReasoningSummaryPartText(params);
      if (!text) {
        return;
      }
      if (typeof itemId === "string" && itemId) {
        const state = ensureReasoningState(this._reasoningState, itemId);
        if (state) {
          state.sawDelta = true;
          state.summaryParts.push(text);
          state.emittedCompleteText = extractReasoningText(state.summaryParts).trim();
        }
      }
      this.emit("event", {
        type: "message.thinking",
        provider: this.provider,
        text,
        delta: false,
        summary: true,
        redacted: false,
      });
      return;
    }

    this.emit("event", {
      type: "session.notice",
      provider: this.provider,
      subtype: method.replace(/\//g, "."),
      payload: params,
    });
  }

  normalizeItemStarted(item) {
    if (!item || typeof item !== "object") return [];

    if (item.type === "commandExecution") {
      return [{
        type: "tool.call",
        provider: this.provider,
        call_id: item.id || null,
        name: "exec_command",
        input: {
          command: item.command || "",
          cwd: item.cwd || null,
          command_actions: item.commandActions || [],
        },
      }];
    }

    if (item.type === "fileChange") {
      return [{
        type: "tool.call",
        provider: this.provider,
        call_id: item.id || null,
        name: "apply_patch",
        input: {
          changes: item.changes || [],
        },
      }];
    }

    if (item.type === "reasoning") {
      return [{
        type: "thinking.started",
        provider: this.provider,
        item_id: item.id || null,
      }];
    }

    return [];
  }

  normalizeItemCompleted(item) {
    if (!item || typeof item !== "object") return [];

    if (item.type === "agentMessage") {
      const itemId = item.id || null;
      if (itemId) {
        this._agentMessageState.delete(itemId);
      }
      const text = typeof item.text === "string" ? item.text : "";
      if (!text) return [];
      return [{
        type: "message.assistant",
        provider: this.provider,
        text,
        delta: false,
      }];
    }

    if (item.type === "commandExecution") {
      return [{
        type: "tool.result",
        provider: this.provider,
        call_id: item.id || null,
        name: "exec_command",
        content: {
          stdout: item.aggregatedOutput || "",
          stderr: "",
          exit_code: item.exitCode ?? null,
          success: item.status === "completed",
          status: item.status || null,
          duration_ms: item.durationMs ?? null,
          command: item.command || "",
          command_actions: item.commandActions || [],
        },
        is_error: item.status === "failed" || item.status === "declined",
      }];
    }

    if (item.type === "fileChange") {
      return [{
        type: "tool.result",
        provider: this.provider,
        call_id: item.id || null,
        name: "apply_patch",
        content: {
          stdout: "",
          stderr: "",
          success: item.status === "completed",
          status: item.status || null,
          changes: item.changes || [],
        },
        is_error: item.status === "failed" || item.status === "declined",
      }];
    }

    if (item.type === "reasoning") {
      const itemId = item.id || null;
      const state = itemId ? this._reasoningState.get(itemId) : null;
      if (itemId) {
        this._reasoningState.delete(itemId);
      }
      const text = extractCompletedReasoningText(item, state);
      if (!text) return [];
      if (state?.emittedCompleteText && state.emittedCompleteText === text) {
        return [];
      }
      return [{
        type: "message.thinking",
        provider: this.provider,
        text,
        redacted: false,
      }];
    }

    return [];
  }

  send(payload) {
    if (!this.alive) throw new Error(`Session ${this.id} is not alive`);
    this._pendingMessages.push(normalizePendingMessage(payload));
    this.drainPendingMessages();
  }

  async ensureThreadStarted() {
    if (this.threadId) return;
    const result = await this.request("thread/start", this.buildThreadStartParams());
    this.captureThread(result?.thread || null);
    this.emitReady();
  }

  async drainPendingMessages() {
    if (!this._ready || !this.alive || this._sending) return;
    if (this._pendingMessages.length === 0) return;
    if (this._pendingControls.size > 0) return;
    if (this._awaitingTurnStart) return;

    this._sending = true;
    try {
      await this.ensureThreadStarted();
      if (this.activeTurnId) {
        if (this._steerWaitTurnId !== this.activeTurnId) {
          this._steerWaitTurnId = this.activeTurnId;
          this.emit("event", {
            type: "session.notice",
            provider: this.provider,
            subtype: "turn.queue_wait",
            payload: {
              thread_id: this.threadId,
              turn_id: this.activeTurnId,
            },
          });
        }
        return;
      } else {
        const nextMessage = this._pendingMessages[0];
        if (nextMessage?.isSlashCommand === true && nextMessage.slashCommandName === "compact") {
          this._awaitingTurnStart = true;
          await this.request("thread/compact/start", {
            threadId: this.threadId,
          });
        } else {
          const result = await this.request("turn/start", this.buildTurnStartParams(nextMessage?.text || ""));
          this.activeTurnId = result?.turn?.id || result?.turnId || this.activeTurnId;
          if (!this.activeTurnId) {
            this._awaitingTurnStart = true;
          }
        }
        this._pendingMessages.shift();
      }
    } catch (err) {
      this._awaitingTurnStart = false;
      if (this._pendingMessages.length > 0) {
        // Host-side delivery state owns retry/skip semantics. Do not keep replaying
        // the same provider payload forever after a fatal turn-start failure.
        this._pendingMessages.shift();
      }
      this.emit("error", err);
    } finally {
      this._sending = false;
      if (
        this._pendingMessages.length > 0
        && this.activeTurnId === null
        && this._pendingControls.size === 0
        && this._awaitingTurnStart === false
      ) {
        queueMicrotask(() => this.drainPendingMessages());
      }
    }
  }

  respondToControl(payload) {
    if (!this.alive) throw new Error(`Session ${this.id} is not alive`);
    if (!isPlainObject(payload)) {
      throw new Error("Codex control response payload must be an object");
    }
    const requestId = String(payload.requestId || payload.request_id || "");
    if (!requestId) {
      throw new Error("Codex control response is missing requestId");
    }
    const pending = this._pendingControls.get(requestId);
    if (!pending) {
      throw new Error(`Unknown Codex approval request: ${requestId}`);
    }
    this._pendingControls.delete(requestId);

    if (pending.kind === "user_input") {
      const answers = normalizeCodexUserInputAnswers(pending.params.questions, payload.answers);
      this.writeJson({
        jsonrpc: "2.0",
        id: requestId,
        result: {
          answers,
        },
      });
      return;
    }

    if (pending.kind === "mcp_elicitation") {
      const response = normalizeCodexElicitationResponse(pending.params, payload.response);
      this.writeJson({
        jsonrpc: "2.0",
        id: requestId,
        result: response,
      });
      return;
    }

    if (typeof payload.allow !== "boolean") {
      throw new Error(`Codex ${pending.kind} control response requires boolean allow`);
    }

    if (pending.kind === "permissions") {
      this.writeJson({
        jsonrpc: "2.0",
        id: requestId,
        result: {
          scope: "turn",
          permissions: payload.allow ? (pending.params.permissions || {}) : {},
        },
      });
      return;
    }

    this.writeJson({
      jsonrpc: "2.0",
      id: requestId,
      result: {
        decision: payload.allow ? "accept" : "decline",
      },
    });
  }

  getPendingControlRequests() {
    return [...this._pendingControls.entries()].map(([requestId, pending]) => ({
      provider: this.provider,
      request_id: requestId,
      subtype: codexPendingControlKindToSubtype(pending.kind),
      payload: pending.params,
    }));
  }

  stop() {
    if (this.proc.stdin.writable) {
      this.proc.stdin.end();
    }
    if (this.alive) {
      this.proc.kill("SIGTERM");
      setTimeout(() => {
        if (this.alive) this.proc.kill("SIGKILL");
      }, 5000);
    }
  }

  kill() {
    this.proc.kill("SIGTERM");
    setTimeout(() => {
      if (this.alive) this.proc.kill("SIGKILL");
    }, 5000);
  }

  interrupt() {
    if (!this.alive) throw new Error(`Session ${this.id} is not alive`);
    if (!this.threadId || !this.activeTurnId) return;
    this.request("turn/interrupt", {
      threadId: this.threadId,
      turnId: this.activeTurnId,
    }).catch((err) => this.emit("error", err));
  }

  request(method, params) {
    const id = String(this._nextRequestId++);
    this.writeJson({
      jsonrpc: "2.0",
      id,
      method,
      params,
    });
    return new Promise((resolve, reject) => {
      this._pendingRequests.set(id, { resolve, reject });
    });
  }

  notify(method, params) {
    this.writeJson({
      jsonrpc: "2.0",
      method,
      params,
    });
  }

  writeJson(payload) {
    if (!this.proc.stdin.writable) {
      throw new Error("Codex app-server stdin is not writable");
    }
    this.proc.stdin.write(`${JSON.stringify(payload)}\n`);
  }
}
