///|
/// Vendor a single TypeScript package's types into the consumer's
/// `/internal/generated//` sub-package.
///
/// `pkg_spec` is an npm-style specifier (`hono`, `@types/react`,
/// `@scope/foo`). The resolver walks the consumer's `node_modules` to
/// find the type entrypoint, generates a MoonBit bridge package, and
/// writes it under `/internal/generated//`.
///
/// `module_spec_override` overrides the runtime import the generated
/// `bridge.js` uses. Defaults to `pkg_spec` (or, for `@types/`
/// inputs, the unscoped runtime name).
///
/// `vendor_root_override` overrides the output root. Defaults to
/// `/internal/generated`.
fn types_package_hint(pkg_spec : String) -> String {
  if pkg_spec.has_prefix("@") {
    match pkg_spec.find("/") {
      Some(idx) =>
        "@types/" +
        pkg_spec[1:idx].to_owned() +
        "__" +
        pkg_spec[idx + 1:].to_owned()
      None => "@types/\{pkg_spec}"
    }
  } else {
    "@types/\{pkg_spec}"
  }
}

///|
pub async fn ts2mbt_vendor_package(
  pkg_spec : String,
  module_spec_override? : String? = None,
  vendor_root_override? : String? = None,
  print_import_hint? : Bool = true,
) -> Bool {
  let vendor_root = match vendor_root_override {
    Some(root) => root
    None =>
      match resolve_default_vendor_root() {
        Some(root) => root
        None => {
          println(
            "vendor: could not resolve `/internal/generated`; run inside a moon module or pass --out ",
          )
          return false
        }
      }
  }
  let entry_path = match
    @parser.resolve_type_module_specifier("__tsmbt_entry__.ts", pkg_spec) {
    Some(path) => path
    None => {
      println(
        "vendor: could not resolve TypeScript types for '\{pkg_spec}'; check that it's installed in node_modules",
      )
      return false
    }
  }
  // A scriptish entry means the package ships no declarations at all —
  // the generated surface will be near-empty (unsafeCast only). Say so
  // instead of silently reporting success on a function-less bridge.
  if entry_path.has_suffix(".js") ||
    entry_path.has_suffix(".cjs") ||
    entry_path.has_suffix(".mjs") {
    let types_pkg_hint = types_package_hint(pkg_spec)
    println(
      "vendor: WARNING '\{pkg_spec}' ships no TypeScript declarations (resolved \{entry_path}); the generated API surface will be minimal — try `npm i -D \{types_pkg_hint}` and re-run",
    )
  }
  let runtime_spec = match module_spec_override {
    Some(spec) => spec
    None => default_runtime_module_spec(pkg_spec)
  }
  let safe_dir = vendor_safe_pkg_dir(pkg_spec)
  let output_dir = main_join_path(vendor_root, safe_dir)
  let bare_specifier = "@tsmbt-bridge/\{safe_dir}"
  println(
    "vendor: \{pkg_spec} -> \{output_dir} (#module \"\{bare_specifier}\")",
  )
  // `emit_moonbit_scaffold_from_ts` writes `bridge.{mbti,mbt,js}`,
  // `package.json`, `moon.pkg`. The bare specifier baked into
  // `bridge.mbt` is a scoped npm name (`@tsmbt-bridge/`) that
  // resolves through `node_modules/@tsmbt-bridge/`. Consumers
  // wire it up by adding `"@tsmbt-bridge/": "file:./..."` to
  // their `package.json` `dependencies` — `pnpm install` /
  // `npm install` then create the link as a real dependency, which
  // also lets `moon test --target js` resolve the require() chain
  // (Node's CJS test scaffold writes a sibling empty `package.json`
  // that would otherwise shadow a `package.json#imports` mapping).
  if !emit_moonbit_scaffold_from_ts(
      entry_path,
      runtime_spec,
      output_dir,
      bare_module_specifier=Some(bare_specifier),
    ) {
    return false
  }
  let _ = ensure_vendor_root_guardrails(vendor_root)
  if print_import_hint {
    print_vendor_import_hint(vendor_root, safe_dir)
    print_vendor_package_json_dependency_hint(vendor_root, [safe_dir])
    print_vendor_module_type_hint()
  }
  true
}

