// Streamable HTTP: one POST per message at one endpoint. A 2026-07-28 request
// mirrors its version, method and name into headers so that intermediaries can
// route without reading the body; the server checks that the two agree before
// it trusts either.

///|
/// The server as an ASGI application serving the MCP endpoint at `path`
/// (← FastMCP's `http_app`). Run it with mooncat, or mount it beside a moonapi
/// app.
///
/// Every message is its own POST, answered with one JSON object, or `202` for a
/// notification. Sessions, the `GET` stream and `DELETE` belong to the
/// revisions before 2026-07-28 and are optional there; this server keeps no
/// session, so `GET` and `DELETE` answer `405`.
///
/// A request whose `Origin` header names another site is refused with `403`, as
/// the transport requires against DNS rebinding. An origin passes when its host
/// is the request's own `Host`, a loopback address, or one listed in `origins`.
pub fn Server::to_asgi(
  self : Server,
  path~ : String = "/mcp",
  origins~ : Array[String] = [],
) -> @moonasgi.AsgiApp {
  (scope, receive, send) => {
    match scope {
      Http(hs) => {
        let body = read_body(receive)
        let (status, headers, payload) = self.over_http(hs, body, path, origins)
        send(HttpResponseStart(status~, headers~, trailers=false))
        send(HttpResponseBody(body=payload, more_body=false))
      }
      WebSocket(_) => send(WebSocketClose(code=1000, reason=""))
      Lifespan(_) =>
        while true {
          match receive() {
            LifespanStartup => send(LifespanStartupComplete)
            LifespanShutdown => {
              send(LifespanShutdownComplete)
              break
            }
            _ => break
          }
        }
    }
  }
}

///|
async fn read_body(receive : @moonasgi.Receive) -> Bytes {
  let out = Buffer()
  while true {
    match receive() {
      HttpRequest(body~, more_body~) => {
        out.write_bytes(body)
        if !more_body {
          break
        }
      }
      _ => break
    }
  }
  out.to_bytes()
}

///|
async fn Server::over_http(
  self : Server,
  hs : @moonasgi.HttpScope,
  body : Bytes,
  path : String,
  origins : Array[String],
) -> (Int32, Array[(String, String)], Bytes) {
  let plain = [("content-type", "text/plain; charset=utf-8")]
  if hs.path != path && hs.path != path + "/" {
    return (404, plain, b"Not Found")
  }
  if header(hs.headers, "origin") is Some(origin) &&
    !allowed(origin, header(hs.headers, "host"), origins) {
    return (403, plain, b"Forbidden")
  }
  if hs.http_method != "POST" {
    return (405, [("allow", "POST"), ..plain], b"Method Not Allowed")
  }
  guard header(hs.headers, "content-type") is Some(kind) &&
    kind.to_lower().has_prefix("application/json") else {
    return (415, plain, b"Unsupported Media Type")
  }
  let message = @json.parse(@utf8.decode_lossy(body[:])) catch {
    _ =>
      return json_reply(
        400,
        refusal(Json::null(), fail(parse_error, "Parse error")).1,
      )
  }
  if message is Array(_) {
    return json_reply(
      400,
      refusal(Json::null(), fail(invalid_request, "Batches are not accepted")).1,
    )
  }
  let id = match message {
    Object(m) => m.get("id").unwrap_or(Json::null())
    _ => Json::null()
  }
  let modern = message is Object(m) &&
    m.get("params") is Some(Object(p)) &&
    era_of(p) is Modern(_)
  let refused = if modern {
    self.check_headers(hs.headers, message)
  } else {
    legacy_header(hs.headers, message)
  }
  if refused is Some(f) {
    return json_reply(400, refusal(id, f).1)
  }
  match self.answer(message) {
    None => (202, [], b"")
    Some((code, reply)) => {
      let status = match code {
        Some(c) if c == parse_error || c == invalid_request => 400
        Some(c) if modern && c >= unsupported_version && c <= header_mismatch =>
          400
        Some(c) if modern && c == method_not_found => 404
        _ => 200
      }
      json_reply(status, reply)
    }
  }
}

///|
fn json_reply(
  status : Int32,
  reply : Json,
) -> (Int32, Array[(String, String)], Bytes) {
  (
    status,
    [("content-type", "application/json")],
    @utf8.encode(reply.stringify()),
  )
}

///|
/// The first header named `name`; ASGI carries names in lower case.
fn header(headers : Array[(String, String)], name : String) -> String? {
  headers.iter().find_first(h => h.0 == name).map(h => h.1)
}

///|
/// Whether `origin` may call: the same host the request was sent to, a
/// loopback address, or one of `origins`.
fn allowed(origin : String, host : String?, origins : Array[String]) -> Bool {
  if origins.contains(origin) {
    return true
  }
  let rest = match origin.find("://") {
    Some(k) => origin.view(start_offset=k + 3).to_owned()
    None => return false
  }
  if host is Some(h) && rest == h {
    return true
  }
  let name = match rest.rev_find(":") {
    Some(k) if !rest.has_suffix("]") => rest.view(end_offset=k).to_owned()
    _ => rest
  }
  name is ("localhost" | "127.0.0.1" | "[::1]")
}

