///|
/// The connection scope: one value per HTTP request, WebSocket connection, or
/// lifespan run. It carries the immutable connection metadata a server hands to
/// an application, mirroring ASGI 3.0's `scope` dict as a typed sum.
pub(all) enum Scope {
  Http(HttpScope)
  WebSocket(WebSocketScope)
  Lifespan(LifespanScope)
}

///|
/// The `asgi` sub-dict every scope carries: the ASGI protocol `version`
/// (`"3.0"`) and the `spec_version` of the concrete http / websocket / lifespan
/// sub-spec the server implements. Applications negotiate optional behaviour
/// against `spec_version` with `at_least`.
pub(all) struct AsgiVersion {
  version : String
  spec_version : String
} derive(Eq)

///|
/// The default ASGI handshake: protocol `3.0`, sub-spec `2.5` (the current
/// HTTP / WebSocket spec revision, 2024-06-05). An alias of `http` — the common
/// case.
pub fn AsgiVersion::default() -> AsgiVersion {
  AsgiVersion::http()
}

///|
/// The HTTP handshake: protocol `3.0`, HTTP sub-spec `2.5`. This is the revision
/// that carries `state`, the extension message set (`http.response.trailers` /
/// `http.response.push` / `http.response.pathsend` / `http.response.early_hint`),
/// and the `raw_path` guarantee.
pub fn AsgiVersion::http() -> AsgiVersion {
  { version: "3.0", spec_version: "2.5" }
}

///|
/// The WebSocket handshake: protocol `3.0`, WebSocket sub-spec `2.5` — the
/// revision that adds `reason` to the disconnect event, alongside `state` and the
/// `websocket.http.response` denial extension.
pub fn AsgiVersion::websocket() -> AsgiVersion {
  { version: "3.0", spec_version: "2.5" }
}

///|
/// The Lifespan handshake: protocol `3.0`, lifespan sub-spec `2.0` (the revision
/// that carries `state`). Lifespan versions independently of http/websocket, so
/// a lifespan scope must not borrow the http `spec_version`.
pub fn AsgiVersion::lifespan() -> AsgiVersion {
  { version: "3.0", spec_version: "2.0" }
}

///|
/// Strict `spec_version` negotiation: is this scope's sub-spec at least
/// `major.minor`? Parses the dotted `spec_version` (non-digit runs count as 0)
/// and compares numerically, so `"2.4".at_least(major=2, minor=3)` is true.
/// An application guards a version-gated feature with this instead of string
/// equality.
pub fn AsgiVersion::at_least(
  self : AsgiVersion,
  major~ : Int,
  minor~ : Int,
) -> Bool {
  let mut maj = 0
  let mut min = 0
  let mut after_dot = false
  for i in 0..= 48 && c <= 57 {
      if after_dot {
        min = min * 10 + (c - 48)
      } else {
        maj = maj * 10 + (c - 48)
      }
    }
  }
  maj > major || (maj == major && min >= minor)
}

///|
/// The `tls` extension's per-connection data (ASGI TLS extension). Present in a
/// scope's `extensions` only when the connection is TLS-terminated by the
/// server. Certificates are PEM text; `tls_version` / `cipher_suite` are the
/// numeric IANA identifiers, `None` when the server does not expose them.
pub(all) struct TlsExtension {
  server_cert : String?
  client_cert_chain : Array[String]
  client_cert_name : String?
  client_cert_error : String?
  tls_version : Int?
  cipher_suite : Int?
} derive(Eq)

///|
/// The scope's `extensions` map, modelled as typed capability flags plus the
/// one extension that carries data (`tls`). A server sets a flag to advertise
/// that it will honour the matching outbound event — an application checks the
/// flag before emitting `HttpResponsePush` / `HttpResponsePathSend` /
/// `HttpResponseEarlyHint` / `HttpResponseTrailers` /
/// `WebSocketHttpResponseStart`, exactly as an ASGI app tests
/// `"http.response.push" in scope["extensions"]`.
pub(all) struct Extensions {
  tls : TlsExtension?
  http_response_push : Bool
  http_response_trailers : Bool
  http_response_pathsend : Bool
  http_response_early_hint : Bool
  websocket_http_response : Bool
} derive(Eq)

///|
/// No extensions advertised: every capability off, no TLS data. The starting
/// point servers and tests build from with the `enable_*` / `with_tls` helpers.
pub fn Extensions::none() -> Extensions {
  {
    tls: None,
    http_response_push: false,
    http_response_trailers: false,
    http_response_pathsend: false,
    http_response_early_hint: false,
    websocket_http_response: false,
  }
}

///|
/// Advertise the server-push extension (`http.response.push`).
pub fn Extensions::enable_push(self : Extensions) -> Extensions {
  { ..self, http_response_push: true }
}

