// The middlewares that have no configuration of their own: recovery, CORS,
// request ids and access logging.
//
// A middleware here is an ASGI application transformer. Layers that already
// exist elsewhere in the family arrive as moonasgi's `mid.Layer` and are adapted
// by `layer` rather than reimplemented: one concept, one home.

///|
/// Adapt a moonasgi `mid.Layer` into a moonzero `Middleware`. The two shapes are
/// the same ASGI transformer, so this is the whole bridge — a layer written for
/// moonasgi (`cors`, `gzip`, `body_limit`, `server_errors`, `trusted_hosts`,
/// `sessions`, or a user's own) drops into a moonzero chain unchanged.
pub fn layer(l : &@mid.Layer) -> Middleware {
  inner => l.wrap(inner)
}

///|
/// The event stream recovery emits in place of a failed request: a `500` start
/// followed by a short plain-text body.
fn internal_error_events() -> Array[@moonasgi.Event] {
  [
    @moonasgi.Event::HttpResponseStart(
      status=500,
      headers=[("content-type", "text/plain; charset=utf-8")],
      trailers=false,
    ),
    @moonasgi.Event::HttpResponseBody(
      body=b"Internal Server Error",
      more_body=false,
    ),
  ]
}

///|
/// Recovery middleware: run the wrapped application inside a `try`, and if it
/// raises before any part of the response has gone out, emit a `500 Internal
/// Server Error` instead of letting the failure escape to the server.
///
/// Once the response has started, a `500` can no longer be written — the status
/// line is already on the wire — so the error is re-raised for the server to
/// close the connection on. Answering late would append a second status to a
/// response the client is already reading.
pub fn recovery(inner : @moonasgi.AsgiApp) -> @moonasgi.AsgiApp {
  (scope, receive, send) => {
    let started : Ref[Bool] = { val: false, }
    let observed : @moonasgi.Send = event => {
      match event {
        HttpResponseStart(_) => started.val = true
        _ => ()
      }
      send(event)
    }
    inner(scope, receive, observed) catch {
      error => {
        if started.val {
          raise error
        }
        for event in internal_error_events() {
          send(event)
        }
      }
    }
  }
}

///|
/// CORS configuration: which origins may call the service, and what a preflight
/// is told about the rest of the policy.
pub(all) struct CorsConf {
  /// Origins allowed to call. `["*"]` — the default — allows any, and an entry
  /// also matches its own subdomains, so `example.com` allows
  /// `api.example.com`.
  origins : Array[String]
  methods : String
  headers : String
  expose : String
  max_age : Int32
}

///|
/// The method set go-zero advertises.
pub let cors_methods : String = "GET, HEAD, POST, PATCH, PUT, DELETE"

///|
/// The request headers go-zero lets through a preflight.
pub let cors_headers : String = "Content-Type, Origin, X-CSRF-Token, Authorization, AccessToken, Token, Range"

///|
/// The response headers go-zero exposes to script.
pub let cors_expose : String = "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers"

///|
/// A permissive CORS config: any origin, go-zero's method and header sets, and a
/// one-day preflight cache.
pub fn CorsConf::new(
  origins~ : Array[String] = ["*"],
  methods~ : String = cors_methods,
  headers~ : String = cors_headers,
  expose~ : String = cors_expose,
  max_age~ : Int32 = 86400,
) -> CorsConf {
  { origins, methods, headers, expose, max_age, }
}

///|
/// Whether `origin` is allowed by `allows`: an exact match, a subdomain of an
/// allowed origin, or a wildcard. Matching is case-insensitive on both sides, as
/// the origin comparison in the Fetch standard is.
pub fn cors_allows(allows : Array[String], origin : String) -> Bool {
  let origin = origin.to_lower()
  for allow in allows {
    if allow == "*" {
      return true
    }
    let allow = allow.to_lower()
    if origin == allow || origin.has_suffix("." + allow) {
      return true
    }
  }
  false
}

///|
/// The headers a CORS response carries for `origin`: the echoed origin (or `*`),
/// the method/header sets, the exposed headers and the preflight cache age.
/// Credentials are advertised only for an echoed origin, because `*` and
/// credentials are mutually exclusive in the Fetch standard.
fn cors_response_headers(
  conf : CorsConf,
  origin : String?,
) -> Array[(String, String)] {
  // A wildcard policy answers "*" literally: echoing the request's origin would
  // then also permit credentials, and `*` with credentials is forbidden.
  let wildcard = conf.origins.length() == 0 || conf.origins.contains("*")
  let allow_origin = if wildcard {
    "*"
  } else {
    match origin {
      Some(origin) =>
        if cors_allows(conf.origins, origin) {
          origin
        } else {
          ""
        }
      None => ""
    }
  }
  if allow_origin == "" {
    return []
  }
  let out : Array[(String, String)] = [
    ("access-control-allow-origin", allow_origin),
    ("access-control-allow-methods", conf.methods),
    ("access-control-allow-headers", conf.headers),
    ("access-control-expose-headers", conf.expose),
    ("access-control-max-age", conf.max_age.to_string()),
  ]
  if allow_origin != "*" {
    out.push(("access-control-allow-credentials", "true"))
  }
  out
}

