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

///|
priv enum EndpointResponse {
  TextResponse(Int, String, String, String?)
  MultipartResponse(String, Array[@dhttp.MultipartChunk])
}

///|
fn text_response(
  status : Int,
  reason : String,
  body : String,
  content_type : String?,
) -> EndpointResponse {
  TextResponse(status, reason, body, content_type)
}

///|
fn multipart_response(
  response : @model.InteractionResponse,
  files : Array[@dhttp.FileUpload],
) -> EndpointResponse raise {
  let (content_type, chunks) = @dhttp.encode_multipart_body(
    response.to_json().stringify(),
    files,
  )
  MultipartResponse(content_type, chunks)
}

///|
async fn route_request(
  request : @ahttp.Request,
  body_reader : &@io.Reader,
  conn : @ahttp.ServerConnection,
  endpoint : @app.InteractionEndpoint,
  verifier : @verify.InteractionVerifier,
  path : String,
  deadline_ms : Int,
) -> EndpointResponse {
  if request.meth != Post {
    conn.skip_request_body()
    return text_response(
      405,
      "Method Not Allowed",
      "method not allowed",
      Some("text/plain; charset=utf-8"),
    )
  }
  if request.path != path {
    conn.skip_request_body()
    return text_response(
      404,
      "Not Found",
      "not found",
      Some("text/plain; charset=utf-8"),
    )
  }
  guard request.headers.get("x-signature-ed25519") is Some(signature) &&
    request.headers.get("x-signature-timestamp") is Some(timestamp) else {
    conn.skip_request_body()
    return text_response(
      401,
      "Unauthorized",
      "missing signature",
      Some("text/plain; charset=utf-8"),
    )
  }
  let body_bytes = body_reader.read_all().binary()
  if !verifier.verify(signature~, timestamp~, body=body_bytes) {
    return text_response(
      401,
      "Unauthorized",
      "invalid signature",
      Some("text/plain; charset=utf-8"),
    )
  }
  let body = @utf8.decode(body_bytes) catch {
    _ =>
      return text_response(
        400,
        "Bad Request",
        "invalid UTF-8 body",
        Some("text/plain; charset=utf-8"),
      )
  }
  let interaction : @model.Interaction? = Some(
    @json.from_json(@json.parse(body)),
  ) catch {
    _ => None
  }
  guard interaction is Some(interaction) else {
    return text_response(
      400,
      "Bad Request",
      "invalid interaction JSON",
      Some("text/plain; charset=utf-8"),
    )
  }
  match endpoint.handle_interaction(interaction, deadline_ms~) {
    Reply(response~, files=None) =>
      text_response(
        200,
        "OK",
        response.to_json().stringify(),
        Some("application/json"),
      )
    Reply(response~, files=Some(files)) => multipart_response(response, files)
    NoRoute =>
      text_response(
        404,
        "Not Found",
        "not found",
        Some("text/plain; charset=utf-8"),
      )
    NoResponse | TimedOut => text_response(202, "Accepted", "", None)
  }
}

///|
async fn send_response(
  conn : @ahttp.ServerConnection,
  response : EndpointResponse,
) -> Unit {
  match response {
    TextResponse(status, reason, body, content_type) => {
      let headers : @ahttp.Headers = Map([])
      if content_type is Some(value) {
        headers["Content-Type"] = value
      }
      conn.send_response(status, reason, extra_headers=headers)
      if !body.is_empty() {
        conn.write_string(body)
      }
      conn.end_response()
    }
    MultipartResponse(content_type, chunks) => {
      conn.send_response(200, "OK", extra_headers={
        "Content-Type": content_type,
      })
      for chunk in chunks {
        match chunk {
          Text(text) => conn.write_string(text)
          Blob(bytes) => @io.Writer::write(conn, bytes)
        }
      }
      conn.end_response()
    }
  }
}

///|
async fn handle_request(
  request : @ahttp.Request,
  body_reader : &@io.Reader,
  conn : @ahttp.ServerConnection,
  endpoint : @app.InteractionEndpoint,
  verifier : @verify.InteractionVerifier,
  path : String,
  deadline_ms : Int,
  warn : (String) -> Unit,
) -> Unit {
  let response = try
    route_request(
      request, body_reader, conn, endpoint, verifier, path, deadline_ms,
    )
  catch {
    error if @async.is_being_cancelled() || @async.is_cancellation_error(error) =>
      raise error
    error => {
      warn("interaction HTTP request failed: \{Repr(error)}")
      text_response(
        500,
        "Internal Server Error",
        "internal server error",
        Some("text/plain; charset=utf-8"),
      )
    }
  } noraise {
    response => response
  }
  send_response(conn, response) catch {
    error if @async.is_being_cancelled() || @async.is_cancellation_error(error) =>
      raise error
    error => {
      warn("interaction HTTP response failed: \{Repr(error)}")
      conn.close()
    }
  }
}

///|
/// A native HTTP server dispatching Discord interactions through an `App`.
pub struct InteractionsServer {
  priv address : String
  priv runner : Ref[@async.TaskGroup[Unit]?]
}

///|
/// 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 fn InteractionsServer::close(self : InteractionsServer) -> Unit {
  if self.runner.val is Some(inner) {
    inner.return_immediately(()) catch {
      _ => ()
    }
  }
}

///|
/// Start a native Discord HTTP-interactions server in `group`.
///
/// Use port zero in `addr` to let the OS choose a free port, then read the
/// resolved address from `InteractionsServer::addr`.
pub async fn[X] serve_interactions(
  group : @async.TaskGroup[X],
  app : @app.App,
  addr~ : String,
  public_key~ : String,
  client? : @dhttp.Client,
  token? : String,
  application_id? : @model.ApplicationId,
  sync? : Bool = false,
  path? : String = "/",
  deadline_ms? : Int = 2500,
) -> InteractionsServer {
  let verifier = @verify.InteractionVerifier::new(public_key)
  let server = @ahttp.Server(@socket.Addr::parse(addr))
  let address = server.addr.to_string()
  let runner : Ref[@async.TaskGroup[Unit]?] = Ref(None)
  let startup : @aqueue.Queue[ServerStartup] = Queue(kind=Unbounded)
  let warn : (String) -> Unit = message => app.warn(message)
  group.spawn_bg(allow_failure=true, () => {
    let mut ready = false
    @async.with_task_group(inner => {
      runner.val = Some(inner)
      let endpoint = app.serve(inner, client?, token?, application_id?, sync~)
      inner.spawn_bg(() => {
        server.run_forever(allow_failure=true, (request, body_reader, conn) => {
          handle_request(
            request, body_reader, conn, endpoint, verifier, path, deadline_ms, warn,
          )
        })
      })
      startup.put(Ready)
      ready = true
    }) catch {
      error if @async.is_being_cancelled() ||
        @async.is_cancellation_error(error) => raise error
      error =>
        if ready {
          warn("interaction HTTP server stopped: \{Repr(error)}")
        } else {
          startup.put(Failed(error))
        }
    }
  })
  match startup.get() {
    Ready => { address, runner, }
    Failed(error) => raise error
  }
}