///|
fn usage() -> @argparse.Command {
  @argparse.Command("cron-mbt", about="MoonBit cron parser and scheduler CLI", subcommands=[
    @argparse.Command(
      "parse",
      about="Parse a cron expression and print the normalized fields",
      positionals=[@argparse.PositionArg("expr")],
    ),
    @argparse.Command(
      "next",
      about="Calculate the next trigger time after a timestamp",
      positionals=[
        @argparse.PositionArg("expr"),
        @argparse.PositionArg("year"),
        @argparse.PositionArg("month"),
        @argparse.PositionArg("day"),
        @argparse.PositionArg("hour"),
        @argparse.PositionArg("minute"),
      ],
    ),
    @argparse.Command(
      "match",
      about="Check whether a timestamp matches a cron expression",
      positionals=[
        @argparse.PositionArg("expr"),
        @argparse.PositionArg("year"),
        @argparse.PositionArg("month"),
        @argparse.PositionArg("day"),
        @argparse.PositionArg("hour"),
        @argparse.PositionArg("minute"),
        @argparse.PositionArg("weekday"),
      ],
    ),
    @argparse.Command(
      "explain",
      about="Print a canonical expression and an operational explanation",
      positionals=[@argparse.PositionArg("expr")],
    ),
    @argparse.Command(
      "next-many",
      about="Print a bounded batch of next trigger times",
      positionals=[
        @argparse.PositionArg("expr"),
        @argparse.PositionArg("year"),
        @argparse.PositionArg("month"),
        @argparse.PositionArg("day"),
        @argparse.PositionArg("hour"),
        @argparse.PositionArg("minute"),
        @argparse.PositionArg("count"),
      ],
    ),
    @argparse.Command(
      "previous",
      about="Print the previous trigger time before a timestamp",
      positionals=[
        @argparse.PositionArg("expr"),
        @argparse.PositionArg("year"),
        @argparse.PositionArg("month"),
        @argparse.PositionArg("day"),
        @argparse.PositionArg("hour"),
        @argparse.PositionArg("minute"),
      ],
    ),
    @argparse.Command(
      "validate",
      about="Validate a timestamp before submitting a schedule",
      positionals=[
        @argparse.PositionArg("year"),
        @argparse.PositionArg("month"),
        @argparse.PositionArg("day"),
        @argparse.PositionArg("hour"),
        @argparse.PositionArg("minute"),
      ],
    ),
  ])
}

///|
fn get_required(matches : @argparse.Matches, name : String) -> String {
  match matches.values.get(name) {
    Some(values) => values[0]
    None => panic()
  }
}

///|
fn parse_int_arg(matches : @argparse.Matches, name : String) -> Int {
  @string.parse_int(get_required(matches, name)) catch {
    _ => panic()
  }
}

///|
fn two_digits(value : Int) -> String {
  if value < 10 {
    "0" + value.to_string()
  } else {
    value.to_string()
  }
}

///|
fn print_expr(expr : @cron.CronExpr) -> Unit {
  println("minutes=\{@cron.format_int_list(expr.minutes)}")
  println("hours=\{@cron.format_int_list(expr.hours)}")
  println("days_of_month=\{@cron.format_int_list(expr.days_of_month)}")
  println("months=\{@cron.format_int_list(expr.months)}")
  println("days_of_week=\{@cron.format_int_list(expr.days_of_week)}")
  println("has_last_day_of_month=\{expr.has_last_day_of_month}")
}

///|
fn print_datetime(dt : @cron.DateTime) -> Unit {
  println(
    "\{dt.year}-\{two_digits(dt.month)}-\{two_digits(dt.day)} \{two_digits(dt.hour)}:\{two_digits(dt.minute)}",
  )
}

///|
fn get_datetime(matches : @argparse.Matches) -> @cron.DateTime {
  {
    year: parse_int_arg(matches, "year"),
    month: parse_int_arg(matches, "month"),
    day: parse_int_arg(matches, "day"),
    hour: parse_int_arg(matches, "hour"),
    minute: parse_int_arg(matches, "minute"),
  }
}

///|
fn main {
  let matches = usage().parse() catch { _ => return }
  match matches.subcommand {
    Some(("parse", sub)) => {
      let raw = get_required(sub, "expr")
      match @cron.parse(raw) {
        Ok(expr) => print_expr(expr)
        Err(err) => println(err.to_string())
      }
    }
    Some(("next", sub)) => {
      let raw = get_required(sub, "expr")
      let current = get_datetime(sub)
      match @cron.parse(raw) {
        Ok(expr) =>
          match expr.next_time(current) {
            Ok(next) => print_datetime(next)
            Err(err) => println(err.to_string())
          }
        Err(err) => println(err.to_string())
      }
    }
    Some(("explain", sub)) =>
      match @cron.parse(get_required(sub, "expr")) {
        Ok(expr) => {
          println("canonical=\{expr.canonical()}")
          println("description=\{expr.describe()}")
        }
        Err(err) => println(err.to_string())
      }
    Some(("next-many", sub)) =>
      match @cron.parse(get_required(sub, "expr")) {
        Ok(expr) =>
          match
            expr.next_times(get_datetime(sub), parse_int_arg(sub, "count")) {
            Ok(values) =>
              for value in values {
                print_datetime(value)
              }
            Err(err) => println(err.to_string())
          }
        Err(err) => println(err.to_string())
      }
    Some(("previous", sub)) =>
      match @cron.parse(get_required(sub, "expr")) {
        Ok(expr) =>
          match expr.previous_time(get_datetime(sub)) {
            Ok(value) => print_datetime(value)
            Err(err) => println(err.to_string())
          }
        Err(err) => println(err.to_string())
      }
    Some(("validate", sub)) => {
      let datetime = get_datetime(sub)
      match datetime.validate() {
        Ok(_) => println("valid")
        Err(err) => println("invalid: \{err}")
      }
    }
    Some(("match", sub)) => {
      let raw = get_required(sub, "expr")
      match @cron.parse(raw) {
        Ok(expr) => {
          let matched = expr.matches(
            parse_int_arg(sub, "year"),
            parse_int_arg(sub, "minute"),
            parse_int_arg(sub, "hour"),
            parse_int_arg(sub, "day"),
            parse_int_arg(sub, "month"),
            parse_int_arg(sub, "weekday"),
          )
          println(matched.to_string())
        }
        Err(err) => println(err.to_string())
      }
    }
    _ => println(usage().render_help())
  }
}