// Serving: the ASGI application an `App` compiles to. HTTP requests are matched to routes, then
// mounts, then answered 405 or 404; WebSocket connections the same way; the lifespan runs the
// hooks. Every request goes this one way, the layers outermost.

///|
/// Compile the app to an ASGI application a server can run: the layers around the router, in
/// registration order with the first outermost.
pub fn App::to_asgi(self : App) -> @moonasgi.AsgiApp {
  let mounted = self.mounts.map(entry => {
    let (prefix, target) = entry
    let app = match target {
      Sub(sub) => sub.to_asgi()
      Asgi(app) => app
    }
    (prefix, app)
  })
  let router : @moonasgi.AsgiApp = (scope, receive, send) => {
    match scope {
      Http(hs) => self.serve_http(hs, receive, send, mounted)
      WebSocket(ws) => self.serve_websocket(ws, receive, send, mounted)
      Lifespan(ls) => self.serve_lifespan(ls, receive, send)
    }
  }
  @mid.stack(self.layers, router)
}

///|
/// Run `request` through the app in-process and return the response, the way a server would
/// see it answered: the same ASGI path, layers included (← Starlette's `TestClient`). A
/// streamed response comes back with its body joined.
pub async fn App::handle(
  self : App,
  request : @moonasgi.Request,
) -> @moonasgi.Response {
  let query = @utf8.decode_lossy(request.query_string)
  let target = if query == "" {
    request.path
  } else {
    request.path + "?" + query
  }
  let r = @testing.Client::new(self.to_asgi()).request(
    http_method=request.http_method,
    path=target,
    headers=request.headers,
    body=request.body,
  )
  @moonasgi.Response::new(r.status, r.headers, r.body)
}

///|
/// The method a request names. A name that is not a token matches no route.
fn parse_method(s : String) -> @route.Method? {
  try @method.Method::of(s) catch {
    _ => None
  } noraise {
    m => Some(m)
  }
}

///|
/// Match a route pattern against a request path, extracting `:name` params.
fn match_path(pattern : String, path : String) -> Map[String, String]? {
  let pp = @route.segments(pattern)
  let ph = @route.segments(path)
  if pp.length() != ph.length() {
    return None
  }
  let params : Map[String, String] = Map([])
  for i, seg in pp {
    if seg.length() > 0 && seg[0] == ':' {
      params[seg[1:].to_owned()] = ph[i]
    } else if seg != ph[i] {
      return None
    }
  }
  Some(params)
}

///|
/// Strip a mount `prefix` off `path` by whole segments, returning the sub-app's
/// path (`"/"` when the request hits the mount root). `None` when `path` does not
/// lie under `prefix` — the comparison is segment-wise, so `/sub` does not match
/// `/subway`.
fn strip_prefix(prefix : String, path : String) -> String? {
  let pp = @route.segments(prefix)
  let ph = @route.segments(path)
  if pp.length() > ph.length() {
    return None
  }
  for i, seg in pp {
    if seg != ph[i] {
      return None
    }
  }
  let rest = ph[pp.length():].iter().map(s => "/" + s).collect().join("")
  Some(if rest == "" { "/" } else { rest })
}

///|
/// The first mount `path` lies under, with the path that remains and the prefix in the form
/// `root_path` grows by.
fn mount_for(
  mounted : Array[(String, @moonasgi.AsgiApp)],
  path : String,
) -> (@moonasgi.AsgiApp, String, String)? {
  for entry in mounted {
    let (prefix, app) = entry
    if strip_prefix(prefix, path) is Some(rest) {
      return Some((app, rest, join_prefix(prefix, "")))
    }
  }
  None
}

///|
/// `raw` without the octets of `prefix` it starts with; unchanged when it does not.
fn strip_raw(raw : Bytes, prefix : String) -> Bytes {
  let p = @utf8.encode(prefix)
  if raw.length() >= p.length() && raw[:p.length()] == p[:] {
    let rest = raw[p.length():].to_owned()
    if rest.length() == 0 {
      b"/"
    } else {
      rest
    }
  } else {
    raw
  }
}

///|
/// The request a handler sees: the scope's line and headers with the body read in full.
fn request_of(hs : @moonasgi.HttpScope, body : Bytes) -> @moonasgi.Request {
  {
    http_method: hs.http_method,
    path: hs.path,
    query_string: hs.query_string,
    headers: hs.headers,
    body,
  }
}

