import { spawn, type ChildProcessWithoutNullStreams, type SpawnOptions } from "node:child_process";
import { mkdir, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { tmpdir } from "node:os";
import { join } from "node:path";

import type { McpServerConfig } from "@cursor/sdk";

import type {
  AgentCreateSpec,
  AgentSdkAdapter,
  AgentSendSpec,
} from "./AgentSdkAdapter.ts";
import type { RunHandle } from "../types/state.ts";
import { CodexCliRunHandle } from "../session/CodexCliRunHandle.ts";
import { CodexAppServerRunHandle } from "../session/CodexAppServerRunHandle.ts";

export type CodexCliSpawnFn = (
  command: string,
  args: readonly string[],
  options: SpawnOptions,
) => ChildProcessWithoutNullStreams;

export type CodexCliVersionProbe = (
  command: string,
  env: NodeJS.ProcessEnv,
) => Promise<void>;

export type CodexCliAuthMode = "chatgpt-subscription" | "custom-provider";

export interface CodexCliAdapterOptions {
  authMode?: CodexCliAuthMode;
  defaultCwd?: string;
  command?: string;
  defaultModel?: string;
  providerId?: string;
  providerName?: string;
  baseUrl?: string;
  apiKeyEnv?: string;
  codexHomeRoot?: string;
  spawnFn?: CodexCliSpawnFn;
  probeVersion?: CodexCliVersionProbe;
}

const DEFAULT_PROVIDER_ID = "volcengine_ark";
const DEFAULT_PROVIDER_NAME = "Volcengine Ark";
const DEFAULT_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";

/**
 * Runtime registries persist the physical SDK id returned by the provider
 * that originally created an agent record.  Switching an existing instance
 * from Cursor to Codex therefore legitimately presents Cursor-shaped
 * `agent-...` ids to this adapter.  Codex does not use the value as a process
 * argument or filesystem path, so accept any bounded opaque registry token
 * while still rejecting empty or unsafe input.
 */
export function isSafeSdkAgentId(value: string): boolean {
  return /^[A-Za-z0-9][A-Za-z0-9:._-]{0,255}$/.test(value);
}

function tomlString(value: string): string {
  return JSON.stringify(value);
}

function tomlArray(values: readonly string[]): string {
  return `[${values.map(tomlString).join(", ")}]`;
}

function safeSegment(value: string): string {
  return value.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 120) || "session";
}

function codexConfigOverride(key: string, value: string): string[] {
  return ["-c", `${key}=${value}`];
}

function mcpConfigOverrides(mcpServers?: Record<string, McpServerConfig>): string[] {
  const args: string[] = [];
  for (const [name, config] of Object.entries(mcpServers ?? {})) {
    if (!isStdioMcp(config)) continue;
    if (!/^[A-Za-z0-9_-]+$/.test(name)) {
      throw new Error(`CodexCliAdapter: invalid MCP server name=${name}`);
    }
    const section = `mcp_servers.${name}`;
    args.push(...codexConfigOverride(`${section}.command`, tomlString(config.command)));
    const commandArgs = (config as { args?: string[] }).args;
    if (Array.isArray(commandArgs) && commandArgs.length > 0) {
      args.push(...codexConfigOverride(`${section}.args`, tomlArray(commandArgs)));
    }
    const env = (config as { env?: Record<string, string> }).env;
    for (const [key, value] of Object.entries(env ?? {})) {
      if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
        throw new Error(`CodexCliAdapter: invalid MCP environment key=${key}`);
      }
      args.push(...codexConfigOverride(`${section}.env.${key}`, tomlString(value)));
    }
    const cwd = (config as { cwd?: string }).cwd;
    if (typeof cwd === "string" && cwd.trim()) {
      args.push(...codexConfigOverride(`${section}.cwd`, tomlString(cwd)));
    }
  }
  return args;
}

export function buildCodexAppServerArgs(input: {
  mcpServers?: Record<string, McpServerConfig>;
}): string[] {
  return [
    "app-server",
    "--stdio",
    ...codexConfigOverride("model_provider", tomlString("openai")),
    ...codexConfigOverride("approval_policy", tomlString("never")),
    ...codexConfigOverride("approvals_reviewer", tomlString("auto_review")),
    ...mcpConfigOverrides(input.mcpServers),
  ];
}

export function buildCodexCliExecArgs(input: {
  authMode: CodexCliAuthMode;
  workspace: string;
  model: string;
  text: string;
  mcpServers?: Record<string, McpServerConfig>;
}): string[] {
  const subscriptionMode = input.authMode === "chatgpt-subscription";
  return [
    "exec",
    "--json",
    "--skip-git-repo-check",
    "--sandbox", "workspace-write",
    "--cd", input.workspace,
    "--model", input.model,
    ...(subscriptionMode
      ? [
          ...codexConfigOverride("model_provider", tomlString("openai")),
          ...codexConfigOverride("approval_policy", tomlString("never")),
          // `codex exec` cannot present interactive MCP approval prompts.
          // Route them to Codex's built-in risk reviewer so safe read tools
          // can proceed and risky tools still receive a semantic decision.
          ...codexConfigOverride("approvals_reviewer", tomlString("auto_review")),
          ...mcpConfigOverrides(input.mcpServers),
        ]
      : []),
    input.text,
  ];
}

