// 命令行入口。
//
// 参数解析交给官方库 moonbitlang/core/argparse,文件读取交给
// moonbitlang/x/fs,退出码用 moonbitlang/x/sys。
//
// 给一个 STL / OBJ / 3MF / PLY 文件,打印它的体检报告:几何数据加四项校验结论。
// 可选 --svg 导出等轴测预览图,--fix 修复并存到指定文件。

///|
/// 命令行定义。
///
/// 和上个项目一样特意关掉 argparse 自带的 `--help`:它内部直接调 wasi 的
/// proc_exit 结束进程,退出码和输出都不受我们控制。换成一枚普通 flag
/// 自己处理,这样还能在参数表后面接一段中文示例。
fn command() -> @argparse.Command {
  @argparse.Command(
    "stlkit",
    about="STL / OBJ / 3MF / PLY 三维网格工具链——解析、校验、分析 3D 打印模型",
    positionals=[
      @argparse.PositionArg(
        "file",
        about="要处理的模型文件路径(STL / OBJ / 3MF / PLY)",
        num_args=@argparse.ValueRange(lower=0, upper=1),
      ),
    ],
    options=[
      @argparse.OptionArg(
        "max-input-bytes",
        about="输入字节上限,默认 67108864",
      ),
      @argparse.OptionArg(
        "max-triangles",
        about="解析或展开三角形上限,默认 2000000",
      ),
      @argparse.OptionArg(
        "max-vertices",
        about="顶点记录上限,默认 6000000",
      ),
      @argparse.OptionArg(
        "max-uncompressed-bytes",
        about="3MF 总解压字节上限,默认 104857600",
      ),
      @argparse.OptionArg(
        "nozzle",
        about="喷嘴直径(毫米),仅记录,不参与工艺判定",
      ),
      @argparse.OptionArg(
        "supports",
        about="支撑设置 on/off/unknown,仅记录,不计算支撑需求",
      ),
      @argparse.OptionArg(
        "samples",
        about="壁厚射线样本数,1..100000,默认1000;面数×样本须≤20000000",
      ),
      @argparse.OptionArg(
        "min-thickness",
        about="参考最小壁厚(毫米),默认0.8;0不设阈值",
      ),
      @argparse.OptionArg(
        "svg",
        about="导出等轴测 SVG 预览;与 --fix 同用时预览修复后的网格",
      ),
      @argparse.OptionArg(
        "fix",
        about="修复网格并存到指定文件(按后缀选格式:.stl / .obj / .ply)",
      ),
    ],
    flags=[
      @argparse.FlagArg(
        "json",
        about="输出 schema_version=1 JSON;可与 --strict、阈值及限额组合",
      ),
      @argparse.FlagArg(
        "stream-only",
        about="只分块统计二进制 STL;顶点和边记录仍增长",
      ),
      @argparse.FlagArg(
        "strict",
        about="模型有问题时以退出码 1 结束(给 CI 用)",
      ),
      @argparse.FlagArg(
        "bench",
        about="跑基准测试,对比耗时并估算主要数据字段量",
      ),
      @argparse.FlagArg("help", short='h', about="显示这份帮助"),
    ],
    disable_help_flag=true,
  )
}

// ── 基准测试 ────────────────────────────────────────────────────────────
//
// 回答一个问题:处理超大模型时,**不保留网格**能省多少。
//
// 仅估算文件及主要数据字段,不包含哈希索引、对象头、分配器和运行时开销。
// 各路径还会分配额外分析数据;这些数字不能用来计算实际内存节省比例。

///|
/// 文件与三角形坐标数据量(字节),不是完整内存占用。
///
/// - 文件字节:读进来之后一直在内存里
/// - 每个三角形是 4 个 Vec3 = 12 个 Double = 96 字节
/// 不计容器、对象头、顶点焊接与绕向分析的附加存储。
fn full_memory(file_size : Int32, triangles : Int32) -> Int64 {
  file_size.to_int64() + triangles.to_int64() * 96L
}

///|
/// 文件、顶点字段与全部边记录的数据量(字节)。
///
/// - 文件字节:整块扫描入口的输入
/// - 顶点字段:三个 Double 坐标与一个 Int 编号,28 字节/项
/// - 边记录:每个三角形存三条 UInt64,24 字节/三角形,含重复边
/// 不计坐标键标签、哈希索引、对象头、容器与分配器开销。
fn stream_memory(
  file_size : Int32,
  vertices : Int32,
  triangles : Int32,
) -> Int64 {
  file_size.to_int64() + vertices.to_int64() * 28L + triangles.to_int64() * 24L
}

