///|
let version : String = "0.3.3"

///|
let about : String =
  #|Parse, validate, query, and format TOML files.
  #|
  #|Every command reads from a file argument when given, and from standard
  #|input otherwise, so it composes in pipelines like `jq`:
  #|
  #|  cat config.toml | toml_cli get .server.host
  #|  echo 'a = 1' | toml_cli check
  #|
  #|Run it from mooncakes.io without installing (the binary is fetched and
  #|cached on first use; pin a version with moonbit-community/toml_cli@):
  #|
  #|  moonx moonbit-community/toml_cli check config.toml
  #|  moonx moonbit-community/toml_cli format config.toml
  #|  moonx moonbit-community/toml_cli tojson config.toml
  #|  moonx moonbit-community/toml_cli get .server.host config.toml
  #|
  #|Exit codes:
  #|  0  success
  #|  1  the input could not be read or is not valid TOML, or the queried
  #|     path does not exist; a human-readable error is printed to stdout
  #|  2  usage error (unknown subcommand or missing required argument)
  #|
  #|`format` prints the normalized document to stdout; it never rewrites the
  #|input file. To format in place, redirect stdout to a temporary file and
  #|move it over the original after checking the exit code.
  #|
  #|`get` prints the value at a dotted path (e.g. `.server.host`, `.ports[0]`);
  #|strings are printed raw, other values in TOML form.

///|
fn toml_command() -> @argparse.Command {
  Command(
    "toml_cli",
    about~,
    version~,
    positionals=[
      PositionArg(
        "file",
        about="Parse TOML and print normalized TOML (default: read stdin).",
        num_args=@argparse.ValueRange(lower=0, upper=1),
      ),
    ],
    subcommands=[
      Command("format", about="Parse TOML and print normalized TOML.", positionals=[
        PositionArg(
          "file",
          about="TOML file to format (default: read stdin).",
          num_args=@argparse.ValueRange(lower=0, upper=1),
        ),
      ]),
      Command("check", about="Validate TOML without printing parsed output.", positionals=[
        PositionArg(
          "file",
          about="TOML file to validate (default: read stdin).",
          num_args=@argparse.ValueRange(lower=0, upper=1),
        ),
      ]),
      Command("tojson", about="Parse TOML and print it as JSON.", positionals=[
        PositionArg(
          "file",
          about="TOML file to convert (default: read stdin).",
          num_args=@argparse.ValueRange(lower=0, upper=1),
        ),
      ]),
      Command(
        "get",
        about="Print the value at a dotted path, e.g. .server.host.",
        positionals=[
          PositionArg(
            "path",
            about="Path expression, e.g. .server.host or .ports[0].",
            num_args=@argparse.ValueRange::single(),
          ),
          PositionArg(
            "file",
            about="TOML file to read (default: read stdin).",
            num_args=@argparse.ValueRange(lower=0, upper=1),
          ),
        ],
      ),
    ],
  )
}

///|
fn main {
  let code = run(get_args()[1:])
  if code != 0 {
    exit(code)
  }
}

///|
fn run(args : ArrayView[String]) -> Int {
  let matches = @argparse.parse(toml_command(), argv=args, env=Map([])) catch {
    err => {
      println(err)
      return 2
    }
  }
  match matches.subcommand {
    Some(("format", child)) => dispatch_source(child, format_source)
    Some(("check", child)) => dispatch_source(child, check_source)
    Some(("tojson", child)) => dispatch_source(child, tojson_source)
    Some(("get", child)) => dispatch_get(child)
    Some((name, _)) => {
      println("error: unsupported command `\{name}`")
      2
    }
    None => dispatch_source(matches, format_source)
  }
}

///|
fn dispatch_source(
  matches : @argparse.Matches,
  action : (String, String) -> Int,
) -> Int {
  match matches.values.get("file") {
    Some([path]) if path != "-" =>
      match read_source_file(path) {
        Some(source) => action(source, path)
        None => 1
      }
    _ => action(read_stdin(), "-")
  }
}

///|
fn dispatch_get(matches : @argparse.Matches) -> Int {
  let path = match matches.values.get("path") {
    Some([p]) => p
    _ => return 2
  }
  match matches.values.get("file") {
    Some([file]) if file != "-" =>
      match read_source_file(file) {
        Some(source) => get_source(source, path)
        None => 1
      }
    _ => get_source(read_stdin(), path)
  }
}

///|
fn fail_parse(label : String, err : Error) -> Int {
  let source = if label == "-" { "input" } else { label }
  println("error: failed to parse \{source}: \{err}")
  1
}

///|
fn get_source(source : String, path : String) -> Int {
  let value = @toml_lib.parse(source) catch {
    err => {
      println("error: failed to parse input: \{err}")
      return 1
    }
  }
  match get_path(value, path) {
    Some(result) => {
      println(render(result))
      0
    }
    None => {
      println("error: no value at path `\{path}`")
      1
    }
  }
}

///|
fn format_source(source : String, label : String) -> Int {
  let value = @toml_lib.parse(source) catch {
    err => return fail_parse(label, err)
  }
  println(value.to_string().trim_end().to_owned())
  0
}

///|
fn check_source(source : String, label : String) -> Int {
  let _ = @toml_lib.parse(source) catch { err => return fail_parse(label, err) }
  if label == "-" {
    println("OK")
  } else {
    println("\{label}: OK")
  }
  0
}

///|
fn tojson_source(source : String, label : String) -> Int {
  let value = @toml_lib.parse(source) catch {
    err => return fail_parse(label, err)
  }
  println(to_json(value).stringify(indent=2))
  0
}