///|
/// Return the stable command-line help text.
pub fn cli_usage() -> String {
  let usage =
    #|MoonL10n 0.2.0
    #|Usage:
    #|  moon run cmd/main -- validate   [locale] [--json]
    #|  moon run cmd/main -- diff   [locale] [--json]
    #|  moon run cmd/main -- render    [name=value ...] [--json]
  usage
}

///|
fn command_kind(name : String) -> CliCommandKind? {
  match name {
    "validate" => Some(Validate)
    "diff" => Some(Diff)
    "render" => Some(Render)
    "help" | "--help" | "-h" => Some(Help)
    _ => None
  }
}

///|
fn parse_cli_value(value : String) -> Argument {
  try @string.parse_int(value) catch {
    _ => Text(value)
  } noraise {
    number => Number(number)
  }
}

///|
fn split_argument_pair(value : String) -> (String, String)? {
  let name = StringBuilder::new()
  let content = StringBuilder::new()
  let mut separator = false
  for ch in value {
    if ch == '=' && !separator {
      separator = true
    } else if separator {
      content.write_char(ch)
    } else {
      name.write_char(ch)
    }
  }
  if !separator || name.is_empty() {
    None
  } else {
    Some((name.to_string(), content.to_string()))
  }
}

///|
/// Parse one `name=value` CLI argument.
pub fn parse_cli_argument(
  value : String,
) -> (String, Argument) raise MessageError {
  match split_argument_pair(value) {
    Some((name, content)) => (name, parse_cli_value(content))
    None =>
      raise InvalidCommand(
        "Expected argument in name=value form, got '\{value}'.",
      )
  }
}

///|
fn collect_render_arguments(
  values : Array[String],
) -> Map[String, Argument] raise MessageError {
  let arguments : Map[String, Argument] = Map([])
  for value in values {
    let (name, argument) = parse_cli_argument(value)
    if arguments.contains(name) {
      raise InvalidCommand("Duplicate render argument '\{name}'.")
    }
    arguments[name] = argument
  }
  arguments
}

///|
fn without_json_flag(args : Array[String]) -> (Array[String], Bool) {
  let output : Array[String] = []
  let mut json = false
  for arg in args {
    if arg == "--json" {
      json = true
    } else {
      output.push(arg)
    }
  }
  (output, json)
}

///|
fn parse_validate_request(
  kind : CliCommandKind,
  args : Array[String],
  json : Bool,
) -> CliRequest raise MessageError {
  if args.length() < 3 || args.length() > 4 {
    raise InvalidCommand(
      "validate and diff require two catalog paths and an optional locale.",
    )
  }
  let locale = if args.length() == 4 { Some(args[3]) } else { None }
  {
    kind,
    paths: [args[1], args[2]],
    locale,
    key: None,
    arguments: Map([]),
    json,
  }
}

