///|
priv enum OutputMode {
  Human
  JsonDocument
  JsonLines
}

///|
fn transaction_failure(error : @transaction.TransactionError) -> CliFailure {
  CliFailure(error.to_protocol_error())
}

///|
fn unexpected_cli_failure(
  code : String,
  operation : String,
  error : Error,
) -> CliFailure {
  CliFailure(
    @lib.protocol_error(
      code,
      "\{operation} failed: \{bounded_text(error.to_string(), 240)}",
    ),
  )
}

///|
fn format_resource_failure(
  format : String,
  resource : String,
  limit : Int,
  actual? : Int,
) -> CliFailure {
  let fields : Map[String, Json] = {
    "resource": Json::string(resource),
    "limit": Json::number(limit.to_double()),
  }
  match actual {
    Some(value) => fields["actual"] = Json::number(value.to_double())
    None => ()
  }
  CliFailure(
    @lib.protocol_error(
      "office.\{format}.resource_limit",
      "\{format.to_upper()} \{resource} exceeds the configured limit of \{limit}",
      details=Json::object(fields),
    ),
  )
}

///|
fn terminal_hex_digit(value : Int) -> Char {
  match value {
    0 => '0'
    1 => '1'
    2 => '2'
    3 => '3'
    4 => '4'
    5 => '5'
    6 => '6'
    7 => '7'
    8 => '8'
    9 => '9'
    10 => 'a'
    11 => 'b'
    12 => 'c'
    13 => 'd'
    14 => 'e'
    _ => 'f'
  }
}

///|
fn output_flags_from_args(args : Array[String]) -> (Bool, Bool) {
  let mut json = false
  let mut jsonl = false
  for argument in args {
    if argument == "--" {
      break
    } else if argument == "--json" {
      json = true
    } else if argument == "--jsonl" {
      jsonl = true
    }
  }
  (json, jsonl)
}

///|
fn output_mode_from_args(args : Array[String]) -> OutputMode {
  let (json, jsonl) = output_flags_from_args(args)
  if jsonl && !json {
    JsonLines
  } else if json {
    JsonDocument
  } else {
    Human
  }
}

///|
fn bounded_text(value : String, maximum_chars : Int) -> String {
  let output = StringBuilder()
  let mut count = 0
  let mut truncated = false
  for char in value {
    if count < maximum_chars {
      output.write_char(char) |> ignore
    } else {
      truncated = true
    }
    count = count + 1
  }
  if truncated {
    output.write_string("…") |> ignore
  }
  output.to_string()
}

///|
fn terminal_safe_text(value : String) -> String {
  let output = StringBuilder()
  for character in value {
    match character {
      '\n' => output.write_string("\\n") |> ignore
      '\r' => output.write_string("\\r") |> ignore
      '\t' => output.write_string("\\t") |> ignore
      _ => {
        let code = character.to_int()
        if code < 0x20 || code == 0x7f || (code >= 0x80 && code <= 0x9f) {
          output.write_string("\\u") |> ignore
          output.write_char(terminal_hex_digit((code >> 12) & 0xf)) |> ignore
          output.write_char(terminal_hex_digit((code >> 8) & 0xf)) |> ignore
          output.write_char(terminal_hex_digit((code >> 4) & 0xf)) |> ignore
          output.write_char(terminal_hex_digit(code & 0xf)) |> ignore
        } else {
          output.write_char(character) |> ignore
        }
      }
    }
  }
  output.to_string()
}

///|
fn human_text(value : String, maximum : Int) -> String {
  terminal_safe_text(bounded_text(value, maximum))
}

///|
fn human_transaction_status(report : @transaction.TransactionReport) -> String {
  if report.dry_run {
    "validated dry run"
  } else if report.committed {
    "committed"
  } else if !report.changed {
    "validated no-op"
  } else {
    "validated without publication"
  }
}

///|
fn normalize_argument_error(value : String) -> String {
  let first_line = match value.find("\n") {
    Some(index) => value[:index].to_owned()
    None => value
  }
  let bounded = bounded_text(first_line, 240)
  if bounded.has_prefix("error: ") {
    bounded["error: ".length():].to_owned()
  } else if bounded.has_prefix("office: ") {
    bounded["office: ".length():].to_owned()
  } else {
    bounded
  }
}

///|
fn render_failure(error : @lib.ProtocolError, mode : OutputMode) -> String {
  match mode {
    Human => "office: " + terminal_safe_text(error.message)
    JsonDocument => @lib.output_failure(error).stringify(indent=2)
    JsonLines => @lib.output_failure(error).stringify()
  }
}