// The message and scope dicts of the specification, as JSON.
//
// The seam carries a typed sum rather than the reference implementation's
// stringly-keyed dicts, but a log line, a trace viewer or a debug tool has to
// show what a reader of the specification expects to see. This module is that
// translation and nothing else: `Json(event)` is the dict the specification
// describes for that message, key for key, and `Event::kind` is its `type`.
//
// ASGI types its byte strings as bytes. JSON has no bytes, so they are written
// as latin-1 text — the same bijection `latin1_decode` uses for headers, one
// code point per byte, so nothing is lost on the way in or out.

///|
/// The specification's name for a message: `http.request`, `websocket.accept`,
/// `lifespan.shutdown.failed`. The `type` key of the dict `Json(event)` writes,
/// on its own — a log line, a metrics label or a match on the message family
/// without building the whole dict.
///
/// An `Other` message is named by the type the two sides agreed on, since that
/// is what it is.
pub fn Event::kind(self : Event) -> String {
  match self {
    HttpRequest(_) => "http.request"
    HttpDisconnect => "http.disconnect"
    HttpResponseStart(_) => "http.response.start"
    HttpResponseBody(_) => "http.response.body"
    HttpResponseTrailers(_) => "http.response.trailers"
    HttpResponsePush(_) => "http.response.push"
    HttpResponsePathSend(_) => "http.response.pathsend"
    HttpResponseZeroCopySend(_) => "http.response.zerocopysend"
    HttpResponseDebug(_) => "http.response.debug"
    HttpResponseEarlyHint(_) => "http.response.early_hint"
    WebSocketConnect => "websocket.connect"
    WebSocketReceive(_) => "websocket.receive"
    WebSocketDisconnect(_) => "websocket.disconnect"
    WebSocketAccept(_) => "websocket.accept"
    WebSocketSendText(_) | WebSocketSendBytes(_) => "websocket.send"
    WebSocketClose(_) => "websocket.close"
    WebSocketHttpResponseStart(_) => "websocket.http.response.start"
    WebSocketHttpResponseBody(_) => "websocket.http.response.body"
    LifespanStartup => "lifespan.startup"
    LifespanShutdown => "lifespan.shutdown"
    LifespanStartupComplete => "lifespan.startup.complete"
    LifespanStartupFailed(_) => "lifespan.startup.failed"
    LifespanShutdownComplete => "lifespan.shutdown.complete"
    LifespanShutdownFailed(_) => "lifespan.shutdown.failed"
    Other(type_~, ..) => type_
  }
}

///|
/// A byte string as the dict carries it: latin-1 text, one code point per byte.
fn bytes_text(bytes : Bytes) -> Json {
  Json::string(latin1_decode(bytes))
}

///|
/// A wire header list, `[["content-type", "text/plain"]]`, each pair a two-element
/// list in the order the specification writes it.
fn headers_value(headers : Array[(String, String)]) -> Json {
  Json::array(
    headers.map(fn(pair) {
      Json::array([Json::string(pair.0), Json::string(pair.1)])
    }),
  )
}

///|
/// A keyed object with `type` first, then the rest in the order the
/// specification lists them.
fn dict(type_ : String, fields : Array[(String, Json)]) -> Json {
  let out = Map([])
  out["type"] = Json::string(type_)
  for pair in fields {
    out[pair.0] = pair.1
  }
  Json::object(out)
}

