///|
struct WgetOptions {
  urls : Array[String]
  input_files : Array[String]
  mut help : Bool
  mut quiet : Bool
  mut output_document : String?
  mut log_file : String?
  mut append_log : Bool
  mut continue_download : Bool
  mut timestamping : Bool
  mut tries : Int
  mut retry_connection_refused : Bool
  retry_http_statuses : Array[Int]
  mut retry_delay_ms : Int
  mut max_redirects : Int
  mut content_disposition : Bool
  headers : Array[@netops.HttpHeader]
  mut request_method : @netops.HttpMethod
  mut method_explicit : Bool
  mut body : @netops.RequestBody
  mut body_options_conflict : Bool
  mut no_check_certificate : Bool
  mut no_proxy : Bool
  mut connect_timeout_ms : Int
  mut read_timeout_ms : Int
}

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

///|
fn usage() -> String {
  let text =
    #|Usage: wget [OPTION]... [URL]...
    #|  -q, --quiet                     quiet (no output)
    #|  -O, --output-document=FILE      write documents to FILE
    #|  -o, --output-file=FILE          log messages to FILE
    #|  -a, --append-output=FILE        append messages to FILE
    #|  -i, --input-file=FILE           download URLs found in FILE
    #|  -c, --continue                  resume a partially downloaded file
    #|  -N, --timestamping              do not re-retrieve unchanged files
    #|  -t, --tries=NUMBER              set number of retries (0 means unlimited)
    #|      --retry-connrefused         retry even if the connection is refused
    #|      --retry-on-http-error=CODES retry comma-separated HTTP status codes
    #|      --waitretry=SECONDS         wait between retries
    #|      --max-redirect=NUMBER       maximum redirects (default 20)
    #|      --content-disposition       honor Content-Disposition filenames
    #|      --header=STRING             insert STRING among request headers
    #|      --method=HTTPMethod         use a supported HTTP method
    #|      --body-data=STRING          send STRING with an explicit --method
    #|      --body-file=FILE            send FILE with an explicit --method
    #|      --no-check-certificate      disable TLS certificate verification
    #|      --no-proxy                  do not use proxy environment variables
    #|  -T, --timeout=SECONDS           set connect and read timeouts
    #|      --connect-timeout=SECONDS   set connection timeout
    #|      --read-timeout=SECONDS      set response inactivity timeout
    #|      --idle-timeout=SECONDS      extension: response inactivity timeout
    #|
    #|Compatibility limits: HTTP/HTTPS only; --method accepts GET, HEAD, POST,
    #|PUT, DELETE, CONNECT, OPTIONS, TRACE, and PATCH. Timer values are bounded
    #|by the runtime millisecond timer range.
  text
}

///|
fn default_options() -> WgetOptions {
  {
    urls: [],
    input_files: [],
    help: false,
    quiet: false,
    output_document: None,
    log_file: None,
    append_log: false,
    continue_download: false,
    timestamping: false,
    tries: 20,
    retry_connection_refused: false,
    retry_http_statuses: [],
    retry_delay_ms: 0,
    max_redirects: 20,
    content_disposition: false,
    headers: [],
    request_method: @netops.Get,
    method_explicit: false,
    body: @netops.EmptyBody,
    body_options_conflict: false,
    no_check_certificate: false,
    no_proxy: false,
    connect_timeout_ms: 900_000,
    read_timeout_ms: 900_000,
  }
}

///|
fn parse_nonnegative(
  value : String,
  option : String,
) -> Int raise WgetUsageError {
  let result = @string.parse_int(value) catch {
    _ => raise WgetUsageError("\{option}: invalid number '\{value}'")
  }
  if result < 0 {
    raise WgetUsageError("\{option}: invalid number '\{value}'")
  }
  result
}

///|
fn parse_seconds(value : String, option : String) -> Int raise WgetUsageError {
  @cli.parse_idle_timeout(value) catch {
    _ => raise WgetUsageError("\{option}: invalid time period '\{value}'")
  }
}

///|
fn parse_header(value : String) -> @netops.HttpHeader raise WgetUsageError {
  guard value.find(":") is Some(index) && index > 0 else {
    raise WgetUsageError("--header: invalid header '\{value}'")
  }
  @netops.http_header(
    value[:index].to_owned().trim().to_owned(),
    value[index + 1:].to_owned().trim().to_owned(),
  )
}

