///|
/// WASM sandbox: run WASI modules in an isolated tempdir.
///
/// Instead of granting access to the real workspace via --dir,
/// we materialize only the necessary files into a tempdir,
/// run the WASM module there, and read results back.
/// The module never sees the host filesystem.

///|
pub struct WasmSandbox {
  tempdir : String
  env_path : String
  output_path : String
  path_path : String
  summary_path : String
  state_path : String
}

///|
fn WasmSandbox::to_file_commands(self : WasmSandbox) -> StepFileCommands {
  {
    env_path: self.env_path,
    path_path: self.path_path,
    output_path: self.output_path,
    summary_path: self.summary_path,
    state_path: self.state_path,
    script_path: "",
  }
}

///|
pub struct WasmSandboxResult {
  env_updates : Map[String, String]
  path_entries : Array[String]
  output_values : Map[String, String]
  state_updates : Map[String, String]
  summary : String
}

///|
pub fn create_wasm_sandbox(step_id : String) -> WasmSandbox? {
  let safe_id = sanitize_path_component(step_id)
  let pid = @xsys.get_env_var("PPID").unwrap_or("0")
  let tempdir = "/tmp/actrun-wasm-" + pid + "-" + safe_id
  if @xfs.path_exists(tempdir) {
    ignore(exec_remove_tree(tempdir))
  }
  try @xfs.create_dir(tempdir) catch {
    _ => return None
  } noraise {
    _ => ()
  }
  // Create file command files inside the sandbox
  let env_path = tempdir + "/github_env"
  let output_path = tempdir + "/github_output"
  let path_path = tempdir + "/github_path"
  let summary_path = tempdir + "/step_summary"
  let state_path = tempdir + "/github_state"
  try {
    @xfs.write_string_to_file(env_path, "")
    @xfs.write_string_to_file(output_path, "")
    @xfs.write_string_to_file(path_path, "")
    @xfs.write_string_to_file(summary_path, "")
    @xfs.write_string_to_file(state_path, "")
  } catch {
    _ => return None
  } noraise {
    _ => ()
  }
  Some({ tempdir, env_path, output_path, path_path, summary_path, state_path })
}

///|
fn sanitize_path_component(s : String) -> String {
  let buf = StringBuilder::new()
  for ch in s {
    if (ch >= 'a' && ch <= 'z') ||
      (ch >= 'A' && ch <= 'Z') ||
      (ch >= '0' && ch <= '9') ||
      ch == '-' ||
      ch == '_' {
      buf.write_char(ch)
    } else {
      buf.write_char('_')
    }
  }
  buf.to_string()
}

///|
/// Build wasmtime arguments for sandboxed execution.
/// Only the tempdir is mounted — no access to host filesystem.
pub fn build_sandboxed_wasmtime_args(
  sandbox : WasmSandbox,
  env : Map[String, String],
  module_path : String,
) -> Array[String] {
  let args : Array[String] = ["run"]
  for key, value in env {
    let actual = match key {
      "GITHUB_ENV" => sandbox.env_path
      "GITHUB_OUTPUT" => sandbox.output_path
      "GITHUB_PATH" => sandbox.path_path
      "GITHUB_STEP_SUMMARY" => sandbox.summary_path
      "GITHUB_STATE" => sandbox.state_path
      "GITHUB_WORKSPACE" => sandbox.tempdir
      _ => value
    }
    args.push("--env")
    args.push(key + "=" + actual)
  }
  args.push("--dir")
  args.push(sandbox.tempdir)
  args.push(module_path)
  args
}

///|
fn normalize_wasm_runner_kind(kind : String) -> String? {
  match kind {
    "wasmtime" => Some("wasmtime")
    "deno" => Some("deno")
    "v8" => Some("v8")
    "node" => Some("v8")
    "nodejs" => Some("v8")
    "bun" => Some("v8")
    "js" => Some("v8")
    "javascript" => Some("v8")
    _ => None
  }
}

///|
fn infer_wasm_runner_kind(wasm_bin : String) -> String {
  let bin_name = normalized_exec_name(wasm_bin)
  if bin_name == "deno" {
    "deno"
  } else if is_js_wasi_runtime(bin_name) {
    "v8"
  } else {
    "wasmtime"
  }
}