///|
/// The revisions before 2026-07-28 name their version in `MCP-Protocol-Version`
/// from 2025-06-18 on; one the server does not speak is refused. `initialize`
/// carries its version in the body instead.
fn legacy_header(headers : Array[(String, String)], message : Json) -> Fail? {
  if message is Object(m) && m.get("method") is Some(String("initialize")) {
    return None
  }
  match header(headers, "mcp-protocol-version") {
    Some(v) if !legacy_versions.contains(v) && !modern_versions.contains(v) =>
      Some(fail(invalid_request, "Unsupported protocol version: \{v}"))
    _ => None
  }
}

///|
/// The checks the 2026-07-28 transport makes before it trusts a request: the
/// version, method and name headers are there and say what the body says, and
/// so does every `Mcp-Param-*` header a tool's schema asks for.
fn Server::check_headers(
  self : Server,
  headers : Array[(String, String)],
  message : Json,
) -> Fail? {
  guard message is Object(m) &&
    m.get("method") is Some(String(op)) &&
    m.get("params") is Some(Object(params)) &&
    era_of(params) is Modern(version) else {
    return None
  }
  let mismatch = (what : String) => {
    Some(fail(header_mismatch, "Header mismatch: \{what}"))
  }
  match header(headers, "mcp-protocol-version") {
    Some(v) if v == version => ()
    Some(v) =>
      return mismatch(
        "MCP-Protocol-Version header value '\{v}' does not match body value '\{version}'",
      )
    None => return mismatch("MCP-Protocol-Version header is missing")
  }
  match header(headers, "mcp-method") {
    Some(v) if v == op => ()
    Some(v) =>
      return mismatch(
        "Mcp-Method header value '\{v}' does not match body value '\{op}'",
      )
    None => return mismatch("Mcp-Method header is missing")
  }
  let named = match op {
    "tools/call" | "prompts/get" => Some("name")
    "resources/read" => Some("uri")
    _ => None
  }
  if named is Some(field) {
    guard params.get(field) is Some(String(want)) else { return None }
    match header(headers, "mcp-name").map(decoded) {
      Some(Some(got)) if got == want => ()
      Some(_) =>
        return mismatch("Mcp-Name header does not match body value '\{want}'")
      None => return mismatch("Mcp-Name header is missing")
    }
  }
  if op == "tools/call" &&
    params.get("name") is Some(String(name)) &&
    self.tools.get(name) is Some(tool) {
    let args = match params.get("arguments") {
      Some(Object(a)) => Json(a)
      _ => Json::object(Map([]))
    }
    for mirrored in mirrored_params(tool.parameters, []) {
      let (label, route) = mirrored
      guard value_at(args, route) is Some(value) && !(value is Null) else {
        continue
      }
      let got = header(headers, "mcp-param-" + label.to_lower()).map(decoded)
      guard got is Some(Some(text)) && same_value(text, value) else {
        return mismatch(
          "Mcp-Param-\{label} header does not match the arguments",
        )
      }
    }
  }
  None
}

///|
/// A header value with the `=?base64?…?=` sentinel decoded; `None` when the
/// sentinel holds no valid Base64 or UTF-8.
fn decoded(value : String) -> String? {
  guard value.has_prefix("=?base64?") && value.has_suffix("?=") else {
    return Some(value)
  }
  let inner = value.view(start_offset=9, end_offset=value.length() - 2)
  let bytes = @base64.decode(inner) catch { _ => return None }
  try @utf8.decode(bytes[:]) catch {
    _ => None
  } noraise {
    text => Some(text)
  }
}

///|
/// Every `x-mcp-header` in `schema`, with the chain of `properties` keys that
/// leads to it; the annotation is only defined where that chain is all there
/// is.
fn mirrored_params(
  schema : Json,
  route : Array[String],
) -> Array[(String, Array[String])] {
  let out = []
  guard schema is Object(s) && s.get("properties") is Some(Object(props)) else {
    return out
  }
  for key, sub in props {
    let here = [..route, key]
    if sub is Object(o) && o.get("x-mcp-header") is Some(String(label)) {
      out.push((label, here))
    }
    for found in mirrored_params(sub, here) {
      out.push(found)
    }
  }
  out
}

///|
fn value_at(json : Json, route : Array[String]) -> Json? {
  let mut at = json
  for key in route {
    guard at is Object(o) && o.get(key) is Some(next) else { return None }
    at = next
  }
  Some(at)
}

///|
/// Whether a header value stands for `value`: a string as written, a boolean
/// as `true` or `false`, and a number compared as a number, so `42` and `42.0`
/// agree.
fn same_value(text : String, value : Json) -> Bool {
  match value {
    String(s) => text == s
    True => text == "true"
    False => text == "false"
    Number(n, ..) =>
      (@string.parse_double(text) catch { _ => return false }) == n
    _ => false
  }
}