///|
fn usage() -> String {
  (
    #|moon-lot-recall: offline food-lot recall impact tracing
    #|
    #|Usage:
    #|  moon run cmd/main --target js -- validate LEDGER.json
    #|  moon run cmd/main --target js -- analyze LEDGER.json RECALL.json [json|markdown|csv]
    #|  moon run cmd/main --target js -- actions LEDGER.json RECALL.json
    #|  moon run cmd/main --target js -- inventory LEDGER.json AS_OF
    #|  moon run cmd/main --target js -- upstream LEDGER.json LOT_ID AS_OF
    #|  moon run cmd/main --target js -- reconcile LEDGER.json COUNTS.csv AS_OF [json|markdown]
    #|  moon run cmd/main --target js -- consignees LEDGER.json RECALL.json [json|markdown|csv]
    #|  moon run cmd/main --target js -- responses LEDGER.json RECALL.json EVENTS.csv [json|markdown]
    #|  moon run cmd/main --target js -- delta LEDGER.json RECALL.json NEW_AS_OF [json|markdown]
    #|  moon run cmd/main --target js -- readiness LEDGER.json RECALL.json COUNTS.csv EVENTS.csv [json|markdown]
    #|  moon run cmd/main --target js -- import-csv LOTS.csv TRANSFORMATIONS.csv SHIPMENTS.csv OUT.json
    #|
    #|All timestamps are Unix seconds; all quantities are integer grams.
    #|An impact path indicates possible exposure, not proven contamination.
    #|
  )
}

///|
fn fail_cli(message : String) -> Unit {
  println("ERROR: \{message}")
  @sys.exit(2)
}

///|
fn read_text(path : String) -> String? {
  let text = @fs.read_file_to_string(path) catch {
    error => {
      fail_cli("cannot read \{path}: \{Repr(error)}")
      return None
    }
  }
  Some(text)
}

///|
fn read_ledger(path : String) -> @lib.Ledger? {
  let text = match read_text(path) {
    None => return None
    Some(value) => value
  }
  match @lib.decode_ledger(text) {
    Ok(ledger) => Some(ledger)
    Err(error) => {
      fail_cli("\{error.code}: \{error.message}")
      None
    }
  }
}

///|
fn read_request(path : String) -> @lib.RecallRequest? {
  let text = match read_text(path) {
    None => return None
    Some(value) => value
  }
  match @lib.decode_request(text) {
    Ok(request) => Some(request)
    Err(error) => {
      fail_cli("\{error.code}: \{error.message}")
      None
    }
  }
}

///|
fn parse_time(text : String) -> Int64? {
  let value = @string.parse_int64(text) catch {
    _ => {
      fail_cli("AS_OF must be an integer Unix timestamp")
      return None
    }
  }
  Some(value)
}

///|
fn run_validate(args : Array[String]) -> Unit {
  if args.length() != 2 {
    fail_cli("validate requires LEDGER.json")
    return
  }
  match read_ledger(args[1]) {
    None => ()
    Some(ledger) => {
      let validation = @lib.validate_ledger(ledger)
      println(@lib.render_validation(validation))
      if !validation.valid {
        @sys.exit(2)
      }
    }
  }
}

///|
fn run_analyze(args : Array[String]) -> Unit {
  if args.length() < 3 || args.length() > 4 {
    fail_cli("analyze requires LEDGER.json RECALL.json [json|markdown|csv]")
    return
  }
  let ledger = match read_ledger(args[1]) {
    None => return
    Some(value) => value
  }
  let request = match read_request(args[2]) {
    None => return
    Some(value) => value
  }
  let format = if args.length() == 4 { args[3] } else { "markdown" }
  match @lib.analyze(ledger, request) {
    Err(validation) => {
      println(@lib.render_validation(validation))
      @sys.exit(2)
    }
    Ok(report) =>
      match format {
        "json" => println(@lib.encode_report(report))
        "markdown" => println(@lib.render_markdown(report))
        "csv" => println(@lib.render_shipments_csv(report))
        _ => fail_cli("format must be json, markdown, or csv")
      }
  }
}

///|
fn run_actions(args : Array[String]) -> Unit {
  if args.length() != 3 {
    fail_cli("actions requires LEDGER.json RECALL.json")
    return
  }
  let ledger = match read_ledger(args[1]) {
    None => return
    Some(value) => value
  }
  let request = match read_request(args[2]) {
    None => return
    Some(value) => value
  }
  match @lib.plan_actions(ledger, request) {
    Err(validation) => {
      println(@lib.render_validation(validation))
      @sys.exit(2)
    }
    Ok(plan) => println(@lib.render_actions_markdown(plan))
  }
}