///|
/// Resolve the WASM runner command.
/// - "wasmtime" (default): use wasmtime-compatible CLI directly
/// - "deno": use deno with wasi-runner.mjs shim
/// - "v8": use node-compatible JS runtime with wasi-runner.mjs shim
/// If runner_kind is empty, infer from the configured binary for backward compatibility.
fn resolve_wasm_runner_command_with_kind(
  runner_kind : String,
  wasm_bin : String,
  wasm_args : Array[String],
  workspace_root : String,
) -> (String, Array[String]) {
  let effective_kind = match normalize_wasm_runner_kind(runner_kind) {
    Some(kind) => kind
    None => infer_wasm_runner_kind(wasm_bin)
  }
  if effective_kind == "deno" || effective_kind == "v8" {
    let shim_path = find_wasi_runner_shim(workspace_root)
    let args : Array[String] = []
    if effective_kind == "deno" {
      args.push("run")
      args.push("--allow-all")
    }
    args.push(shim_path)
    for arg in wasm_args {
      args.push(arg)
    }
    (wasm_bin, args)
  } else {
    (wasm_bin, wasm_args)
  }
}

///|
/// Backward-compatible wrapper that infers runner kind from the configured binary.
pub fn resolve_wasm_runner_command(
  wasm_bin : String,
  wasm_args : Array[String],
  workspace_root : String,
) -> (String, Array[String]) {
  resolve_wasm_runner_command_with_kind("", wasm_bin, wasm_args, workspace_root)
}

///|
fn is_js_wasi_runtime(bin_name : String) -> Bool {
  bin_name == "deno" ||
  bin_name == "node" ||
  bin_name == "nodejs" ||
  bin_name == "bun"
}

///|
fn normalized_exec_name(path : String) -> String {
  let bin_name = basename_of(path)
  if bin_name.has_suffix(".exe") && bin_name.length() > 4 {
    exec_text_slice(bin_name, 0, bin_name.length() - 4)
  } else {
    bin_name
  }
}

///|
fn basename_of(path : String) -> String {
  let slash = '/'.to_int().to_uint16()
  let mut last_slash = -1
  let mut idx = 0
  while idx < path.length() {
    if path.unsafe_get(idx) == slash {
      last_slash = idx
    }
    idx += 1
  }
  if last_slash >= 0 {
    exec_text_slice(path, last_slash + 1, path.length())
  } else {
    path
  }
}

///|
fn wasi_runner_shim_candidates(workspace_root : String) -> Array[String] {
  let candidates = [
    "scripts/wasi-runner.mjs", "node_modules/@mizchi/actrun/scripts/wasi-runner.mjs",
  ]
  if workspace_root.length() > 0 {
    candidates.push(workspace_root + "/scripts/wasi-runner.mjs")
    candidates.push(
      workspace_root + "/node_modules/@mizchi/actrun/scripts/wasi-runner.mjs",
    )
  }
  candidates.push("/usr/local/lib/actrun/wasi-runner.mjs")
  candidates
}

///|
fn find_wasi_runner_shim(workspace_root : String) -> String {
  // Look for wasi-runner.mjs relative to cwd first, then workspace,
  // then installed locations used by packaged/containerized actrun.
  let candidates = wasi_runner_shim_candidates(workspace_root)
  for candidate in candidates {
    if @xfs.path_exists(candidate) {
      return absolute_exec_path(candidate)
    }
  }
  // Fallback: return absolute path to cwd-relative location
  absolute_exec_path("scripts/wasi-runner.mjs")
}

///|
/// Read results from the sandbox after WASM execution.
pub fn read_sandbox_results(sandbox : WasmSandbox) -> WasmSandboxResult {
  {
    env_updates: parse_env_updates(sandbox.env_path),
    path_entries: parse_path_updates(sandbox.path_path),
    output_values: parse_output_values(sandbox.output_path),
    state_updates: parse_state_values(sandbox.state_path),
    summary: parse_summary_text(sandbox.summary_path),
  }
}

///|
/// Clean up the sandbox tempdir after use.
pub fn cleanup_wasm_sandbox(sandbox : WasmSandbox) -> Unit {
  ignore(exec_remove_tree(sandbox.tempdir))
}