// CLI —— copybook 命令行工具
//
// 四个子命令:
//   copybook layout --copybook 
//   copybook decode --copybook  --data  [--format jsonl|table] [--record-length N]
//   copybook encode --copybook  --json  --out 
//   copybook verify --copybook  --data 
//
// 退出码:0 成功 / 1 数据错误 / 2 用法错误
//
// 参数解析手写(不引入 argparse):四个子命令的参数集很小,手写更可控,
// 也避免把 `--record-length` 这类项目特有语义硬塞进通用解析器。

///|
/// 退出码常量
const EXIT_OK : Int32 = 0

///|
const EXIT_DATA : Int32 = 1

///|
const EXIT_USAGE : Int32 = 2

///|
/// 用法说明(打印到 stderr)
fn usage_text() -> String {
  let sb = StringBuilder()
  sb.write_string("copybook — COBOL copybook 编解码工具\n\n")
  sb.write_string("用法:\n")
  sb.write_string("  copybook layout --copybook \n")
  sb.write_string(
    "  copybook decode --copybook  --data  [--format jsonl|table] [--record-length N]\n",
  )
  sb.write_string(
    "  copybook encode --copybook  --json  --out \n",
  )
  sb.write_string("  copybook verify --copybook  --data \n")
  sb.write_string("\n全局选项:\n")
  sb.write_string(
    "  --float-mode hfp|ieee  COMP-1/COMP-2 的字节解释方式(默认 hfp)\n",
  )
  sb.write_string(
    "                         hfp  = IBM z/OS 主机十六进制浮点(base-16 指数)\n",
  )
  sb.write_string(
    "                         ieee = IEEE 754 binary32/binary64(GnuCOBOL/x86)\n",
  )
  sb.write_string(
    "                         两者字节布局互不兼容,必须先确定数据源属于哪一种\n",
  )
  sb.write_string("\n退出码: 0 成功 / 1 数据错误 / 2 用法错误\n")
  sb.to_string()
}

///|
/// 解析后的命令行选项。
priv struct Options {
  mut copybook : String?
  mut data : String?
  mut json : String?
  mut out : String?
  mut format : String
  mut record_length : Int32?
  /// COMP-1 / COMP-2 的字节解释方式(`hfp` 默认 / `ieee`)。
  ///
  /// 这不是"精度选项",而是**数据源属性**:IBM z/OS 写 HFP、
  /// GnuCOBOL / Micro Focus 写 IEEE 754,同一份 copybook 两种字节布局完全
  /// 互不兼容。默认 `hfp`(本项目目标平台)。见 `--float-mode`。
  mut float_mode : @lib.FloatMode
}

///|
/// 逐个 token 解析 `--key value` 形式。
///
/// 支持 `--key=value` 与 `--key value` 两种写法(后者更常见于手敲)。
/// 未知选项直接报错 —— 静默忽略会让 "打错一个字就用了默认值" 变得不可见。
fn parse_options(args : ArrayView[String]) -> Result[Options, String] {
  let opts : Options = {
    copybook: None,
    data: None,
    json: None,
    out: None,
    format: "jsonl",
    record_length: None,
    float_mode: @lib.Hfp,
  }
  let mut i = 0
  let n = args.length()
  while i < n {
    let tok = args[i]
    // 拆 --key=value
    let mut key = tok
    let mut inline_value : String? = None
    match tok.find("=") {
      Some(eq) => {
        key = tok[:eq].to_owned()
        inline_value = Some(tok[eq + 1:].to_owned())
      }
      None => ()
    }
    // 取值辅助:有 inline 用 inline,否则吃下一个 token
    let take_value = fn() -> Result[String, String] {
      match inline_value {
        Some(v) => Ok(v)
        None =>
          if i + 1 >= n {
            Err("option \{key} requires a value")
          } else {
            i = i + 1
            Ok(args[i])
          }
      }
    }
    match key {
      "--copybook" =>
        match take_value() {
          Ok(v) => opts.copybook = Some(v)
          Err(e) => return Err(e)
        }
      "--data" =>
        match take_value() {
          Ok(v) => opts.data = Some(v)
          Err(e) => return Err(e)
        }
      "--json" =>
        match take_value() {
          Ok(v) => opts.json = Some(v)
          Err(e) => return Err(e)
        }
      "--out" =>
        match take_value() {
          Ok(v) => opts.out = Some(v)
          Err(e) => return Err(e)
        }
      "--format" =>
        match take_value() {
          Ok(v) => opts.format = v
          Err(e) => return Err(e)
        }
      "--record-length" =>
        match take_value() {
          Ok(v) =>
            match parse_positive_int(v) {
              Some(n) => opts.record_length = Some(n)
              None =>
                return Err(
                  "--record-length expects a positive integer, got '\{v}'",
                )
            }
          Err(e) => return Err(e)
        }
      "--float-mode" =>
        match take_value() {
          Ok(v) =>
            match v.to_lower() {
              "hfp" | "ibm" | "hex" => opts.float_mode = @lib.Hfp
              "ieee" | "ieee754" | "binary" => opts.float_mode = @lib.Ieee754
              other =>
                return Err(
                  "--float-mode expects 'hfp' (IBM 主机, 默认) or 'ieee' (GnuCOBOL/x86), got '\{other}'",
                )
            }
          Err(e) => return Err(e)
        }
      _ => return Err("unknown option '\{key}'")
    }
    i = i + 1
  }
  Ok(opts)
}