type StdioMcpConfig = Extract<McpServerConfig, { command: string }>;

function isStdioMcp(config: McpServerConfig): config is StdioMcpConfig {
  return typeof (config as { command?: unknown }).command === "string";
}

export function buildCodexCliConfig(input: {
  model: string;
  providerId?: string;
  providerName?: string;
  baseUrl?: string;
  apiKeyEnv?: string;
  mcpServers?: Record<string, McpServerConfig>;
}): string {
  const providerId = input.providerId ?? DEFAULT_PROVIDER_ID;
  const lines = [
    `model = ${tomlString(input.model)}`,
    `model_provider = ${tomlString(providerId)}`,
    `approval_policy = "never"`,
    "",
    `[model_providers.${providerId}]`,
    `name = ${tomlString(input.providerName ?? DEFAULT_PROVIDER_NAME)}`,
    `base_url = ${tomlString((input.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, ""))}`,
    `env_key = ${tomlString(input.apiKeyEnv ?? "ARK_API_KEY")}`,
    `wire_api = "responses"`,
    `requires_openai_auth = false`,
  ];

  for (const [name, config] of Object.entries(input.mcpServers ?? {})) {
    if (!isStdioMcp(config)) continue;
    const section = JSON.stringify(name);
    lines.push("", `[mcp_servers.${section}]`, `command = ${tomlString(config.command)}`);
    const args = (config as { args?: string[] }).args;
    if (Array.isArray(args) && args.length > 0) lines.push(`args = ${tomlArray(args)}`);
    const env = (config as { env?: Record<string, string> }).env;
    if (env && Object.keys(env).length > 0) {
      lines.push("", `[mcp_servers.${section}.env]`);
      for (const [key, value] of Object.entries(env)) {
        lines.push(`${JSON.stringify(key)} = ${tomlString(value)}`);
      }
    }
  }
  return `${lines.join("\n")}\n`;
}

function defaultVersionProbe(command: string, env: NodeJS.ProcessEnv): Promise<void> {
  if (/\\WindowsApps\\/i.test(command)) {
    return Promise.reject(new Error(
      "CODEX_CLI_DESKTOP_BINARY_UNSUPPORTED: install the standalone @openai/codex CLI and set CODEX_CLI_BIN",
    ));
  }
  return new Promise((resolve, reject) => {
    const child = spawn(command, ["--version"], {
      env,
      shell: false,
      stdio: ["ignore", "pipe", "pipe"],
    });
    let stderr = "";
    child.stderr.on("data", (chunk: Buffer | string) => { stderr += chunk.toString(); });
    child.on("error", (error) => reject(new Error(
      `CODEX_CLI_NOT_FOUND: ${command}: ${error.message}. Install @openai/codex or set CODEX_CLI_BIN.`,
    )));
    child.on("close", (code) => {
      if (code === 0) resolve();
      else reject(new Error(
        `CODEX_CLI_PROBE_FAILED: ${command} --version exited ${code ?? "unknown"}: ${stderr.trim() || "no stderr"}`,
      ));
    });
  });
}

/**
 * AgentSdkAdapter backed by standalone Codex CLI.
 *
 * The Codex execution harness supplies sandbox/tool/MCP orchestration. It can
 * authenticate either with an existing ChatGPT subscription or with a custom
 * Responses-compatible provider. Provider credentials are never embedded in
 * generated config.
 */
