///|
pub async fn emit_moonbit_decl(
  file_path : String,
  output_path : String?,
) -> Bool {
  let emitted = emit_moonbit_decl_text(file_path) catch {
    @bridge.ModuleGraphError::ReadError(msg)
    | @bridge.ModuleGraphError::ParseError(msg)
    | @bridge.ModuleGraphError::ResolveError(msg) => {
      println("Emit error: \{cli_clean_error(msg)}")
      return false
    }
  }
  match output_path {
    Some(path) => {
      let _ = @fs.write_file(path, string_to_bytes(emitted), create=0o644) catch {
        e => {
          println("Write error: \{e}")
          return false
        }
      }
      println("Wrote MoonBit declarations to \{path}")
    }
    None => println(emitted)
  }
  true
}

///|
async fn emit_moonbit_decl_text(
  file_path : String,
) -> String raise @bridge.ModuleGraphError {
  @bridge.emit_moonbit_decl_from_entry_path(file_path)
}

///|
pub async fn emit_typescript_decl(
  file_path : String,
  output_path : String?,
) -> Bool {
  let emitted = emit_typescript_decl_text(file_path) catch {
    @bridge.ModuleGraphError::ReadError(msg)
    | @bridge.ModuleGraphError::ParseError(msg)
    | @bridge.ModuleGraphError::ResolveError(msg) => {
      println("Emit error: \{cli_clean_error(msg)}")
      return false
    }
  }
  match output_path {
    Some(path) => {
      let _ = @fs.write_file(path, string_to_bytes(emitted), create=0o644) catch {
        e => {
          println("Write error: \{e}")
          return false
        }
      }
      println("Wrote normalized TypeScript declarations to \{path}")
    }
    None => println(emitted)
  }
  true
}

///|
async fn emit_typescript_decl_text(
  file_path : String,
) -> String raise @bridge.ModuleGraphError {
  @bridge.normalize_moonbit_generated_typescript_decl_from_entry_path(file_path)
}

///|
pub async fn emit_typescript_decl_from_mbti(
  file_path : String,
  output_path : String?,
) -> Bool {
  let emitted = emit_typescript_decl_from_mbti_text(file_path) catch {
    @bridge.MbtiTypescriptDeclError::ReadError(msg)
    | @bridge.MbtiTypescriptDeclError::ParseError(msg) => {
      println("Emit error: \{cli_clean_error(msg)}")
      return false
    }
  }
  match output_path {
    Some(path) => {
      let _ = @fs.write_file(path, string_to_bytes(emitted), create=0o644) catch {
        e => {
          println("Write error: \{e}")
          return false
        }
      }
      println("Wrote TypeScript declarations to \{path}")
    }
    None => println(emitted)
  }
  true
}

///|
async fn emit_typescript_decl_from_mbti_text(
  file_path : String,
) -> String raise @bridge.MbtiTypescriptDeclError {
  @bridge.emit_typescript_decl_from_mbti_path(file_path)
}

///|
pub async fn emit_js_link_config_from_mbti(
  file_path : String,
  output_path : String?,
) -> Bool {
  let emitted = emit_js_link_config_from_mbti_text(file_path) catch {
    @bridge.MbtiJsLinkConfigError::ReadError(msg)
    | @bridge.MbtiJsLinkConfigError::ParseError(msg) => {
      println("Emit error: \{cli_clean_error(msg)}")
      return false
    }
  }
  match output_path {
    Some(path) => {
      let _ = @fs.write_file(path, string_to_bytes(emitted), create=0o644) catch {
        e => {
          println("Write error: \{e}")
          return false
        }
      }
      println("Wrote MoonBit JS link config to \{path}")
    }
    None => println(emitted)
  }
  true
}

///|
async fn emit_js_link_config_from_mbti_text(
  file_path : String,
) -> String raise @bridge.MbtiJsLinkConfigError {
  @bridge.emit_moonbit_js_link_config_from_mbti_path(file_path)
}

///|
async fn load_typescript_import_rewrite_map(
  import_rewrite_path : String?,
) -> Map[String, String]? {
  match import_rewrite_path {
    Some(path) => {
      let source = @fs.read_file(path).text() catch {
        e => {
          println(
            "Emit error: failed to read import rewrite map '\{path}': \{e}",
          )
          return None
        }
      }
      match @bridge.parse_typescript_import_rewrite_map_source(source) {
        Some(rewrites) => Some(rewrites)
        None => {
          println(
            "Emit error: invalid import rewrite map '\{path}', expected a JSON object of string-to-string entries",
          )
          None
        }
      }
    }
    None => Some({})
  }
}

///|
fn main_dirname(path : String) -> String {
  match path.rev_find("/") {
    Some(0) => "/"
    Some(idx) => path[:idx].to_string()
    None => "."
  }
}

///|
fn main_join_path(base : String, child : String) -> String {
  if base == "." || base == "" {
    child
  } else if base.has_suffix("/") {
    base + child
  } else {
    base + "/" + child
  }
}

///|
async fn ensure_dir_tree(path : String) -> Unit raise Error {
  if path == "." || path == "" || @fs.exists(path) {
    return
  }
  let parent = main_dirname(path)
  if parent != path {
    ensure_dir_tree(parent)
  }
  if !@fs.exists(path) {
    @fs.mkdir(path, permission=0o755)
  }
}

