///|
pub(all) struct HttpRequest {
  http_method : String
  /// The request-target path, without query string or fragment.
  /// Routing matches against this value.
  url : String
  /// The raw query string without the leading `?`; empty when absent.
  /// Fragments (`#...`) are stripped.
  query : String
  /// Case-insensitive request headers (HTTP field names are case-insensitive).
  headers : Map[@http.CaseInsensitiveString, StringView]
  /// The request body. It can be consumed exactly once through `body()`.
  reader : &@io.Reader
}

///|
pub(open) trait BodyReader {
  async fn from_request(req : HttpRequest) -> Self
}

///|
/// Decode the request body with `T`. On native HTTP requests this consumes the
/// network body incrementally when the selected BodyReader supports it.
pub async fn[T : BodyReader] HttpRequest::body(self : HttpRequest) -> T {
  T::from_request(self)
}

///|
pub async fn[T : FromJson] HttpRequest::json(self : HttpRequest) -> T {
  @json.from_json(self.body())
}

///|
async fn HttpRequest::read_all(self : HttpRequest) -> Bytes {
  self.reader.read_all().binary()
}

///|
// 返回 URL 解码后的查询参数键值对。例如 `GET /search?q=moon&page=2`
// 得到 `{ "q": "moon", "page": "2" }`。
pub fn HttpRequest::query(self : HttpRequest) -> Map[String, String] {
  parse_query(self.query)
}

///|
pub impl BodyReader for String with fn from_request(req : HttpRequest) -> String {
  let bytes = req.read_all()
  let arr = bytes.to_array()
  if arr.length() > 0 {
    let mut zero_count = 0
    arr.each(fn(b) { if b == b'\x00' { zero_count = zero_count + 1 } })
    // Some servers may return UTF-16-ish payloads for HTML; printing such
    // strings directly often looks like only a few characters (e.g. " arr.length() {
      let filtered = arr.filter(fn(b) { b != b'\x00' })
      return @utf8.decode(Bytes::from_array(filtered))
    }
  }
  @utf8.decode(bytes)
}

///|
pub impl BodyReader for Json with fn from_request(req : HttpRequest) -> Json {
  @json.parse(@utf8.decode(req.read_all()))
}

///|
/// A parsed `multipart/form-data` request body.
///
/// Each form field is indexed by name. Text fields and uploaded files both use
/// `MultipartFormValue`; an upload has a `filename`, while a normal text field
/// does not.
pub(all) struct FormData {
  fields : Map[String, MultipartFormValue]
  mut response_boundary : String?
}

///|
/// Creates multipart form data for use as a request body or response body.
pub fn FormData::new(fields : Map[String, MultipartFormValue]) -> FormData {
  { fields, response_boundary: None, }
}

///|
/// Looks up a form field or uploaded file by its field name.
pub fn FormData::get(self : FormData, name : String) -> MultipartFormValue? {
  self.fields.get(name)
}

///|
/// Returns all parsed form fields. For duplicate field names, the final value
/// in the multipart body is retained.
pub fn FormData::fields(self : FormData) -> Map[String, MultipartFormValue] {
  self.fields
}

///|
/// Decodes a `multipart/form-data` request, including its boundary parameter.
///
/// The request must include a `Content-Type: multipart/form-data; boundary=…`
/// header. Parsed file data remains in memory as `BytesView`.
pub impl BodyReader for FormData with fn from_request(req : HttpRequest) -> FormData raise {
  let content_type = match req.headers.get("content-type") {
    Some(value) => value
    None => raise MissingContentType
  }
  let parsed = match parse_content_type(content_type) {
    Some(value) => value
    None => raise InvalidContentType(content_type.to_owned())
  }
  if parsed.media_type.to_lower() != "multipart" ||
    parsed.subtype.to_lower() != "form-data" {
    raise UnsupportedContentType(content_type.to_owned())
  }
  let boundary = match parsed.params.get("boundary") {
    Some(value) if value != "" => value
    _ => raise MissingBoundary
  }
  let form = @multipart.Form(req.reader, boundary=boundary.to_owned())
  let fields = Map([])
  while form.next_part() is Some(part) {
    let data = part.read_all().binary()
    let content_type = match part.headers().get("Content-Type") {
      Some(value) => Some(value)
      None => None
    }
    fields.set(part.name(), {
      filename: part.filename(),
      content_type,
      data: data[:],
    })
  }
  FormData::new(fields)
}

