///|
suberror CliError {
  CliError(String)
}

///|
impl Show for CliError with fn output(self, logger) {
  match self {
    CliError(message) => logger.write_string(message)
  }
}

///|
priv suberror CliFailure {
  CliFailure(@lib.ProtocolError)
}

///|
fn required_value(
  matches : @argparse.Matches,
  name : String,
) -> String raise CliFailure {
  match matches.values.get(name) {
    Some([value, ..]) => value
    _ =>
      raise CliFailure(
        @lib.protocol_error(
          "office.invalid_arguments",
          "required argument '\{name}' was not provided",
          details=Json::object({ "argument": Json::string(name) }),
        ),
      )
  }
}

///|
fn identify_error(
  error : @lib.OfficeError,
  file : String,
) -> @lib.ProtocolError {
  let bounded_file = bounded_text(file, 160)
  match error {
    UnsupportedFileExtension(_) =>
      @lib.protocol_error(
        "office.unsupported_extension",
        "unsupported file extension for '\{bounded_file}' (expected .xlsx or .docx)",
        details=Json::object({
          "file": Json::string(bounded_file),
          "expected_extensions": Json::array([
            Json::string("docx"),
            Json::string("xlsx"),
          ]),
        }),
      )
    InvalidPackage(message) =>
      @lib.protocol_error(
        "office.invalid_package",
        "invalid Office package: " + bounded_text(message, 240),
        details=Json::object({ "file": Json::string(bounded_file) }),
      )
    FormatMismatch(expected~, actual~) =>
      @lib.protocol_error(
        "office.format_mismatch",
        "file extension says \{expected.name()}, but package content is \{actual.name()}",
        details=Json::object({
          "file": Json::string(bounded_file),
          "expected": Json::string(expected.name()),
          "actual": Json::string(actual.name()),
        }),
      )
    ResourceLimit(kind~, limit~, actual~) =>
      @lib.protocol_error(
        "office.resource_limit",
        "Office \{bounded_text(kind, 80)} exceeds the configured limit",
        details=Json::object({
          "file": Json::string(bounded_file),
          "resource": Json::string(bounded_text(kind, 80)),
          "limit": Json::number(limit.to_double()),
          "actual": Json::number(actual.to_double()),
        }),
      )
    Cancelled =>
      @lib.protocol_error("office.cancelled", "Office read was cancelled")
  }
}

///|
async fn run_identify(matches : @argparse.Matches) -> Unit {
  let file = required_value(matches, "file")
  let source = read_office_package(file, cancelled=office_async_cancelled)
  let format = source.format
  if matches.flags.get_or_default("json", false) {
    println(
      @lib.output_success(
        Json::object({
          "schema": Json::string("office.identify/1"),
          "file": Json::string(file),
          "format": Json::string(format.name()),
        }),
      ).stringify(indent=2),
    )
  } else {
    println(format.name())
  }
}

///|
fn identify_command() -> @argparse.Command {
  let summary = match @lib.find_capability_command("identify") {
    Some(command) => command.summary
    None => "Identify a structurally valid XLSX or DOCX package"
  }
  Command(
    "identify",
    about=summary,
    positionals=[
      PositionArg(
        "file",
        about="path to an .xlsx or .docx file",
        num_args=@argparse.ValueRange::single(),
      ),
    ],
    flags=[FlagArg("json", long="json", about="print office.output/1 JSON")],
  )
}

///|
fn build_command() -> @argparse.Command {
  Command(
    "office",
    about="Create, inspect, and edit XLSX and DOCX documents",
    disable_help_subcommand=true,
    subcommand_required=true,
    subcommands=[
      help_command(),
      identify_command(),
      outline_command(),
      get_command(),
      text_command(),
      query_command(),
      docx_find_command(),
      replace_command(),
      format_command(),
      insert_paragraph_command(),
      delete_paragraph_command(),
      validate_command(),
      dump_command(),
      replay_command(),
      issues_command(),
      preview_command(),
      render_command(),
      create_command(),
      batch_command(),
      template_command(),
      edit_command(),
      annotate_command(),
      raw_command(),
    ],
  )
}

///|
fn is_implemented_command(name : StringView) -> Bool {
  match name {
    "help"
    | "identify"
    | "outline"
    | "get"
    | "text"
    | "query"
    | "find"
    | "replace"
    | "format"
    | "insert-paragraph"
    | "delete-paragraph"
    | "validate"
    | "dump"
    | "replay"
    | "issues"
    | "preview"
    | "render"
    | "create"
    | "batch"
    | "template"
    | "edit"
    | "annotate"
    | "raw" => true
    _ => false
  }
}

///|
async fn run_cli(args : Array[String]) -> Unit {
  let command = build_command()
  let matches = command.parse(argv=args, env=Map([]))
  match matches.subcommand {
    Some((name, values)) if is_implemented_command(name) =>
      match name {
        "help" => run_help(values)
        "identify" => run_identify(values)
        "outline" => run_docx_outline(values)
        "get" => run_docx_get(values)
        "text" => run_docx_text(values)
        "query" => run_docx_query(values)
        "find" => run_docx_find(values)
        "replace" => run_replace(values)
        "format" => run_format(values)
        "insert-paragraph" => run_insert_paragraph(values)
        "delete-paragraph" => run_delete_paragraph(values)
        "validate" => run_validate(values)
        "dump" => run_dump(values)
        "replay" => run_replay(values)
        "issues" => run_issues(values)
        "preview" => run_preview(values)
        "render" => run_render(values)
        "create" => run_create(values)
        "batch" => run_batch(values)
        "template" => run_template(values)
        "edit" => run_edit(values)
        "annotate" => run_annotate(values)
        "raw" => run_raw(values)
        _ => ()
      }
    _ =>
      raise CliFailure(
        @lib.protocol_error(
          "office.invalid_arguments", "an implemented subcommand is required; try 'office help'",
        ),
      )
  }
}

///|
async fn run() -> Unit {
  let argv = @env.args()
  let args = if argv.length() > 0 { argv[1:].to_owned() } else { [] }
  let mode = output_mode_from_args(args)
  let (has_json, has_jsonl) = output_flags_from_args(args)
  try {
    if has_json && has_jsonl {
      raise CliFailure(
        @lib.protocol_error(
          "office.output_mode_conflict",
          "--json and --jsonl are mutually exclusive",
          details=Json::object({
            "options": Json::array([
              Json::string("--json"),
              Json::string("--jsonl"),
            ]),
          }),
        ),
      )
    }
    run_cli(args)
  } catch {
    error if @async.is_being_cancelled() => raise error
    CliFailure(error) => raise CliError(render_failure(error, mode))
    error => {
      let message = normalize_argument_error(error.to_string())
      raise CliError(
        render_failure(
          @lib.protocol_error("office.invalid_arguments", message),
          mode,
        ),
      )
    }
  }
}

///|
async fn main {
  run() catch {
    error if @async.is_being_cancelled() => raise error
    error => raise CliError(error.to_string())
  }
}