///|
/// 解析正整数(只用 ASCII 数字,拒绝空串/负数/杂字符)。
fn parse_positive_int(s : StringView) -> Int32? {
  let n = s.length()
  if n == 0 {
    return None
  }
  let mut v = 0
  for i = 0; i < n; i = i + 1 {
    let c = s[i]
    if c < '0' || c > '9' {
      return None
    }
    v = v * 10 + (c.to_int() - '0'.to_int())
  }
  if v <= 0 {
    None
  } else {
    Some(v)
  }
}

///|
/// 从文件读 copybook 并编译布局。
fn load_layout(
  path : String,
  float_mode : @lib.FloatMode,
) -> Result[@lib.RecordLayout, String] {
  let text = match read_text_file(path) {
    Some(t) => t
    None => return Err("cannot read copybook '\{path}'")
  }
  let specs = match compile_specs(text) {
    Ok(s) => s
    Err(msg) => return Err("copybook parse error: \{msg}")
  }
  match try_compile(specs, float_mode) {
    Ok(l) => Ok(l)
    Err(msg) => Err("layout error: \{msg}")
  }
}

///|
/// 分隔 try 的边界:@lib.parse_copybook raise @lib.ParseError
fn compile_specs(text : String) -> Result[Array[@lib.FieldSpec], String] {
  match try_parse(text) {
    Ok(s) => Ok(s)
    Err(msg) => Err(msg)
  }
}

///|
fn try_parse(text : String) -> Result[Array[@lib.FieldSpec], String] {
  let r : Result[Array[@lib.FieldSpec], @lib.ParseError] = try_parse_raw(text)
  match r {
    Ok(s) => Ok(s)
    Err(e) => Err(e.to_display())
  }
}

///|
fn try_parse_raw(
  text : String,
) -> Result[Array[@lib.FieldSpec], @lib.ParseError] {
  try @lib.parse_copybook(text) catch {
    e => Err(e)
  } noraise {
    v => Ok(v)
  }
}

///|
fn try_compile(
  specs : Array[@lib.FieldSpec],
  float_mode : @lib.FloatMode,
) -> Result[@lib.RecordLayout, String] {
  try @lib.compile_layout_with_float_mode(specs, float_mode) catch {
    e => Err(e.to_display())
  } noraise {
    v => Ok(v)
  }
}

///|
/// 读文本文件(UTF-8)。失败返回 `None`(调用方决定退出码)。
fn read_text_file(path : String) -> String? {
  try Some(@fs.read_file_to_string(path)) catch {
    _ => None
  } noraise {
    v => v
  }
}

///|
/// 读二进制文件。
fn read_binary_file(path : String) -> Bytes? {
  try Some(@fs.read_file_to_bytes(path)) catch {
    _ => None
  } noraise {
    v => v
  }
}

///|
/// 子命令:layout
fn cmd_layout(opts : Options) -> Int32 {
  let cpy = match opts.copybook {
    Some(p) => p
    None => {
      eprintln("layout: --copybook is required")
      return EXIT_USAGE
    }
  }
  let layout = match load_layout(cpy, opts.float_mode) {
    Ok(l) => l
    Err(msg) => {
      eprintln(msg)
      return EXIT_DATA
    }
  }
  if layout.byte_length == 0 {
    eprintln("copybook has zero record length (no fields)")
    return EXIT_DATA
  }
  println(@lib.render_layout(layout))
  EXIT_OK
}

