///|
priv enum TsmbtDirection {
  Auto
  TsToMbt
  MbtToTs
}

///|
priv struct UnifiedTsmbtOptions {
  input : String?
  out : String?
  direction : TsmbtDirection
  module_spec : String?
  import_rewrite_path : String?
  diagnostics_path : String?
  strict : Bool
  facade : Bool
}

///|
fn parse_tsmbt_direction(value : String) -> TsmbtDirection? {
  match value {
    "auto" => Some(TsmbtDirection::Auto)
    "ts-to-mbt" | "ts2mbt" | "ts" => Some(TsmbtDirection::TsToMbt)
    "mbt-to-ts" | "mbt2ts" | "mbt" => Some(TsmbtDirection::MbtToTs)
    _ => None
  }
}

///|
fn parse_unified_tsmbt_options(
  args : Array[String],
  start : Int,
) -> (UnifiedTsmbtOptions?, String?) {
  let mut input : String? = None
  let mut out : String? = None
  let mut direction = TsmbtDirection::Auto
  let mut module_spec : String? = None
  let mut import_rewrite_path : String? = None
  let mut diagnostics_path : String? = None
  let mut strict = false
  let mut facade = true
  let mut idx = start
  while idx < args.length() {
    let arg = args[idx]
    match arg {
      "--help" | "-h" => return (None, Some("help"))
      "--input" | "-i" => {
        if idx + 1 >= args.length() {
          return (None, Some("missing value for \{arg}"))
        }
        input = Some(args[idx + 1])
        idx += 2
        continue
      }
      "--out" | "-o" => {
        if idx + 1 >= args.length() {
          return (None, Some("missing value for \{arg}"))
        }
        out = Some(args[idx + 1])
        idx += 2
        continue
      }
      "--direction" => {
        if idx + 1 >= args.length() {
          return (None, Some("missing value for --direction"))
        }
        match parse_tsmbt_direction(args[idx + 1]) {
          Some(parsed) => direction = parsed
          None =>
            return (
              None,
              Some(
                "invalid --direction '\{args[idx + 1]}', expected auto, mbt-to-ts, or ts-to-mbt",
              ),
            )
        }
        idx += 2
        continue
      }
      "--module-spec" | "--runtime-module" => {
        if idx + 1 >= args.length() {
          return (None, Some("missing value for \{arg}"))
        }
        module_spec = Some(args[idx + 1])
        idx += 2
        continue
      }
      "--import-rewrites" | "--import-rewrite" => {
        if idx + 1 >= args.length() {
          return (None, Some("missing value for \{arg}"))
        }
        import_rewrite_path = Some(args[idx + 1])
        idx += 2
        continue
      }
      "--diagnostics" => {
        if idx + 1 >= args.length() {
          return (None, Some("missing value for --diagnostics"))
        }
        diagnostics_path = Some(args[idx + 1])
        idx += 2
        continue
      }
      "--strict" => {
        strict = true
        idx += 1
        continue
      }
      "--no-strict" => {
        strict = false
        idx += 1
        continue
      }
      "--facade" => {
        facade = true
        idx += 1
        continue
      }
      "--no-facade" => {
        facade = false
        idx += 1
        continue
      }
      _ => ()
    }
    if arg.has_prefix("--input=") {
      input = Some(arg["--input=".length():arg.length()].to_string())
    } else if arg.has_prefix("--out=") {
      out = Some(arg["--out=".length():arg.length()].to_string())
    } else if arg.has_prefix("--direction=") {
      let value = arg["--direction=".length():arg.length()].to_string()
      match parse_tsmbt_direction(value) {
        Some(parsed) => direction = parsed
        None =>
          return (
            None,
            Some(
              "invalid --direction '\{value}', expected auto, mbt-to-ts, or ts-to-mbt",
            ),
          )
      }
    } else if arg.has_prefix("--module-spec=") {
      module_spec = Some(
        arg["--module-spec=".length():arg.length()].to_string(),
      )
    } else if arg.has_prefix("--runtime-module=") {
      module_spec = Some(
        arg["--runtime-module=".length():arg.length()].to_string(),
      )
    } else if arg.has_prefix("--import-rewrites=") {
      import_rewrite_path = Some(
        arg["--import-rewrites=".length():arg.length()].to_string(),
      )
    } else if arg.has_prefix("--import-rewrite=") {
      import_rewrite_path = Some(
        arg["--import-rewrite=".length():arg.length()].to_string(),
      )
    } else if arg.has_prefix("--diagnostics=") {
      diagnostics_path = Some(
        arg["--diagnostics=".length():arg.length()].to_string(),
      )
    } else if !arg.has_prefix("-") && input is None {
      input = Some(arg)
    } else if !arg.has_prefix("-") && out is None {
      out = Some(arg)
    } else {
      return (None, Some("unknown option: \{arg}"))
    }
    idx += 1
  }
  (
    Some({
      input,
      out,
      direction,
      module_spec,
      import_rewrite_path,
      diagnostics_path,
      strict,
      facade,
    }),
    None,
  )
}