///|
pub async fn emit_typescript_package_from_mbti(
  file_path : String,
  output_dir : String,
  import_rewrite_path : String?,
) -> Bool {
  let import_rewrites = match
    load_typescript_import_rewrite_map(import_rewrite_path) {
    Some(rewrites) => rewrites
    None => return false
  }
  let files = @bridge.emit_typescript_package_bundle_from_mbti_path_with_import_rewrites(
    file_path, import_rewrites,
  ) catch {
    @bridge.MbtiTypescriptDeclError::ReadError(msg)
    | @bridge.MbtiTypescriptDeclError::ParseError(msg) => {
      println("Emit error: \{cli_clean_error(msg)}")
      return false
    }
  }
  let _ = ensure_dir_tree(output_dir) catch {
    e => {
      println("Write error: \{e}")
      return false
    }
  }
  for file in files {
    let output_path = main_join_path(output_dir, file.relative_path)
    let parent_dir = main_dirname(output_path)
    let _ = ensure_dir_tree(parent_dir) catch {
      e => {
        println("Write error: \{e}")
        return false
      }
    }
    let _ = @fs.write_file(
      output_path,
      string_to_bytes(file.content),
      create=0o644,
    ) catch {
      e => {
        println("Write error: \{e}")
        return false
      }
    }
  }
  println("Wrote TypeScript declaration package to \{output_dir}")
  true
}

///|
priv struct MoonbitJsBuildContext {
  module_root : String
  glue_dir_name : String
  glue_dir_path : String
  build_arg : String
  build_output_dir : String
}

///|
fn parse_moon_mod_string_field(source : String, field_name : String) -> String? {
  let json = @json.parse(source) catch { _ => return None }
  guard json is Object(members) else { return None }
  for pair in members {
    let (name, value) = pair
    if name == field_name {
      guard value is String(text) else { return None }
      return Some(text)
    }
  }
  None
}

///|
fn sanitize_glue_dir_part(source : String) -> String {
  let mut rendered = ""
  for c in source {
    if (c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9') {
      rendered += c.to_string()
    } else if !rendered.has_suffix("_") {
      rendered += "_"
    }
  }
  let trimmed = rendered.trim(chars="_").to_string()
  if trimmed == "" {
    "package"
  } else {
    trimmed
  }
}

///|
fn hash_glue_dir_part(source : String) -> String {
  let mut hash = 5381
  for c in source {
    hash = (hash * 33 + c.to_int()) % 1000003
  }
  hash.to_string()
}

///|
async fn find_nearest_moon_mod_dir(start_dir : String) -> String? {
  let mut current = start_dir
  while true {
    let moon_mod = main_join_path(current, "moon.mod.json")
    if @fs.exists(moon_mod) {
      return Some(current)
    }
    let parent = main_dirname(current)
    if parent == current {
      return None
    }
    current = parent
  }
  None
}

///|
fn strip_dir_prefix(root : String, path : String) -> String? {
  if path == root {
    return Some("")
  }
  let prefix = if root.has_suffix("/") { root } else { root + "/" }
  if path.has_prefix(prefix) {
    Some(path[prefix.length():path.length()].to_string())
  } else {
    None
  }
}

///|
async fn resolve_moonbit_js_build_context(
  mbti_path : String,
  output_dir : String,
) -> MoonbitJsBuildContext? {
  let source = @fs.read_file(mbti_path).text() catch { _ => return None }
  let package_name = match parse_mbti_package_name_from_source(source) {
    Some(name) => name
    None => return None
  }
  let mbti_realpath = @fs.realpath(mbti_path) catch { _ => mbti_path }
  let package_dir = main_dirname(mbti_realpath)
  let module_root = match find_nearest_moon_mod_dir(package_dir) {
    Some(root) => @fs.realpath(root) catch { _ => root }
    None => return None
  }
  let moon_mod_path = main_join_path(module_root, "moon.mod.json")
  let moon_mod_source = @fs.read_file(moon_mod_path).text() catch {
    _ => return None
  }
  let source_root_rel = match
    parse_moon_mod_string_field(moon_mod_source, "source") {
    Some(source) => source
    None => "."
  }
  let source_root = if source_root_rel == "." || source_root_rel == "" {
    module_root
  } else {
    main_join_path(module_root, source_root_rel)
  }
  let source_root_realpath = @fs.realpath(source_root) catch {
    _ => source_root
  }
  match strip_dir_prefix(source_root_realpath, package_dir) {
    Some(_) => ()
    None => return None
  }
  let glue_dir_name = "__tsmbt_glue__" +
    sanitize_glue_dir_part(package_name) +
    "__" +
    hash_glue_dir_part(output_dir)
  let glue_dir_path = main_join_path(source_root_realpath, glue_dir_name)
  let build_arg = if source_root_rel == "." || source_root_rel == "" {
    glue_dir_name
  } else {
    main_join_path(source_root_rel, glue_dir_name)
  }
  let build_output_dir = main_join_path(
    main_join_path(
      main_join_path(
        main_join_path(main_join_path(module_root, "_build"), "js"),
        "debug",
      ),
      "build",
    ),
    glue_dir_name,
  )
  Some({
    module_root,
    glue_dir_name,
    glue_dir_path,
    build_arg,
    build_output_dir,
  })
}

///|
async fn write_text_file(path : String, content : String) -> Bool {
  let parent = main_dirname(path)
  let _ = ensure_dir_tree(parent) catch {
    e => {
      println("Write error: \{e}")
      return false
    }
  }
  let _ = @fs.write_file(path, string_to_bytes(content), create=0o644) catch {
    e => {
      println("Write error: \{e}")
      return false
    }
  }
  true
}

///|
async fn copy_text_file_with_rewrite(
  from : String,
  to : String,
  old_text : String,
  new_text : String,
) -> Bool {
  let source = @fs.read_file(from).text() catch {
    e => {
      println("Build output error: failed to read \{from}: \{e}")
      return false
    }
  }
  write_text_file(
    to,
    rewrite_moonbit_js_runtime_text(source, old_text, new_text),
  )
}