///|
fn parse_statuses(value : String) -> Array[Int] raise WgetUsageError {
  let statuses : Array[Int] = []
  for item in value.split(",") {
    let status = parse_nonnegative(
      item.trim().to_owned(),
      "--retry-on-http-error",
    )
    if status < 100 || status > 599 {
      raise WgetUsageError("--retry-on-http-error: invalid HTTP status")
    }
    statuses.push(status)
  }
  statuses
}

///|
fn parse_wget_options(
  args : ArrayView[String],
) -> WgetOptions raise WgetUsageError {
  let parsed = @cli.parse(args, [
    @cli.flag("quiet", short='q'),
    @cli.option("output-document", short='O'),
    @cli.option("output-file", short='o'),
    @cli.option("append-output", short='a'),
    @cli.option("input-file", short='i'),
    @cli.flag("continue", short='c'),
    @cli.flag("timestamping", short='N'),
    @cli.option("tries", short='t'),
    @cli.flag("retry-connrefused"),
    @cli.option("retry-on-http-error"),
    @cli.option("waitretry"),
    @cli.option("max-redirect"),
    @cli.flag("content-disposition"),
    @cli.option("header"),
    @cli.option("method"),
    @cli.option("body-data"),
    @cli.option("body-file"),
    @cli.flag("no-check-certificate"),
    @cli.flag("no-proxy"),
    @cli.option("timeout", short='T'),
    @cli.option("connect-timeout"),
    @cli.option("read-timeout"),
    @cli.option("idle-timeout"),
    @cli.flag("help"),
  ]) catch {
    @cli.CliError(option~, message~, ..) =>
      raise WgetUsageError("\{message}: '\{option}'")
  }
  let options = default_options()
  options.urls.append(parsed.operands)
  options.input_files.append(parsed.values("input-file"))
  options.help = parsed.contains("help")
  options.quiet = parsed.contains("quiet")
  options.output_document = parsed.last_value("output-document")
  options.continue_download = parsed.contains("continue")
  options.timestamping = parsed.contains("timestamping")
  options.retry_connection_refused = parsed.contains("retry-connrefused")
  options.content_disposition = parsed.contains("content-disposition")
  options.no_check_certificate = parsed.contains("no-check-certificate")
  options.no_proxy = parsed.contains("no-proxy")
  match parsed.last_occurrence(["output-file", "append-output"]) {
    Some("output-file") => {
      options.log_file = parsed.last_value("output-file")
      options.append_log = false
    }
    Some("append-output") => {
      options.log_file = parsed.last_value("append-output")
      options.append_log = true
    }
    _ => ()
  }
  if parsed.last_value("tries") is Some(value) {
    options.tries = parse_nonnegative(value, "--tries")
  }
  if parsed.last_value("waitretry") is Some(value) {
    options.retry_delay_ms = parse_seconds(value, "--waitretry")
  }
  if parsed.last_value("max-redirect") is Some(value) {
    options.max_redirects = parse_nonnegative(value, "--max-redirect")
  }
  for value in parsed.values("retry-on-http-error") {
    options.retry_http_statuses.append(parse_statuses(value))
  }
  for value in parsed.values("header") {
    options.headers.push(parse_header(value))
  }
  if parsed.last_value("method") is Some(value) {
    options.request_method = @netops.parse_http_method(value) catch {
      _ => raise WgetUsageError("--method: unsupported HTTP method '\{value}'")
    }
    options.method_explicit = true
  }
  options.body_options_conflict = parsed.contains("body-data") &&
    parsed.contains("body-file")
  match parsed.last_occurrence(["body-data", "body-file"]) {
    Some("body-data") => {
      let value = parsed.last_value("body-data").unwrap()
      options.body = @netops.BytesBody(@utf8.encode(value))
    }
    Some("body-file") => {
      let value = parsed.last_value("body-file").unwrap()
      options.body = if value == "-" {
        @netops.StdinBody
      } else {
        @netops.FileBody(value)
      }
    }
    _ => ()
  }
  if parsed.last_value("timeout") is Some(value) {
    let timeout = parse_seconds(value, "--timeout")
    options.connect_timeout_ms = timeout
    options.read_timeout_ms = timeout
  }
  if parsed.last_value("connect-timeout") is Some(value) {
    options.connect_timeout_ms = parse_seconds(value, "--connect-timeout")
  }
  match parsed.last_occurrence(["read-timeout", "idle-timeout"]) {
    Some(name) => {
      let value = parsed.last_value(name).unwrap()
      options.read_timeout_ms = parse_seconds(value, "--read-timeout")
    }
    None => ()
  }
  options
}