///|
async fn main_file_exists(path : String) -> Bool {
  let kind = @fs.kind(path, follow_symlink=false) catch {
    _ => @fs.FileKind::Unknown
  }
  kind is @fs.FileKind::Regular
}

///|
fn unified_tsmbt_skip_scan_dir(name : String) -> Bool {
  name == ".git" ||
  name == ".mooncakes" ||
  name == "_build" ||
  name == "target" ||
  name == "node_modules" ||
  name == "test262"
}

///|
async fn collect_pkg_generated_mbti_files(
  path : String,
  out : Array[String],
) -> Unit {
  let kind = @fs.kind(path, follow_symlink=false) catch {
    _ => @fs.FileKind::Unknown
  }
  match kind {
    @fs.FileKind::Directory => {
      let entries = @fs.readdir(
        path,
        include_hidden=false,
        include_special=false,
        sort=true,
      ) catch {
        _ => []
      }
      for name in entries {
        if unified_tsmbt_skip_scan_dir(name) {
          continue
        }
        collect_pkg_generated_mbti_files(main_join_path(path, name), out)
      }
    }
    @fs.FileKind::Regular =>
      if path.has_suffix("pkg.generated.mbti") {
        out.push(path)
      }
    _ => ()
  }
}

///|
fn parse_mbti_package_name_from_source(source : String) -> String? {
  for line_view in source.split("\n") {
    let line = line_view.trim().to_string()
    if !line.has_prefix("package \"") {
      continue
    }
    let rest = line["package \"".length():line.length()].to_string()
    match rest.find("\"") {
      Some(end_idx) => return Some(rest[:end_idx].to_string())
      None => ()
    }
  }
  None
}

///|
async fn resolve_unified_mbt_input_path(input : String) -> String? {
  let roots : Array[String] = ["."]
  match infer_ghq_github_root_from_cwd() {
    Some(root) => roots.push(root)
    None => ()
  }
  resolve_unified_mbt_input_path_from_roots(input, roots)
}

///|
fn ghq_candidate_repo_paths(
  github_root : String,
  input : String,
) -> Array[String] {
  let paths = [main_join_path(github_root, input)]
  if !input.has_suffix(".mbt") {
    paths.push(main_join_path(github_root, input + ".mbt"))
  }
  paths
}

///|
async fn ghq_module_name_candidate_repo_paths(
  github_root : String,
  input : String,
) -> Array[String] {
  let paths : Array[String] = []
  let owner = match input.find("/") {
    Some(sep_idx) => input[:sep_idx].to_string()
    None => return paths
  }
  let owner_root = main_join_path(github_root, owner)
  if !@fs.exists(owner_root) {
    return paths
  }
  let entries = @fs.readdir(
    owner_root,
    include_hidden=false,
    include_special=false,
    sort=true,
  ) catch {
    _ => return paths
  }
  for name in entries {
    if unified_tsmbt_skip_scan_dir(name) {
      continue
    }
    let repo_path = main_join_path(owner_root, name)
    let kind = @fs.kind(repo_path, follow_symlink=false) catch {
      _ => @fs.FileKind::Unknown
    }
    if !(kind is @fs.FileKind::Directory) {
      continue
    }
    let moon_mod_path = main_join_path(repo_path, "moon.mod.json")
    if !main_file_exists(moon_mod_path) {
      continue
    }
    let moon_mod_source = @fs.read_file(moon_mod_path).text() catch {
      _ => continue
    }
    match parse_moon_mod_string_field(moon_mod_source, "name") {
      Some(module_name) if module_name == input => paths.push(repo_path)
      _ => ()
    }
  }
  paths
}

