///|
priv enum ServerStartup {
  Ready(String)
  Failed(Error)
}

///|
/// Configuration errors reported by the bounded HTTP server.
pub using @h1server {type ServerConfigError}

///|
/// Listener configuration, bind and accept failures.
pub using @server {type ListenError}

///|
/// An HTTP server dispatching Discord interactions on native or moonrun Wasm.
pub struct InteractionsServer {
  priv address : String
  priv runner : Ref[@async.TaskGroup?]
}

///|
/// Return the actual listening address, including an OS-assigned port.
pub fn InteractionsServer::addr(self : InteractionsServer) -> String {
  self.address
}

///|
/// Stop accepting requests and cancel active connection handlers.
pub async fn InteractionsServer::close(
  self : InteractionsServer,
) -> Unit noraise {
  if self.runner.val is Some(inner) {
    inner.return_immediately()
  }
}

///|
/// Start a Discord HTTP-interactions server in `group` on native or moonrun Wasm.
///
/// Use port zero in `addr` to let the OS choose a free port, then read the
/// resolved address from `InteractionsServer::addr`.
///
/// A request body larger than `max_body_bytes` gets 413, and one not received
/// within `body_timeout_ms` gets 408; both close the connection. Discord's
/// interaction payloads are far below the 1 MiB default. A connection whose
/// next request head does not arrive within `header_timeout_ms`, idle
/// keep-alive time included, is closed (408 if a partial head arrived).
/// Request heads and trailers are each limited to 16 KiB. A declared body
/// over the limit is rejected before signature authentication.
/// At most `max_connections` clients
/// are served at once; more wait to be accepted. Non-positive limits raise
/// `ListenError::Config(ServerConfigError)` before the server listens.
pub async fn serve_interactions(
  group : @async.TaskGroup,
  app : @app.App,
  addr~ : String,
  public_key~ : String,
  access~ : @app.RestAccess,
  application_id? : @model.ApplicationId,
  path~ : String = "/",
  deadline_ms~ : Int32 = 2500,
  max_body_bytes~ : Int32 = 1048576,
  body_timeout_ms~ : Int32 = 5000,
  header_timeout_ms~ : Int32 = 5000,
  max_connections~ : Int32 = 256,
) -> InteractionsServer {
  guard max_body_bytes > 0 else {
    raise @server.Config(
      @h1server.ServerConfigError::InvalidMaxBodyBytes(value=max_body_bytes),
    )
  }
  guard body_timeout_ms > 0 else {
    raise @server.Config(
      @h1server.ServerConfigError::InvalidBodyTimeoutMs(value=body_timeout_ms),
    )
  }
  guard header_timeout_ms > 0 else {
    raise @server.Config(
      @h1server.ServerConfigError::InvalidHeaderTimeoutMs(
        value=header_timeout_ms,
      ),
    )
  }
  guard max_connections > 0 else {
    raise @server.Config(
      @h1server.ServerConfigError::InvalidMaxConnections(value=max_connections),
    )
  }
  let verifier = @verify.InteractionVerifier::new(public_key)
  let runner : Ref[@async.TaskGroup?] = Ref(None)
  let startup : @aqueue.Queue[ServerStartup] = Queue(kind=Unbounded)
  let warn : (@app.AppWarning) -> Unit = warning => app.warn(warning)
  group.spawn_bg(allow_failure=true, () => {
    let mut ready = false
    @async.with_task_group(inner => {
      runner.val = Some(inner)
      let endpoint = app.serve(inner, access~, application_id?)
      let handler = interaction_handler(
        endpoint,
        verifier,
        path~,
        deadline_ms~,
        max_body_bytes~,
        warn~,
      )
      @server.serve(
        handler,
        addr~,
        limits={
          ..@h1server.ServerLimits::default(),
          max_body_bytes,
          body_timeout_ms,
          header_timeout_ms,
          max_connections,
          drain_ms: 0,
          handler_timeout_ms: None,
        },
        on_listen=address => {
          ready = true
          ignore(startup.try_put(Ready(address)) catch { _ => false })
        },
        on_error=error => {
          warn(
            @app.Executor(
              source="http",
              message="interaction HTTP request failed: \{error.message()}",
            ),
          )
        },
      )
    }) catch {
      error if @async.is_being_cancelled() => raise error
      error =>
        if ready {
          warn(
            @app.Executor(
              source="http",
              message="interaction HTTP server stopped: \{Repr(error)}",
            ),
          )
        } else {
          startup.put(Failed(error))
        }
    }
  })
  match startup.get() {
    Ready(address) => { address, runner, }
    Failed(error) => raise error
  }
}