///|
/// Drop a `.gitignore` and `AGENTS.md` at the vendor root so AI agents
/// (Claude / Cursor / Aider / etc.) know not to hand-edit the
/// generated files, and so consumers can leave the directory out of
/// version control with a single ignore rule. Both files are
/// idempotent — overwriting on every vendor run keeps them current.
async fn ensure_vendor_root_guardrails(vendor_root : String) -> Bool {
  let _ = ensure_dir_tree(vendor_root) catch { _ => return false }
  let gitignore_body = "# AUTO-GENERATED by `ts2mbt` (mizchi/ts). DO NOT EDIT.\n# Regenerate via `ts2mbt generate` or `ts2mbt vendor `.\n*\n!.gitignore\n!AGENTS.md\n"
  let gitignore_path = main_join_path(vendor_root, ".gitignore")
  let _ = @fs.write_file(
    gitignore_path,
    string_to_bytes(gitignore_body),
    create=0o644,
  ) catch {
    _ => ()
  }
  let agents_body = "# Auto-generated TypeScript bridges\n\nFiles under this directory are generated by `ts2mbt generate` /\n`ts2mbt vendor` from upstream `.d.ts` declarations. Do not edit\nthem by hand -- your changes will be lost on the next regen.\n\nTo regenerate:\n\n```\nts2mbt generate          # everything in package.json (typical)\nts2mbt vendor       # just one package\n```\n\nIf the upstream typings change, re-run the same command. The\nruntime `bridge.js` files are wired up as scoped npm packages\nunder `@tsmbt-bridge/`; `ts2mbt` writes the matching `file:`\nentries into the consumer's `package.json` `dependencies` (or\nprints them when the file can't be edited safely), and\n`pnpm install` / `npm install` then links them into\n`node_modules/@tsmbt-bridge/`.\n"
  let agents_path = main_join_path(vendor_root, "AGENTS.md")
  let _ = @fs.write_file(
    agents_path,
    string_to_bytes(agents_body),
    create=0o644,
  ) catch {
    _ => ()
  }
  true
}

///|
/// Print a copy-paste-ready `moon.pkg` import entry for a freshly
/// vendored package. Resolves the consumer's module name from
/// `moon.mod.json` so the suggestion is concrete, falling back to a
/// `` placeholder when no module is found.
async fn print_vendor_import_hint(
  vendor_root : String,
  safe_dir : String,
) -> Unit {
  let prefix = vendor_import_path_prefix(vendor_root)
  println("")
  println("Add to your consumer moon.pkg import block:")
  println("  \"\{prefix}/\{safe_dir}\" @\{safe_dir},")
}

///|
/// The moon import-path prefix for packages under `vendor_root`:
/// `/`.
/// Falls back to the documented default layout when the module name or
/// the relative position can't be resolved (e.g. `--out` points outside
/// the module's source tree).
async fn vendor_import_path_prefix(vendor_root : String) -> String {
  let module_name = read_consumer_module_name()
  let fallback = "\{module_name}/internal/generated"
  let module_root = match find_nearest_moon_mod_dir(".") {
    Some(root) => root
    None => return fallback
  }
  let mod_source = main_join_path(module_root, "moon.mod.json")
  let mod_text = @fs.read_file(mod_source).text() catch { _ => "" }
  let source_field = match parse_moon_mod_string_field(mod_text, "source") {
    Some(dir) => dir
    None => "src"
  }
  let src_dir = main_join_path(module_root, source_field)
  let src_real = @fs.realpath(src_dir) catch { _ => src_dir }
  let vendor_real = @fs.realpath(vendor_root) catch { _ => vendor_root }
  match strip_dir_prefix(src_real, vendor_real) {
    Some(rel) if rel != "" => "\{module_name}/\{rel}"
    _ => fallback
  }
}

///|
/// Resolve the consumer's moon module name from `moon.mod.json`. Falls
/// back to a `` placeholder so the printed hint still
/// reads as a template even outside a moon module.
async fn read_consumer_module_name() -> String {
  let module_root = match find_nearest_moon_mod_dir(".") {
    Some(root) => root
    None => return ""
  }
  let path = main_join_path(module_root, "moon.mod.json")
  let source = @fs.read_file(path).text() catch { _ => return "" }
  match parse_moon_mod_string_field(source, "name") {
    Some(name) => name
    None => ""
  }
}