///|
async fn infer_ghq_github_root_from_cwd() -> String? {
  let cwd = @fs.realpath(".") catch { _ => "." }
  match cwd.find("/ghq/github.com/") {
    Some(idx) => Some(cwd[:idx + "/ghq/github.com".length()].to_string())
    None => None
  }
}

///|
async fn resolve_unified_mbt_input_path_from_roots(
  input : String,
  roots : Array[String],
) -> String? {
  if main_file_exists(input) {
    return Some(input)
  }
  let direct_pkg_path = main_join_path(input, "pkg.generated.mbti")
  if main_file_exists(direct_pkg_path) {
    return Some(direct_pkg_path)
  }
  let scan_order : Array[String] = []
  for root in roots {
    if root.has_suffix("/github.com") && !scan_order.contains(root) {
      scan_order.push(root)
    }
  }
  for root in roots {
    if !root.has_suffix("/github.com") && !scan_order.contains(root) {
      scan_order.push(root)
    }
  }
  for root in scan_order {
    let scan_roots = if root.has_suffix("/github.com") && input.contains("/") {
      let paths = ghq_candidate_repo_paths(root, input)
      for path in ghq_module_name_candidate_repo_paths(root, input) {
        if !paths.contains(path) {
          paths.push(path)
        }
      }
      paths
    } else {
      [root]
    }
    for scan_root in scan_roots {
      if !@fs.exists(scan_root) {
        continue
      }
      let candidates : Array[String] = []
      collect_pkg_generated_mbti_files(scan_root, candidates)
      for path in candidates {
        let source = @fs.read_file(path).text() catch { _ => continue }
        match parse_mbti_package_name_from_source(source) {
          Some(package_name) if package_name == input => return Some(path)
          _ => ()
        }
      }
    }
  }
  None
}

///|
fn is_ts_input_path_like(input : String) -> Bool {
  input.has_suffix(".d.ts") ||
  input.has_suffix(".d.mts") ||
  input.has_suffix(".d.cts") ||
  input.has_suffix(".ts") ||
  input.has_suffix(".tsx") ||
  input.has_suffix(".mts") ||
  input.has_suffix(".cts")
}

///|
async fn resolve_unified_ts_input_path(input : String) -> String? {
  if main_file_exists(input) {
    return Some(input)
  }
  @parser.resolve_type_module_specifier("__tsmbt_entry__.ts", input)
}

///|
async fn infer_unified_tsmbt_direction(input : String) -> TsmbtDirection? {
  if is_ts_input_path_like(input) {
    return Some(TsmbtDirection::TsToMbt)
  }
  if input.has_suffix(".mbti") {
    return Some(TsmbtDirection::MbtToTs)
  }
  match resolve_unified_mbt_input_path(input) {
    Some(_) => return Some(TsmbtDirection::MbtToTs)
    None => ()
  }
  match resolve_unified_ts_input_path(input) {
    Some(_) => Some(TsmbtDirection::TsToMbt)
    None => None
  }
}

///|
fn unified_ts_scaffold_diagnostics_path(
  output_dir : String,
  diagnostics_path : String?,
) -> String {
  match diagnostics_path {
    Some(path) => path
    None => main_join_path(output_dir, "SCAFFOLD_DIAGNOSTICS.md")
  }
}