///|
fn parse_render_request(
  args : Array[String],
  json : Bool,
) -> CliRequest raise MessageError {
  if args.length() < 4 {
    raise InvalidCommand(
      "render requires a catalog path, locale, key, and optional name=value arguments.",
    )
  }
  let argument_text : Array[String] = []
  for index in 4.. CliRequest raise MessageError {
  let (clean, json) = without_json_flag(args)
  if clean.length() == 0 {
    return {
      kind: Help,
      paths: [],
      locale: None,
      key: None,
      arguments: Map([]),
      json,
    }
  }
  match command_kind(clean[0]) {
    Some(Help) =>
      {
        kind: Help,
        paths: [],
        locale: None,
        key: None,
        arguments: Map([]),
        json,
      }
    Some(Validate) => parse_validate_request(Validate, clean, json)
    Some(Diff) => parse_validate_request(Diff, clean, json)
    Some(Render) => parse_render_request(clean, json)
    None => raise InvalidCommand("Unknown command '\{clean[0]}'.")
  }
}

///|
pub fn CliCommandKind::name(self : CliCommandKind) -> String {
  match self {
    Validate => "validate"
    Diff => "diff"
    Render => "render"
    Help => "help"
  }
}

///|
pub fn CliRequest::kind(self : CliRequest) -> CliCommandKind {
  self.kind
}

///|
pub fn CliRequest::paths(self : CliRequest) -> Array[String] {
  self.paths
}

///|
pub fn CliRequest::locale(self : CliRequest) -> String? {
  self.locale
}

///|
pub fn CliRequest::key(self : CliRequest) -> String? {
  self.key
}

///|
pub fn CliRequest::arguments(self : CliRequest) -> Map[String, Argument] {
  let output : Map[String, Argument] = Map([])
  for name, value in self.arguments {
    output[name] = value
  }
  output
}

///|
pub fn CliRequest::json(self : CliRequest) -> Bool {
  self.json
}

///|
pub fn CliRequest::path(self : CliRequest, index : Int) -> String? {
  self.paths.get(index)
}

///|
fn cli_success(stdout : String) -> CliOutput {
  { exit_code: 0, stdout, stderr: "" }
}

///|
fn cli_lint_result(report : LintReport, json : Bool) -> CliOutput {
  {
    exit_code: report.exit_code(),
    stdout: if json {
      report.to_json()
    } else {
      report.to_text()
    },
    stderr: "",
  }
}

///|
fn write_string_array_json(
  output : StringBuilder,
  values : Array[String],
) -> Unit {
  output.write_char('[')
  for index, value in values {
    if index > 0 {
      output.write_char(',')
    }
    write_json_string(output, value)
  }
  output.write_char(']')
}

///|
fn catalog_diff_json(diff : CatalogDiff) -> String {
  let output = StringBuilder::new()
  output.write_char('{')
  write_json_string(output, "reference_locale")
  output.write_char(':')
  write_json_string(output, diff.reference_locale)
  output.write_char(',')
  write_json_string(output, "translation_locale")
  output.write_char(':')
  write_json_string(output, diff.translation_locale)
  output.write_char(',')
  write_json_string(output, "shared")
  output.write_char(':')
  write_string_array_json(output, diff.shared_keys)
  output.write_char(',')
  write_json_string(output, "missing")
  output.write_char(':')
  write_string_array_json(output, diff.missing_keys)
  output.write_char(',')
  write_json_string(output, "extra")
  output.write_char(':')
  write_string_array_json(output, diff.extra_keys)
  output.write_char('}')
  output.to_string()
}

///|
fn catalog_diff_text(diff : CatalogDiff) -> String {
  if diff.is_equal() {
    return "OK"
  }
  let output = StringBuilder::new()
  for key in diff.missing_keys {
    output.write_string("missing-key ")
    output.write_string(key)
    output.write_char('\n')
  }
  for key in diff.extra_keys {
    output.write_string("extra-key ")
    output.write_string(key)
    output.write_char('\n')
  }
  output.write_string(diff.coverage().summary())
  output.to_string()
}

///|
fn cli_diff_result(diff : CatalogDiff, json : Bool) -> CliOutput {
  let exit_code = if diff.missing_keys.length() > 0 {
    2
  } else if diff.extra_keys.length() > 0 {
    1
  } else {
    0
  }
  {
    exit_code,
    stdout: if json {
      catalog_diff_json(diff)
    } else {
      catalog_diff_text(diff)
    },
    stderr: "",
  }
}

///|
/// Validate two in-memory catalog sources.
pub fn cli_validate_sources(
  reference_source : String,
  translation_source : String,
  locale : String,
  json? : Bool = false,
) -> CliOutput raise MessageError {
  let reference = Catalog::from_json_lenient("reference", reference_source)
  let translation = Catalog::from_json_lenient(locale, translation_source)
  cli_lint_result(lint_catalogs(reference, translation), json)
}

///|
/// Compare the key sets of two in-memory catalog sources.
pub fn cli_diff_sources(
  reference_source : String,
  translation_source : String,
  locale : String,
  json? : Bool = false,
) -> CliOutput raise MessageError {
  let reference = Catalog::from_json_lenient("reference", reference_source)
  let translation = Catalog::from_json_lenient(locale, translation_source)
  cli_diff_result(diff_catalogs(reference, translation), json)
}

///|
fn translation_result_json(result : TranslationResult) -> String {
  let output = StringBuilder::new()
  output.write_char('{')
  write_json_string(output, "key")
  output.write_char(':')
  write_json_string(output, result.key)
  output.write_char(',')
  write_json_string(output, "requested_locale")
  output.write_char(':')
  write_json_string(output, result.requested_locale)
  output.write_char(',')
  write_json_string(output, "resolved_locale")
  output.write_char(':')
  write_json_string(output, result.resolved_locale)
  output.write_char(',')
  write_json_string(output, "used_fallback")
  output.write_char(':')
  output.write_string(if result.used_fallback { "true" } else { "false" })
  output.write_char(',')
  write_json_string(output, "value")
  output.write_char(':')
  write_json_string(output, result.value)
  output.write_char('}')
  output.to_string()
}

///|
/// Render one key from an in-memory catalog source.
pub fn cli_render_source(
  source : String,
  locale : String,
  key : String,
  arguments : Map[String, Argument],
  json? : Bool = false,
) -> CliOutput raise MessageError {
  let catalog = Catalog::from_json(locale, source)
  let translator = Translator::new(locale, locale)
  translator.add_catalog(catalog)
  let result = translator.translate_detailed(key, arguments, locale~)
  cli_success(if json { translation_result_json(result) } else { result.value })
}

///|
pub fn CliOutput::exit_code(self : CliOutput) -> Int {
  self.exit_code
}

///|
pub fn CliOutput::stdout(self : CliOutput) -> String {
  self.stdout
}

///|
pub fn CliOutput::stderr(self : CliOutput) -> String {
  self.stderr
}

///|
pub fn CliOutput::succeeded(self : CliOutput) -> Bool {
  self.exit_code == 0
}

///|
pub fn CliOutput::failed(self : CliOutput) -> Bool {
  self.exit_code != 0
}

///|
pub fn CliOutput::display(self : CliOutput) -> String {
  if self.stderr == "" {
    self.stdout
  } else if self.stdout == "" {
    self.stderr
  } else {
    self.stdout + "\n" + self.stderr
  }
}