///|
fn usage() -> Unit {
  println("moondelta-core 0.3.0 — MoonBit Delta Lake table-format kernel")
  println("")
  println("Usage:")
  println("  delta inspect ")
  println("  delta history ")
  println(
    "  delta scan     [--partition k=v]... [--where col>=number]",
  )
  println("  delta read     [--limit N]")
  println(
    "  delta create    [--partition k1,k2]",
  )
  println("  delta version")
}

///|
fn tail(args : Array[String], start : Int32) -> Array[String] {
  let out : Array[String] = []
  let mut i = start
  while i < args.length() {
    out.push(args[i])
    i = i + 1
  }
  out
}

///|
fn join(items : Array[String], sep : String) -> String {
  let sb = StringBuilder()
  let mut i = 0
  while i < items.length() {
    if i > 0 {
      sb.write_string(sep)
    }
    sb.write_string(items[i])
    i = i + 1
  }
  sb.to_string()
}

///|
/// 错误输出到 stderr;配合各 cmd_* 返回 false → main 以非 0 退出码结束
fn fail(message : String) -> Unit {
  eprintln("error: " + message)
}

///|
fn cmd_inspect(root : String) -> Bool {
  let table = @delta.DeltaTable::open(root) catch {
    err => {
      fail("\{err}")
      return false
    }
  }
  let inspected = table.inspect() catch {
    err => {
      fail("\{err}")
      return false
    }
  }
  let (snapshot, checkpoint, has_checkpoint_file) = inspected
  let versions = table.versions() catch {
    err => {
      fail("\{err}")
      return false
    }
  }
  println("table            : " + root)
  println(
    "current version  : " +
    snapshot.version.to_string() +
    "  (log versions: " +
    versions.length().to_string() +
    ")",
  )
  println(
    "files            : " +
    snapshot.files.length().to_string() +
    "  rows: " +
    snapshot.total_rows.to_string() +
    "  bytes: " +
    snapshot.total_bytes.to_string(),
  )
  println(
    "protocol         : reader=" +
    snapshot.protocol.min_reader_version.to_string() +
    " writer=" +
    snapshot.protocol.min_writer_version.to_string(),
  )
  println("format           : " + snapshot.metadata.format_provider)
  let partitions = if snapshot.metadata.partition_columns.length() == 0 {
    "(none)"
  } else {
    join(snapshot.metadata.partition_columns, ", ")
  }
  println("partition columns: " + partitions)
  println("schema:")
  for f in snapshot.metadata.schema.fields {
    let nn = if f.nullable { "" } else { " (not null)" }
    println("  " + f.name + ": " + @delta.delta_type_name(f.type_) + nn)
  }
  match checkpoint {
    Some(cp) => println("last checkpoint  : version=" + cp.version.to_string())
    None => ()
  }
  if has_checkpoint_file {
    println(
      "note             : checkpoint present; truncated / checkpoint-only tables are recovered from the latest single-file checkpoint",
    )
  }
  if snapshot.ignored_actions.length() > 0 {
    println(
      "note             : ignored (snapshot-irrelevant) actions in log: " +
      join(snapshot.ignored_actions, ", "),
    )
  }
  true
}

///|
fn cmd_history(root : String) -> Bool {
  let table = @delta.DeltaTable::open(root) catch {
    err => {
      fail("\{err}")
      return false
    }
  }
  let history = table.history() catch {
    err => {
      fail("\{err}")
      return false
    }
  }
  println("version | timestamp     | operation")
  println("--------+---------------+----------")
  for h in history {
    let ts = match h.timestamp {
      Some(t) => t.to_string()
      None => "-"
    }
    let op = match h.operation {
      Some(o) => o
      None => "-"
    }
    println(h.version.to_string() + " | " + ts + " | " + op)
  }
  true
}

///|
fn parse_where(text : String) -> @delta.ColumnPredicate? {
  let cleaned = text.replace_all(old=" ", new="")
  let ops : Array[(String, @delta.CompareOp)] = [
    (">=", GeV),
    ("<=", LeV),
    (">", GtV),
    ("<", LtV),
    ("=", EqV),
  ]
  for pair in ops {
    let (op_text, op) = pair
    match cleaned.split_once(op_text) {
      Some(split) => {
        let (left, right) = split
        let value = @string.parse_double(right) catch {
          _ => {
            fail("invalid number in --where: " + text)
            return None
          }
        }
        if left.char_length() == 0 {
          fail("invalid --where expression, missing column: " + text)
          return None
        }
        return Some({ column: left.to_owned(), op, value, })
      }
      None => ()
    }
  }
  fail("invalid --where expression, expected like: id>=60 (no spaces)")
  None
}

///|
/// 解析 scan 的选项。返回 None 表示参数错误(已输出错误信息)。
fn parse_scan_options(rest : Array[String]) -> (@delta.ScanFilter)? {
  let partitions : Array[(String, String)] = []
  let mut predicate : @delta.ColumnPredicate? = None
  let mut i = 0
  while i < rest.length() {
    if rest[i] == "--partition" {
      if i + 1 >= rest.length() {
        fail("--partition requires a k=v value")
        return None
      }
      match rest[i + 1].split_once("=") {
        Some(split) => {
          let (k, v) = split
          partitions.push((k.to_owned(), v.to_owned()))
        }
        None => {
          fail("invalid --partition, expected k=v")
          return None
        }
      }
      i = i + 2
    } else if rest[i] == "--where" {
      if i + 1 >= rest.length() {
        fail("--where requires an expression")
        return None
      }
      match parse_where(rest[i + 1]) {
        Some(pred) => predicate = Some(pred)
        None => return None
      }
      i = i + 2
    } else {
      fail("unknown argument '\{rest[i]}'")
      return None
    }
  }
  Some({ partitions, predicate, })
}