///|
/// The request body, read to the end.
async fn read_body(receive : @moonasgi.Receive) -> Bytes {
  let buf = Buffer()
  for ;; {
    match receive() {
      HttpRequest(body~, more_body~) => {
        buf.write_bytes(body)
        if !more_body {
          break
        }
      }
      _ => break
    }
  }
  buf.to_bytes()
}

///|
/// Send `resp` whole. The body goes out for `HEAD` as well: the server drops it and keeps the
/// length it gives, which is what a `HEAD` answer is to carry.
async fn send_response(
  send : @moonasgi.Send,
  resp : @moonasgi.Response,
) -> Unit {
  send(
    HttpResponseStart(status=resp.status, headers=resp.headers, trailers=false),
  )
  send(HttpResponseBody(body=resp.body, more_body=false))
}

///|
/// Send `stream`: the early hints, the head, then the body as `stream.body` writes it, run beside
/// a watch on the connection that cancels it when the client goes (← Starlette's
/// `listen_for_disconnect`). For `HEAD` the body is not produced at all.
async fn send_stream(
  stream : @status.Stream,
  receive : @moonasgi.Receive,
  send : @moonasgi.Send,
  head : Bool,
) -> Unit {
  for links in stream.early_hints {
    send(HttpResponseEarlyHint(links~))
  }
  let trailing = stream.trailers && !head
  send(
    HttpResponseStart(
      status=stream.status,
      headers=stream.headers,
      trailers=trailing,
    ),
  )
  if head {
    send(HttpResponseBody(body=b"", more_body=false))
    return
  }
  let writer = @status.Writer::new(chunk => {
    send(HttpResponseBody(body=chunk, more_body=true))
  })
  let gone = Ref(false)
  @async.with_task_group(g => {
    g.spawn_bg(no_wait=true, () => {
      while !(receive() is HttpDisconnect) {

      }
      gone.val = true
      g.return_immediately(())
    })
    (stream.body)(writer)
  })
  if gone.val {
    return
  }
  send(HttpResponseBody(body=b"", more_body=false))
  if trailing {
    send(HttpResponseTrailers(headers=writer.trailers(), more_trailers=false))
  }
}

///|
/// An HTTP request: the first route whose path and method match, else the first mount it lies
/// under, else `405` (a path matched, no method did) or `404`, through the status handlers.
async fn App::serve_http(
  self : App,
  hs : @moonasgi.HttpScope,
  receive : @moonasgi.Receive,
  send : @moonasgi.Send,
  mounted : Array[(String, @moonasgi.AsgiApp)],
) -> Unit {
  let verb = parse_method(hs.http_method)
  let allowed : Array[String] = []
  for route in self.router.routes {
    guard match_path(route.path, hs.path) is Some(params) else { continue }
    // A GET route answers HEAD too, as Starlette's does.
    if verb is Some(v) && (route.verb == v || (v is Head && route.verb is Get)) {
      self.answer(route, params, hs, receive, send, head=verb is Some(Head))
      return
    }
    allowed.push(route.verb.name())
    if route.verb is Get {
      allowed.push("HEAD")
    }
  }
  if mount_for(mounted, hs.path) is Some((app, rest, prefix)) {
    let scope : @moonasgi.HttpScope = {
      ..hs,
      path: rest,
      raw_path: strip_raw(hs.raw_path, prefix),
      root_path: hs.root_path + prefix,
    }
    app(Http(scope), receive, send)
    return
  }
  let ctx = @ctx.Context::new(request_of(hs, b""), scope=hs)
  let resp = if allowed.length() > 0 {
    // RFC 9110 §15.5.6 requires the Allow header on a 405; a client that probes
    // a resource reads it rather than guessing.
    http_exception_response(405, Json("Method Not Allowed"), [
      ("allow", dedupe(allowed).join(", ")),
    ])
  } else {
    http_exception_response(404, Json("Not Found"), [])
  }
  send_response(send, self.apply_status(ctx, resp))
}

///|
/// Answer a matched route: read the body, run the route, send what it produced, then run the
/// background tasks.
async fn App::answer(
  self : App,
  route : @route.Route,
  params : Map[String, String],
  hs : @moonasgi.HttpScope,
  receive : @moonasgi.Receive,
  send : @moonasgi.Send,
  head~ : Bool,
) -> Unit {
  let ctx = @ctx.Context::new(
    request_of(hs, read_body(receive)),
    params~,
    scope=hs,
  )
  match self.run_route(route, ctx) {
    Buffered(resp) => send_response(send, resp)
    Streamed(stream) => send_stream(stream, receive, send, head)
  }
  ctx.background.run()
}