///|
/// The dict of a scope or a message, shaped as the specification writes it.
///
/// `Json(event)` gives `{"type": "http.response.start", "status": 200,
/// "headers": [["content-type", "text/plain"]], "trailers": false}`, and
/// `Json(scope)` gives the scope dict with `asgi`, the connection metadata and
/// `extensions` in the specification's order. Byte strings are latin-1 text, so
/// `latin1_encode` turns one back into the bytes.
pub impl ToJson for Event with fn to_json(self) {
  match self {
    HttpRequest(body~, more_body~) =>
      dict(self.kind(), [
        ("body", bytes_text(body)),
        ("more_body", Json::boolean(more_body)),
      ])
    HttpDisconnect => dict(self.kind(), [])
    HttpResponseStart(status~, headers~, trailers~) =>
      dict(self.kind(), [
        ("status", integer(status)),
        ("headers", headers_value(headers)),
        ("trailers", Json::boolean(trailers)),
      ])
    HttpResponseBody(body~, more_body~) =>
      dict(self.kind(), [
        ("body", bytes_text(body)),
        ("more_body", Json::boolean(more_body)),
      ])
    HttpResponseTrailers(headers~, more_trailers~) =>
      dict(self.kind(), [
        ("headers", headers_value(headers)),
        ("more_trailers", Json::boolean(more_trailers)),
      ])
    HttpResponsePush(path~, headers~) =>
      dict(self.kind(), [
        ("path", Json::string(path)),
        ("headers", headers_value(headers)),
      ])
    HttpResponsePathSend(path~) =>
      dict(self.kind(), [("path", Json::string(path))])
    // The specification calls the descriptor `file`; `offset` and `count` come
    // from the two-way form and are absent from the one-way one.
    HttpResponseZeroCopySend(fd~, offset~, count~, more_body~) =>
      dict(self.kind(), [
        ("file", integer(fd)),
        ("offset", optional(offset, integer)),
        ("count", optional(count, integer)),
        ("more_body", Json::boolean(more_body)),
      ])
    HttpResponseDebug(info~) => dict(self.kind(), [("info", info)])
    HttpResponseEarlyHint(links~) =>
      dict(self.kind(), [("links", Json::array(links.map(Json::string)))])
    WebSocketConnect => dict(self.kind(), [])
    // One of the two is `null`, as the specification's own example writes it.
    WebSocketReceive(text~, bytes~) =>
      dict(self.kind(), [
        ("text", optional(text, Json::string)),
        ("bytes", optional(bytes, bytes_text)),
      ])
    WebSocketDisconnect(code~, reason~) =>
      dict(self.kind(), [
        ("code", integer(code)),
        ("reason", optional(reason, Json::string)),
      ])
    WebSocketAccept(subprotocol~, headers~) =>
      dict(self.kind(), [
        ("subprotocol", optional(subprotocol, Json::string)),
        ("headers", headers_value(headers)),
      ])
    WebSocketSendText(text) =>
      dict(self.kind(), [("text", Json::string(text)), ("bytes", Json::null())])
    WebSocketSendBytes(bytes) =>
      dict(self.kind(), [("text", Json::null()), ("bytes", bytes_text(bytes))])
    WebSocketClose(code~, reason~) =>
      dict(self.kind(), [
        ("code", integer(code)),
        ("reason", Json::string(reason)),
      ])
    // The WebSocket denial response has no trailers in the specification: a
    // handshake is refused with a status, a header list and a body.
    WebSocketHttpResponseStart(status~, headers~, ..) =>
      dict(self.kind(), [
        ("status", integer(status)),
        ("headers", headers_value(headers)),
      ])
    WebSocketHttpResponseBody(body~, more_body~) =>
      dict(self.kind(), [
        ("body", bytes_text(body)),
        ("more_body", Json::boolean(more_body)),
      ])
    LifespanStartup
    | LifespanShutdown
    | LifespanStartupComplete
    | LifespanShutdownComplete => dict(self.kind(), [])
    LifespanStartupFailed(message=reason)
    | LifespanShutdownFailed(message=reason) =>
      dict(self.kind(), [("message", Json::string(reason))])
    // An agreed-on type is a dict of its own: its payload's keys sit beside
    // `type`, and a payload that is not an object goes under `payload` rather
    // than disappearing.
    Other(type_~, payload~) =>
      match payload {
        Object(fields) => {
          let out = Map([])
          out["type"] = Json::string(type_)
          for key, value in fields {
            if key != "type" {
              out[key] = value
            }
          }
          Json::object(out)
        }
        other => dict(type_, [("payload", other)])
      }
  }
}

///|
/// A number as JSON a reader recognises: `200`, not `200.000000`. `Double` holds
/// every `Int32` and every `Int64` up to 2^53 exactly, and the digits are written
/// from the integer rather than from the `Double`, so a large one stays right.
fn integer(value : Int32) -> Json {
  Json::number(value.to_double(), repr=value.to_string())
}

///|
/// A field the dict carries as `null` when the message did not set it.
fn[T] optional(value : T?, write : (T) -> Json) -> Json {
  match value {
    Some(v) => write(v)
    None => Json::null()
  }
}

///|
/// A `(host, port)` peer as the dict writes it: `["127.0.0.1", 8000]`, or `null`
/// when the transport did not report one.
fn peer(value : (String, Int32)?) -> Json {
  match value {
    Some(host_port) =>
      Json::array([Json::string(host_port.0), integer(host_port.1)])
    None => Json::null()
  }
}

///|
/// A listening `(host, port)` as the dict writes it, with the port optionally
/// unknown: `["127.0.0.1", 8000]` or `["127.0.0.1", null]`.
fn server_peer(value : (String, Int32?)?) -> Json {
  match value {
    Some(host_port) =>
      Json::array([Json::string(host_port.0), optional(host_port.1, integer)])
    None => Json::null()
  }
}