///|
/// The `Vary` values a CORS response must carry so a shared cache does not hand
/// one origin's answer to another.
fn cors_vary(preflight : Bool) -> Array[(String, String)] {
  let value = if preflight {
    "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
  } else {
    "Origin"
  }
  [("vary", value)]
}

///|
/// CORS middleware: answer a preflight `OPTIONS` with `204 No Content` and the
/// policy headers, and stamp the same headers onto every other response.
///
/// A request from an origin the policy does not allow is served without the
/// `Access-Control-*` headers rather than refused — the browser is the one that
/// enforces the policy, and a server-side refusal would break non-browser
/// clients that never asked for CORS at all.
pub fn cors(conf : CorsConf) -> Middleware {
  inner => {
    (scope, receive, send) => {
      match scope {
        Http(hs) => {
          let origin = scope_header(scope, "origin")
          let preflight = hs.http_method == "OPTIONS"
          let policy = [
            ..cors_vary(preflight),
            ..cors_response_headers(conf, origin),
          ]
          if preflight {
            send(
              @moonasgi.Event::HttpResponseStart(
                status=204,
                headers=policy,
                trailers=false,
              ),
            )
            send(@moonasgi.Event::HttpResponseBody(body=b"", more_body=false))
          } else {
            let wrapped : @moonasgi.Send = event => {
              match event {
                HttpResponseStart(status~, headers=own, trailers~) =>
                  send(
                    @moonasgi.Event::HttpResponseStart(
                      status~,
                      headers=[..policy, ..own],
                      trailers~,
                    ),
                  )
                other => send(other)
              }
            }
            inner(scope, receive, wrapped)
          }
        }
        _ => inner(scope, receive, send)
      }
    }
  }
}

///|
/// Read a header from an HTTP scope (lowercased-name convention); `None` for
/// non-HTTP scopes or a missing header.
pub fn scope_header(scope : @moonasgi.Scope, name : String) -> String? {
  match scope {
    Http(hs) => {
      for pair in hs.headers {
        if pair.0 == name {
          return Some(pair.1)
        }
      }
      None
    }
    _ => None
  }
}

///|
/// Resolve the id for a request: reuse an inbound one verbatim, otherwise mint the
/// next monotonic `req-N` from the shared counter.
fn resolve_request_id(inbound : String?, counter : Ref[Int32]) -> String {
  match inbound {
    Some(existing) => existing
    None => {
      counter.val = counter.val + 1
      "req-" + counter.val.to_string()
    }
  }
}

///|
/// Request-ID middleware: reuse an inbound `x-request-id` if the client sent one,
/// otherwise mint a fresh monotonic id, and stamp it onto every response's
/// `HttpResponseStart`. The counter is captured once per assembly, so ids stay
/// unique across the requests this layer serves.
pub fn request_id(header~ : String = "x-request-id") -> Middleware {
  let counter : Ref[Int32] = { val: 0, }
  inner => {
    (scope, receive, send) => {
      let id = resolve_request_id(scope_header(scope, header), counter)
      let wrapped : @moonasgi.Send = event => {
        match event {
          HttpResponseStart(status~, headers~, trailers~) =>
            send(
              @moonasgi.Event::HttpResponseStart(
                status~,
                headers=[(header, id), ..headers],
                trailers~,
              ),
            )
          other => send(other)
        }
      }
      inner(scope, receive, wrapped)
    }
  }
}

///|
/// An AsgiApp transformer — one layer of the middleware onion.
pub type Middleware = (@moonasgi.AsgiApp) -> @moonasgi.AsgiApp

///|
/// A receive channel that hands the application `body` once and then reports the
/// connection closed — the shape an ASGI application sees for a body a layer has
/// already buffered.
pub fn buffered_body(body : Bytes) -> @moonasgi.Receive {
  let served : Ref[Bool] = { val: false, }
  () => {
    if served.val {
      @moonasgi.Event::HttpDisconnect
    } else {
      served.val = true
      @moonasgi.Event::HttpRequest(body~, more_body=false)
    }
  }
}

///|
/// Drain a receive channel into one body. Used by the layers that have to read
/// the whole payload before they can decide anything.
pub async fn read_body(receive : @moonasgi.Receive) -> Bytes {
  let body = Buffer()
  let mut more = true
  while more {
    match receive() {
      HttpRequest(body=chunk, more_body=next) => {
        body.write_bytes(chunk)
        more = next
      }
      _ => more = false
    }
  }
  body.to_bytes()
}

///|
/// A request-logging middleware: writes `METHOD path` through the process `logx`
/// at `info` for each HTTP request, then delegates to the wrapped application.
/// It logs on the way in and knows nothing of the response; `structured_logging`
/// is the layer that times the request and records its status.
pub fn logging(inner : @moonasgi.AsgiApp) -> @moonasgi.AsgiApp {
  (scope, receive, send) => {
    match scope {
      Http(hs) => @logx.logx.info(hs.http_method + " " + hs.path)
      _ => ()
    }
    inner(scope, receive, send)
  }
}