///|
/// Security, then validation, then the declared dependencies around the handler. Whatever
/// fails becomes a response here, so what comes back is always something to send.
async fn App::run_route(
  self : App,
  route : @route.Route,
  ctx : @ctx.Context,
) -> @route.Reply {
  if self.enforce_security(ctx, route.security) is Some(denied) {
    return Buffered(self.apply_status(ctx, denied))
  }
  if route.validate && route.endpoint is Some(ep) {
    let errs = ep.validate(ctx)
    if errs.length() > 0 {
      return Buffered(self.apply_status(ctx, @schema.unprocessable(errs)))
    }
  }
  self.run_with_deps(route.dependencies, () => (route.run)(ctx)) catch {
    e => Buffered(self.dispatch_exception(ctx, e))
  }
}

///|
/// The error a route's unprovidable dependency is answered with. It is a `500`
/// because the fault is the app's wiring, not the request.
fn unresolved_dep(key : String) -> Error {
  @exceptions.http_error(500, "Unresolved dependency: " + key)
}

///|
/// Resolve a route's declared dependencies into one request scope, run `body`,
/// then close the scope — handing whatever error the body raised to the
/// teardowns before re-raising it, so a `yield` dependency's cleanup sees the
/// failure. A route with no dependencies never opens a scope.
async fn App::run_with_deps(
  self : App,
  keys : Array[String],
  body : async () -> @route.Reply,
) -> @route.Reply {
  if keys.is_empty() {
    return body()
  }
  let scope = match self.deps {
    Some(d) => (d.open)()
    None => raise unresolved_dep(keys[0])
  }
  if keys.iter().find_first(k => !(scope.resolve)(k)) is Some(missing) {
    // Whatever resolved before the gap still has to be released.
    let err = unresolved_dep(missing)
    (scope.close)(Some(err))
    raise err
  }
  let outcome : Result[@route.Reply, Error] = Ok(body()) catch {
    err => Err(err)
  }
  match outcome {
    Ok(reply) => {
      (scope.close)(None)
      reply
    }
    Err(err) => {
      (scope.close)(Some(err))
      raise err
    }
  }
}

///|
/// Enforce a route's security requirements. `None` means allow (nothing to
/// enforce, or a requirement passed); `Some(resp)` is the 401/403 to return.
/// Requirements are alternatives (OpenAPI OR-semantics): the first enforceable
/// one that passes admits the request; if every enforceable requirement fails,
/// the last failure response is returned. A requirement naming a
/// documentation-only scheme (no enforcer) grants nothing by itself.
fn App::enforce_security(
  self : App,
  ctx : @ctx.Context,
  reqs : Array[@security.SecurityRequirement],
) -> @moonasgi.Response? {
  if reqs.is_empty() {
    return None
  }
  let now = (self.clock)()
  let mut enforceable = false
  let mut last : @moonasgi.Response? = None
  for r in reqs {
    guard self.enforcers.get(r.scheme) is Some(enf) else { continue }
    enforceable = true
    match enf(ctx, @security.SecurityScopes::new(scopes=r.scopes), now) {
      Ok(_) => return None
      Err(resp) => last = Some(resp)
    }
  }
  if enforceable {
    last
  } else {
    None
  }
}

///|
/// Rewrite an error-path response through a registered per-status handler, if one
/// is registered for its status; otherwise pass it through unchanged.
fn App::apply_status(
  self : App,
  ctx : @ctx.Context,
  resp : @moonasgi.Response,
) -> @moonasgi.Response {
  match self.status_handlers.get(resp.status) {
    Some(h) => h(ctx)
    None => resp
  }
}

///|
/// Try each registered exception handler in order; the first to claim the error
/// wins outright. Otherwise fall through to the built-in mapping, which a
/// per-status handler may still rewrite (a custom `500` page, say).
fn App::dispatch_exception(
  self : App,
  ctx : @ctx.Context,
  err : Error,
) -> @moonasgi.Response {
  for h in self.exception_handlers {
    if h(ctx, err) is Some(resp) {
      return resp
    }
  }
  self.apply_status(ctx, default_exception_response(err))
}