///|
pub impl BodyReader for Bytes with fn from_request(req : HttpRequest) -> Bytes raise {
  req.read_all()
}

///|
pub impl BodyReader for FixedArray[Byte] with fn from_request(req : HttpRequest) -> FixedArray[
  Byte,
] raise {
  req.read_all().to_fixedarray()
}

///|
pub impl BodyReader for Array[Byte] with fn from_request(req : HttpRequest) -> Array[
  Byte,
] raise {
  req.read_all().to_array()
}

///|
async test "read_body" {
  let req = HttpRequest::{
    http_method: "POST",
    url: "/",
    query: "",
    headers: Map([]),
    reader: @io.MemoryReader(writer => writer.write(b"{\"Hello\":\"World!\"}")),
  }
  let text_req = HttpRequest::{
    http_method: "POST",
    url: "/",
    query: "",
    headers: Map([]),
    reader: @io.MemoryReader(writer => writer.write(b"{\"Hello\":\"World!\"}")),
  }
  let text : String = text_req.body()
  let json : Json = req.body()
  debug_inspect(
    text,
    content=(
      #|"{\"Hello\":\"World!\"}"
    ),
  )
  json_inspect(json, content={ "Hello": "World!" })
}

///|
async test "form_data_body_reader" {
  let req = HttpRequest::{
    http_method: "POST",
    url: "/upload",
    query: "",
    headers: {
      "Content-Type": "multipart/form-data; boundary=example-boundary",
    },
    reader: @io.MemoryReader(writer => {
      writer.write(
        b"--example-boundary\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\nMoonBit\r\n--example-boundary\r\nContent-Disposition: form-data; name=\"file\"; filename=\"hello.txt\"\r\nContent-Type: text/plain\r\n\r\nhello\r\n--example-boundary--\r\n",
      )
    }),
  }
  let form : FormData = req.body()
  @test.assert_eq(
    form.get("title").map(value => @utf8.decode(value.data) catch { _ => "" }),
    Some("MoonBit"),
  )
  @test.assert_eq(
    form.get("file").bind(value => value.filename),
    Some("hello.txt"),
  )
  @test.assert_eq(
    form.get("file").bind(value => value.content_type),
    Some("text/plain"),
  )
}

///|
async test "form_data_body_reader_requires_multipart_content_type" {
  let req = HttpRequest::{
    http_method: "POST",
    url: "/upload",
    query: "",
    headers: {},
    reader: @io.MemoryReader(writer => writer.write(b"")),
  }
  let result = try {
    let _ : FormData = req.body()
    "parsed"
  } catch {
    MissingContentType => "missing-content-type"
    _ => "unexpected"
  }
  @test.assert_eq(result, "missing-content-type")
}

///|
test "query_parsing" {
  let req = HttpRequest::{
    http_method: "GET",
    url: "/search",
    query: "q=moon&page=2&tag=hello+world",
    headers: Map([]),
    reader: @io.MemoryReader(writer => writer.write(b"")),
  }
  let map = req.query()
  @test.assert_eq(map.get("q").unwrap_or(""), "moon")
  @test.assert_eq(map.get("page").unwrap_or(""), "2")
  @test.assert_eq(map.get("tag").unwrap_or(""), "hello world")
  let empty = HttpRequest::{
    http_method: "GET",
    url: "/plain",
    query: "",
    headers: Map([]),
    reader: @io.MemoryReader(writer => writer.write(b"")),
  }
  @test.assert_eq(empty.query().length(), 0)
}