///|
/// The exact bytes received from an HTTP host. Never parse or reserialize
/// `body` before verifying the Discord signature.
pub(all) struct InteractionHttpRequest {
  http_method : String
  signature : String?
  timestamp : String?
  body : Bytes
}

///|
/// A callback body. Multipart chunks retain file bytes without a second copy.
pub(all) enum InteractionHttpBody {
  Empty
  Bytes(Bytes)
  Chunks(Array[@dhttp.MultipartChunk])
}

///|
/// A host-independent HTTP response for a Discord interaction.
pub(all) struct InteractionHttpResponse {
  status : Int
  content_type : String?
  body : InteractionHttpBody
}

///|
fn interaction_text_response(
  status : Int,
  body : String,
) -> InteractionHttpResponse {
  {
    status,
    content_type: Some("text/plain; charset=utf-8"),
    body: Bytes(@utf8.encode(body)),
  }
}

///|
/// Verify the raw HTTP body, decode one interaction, and dispatch it.
///
/// The caller owns the task group supplied to `App::serve`. Keep that group
/// alive after this method returns so deferred handlers can finish.
pub async fn InteractionEndpoint::handle_signed_http(
  self : InteractionEndpoint,
  request : InteractionHttpRequest,
  verifier : @verify.InteractionVerifier,
  deadline_ms? : Int = 2500,
) -> InteractionHttpResponse {
  if request.http_method != "POST" {
    return interaction_text_response(405, "method not allowed")
  }
  guard request.signature is Some(signature) &&
    request.timestamp is Some(timestamp) else {
    return interaction_text_response(401, "missing signature")
  }
  if !verifier.verify(signature~, timestamp~, body=request.body) {
    return interaction_text_response(401, "invalid signature")
  }
  let body = @utf8.decode(request.body) catch {
    _ => return interaction_text_response(400, "invalid UTF-8 body")
  }
  let interaction : @model.Interaction? = Some(
    @json.from_json(@json.parse(body)),
  ) catch {
    _ => None
  }
  guard interaction is Some(interaction) else {
    return interaction_text_response(400, "invalid interaction JSON")
  }
  match self.handle_interaction(interaction, deadline_ms~) {
    Reply(response~, files=None) =>
      {
        status: 200,
        content_type: Some("application/json"),
        body: Bytes(@utf8.encode(response.to_json().stringify())),
      }
    Reply(response~, files=Some(files)) => {
      let (content_type, chunks) = @dhttp.encode_multipart_body(
        response.to_json().stringify(),
        files,
      )
      { status: 200, content_type: Some(content_type), body: Chunks(chunks), }
    }
    NoRoute => interaction_text_response(404, "not found")
    NoResponse => { status: 202, content_type: None, body: Empty, }
    TimedOut => interaction_text_response(504, "initial response timed out")
  }
}