///|
fn run_inventory(args : Array[String]) -> Unit {
  if args.length() != 3 {
    fail_cli("inventory requires LEDGER.json AS_OF")
    return
  }
  let ledger = match read_ledger(args[1]) {
    None => return
    Some(value) => value
  }
  let as_of = match parse_time(args[2]) {
    None => return
    Some(value) => value
  }
  match @lib.inventory_at(ledger, as_of) {
    Err(validation) => {
      println(@lib.render_validation(validation))
      @sys.exit(2)
    }
    Ok(report) => println(@json.to_json(report).stringify(indent=2))
  }
}

///|
fn run_upstream(args : Array[String]) -> Unit {
  if args.length() != 4 {
    fail_cli("upstream requires LEDGER.json LOT_ID AS_OF")
    return
  }
  let ledger = match read_ledger(args[1]) {
    None => return
    Some(value) => value
  }
  let as_of = match parse_time(args[3]) {
    None => return
    Some(value) => value
  }
  match @lib.trace_upstream(ledger, args[2], as_of) {
    Err(validation) => {
      println(@lib.render_validation(validation))
      @sys.exit(2)
    }
    Ok(report) => println(@lib.render_upstream_markdown(report))
  }
}

///|
fn run_import_csv(args : Array[String]) -> Unit {
  if args.length() != 5 {
    fail_cli(
      "import-csv requires LOTS.csv TRANSFORMATIONS.csv SHIPMENTS.csv OUT.json",
    )
    return
  }
  let lots = match read_text(args[1]) {
    None => return
    Some(value) => value
  }
  let transformations = match read_text(args[2]) {
    None => return
    Some(value) => value
  }
  let shipments = match read_text(args[3]) {
    None => return
    Some(value) => value
  }
  match @lib.import_csv(lots, transformations, shipments) {
    Err(validation) => {
      println(@lib.render_validation(validation))
      @sys.exit(2)
    }
    Ok(ledger) => {
      @fs.write_string_to_file(args[4], @lib.encode_ledger(ledger)) catch {
        error => fail_cli("cannot write \{args[4]}: \{Repr(error)}")
      }
      println("Wrote validated ledger to \{args[4]}")
    }
  }
}

///|
fn run_reconcile(args : Array[String]) -> Unit {
  if args.length() < 4 || args.length() > 5 {
    fail_cli("reconcile requires LEDGER.json COUNTS.csv AS_OF [json|markdown]")
    return
  }
  let ledger = match read_ledger(args[1]) {
    None => return
    Some(value) => value
  }
  let text = match read_text(args[2]) {
    None => return
    Some(value) => value
  }
  let cutoff = match parse_time(args[3]) {
    None => return
    Some(value) => value
  }
  let format = if args.length() == 5 { args[4] } else { "markdown" }
  let counts = match @lib.parse_stock_counts_csv(text) {
    Err(validation) => {
      println(@lib.render_validation(validation))
      @sys.exit(2)
      return
    }
    Ok(value) => value
  }
  match @lib.reconcile_stock(ledger, counts, cutoff) {
    Err(validation) => {
      println(@lib.render_validation(validation))
      @sys.exit(2)
    }
    Ok(report) =>
      match format {
        "json" => println(@json.to_json(report).stringify(indent=2))
        "markdown" => println(@lib.render_reconciliation_markdown(report))
        _ => fail_cli("format must be json or markdown")
      }
  }
}

///|
fn run_consignees(args : Array[String]) -> Unit {
  if args.length() < 3 || args.length() > 4 {
    fail_cli("consignees requires LEDGER.json RECALL.json [json|markdown|csv]")
    return
  }
  let ledger = match read_ledger(args[1]) {
    None => return
    Some(value) => value
  }
  let request = match read_request(args[2]) {
    None => return
    Some(value) => value
  }
  let format = if args.length() == 4 { args[3] } else { "markdown" }
  match @lib.group_consignees(ledger, request) {
    Err(validation) => {
      println(@lib.render_validation(validation))
      @sys.exit(2)
    }
    Ok(report) =>
      match format {
        "json" => println(@json.to_json(report).stringify(indent=2))
        "markdown" => println(@lib.render_consignees_markdown(report))
        "csv" => println(@lib.render_consignees_csv(report))
        _ => fail_cli("format must be json, markdown, or csv")
      }
  }
}