///|
/// The built-in fallback when no registered handler claims a raised error: an
/// `HttpException` becomes its own status and detail; anything else is a
/// `500 {"detail": "Internal Server Error"}`. This is always in effect, so a
/// handler that raises `HttpException` needs no explicit registration.
fn default_exception_response(err : Error) -> @moonasgi.Response {
  match err {
    @exceptions.HttpException(status~, detail~, headers~) =>
      http_exception_response(status, detail, headers)
    _ => http_exception_response(500, Json("Internal Server Error"), [])
  }
}

///|
/// The response for an `HttpException`: `{"detail": }` as JSON, at the
/// given status, with the exception's extra headers merged in after the
/// content-type.
fn http_exception_response(
  status : Int32,
  detail : Json,
  headers : Array[(String, String)],
) -> @moonasgi.Response {
  @moonasgi.Response::new(
    status,
    [("content-type", "application/json"), ..headers],
    @moonjson.dump({ "detail": detail }),
  )
}

///|
/// The distinct entries of `names`, in first-seen order.
fn dedupe(names : Array[String]) -> Array[String] {
  let out : Array[String] = []
  for n in names {
    if !out.contains(n) {
      out.push(n)
    }
  }
  out
}

///|
/// A WebSocket connection: the first route whose path matches, else the first mount, else a
/// close, which a server answers with `403`. A handler that ends on the peer's disconnect ends
/// the connection quietly.
async fn App::serve_websocket(
  self : App,
  ws : @moonasgi.WebSocketScope,
  receive : @moonasgi.Receive,
  send : @moonasgi.Send,
  mounted : Array[(String, @moonasgi.AsgiApp)],
) -> Unit {
  for route in self.router.ws_routes {
    guard match_path(route.path, ws.path) is Some(params) else { continue }
    (route.handler)(@socket.WebSocket::new(ws, receive, send, params~)) catch {
      @socket.Disconnected(..) => ()
      e => raise e
    }
    return
  }
  if mount_for(mounted, ws.path) is Some((app, rest, prefix)) {
    let scope : @moonasgi.WebSocketScope = {
      ..ws,
      path: rest,
      raw_path: strip_raw(ws.raw_path, prefix),
      root_path: ws.root_path + prefix,
    }
    app(WebSocket(scope), receive, send)
    return
  }
  send(WebSocketClose(code=1000, reason=""))
}

///|
/// The lifespan protocol: run the startup hooks on `lifespan.startup` and the shutdown hooks on
/// `lifespan.shutdown`, with the scope's state. A failed startup ends the protocol there, as ASGI
/// has the server abort the boot and never send `lifespan.shutdown`.
async fn App::serve_lifespan(
  self : App,
  scope : @moonasgi.LifespanScope,
  receive : @moonasgi.Receive,
  send : @moonasgi.Send,
) -> Unit {
  for ;; {
    match receive() {
      LifespanStartup =>
        match self.start(scope.state) {
          None => send(LifespanStartupComplete)
          Some(why) => {
            send(LifespanStartupFailed(message=why))
            return
          }
        }
      LifespanShutdown => {
        match self.stop(scope.state) {
          None => send(LifespanShutdownComplete)
          Some(why) => send(LifespanShutdownFailed(message=why))
        }
        return
      }
      _ => ()
    }
  }
}

///|
/// Run every startup hook, this app's mounts included, stopping at the first that
/// raises and returning its message.
async fn App::start(self : App, state : Map[String, Json]) -> String? {
  for hook in self.startup_hooks {
    hook(state) catch {
      err => return Some(err.to_string())
    }
  }
  for mount in self.mounts {
    if mount.1 is Sub(sub) && sub.start(state) is Some(why) {
      return Some(why)
    }
  }
  None
}

///|
/// Run every shutdown hook in reverse registration order, mounts first since they
/// were started last. Every hook runs even if an earlier one raised; the first
/// failure is what comes back.
async fn App::stop(self : App, state : Map[String, Json]) -> String? {
  let mut failure = None
  for i = self.mounts.length() - 1; i >= 0; i = i - 1 {
    if self.mounts[i].1 is Sub(sub) &&
      sub.stop(state) is Some(why) &&
      failure is None {
      failure = Some(why)
    }
  }
  for i = self.shutdown_hooks.length() - 1; i >= 0; i = i - 1 {
    self.shutdown_hooks[i](state) catch {
      err => if failure is None { failure = Some(err.to_string()) }
    }
  }
  failure
}