///|
/// 把字节数格式化成好读的样子。
fn fmt_bytes(n : Int64) -> String {
  let mb = 1024.0 * 1024.0
  let kb = 1024.0
  let d = n.to_double()
  if d >= mb {
    "\{fmt(d / mb, 2)} MB"
  } else if d >= kb {
    "\{fmt(d / kb, 2)} KB"
  } else {
    "\{n} 字节"
  }
}

///|
/// 分块读取文件并流式扫描。
///
/// 不将完整文件读入内存;读取缓冲区固定,顶点表和边记录仍随规模增长。
///
/// 走 `moonbitlang/async/fs` 是因为 `moonbitlang/x/fs` 只有
/// `read_file_to_bytes`(一次读完),没有带偏移量的读取。
async fn scan_chunked_storage(
  path : String,
  limits~ : @stlkit.ParseLimits = @stlkit.ParseLimits::default(),
) -> Result[(@stlkit.StreamReport, @stlkit.ScanStorage), String] {
  let file = @asyncfs.open(path, mode=@asyncfs.Mode::ReadOnly) catch {
    err => return Err("打不开文件「\{path}」:\{err}")
  }
  let size = file.size() catch {
    err => {
      let _ = file.close()
      return Err("读不了文件大小:\{err}")
    }
  }
  if size < 0L || size > limits.max_input_bytes.to_int64() {
    let _ = file.close()
    return Err(
      "resource_limit: max_input_bytes (max=\{limits.max_input_bytes})",
    )
  }
  let scanner = @stlkit.StlScanner::new(limits~)
  // 256 KB 一块。这是权衡:太小了系统调用次数多,太大了缓冲区本身占内存。
  // 关键是它**固定**——文件再大也不会变大
  let chunk = 262144
  let buf : FixedArray[Byte] = FixedArray::make(chunk, b'\x00')
  let mut pos = 0L
  let mut failure : String? = None
  while pos < size && failure is None {
    let remain = size - pos
    let want = if remain < chunk.to_int64() { remain.to_int() } else { chunk }
    let n = file.read_at(buf, position=pos, offset=0, len=want) catch {
      err => {
        failure = Some("读文件失败:\{err}")
        0
      }
    }
    if n <= 0 {
      break
    }
    // 把这一块读到的部分交给扫描器
    let part : Array[Byte] = []
    for i = 0; i < n; i = i + 1 {
      part.push(buf[i])
    }
    scanner.feed(Bytes::from_array(part))
    if scanner.error() is Some(message) {
      failure = Some(message)
    }
    pos = pos + n.to_int64()
  }
  let _ = file.close()
  match failure {
    Some(msg) => Err(msg)
    None =>
      match scanner.finish() {
        Ok(report) => Ok((report, scanner.storage()))
        Err(msg) => Err(msg)
      }
  }
}

///|
async fn scan_chunked(
  path : String,
  limits~ : @stlkit.ParseLimits = @stlkit.ParseLimits::default(),
) -> Result[@stlkit.StreamReport, String] {
  match scan_chunked_storage(path, limits~) {
    Ok((report, _)) => Ok(report)
    Err(msg) => Err(msg)
  }
}

///|
async fn stream_only(
  path : String,
  limits~ : @stlkit.ParseLimits = @stlkit.ParseLimits::default(),
) -> Result[Bool, String] {
  match scan_chunked_storage(path, limits~) {
    Err(msg) => Err("分块入口只支持二进制 STL:\{msg}")
    Ok((report, storage)) => {
      println("二进制 STL 分块统计;不构造完整 Mesh")
      println(
        "三角形 \{report.triangle_count};顶点表 \{storage.unique_vertices} 项;边记录 \{storage.edge_records} 项",
      )
      println(
        "扫描器单次缓冲峰值 \{storage.peak_buffered_bytes} 字节;残留 \{storage.pending_bytes} 字节(不是进程内存实测)",
      )
      println(
        "总存储 O(V+F+chunk):保存全部三角形边记录,排序前包含重复边;并非固定总内存。",
      )
      println(
        "此入口未检查绕向、壁厚或工艺,不能产生可打印结论。",
      )
      Ok(false)
    }
  }
}