///|
fn rewrite_moonbit_js_runtime_text(
  source : String,
  old_map_name : String,
  new_map_name : String,
) -> String {
  let with_map_name = source.replace_all(old=old_map_name, new=new_map_name)
  let rewritten = with_map_name.replace_all(
    old="= %identity;",
    new="= (x) => x;",
  )
  if rewritten.contains("require(") &&
    !rewritten.contains("__tsmbtCreateRequire") {
    "import { createRequire as __tsmbtCreateRequire } from \"node:module\";\nconst require = __tsmbtCreateRequire(import.meta.url);\n" +
    rewritten
  } else {
    rewritten
  }
}

///|
async fn copy_binary_file(from : String, to : String) -> Bool {
  let bytes = @fs.read_file(from) catch {
    e => {
      println("Build output error: failed to read \{from}: \{e}")
      return false
    }
  }
  let parent = main_dirname(to)
  let _ = ensure_dir_tree(parent) catch {
    e => {
      println("Write error: \{e}")
      return false
    }
  }
  let _ = @fs.write_file(to, bytes, create=0o644) catch {
    e => {
      println("Write error: \{e}")
      return false
    }
  }
  true
}

///|
async fn remove_moonbit_js_glue_dir(path : String) -> Unit {
  let _ = @fs.rmdir(path, recursive=true) catch { _ => () }
}

///|
fn collect_async_js_export_modes_from_glue(
  glue_mbt : String,
) -> Map[String, Bool] {
  let modes : Map[String, Bool] = {}
  for line_view in glue_mbt.split("\n") {
    let trimmed = line_view.trim().to_string()
    if !trimmed.has_prefix("pub async fn ") {
      continue
    }
    let rest = trimmed["pub async fn ".length():trimmed.length()]
      .trim()
      .to_string()
    match rest.find("(") {
      Some(open_idx) => {
        let name = rest[:open_idx].trim().to_string()
        if name != "" {
          modes[name] = trimmed.contains(" raise")
        }
      }
      None => ()
    }
  }
  modes
}

///|
fn render_async_js_export_wrappers(
  async_exports : Array[(String, String, Bool)],
) -> Array[String] {
  if async_exports.length() == 0 {
    return []
  }
  let lines : Array[String] = []
  lines.push(
    "function __tsmbt_async_finish(value, resolve, reject, preserveResult) {",
  )
  lines.push(
    "  if (!preserveResult && value && typeof value === \"object\" && \"$tag\" in value && \"_0\" in value) {",
  )
  lines.push(
    "    if (value.$tag === 1) { resolve(value._0); } else { reject(value._0); }",
  )
  lines.push("  } else {")
  lines.push("    resolve(value);")
  lines.push("  }")
  lines.push("}")
  lines.push(
    "function __tsmbt_async_result_to_promise(start, preserveResult) {",
  )
  lines.push("  return new Promise((resolve, reject) => {")
  lines.push("    let settled = false;")
  lines.push(
    "    const ok = (value) => { if (!settled) { settled = true; __tsmbt_async_finish(value, resolve, reject, preserveResult); } };",
  )
  lines.push(
    "    const err = (error) => { if (!settled) { settled = true; reject(error); } };",
  )
  lines.push("    try {")
  lines.push("      const value = start(ok, err);")
  lines.push("      if (value !== undefined) { ok(value); }")
  lines.push("    } catch (error) {")
  lines.push("      err(error);")
  lines.push("    }")
  lines.push("  });")
  lines.push("}")
  for item in async_exports {
    let (local_name, export_name, preserve_result) = item
    let preserve_result_js = if preserve_result { "true" } else { "false" }
    lines.push(
      "export function \{export_name}(...args) { const callArgs = args.slice(); while (callArgs.length < \{local_name}.length - 2) { callArgs.push(undefined); } return __tsmbt_async_result_to_promise((ok, err) => \{local_name}(...callArgs, ok, err), \{preserve_result_js}); }",
    )
  }
  lines
}

///|
fn rewrite_async_js_exports(
  source : String,
  async_modes : Map[String, Bool],
) -> String {
  if async_modes.length() == 0 {
    return source
  }
  let output_lines : Array[String] = []
  let async_exports : Array[(String, String, Bool)] = []
  let mut inserted_wrappers = false
  for line_view in source.split("\n") {
    let line = line_view.to_string()
    let trimmed = line.trim().to_string()
    if trimmed.has_prefix("export {") && trimmed.contains(" as ") {
      match (line.find("{"), line.rev_find("}")) {
        (Some(open_idx), Some(close_idx)) if close_idx > open_idx => {
          let inner = line[open_idx + 1:close_idx].to_string()
          let kept_specs : Array[String] = []
          for raw_spec in inner.split(",") {
            let spec = raw_spec.trim().to_string()
            if spec == "" {
              continue
            }
            match spec.find(" as ") {
              Some(as_idx) => {
                let local_name = spec[:as_idx].trim().to_string()
                let export_name = spec[as_idx + " as ".length():spec.length()]
                  .trim()
                  .to_string()
                match async_modes.get(export_name) {
                  Some(preserve_result) =>
                    async_exports.push(
                      (local_name, export_name, preserve_result),
                    )
                  None => kept_specs.push(spec)
                }
              }
              None => kept_specs.push(spec)
            }
          }
          if kept_specs.length() > 0 {
            output_lines.push("export { " + kept_specs.join(", ") + " }")
          }
          continue
        }
        _ => ()
      }
    }
    if !inserted_wrappers && trimmed.has_prefix("//# sourceMappingURL=") {
      for wrapper_line in render_async_js_export_wrappers(async_exports) {
        output_lines.push(wrapper_line)
      }
      inserted_wrappers = true
    }
    output_lines.push(line)
  }
  if !inserted_wrappers {
    for wrapper_line in render_async_js_export_wrappers(async_exports) {
      output_lines.push(wrapper_line)
    }
  }
  output_lines.join("\n")
}