///|
fn unified_ts_fallback_policy_for_package(
  package_spec : String,
) -> (String, String) {
  match package_spec {
    "clsx"
    | "node:path"
    | "node:crypto"
    | "node:os"
    | "node:url"
    | "node:querystring"
    | "node:buffer" =>
      (
        "zero-target", "Keep public JSValue surface at zero; any fallback is a regression.",
      )
    "hono" =>
      (
        "naturalize-target", "Reduce generic context/router fallbacks while keeping route handlers and response helpers natural.",
      )
    "react-router" =>
      (
        "naturalize-target", "Reduce route/path utility overload and option-object fallbacks; keep typed navigation helpers usable from MoonBit.",
      )
    "jose" =>
      (
        "naturalize-target", "Reduce builder option and compact JWS/JWT overload fallbacks around the smoke-tested APIs.",
      )
    "glob" =>
      (
        "naturalize-target", "Reduce pattern/options namespace fallbacks for common sync glob calls.",
      )
    "node:assert" | "node:util" =>
      (
        "naturalize-target", "Shrink remaining Node built-in overload/unknown fallbacks toward zero for the common API surface.",
      )
    "date-fns" | "magic-string" | "source-map" | "node:sqlite" | "node:fs" =>
      (
        "naturalize-target", "Keep reducing fallback around finite option bags, tuple results, and class/value helper APIs.",
      )
    "zod" | "valibot" =>
      (
        "budgeted-fallback", "Schema/parser generics are intentionally smoke-tested and budgeted, not treated as naturally typed MoonBit APIs yet.",
      )
    "preact" =>
      (
        "budgeted-fallback", "JSX/component/children generics are intentionally budgeted until a dedicated JSX/component binding layer exists.",
      )
    "playwright" =>
      (
        "budgeted-fallback", "Large event/callback-heavy API is smoke-tested; only selected launch/device/options surfaces are naturalization targets.",
      )
    "chalk"
    | "dotenv"
    | "ignore"
    | "colorette"
    | "immer"
    | "execa"
    | "vitest/runtime"
    | "express" =>
      (
        "low-fallback-maintain", "Current fallback is small and explicitly budgeted; naturalize only when a real smoke use case needs it.",
      )
    _ =>
      (
        "unclassified", "Add an explicit fallback policy before accepting this package into the real-world corpus.",
      )
  }
}

///|
fn render_unified_ts_fallback_policy_md(module_spec : String) -> String {
  let (class_name, policy) = unified_ts_fallback_policy_for_package(module_spec)
  [
    "## Fallback Policy",
    "",
    "This classification mirrors the real-world bridge quality policy. It is informational in non-strict mode; strict mode still rejects generated `JSValue` fallbacks.",
    "",
    "| package | class | policy |",
    "| --- | --- | --- |",
    "| `\{module_spec}` | \{class_name} | \{policy} |",
  ].join("\n")
}

///|
fn render_unified_ts_scaffold_diagnostics_md(
  module_spec : String,
  unsupported_exports : Array[String],
  jsvalue_fallbacks : Array[String],
) -> String {
  let mut base = render_moonbit_scaffold_diagnostics_md(unsupported_exports)
  // Avoid the contradictory "No unsupported exports were detected." banner
  // when the JSValue fallback list below it is non-empty: those JSValue
  // occurrences are themselves widened TypeScript boundary surfaces and
  // count as unsupported in any practical sense.
  if unsupported_exports.length() == 0 && jsvalue_fallbacks.length() > 0 {
    base = base.replace(
      old="No unsupported exports were detected.",
      new="No structural unsupported exports were detected; \{jsvalue_fallbacks.length()} `JSValue` boundary fallbacks are listed below.",
    )
  }
  let fallback_policy = render_unified_ts_fallback_policy_md(module_spec)
  if jsvalue_fallbacks.length() == 0 {
    return [base, "", fallback_policy].join("\n")
  }
  let lines = [
    base, "", fallback_policy, "", "## JSValue Fallbacks", "", "Strict mode treats these generated `JSValue` occurrences as unbudgeted TypeScript boundary fallbacks.",
    "",
  ]
  for item in jsvalue_fallbacks {
    lines.push("- " + item)
  }
  lines.join("\n")
}

///|
async fn write_unified_ts_scaffold_diagnostics(
  output_dir : String,
  diagnostics_path : String?,
  module_spec : String,
  unsupported_exports : Array[String],
  jsvalue_fallbacks : Array[String],
) -> Bool {
  let path = unified_ts_scaffold_diagnostics_path(output_dir, diagnostics_path)
  if !write_text_file(
      path,
      render_unified_ts_scaffold_diagnostics_md(
        module_spec, unsupported_exports, jsvalue_fallbacks,
      ),
    ) {
    return false
  }
  println("Wrote scaffold diagnostics to \{path}")
  true
}