///|
/// 跑三种解析方式,记录耗时与主要字段数据量估算。
///
/// 估算公式见 `full_memory` / `stream_memory`,不是进程内存实测。
async fn bench(
  path : String,
  limits~ : @stlkit.ParseLimits = @stlkit.ParseLimits::default(),
) -> Result[Bool, String] {
  // ── 先做分块流式:这一步不碰整个文件,测出来的才是真流式 ──
  //
  // 分块扫描目前只支持**二进制** STL——那边每个三角形固定 50 字节,
  // 切块就是数数;ASCII 格式的记录是变长的文本行,得另写一套按行的缓冲。
  // 所以先试一下,不是二进制就跳过这一档,并在报告里说明原因,
  // 而不是让整个基准测试失败。
  let t2 = @bench.monotonic_clock_start()
  let chunked : @stlkit.StreamReport? = match scan_chunked(path, limits~) {
    Ok(r) => Some(r)
    Err(_) => None
  }
  let chunked_us = @bench.monotonic_clock_end(t2)

  // ── 后面两种都要整个文件在内存里 ──
  let data = match read_input(path, limits) {
    Ok(data) => data
    Err(error) => return Err(error)
  }
  let file_size = data.length()

  // 本工具的流式实现仅针对 STL,不将其他格式送入 STL 扫描器。
  let format = @stlkit.detect_format(data)
  if !(format is @stlkit.Ascii) && !(format is @stlkit.Binary) {
    return Err(
      "--bench 目前只支持 STL;其他格式请不带 --bench 查看报告",
    )
  }

  // 全量:解析出完整网格,能校验、能修复
  let t0 = @bench.monotonic_clock_start()
  let mesh = match @stlkit.parse_mesh(data, limits~) {
    Ok(mesh) => mesh
    Err(msg) => return Err(msg)
  }
  let check = @stlkit.validate_default(mesh)
  let winding = @stlkit.check_winding_default(mesh)
  let full_us = @bench.monotonic_clock_end(t0)

  // 流式:不保留网格,但文件还在内存里
  let t1 = @bench.monotonic_clock_start()
  let stream = match @stlkit.detect_format(data) {
    @stlkit.Ascii =>
      match @stlkit.stream_stl_ascii(@utf8.decode_lossy(data), limits~) {
        Ok(r) => r
        Err(msg) => return Err(msg)
      }
    _ =>
      match @stlkit.stream_stl_binary(data, limits~) {
        Ok(r) => r
        Err(msg) => return Err(msg)
      }
  }
  let stream_us = @bench.monotonic_clock_end(t1)

  // 结论必须一致,不然快也没意义
  if check.triangle_count != stream.triangle_count ||
    check.unique_vertex_count != stream.unique_vertex_count ||
    check.boundary_edge_count != stream.boundary_edge_count ||
    check.non_manifold_edge_count != stream.non_manifold_edge_count {
    return Err("全量和流式的结论对不上,不能作为基准")
  }
  match chunked {
    Some(c) =>
      if c.triangle_count != stream.triangle_count ||
        c.unique_vertex_count != stream.unique_vertex_count ||
        c.boundary_edge_count != stream.boundary_edge_count ||
        c.non_manifold_edge_count != stream.non_manifold_edge_count {
        return Err(
          "分块流式和整块流式的结论对不上,不能作为基准",
        )
      }
    None => ()
  }

  let triangles = mesh.triangle_count()
  // 分块输入缓冲加保留的字段,不含完整文件。
  let chunk_bytes = 262144L
  let full_mem = full_memory(file_size, triangles)
  let stream_fields = stream_memory(0, stream.unique_vertex_count, triangles)
  let stream_mem = stream_memory(
    file_size,
    stream.unique_vertex_count,
    triangles,
  )
  let chunked_mem = match chunked {
    Some(c) =>
      chunk_bytes + stream_memory(0, c.unique_vertex_count, c.triangle_count)
    None => 0L
  }

  println(path)
  println("  文件大小    \{fmt_bytes(file_size.to_int64())}")
  println("  三角形      \{triangles}")
  println("")
  println("  全量解析(保留网格,能校验能修复)")
  println("    耗时      \{fmt(full_us / 1000.0, 1)} ms")
  println(
    "    数据量估算 文件 \{fmt_bytes(file_size.to_int64())} + 三角形字段 \{fmt_bytes(triangles.to_int64() * 96L)} = \{fmt_bytes(full_mem)}",
  )
  println("")
  println("  流式扫描(不保留网格,文件整块读)")
  println("    耗时      \{fmt(stream_us / 1000.0, 1)} ms")
  println(
    "    数据量估算 文件 \{fmt_bytes(file_size.to_int64())} + 顶点边字段 \{fmt_bytes(stream_fields)} = \{fmt_bytes(stream_mem)}",
  )
  println("")
  println("  流式扫描(不保留网格,文件分块读)")
  match chunked {
    Some(_) => {
      println("    耗时      \{fmt(chunked_us / 1000.0, 1)} ms")
      println(
        "    数据量估算 缓冲 \{fmt_bytes(chunk_bytes)} + 顶点边字段 \{fmt_bytes(chunked_mem - chunk_bytes)} = \{fmt_bytes(chunked_mem)}",
      )
    }
    None =>
      println(
        "    跳过      分块扫描目前只支持二进制 STL,这个文件是 ASCII 格式",
      )
  }
  println("")

  println(
    "  交叉验证    三角形、顶点、边界边和非流形边计数一致(通过)",
  )
  println(
    "              绕向一致性:全量查出 \{winding.flipped_faces} 个反向面(流式做不到这项)",
  )
  println("")
  println(
    "  说明        以上是主要数据字段量的粗略估算,不是进程内存实测;",
  )
  println(
    "              耗时对比对流式有利,因为全量还多做了绕向分析。",
  )
  let best_us = if chunked_mem > 0L { chunked_us } else { stream_us }
  if full_us > 0.0 && best_us > 0.0 {
    println(
      "  本次耗时比  全量 / 流式 = \{fmt(full_us / best_us, 1)}(包含的分析步骤不同)",
    )
  }
  Ok(false)
}