///|
fn run_responses(args : Array[String]) -> Unit {
  if args.length() < 4 || args.length() > 5 {
    fail_cli(
      "responses requires LEDGER.json RECALL.json EVENTS.csv [json|markdown]",
    )
    return
  }
  let ledger = match read_ledger(args[1]) {
    None => return
    Some(value) => value
  }
  let request = match read_request(args[2]) {
    None => return
    Some(value) => value
  }
  let source = match read_text(args[3]) {
    None => return
    Some(value) => value
  }
  let format = if args.length() == 5 { args[4] } else { "markdown" }
  let events = match @lib.parse_response_events_csv(source) {
    Err(validation) => {
      println(@lib.render_validation(validation))
      @sys.exit(2)
      return
    }
    Ok(value) => value
  }
  match @lib.review_responses(ledger, request, events) {
    Err(validation) => {
      println(@lib.render_validation(validation))
      @sys.exit(2)
    }
    Ok(report) =>
      match format {
        "json" => println(@json.to_json(report).stringify(indent=2))
        "markdown" => println(@lib.render_responses_markdown(report))
        _ => fail_cli("format must be json or markdown")
      }
  }
}

///|
fn run_delta(args : Array[String]) -> Unit {
  if args.length() < 4 || args.length() > 5 {
    fail_cli("delta requires LEDGER.json RECALL.json NEW_AS_OF [json|markdown]")
    return
  }
  let ledger = match read_ledger(args[1]) {
    None => return
    Some(value) => value
  }
  let request = match read_request(args[2]) {
    None => return
    Some(value) => value
  }
  let cutoff = match parse_time(args[3]) {
    None => return
    Some(value) => value
  }
  let format = if args.length() == 5 { args[4] } else { "markdown" }
  match @lib.compare_cutoffs(ledger, request, cutoff) {
    Err(validation) => {
      println(@lib.render_validation(validation))
      @sys.exit(2)
    }
    Ok(delta) =>
      match format {
        "json" => println(@json.to_json(delta).stringify(indent=2))
        "markdown" => println(@lib.render_delta_markdown(delta))
        _ => fail_cli("format must be json or markdown")
      }
  }
}

///|
fn run_readiness(args : Array[String]) -> Unit {
  if args.length() < 5 || args.length() > 6 {
    fail_cli(
      "readiness requires LEDGER.json RECALL.json COUNTS.csv EVENTS.csv [json|markdown]",
    )
    return
  }
  let ledger = match read_ledger(args[1]) {
    None => return
    Some(value) => value
  }
  let request = match read_request(args[2]) {
    None => return
    Some(value) => value
  }
  let count_text = match read_text(args[3]) {
    None => return
    Some(value) => value
  }
  let event_text = match read_text(args[4]) {
    None => return
    Some(value) => value
  }
  let counts = match @lib.parse_stock_counts_csv(count_text) {
    Err(validation) => {
      println(@lib.render_validation(validation))
      @sys.exit(2)
      return
    }
    Ok(value) => value
  }
  let events = match @lib.parse_response_events_csv(event_text) {
    Err(validation) => {
      println(@lib.render_validation(validation))
      @sys.exit(2)
      return
    }
    Ok(value) => value
  }
  let format = if args.length() == 6 { args[5] } else { "markdown" }
  match @lib.review_readiness(ledger, request, counts, events) {
    Err(validation) => {
      println(@lib.render_validation(validation))
      @sys.exit(2)
    }
    Ok(report) =>
      match format {
        "json" => println(@json.to_json(report).stringify(indent=2))
        "markdown" => println(@lib.render_readiness_markdown(report))
        _ => fail_cli("format must be json or markdown")
      }
  }
}

///|
fn main {
  // The Node.js backend exposes [node, compiled-script, user-arguments...].
  let raw = @env.args()
  let args = raw[2:].to_owned()
  if args.is_empty() || args[0] == "--help" || args[0] == "help" {
    println(usage())
    return
  }
  match args[0] {
    "validate" => run_validate(args)
    "analyze" => run_analyze(args)
    "actions" => run_actions(args)
    "inventory" => run_inventory(args)
    "upstream" => run_upstream(args)
    "reconcile" => run_reconcile(args)
    "consignees" => run_consignees(args)
    "responses" => run_responses(args)
    "delta" => run_delta(args)
    "readiness" => run_readiness(args)
    "import-csv" => run_import_csv(args)
    _ => {
      println(usage())
      @sys.exit(2)
    }
  }
}