///|
fn collect_unified_jsvalue_fallbacks_from_source(
  relative_path : String,
  source : String,
  out : Array[String],
) -> Unit {
  let mut line_no = 1
  for line_view in source.split("\n") {
    let line = line_view.trim().to_string()
    if unified_jsvalue_line_is_fallback(line) {
      out.push("\{relative_path}:\{line_no}: \{line}")
    }
    line_no += 1
  }
}

///|
fn unified_jsvalue_line_is_fallback(line : String) -> Bool {
  if !line.contains("JSValue") {
    return false
  }
  match line {
    "/// Complex or unsupported TypeScript types are widened to JSValue."
    | "declare pub type JSValue"
    | "pub type JSValue"
    | "pub type JSValue = @js.Any" => false
    _ => true
  }
}

///|
async fn collect_unified_moonbit_scaffold_jsvalue_fallbacks(
  output_dir : String,
) -> Array[String] {
  let fallbacks : Array[String] = []
  for relative_path in ["bridge.mbti", "bridge.mbt"] {
    let path = main_join_path(output_dir, relative_path)
    let source = @fs.read_file(path).text() catch { _ => continue }
    collect_unified_jsvalue_fallbacks_from_source(
      relative_path, source, fallbacks,
    )
  }
  fallbacks.sort()
  fallbacks
}

///|
fn unified_autolink_diagnostics_has_omissions(source : String) -> Bool {
  source.contains("\n## ")
}

///|
async fn write_requested_unified_autolink_diagnostics(
  output_dir : String,
  diagnostics_path : String?,
) -> Bool {
  let path = match diagnostics_path {
    Some(path) => path
    None => return true
  }
  let autolink_path = main_join_path(output_dir, "AUTOLINK_DIAGNOSTICS.md")
  let diagnostics_md = @fs.read_file(autolink_path).text() catch {
    e => {
      println("Read error: \{e}")
      return false
    }
  }
  if !write_text_file(path, diagnostics_md) {
    return false
  }
  println("Wrote autolink diagnostics to \{path}")
  true
}

///|
async fn emit_unified_tsmbt_scaffold(
  input : String,
  out : String,
  direction~ : TsmbtDirection,
  module_spec~ : String?,
  import_rewrite_path~ : String?,
  diagnostics_path? : String? = None,
  strict? : Bool = false,
  facade~ : Bool,
) -> Bool {
  let resolved_direction = match direction {
    TsmbtDirection::Auto =>
      match infer_unified_tsmbt_direction(input) {
        Some(direction) => direction
        None => {
          println(
            "Error: could not infer direction for '\{input}'. Use --direction mbt-to-ts or --direction ts-to-mbt.",
          )
          return false
        }
      }
    _ => direction
  }
  match resolved_direction {
    TsmbtDirection::MbtToTs => {
      let mbti_path = match resolve_unified_mbt_input_path(input) {
        Some(path) => path
        None => {
          println(
            "Error: could not resolve MoonBit package or pkg.generated.mbti '\{input}'. Run moon info first or pass a pkg.generated.mbti path.",
          )
          return false
        }
      }
      let ok = if facade {
        emit_typescript_facade_scaffold_from_mbti(
          mbti_path, out, import_rewrite_path,
        )
      } else {
        emit_typescript_scaffold_from_mbti(mbti_path, out, import_rewrite_path)
      }
      if !ok {
        return false
      }
      let diagnostics_md = @fs.read_file(
        main_join_path(out, "AUTOLINK_DIAGNOSTICS.md"),
      ).text() catch {
        e => {
          println("Read error: \{e}")
          return false
        }
      }
      if !write_requested_unified_autolink_diagnostics(out, diagnostics_path) {
        return false
      }
      if strict && unified_autolink_diagnostics_has_omissions(diagnostics_md) {
        println(
          "Strict mode rejected MoonBit scaffold: omitted autolink members detected.",
        )
        return false
      }
      true
    }
    TsmbtDirection::TsToMbt => {
      let entry_path = match resolve_unified_ts_input_path(input) {
        Some(path) => path
        None => {
          println(
            "Error: could not resolve TypeScript entrypoint '\{input}'. Pass a .d.ts/.ts path or an installed package specifier.",
          )
          return false
        }
      }
      let runtime_module_spec = match module_spec {
        Some(spec) => spec
        None => input
      }
      let unsupported_exports = collect_moonbit_ts_scaffold_unsupported_exports(
        entry_path,
      ) catch {
        @bridge.ModuleGraphError::ReadError(msg)
        | @bridge.ModuleGraphError::ParseError(msg)
        | @bridge.ModuleGraphError::ResolveError(msg) => {
          println("Emit error: \{msg}")
          return false
        }
      }
      if strict && unsupported_exports.length() > 0 {
        if !write_unified_ts_scaffold_diagnostics(
            out,
            diagnostics_path,
            runtime_module_spec,
            unsupported_exports,
            [],
          ) {
          return false
        }
        println(
          "Strict mode rejected TypeScript scaffold: unsupported exports detected.",
        )
        return false
      }
      if !emit_moonbit_scaffold_from_ts(
          entry_path,
          runtime_module_spec,
          out,
          write_diagnostics=false,
        ) {
        return false
      }
      let jsvalue_fallbacks = collect_unified_moonbit_scaffold_jsvalue_fallbacks(
        out,
      )
      if !write_unified_ts_scaffold_diagnostics(
          out, diagnostics_path, runtime_module_spec, unsupported_exports, jsvalue_fallbacks,
        ) {
        return false
      }
      if strict && jsvalue_fallbacks.length() > 0 {
        println(
          "Strict mode rejected TypeScript scaffold: JSValue fallbacks detected.",
        )
        return false
      }
      true
    }
    TsmbtDirection::Auto => true
  }
}