export class CodexCliAdapter implements AgentSdkAdapter {
  readonly #options: Required<Omit<CodexCliAdapterOptions, "spawnFn" | "probeVersion">> & {
    spawnFn: CodexCliSpawnFn;
    probeVersion: CodexCliVersionProbe;
  };
  readonly #knownAgents = new Set<string>();

  constructor(options: CodexCliAdapterOptions = {}) {
    const authMode = options.authMode ?? "custom-provider";
    const defaultModel = options.defaultModel?.trim() || (
      authMode === "chatgpt-subscription"
        ? process.env["CODEX_SUBSCRIPTION_MODEL"]?.trim() || "gpt-5.6-sol"
        : process.env["ARK_MODEL_ID"]?.trim()
    );
    if (!defaultModel) {
      throw new Error(
        authMode === "chatgpt-subscription"
          ? "CodexCliAdapter requires CODEX_SUBSCRIPTION_MODEL/defaultModel"
          : "CodexCliAdapter requires ARK_MODEL_ID/defaultModel",
      );
    }
    this.#options = {
      authMode,
      defaultCwd: options.defaultCwd ?? process.cwd(),
      command: options.command ?? process.env["CODEX_CLI_BIN"]?.trim() ?? "codex",
      defaultModel,
      providerId: options.providerId ?? DEFAULT_PROVIDER_ID,
      providerName: options.providerName ?? DEFAULT_PROVIDER_NAME,
      baseUrl: options.baseUrl ?? process.env["ARK_BASE_URL"]?.trim() ?? DEFAULT_BASE_URL,
      apiKeyEnv: options.apiKeyEnv ?? "ARK_API_KEY",
      codexHomeRoot: options.codexHomeRoot ?? process.env["CODEX_CLI_HOME_ROOT"]?.trim() ?? (
        authMode === "chatgpt-subscription"
          ? process.env["CODEX_HOME"]?.trim() || join(homedir(), ".codex")
          : join(tmpdir(), "codeflowmu-codex-cli")
      ),
      spawnFn: options.spawnFn ?? ((command, args, spawnOptions) =>
        spawn(command, [...args], spawnOptions) as ChildProcessWithoutNullStreams),
      probeVersion: options.probeVersion ?? defaultVersionProbe,
    };
  }

  async create(spec: AgentCreateSpec): Promise<{ sdk_agent_id: string }> {
    const id = `codex-cli:${spec.agentId}`;
    this.#knownAgents.add(id);
    return { sdk_agent_id: id };
  }

  async list(): Promise<string[]> {
    return [...this.#knownAgents];
  }

  async resume(sdkAgentId: string): Promise<void> {
    if (!isSafeSdkAgentId(sdkAgentId)) {
      throw new Error(`CodexCliAdapter.resume: unexpected sdk_agent_id=${sdkAgentId}`);
    }
    await this.#options.probeVersion(this.#options.command, process.env);
    this.#knownAgents.add(sdkAgentId);
  }

  async send(spec: AgentSendSpec, sdkAgentId: string): Promise<RunHandle> {
    if (!isSafeSdkAgentId(sdkAgentId)) {
      throw new Error(`CodexCliAdapter.send: unexpected sdk_agent_id=${sdkAgentId}`);
    }
    const model = spec.modelId && !/^(?:auto|auto-smart|default|inherit)$/i.test(spec.modelId)
      ? spec.modelId
      : this.#options.defaultModel;
    const workspace = spec.workspace ?? this.#options.defaultCwd;
    const subscriptionMode = this.#options.authMode === "chatgpt-subscription";
    const codexHome = subscriptionMode
      ? this.#options.codexHomeRoot
      : join(this.#options.codexHomeRoot, safeSegment(spec.sessionId));
    if (!subscriptionMode) {
      await mkdir(codexHome, { recursive: true });
      await writeFile(join(codexHome, "config.toml"), buildCodexCliConfig({
        model,
        providerId: this.#options.providerId,
        providerName: this.#options.providerName,
        baseUrl: this.#options.baseUrl,
        apiKeyEnv: this.#options.apiKeyEnv,
        mcpServers: spec.mcpServers,
      }), "utf8");
    }

    const env: NodeJS.ProcessEnv = {
      ...process.env,
      CODEX_HOME: codexHome,
    };

    if (subscriptionMode) {
      const handle = new CodexAppServerRunHandle({
        sessionId: spec.sessionId,
        agentId: spec.agentId,
        provider: "openai-chatgpt-subscription",
        model,
        workspace,
        text: spec.text,
        mcpServers: spec.mcpServers,
        maxToolRounds: spec.maxToolRounds,
      });
      const args = buildCodexAppServerArgs({ mcpServers: spec.mcpServers });
      setImmediate(() => {
        try {
          const child = this.#options.spawnFn(this.#options.command, args, {
            cwd: workspace,
            env,
            shell: false,
            stdio: ["pipe", "pipe", "pipe"],
          });
          handle.attachProcess(child);
        } catch (error) {
          handle.acceptMessageForTest({
            method: "error",
            params: { error: error instanceof Error ? error.message : String(error) },
          });
        }
      });
      return handle;
    }

    const handle = new CodexCliRunHandle({
      sessionId: spec.sessionId,
      agentId: spec.agentId,
      provider: this.#options.providerId,
      model,
      maxToolRounds: spec.maxToolRounds,
    });
    const args = buildCodexCliExecArgs({
      authMode: this.#options.authMode,
      workspace,
      model,
      text: spec.text,
      mcpServers: spec.mcpServers,
    });

    setImmediate(() => {
      try {
        const child = this.#options.spawnFn(this.#options.command, args, {
          cwd: workspace,
          env,
          shell: false,
          stdio: ["ignore", "pipe", "pipe"],
        });
        handle.attachProcess(child);
      } catch (error) {
        handle.acceptEventForTest({
          type: "error",
          error: error instanceof Error ? error.message : String(error),
        });
      }
    });
    return handle;
  }

  async listModels(): Promise<string[]> {
    return [this.#options.defaultModel];
  }
}