///|
fn cmd_scan(root : String, rest : Array[String]) -> Bool {
  let table = @delta.DeltaTable::open(root) catch {
    err => {
      fail("\{err}")
      return false
    }
  }
  let snapshot = table.snapshot() catch {
    err => {
      fail("\{err}")
      return false
    }
  }
  let filter = match parse_scan_options(rest) {
    Some(f) => f
    None => return false
  }
  let report = @delta.scan(snapshot, filter) catch {
    err => {
      fail("\{err}")
      return false
    }
  }
  println(
    "table            : " +
    root +
    " (version " +
    snapshot.version.to_string() +
    ")",
  )
  println("total files      : " + snapshot.files.length().to_string())
  println("selected files   : " + report.files.length().to_string())
  println("skipped partition: " + report.skipped_partition.to_string())
  println("skipped stats    : " + report.skipped_stats.to_string())
  println("matched rows     : " + report.matched_rows.to_string())
  println("matched bytes    : " + report.matched_bytes.to_string())
  let mut printed = 0
  for f in report.files {
    if printed >= 5 {
      break
    }
    println("  - " + f.path)
    printed = printed + 1
  }
  true
}

///|
fn cmd_read(root : String, rest : Array[String]) -> Bool {
  let table = @delta.DeltaTable::open(root) catch {
    err => {
      fail("\{err}")
      return false
    }
  }
  let snapshot = table.snapshot() catch {
    err => {
      fail("\{err}")
      return false
    }
  }
  let mut limit : Int32? = Some(10)
  let mut i = 0
  while i < rest.length() {
    if rest[i] == "--limit" {
      if i + 1 >= rest.length() {
        fail("--limit requires a number")
        return false
      }
      limit = Some(
        @string.parse_int(rest[i + 1], base=10) catch {
          _ => {
            fail("invalid --limit")
            return false
          }
        },
      )
      i = i + 2
    } else {
      fail("unknown argument '\{rest[i]}'")
      return false
    }
  }
  let result = @delta.read_snapshot(root, snapshot, limit) catch {
    err => {
      fail("\{err}")
      return false
    }
  }
  println(
    "read " +
    result.files_read.to_string() +
    " file(s), " +
    result.row_count.to_string() +
    " row(s), version " +
    snapshot.version.to_string(),
  )
  let header : Array[String] = []
  for c in result.columns {
    header.push(c.name + " (" + @delta.delta_type_name(c.type_) + ")")
  }
  println(join(header, " | "))
  let mut r = 0
  while r < result.row_count && r < 10 {
    let cells : Array[String] = []
    for c in result.columns {
      cells.push(@delta.cell_to_display(c.cells[r]))
    }
    println(join(cells, " | "))
    r = r + 1
  }
  if result.row_count > 10 {
    println("... (" + (result.row_count - 10).to_string() + " more rows)")
  }
  true
}

///|
fn cmd_create(root : String, rest : Array[String]) -> Bool {
  if rest.length() < 1 {
    usage()
    return false
  }
  let schema_arg = rest[0]
  let schema_json = if @fs.path_exists(schema_arg) {
    @delta.read_text(schema_arg) catch {
      err => {
        fail("\{err}")
        return false
      }
    }
  } else {
    schema_arg
  }
  let partitions : Array[String] = []
  let mut i = 1
  while i < rest.length() {
    if rest[i] == "--partition" {
      if i + 1 >= rest.length() {
        fail("--partition requires a comma-separated list")
        return false
      }
      for p in rest[i + 1].split(",") {
        partitions.push(p.to_owned())
      }
      i = i + 2
    } else {
      fail("unknown argument '\{rest[i]}'")
      return false
    }
  }
  let version = @delta.create_table(root, schema_json, partitions) catch {
    err => {
      fail("\{err}")
      return false
    }
  }
  println(
    "created Delta table at " + root + " (version " + version.to_string() + ")",
  )
  true
}

///|
fn main {
  let args = @env.args()
  if args.length() < 2 {
    usage()
    @sys.exit(1)
    return
  }
  let cmd = args[1]
  let rest = tail(args, 2)
  let ok = if cmd == "version" {
    println("moondelta-core 0.3.0")
    true
  } else if cmd == "inspect" {
    if rest.length() < 1 {
      usage()
      false
    } else {
      cmd_inspect(rest[0])
    }
  } else if cmd == "history" {
    if rest.length() < 1 {
      usage()
      false
    } else {
      cmd_history(rest[0])
    }
  } else if cmd == "scan" {
    if rest.length() < 1 {
      usage()
      false
    } else {
      cmd_scan(rest[0], tail(rest, 1))
    }
  } else if cmd == "read" {
    if rest.length() < 1 {
      usage()
      false
    } else {
      cmd_read(rest[0], tail(rest, 1))
    }
  } else if cmd == "create" {
    if rest.length() < 2 {
      usage()
      false
    } else {
      cmd_create(rest[0], tail(rest, 1))
    }
  } else {
    usage()
    false
  }
  if not(ok) {
    @sys.exit(1)
  }
}