///|
async fn rewrite_async_js_exports_file(
  path : String,
  async_modes : Map[String, Bool],
) -> Bool {
  if async_modes.length() == 0 {
    return true
  }
  let source = @fs.read_file(path).text() catch {
    e => {
      println("Read error: \{e}")
      return false
    }
  }
  write_text_file(path, rewrite_async_js_exports(source, async_modes))
}

///|
async fn build_moonbit_js_runtime_from_mbti(
  mbti_path : String,
  output_dir : String,
  bundle : @bridge.MbtiTypescriptScaffoldBundle,
) -> Bool {
  if bundle.autolink_glue_mbt == "" {
    println("Build error: no MoonBit JS glue exports were generated")
    return false
  }
  let context = match resolve_moonbit_js_build_context(mbti_path, output_dir) {
    Some(context) => context
    None => {
      println(
        "Build error: could not locate a MoonBit source package for \{mbti_path}; pass a package with real source, not only a pkg.generated.mbti fixture.",
      )
      return false
    }
  }
  remove_moonbit_js_glue_dir(context.glue_dir_path)
  let _ = ensure_dir_tree(context.glue_dir_path) catch {
    e => {
      println("Write error: \{e}")
      remove_moonbit_js_glue_dir(context.glue_dir_path)
      return false
    }
  }
  if !write_text_file(
      main_join_path(context.glue_dir_path, "moon.pkg"),
      bundle.moon_pkg,
    ) {
    remove_moonbit_js_glue_dir(context.glue_dir_path)
    return false
  }
  if !write_text_file(
      main_join_path(context.glue_dir_path, "glue.mbt"),
      bundle.autolink_glue_mbt,
    ) {
    remove_moonbit_js_glue_dir(context.glue_dir_path)
    return false
  }
  let (exit_code, output) = @process.collect_output_merged(
    "moon",
    ["build", "--target", "js", context.build_arg],
    cwd=context.module_root,
  )
  if exit_code != 0 {
    println("Build error: moon build --target js failed")
    println(output.text())
    remove_moonbit_js_glue_dir(context.glue_dir_path)
    return false
  }
  let built_js_name = context.glue_dir_name + ".js"
  let built_js_path = main_join_path(context.build_output_dir, built_js_name)
  let built_map_path = built_js_path + ".map"
  let output_js_path = main_join_path(output_dir, "index.js")
  let output_map_path = main_join_path(output_dir, "index.js.map")
  if !copy_text_file_with_rewrite(
      built_js_path,
      output_js_path,
      built_js_name + ".map",
      "index.js.map",
    ) {
    remove_moonbit_js_glue_dir(context.glue_dir_path)
    return false
  }
  if !rewrite_async_js_exports_file(
      output_js_path,
      collect_async_js_export_modes_from_glue(bundle.autolink_glue_mbt),
    ) {
    remove_moonbit_js_glue_dir(context.glue_dir_path)
    return false
  }
  if @fs.exists(built_map_path) {
    if !copy_binary_file(built_map_path, output_map_path) {
      remove_moonbit_js_glue_dir(context.glue_dir_path)
      return false
    }
  }
  remove_moonbit_js_glue_dir(context.glue_dir_path)
  true
}

///|
async fn write_typescript_scaffold_bundle_files(
  output_dir : String,
  bundle : @bridge.MbtiTypescriptScaffoldBundle,
) -> Bool {
  let _ = ensure_dir_tree(output_dir) catch {
    e => {
      println("Write error: \{e}")
      return false
    }
  }
  if !write_text_file(
      main_join_path(output_dir, "package.json"),
      bundle.package_json,
    ) {
    return false
  }
  if !write_text_file(
      main_join_path(output_dir, "AUTOLINK_DIAGNOSTICS.md"),
      bundle.autolink_diagnostics_md,
    ) {
    return false
  }
  for file in bundle.files {
    if !write_text_file(
        main_join_path(output_dir, file.relative_path),
        file.content,
      ) {
      return false
    }
  }
  true
}

///|
pub async fn emit_typescript_scaffold_from_mbti(
  file_path : String,
  output_dir : String,
  import_rewrite_path : String?,
) -> Bool {
  let import_rewrites = match
    load_typescript_import_rewrite_map(import_rewrite_path) {
    Some(rewrites) => rewrites
    None => return false
  }
  let bundle = @bridge.emit_typescript_scaffold_bundle_from_mbti_path_with_import_rewrites(
    file_path, import_rewrites,
  ) catch {
    @bridge.MbtiTypescriptDeclError::ReadError(msg)
    | @bridge.MbtiTypescriptDeclError::ParseError(msg) => {
      println("Emit error: \{cli_clean_error(msg)}")
      return false
    }
  }
  if !build_moonbit_js_runtime_from_mbti(file_path, output_dir, bundle) {
    return false
  }
  if !write_typescript_scaffold_bundle_files(output_dir, bundle) {
    return false
  }
  println("Wrote TypeScript scaffold to \{output_dir}")
  true
}

///|
pub async fn emit_typescript_facade_scaffold_from_mbti(
  file_path : String,
  output_dir : String,
  import_rewrite_path : String?,
) -> Bool {
  let import_rewrites = match
    load_typescript_import_rewrite_map(import_rewrite_path) {
    Some(rewrites) => rewrites
    None => return false
  }
  let bundle = @bridge.emit_typescript_facade_scaffold_bundle_from_mbti_path_with_import_rewrites(
    file_path, import_rewrites,
  ) catch {
    @bridge.MbtiTypescriptDeclError::ReadError(msg)
    | @bridge.MbtiTypescriptDeclError::ParseError(msg) => {
      println("Emit error: \{cli_clean_error(msg)}")
      return false
    }
  }
  if !build_moonbit_js_runtime_from_mbti(file_path, output_dir, bundle) {
    return false
  }
  if !write_typescript_scaffold_bundle_files(output_dir, bundle) {
    return false
  }
  println("Wrote TypeScript facade scaffold to \{output_dir}")
  true
}

