///|
fn resolve_report_options(
  options : ReportOptions,
) -> Result[ReportOptions, String] {
  let resolved = if options.config_file.length() == 0 {
    options
  } else {
    match load_cli_config(options.config_file) {
      Err(message) => return Err(message)
      Ok(config) => merge_report_options(options, config)
    }
  }
  if resolved.ignore_file.length() > 0 && resolved.policy_file.length() > 0 {
    Err("error: --ignore-file and --policy-file cannot be used together")
  } else {
    Ok(resolved)
  }
}

///|
fn resolve_check_options(
  options : CheckOptions,
) -> Result[CheckOptions, String] {
  let resolved = if options.config_file.length() == 0 {
    options
  } else {
    match load_cli_config(options.config_file) {
      Err(message) => return Err(message)
      Ok(config) => merge_check_options(options, config)
    }
  }
  if resolved.current.length() == 0 {
    Err("error: check requires --current major.minor.patch")
  } else if resolved.next.length() == 0 {
    Err("error: check requires --next major.minor.patch")
  } else if resolved.ignore_file.length() > 0 &&
    resolved.policy_file.length() > 0 {
    Err("error: --ignore-file and --policy-file cannot be used together")
  } else {
    Ok(resolved)
  }
}

///|
fn load_cli_config(path : String) -> Result[CliConfig, String] {
  let text = read_file_raw(path)
  if text.has_prefix(read_error_prefix) {
    let message = text[read_error_prefix.length():].to_owned()
    return Err("error: cannot read config file '\{path}': \{message}")
  }
  parse_cli_config(path, text)
}

///|
fn merge_report_options(
  options : ReportOptions,
  config : CliConfig,
) -> ReportOptions {
  let format = if options.has_format || config.format.length() == 0 {
    options.format
  } else {
    config.format
  }
  let ignore_file = if options.has_ignore_file ||
    config.ignore_file.length() == 0 {
    options.ignore_file
  } else {
    config.ignore_file
  }
  let policy_file = if options.has_policy_file ||
    config.policy_file.length() == 0 {
    options.policy_file
  } else {
    config.policy_file
  }
  let policy_version = if options.has_policy_version ||
    config.policy_version.length() == 0 {
    options.policy_version
  } else {
    config.policy_version
  }
  {
    format,
    ignore_file,
    policy_file,
    policy_version,
    baseline: config.baseline,
    target: config.target,
    baseline_dir: config.baseline_dir,
    target_dir: config.target_dir,
    config_file: options.config_file,
    has_format: options.has_format || config.format.length() > 0,
    has_ignore_file: options.has_ignore_file || config.ignore_file.length() > 0,
    has_policy_file: options.has_policy_file || config.policy_file.length() > 0,
    has_policy_version: options.has_policy_version ||
    config.policy_version.length() > 0,
  }
}

///|
fn merge_check_options(
  options : CheckOptions,
  config : CliConfig,
) -> CheckOptions {
  let report_options = merge_report_options(
    {
      format: options.format,
      ignore_file: options.ignore_file,
      policy_file: options.policy_file,
      policy_version: options.policy_version,
      baseline: options.baseline,
      target: options.target,
      baseline_dir: options.baseline_dir,
      target_dir: options.target_dir,
      config_file: options.config_file,
      has_format: options.has_format,
      has_ignore_file: options.has_ignore_file,
      has_policy_file: options.has_policy_file,
      has_policy_version: options.has_policy_version,
    },
    config,
  )
  let current = if options.has_current || config.current.length() == 0 {
    options.current
  } else {
    config.current
  }
  let next = if options.has_next || config.next.length() == 0 {
    options.next
  } else {
    config.next
  }
  let policy_version = if options.has_policy_version {
    options.policy_version
  } else if options.has_next {
    options.next
  } else if config.policy_version.length() > 0 {
    config.policy_version
  } else {
    next
  }
  {
    current,
    next,
    format: report_options.format,
    ignore_file: report_options.ignore_file,
    policy_file: report_options.policy_file,
    policy_version,
    baseline: report_options.baseline,
    target: report_options.target,
    baseline_dir: report_options.baseline_dir,
    target_dir: report_options.target_dir,
    config_file: options.config_file,
    has_current: options.has_current || config.current.length() > 0,
    has_next: options.has_next || config.next.length() > 0,
    has_format: report_options.has_format,
    has_ignore_file: report_options.has_ignore_file,
    has_policy_file: report_options.has_policy_file,
    has_policy_version: options.has_policy_version ||
    config.policy_version.length() > 0 ||
    next.length() > 0,
  }
}

///|
fn parse_cli_config(path : String, text : String) -> Result[CliConfig, String] {
  let errors : Array[String] = []
  let mut format = ""
  let mut ignore_file = ""
  let mut policy_file = ""
  let mut policy_version = ""
  let mut current = ""
  let mut next = ""
  let mut baseline = ""
  let mut target = ""
  let mut baseline_dir = ""
  let mut target_dir = ""
  let mut line_number = 1
  for raw_line in split_lines(text[:]) {
    let line = strip_config_comment(raw_line).trim()
    if line.length() > 0 {
      match parse_config_assignment(line) {
        None => errors.push("line \{line_number}: expected KEY = VALUE")
        Some((key, value)) =>
          if key.length() == 0 {
            errors.push("line \{line_number}: config key must not be empty")
          } else if value.length() == 0 {
            errors.push(
              "line \{line_number}: config value for '\{key}' must not be empty",
            )
          } else if key == "format" {
            match normalize_format(value) {
              None =>
                errors.push(
                  "line \{line_number}: unsupported report format '\{value}'",
                )
              Some(normalized) => format = normalized
            }
          } else if key == "ignore_file" || key == "ignore-file" {
            ignore_file = value
          } else if key == "policy_file" || key == "policy-file" {
            policy_file = value
          } else if key == "policy_version" || key == "policy-version" {
            policy_version = value
          } else if key == "current" {
            current = value
          } else if key == "next" {
            next = value
          } else if key == "baseline" {
            baseline = value
          } else if key == "target" {
            target = value
          } else if key == "baseline_dir" || key == "baseline-dir" {
            baseline_dir = value
          } else if key == "target_dir" || key == "target-dir" {
            target_dir = value
          } else {
            errors.push("line \{line_number}: unknown config key '\{key}'")
          }
      }
    }
    line_number += 1
  }
  if errors.is_empty() {
    Ok({
      format,
      ignore_file,
      policy_file,
      policy_version,
      current,
      next,
      baseline,
      target,
      baseline_dir,
      target_dir,
    })
  } else {
    Err(render_config_errors(path, errors))
  }
}

///|
fn strip_config_comment(line : StringView) -> StringView {
  match line.find("#") {
    Some(index) => line.view(start_offset=0, end_offset=index)
    None => line
  }
}

///|
fn parse_config_assignment(line : StringView) -> (String, String)? {
  match line.find("=") {
    Some(index) =>
      Some(
        (
          line.view(start_offset=0, end_offset=index).trim().to_owned(),
          line.view(start_offset=index + 1).trim().to_owned(),
        ),
      )
    None => None
  }
}

///|
fn render_config_errors(config_file : String, errors : Array[String]) -> String {
  let out = StringBuilder()
  out.write("error: invalid config file '")
  out.write(config_file)
  out.write("'")
  for error in errors {
    out.write("\n  ")
    out.write(error)
  }
  out.to_string()
}