///|
/// The `asgi` sub-dict: the protocol version and the sub-specification's.
pub impl ToJson for AsgiVersion with fn to_json(self) {
  Json::object(
    Map([
      ("version", Json::string(self.version)),
      ("spec_version", Json::string(self.spec_version)),
    ]),
  )
}

///|
/// The `extensions` sub-dict. A server advertises a capability by naming it, with
/// no value of its own — the specification's own example is an empty object — so
/// only the extensions that are on appear, and the ones this seam does not name
/// are copied in as the server wrote them.
pub impl ToJson for Extensions with fn to_json(self) {
  let out = Map([])
  let advertise = fn(name : String) { out[name] = Json::object(Map([])) }
  if self.http_response_push {
    advertise("http.response.push")
  }
  if self.http_response_trailers {
    advertise("http.response.trailers")
  }
  if self.http_response_pathsend {
    advertise("http.response.pathsend")
  }
  if self.http_response_zerocopysend {
    advertise("http.response.zerocopysend")
  }
  if self.http_response_early_hint {
    advertise("http.response.early_hint")
  }
  if self.http_response_debug {
    advertise("http.response.debug")
  }
  if self.websocket_http_response {
    advertise("websocket.http.response")
  }
  for name, value in self.custom {
    out[name] = value
  }
  match self.tls {
    Some(tls) => out["tls"] = tls.to_json()
    None => ()
  }
  Json::object(out)
}

///|
/// The `tls` extension's data, as the specification's dict of the same name.
pub impl ToJson for TlsExtension with fn to_json(self) {
  Json::object(
    Map([
      ("server_cert", optional(self.server_cert, Json::string)),
      (
        "client_cert_chain",
        Json::array(self.client_cert_chain.map(Json::string)),
      ),
      ("client_cert_name", optional(self.client_cert_name, Json::string)),
      ("client_cert_error", optional(self.client_cert_error, Json::string)),
      ("tls_version", optional(self.tls_version, integer)),
      ("cipher_suite", optional(self.cipher_suite, integer)),
    ]),
  )
}

///|
/// The HTTP scope dict: `type`, the `asgi` handshake, the request line, the raw
/// target pieces, the header list, the peers, `extensions` and `state`.
pub impl ToJson for HttpScope with fn to_json(self) {
  dict("http", [
    ("asgi", self.asgi.to_json()),
    ("http_version", Json::string(self.http_version)),
    ("method", Json::string(self.http_method)),
    ("scheme", Json::string(self.scheme)),
    ("path", Json::string(self.path)),
    ("raw_path", optional(self.raw_path, bytes_text)),
    ("query_string", bytes_text(self.query_string)),
    ("root_path", Json::string(self.root_path)),
    ("headers", headers_value(self.headers)),
    ("client", peer(self.client)),
    ("server", server_peer(self.server)),
    ("extensions", self.extensions.to_json()),
    ("state", Json::object(self.state)),
  ])
}

///|
/// The WebSocket scope dict, which is the HTTP one plus the subprotocols on offer
/// and without the method.
pub impl ToJson for WebSocketScope with fn to_json(self) {
  dict("websocket", [
    ("asgi", self.asgi.to_json()),
    ("http_version", Json::string(self.http_version)),
    ("scheme", Json::string(self.scheme)),
    ("path", Json::string(self.path)),
    ("raw_path", optional(self.raw_path, bytes_text)),
    ("query_string", bytes_text(self.query_string)),
    ("root_path", Json::string(self.root_path)),
    ("headers", headers_value(self.headers)),
    ("client", peer(self.client)),
    ("server", server_peer(self.server)),
    ("subprotocols", Json::array(self.subprotocols.map(Json::string))),
    ("extensions", self.extensions.to_json()),
    ("state", Json::object(self.state)),
  ])
}

///|
/// The lifespan scope dict: the handshake and the state a startup seeds.
pub impl ToJson for LifespanScope with fn to_json(self) {
  dict("lifespan", [
    ("asgi", self.asgi.to_json()),
    ("state", Json::object(self.state)),
  ])
}

///|
/// Any scope's dict, whichever of the three it is.
pub impl ToJson for Scope with fn to_json(self) {
  match self {
    Http(scope) => scope.to_json()
    WebSocket(scope) => scope.to_json()
    Lifespan(scope) => scope.to_json()
  }
}

///|
pub extend Event with ToJson::{to_json}

///|
pub extend AsgiVersion with ToJson::{to_json}

///|
pub extend Extensions with ToJson::{to_json}

///|
pub extend TlsExtension with ToJson::{to_json}

///|
pub extend HttpScope with ToJson::{to_json}

///|
pub extend WebSocketScope with ToJson::{to_json}

///|
pub extend LifespanScope with ToJson::{to_json}

///|
pub extend Scope with ToJson::{to_json}