///|
pub async fn emit_moonbit_js_ffi(
  file_path : String,
  module_spec : String,
  ffi_output_path : String?,
  bridge_output_path : String?,
) -> Bool {
  let bundle = emit_moonbit_js_ffi_texts(file_path, module_spec) catch {
    @bridge.ModuleGraphError::ReadError(msg)
    | @bridge.ModuleGraphError::ParseError(msg)
    | @bridge.ModuleGraphError::ResolveError(msg) => {
      println("Emit error: \{cli_clean_error(msg)}")
      return false
    }
  }
  match ffi_output_path {
    Some(path) => {
      let _ = @fs.write_file(
        path,
        string_to_bytes(bundle.ffi_mbt),
        create=0o644,
      ) catch {
        e => {
          println("Write error: \{e}")
          return false
        }
      }
      println("Wrote MoonBit JS FFI stubs to \{path}")
    }
    None => {
      println("=== ffi.mbt ===")
      println(bundle.ffi_mbt)
    }
  }
  match bridge_output_path {
    Some(path) => {
      let _ = @fs.write_file(
        path,
        string_to_bytes(bundle.bridge_js),
        create=0o644,
      ) catch {
        e => {
          println("Write error: \{e}")
          return false
        }
      }
      println("Wrote JS bridge to \{path}")
    }
    None => {
      println("=== bridge.js ===")
      println(bundle.bridge_js)
    }
  }
  true
}

///|
pub async fn emit_moonbit_bridge(
  file_path : String,
  module_spec : String,
  decl_output_path : String?,
  ffi_output_path : String?,
  bridge_output_path : String?,
) -> Bool {
  let bundle = emit_moonbit_bridge_texts(file_path, module_spec) catch {
    @bridge.ModuleGraphError::ReadError(msg)
    | @bridge.ModuleGraphError::ParseError(msg)
    | @bridge.ModuleGraphError::ResolveError(msg) => {
      println("Emit error: \{cli_clean_error(msg)}")
      return false
    }
  }
  match decl_output_path {
    Some(path) => {
      let _ = @fs.write_file(
        path,
        string_to_bytes(bundle.decl_mbt),
        create=0o644,
      ) catch {
        e => {
          println("Write error: \{e}")
          return false
        }
      }
      println("Wrote MoonBit declarations to \{path}")
    }
    None => {
      println("=== bridge.mbti ===")
      println(bundle.decl_mbt)
    }
  }
  match ffi_output_path {
    Some(path) => {
      let _ = @fs.write_file(
        path,
        string_to_bytes(bundle.ffi_mbt),
        create=0o644,
      ) catch {
        e => {
          println("Write error: \{e}")
          return false
        }
      }
      println("Wrote MoonBit JS FFI stubs to \{path}")
    }
    None => {
      println("=== bridge.mbt ===")
      println(bundle.ffi_mbt)
    }
  }
  match bridge_output_path {
    Some(path) => {
      let _ = @fs.write_file(
        path,
        string_to_bytes(bundle.bridge_js),
        create=0o644,
      ) catch {
        e => {
          println("Write error: \{e}")
          return false
        }
      }
      println("Wrote JS bridge to \{path}")
    }
    None => {
      println("=== bridge.js ===")
      println(bundle.bridge_js)
    }
  }
  true
}

///|
pub async fn emit_moonbit_bridge_package(
  file_path : String,
  module_spec : String,
  output_dir : String,
  bare_module_specifier? : String? = None,
) -> Bool {
  let bundle = emit_moonbit_bridge_package_texts(file_path, module_spec) catch {
    @bridge.ModuleGraphError::ReadError(msg)
    | @bridge.ModuleGraphError::ParseError(msg)
    | @bridge.ModuleGraphError::ResolveError(msg) => {
      println("Emit error: \{cli_clean_error(msg)}")
      return false
    }
  }
  if !@fs.exists(output_dir) {
    let _ = ensure_dir_tree(output_dir) catch {
      e => {
        println("Write error: \{e}")
        return false
      }
    }
  }
  // `#module(...)` only accepts npm-style bare specifiers — no relative
  // or absolute filesystem paths. The generated bridge therefore always
  // references its sibling `bridge.js` through a bare specifier under
  // the `@tsmbt-bridge/` scope. Callers can override the slug; the
  // default derives it from the output directory name.
  let resolved_specifier = match bare_module_specifier {
    Some(name) => name
    None => default_bridge_bare_specifier(output_dir)
  }
  let bridge_mbt = rewrite_bridge_package_module_path(
    bundle.bridge_mbt,
    resolved_specifier,
  )
  let package_json = render_bridge_package_json(resolved_specifier)
  let files : Array[(String, String)] = [
    ("moon.pkg", bundle.moon_pkg),
    ("bridge.mbti", bundle.bridge_mbti),
    ("package.json", package_json),
  ]
  for file in bridge_package_mbt_files(bridge_mbt, force_split=false) {
    files.push(file)
  }
  files.push(("bridge.js", bundle.bridge_js))
  // Refresh the sibling `node_modules/@tsmbt-bridge/` symlink so
  // `require("@tsmbt-bridge/")` from generated test code lands at
  // the bridge dir. The vendor flow expects users to also list this
  // bridge as a `file:` dependency; `pnpm install` then replaces our
  // symlink with its own pnpm-managed link, but both states resolve to
  // the same bridge directory.
  if !ensure_bridge_node_modules_link(output_dir, resolved_specifier) {
    return false
  }
  for file in files {
    let (name, content) = file
    let path = join_output_path(output_dir, name)
    let _ = @fs.write_file(path, string_to_bytes(content), create=0o644) catch {
      e => {
        println("Write error: \{e}")
        return false
      }
    }
  }
  println("Wrote MoonBit bridge package to \{output_dir}")
  true
}

