///|
suberror ConfigError {
  ConfigError(Int32, String)
}

///|
fn config_name(name : String) -> String {
  name.to_lower().replace_all(old="-", new="").replace_all(old="_", new="")
}

///|
fn config_line(raw : String) -> StringView {
  raw[:raw.find("\u0000").unwrap_or(raw.length())].trim(
    chars=" \t\r\n\u000b\u000c",
  )
}

///|
fn config_setting(raw : String) -> Array[String] raise UsageError {
  let args : Array[String] = []
  let specs = option_specs()
  let line = config_line(raw)
  if line.is_empty() || line.has_prefix("#") {
    return []
  }
  guard line.split_once("=") is Some((key, value)) else {
    raise UsageError("invalid config line: " + line.to_owned())
  }
  let name = config_name(key.trim().to_owned())
  let mut value = value.trim(chars=" \t\r\n\u000b\u000c").to_owned()
  if name == "passiveftp" {
    // Distribution startup files configure FTP even for HTTP-only requests.
    if !["on", "off", "yes", "no", "1", "0"].contains(value.to_lower()) {
      raise UsageError(
        key.trim().to_owned() +
        ": Invalid boolean '" +
        value +
        "'; use `on' or `off'.",
      )
    }
    return []
  }
  let canonical = match name {
    "checkcertificate" => "check-certificate"
    "useproxy" => "proxy"
    "logfile" => "output-file"
    "input" => "input-file"
    _ => {
      guard specs
        .iter()
        .find_first(spec => {
          !spec.name.has_prefix("no-") && config_name(spec.name) == name
        })
        is Some(spec) else {
        raise UsageError("unsupported config setting: " + key.to_owned())
      }
      spec.name
    }
  }
  if ["config", "no-config", "execute", "help"].contains(canonical) {
    raise UsageError("unsupported config setting: " + key.to_owned())
  }
  let spec = specs.iter().find_first(spec => spec.name == canonical).unwrap()
  if canonical == "tries" || canonical == "max-redirect" {
    value = config_number(key.trim().to_owned(), value).to_string()
  }
  if !spec.takes_value {
    let enabled = match value.to_lower() {
      "on" | "yes" | "1" => true
      "off" | "no" | "0" => false
      _ => raise UsageError("invalid config boolean: " + value)
    }
    args.push("--" + (if enabled { canonical } else { "no-" + canonical }))
  } else if canonical == "auth-no-challenge" {
    args.push("--auth-no-challenge=" + value)
  } else {
    args.append(["--" + canonical, value])
  }
  ignore(parse_options(args))
  args
}

///|
fn config_number(name : String, value : String) -> Int32 raise UsageError {
  let mut position = 0
  let negative = value.has_prefix("-")
  if negative || value.has_prefix("+") {
    position = 1
  }
  let mut number = 0L
  while position < value.length() {
    let digit = value.code_unit_at(position).to_int() - 48
    if digit < 0 || digit > 9 {
      break
    }
    number = number * 10L + digit.to_int64()
    if number > 2147483647L {
      raise UsageError(name + ": Invalid number '" + value + "'.")
    }
    position += 1
  }
  if negative && number > 0L {
    raise UsageError(name + ": Invalid number '" + value + "'.")
  }
  number.to_int()
}

///|
fn config_file_arguments(text : String, path : String) -> Array[String] raise {
  let args : Array[String] = []
  let errors : Array[String] = []
  for index, raw in text.split("\n").to_array() {
    let parsed = config_setting(raw.to_owned()) catch {
      UsageError(message) => {
        errors.append([
          "wget: " + message,
          "wget: Error in " + path + " at line " + (index + 1).to_string() + ".",
        ])
        continue
      }
    }
    args.append(parsed)
  }
  if !errors.is_empty() {
    errors.push("Exiting due to error in " + path)
    raise ConfigError(
      2,
      errors.join("\n").strip_prefix("wget: ").unwrap().to_owned(),
    )
  }
  args
}

///|
fn execute_arguments(text : String) -> Array[String] raise UsageError {
  let line = config_line(text)
  if line.is_empty() || line.has_prefix("#") {
    raise UsageError("Invalid --execute command '" + text + "'")
  }
  config_setting(text)
}