///|
/// 子命令:decode
fn cmd_decode(opts : Options) -> Int32 {
  let cpy = match opts.copybook {
    Some(p) => p
    None => {
      eprintln("decode: --copybook is required")
      return EXIT_USAGE
    }
  }
  let data_path = match opts.data {
    Some(p) => p
    None => {
      eprintln("decode: --data is required")
      return EXIT_USAGE
    }
  }
  // 先校验 format,避免读完文件才发现拼错
  let as_table = match opts.format {
    "jsonl" => false
    "table" => true
    other => {
      eprintln("decode: unknown --format '\{other}' (expected jsonl or table)")
      return EXIT_USAGE
    }
  }
  let layout = match load_layout(cpy, opts.float_mode) {
    Ok(l) => l
    Err(msg) => {
      eprintln(msg)
      return EXIT_DATA
    }
  }
  if layout.byte_length == 0 {
    eprintln("copybook has zero record length (no fields)")
    return EXIT_DATA
  }
  let data = match read_binary_file(data_path) {
    Some(b) => b
    None => {
      eprintln("decode: cannot read data file '\{data_path}'")
      return EXIT_DATA
    }
  }
  let rec_len = match opts.record_length {
    Some(n) => n
    None => layout.byte_length
  }
  if rec_len < layout.byte_length {
    eprintln(
      "decode: --record-length \{rec_len} is smaller than the layout length \{layout.byte_length}",
    )
    return EXIT_USAGE
  }
  // --record-length 大于布局长度:每条记录的尾部(rec_len - 布局长度)字节
  // 不属于任何字段,解码跳过、重编码也不会还原。这是 RDW/BDW 头或自定义
  // 填充的合法用法,但静默截尾会让「8 字节数据每条丢 1 字节」无迹可查——
  // 明确警告一次(不改变退出码:解码本身仍逐条成功)。
  if rec_len > layout.byte_length {
    eprintln(
      "warning: --record-length \{rec_len} exceeds the layout length \{layout.byte_length}; the trailing \{rec_len - layout.byte_length} byte(s) of each record are outside the copybook and are skipped on decode (they are NOT restored by encode)",
    )
  }
  // 空文件不是「成功解码了 0 条记录」,而是没有数据可解。
  // 静默返回 0 会让调用方(尤其是脚本)误以为文件正常。
  if data.length() == 0 {
    eprintln(
      "decode: data file '\{data_path}' is empty (expected \{rec_len}-byte records)",
    )
    return EXIT_DATA
  }
  match @lib.decode_records(layout, data, rec_len) {
    Err(e) => {
      eprintln("decode: " + e.to_display())
      EXIT_DATA
    }
    Ok(rows) =>
      if as_table {
        println(@lib.render_table(layout, rows))
        EXIT_OK
      } else {
        // render_jsonl 的每行已自带 \n,println 会再补一个换行 →
        // 5 条记录导出成 6 行(尾部多一个空行)。去掉尾部的单个换行
        // 再 println,保证恰好 N 条记录 N 行。
        let text = @lib.render_jsonl(layout, rows)
        println(if text.length() > 0 { text[:text.length() - 1] } else { text })
        EXIT_OK
      }
  }
}

///|
/// 子命令:encode
fn cmd_encode(opts : Options) -> Int32 {
  let cpy = match opts.copybook {
    Some(p) => p
    None => {
      eprintln("encode: --copybook is required")
      return EXIT_USAGE
    }
  }
  let json_path = match opts.json {
    Some(p) => p
    None => {
      eprintln("encode: --json is required")
      return EXIT_USAGE
    }
  }
  let out_path = match opts.out {
    Some(p) => p
    None => {
      eprintln("encode: --out is required")
      return EXIT_USAGE
    }
  }
  let layout = match load_layout(cpy, opts.float_mode) {
    Ok(l) => l
    Err(msg) => {
      eprintln(msg)
      return EXIT_DATA
    }
  }
  if layout.byte_length == 0 {
    eprintln("copybook has zero record length (no fields)")
    return EXIT_DATA
  }
  let text = match read_text_file(json_path) {
    Some(t) => t
    None => {
      eprintln("encode: cannot read json file '\{json_path}'")
      return EXIT_DATA
    }
  }
  // 逐行解析 JSONL → Map[路径, @lib.CellValue]
  let rows : Array[Map[String, @lib.CellValue]] = []
  let lines = text.split("\n").collect()
  for i = 0; i < lines.length(); i = i + 1 {
    let line = lines[i].to_owned()
    let trimmed = line.trim(chars=" \t\r").to_owned()
    if trimmed.length() == 0 {
      continue
    }
    match @lib.parse_jsonl_row(layout, trimmed) {
      Ok(m) => rows.push(m)
      Err(msg) => {
        eprintln("encode: line \{i + 1}: \{msg}")
        return EXIT_DATA
      }
    }
  }
  // 空 JSONL 与 decode 的空数据文件对称:写 0 字节并报成功会让脚本
  // 误以为转换完成,实际什么都没发生。
  if rows.length() == 0 {
    eprintln("encode: input contains no records")
    return EXIT_DATA
  }
  let bytes = match @lib.encode_records(layout, rows) {
    Ok(b) => b
    Err(e) => {
      eprintln("encode: " + e.to_display())
      return EXIT_DATA
    }
  }
  match write_binary_file(out_path, bytes) {
    Ok(_) => {
      eprintln(
        "encoded \{rows.length()} record(s), \{bytes.length()} bytes -> \{out_path}",
      )
      EXIT_OK
    }
    Err(msg) => {
      eprintln("encode: \{msg}")
      EXIT_DATA
    }
  }
}