///|
/// Print copy-paste-ready `package.json#dependencies` entries for
/// the freshly-vendored bridges. Each entry maps a scoped name
/// (`@tsmbt-bridge/`) to a `file:` reference into the vendor
/// tree; `pnpm install` / `npm install` then materialize the bridge
/// as a real `node_modules/@tsmbt-bridge/` package, surviving
/// future installs. Stays silent on entries that are already wired
/// up so re-running `vendor` / `generate` is quiet on configured
/// projects.
///
/// Missing entries are written into `package.json` in place via a
/// line-oriented insertion into the `dependencies` block, so the rest
/// of the file's formatting / key order stays byte-identical. When the
/// file's shape defeats the inserter, the entries are printed for
/// copy-paste instead — never silently dropped.
async fn print_vendor_package_json_dependency_hint(
  vendor_root : String,
  safe_dirs : Array[String],
) -> Unit {
  if safe_dirs.length() == 0 {
    return
  }
  // Anchor on the moon module when inside one; `generate` / `vendor`
  // is also useful before `moon.mod.json` exists, so fall back to the
  // working directory's `package.json` (the file `generate` read the
  // dependency list from).
  let module_root = match find_nearest_moon_mod_dir(".") {
    Some(root) => root
    None => "."
  }
  let pkg_json_path = main_join_path(module_root, "package.json")
  if !@fs.exists(pkg_json_path) {
    return
  }
  let module_real = @fs.realpath(module_root) catch { _ => module_root }
  let vendor_real = @fs.realpath(vendor_root) catch { _ => vendor_root }
  let rel_to_vendor = match strip_dir_prefix(module_real, vendor_real) {
    Some(rest) => rest
    None => vendor_real
  }
  let source = @fs.read_file(pkg_json_path).text() catch { _ => return }
  let json = @json.parse(source) catch { _ => return }
  let missing : Array[(String, String)] = []
  for safe_dir in safe_dirs {
    let key = "@tsmbt-bridge/\{safe_dir}"
    let target = "file:./\{rel_to_vendor}/\{safe_dir}"
    if !package_json_dependency_entry_matches(json, key, target) {
      missing.push((key, target))
    }
  }
  if missing.length() == 0 {
    return
  }
  // Vendor roots under `_build/` are throwaway scaffolds (example /
  // fixture gates run `vendor` inside the repo); writing those into a
  // real `package.json` pollutes it with paths that vanish on the next
  // clean. Print instead of writing for those.
  if rel_to_vendor.has_prefix("_build/") ||
    rel_to_vendor.contains("/_build/") ||
    rel_to_vendor == "_build" {
    print_package_json_dependency_block(missing)
    return
  }
  match insert_package_json_dependencies(source, missing) {
    Some(updated) => {
      @fs.write_file(pkg_json_path, string_to_bytes(updated)) catch {
        _ => {
          print_package_json_dependency_block(missing)
          return
        }
      }
      println("")
      println("Wired into \{pkg_json_path} (`dependencies`):")
      for entry in missing {
        let (key, target) = entry
        println("  \"\{key}\": \"\{target}\"")
      }
      println(
        "Run `pnpm install` / `npm install` to materialize the links under node_modules/@tsmbt-bridge/.",
      )
    }
    None => print_package_json_dependency_block(missing)
  }
}