///|
/// 帮助文本:argparse 渲染的参数表,加上中文示例。
fn help_text(cmd : @argparse.Command) -> String {
  cmd.render_help() +
  "\n示例:\n" +
  "  stlkit model.stl                  读一个模型,出体检报告\n" +
  "  stlkit model.stl --fix fixed.stl  修复并存到 fixed.stl\n" +
  "  stlkit model.stl --svg a.svg      顺便导出等轴测预览图\n" +
  "  stlkit model.stl --strict         模型有问题时退出码为 1(给 CI 用)\n" +
  "  stlkit model.stl --bench          跑基准测试,记录耗时与数据量估算\n" +
  "  stlkit                            不带参数时打印这份帮助\n" +
  "\n" +
  "格式:二进制 STL、ASCII STL、OBJ、3MF、PLY 五种都能认,按文件内容判断,\n" +
  "不看扩展名——从别处拷来的文件经常带着错的扩展名。\n"
}

///|
/// 把浮点数格式化成固定小数位。
///
/// 直接用 Double 的默认输出会得到 0.9999999999999999 这种,对看报告的
/// 人没有意义——网格数据本身就带浮点误差,显示上保留几位就够。
fn fmt(value : Double, digits : Int32) -> String {
  let mut scale = 1.0
  for _i = 0; _i < digits; _i = _i + 1 {
    scale = scale * 10.0
  }
  let rounded = (value * scale).round() / scale
  rounded.to_string()
}

