///|
/// Raised when an application reports `lifespan.startup.failed` or
/// `lifespan.shutdown.failed`, carrying the failure message the app supplied.
pub suberror LifespanError {
  LifespanError(String)
}

///|
/// Drives an ASGI application's lifespan protocol (← uvicorn `LifespanOn`).
///
/// A single long-lived invocation of the app runs under a `Lifespan` scope. The
/// server and that invocation exchange messages through two async queues: the
/// server pushes `lifespan.startup` / `lifespan.shutdown` onto `inbox` (which
/// backs the app's `Receive`), and the app pushes its `*.complete` / `*.failed`
/// replies onto `outbox` (which the app's `Send` writes to). `state` seeds the
/// lifespan scope and is shared into request scopes by the caller.
pub struct Lifespan {
  app : @moonasgi.AsgiApp
  inbox : @aqueue.Queue[@moonasgi.Event]
  outbox : @aqueue.Queue[@moonasgi.Event]
  state : Map[String, Json]
}

///|
/// Create a lifespan driver for `app`, with empty unbounded message queues and
/// empty lifespan state.
pub fn Lifespan::new(app : @moonasgi.AsgiApp) -> Lifespan {
  {
    app,
    inbox: @aqueue.Queue::Queue(kind=Unbounded),
    outbox: @aqueue.Queue::Queue(kind=Unbounded),
    state: Map([]),
  }
}

///|
/// Spawn the application under a `Lifespan` scope as a task in `g`, returning its
/// handle. The task blocks on `receive()` until `startup()` / `shutdown()` push
/// signals; a lifespan-aware app therefore stays parked for the server's whole
/// life, while an app that ignores the lifespan scope simply returns at once
/// (detected via the returned task in `startup` / `shutdown`).
pub fn Lifespan::spawn(
  self : Lifespan,
  g : @async.TaskGroup[Unit],
) -> @async.Task[Unit] {
  let receive : @moonasgi.Receive = () => self.inbox.get()
  let send : @moonasgi.Send = event => {
    match event {
      LifespanStartupComplete
      | LifespanStartupFailed(_)
      | LifespanShutdownComplete
      | LifespanShutdownFailed(_) => self.outbox.put(event)
      _ => ()
    }
  }
  let scope = @moonasgi.Scope::Lifespan({
    asgi: @moonasgi.AsgiVersion::lifespan(),
    state: self.state,
  })
  g.spawn(() => (self.app)(scope, receive, send))
}

///|
/// Run ASGI lifespan **startup**: push `lifespan.startup` and await the app's
/// reply. Returns once the app sends `lifespan.startup.complete`, or once the
/// app returns without lifespan support (its `task` finishing wins the race, and
/// startup is treated as a no-op, matching uvicorn's `lifespan="auto"`). Raises
/// `LifespanError` if the app reports `lifespan.startup.failed`.
pub async fn Lifespan::startup(
  self : Lifespan,
  task : @async.Task[Unit],
) -> Unit {
  self.inbox.put(LifespanStartup)
  let ev : @moonasgi.Event? = @async.any([
    () => Some(self.outbox.get()),
    () => {
      task.wait()
      None
    },
  ])
  match ev {
    Some(LifespanStartupFailed(message~)) => raise LifespanError(message)
    _ => ()
  }
}

///|
/// Run ASGI lifespan **shutdown**: push `lifespan.shutdown` and await the app's
/// reply, or return immediately if the app invocation has already finished.
/// Raises `LifespanError` if the app reports `lifespan.shutdown.failed`.
pub async fn Lifespan::shutdown(
  self : Lifespan,
  task : @async.Task[Unit],
) -> Unit {
  self.inbox.put(LifespanShutdown)
  let ev : @moonasgi.Event? = @async.any([
    () => Some(self.outbox.get()),
    () => {
      task.wait()
      None
    },
  ])
  match ev {
    Some(LifespanShutdownFailed(message~)) => raise LifespanError(message)
    _ => ()
  }
}