///|
/// Copy-paste fallback for when the in-place `package.json` edit can't
/// run (unwritable file, shape the line inserter doesn't understand).
fn print_package_json_dependency_block(
  missing : Array[(String, String)],
) -> Unit {
  println("")
  println("Add to your consumer package.json (`dependencies` field):")
  println("  \"dependencies\": {")
  for idx in 0.. String? {
  let lines : Array[String] = []
  for line_view in source.split("\n") {
    lines.push(line_view.to_owned())
  }
  fn leading_whitespace(line : String) -> String {
    let mut idx = 0
    while idx < line.length() && (line[idx] == ' ' || line[idx] == '\t') {
      idx += 1
    }
    line[:idx].to_owned()
  }
  // Locate a multi-line `"dependencies": {` opener.
  for i, line in lines {
    if line.contains("\"dependencies\"") && line.trim().has_suffix("{") {
      let indent = leading_whitespace(line)
      let entry_indent = if i + 1 < lines.length() &&
        lines[i + 1].trim() != "}" &&
        lines[i + 1].trim() != "}," {
        leading_whitespace(lines[i + 1])
      } else {
        indent + "  "
      }
      // An empty block needs the last inserted entry comma-free; a
      // populated block keeps commas on every inserted line because
      // the existing first entry follows.
      let block_is_empty = i + 1 < lines.length() &&
        (lines[i + 1].trim() == "}" || lines[i + 1].trim() == "},")
      let inserted : Array[String] = []
      for idx in 0.. Bool {
  guard json is Object(top) else { return false }
  for pair in top {
    let (name, val) = pair
    if name != "dependencies" && name != "devDependencies" {
      continue
    }
    guard val is Object(deps) else { continue }
    for entry in deps {
      let (k, v) = entry
      if k == key {
        guard v is String(s) else { return false }
        if s == target {
          return true
        }
      }
    }
  }
  false
}

///|
/// Detect a missing `"type": "module"` in the consumer's `package.json`
/// and surface a one-line hint. Generated `bridge.js` is ESM, so Node
/// prints a perf warning ("Reparsing as ES module ...") when the
/// containing package.json doesn't declare `type: module`.
async fn print_vendor_module_type_hint() -> Unit {
  let module_root = match find_nearest_moon_mod_dir(".") {
    Some(root) => root
    None => return
  }
  let pkg_json_path = main_join_path(module_root, "package.json")
  if !@fs.exists(pkg_json_path) {
    return
  }
  let source = @fs.read_file(pkg_json_path).text() catch { _ => return }
  if source.contains("\"type\"") {
    return
  }
  println(
    "  (note) add `\"type\": \"module\"` to package.json to silence Node's ESM-reparse warning",
  )
}

///|
/// Generate MoonBit bridges for every dependency listed in a
/// `package.json` (`dependencies` + `devDependencies`) and write them
/// under `/internal/generated//`. Emits a per-package
/// summary at the end and returns `true` only when every package
/// generated successfully.
///
/// `package_json_path_override` defaults to `./package.json`.
pub async fn ts2mbt_generate_from_package_json(
  package_json_path_override? : String? = None,
  vendor_root_override? : String? = None,
) -> Bool {
  let package_json_path = match package_json_path_override {
    Some(path) => path
    None => "package.json"
  }
  let deps = read_package_json_dependency_names(package_json_path)
  if deps.length() == 0 {
    println(
      "generate: no dependencies / devDependencies found in '\{package_json_path}'",
    )
    return true
  }
  let succeeded : Array[String] = []
  let failed : Array[String] = []
  for pkg_spec in deps {
    if ts2mbt_vendor_package(
        pkg_spec,
        vendor_root_override~,
        print_import_hint=false,
      ) {
      succeeded.push(pkg_spec)
    } else {
      failed.push(pkg_spec)
    }
  }
  println("")
  println(
    "generate summary: \{succeeded.length()} ok, \{failed.length()} failed (of \{deps.length()})",
  )
  if failed.length() > 0 {
    println("failed packages:")
    for pkg in failed {
      println("  - \{pkg}")
    }
  }
  // Print one consolidated import block at the end so the user gets a
  // copy-paste-ready chunk for their `moon.pkg`.
  if succeeded.length() > 0 {
    let vendor_root = match vendor_root_override {
      Some(root) => root
      None =>
        match resolve_default_vendor_root() {
          Some(root) => root
          None => return failed.length() == 0
        }
    }
    print_generate_import_block(vendor_root, succeeded)
    let safe_dirs : Array[String] = []
    for pkg_spec in succeeded {
      safe_dirs.push(vendor_safe_pkg_dir(pkg_spec))
    }
    print_vendor_package_json_dependency_hint(vendor_root, safe_dirs)
    print_vendor_module_type_hint()
  }
  failed.length() == 0
}

///|
/// Print one combined `moon.pkg` import block listing every
/// successfully generated package.
async fn print_generate_import_block(
  vendor_root : String,
  pkg_specs : Array[String],
) -> Unit {
  let prefix = vendor_import_path_prefix(vendor_root)
  println("")
  println("Add to your consumer moon.pkg import block:")
  for pkg_spec in pkg_specs {
    let safe_dir = vendor_safe_pkg_dir(pkg_spec)
    println("  \"\{prefix}/\{safe_dir}\" @\{safe_dir},")
  }
}

///|
/// Sanitize an npm package specifier into a directory-name-safe slug.
/// `@scope/foo-bar` -> `scope__foo_bar`; `react` -> `react`.
fn vendor_safe_pkg_dir(name : String) -> String {
  let mut result = ""
  for c in name {
    if c == '@' {
      // Strip leading scope marker
      ()
    } else if (c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9') {
      result += c.to_string()
    } else if c == '/' {
      result += "__"
    } else {
      result += "_"
    }
  }
  if result == "" {
    "package"
  } else {
    result
  }
}

///|
/// Default runtime module spec for a package vendored from node_modules.
/// `@types/react` -> `react`; everything else passes through.
fn default_runtime_module_spec(pkg_spec : String) -> String {
  let prefix = "@types/"
  if pkg_spec.has_prefix(prefix) {
    pkg_spec[prefix.length():pkg_spec.length()].to_owned()
  } else {
    pkg_spec
  }
}

///|
/// Pure-function variant of `resolve_default_vendor_root` for testing.
/// Given a moon module root and the raw `moon.mod.json` contents,
/// returns the directory the vendor pipeline should write into.
///
/// When `moon.mod.json` does not declare a `source` field MoonBit
/// treats the module root itself as the source directory, so the
/// fallback here is `"."` rather than `"src"`. Picking `"src"` for
/// missing-source projects would emit the bridge under a phantom
/// `/src/internal/generated/...` path that the moon module
/// can't see, and `moon build` would silently skip it.
pub fn compute_vendor_root(
  module_root : String,
  moon_mod_source : String,
) -> String {
  let source_dir_rel = match
    parse_moon_mod_string_field(moon_mod_source, "source") {
    Some(value) => value
    None => "."
  }
  let source_dir = if source_dir_rel == "" || source_dir_rel == "." {
    module_root
  } else {
    main_join_path(module_root, source_dir_rel)
  }
  main_join_path(source_dir, "internal/generated")
}

///|
/// Resolve `/internal/generated` from the cwd's nearest
/// module manifest. Returns `None` if no module is found.
async fn resolve_default_vendor_root() -> String? {
  let module_root = match find_nearest_moon_mod_dir(".") {
    Some(root) => root
    None => return None
  }
  let source = match read_moon_mod_source(module_root) {
    Some(source) => source
    None => return None
  }
  Some(compute_vendor_root(module_root, source))
}

///|
/// Read the names listed under `dependencies` and `devDependencies` in
/// the given `package.json`. Order: deps first, devDeps second, with
/// each block in the order JSON returned. Duplicates are de-duped.
async fn read_package_json_dependency_names(path : String) -> Array[String] {
  let result : Array[String] = []
  let source = @fs.read_file(path).text() catch {
    _ => {
      println("vendor: could not read '\{path}'")
      return result
    }
  }
  let json = @json.parse(source) catch {
    _ => {
      println("vendor: '\{path}' is not valid JSON")
      return result
    }
  }
  guard json is Object(members) else { return result }
  for field in ["dependencies", "devDependencies"] {
    for pair in members {
      let (name, value) = pair
      if name != field {
        continue
      }
      guard value is Object(deps) else { continue }
      for dep_pair in deps {
        let (dep_name, _) = dep_pair
        // Skip bridges already vendored under `@tsmbt-bridge/...`; they
        // are bridge artifacts the consumer wired up via `file:` deps,
        // not upstream npm packages we should re-vendor.
        if dep_name.has_prefix("@tsmbt-bridge/") {
          continue
        }
        if !result.contains(dep_name) {
          result.push(dep_name)
        }
      }
    }
  }
  result
}