///|
/// 处理一个模型文件,把报告打印出来。
///
/// `svg_path` 给了的话,导出最终网格的等轴测预览图(含修复后的结果)。
/// `fix_path` 给了的话,修一遍网格存到那里,并把修复前后的校验结果对照着打出来。
///
/// 返回值里的 `Bool` 是「这个模型有没有问题」——`--strict` 模式要靠它定退出码。
/// 分开放而不是塞进错误里,是因为「模型有问题」不是**程序出错**:报告已经
/// 正常打印出来了,只是结论是负面的。
async fn report(
  path : String,
  svg_path : String,
  fix_path : String,
  min_thickness : Double,
  samples : Int32,
  limits : @stlkit.ParseLimits,
) -> Result[Bool, String] {
  let input = @path.Path(path).resolve()
  for output in [fix_path, svg_path] {
    if output != "" &&
      @path.Path(output).resolve().relative(base=input).to_string() == "" {
      return Err(
        "输出路径不能与输入模型相同(规范化后相同);请选择新文件名",
      )
    }
  }
  if fix_path != "" && svg_path != "" {
    // 使用宿主平台规则解析绝对路径,处理 .、..、重复分隔符与 Windows 大小写。
    let fix_output = @path.Path(fix_path).resolve()
    let svg_output = @path.Path(svg_path).resolve()
    if svg_output.relative(base=fix_output).to_string() == "" {
      return Err(
        "--fix 与 --svg 必须使用不同的输出路径(规范化后相同)",
      )
    }
  }
  match output_paths_safe(path, fix_path, svg_path) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let data = match read_input(path, limits) {
    Ok(data) => data
    Err(error) => return Err(error)
  }
  let format_name = match @stlkit.detect_format(data) {
    @stlkit.Binary => "二进制 STL"
    @stlkit.Ascii => "ASCII STL"
    @stlkit.ThreeMF => "3MF"
    @stlkit.Ply => "PLY"
    // 内容提示与解析有效性分开;未知文本不再标为 OBJ。
    @stlkit.Obj => "OBJ"
    @stlkit.Unknown => "未知格式"
  }
  let mesh = match @stlkit.parse_mesh(data, limits~) {
    Ok(mesh) => mesh
    Err(msg) => return Err(msg)
  }
  match sampling_budget_error(mesh.triangle_count(), samples) {
    Some(message) => return Err(message)
    None => ()
  }
  let review = @stlkit.check_printability(mesh, min_thickness, samples~)
  let check = review.validation

  println(path)
  println("  格式      \{format_name}")
  println("  参考阈值  \{min_thickness} mm;样本预算 \{samples}")
  println("  三角形    \{mesh.triangle_count()} 个")
  println(
    "  顶点      \{check.raw_vertex_count} 个(去重后 \{check.unique_vertex_count} 个)",
  )
  match mesh.bounding_box() {
    Some(box) => {
      let size = box.size()
      println(
        "  包围盒    \{fmt(size.x, 3)} × \{fmt(size.y, 3)} × \{fmt(size.z, 3)} mm",
      )
    }
    None => ()
  }
  // 定向包围盒。斜着摆的模型用轴对齐的盒子量会虚胖,
  // 这个贴着模型走,能看出「它实际占多大地方」
  match mesh.oriented_bounding_box() {
    Some(obb) => {
      let size = obb.size()
      println(
        "  定向包围盒 \{fmt(size.x, 3)} × \{fmt(size.y, 3)} × \{fmt(size.z, 3)} mm",
      )
    }
    None => ()
  }
  if review.winding.orientation_reliable() {
    println("  体积      \{fmt(mesh.volume(), 3)} mm³")
  } else {
    println(
      "  有符号贡献绝对值 \{fmt(mesh.volume(), 3)} mm³(朝向未通过复核,不作可靠实体体积)",
    )
  }
  println("  表面积    \{fmt(mesh.surface_area(), 3)} mm²")
  // 零件数:几块互不相连的几何。单独一项显示,不是「错误」——
  // 分开的零件本身是合法模型,只是要知道
  let parts = @stlkit.count_parts_default(mesh)
  println("  零件      \{parts} 个")
  // 欧拉示性数只在网格水密时才有拓扑含义(2 = 一个实心块,0 = 有一个
  // 穿透的洞……)。不水密的时候它只是「顶点数减边数加面数」,
  // 报出来会让人以为读出了什么,所以不显示
  if check.is_watertight() && review.winding.orientation_reliable() {
    println(
      "  欧拉数    \{check.euler_number()}(单个闭合可定向曲面:χ = 2 − 2g)",
    )
  }
  // 质量属性使用库中统一的前提检查;边计数闭合并不足够。
  if review.mass_properties_available() && mesh.mass_properties_numeric_valid() {
    let c = mesh.center_mass()
    println("  重心      \{fmt(c.x, 3)}, \{fmt(c.y, 3)}, \{fmt(c.z, 3)} mm")
    let pm = mesh.principal_moments()
    // 用 3 位小数,和别的字段一致。不能只留 1 位——单位立方体的主惯量是
    // 1/6 ≈ 0.167,只留 1 位就变成「0.2」,对着公式验的人会被误导
    println(
      "  主惯量    \{fmt(pm[0], 3)} / \{fmt(pm[1], 3)} / \{fmt(pm[2], 3)}(密度取 1)",
    )
  } else {
    println("  重心      未评估")
    println("  主惯量    未评估")
    println(
      "  质量属性前提  需非空、闭合、无退化面、绕向一致且分量朝向可靠(无潜在嵌套或重叠)",
    )
  }
  match review.thickness {
    Some(thick) =>
      if thick.hits > 0 {
        println(
          "  采样最小厚度 \{fmt(thick.min_thickness, 3)} mm(命中 \{thick.hits}/\{thick.samples})",
        )
      }
    None => ()
  }
  let numeric_failure = review.mass_properties_available() &&
    !mesh.mass_properties_numeric_valid()
  let has_problem = review.needs_review() || numeric_failure
  println("")
  print_findings(review, parts, numeric_failure~)

  let (final_mesh, final_has_problem) = if fix_path != "" {
    match
      fix_and_save(
        mesh,
        check,
        review.winding,
        fix_path,
        min_thickness,
        samples,
      ) {
      Ok(result) => result
      Err(message) => return Err(message)
    }
  } else {
    (mesh, has_problem)
  }

  if svg_path != "" {
    if final_mesh.triangle_count() > 20000 {
      println(
        "  预览降采样:从 \{final_mesh.triangle_count()} 面确定性选择最多 20000 面",
      )
    }
    let svg = match
      @stlkit.render_svg_checked(final_mesh, @stlkit.PreviewOptions::default()) {
      Ok(svg) => svg
      Err(message) => return Err("无法生成预览图:" + message)
    }
    @fs.write_string_to_file(svg_path, svg) catch {
      @fs.IOError(msg) =>
        return Err("写不了预览图「\{svg_path}」:\{msg}")
    }
    println("")
    println("  预览图已导出到 \{svg_path}")
  }
  Ok(final_has_problem)
}