///|
/// 子命令:verify(往返自检)
fn cmd_verify(opts : Options) -> Int32 {
  let cpy = match opts.copybook {
    Some(p) => p
    None => {
      eprintln("verify: --copybook is required")
      return EXIT_USAGE
    }
  }
  let data_path = match opts.data {
    Some(p) => p
    None => {
      eprintln("verify: --data is required")
      return EXIT_USAGE
    }
  }
  let layout = match load_layout(cpy, opts.float_mode) {
    Ok(l) => l
    Err(msg) => {
      eprintln(msg)
      return EXIT_DATA
    }
  }
  if layout.byte_length == 0 {
    eprintln("copybook has zero record length (no fields)")
    return EXIT_DATA
  }
  let data = match read_binary_file(data_path) {
    Some(b) => b
    None => {
      eprintln("verify: cannot read data file '\{data_path}'")
      return EXIT_DATA
    }
  }
  let rec_len = layout.byte_length
  // 零长度布局(如只有 `01 EMPTY.`)没有任何字段,除零会以运行时栈崩溃的
  // 方式暴露。空布局意味着「没有任何数据契约」,verify 对它无事可做。
  if rec_len == 0 {
    eprintln("verify: layout has zero length (no fields); nothing to verify")
    return EXIT_DATA
  }
  // 空文件不是「成功校验了 0 条记录」,而是没有数据可校验——与 decode
  // 的空数据分支(main.mbt decode 侧)同一措辞与退出码,避免脚本把
  // 「verify: 0 record(s) OK」误读为文件正常。
  if data.length() == 0 {
    eprintln(
      "verify: data file '\{data_path}' is empty (expected \{rec_len}-byte records)",
    )
    return EXIT_DATA
  }
  if data.length() % rec_len != 0 {
    eprintln(
      "verify: data length \{data.length()} is not a multiple of record length \{rec_len}",
    )
    return EXIT_DATA
  }
  let mut checked = 0
  let mut pos = 0
  while pos < data.length() {
    let rec = data[pos:pos + rec_len]
    match @lib.roundtrip_check(layout, rec) {
      Err(e) => {
        // CB-19:decode 与 encode 的失败含义不同,措辞必须区分 ——
        // 编码失败说明「数据解得出来,但写不回去」,把它说成 decode failed
        // 会把排查方向指错。EncodeFailed 自带的文案已含 "encode failed"。
        match e {
          @lib.EncodeFailed(_) =>
            eprintln("verify: record \{checked}: " + e.to_display())
          _ =>
            eprintln(
              "verify: record \{checked}: decode failed: " + e.to_display(),
            )
        }
        return EXIT_DATA
      }
      Ok(Some(off)) => {
        eprintln("verify: record \{checked}: byte mismatch at offset \{off}")
        return EXIT_DATA
      }
      Ok(None) => ()
    }
    checked = checked + 1
    pos = pos + rec_len
  }
  eprintln("verify: \{checked} record(s) OK, byte-identical roundtrip")
  EXIT_OK
}

///|
fn write_binary_file(path : String, bytes : Bytes) -> Result[Unit, String] {
  let r = try_write(path, bytes)
  match r {
    Ok(_) => Ok(())
    Err(_) => Err("cannot write output file")
  }
}

///|
fn try_write(path : String, bytes : Bytes) -> Result[Unit, @fs.IOError] {
  try {
    @fs.write_bytes_to_file(path, bytes)
    Ok(())
  } catch {
    e => Err(e)
  } noraise {
    v => v
  }
}

///|
fn main {
  let args = @env.args()
  // args[0] 是可执行文件路径
  if args.length() < 2 {
    eprintln(usage_text())
    @sys.exit(EXIT_USAGE)
  }
  let sub = args[1]
  if sub == "--help" || sub == "-h" || sub == "help" {
    println(usage_text())
    @sys.exit(EXIT_OK)
  }
  let rest = args[2:]
  let opts = match parse_options(rest) {
    Ok(o) => o
    Err(msg) => {
      eprintln("error: \{msg}")
      eprintln(usage_text())
      @sys.exit(EXIT_USAGE)
      Options::{
        copybook: None,
        data: None,
        json: None,
        out: None,
        format: "",
        record_length: None,
        float_mode: @lib.Hfp,
      }
    }
  }
  let code = match sub {
    "layout" => cmd_layout(opts)
    "decode" => cmd_decode(opts)
    "encode" => cmd_encode(opts)
    "verify" => cmd_verify(opts)
    other => {
      eprintln("error: unknown subcommand '\{other}'")
      eprintln(usage_text())
      EXIT_USAGE
    }
  }
  @sys.exit(code)
}