///|
/// Run the unified `--input/--out/...` driver with a forced direction.
/// `print_help` is invoked on `--help`, missing required flags, or option
/// errors so each cmd module owns its own help banner.
async fn run_unified_cli_with_direction(
  args : Array[String],
  start : Int,
  forced_direction~ : TsmbtDirection,
  print_help~ : () -> Unit,
) -> Bool {
  let options = match parse_unified_tsmbt_options(args, start) {
    (Some(options), None) => options
    (_, Some("help")) => {
      print_help()
      return true
    }
    (_, Some(message)) => {
      println("Error: \{message}")
      print_help()
      return false
    }
    _ => {
      print_help()
      return false
    }
  }
  let direction = match (forced_direction, options.direction) {
    (TsmbtDirection::Auto, requested) => requested
    (TsmbtDirection::TsToMbt, TsmbtDirection::Auto)
    | (TsmbtDirection::TsToMbt, TsmbtDirection::TsToMbt) =>
      TsmbtDirection::TsToMbt
    (TsmbtDirection::MbtToTs, TsmbtDirection::Auto)
    | (TsmbtDirection::MbtToTs, TsmbtDirection::MbtToTs) =>
      TsmbtDirection::MbtToTs
    _ => {
      println(
        "Error: --direction conflicts with this command's fixed direction.",
      )
      return false
    }
  }
  let input = match options.input {
    Some(input) => input
    None => {
      println("Error: expected --input ")
      print_help()
      return false
    }
  }
  let out = match options.out {
    Some(out) => out
    None => {
      println("Error: expected --out ")
      print_help()
      return false
    }
  }
  emit_unified_tsmbt_scaffold(
    input,
    out,
    direction~,
    module_spec=options.module_spec,
    import_rewrite_path=options.import_rewrite_path,
    diagnostics_path=options.diagnostics_path,
    strict=options.strict,
    facade=options.facade,
  )
}

///|
/// Public entry: run the unified TS -> MoonBit scaffold driver with the
/// direction locked to `ts-to-mbt`. Used by `src/cmd/ts2mbt`.
pub async fn run_ts_to_mbt_unified_cli(
  args : Array[String],
  start : Int,
  print_help~ : () -> Unit,
) -> Bool {
  run_unified_cli_with_direction(
    args,
    start,
    forced_direction=TsmbtDirection::TsToMbt,
    print_help~,
  )
}

///|
/// Public entry: run the unified MoonBit -> TypeScript scaffold driver with
/// the direction locked to `mbt-to-ts`. Used by `src/cmd/mbt2ts`.
pub async fn run_mbt_to_ts_unified_cli(
  args : Array[String],
  start : Int,
  print_help~ : () -> Unit,
) -> Bool {
  run_unified_cli_with_direction(
    args,
    start,
    forced_direction=TsmbtDirection::MbtToTs,
    print_help~,
  )
}