///|
fn config_tokens(
  args : ArrayView[String],
) -> Array[(String, String?)] raise UsageError {
  let specs = option_specs()
  let output : Array[(String, String?)] = []
  let mut index = 0
  while index < args.length() {
    let arg = args[index]
    index += 1
    if arg == "--" {
      output.push((arg, None))
      for value in args[index:] {
        output.push((value, None))
      }
      break
    }
    let tokens : Array[(String, String?)] = []
    if arg.has_prefix("--") {
      match arg.split_once("=") {
        Some((key, value)) =>
          tokens.push((key.to_owned(), Some(value.to_owned())))
        None => tokens.push((arg, None))
      }
    } else if arg.has_prefix("-") && arg != "-" {
      let chars = arg.iter().to_array()
      for position = 1; position < chars.length(); position = position + 1 {
        let short = chars[position]
        let name = "-" + short.to_string()
        if specs.any(spec => spec.short == Some(short) && spec.takes_value) {
          tokens.push(
            (
              name,
              if position + 1 < chars.length() {
                Some(String::from_iter(chars[position + 1:].iter()))
              } else {
                None
              },
            ),
          )
          break
        }
        tokens.push((name, None))
      }
    } else {
      tokens.push((arg, None))
    }
    for (name, attached) in tokens {
      let takes_value = specs.any(spec => {
        spec.takes_value &&
        (
          name == "--" + spec.name ||
          spec.short.map(char => "-" + char.to_string()) == Some(name)
        )
      })
      let value = if takes_value &&
        attached is None &&
        name != "--auth-no-challenge" {
        guard args.get(index) is Some(value) else {
          raise UsageError(name + " requires an argument")
        }
        index += 1
        Some(value)
      } else {
        attached
      }
      output.push((name, value))
    }
  }
  output
}

///|
async fn system_config_arguments(
  default_path : String,
  override_path : String?,
) -> Array[String] {
  let paths = match override_path {
    Some(path) if path != "" && path != default_path => [path, default_path]
    _ => [default_path]
  }
  for path in paths {
    let text = try {
      if @fs.kind(path, follow_symlink=true) == Regular {
        Some(@fs.read_file(path).text())
      } else {
        None
      }
    } catch {
      _ => None
    }
    if text is Some(text) {
      return config_file_arguments(text, path)
    }
  }
  []
}

///|
async fn configured_arguments(args : ArrayView[String]) -> Array[String] {
  let tokens = config_tokens(args)
  let output : Array[String] = []
  let mut selected = false
  for (name, value) in tokens {
    if name == "--" {
      break
    }
    if name == "--no-config" {
      selected = true
      break
    }
    if name == "--config" {
      let path = value.unwrap()
      let text = @fs.read_file(path).text() catch {
        _ => raise ConfigError(2, "cannot read config from '" + path + "'")
      }
      output.append(config_file_arguments(text, path))
      selected = true
      break
    }
  }
  if !selected {
    output.append(
      system_config_arguments("/etc/wgetrc", @env.get_env_var("SYSTEM_WGETRC")),
    )
    let path = match @env.get_env_var("WGETRC") {
      Some(path) if path != "" => Some((path, true))
      _ =>
        @env.get_env_var("HOME").map(directory => {
          (directory + "/.wgetrc", false)
        })
    }
    if path is Some((path, required)) {
      let text = @fs.read_file(path).text() catch {
        _ =>
          if required {
            raise ConfigError(1, "cannot read WGETRC from '" + path + "'")
          } else {
            ""
          }
      }
      output.append(config_file_arguments(text, path))
    }
  }
  let mut literal = false
  for (name, value) in tokens {
    if name == "--" {
      literal = true
    }
    if !literal && name == "--no-config" {
      if value is Some(value) &&
        !["on", "off", "yes", "no", "1", "0"].contains(value.to_lower()) {
        raise UsageError(
          "--no-config: Invalid boolean '" + value + "'; use `on' or `off'.",
        )
      }
      continue
    }
    if !literal && name == "--config" {
      continue
    }
    if !literal && (name == "--execute" || name == "-e") {
      output.append(execute_arguments(value.unwrap()))
    } else if value is Some(value) {
      // Attached spelling preserves optional-value and flag diagnostics.
      if name.has_prefix("--") {
        output.push(name + "=" + value)
      } else {
        output.append([name, value])
      }
    } else {
      output.push(name)
    }
  }
  output
}