///|
/// The WebSocket knobs uvicorn exposes on its own config (`--ws-max-size`).
///
/// `max_size` is the ceiling on a received payload, in bytes: it bounds a single frame and, across
/// continuation frames, a reassembled message, and a peer that announces more fails the connection
/// with `1009`. It is the knob with teeth on the receive side — an unbounded length is exactly what
/// a hostile client sends.
///
/// uvicorn's remaining WebSocket knobs are not here rather than here-and-ignored:
/// `--ws-ping-interval` / `--ws-ping-timeout` need a heartbeat task alongside a reader that can be
/// interrupted between messages and resumed, `--ws-max-queue` needs that same reader pumping a
/// bounded queue, and neither is expressible while a native blocking read cannot be cancelled and
/// resumed mid-stream; `--ws-per-message-deflate` needs a raw DEFLATE codec, which the async
/// library's `gzip` package (gzip-framed streams only) does not provide. Accepting the extension
/// without one would corrupt every frame.
pub(all) struct WsConfig {
  max_size : Int
}

///|
/// Build a `WsConfig`, defaulting `max_size` to uvicorn's own 16 MiB.
pub fn WsConfig::new(max_size? : Int = ws_max_size) -> WsConfig {
  { max_size, }
}

///|
/// Server configuration (← uvicorn `Config`): the bind address, the listen
/// backlog, and the HTTP/1.1 transport knobs the `moonbitlang/async` server
/// exposes. Constructed once and handed to `serve_config`.
///
/// `backlog` mirrors uvicorn's `listen(2)` backlog. The current
/// `moonbitlang/async` transport does not expose a backlog setter on its TCP
/// listener — `TcpServer` calls `listen()` itself with a fixed depth and offers
/// no way to re-`listen` — so the value is recorded on the config for parity and
/// future use but is not applied to the socket. This is a transport-capability
/// gap, not a behavioural choice, and it is the one field here that does nothing.
///
/// The remaining fields map one-to-one onto knobs mooncat *does* honour:
/// `dual_stack` / `reuse_addr` on the listening socket, `server_headers`
/// stamped onto every response (uvicorn's `Server:` header lives here),
/// `max_connections` for the parallel-client ceiling, and `allow_failure` for
/// whether a handler error tears the whole server down.
pub(all) struct Config {
  host : String
  port : Int
  backlog : Int
  dual_stack : Bool
  reuse_addr : Bool
  server_headers : Map[String, String]
  max_connections : Int?
  allow_failure : Bool
  graceful_timeout : Int?
  // The path prefix this app is mounted under when something in front strips it
  // (← uvicorn's `--root-path`). It reaches the app as `scope["root_path"]`, which
  // is what lets the app build correct absolute URLs for itself.
  root_path : String
  // What the server says while it runs. Off by default in the sense that a caller
  // can hand in `Logger::silent()`, but a server that reports nothing is one nobody
  // can operate, so the default talks.
  logger : Logger
  // How long an idle keep-alive connection is held open waiting for the next request,
  // in milliseconds (← uvicorn's `--timeout-keep-alive`). A connection nobody is using
  // still costs a socket and a task, and a peer that opens many and sends nothing is
  // how a server runs out of both.
  timeout_keep_alive : Int
  // Stop serving after this many requests (← uvicorn's `--limit-max-requests`), the
  // knob a supervisor uses to recycle a worker before it accumulates whatever it
  // accumulates. `None` serves forever.
  limit_max_requests : Int?
  // The concurrent-request ceiling above which a request is refused with `503`
  // (← uvicorn's `--limit-concurrency`). Distinct from `max_connections`, which makes
  // a client *wait* for a slot; this one answers rather than queues.
  limit_concurrency : Int?
  // The cap on a request's head, request line and header block together, in bytes
  // (← uvicorn's `--h11-max-incomplete-event-size`). A peer that never sends the
  // terminating blank line must not be able to grow the buffer without limit.
  // Applies to the connections mooncat parses itself (HTTPS, and anything else on
  // the self-built HTTP/1.1 codec); the async library's own `ServerConnection`,
  // which the plaintext paths use, bounds its head by rules it does not expose.
  max_head_size : Int
  // Whether to stamp a `Date` header on every response (← uvicorn's `--date-header`).
  date_header : Bool
  // Whether to trust `X-Forwarded-For` / `X-Forwarded-Proto` from a peer in
  // `forwarded_allow_ips` (← uvicorn's `--proxy-headers`, on by default).
  proxy_headers : Bool
  // The peers whose forwarded headers are believed (← uvicorn's
  // `--forwarded-allow-ips`, `127.0.0.1` by default). A single `"*"` trusts every
  // peer, which is only ever right when nothing but a proxy can reach the port.
  forwarded_allow_ips : Array[String]
  // The WebSocket receive limits.
  ws : WsConfig
}

///|
/// Build a `Config`, defaulting to uvicorn's own defaults: host `127.0.0.1`,
/// port `8000`, backlog `2048`, a 5-second idle keep-alive timeout, a 16 KiB
/// request-head cap, the `Date` header on, proxy headers trusted from
/// `127.0.0.1`, and no request-count or concurrency limit. Transport knobs
/// default to the async server's own defaults: `reuse_addr` on (uvicorn sets
/// `SO_REUSEADDR`), single-stack binding, no extra response headers, an
/// unbounded connection ceiling, and `allow_failure` on so a single failing
/// handler never crashes the listener.
///
/// `graceful_timeout` bounds how long `serve_graceful` waits for in-flight
/// requests to drain before it runs lifespan shutdown anyway (← uvicorn's
/// `timeout_graceful_shutdown`); `None` waits until the last request finishes,
/// as uvicorn does by default.
pub fn Config::new(
  host? : String = "127.0.0.1",
  port? : Int = 8000,
  backlog? : Int = 2048,
  dual_stack? : Bool = false,
  reuse_addr? : Bool = true,
  server_headers? : Map[String, String] = Map([("server", "mooncat")]),
  max_connections? : Int? = None,
  allow_failure? : Bool = true,
  graceful_timeout? : Int? = None,
  root_path? : String = "",
  logger? : Logger = Logger::new(),
  timeout_keep_alive? : Int = 5000,
  limit_max_requests? : Int? = None,
  limit_concurrency? : Int? = None,
  max_head_size? : Int = 16 * 1024,
  date_header? : Bool = true,
  proxy_headers? : Bool = true,
  forwarded_allow_ips? : Array[String] = ["127.0.0.1"],
  ws? : WsConfig = WsConfig::new(),
) -> Config {
  {
    host,
    port,
    backlog,
    dual_stack,
    reuse_addr,
    server_headers,
    max_connections,
    allow_failure,
    graceful_timeout,
    root_path,
    logger,
    timeout_keep_alive,
    limit_max_requests,
    limit_concurrency,
    max_head_size,
    date_header,
    proxy_headers,
    forwarded_allow_ips,
    ws,
  }
}

///|
/// The `host:port` string used to resolve the listen address.
pub fn Config::bind(self : Config) -> String {
  "\{self.host}:\{self.port}"
}