///|
/// 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 are written in order and retain file
/// bytes without a second copy.
pub(all) enum InteractionHttpBody {
  Empty
  Bytes(Bytes)
  Chunks(Array[Bytes])
}

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

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

///|
/// Verify the raw HTTP body and decode one interaction, or the error response
/// that refuses it.
fn decode_signed_request(
  request : InteractionHttpRequest,
  verifier : @verify.InteractionVerifier,
) -> Result[@model.Interaction, InteractionHttpResponse] {
  if request.http_method != "POST" {
    return Err(interaction_text_response(405, "method not allowed"))
  }
  guard request.signature is Some(signature) &&
    request.timestamp is Some(timestamp) else {
    return Err(interaction_text_response(401, "missing signature"))
  }
  if !verifier.verify(signature~, timestamp~, body=request.body) {
    return Err(interaction_text_response(401, "invalid signature"))
  }
  let body = @utf8.decode(request.body) catch {
    _ => return Err(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 Err(interaction_text_response(400, "invalid interaction JSON"))
  }
  Ok(interaction)
}

///|
/// The HTTP response that carries a dispatch outcome.
fn outcome_http_response(
  outcome : InteractionOutcome,
) -> InteractionHttpResponse raise @dhttp.DiscordHttpError {
  match outcome {
    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")
  }
}

///|
/// 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~ : Int32 = 2500,
) -> InteractionHttpResponse {
  match decode_signed_request(request, verifier) {
    Ok(interaction) =>
      outcome_http_response(self.handle_interaction(interaction, deadline_ms~))
    Err(refusal) => refusal
  }
}