// Responses that are an envelope rather than a body: a redirect, a download,
// a data URL, and a download that weighs the request first.

///|
/// [`file_response`] with the request weighed first (RFC 9110 §13, §14):
///
/// * `304` — the client's copy is current.
/// * `412` — a precondition it stated does not hold.
/// * `206` — it asked for part, one `Content-Range` or a `multipart/byteranges`.
/// * `416` — it asked for what is not there.
///
/// Everything turns on `etag`. Left out, one is hashed from the content, which
/// costs a pass over the body; pass `@conditional.stamp(length~, modified~)` for
/// one stat's worth instead, or any tag of your own.
///
/// `ranges=false` withdraws the offer in `Accept-Ranges`.
pub fn Context::serve(
  self : Context,
  content : Bytes,
  filename~ : String = "",
  media_type~ : String = "",
  inline~ : Bool = false,
  etag? : @conditional.Tag,
  modified? : @moondate.Moment,
  ranges~ : Bool = true,
) -> @moonasgi.Response {
  let tag = match etag {
    Some(tag) => tag
    None => @conditional.hash(content[:], digest=@sha2.Hasher::new())
  }
  let verb = self.request.http_method
  let ask = @conditional.Ask::read(self.request.headers[:])
  let validators = [("etag", tag.encode())]
  match modified {
    Some(modified) =>
      validators.push(("last-modified", modified.http_text())) catch {
        _ => ()
      }
    None => ()
  }
  match ask.evaluate(verb~, etag=tag, modified?) {
    // §15.4.5: no body, and the validators the 200 would have carried.
    Fresh => @moonasgi.Response::new(304, validators, b"")
    Failed => @moonasgi.Response::new(412, validators, b"")
    Go => {
      let headers = @status.envelope(
        content.length(),
        filename,
        media_type,
        inline,
      )
      for one in validators {
        headers.push(one)
      }
      headers.push(("accept-ranges", if ranges { "bytes" } else { "none" }))
      if !ranges || verb != "GET" {
        return @moonasgi.Response::new(200, headers, body(verb, content))
      }
      // §13.1.5: a Range that If-Range ruled out asks for the whole thing.
      guard asked(self.request.headers, "range") is Some(field) &&
        ask.unchanged(etag=tag, modified?) else {
        return @moonasgi.Response::new(200, headers, body(verb, content))
      }
      guard @range.decode(field[:]) is Some(specs) else {
        return @moonasgi.Response::new(200, headers, body(verb, content))
      }
      let whole = content.length().to_int64()
      guard @range.measure(specs[:], length=whole) is Some(parts) else {
        let refused = [("content-range", @range.unsatisfied(whole).encode())]
        return @moonasgi.Response::new(416, refused, b"")
      }
      partial(verb, content, parts, headers, whole)
    }
  }
}

///|
/// The 206, in whichever shape was asked for (§14.4, §14.6).
fn partial(
  verb : String,
  content : Bytes,
  parts : Array[@range.Part],
  headers : Array[(String, String)],
  whole : Int64,
) -> @moonasgi.Response {
  let kind = field_of(headers, "content-type")
  if parts.length() == 1 {
    let part = parts[0]
    let piece = cut(content, part)
    let out = without(headers, ["content-length", "accept-ranges"])
    out.push(("content-length", piece.length().to_string()))
    out.push(("accept-ranges", "bytes"))
    out.push(
      ("content-range", @range.Content::new(part, complete=whole).encode()),
    )
    return @moonasgi.Response::new(206, out, body(verb, piece))
  }
  // From the tag rather than at random: this package holds no randomness, and a
  // boundary that changed between identical requests could not be tested.
  let boundary = "moonapi-" + token(field_of(headers, "etag"))
  let pieces = parts.map(fn(part) { (part, cut(content, part)) })
  let piece = @range.parts(pieces[:], boundary~, kind~, complete=whole)
  let out = without(headers, ["content-type", "content-length"])
  out.push(("content-type", @range.multipart(boundary[:])))
  out.push(("content-length", piece.length().to_string()))
  @moonasgi.Response::new(206, out, body(verb, piece))
}

///|
/// A request header by name, ignoring case — the rule `Ask::read` uses, so
/// `Range` and `If-Range` are not read by two different ones.
fn asked(headers : Array[(String, String)], name : String) -> String? {
  for pair in headers {
    if pair.0.to_lower() == name {
      return Some(pair.1)
    }
  }
  None
}

///|
/// A `HEAD` gets the headers and nothing after them (§9.3.2).
fn body(verb : String, content : Bytes) -> Bytes {
  if verb == "HEAD" {
    b""
  } else {
    content
  }
}

///|
fn cut(content : Bytes, part : @range.Part) -> Bytes {
  let out : Array[Byte] = []
  for i = part.at.to_int(); i <= part.last.to_int(); i = i + 1 {
    out.push(content[i])
  }
  Bytes::from_array(out)
}

///|
fn field_of(headers : Array[(String, String)], name : String) -> String {
  for pair in headers {
    if pair.0 == name {
      return pair.1
    }
  }
  ""
}

///|
/// The part of a tag a boundary may contain, within the seventy octets RFC 2046
/// §5.1.1 allows, less the prefix's eight.
fn token(text : String) -> String {
  let out = StringBuilder()
  for i = 0; i < text.length(); i = i + 1 {
    let b = text[i].to_int()
    let ok = (b >= 0x41 && b <= 0x5A) ||
      (b >= 0x61 && b <= 0x7A) ||
      (b >= 0x30 && b <= 0x39) ||
      b == 0x2D
    if ok {
      out.write_char(b.unsafe_to_char())
    }
  }
  let text = out.to_string()
  if text == "" {
    return "part"
  }
  if text.length() > 62 {
    text[text.length() - 62:].to_owned()
  } else {
    text
  }
}

///|
fn without(
  headers : Array[(String, String)],
  names : Array[String],
) -> Array[(String, String)] {
  let out : Array[(String, String)] = []
  for pair in headers {
    let mut drop = false
    for name in names {
      if pair.0 == name {
        drop = true
      }
    }
    if !drop {
      out.push(pair)
    }
  }
  out
}