///|
/// 打印校验结果。
///
/// 每一条「不通过」都要说清楚**为什么这是问题**,不能只报一个数字。
/// 看到「有 3 条边界边」的人不知道这意味着什么,看到「切片软件分不清
/// 哪里是外面」才知道要修。
fn print_findings(
  review : @stlkit.PrintabilityReport,
  parts : Int32,
  numeric_failure~ : Bool = false,
) -> Unit {
  println("  \{@stlkit.review_scope()}")
  for component in review.winding.components {
    println(
      "  分量 \{component.index}:\{component.faces.length()} 面;有符号体积 \{fmt(component.signed_volume, 6)} mm³;\{component.orientation.label()};闭合 \{component.closed};绕向一致 \{component.consistent}",
    )
    if component.potentially_nested {
      println(
        "    ⚠ 分量包围盒重叠,未判定内腔或壳层嵌套,不自动改变整体方向",
      )
    }
  }
  let check = review.validation
  if check.boundary_edge_count > 0 {
    println(
      "  ⚠ 有 \{check.boundary_edge_count} 条边界边,请复核开放面",
    )
  }
  if check.non_manifold_edge_count > 0 {
    println("  ⚠ 有 \{check.non_manifold_edge_count} 条非流形边")
  }
  if check.degenerate_count > 0 {
    println("  ⚠ 有 \{check.degenerate_count} 个退化面")
  }
  if check.wrong_normal_count > 0 {
    println(
      "  ⚠ 有 \{check.wrong_normal_count} 个面的已保留声明面法线方向与三角形绕向相反(不代表完整法线校验)",
    )
  }
  if review.winding.flipped_faces > 0 {
    println(
      "  ⚠ 有 \{review.winding.flipped_faces} 个面的相邻绕向不一致",
    )
  }
  if review.inward() {
    println(
      "  ⚠ 有 \{review.inward_component_count()} 个闭合分量的有符号体积为负,请复核朝向或内腔语义",
    )
  }
  if review.thin_wall() {
    println(
      "  ⚠ 采样厚度低于参考阈值 \{fmt(review.min_thickness, 3)} mm;请按材料和设备确认",
    )
  }
  if review.thickness_unavailable() {
    println("  ⚠ 当前网格无法可靠评估壁厚")
  }
  if parts > 1 {
    println("  提示:有 \{parts} 个按共享边分离的零件")
  }
  if numeric_failure {
    println("  ⚠ 质量属性数值复核未通过,结果未评估")
    println(
      "  结论:数值计算异常,需要复核;不构成可打印结论。",
    )
  } else {
    println("  结论:\{review.verdict()}")
  }
}