///|
fn scaffold_diagnostic_export_name(item : String) -> String {
  match item.find(" (") {
    Some(idx) => item[:idx].to_string()
    None => item
  }
}

///|
fn scaffold_diagnostic_reason(item : String) -> String {
  if item.contains("ambiguous re-export") {
    "ambiguous re-export surface"
  } else {
    "unsupported export surface"
  }
}

///|
fn scaffold_diagnostic_decision(item : String) -> String {
  if item.contains("ambiguous re-export") {
    "widened"
  } else {
    "omitted"
  }
}

///|
fn scaffold_diagnostic_runtime_safety(item : String) -> String {
  if item.contains("ambiguous re-export") {
    "runtime-unsafe; generated bridge stubs abort instead of guessing a runtime binding"
  } else {
    "runtime-safe; the unsupported export is not exposed"
  }
}

///|
fn render_moonbit_scaffold_diagnostics_md(
  unsupported_exports : Array[String],
) -> String {
  let lines = [
    "# Scaffold Diagnostics", "", "The generated MoonBit scaffold is buildable. Unsupported or ambiguous export surfaces are listed below with the decision taken by the generator.",
    "", "## Summary",
  ]
  if unsupported_exports.length() > 0 {
    lines.push("")
    lines.push("| export | decision | reason | runtime safety |")
    lines.push("| --- | --- | --- | --- |")
    for item in unsupported_exports {
      let export_name = scaffold_diagnostic_export_name(item)
      let decision = scaffold_diagnostic_decision(item)
      let reason = scaffold_diagnostic_reason(item)
      let runtime_safety = scaffold_diagnostic_runtime_safety(item)
      lines.push(
        "| `" +
        export_name +
        "` | " +
        decision +
        " | " +
        reason +
        " | " +
        runtime_safety +
        " |",
      )
    }
    lines.push("")
    lines.push("## Runtime Safety")
    lines.push("")
    lines.push(
      "Widened surfaces keep the scaffold buildable, but ambiguous runtime exports are not callable until the source export is made unambiguous.",
    )
    lines.push(
      "Omitted surfaces are intentionally absent from the generated MoonBit API. Bridge-wrapped surfaces are callable through generated `bridge.js` glue when the runtime binding can be resolved.",
    )
    lines.push("")
    lines.push("## Decision Vocabulary")
    lines.push("")
    lines.push(
      "- `widened`: emitted as `JSValue` so dependent code can still build",
    )
    lines.push("- `omitted`: not emitted")
    lines.push("- `bridge-wrapped`: emitted through generated `bridge.js` glue")
    lines.push("")
    lines.push("## Raw Entries")
    lines.push("")
    for item in unsupported_exports {
      lines.push("- " + item)
    }
  } else {
    lines.push("")
    lines.push("No unsupported exports were detected.")
  }
  lines.join("\n")
}

///|
async fn collect_moonbit_ts_scaffold_unsupported_exports(
  file_path : String,
) -> Array[String] raise @bridge.ModuleGraphError {
  @bridge.collect_moonbit_ts_scaffold_unsupported_exports(file_path)
}

///|
pub async fn emit_moonbit_scaffold_from_ts(
  file_path : String,
  module_spec : String,
  output_dir : String,
  write_diagnostics? : Bool = true,
  bare_module_specifier? : String? = None,
) -> Bool {
  let unsupported_exports = collect_moonbit_ts_scaffold_unsupported_exports(
    file_path,
  ) catch {
    @bridge.ModuleGraphError::ReadError(msg)
    | @bridge.ModuleGraphError::ParseError(msg)
    | @bridge.ModuleGraphError::ResolveError(msg) => {
      println("Emit error: \{cli_clean_error(msg)}")
      return false
    }
  }
  if !emit_moonbit_bridge_package(
      file_path,
      module_spec,
      output_dir,
      bare_module_specifier~,
    ) {
    return false
  }
  // The unified `--input/--out` driver emits a richer SCAFFOLD_DIAGNOSTICS.md
  // (with JSValue fallbacks). Skip the inner write when called from there
  // to avoid the duplicate "Wrote scaffold diagnostics" log line and a
  // shorter file getting overwritten by the unified renderer anyway.
  if !write_diagnostics {
    return true
  }
  // Always emit SCAFFOLD_DIAGNOSTICS.md so direct subcommand callers can
  // inspect even the happy-path "no unsupported exports" report.
  let diagnostics_path = join_output_path(output_dir, "SCAFFOLD_DIAGNOSTICS.md")
  let diagnostics_md = render_moonbit_scaffold_diagnostics_md(
    unsupported_exports,
  )
  let _ = @fs.write_file(
    diagnostics_path,
    string_to_bytes(diagnostics_md),
    create=0o644,
  ) catch {
    e => {
      println("Write error: \{e}")
      return false
    }
  }
  println("Wrote scaffold diagnostics to \{diagnostics_path}")
  true
}

///|
fn escape_moonbit_module_path(path : String) -> String {
  let mut escaped = ""
  for c in path {
    if c == '\\' {
      escaped += "\\\\"
    } else if c == '"' {
      escaped += "\\\""
    } else {
      escaped += c.to_string()
    }
  }
  escaped
}

///|
fn bridge_binding_needs_generated_module(line : String) -> Bool {
  line.contains("extern \"js\" fn ") && line.contains(" = \"__ts_mbt_")
}