///|
/// Advertise the response-trailers extension (`http.response.trailers`).
pub fn Extensions::enable_trailers(self : Extensions) -> Extensions {
  { ..self, http_response_trailers: true }
}

///|
/// Advertise the path-send extension (`http.response.pathsend`).
pub fn Extensions::enable_pathsend(self : Extensions) -> Extensions {
  { ..self, http_response_pathsend: true }
}

///|
/// Advertise the early-hints extension (`http.response.early_hint`): the server
/// will forward `HttpResponseEarlyHint` messages as `103 Early Hints`
/// informational responses ahead of the final status.
pub fn Extensions::enable_early_hint(self : Extensions) -> Extensions {
  { ..self, http_response_early_hint: true }
}

///|
/// Advertise the WebSocket-denial-with-response extension
/// (`websocket.http.response`).
pub fn Extensions::enable_websocket_http_response(
  self : Extensions,
) -> Extensions {
  { ..self, websocket_http_response: true }
}

///|
/// Attach `tls` extension data to a connection scope.
pub fn Extensions::with_tls(
  self : Extensions,
  tls : TlsExtension,
) -> Extensions {
  { ..self, tls: Some(tls) }
}

///|
/// HTTP connection scope (ASGI `type == "http"`). `raw_path` / `query_string`
/// stay `Bytes` because they are not guaranteed valid UTF-8; header names follow
/// ASGI's lowercased-latin1 convention. `asgi` carries the version handshake,
/// `extensions` the advertised server capabilities, and `state` is
/// per-connection scratch copied from the lifespan state.
pub(all) struct HttpScope {
  http_version : String
  http_method : String
  scheme : String
  path : String
  raw_path : Bytes
  query_string : Bytes
  root_path : String
  headers : Array[(String, String)]
  client : (String, Int)?
  server : (String, Int)?
  asgi : AsgiVersion
  extensions : Extensions
  state : Map[String, Json]
}

///|
/// Build an `HttpScope` with ASGI's usual defaults filled in — `http_version`
/// `"1.1"`, `scheme` `"http"`, empty query/headers/state, the default `asgi`
/// handshake, and no extensions. `raw_path` defaults to the UTF-8 encoding of
/// `path`. The ergonomic constructor servers and the `TestClient` build scopes
/// through, so callers spell only what differs from the common case.
pub fn HttpScope::new(
  http_method~ : String,
  path~ : String,
  http_version? : String = "1.1",
  scheme? : String = "http",
  raw_path? : Bytes,
  query_string? : Bytes = b"",
  root_path? : String = "",
  headers? : Array[(String, String)] = [],
  client? : (String, Int)? = None,
  server? : (String, Int)? = None,
  asgi? : AsgiVersion = AsgiVersion::default(),
  extensions? : Extensions = Extensions::none(),
  state? : Map[String, Json] = Map([]),
) -> HttpScope {
  {
    http_version,
    http_method,
    scheme,
    path,
    raw_path: raw_path.unwrap_or(@utf8.encode(path)),
    query_string,
    root_path,
    headers,
    client,
    server,
    asgi,
    extensions,
    state,
  }
}

///|
/// WebSocket connection scope (ASGI `type == "websocket"`). `scheme` is
/// `"ws"`/`"wss"`; `subprotocols` are the client-offered values. Carries the
/// same `asgi` handshake and `extensions` capabilities as an http scope.
pub(all) struct WebSocketScope {
  http_version : String
  scheme : String
  path : String
  raw_path : Bytes
  query_string : Bytes
  root_path : String
  headers : Array[(String, String)]
  client : (String, Int)?
  server : (String, Int)?
  subprotocols : Array[String]
  asgi : AsgiVersion
  extensions : Extensions
  state : Map[String, Json]
}

///|
/// Build a `WebSocketScope` with ASGI's usual defaults filled in — `scheme`
/// `"ws"`, empty query/headers/subprotocols/state, the default `asgi`
/// handshake, and no extensions. `raw_path` defaults to the UTF-8 encoding of
/// `path`.
pub fn WebSocketScope::new(
  path~ : String,
  http_version? : String = "1.1",
  scheme? : String = "ws",
  raw_path? : Bytes,
  query_string? : Bytes = b"",
  root_path? : String = "",
  headers? : Array[(String, String)] = [],
  client? : (String, Int)? = None,
  server? : (String, Int)? = None,
  subprotocols? : Array[String] = [],
  asgi? : AsgiVersion = AsgiVersion::default(),
  extensions? : Extensions = Extensions::none(),
  state? : Map[String, Json] = Map([]),
) -> WebSocketScope {
  {
    http_version,
    scheme,
    path,
    raw_path: raw_path.unwrap_or(@utf8.encode(path)),
    query_string,
    root_path,
    headers,
    client,
    server,
    subprotocols,
    asgi,
    extensions,
    state,
  }
}

