///|
/// The WCGI guest: a CGI program (RFC 3875) compiled as a WASI executable. The runner
/// puts the request in the environment, pipes the body to stdin, and turns this
/// process's stdout back into an HTTP response, so one instance serves one request.
///
///   printf 'hi' | wasmer run wcgi.wasm --env REQUEST_METHOD=POST \
///     --env SCRIPT_NAME=/echo --env CONTENT_LENGTH=2
extern "wasm" fn raw_load8(addr : Int32) -> Int32 =
  #|(func (param i32) (result i32) (i32.load8_u (local.get 0)))

///|
extern "wasm" fn raw_load32(addr : Int32) -> Int32 =
  #|(func (param i32) (result i32) (i32.load (local.get 0)))

///|
extern "wasm" fn raw_store8(addr : Int32, value : Int32) -> Unit =
  #|(func (param i32 i32) (i32.store8 (local.get 0) (local.get 1)))

///|
extern "wasm" fn raw_store32(addr : Int32, value : Int32) -> Unit =
  #|(func (param i32 i32) (i32.store (local.get 0) (local.get 1)))

///|
extern "wasm" fn raw_grow(pages : Int32) -> Int32 =
  #|(func (param i32) (result i32) (memory.grow (local.get 0)))

///|
/// The WASI calls the CGI contract needs. Each signature has to match the real one
/// exactly, or Wasmer refuses to link the module.
fn environ_sizes_get(count_ptr : Int32, size_ptr : Int32) -> Int32 = "wasi_snapshot_preview1" "environ_sizes_get"

///|
fn environ_get(ptrs : Int32, buf : Int32) -> Int32 = "wasi_snapshot_preview1" "environ_get"

///|
fn fd_read(
  fd : Int32,
  iovs : Int32,
  iovs_len : Int32,
  nread_ptr : Int32,
) -> Int32 = "wasi_snapshot_preview1" "fd_read"

///|
fn fd_write(
  fd : Int32,
  iovs : Int32,
  iovs_len : Int32,
  nwritten_ptr : Int32,
) -> Int32 = "wasi_snapshot_preview1" "fd_write"

///|
let page_size : Int32 = 65536

///|
/// Claims `bytes` above everything touched so far, so two WASI calls never share a
/// scratch region: `memory.grow` only appends, and a later grow lands above this one.
/// Returns 0, which makes the call fail, when the grow itself did.
fn scratch(bytes : Int32) -> Int32 {
  let pages = (bytes + page_size - 1) / page_size
  let old = raw_grow(pages)
  if old < 0 {
    0
  } else {
    old * page_size
  }
}

///|
/// Writes a `wasi_iovec_t`: `[ptr:u32le][len:u32le]`.
fn iovec(area : Int32, buf : Int32, len : Int32) -> Unit {
  raw_store32(area, buf)
  raw_store32(area + 4, len)
}

///|
/// `String` is UTF-16 on this target, so every wire value is decoded explicitly and a
/// bad sequence reads as empty instead of trapping inside the boundary.
fn decode(raw : Bytes) -> String {
  @utf8.decode(raw) catch {
    _ => ""
  }
}

///|
/// One NUL-terminated `KEY=VALUE` from the environment block, split at the first `=`
/// in byte space so no reinterpretation can move the separator.
fn env_pair(addr : Int32) -> (String, String) {
  let raw : Array[Byte] = []
  let mut eq = -1
  let mut i = 0
  while true {
    let b = raw_load8(addr + i)
    if b == 0 {
      break
    }
    if b == 61 && eq < 0 {
      eq = i
    }
    raw.push(b.to_byte())
    i = i + 1
  }
  guard eq > 0 else { return ("", "") }
  let key : Array[Byte] = []
  let value : Array[Byte] = []
  for j, b in raw.iter2() {
    if j < eq {
      key.push(b)
    } else if j > eq {
      value.push(b)
    }
  }
  (decode(Bytes::from_array(key)), decode(Bytes::from_array(value)))
}