///|
/// Strip the leading `@/` prefix from a bare specifier,
/// returning `(scope, name)`. Returns `None` when the input doesn't
/// look like an npm scope specifier (e.g. unscoped names or
/// `#`-prefix Node imports specifiers).
fn split_scoped_specifier(specifier : String) -> (String, String)? {
  if !specifier.has_prefix("@") {
    return None
  }
  match specifier.find("/") {
    None => None
    Some(idx) => {
      let scope = specifier[:idx].to_string()
      let name = specifier[idx + 1:specifier.length()].to_string()
      Some((scope, name))
    }
  }
}

///|
/// Resolve the consumer's moon module root by walking up from the
/// generated bridge directory. Returns `None` when the bridge isn't
/// inside any moon module.
async fn resolve_consumer_moon_module_root(
  bridge_output_dir : String,
) -> String? {
  let realpath = @fs.realpath(bridge_output_dir) catch {
    _ => bridge_output_dir
  }
  let parent = main_dirname(realpath)
  find_nearest_moon_mod_dir(parent)
}

///|
/// Create / refresh `/node_modules/@/` pointing
/// at the bridge dir so node's `require()`/`import` for the bare
/// specifier resolves at build time. Skips silently for `#`-prefix
/// specifiers (they don't need node_modules at all) and for bridges
/// not under a moon module (we can't infer where to write).
///
/// The link target is computed relative to the consumer module root
/// so the bridge stays portable when the moon module is moved.
async fn ensure_bridge_node_modules_link(
  bridge_output_dir : String,
  bare_specifier : String,
) -> Bool {
  let (scope, name) = match split_scoped_specifier(bare_specifier) {
    Some(parts) => parts
    None => return true
  }
  let module_root = match resolve_consumer_moon_module_root(bridge_output_dir) {
    Some(root) => root
    None => return true
  }
  let scope_dir = main_join_path(
    main_join_path(module_root, "node_modules"),
    scope,
  )
  let _ = ensure_dir_tree(scope_dir) catch {
    e => {
      println("vendor: failed to create \{scope_dir}: \{e}")
      return false
    }
  }
  let link_path = main_join_path(scope_dir, name)
  let _ = @fs.remove(link_path) catch { _ => () }
  let bridge_real = @fs.realpath(bridge_output_dir) catch {
    _ => bridge_output_dir
  }
  let module_real = @fs.realpath(module_root) catch { _ => module_root }
  let bridge_rel_to_module = match strip_dir_prefix(module_real, bridge_real) {
    Some(rest) => rest
    None => bridge_real
  }
  let link_rel_to_module = "node_modules/" + scope + "/" + name
  let target = relative_path_between(link_rel_to_module, bridge_rel_to_module)
  let _ = @fs.symlink(link_path, target~) catch {
    e => {
      println("vendor: failed to symlink \{link_path} -> \{target}: \{e}")
      return false
    }
  }
  true
}

