///|
/// An application's answer to a lifespan phase: `Complete` when startup or
/// shutdown succeeded, or `Failed` with a message the server logs and (for
/// startup) aborts the boot on. Mirrors the two replies ASGI allows to each
/// lifespan message — `lifespan.startup.complete` / `.failed` and
/// `lifespan.shutdown.complete` / `.failed`.
pub(all) enum LifespanReply {
  Complete
  Failed(message~ : String)
} derive(Eq)

///|
/// A synchronous lifespan application in the startup / shutdown shape, the
/// lifespan analog of the http `Handler` and the WebSocket `WebSocketHandler`.
/// `on_startup` runs once at boot: it seeds the scope's `state` in place (the map
/// the server then copies onto every request scope — a DB pool handle, a loaded
/// config) and returns `Complete` or `Failed`. `on_shutdown` runs once at
/// teardown to release those resources. Driven in-process by `run_lifespan`, so
/// boot/teardown logic is testable on every backend without an async runtime —
/// the faithful synchronous core the async server (`mooncat`) lifts onto the
/// lifespan protocol.
pub(all) struct LifespanHandler {
  on_startup : (LifespanScope) -> LifespanReply
  on_shutdown : (LifespanScope) -> LifespanReply
}

///|
/// Build a `LifespanHandler`. Both phases default to succeeding without doing
/// anything, so a caller overrides only the phase it needs — an app that just
/// wants a startup hook leaves `on_shutdown` alone.
pub fn LifespanHandler::new(
  on_startup? : (LifespanScope) -> LifespanReply = fn(_s) { Complete },
  on_shutdown? : (LifespanScope) -> LifespanReply = fn(_s) { Complete },
) -> LifespanHandler {
  { on_startup, on_shutdown }
}

///|
/// Lower one `LifespanReply` to the outbound `Event` a server sends for a phase:
/// `startup` picks the startup reply pair, `shutdown` the shutdown pair.
fn lifespan_reply_event(reply : LifespanReply, startup : Bool) -> Event {
  match (reply, startup) {
    (Complete, true) => LifespanStartupComplete
    (Complete, false) => LifespanShutdownComplete
    (Failed(message~), true) => LifespanStartupFailed(message~)
    (Failed(message~), false) => LifespanShutdownFailed(message~)
  }
}

///|
/// Drive a `LifespanHandler` over a materialised inbound event stream, folding it
/// into the outbound replies a server would send. `LifespanStartup` runs
/// `on_startup` and emits `LifespanStartupComplete` or `LifespanStartupFailed`; a
/// failed startup ends the run, since ASGI has the server abort the boot and
/// never send `lifespan.shutdown`. `LifespanShutdown` runs `on_shutdown` and
/// emits the matching shutdown reply. The handler mutates `scope.state` in place
/// during startup, exactly as an ASGI app populates `scope["state"]`. This is the
/// synchronous lifespan core mirroring `run_http` / `ws_run`; a non-lifespan
/// scope yields `[]`.
pub fn run_lifespan(
  handler : LifespanHandler,
  scope : Scope,
  inbound : Array[Event],
) -> Array[Event] {
  match scope {
    Lifespan(ls) => {
      let out : Array[Event] = []
      let mut startup_failed = false
      for ev in inbound {
        if startup_failed {
          break
        }
        match ev {
          LifespanStartup => {
            let reply = (handler.on_startup)(ls)
            out.push(lifespan_reply_event(reply, true))
            if reply is Failed(..) {
              startup_failed = true
            }
          }
          LifespanShutdown =>
            out.push(lifespan_reply_event((handler.on_shutdown)(ls), false))
          _ => ()
        }
      }
      out
    }
    _ => []
  }
}