///|
/// The whole CGI environment as `(name, value)` pairs, in the order the host passed it.
fn env_block() -> Array[(String, String)] {
  let head = scratch(64)
  guard environ_sizes_get(head, head + 4) == 0 else { return [] }
  let count = raw_load32(head)
  let size = raw_load32(head + 4)
  guard count > 0 && size > 0 else { return [] }
  let room = scratch(count * 4 + size + 64)
  guard environ_get(room, room + count * 4 + 16) == 0 else { return [] }
  let out : Array[(String, String)] = []
  for i = 0; i < count; i = i + 1 {
    let (key, value) = env_pair(raw_load32(room + i * 4))
    if key.length() > 0 {
      out.push((key, value))
    }
  }
  out
}

///|
fn env_value(env : Array[(String, String)], name : String) -> String {
  for (key, value) in env {
    if key == name {
      return value
    }
  }
  ""
}

///|
/// CGI frames the body with `CONTENT_LENGTH`; an unparsable or negative value is taken
/// to mean there is no body.
fn content_length(env : Array[(String, String)]) -> Int32 {
  let n = @string.parse_int(env_value(env, "CONTENT_LENGTH")) catch { _ => 0 }
  if n > 0 {
    n
  } else {
    0
  }
}

///|
/// Reads up to `length` bytes of stdin. One request per instance means fd 0 holds this
/// body and nothing else, but a pipe can hand over less than asked for at a time, so
/// the read repeats until the frame is full or the host closes it.
fn read_stdin(length : Int32) -> Bytes {
  guard length > 0 else { return [] }
  let area = scratch(length + 128)
  let buf = area + 64
  let collected : Array[Byte] = []
  let mut got = 0
  let mut running = true
  while running && got < length {
    iovec(area, buf + got, length - got)
    let errno = fd_read(0, area, 1, area + 8)
    let nread = raw_load32(area + 8)
    if errno != 0 || nread <= 0 {
      running = false
    } else {
      for i = 0; i < nread; i = i + 1 {
        collected.push(raw_load8(buf + got + i).to_byte())
      }
      got = got + nread
    }
  }
  Bytes::from_array(collected)
}

///|
/// Puts the CGI response on stdout. `println` is avoided deliberately: the body has to
/// reach the runner byte for byte, and this writes the encoded bytes as they are.
fn write_stdout(data : Bytes) -> Unit {
  let n = data.length()
  guard n > 0 else { return }
  let area = scratch(n + 128)
  let buf = area + 64
  for i, b in data.iter2() {
    raw_store8(buf + i, b.to_int())
  }
  iovec(area, buf, n)
  let _ = fd_write(1, area, 1, area + 8)
}

///|
/// The largest body the guest accepts. `CONTENT_LENGTH` reaches the environment
/// before the client sends anything, so a hostile value asks for memory the
/// guest never needs; anything over the cap is refused unread.
fn max_body() -> Int32 {
  1048576
}

///|
/// The application. This is the whole wiring a real service needs: routes, a
/// helper or two, and optionally middleware handlers — everything comes from
/// the `anccnuer/onho` package.
fn app() -> @lib.App {
  let app = @lib.App::new()
  app.use_middleware(@lib.logger())
  app.get("/", _req => @lib.response(200, "hello from onho"))
  app.get("/health", _req => @lib.response(200, "ok"))
  app.post("/echo", req => @lib.response(200, req.body))
  app.get("/header", req => @lib.response(200, "x-demo=\{req.header("x-demo")}"))
  app.get("/search", req => @lib.response(200, "q=\{req.query_param("q")}"))
  app.get("/user/:id", req => @lib.response(200, "user:\{req.param("id")}"))
  app.get("/static/*", req => @lib.response(200, "asset:\{req.param("*")}"))
  app.get("/status.json", _req => {
    @lib.response_json(200, "{\"ok\":true,\"via\":\"onho\"}")
  })
  app.get("/redirect", _req => @lib.redirect("/health"))
  app
}

///|
fn main {
  let env = env_block()
  let declared = content_length(env)
  if declared > max_body() {
    let res = @lib.response(413, "body too large")
    write_stdout(@lib.serialize_cgi(res))
    return ()
  }
  let body = decode(read_stdin(declared))
  let res = match @lib.request_from_cgi(env, body) {
    None => @lib.response(400, "malformed request")
    Some(req) => app().handle(req)
  }
  write_stdout(@lib.serialize_cgi(res))
}