///|
/// POSIX relative path from `from_rel` to `to_rel` where both are
/// paths relative to the same anchor. Each `..` segment moves up
/// one directory in `from_rel`'s parents before descending into
/// `to_rel`. Used for portable symlink targets.
fn relative_path_between(from_rel : String, to_rel : String) -> String {
  let from_parts = split_path_segments(from_rel)
  let to_parts = split_path_segments(to_rel)
  let from_dir = from_parts[:from_parts.length() - 1].to_array()
  let mut common = 0
  let limit = if from_dir.length() < to_parts.length() {
    from_dir.length()
  } else {
    to_parts.length()
  }
  for i in 0.. Array[String] {
  let segments : Array[String] = []
  for chunk in path.split("/") {
    let s = chunk.to_string()
    if s != "" && s != "." {
      segments.push(s)
    }
  }
  segments
}

///|
/// Derive a default `@tsmbt-bridge/` bare specifier from the
/// output directory path. The basename is sanitized so it survives as
/// an npm package-name segment (`[a-z0-9_-]`).
///
/// Both scaffold and vendor flows use this scoped name. The bridge is
/// installed under `node_modules/@tsmbt-bridge/` either via a
/// sibling symlink we refresh on every run or via a `file:` dep the
/// consumer adds to their `package.json`; both paths resolve through
/// standard `node_modules` lookup so `moon test --target js` can
/// `require("@tsmbt-bridge/")` from any depth.
fn default_bridge_bare_specifier(output_dir : String) -> String {
  let basename = match output_dir.rev_find("/") {
    Some(idx) => output_dir[idx + 1:output_dir.length()].to_string()
    None => output_dir
  }
  let mut slug = ""
  for c in basename {
    if (c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9') ||
      c == '-' ||
      c == '_' {
      slug += c.to_string()
    } else {
      slug += "_"
    }
  }
  if slug == "" {
    slug = "bridge"
  }
  "@tsmbt-bridge/" + slug
}

///|
/// Render the `package.json` co-located with `bridge.{mbti,mbt,js}`.
/// Always declares `"type": "module"` so node treats `bridge.js` as
/// ESM regardless of the consumer's outer `package.json`.
///
/// `bare_specifier` is a scoped npm name (`@tsmbt-bridge/`); the
/// emitted `name` field lets consumer-side
/// `"@tsmbt-bridge/": "file:..."` deps resolve through standard
/// `node_modules/@/` lookup.
fn render_bridge_package_json(bare_specifier : String) -> String {
  "{\n  \"name\": \"" +
  ffi_escape_json_string(bare_specifier) +
  "\",\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"main\": \"bridge.js\",\n  \"private\": true\n}\n"
}

///|
/// Minimal JSON string escaper for the small subset of characters
/// that appear in a `bare_specifier` we generated ourselves
/// (`[#@a-zA-Z0-9-_/]`). Backslash and quote pass-through suffices;
/// we never embed control characters.
fn ffi_escape_json_string(s : String) -> String {
  let mut out = ""
  for c in s {
    if c == '"' {
      out += "\\\""
    } else if c == '\\' {
      out += "\\\\"
    } else {
      out += c.to_string()
    }
  }
  out
}

///|
fn rewrite_bridge_package_module_path(
  bridge_mbt : String,
  bridge_js_path : String,
) -> String {
  let rewritten : Array[String] = []
  let module_decl = "#module(\"" +
    escape_moonbit_module_path(bridge_js_path) +
    "\")"
  for line_view in bridge_mbt.split("\n") {
    let line = line_view.to_string()
    if bridge_binding_needs_generated_module(line) {
      rewritten.push(module_decl)
    }
    rewritten.push(line)
  }
  rewritten.join("\n")
}

///|
let bridge_package_split_line_threshold : Int = 2000

///|
fn bridge_package_mbt_line_count(source : String) -> Int {
  if source == "" {
    0
  } else {
    let mut count = 0
    for _ in source.split("\n") {
      count += 1
    }
    count
  }
}

///|
fn bridge_package_split_top_level_blocks(source : String) -> Array[String] {
  let blocks : Array[String] = []
  let mut current : Array[String] = []
  for line_view in source.split("\n") {
    let line = line_view.to_string()
    if line.trim() == "" {
      if current.length() > 0 {
        blocks.push(current.join("\n"))
        current = []
      }
    } else {
      current.push(line)
    }
  }
  if current.length() > 0 {
    blocks.push(current.join("\n"))
  }
  blocks
}

///|
fn bridge_package_is_type_block(block : String) -> Bool {
  let trimmed = block.trim()
  trimmed.has_prefix("pub type ") ||
  trimmed.contains("#external\ntype ") ||
  trimmed.contains("#external\npub type ") ||
  trimmed.contains("\npub(all) enum ") ||
  trimmed.has_prefix("pub(all) enum ") ||
  trimmed.contains("\npub(all) struct ") ||
  trimmed.has_prefix("pub(all) struct ") ||
  trimmed.contains("Unsupported export ")
}

///|
fn bridge_package_is_converter_block(block : String) -> Bool {
  let trimmed = block.trim()
  trimmed.has_prefix("fn __ts_mbt_") ||
  (
    trimmed.contains("extern \"js\" fn __ts_mbt_") &&
    (trimmed.contains("_to_js") || trimmed.contains("_from_js"))
  )
}

///|
fn bridge_package_is_guard_block(block : String) -> Bool {
  block.contains("unsafeCast") ||
  (block.contains("::as") && block.contains(" -> ") && block.contains("?"))
}

///|
fn bridge_package_review_file_content(
  section_name : String,
  blocks : Array[String],
) -> String {
  if blocks.length() == 0 {
    "///|\n/// Generated bridge \{section_name} section is empty."
  } else {
    blocks.join("\n\n")
  }
}

///|
fn bridge_package_mbt_files(
  bridge_mbt : String,
  force_split~ : Bool,
) -> Array[(String, String)] {
  if !force_split &&
    bridge_package_mbt_line_count(bridge_mbt) <=
    bridge_package_split_line_threshold {
    return [("bridge.mbt", bridge_mbt)]
  }
  let type_blocks : Array[String] = []
  let converter_blocks : Array[String] = []
  let extern_blocks : Array[String] = []
  let guard_blocks : Array[String] = []
  let bridge_blocks : Array[String] = []
  for block in bridge_package_split_top_level_blocks(bridge_mbt) {
    if bridge_package_is_type_block(block) {
      type_blocks.push(block)
    } else if bridge_package_is_converter_block(block) {
      converter_blocks.push(block)
    } else if bridge_package_is_guard_block(block) {
      guard_blocks.push(block)
    } else if block.contains("extern \"js\" fn ") {
      extern_blocks.push(block)
    } else {
      bridge_blocks.push(block)
    }
  }
  [
    ("types.mbt", bridge_package_review_file_content("types", type_blocks)),
    (
      "converters.mbt",
      bridge_package_review_file_content("converters", converter_blocks),
    ),
    (
      "externs.mbt",
      bridge_package_review_file_content("externs", extern_blocks),
    ),
    ("guards.mbt", bridge_package_review_file_content("guards", guard_blocks)),
    (
      "bridge.mbt",
      bridge_package_review_file_content("public wrappers", bridge_blocks),
    ),
  ]
}

///|
async fn emit_moonbit_js_ffi_texts(
  file_path : String,
  module_spec : String,
) -> @bridge.MoonBitJsFfiBundle raise @bridge.ModuleGraphError {
  @bridge.emit_moonbit_js_ffi_bundle_from_entry_path(file_path, module_spec)
}

///|
async fn emit_moonbit_bridge_texts(
  file_path : String,
  module_spec : String,
) -> @bridge.MoonBitTsBridgeBundle raise @bridge.ModuleGraphError {
  @bridge.emit_moonbit_ts_bridge_bundle_from_entry_path(file_path, module_spec)
}

///|
async fn emit_moonbit_bridge_package_texts(
  file_path : String,
  module_spec : String,
) -> @bridge.MoonBitTsBridgePackageBundle raise @bridge.ModuleGraphError {
  @bridge.emit_moonbit_ts_bridge_package_bundle_from_entry_path(
    file_path, module_spec,
  )
}

///|
fn join_output_path(base : String, child : String) -> String {
  if base.has_suffix("/") {
    base + child
  } else {
    base + "/" + child
  }
}

///|
fn string_to_bytes(s : String) -> Bytes {
  let arr : Array[Byte] = []
  for i = 0; i < s.length(); i = i + 1 {
    arr.push((s[i].to_int() & 0xFF).to_byte())
  }
  Bytes::from_array(arr)
}