///|
/// 修一遍网格,存到指定文件,把修复前后的校验结果对照着打出来。
///
/// 对照着打是有意的:只报「修复完成」没法让人判断到底修好了没有。
/// 显示「边界边 3 → 0」才叫证据。
fn fix_and_save(
  mesh : @stlkit.Mesh,
  before : @stlkit.ValidationReport,
  winding : @stlkit.WindingReport,
  fix_path : String,
  min_thickness : Double,
  samples : Int32,
) -> Result[(@stlkit.Mesh, Bool), String] {
  if before.is_watertight() &&
    before.degenerate_count == 0 &&
    before.wrong_normal_count == 0 &&
    winding.flipped_faces == 0 &&
    winding.orientation_reliable() {
    println("")
    println("未发现当前自动修复能处理的网格问题。")
    let saved = match save_mesh(mesh, fix_path) {
      Ok(saved) => saved
      Err(message) => return Err(message)
    }
    return Ok(
      (
        saved,
        @stlkit.check_printability(saved, min_thickness, samples~).needs_review() ||
        !saved.mass_properties_numeric_valid(),
      ),
    )
  }

  let fixed = match
    @stlkit.repair_checked(mesh, @stlkit.RepairOptions::default()) {
    Ok(fixed) => fixed
    Err(message) => return Err("自动修复拒绝:" + message)
  }
  let after_review = @stlkit.check_printability(
    fixed.mesh,
    min_thickness,
    samples~,
  )
  let after = after_review.validation

  println("")
  println("修复:")
  if fixed.removed_degenerate > 0 {
    println("  删掉 \{fixed.removed_degenerate} 个退化三角形")
  }
  if fixed.removed_duplicates > 0 {
    println("  删掉 \{fixed.removed_duplicates} 个重复三角形")
  }
  if fixed.added_triangles > 0 {
    println("  补洞:新增 \{fixed.added_triangles} 个三角形")
  }
  if fixed.recomputed_normals > 0 {
    println("  重算 \{fixed.recomputed_normals} 个三角形的法线")
  }
  if fixed.reoriented_components > 0 {
    println(
      "  按独立闭合零件假设翻转 \{fixed.reoriented_components} 个负体积分量",
    )
  }
  if fixed.flipped_faces > 0 {
    // 把体积的变化也打出来 —— 绕向反了是「看不出来的坏」,
    // 体积从错变对是它唯一的直接证据
    println(
      "  翻转 \{fixed.flipped_faces} 个绕向反了的面(体积从 \{fmt(mesh.volume(), 3)} 修正为 \{fmt(fixed.mesh.volume(), 3)})",
    )
  }
  if !fixed.changed() {
    println(
      "  没能改动任何东西 —— 这个网格的问题修复功能处理不了",
    )
    return Err("修复没有产生任何改动")
  }

  println("")
  println(
    "  修复前 边界边 \{before.boundary_edge_count}  非流形边 \{before.non_manifold_edge_count}  退化面 \{before.degenerate_count}",
  )
  println(
    "  修复后 边界边 \{after.boundary_edge_count}  非流形边 \{after.non_manifold_edge_count}  退化面 \{after.degenerate_count}",
  )

  let saved = match save_mesh(fixed.mesh, fix_path) {
    Ok(saved) => saved
    Err(message) => return Err(message)
  }
  match sampling_budget_error(saved.triangle_count(), samples) {
    Some(message) => return Err(message)
    None => ()
  }
  let saved_review = @stlkit.check_printability(saved, min_thickness, samples~)
  print_findings(
    saved_review,
    @stlkit.count_parts_default(saved),
    numeric_failure=saved_review.mass_properties_available() &&
      !saved.mass_properties_numeric_valid(),
  )
  Ok(
    (
      saved,
      saved_review.needs_review() ||
      (
        saved_review.mass_properties_available() &&
        !saved.mass_properties_numeric_valid()
      ),
    ),
  )
}

///|
/// 按请求导出最终网格;没有可修复改动时也生成输出文件。
fn save_mesh(
  mesh : @stlkit.Mesh,
  path : String,
) -> Result[@stlkit.Mesh, String] {
  let bytes = match @stlkit.write_mesh(mesh, path) {
    Ok(bytes) => bytes
    Err(msg) => return Err(msg)
  }
  let output_limits = {
    ..@stlkit.ParseLimits::default(),
    max_input_bytes: bytes.length(),
    max_triangles: mesh.triangle_count().max(1),
    max_vertices: (mesh.triangle_count() * 3).max(1),
  }
  match @stlkit.parse_mesh(bytes, limits=output_limits) {
    Err(message) => return Err("导出数据无法重读:" + message)
    Ok(_) => ()
  }
  @fs.write_bytes_to_file(path, bytes) catch {
    @fs.IOError(msg) => return Err("写不了文件「\{path}」:\{msg}")
  }
  let actual = @fs.read_file_to_bytes(path) catch {
    @fs.IOError(message) =>
      return Err("无法重读已导出文件:" + message)
  }
  let restored = match @stlkit.parse_mesh(actual, limits=output_limits) {
    Ok(restored) => restored
    Err(message) => return Err("已导出文件复核失败:" + message)
  }
  println("")
  println("  已存到 \{path}(\{bytes.length()} 字节)")
  println("  已重读导出文件,最终检查与 SVG 使用重读网格")
  Ok(restored)
}

