///|
/// Network helpers built on the system `curl`.
///
/// `curl` ships with macOS and every mainstream Linux distribution and speaks
/// HTTPS, redirects and retries out of the box. Shelling out to it keeps the
/// tool small while behaving correctly against `cli.moonbitlang.com`,
/// `mooncakes.io` and the CloudFront download endpoints.

///|
/// Create a unique temporary file path and return it.
pub fn temp_path() -> String raise MbError {
  let out = run_capture("mktemp \"${TMPDIR:-/tmp}/mb.XXXXXX\"")
  let path = out.trim().to_owned()
  if path == "" {
    raise MbError("failed to create a temporary file")
  }
  path
}

///|
/// Verify the external programs `mb` relies on are present.
pub fn ensure_tools() -> Unit raise MbError {
  let missing = []
  if !run_ok("command -v curl >/dev/null 2>&1") {
    missing.push("curl")
  }
  if !run_ok("command -v tar >/dev/null 2>&1") {
    missing.push("tar")
  }
  if !missing.is_empty() {
    raise MbError(
      "required program(s) not found on PATH: " + missing.join(", "),
    )
  }
}

///|
/// Download `url` to `dest`, following redirects and retrying transient
/// failures. Shows a progress bar for large downloads on a terminal.
pub fn download(
  url : String,
  dest : String,
  progress : Bool,
) -> Unit raise MbError {
  let parent = parent_dir(dest)
  if parent != "" {
    mkdir_p(parent)
  }
  let style = if progress && is_tty() { " --progress-bar" } else { " -s" }
  let cmd = "curl -fL" +
    style +
    " --retry 3 --retry-delay 1 --connect-timeout 30 -o " +
    shell_quote(dest) +
    " " +
    shell_quote(url)
  let code = run_status(cmd)
  if code != 0 {
    rm_rf(dest) catch {
      _ => ()
    }
    raise MbError("failed to download " + url + " (curl exit \{code})")
  }
}

///|
/// Fetch `url` and return the response body as bytes.
pub fn fetch_bytes(url : String) -> Bytes raise MbError {
  let tmp = temp_path()
  download(url, tmp, false)
  let data = read_bytes(tmp)
  rm_rf(tmp) catch {
    _ => ()
  }
  data
}

///|
/// Fetch `url` and return the response body as text.
pub fn fetch_text(url : String) -> String raise MbError {
  @utf8.decode_lossy(fetch_bytes(url))
}

///|
/// Fetch `url` with extra request headers and return the body as text. Used
/// for the GitHub API, which asks callers to identify themselves.
fn fetch_text_headers(
  url : String,
  headers : Array[String],
) -> String raise MbError {
  let tmp = temp_path()
  let mut extra = ""
  for h in headers {
    extra = extra + " -H " + shell_quote(h)
  }
  let cmd = "curl -fL -s" +
    extra +
    " --retry 3 --retry-delay 1 --connect-timeout 30 -o " +
    shell_quote(tmp) +
    " " +
    shell_quote(url)
  let code = run_status(cmd)
  if code != 0 {
    rm_rf(tmp) catch {
      _ => ()
    }
    raise MbError("failed to fetch " + url + " (curl exit \{code})")
  }
  let data = read_bytes(tmp)
  rm_rf(tmp) catch {
    _ => ()
  }
  @utf8.decode_lossy(data)
}

///|
/// Parse the status code printed by `curl -w '%{http_code}'`.
fn parse_http_code(raw : String) -> Int32 {
  let mut n = 0
  let mut any = false
  for c in raw.trim().to_owned().to_array() {
    if c >= '0' && c <= '9' {
      n = n * 10 + (c.to_int() - '0'.to_int())
      any = true
    } else if any {
      break
    }
  }
  if any {
    n
  } else {
    0
  }
}

///|
/// GET `url` and return `(status_code, body)`. A status of `0` means the
/// request could not be made at all (DNS, TLS, timeout, ...).
fn http_get_status(url : String) -> (Int32, String) raise MbError {
  let tmp = temp_path()
  let cmd = "curl -sL --retry 3 --retry-delay 1 --connect-timeout 30 -o " +
    shell_quote(tmp) +
    " -w '%{http_code}' " +
    shell_quote(url)
  let code = parse_http_code(run_capture(cmd))
  let body = read_text(tmp) catch { _ => "" }
  rm_rf(tmp) catch {
    _ => ()
  }
  (code, body)
}

///|
/// Fetch and parse a JSON document.
pub fn fetch_json(url : String) -> Json raise MbError {
  let text = fetch_text(url)
  @json.parse(text) catch {
    _ => raise MbError("invalid JSON returned by " + url)
  }
}