// Direct port of https://github.com/openai/codex/blob/f201c30c52a35f819262865a53df94b6f4ea7a50/sdk/typescript/src/exec.ts
// allow: SIZE_OK — this file intentionally preserves the upstream exec.ts responsibility boundary.

///| Mirrors ExecArgs from the upstream implementation.

///| Upstream: https://github.com/openai/codex/blob/f201c30c52a35f819262865a53df94b6f4ea7a50/sdk/typescript/src/exec.ts#L10-L41

///|
/// MoonBit uses Path for filesystem values and task cancellation instead of an AbortSignal field.
priv struct ExecArgs {
  input : String
  base_url : String?
  api_key : String?
  thread_id : String?
  images : Array[@path.Path]
  model : String?
  sandbox_mode : SandboxMode?
  working_directory : @path.Path?
  additional_directories : Array[@path.Path]
  skip_git_repo_check : Bool?
  output_schema_file : @path.Path?
  model_reasoning_effort : ModelReasoningEffort?
  network_access_enabled : Bool?
  web_search_mode : WebSearchMode?
  web_search_enabled : Bool?
  approval_policy : ApprovalMode?
}

///| Executes the Codex CLI and streams its JSONL output.

///| Upstream: https://github.com/openai/codex/blob/f201c30c52a35f819262865a53df94b6f4ea7a50/sdk/typescript/src/exec.ts#L63-L67

///| Runtime type difference: the upstream type stores `pathDirs` for directories bundled in the npm platform package; this MoonBit package has no npm optional-package resolver, so that property is intentionally absent.

///|
/// Upstream path type and bundled-directory resolution: https://github.com/openai/codex/blob/f201c30c52a35f819262865a53df94b6f4ea7a50/sdk/typescript/src/exec.ts#L58-L65 and https://github.com/openai/codex/blob/f201c30c52a35f819262865a53df94b6f4ea7a50/sdk/typescript/src/exec.ts#L412-L430
struct Exec {
  executable_path : @path.Path
  env_override : Map[String, String]?
  config : ConfigObject?
}

///| Constructs the process adapter used by a Codex client.

///| Upstream: https://github.com/openai/codex/blob/f201c30c52a35f819262865a53df94b6f4ea7a50/sdk/typescript/src/exec.ts#L69-L84

///|
/// Runtime difference: MoonBit packages have no Node optional-package resolution context, so an omitted override resolves `codex` through PATH.
fn Exec::Exec(
  executable_path : @path.Path?,
  env : Map[String, String]?,
  config : ConfigObject?,
) -> Exec {
  {
    // Runtime default-resolution difference: the upstream implementation resolves its npm platform package and bundled executable, whereas MoonBit resolves `codex` through the process PATH when no override is supplied.
    // Upstream: https://github.com/openai/codex/blob/f201c30c52a35f819262865a53df94b6f4ea7a50/sdk/typescript/src/exec.ts#L69-L81 and https://github.com/openai/codex/blob/f201c30c52a35f819262865a53df94b6f4ea7a50/sdk/typescript/src/exec.ts#L334-L410
    executable_path: executable_path.unwrap_or(@path.Path("codex")),
    env_override: env,
    config,
  }
}

///| Runs the CLI with the same argument ordering, environment behavior, and process lifecycle as the upstream generator.

///| Upstream: https://github.com/openai/codex/blob/f201c30c52a35f819262865a53df94b6f4ea7a50/sdk/typescript/src/exec.ts#L86-L243

///| Async type difference: MoonBit has no AsyncGenerator, so each decoded event is delivered to an async callback returning whether iteration should continue.

