// 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.
//
// A request that has something to say before its answer — progress reports, a
// log line, or a subscription that stays open — is answered with an SSE stream
// instead of one JSON object. Which of the two a client gets is decided before
// the message is served, from what the request asks for and what it accepts.

///|
/// 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 2026-07-28 request that offers a progress token, or that opens a
/// subscription, is answered with `text/event-stream` when the client accepts
/// it: the notifications the handler sends arrive on that stream, and the
/// reply ends it.
///
/// 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`.
///
/// `docs` names the path the documentation page is served at, beside the
/// endpoint: a browser opened on it lists what the server offers and calls it,
/// in both eras of the protocol. It is on at `/docs` unless told otherwise, as
/// an API framework's interactive docs are; `docs=None` leaves it off, and a
/// path equal to the endpoint's is ignored. `theme` picks its look, the moonmcp
/// website's by default. The page carries no credentials of its own.
pub fn Server::to_asgi(
  self : Server,
  path~ : String = "/mcp",
  origins~ : Array[String] = [],
  docs~ : String? = Some("/docs"),
  theme~ : @docs.Theme = @docs.theme,
  auth~ : @auth.Bearer? = None,
) -> @moonasgi.AsgiApp {
  (scope, receive, send) => {
    match scope {
      Http(hs) => {
        let body = read_body(receive)
        let page = match docs {
          Some(page) if page != path && at_page(hs.path, page) => Some(page)
          _ => None
        }
        match page {
          Some(_) => {
            let (status, headers, payload) = page_reply(
              hs.http_method,
              @docs.page(endpoint=path, theme~),
            )
            send(HttpResponseStart(status~, headers~, trailers=false))
            send(HttpResponseBody(body=payload, more_body=false))
          }
          None => self.over_http(hs, body, path, origins, send, auth)
        }
      }
      WebSocket(_) => send(WebSocketClose(code=1000, reason=""))
      Lifespan(_) =>
        while true {
          match receive() {
            LifespanStartup => send(LifespanStartupComplete)
            LifespanShutdown => {
              send(LifespanShutdownComplete)
              break
            }
            _ => break
          }
        }
    }
  }
}

///|
/// Whether `got` names `want`: the path as given, or with a trailing slash, as
/// a browser asks for a directory.
fn at_page(got : String, want : String) -> Bool {
  got == want || got == want + "/"
}

///|
/// The documentation page for `GET`, its headers alone for `HEAD`, and `405`
/// for anything else. It goes out with `no-store`, so that a browser never shows a
/// listing from before the server's registrations changed.
fn page_reply(
  http_method : String,
  page : String,
) -> (Int32, Array[(String, String)], Bytes) {
  let headers = [
    ("content-type", "text/html; charset=utf-8"),
    ("cache-control", "no-store"),
  ]
  match http_method {
    "GET" => (200, headers, @utf8.encode(page))
    "HEAD" => (200, headers, b"")
    _ =>
      (
        405,
        [("allow", "GET, HEAD"), ("content-type", "text/plain; charset=utf-8")],
        b"Method Not Allowed",
      )
  }
}