///|
/// 常规检查路径。
async fn run(
  matches : @argparse.Matches,
  path : String,
) -> Result[Bool, String] {
  let limits = match cli_limits(matches) {
    Ok(limits) => limits
    Err(error) => return Err(error)
  }
  let nozzle = match matches.values.get("nozzle") {
    Some([raw, ..]) => {
      let value = @string.parse_double(raw) catch {
        _ => return Err("--nozzle 必须是正的有限数字")
      }
      if value.is_nan() || value.is_inf() || value <= 0.0 {
        return Err("--nozzle 必须是正的有限数字")
      }
      value.to_string() + " mm"
    }
    _ => "未指定"
  }
  let supports = match matches.values.get("supports") {
    Some([raw, ..]) =>
      if raw == "on" || raw == "off" || raw == "unknown" {
        raw
      } else {
        return Err("--supports 只接受 on/off/unknown")
      }
    _ => "unknown"
  }
  let samples = match matches.values.get("samples") {
    Some([raw, ..]) => {
      let value = @string.parse_int(raw) catch {
        _ => return Err("--samples 必须是 1..100000 的整数")
      }
      if value < 1 || value > 100000 {
        return Err("--samples 必须是 1..100000 的整数")
      }
      value
    }
    _ => 1000
  }
  let svg_path = match matches.values.get("svg") {
    Some([first, ..]) => first
    _ => ""
  }
  let fix_path = match matches.values.get("fix") {
    Some([first, ..]) => first
    _ => ""
  }
  let min_thickness = match matches.values.get("min-thickness") {
    Some([raw, ..]) => {
      let value = @string.parse_double(raw) catch {
        _ => return Err("--min-thickness 必须是非负有限数字")
      }
      if value.is_nan() || value.is_inf() || value < 0.0 {
        return Err("--min-thickness 必须是非负有限数字")
      }
      value
    }
    _ => 0.8
  }
  println(
    "场景记录:喷嘴 \{nozzle};支撑 \{supports}(仅记录,不参与通过/失败判定)",
  )
  let has_problem = match
    report(path, svg_path, fix_path, min_thickness, samples, limits) {
    Ok(v) => v
    Err(msg) => return Err(msg)
  }

  // --strict:模型有问题就以退出码 1 结束,给 CI 用。
  // 注意这里不打印「错误:」——报告已经正常打出来了,只是结论是负面的,
  // 加个错误前缀会让人以为是程序出错。
  if matches.flags.get("strict") == Some(true) && has_problem {
    @sys.exit(1)
  }
  Ok(has_problem)
}

///|
/// 命令行入口。出错时以退出码 1 结束,方便脚本判断。
///
/// 写成 `async fn main` 是因为基准测试要分块读文件,走的是
/// `moonbitlang/async/fs`。其余路径都是同步的,只是借了这个入口分派。
async fn main {
  if json_requested() {
    json_main()
    return
  }
  let result = try {
    let cmd = command()
    let matches = cmd.parse()
    let path = match matches.values.get("file") {
      Some([first, ..]) => first
      _ => ""
    }
    if matches.flags.get("help") == Some(true) || path == "" {
      println(help_text(cmd))
      Ok(false)
    } else if matches.flags.get("stream-only") == Some(true) {
      let mut geometry_options = false
      for name, _ in matches.values {
        if name != "file" && !name.has_prefix("max-") {
          geometry_options = true
        }
      }
      if matches.flags.get("bench") == Some(true) ||
        matches.flags.get("strict") == Some(true) ||
        geometry_options {
        Err(
          "--stream-only 只接受文件路径,不与几何检查或基准参数组合",
        )
      } else {
        match cli_limits(matches) {
          Ok(limits) => stream_only(path, limits~)
          Err(error) => Err(error)
        }
      }
    } else if matches.flags.get("bench") == Some(true) {
      match cli_limits(matches) {
        Ok(limits) => bench(path, limits~)
        Err(error) => Err(error)
      }
    } else {
      run(matches, path)
    }
  } catch {
    err => Err(err.to_string())
  }
  match result {
    Ok(_) => ()
    Err(msg) => {
      println("错误:\{msg}")
      @sys.exit(1)
    }
  }
}