///|
/// Cancellation type difference: cancelling the owning MoonBit task replaces the upstream AbortSignal.
async fn Exec::run(
  self : Exec,
  args : ExecArgs,
  on_event : async (ThreadEvent) -> Bool,
) -> Unit {
  let command_args = ["exec", "--experimental-json"]
  if self.config is Some(config) {
    for item in serialize_config_overrides_(config) {
      command_args.push("--config")
      command_args.push(item)
    }
  }
  if args.base_url is Some(base_url) {
    command_args.push("--config")
    command_args.push(
      "openai_base_url=\{to_toml_value_(Json::string(base_url), "openai_base_url")}",
    )
  }
  if args.model is Some(model) {
    command_args.push("--model")
    command_args.push(model)
  }
  if args.sandbox_mode is Some(mode) {
    command_args.push("--sandbox")
    command_args.push(mode.to_string())
  }
  if args.working_directory is Some(directory) {
    command_args.push("--cd")
    command_args.push("\{directory}")
  }
  for directory in args.additional_directories {
    command_args.push("--add-dir")
    command_args.push("\{directory}")
  }
  if args.skip_git_repo_check == Some(true) {
    command_args.push("--skip-git-repo-check")
  }
  if args.output_schema_file is Some(schema_path) {
    command_args.push("--output-schema")
    command_args.push("\{schema_path}")
  }
  if args.model_reasoning_effort is Some(effort) {
    command_args.push("--config")
    command_args.push("model_reasoning_effort=\"\{effort.to_string()}\"")
  }
  if args.network_access_enabled is Some(enabled) {
    command_args.push("--config")
    command_args.push(
      "sandbox_workspace_write.network_access=\{if enabled { "true" } else { "false" }}",
    )
  }
  match (args.web_search_mode, args.web_search_enabled) {
    (Some(mode), _) => {
      command_args.push("--config")
      command_args.push("web_search=\"\{mode.to_string()}\"")
    }
    (None, Some(true)) => {
      command_args.push("--config")
      command_args.push("web_search=\"live\"")
    }
    (None, Some(false)) => {
      command_args.push("--config")
      command_args.push("web_search=\"disabled\"")
    }
    (None, None) => ()
  }
  if args.approval_policy is Some(policy) {
    command_args.push("--config")
    command_args.push("approval_policy=\"\{policy.to_string()}\"")
  }
  if args.thread_id is Some(thread_id) {
    command_args.push("resume")
    command_args.push(thread_id)
  }
  for image in args.images {
    command_args.push("--image")
    command_args.push("\{image}")
  }

  let environment = match self.env_override {
    Some(environment_override) => @copy.Copy::copy(environment_override)
    None => Map([])
  }
  let inherited_originator = if self.env_override is None {
    @env.get_env_var(ORIGINATOR_ENV_)
  } else {
    None
  }
  if environment.get(ORIGINATOR_ENV_) is None && inherited_originator is None {
    environment.set(ORIGINATOR_ENV_, MOONBIT_SDK_ORIGINATOR_)
  }
  if args.api_key is Some(value) {
    environment.set("CODEX_API_KEY", value)
  }
  // Runtime PATH difference: the upstream implementation prepends its bundled `pathDirs` here; MoonBit performs no equivalent mutation because it neither resolves nor stores npm-bundled directories.
  // Upstream: https://github.com/openai/codex/blob/f201c30c52a35f819262865a53df94b6f4ea7a50/sdk/typescript/src/exec.ts#L177-L179
  let inherit_environment = self.env_override is None

  let result = @agent_cli.run(
    @agent_cli.Invocation::new(
      command=self.executable_path.to_string(),
      arguments=command_args,
      environment~,
      inherit_environment~,
      input=args.input,
    ),
    on_event,
  ) catch {
    @agent_cli.AgentCliError::InvalidJson(line~, cause~) =>
      raise InvalidEvent(
        message="Failed to parse item: \{line}; cause: \{cause}",
      )
    error => raise error
  }
  match result {
    @agent_cli.RunResult::Completed | @agent_cli.RunResult::Stopped => ()
    @agent_cli.RunResult::Failed(code~, stderr~) =>
      raise ExecFailed(code~, stderr~)
  }
}