///|
/// Lifespan scope (ASGI `type == "lifespan"`): a single run spanning process
/// startup and shutdown, whose `state` seeds every request scope. Carries its
/// own `asgi` handshake — lifespan versions independently (sub-spec `2.0`), so
/// it does not borrow the http/websocket `spec_version`.
pub(all) struct LifespanScope {
  asgi : AsgiVersion
  state : Map[String, Json]
}

///|
/// Build a `LifespanScope` with the lifespan `asgi` handshake (sub-spec `2.0`)
/// and an empty `state` by default.
pub fn LifespanScope::new(
  asgi? : AsgiVersion = AsgiVersion::lifespan(),
  state? : Map[String, Json] = Map([]),
) -> LifespanScope {
  { asgi, state }
}

///|
/// A protocol event flowing between server and application. Replaces ASGI's
/// stringly-typed message dicts with a typed sum covering the http / websocket /
/// lifespan message sets in both directions, including the standard extension
/// messages (server push, path-send, zero-copy send, response trailers, early
/// hints, debug, and WebSocket denial with a full HTTP response).
pub(all) enum Event {
  HttpRequest(body~ : Bytes, more_body~ : Bool)
  HttpDisconnect
  HttpResponseStart(
    status~ : Int,
    headers~ : Array[(String, String)],
    trailers~ : Bool
  )
  HttpResponseBody(body~ : Bytes, more_body~ : Bool)
  // `http.response.trailers` extension: trailing headers sent after the body,
  // only valid when `HttpResponseStart.trailers` was `true`.
  HttpResponseTrailers(
    headers~ : Array[(String, String)],
    more_trailers~ : Bool
  )
  // `http.response.push` extension: a server-initiated push promise for `path`.
  HttpResponsePush(path~ : String, headers~ : Array[(String, String)])
  // `http.response.pathsend` extension: hand a file `path` to the server to
  // serve zero-copy, emitted after `HttpResponseStart`.
  HttpResponsePathSend(path~ : String)
  // `http.response.zerocopysend` extension: hand an open file descriptor to the
  // server to send zero-copy, from `offset` for `count` bytes (both optional —
  // `None` means "from the current position" / "to end of file"), emitted after
  // `HttpResponseStart`. `more_body` chains further body/zerocopysend messages.
  HttpResponseZeroCopySend(
    fd~ : Int,
    offset~ : Int?,
    count~ : Int?,
    more_body~ : Bool
  )
  // `http.response.debug` extension: an out-of-band `info` map a server may log
  // or surface in a debug UI. Carries no protocol meaning and is valid at any
  // point in the response.
  HttpResponseDebug(info~ : Json)
  // `http.response.early_hint` extension: a `103 Early Hints` informational
  // response carrying `Link` header values, emitted before `HttpResponseStart`.
  // May be sent repeatedly; each message is its own 103 response.
  HttpResponseEarlyHint(links~ : Array[String])
  WebSocketConnect
  WebSocketReceive(text~ : String?, bytes~ : Bytes?)
  // ASGI 2.5 added `reason` to the disconnect event: the close reason the peer
  // sent, or `None` when it sent none (the code then defaults to `1005`).
  WebSocketDisconnect(code~ : Int, reason~ : String?)
  WebSocketAccept(subprotocol~ : String?, headers~ : Array[(String, String)])
  WebSocketSendText(String)
  WebSocketSendBytes(Bytes)
  WebSocketClose(code~ : Int, reason~ : String)
  // `websocket.http.response` extension: deny the handshake with a real HTTP
  // response instead of a bare close (start + body, mirroring the http path).
  WebSocketHttpResponseStart(status~ : Int, headers~ : Array[(String, String)])
  WebSocketHttpResponseBody(body~ : Bytes, more_body~ : Bool)
  LifespanStartup
  LifespanShutdown
  LifespanStartupComplete
  LifespanStartupFailed(message~ : String)
  LifespanShutdownComplete
  LifespanShutdownFailed(message~ : String)
} derive(Eq)

///|
/// Pull the next inbound event. The async awaitable an application calls to read
/// request body chunks, websocket frames, or lifespan signals.
pub type Receive = async () -> Event

///|
/// Push an outbound event. The async awaitable an application calls to emit
/// response start/body, websocket frames, or lifespan completion.
pub type Send = async (Event) -> Unit

///|
/// The load-bearing ASGI callable: `(scope, receive, send)`. Every server binds
/// to this shape and every framework in the suite ultimately compiles down to it.
pub type AsgiApp = async (Scope, Receive, Send) -> Unit