///|
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],
  send : @moonasgi.Send,
  auth : @auth.Bearer?,
) -> Unit {
  let plain = [("content-type", "text/plain; charset=utf-8")]
  if hs.path != path && hs.path != path + "/" {
    return reply_json(send, 404, plain, b"Not Found")
  }
  if header(hs.headers, "origin") is Some(origin) &&
    !allowed(origin, header(hs.headers, "host"), origins) {
    return reply_json(send, 403, plain, b"Forbidden")
  }
  if hs.http_method != "POST" {
    return reply_json(
      send,
      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 reply_json(send, 415, plain, b"Unsupported Media Type")
  }
  // The token is checked before the body is read: a caller who is not allowed
  // in learns nothing about the request, not even whether it parses.
  let principal = match auth {
    Some(bearer) =>
      match header(hs.headers, "authorization") {
        Some(value) =>
          match bearer.check(value) {
            Ok(principal) => Some(principal)
            Err(reason) => return challenge(send, reason)
          }
        None => return challenge(send, "a bearer token is required")
      }
    None => None
  }
  let message = @json.parse(@utf8.decode_lossy(body[:])) catch {
    _ =>
      return json_reply(
        send,
        400,
        refusal(Json::null(), fail(parse_error, "Parse error")).1,
      )
  }
  if message is Array(_) {
    return json_reply(
      send,
      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 = match message {
    Object(m) => m.get("params") is Some(Object(p)) && era_of(p) is Modern(_)
    _ => false
  }
  let refused = if modern {
    self.check_headers(hs.headers, message)
  } else {
    legacy_header(hs.headers, message)
  }
  if refused is Some(f) {
    return json_reply(send, 400, refusal(id, f).1)
  }
  if modern && accepts_event_stream(hs.headers) && streams(message) {
    return self.stream_http(hs, message, send, principal)
  }
  let channel = Channel::new(
    emit=fn(_) { () },
    headers=header_map(hs.headers),
    gone=fn() { false },
    subject?=principal.bind(principal => principal.subject),
    claims=principal.map(principal => principal.claims).unwrap_or(Map([])),
  )
  match self.answer(message, channel~) {
    None => reply_json(send, 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(send, status, reply)
    }
  }
}

///|
/// Whether the request has anything to send beside its answer, which is what
/// makes the answer a stream: a progress token, or a subscription that stays
/// open until the client lets go.
fn streams(message : Json) -> Bool {
  match method_of(message) {
    Some(op) if op == @types.method_subscriptions_listen => true
    _ =>
      params_of(message).get("_meta") is Some(Object(meta)) &&
      meta.get("progressToken") is Some(_)
  }
}

///|
/// Whether the client will take an SSE stream: the transport has it list both
/// content types, and this server honours the listing rather than ignoring it.
fn accepts_event_stream(headers : Array[(String, String)]) -> Bool {
  match header(headers, "accept") {
    Some(accept) => accept.contains("text/event-stream")
    None => false
  }
}

///|
/// Serve one request as an SSE stream: the notifications its handler sends,
/// then the reply, then the end of the stream.
///
/// A write that fails means the client has gone; the handler sees that through
/// the context's own notice, which is what closes the connection in this
/// revision, rather than any cancellation message.
async fn Server::stream_http(
  self : Server,
  hs : @moonasgi.HttpScope,
  message : Json,
  send : @moonasgi.Send,
  principal : @auth.Principal?,
) -> Unit {
  let headers = [
    ("content-type", "text/event-stream"),
    ("cache-control", "no-store"),
    ("x-accel-buffering", "no"),
  ]
  send(HttpResponseStart(status=200, headers~, trailers=false))
  let mut alive = true
  let frame = async fn(out : Json) {
    if !alive {
      return
    }
    send(HttpResponseBody(body=@utf8.encode(sse_frame(out)), more_body=true)) catch {
      _ => alive = false
    }
  }
  let channel = Channel::new(
    emit=frame,
    headers=header_map(hs.headers),
    gone=fn() { !alive },
    subject?=principal.bind(principal => principal.subject),
    claims=principal.map(principal => principal.claims).unwrap_or(Map([])),
  )
  match method_of(message) {
    Some(op) if op == @types.method_subscriptions_listen =>
      match request_id_of(message) {
        Some(id) => {
          self.serve_subscription(id, params_of(message), channel) catch {
            _ => ()
          }
          let closed : Json = {
            "jsonrpc": "2.0",
            "id": id,
            "result": subscription_closed_result(id),
          }
          frame(closed) catch {
            _ => ()
          }
        }
        None => ()
      }
    _ =>
      match self.answer(message, channel~) {
        Some((_, reply)) => frame(reply) catch { _ => () }
        None => ()
      }
  }
  send(HttpResponseBody(body=b"", more_body=false)) catch {
    _ => ()
  }
}

///|
/// The answer to a request whose token is missing or does not verify: `401`
/// with the challenge RFC 6750 asks for, and nothing of the request in it.
async fn challenge(send : @moonasgi.Send, reason : String) -> Unit {
  let header = "Bearer realm=\"mcp\", error=\"invalid_token\", " +
    "error_description=\"" +
    reason +
    "\""
  reply_json(
    send,
    401,
    [
      ("www-authenticate", header),
      ("content-type", "text/plain; charset=utf-8"),
    ],
    b"",
  )
}

///|
/// One server-sent event carrying `message`. The stream serves JSON-RPC
/// messages only, so the event name is fixed and no id is set: streams are not
/// resumed.
fn sse_frame(message : Json) -> String {
  "event: message\ndata: " + message.stringify() + "\n\n"
}

///|
/// Every header a request carried, keyed in lower case, as a handler reads
/// them from its context.
fn header_map(headers : Array[(String, String)]) -> Map[String, String] {
  let map : Map[String, String] = Map([])
  for pair in headers {
    map[pair.0] = pair.1
  }
  map
}

///|
async fn reply_json(
  send : @moonasgi.Send,
  status : Int32,
  headers : Array[(String, String)],
  body : Bytes,
) -> Unit {
  send(HttpResponseStart(status~, headers~, trailers=false))
  send(HttpResponseBody(body~, more_body=false))
}

///|
/// A JSON body with its status.
async fn json_reply(
  send : @moonasgi.Send,
  status : Int32,
  reply : Json,
) -> Unit {
  reply_json(
    send,
    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.exact_view(start=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.exact_view(end=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.exact_view(start=9, end=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
  }
}