///|
/// An error raised when an interaction callback cannot be accepted.
pub(all) suberror ResponseGateError {
  AlreadyResponded
  Expired
  InvalidCallback(
    kind~ : @model.InteractionType,
    typ~ : @model.InteractionResponseType
  )
} derive(Debug)

///|
/// What has happened to an interaction's single initial callback.
pub(all) enum ResponseState {
  Pending
  Sent(@model.InteractionResponseType)
  /// The sink raised; Discord may or may not have accepted the callback.
  Unconfirmed(@model.InteractionResponseType)
  /// The executor gave up the callback window.
  Expired
} derive(Debug, Eq)

///|
/// A single-use interaction callback gate.
pub struct ResponseGate {
  priv kind : @model.InteractionType
  priv mut state : ResponseState
  priv sink : async (@model.InteractionResponse, Array[@dhttp.FileUpload]?) -> Unit
  priv rejected : (Error) -> Bool
}

///|
/// A callback plus any multipart files captured by an in-memory gate.
pub(all) struct CapturedResponse {
  response : @model.InteractionResponse
  files : Array[@dhttp.FileUpload]?
}

///|
/// The receiving side of a capture gate.
pub struct ResponseCapture {
  priv queue : @aqueue.Queue[CapturedResponse]
}

///|
/// Construct a gate around an injected callback sink.
/// `rejected` identifies definitive refusals that leave the callback unused.
/// Cancellation always leaves delivery unconfirmed, regardless of the classifier.
pub fn ResponseGate::ResponseGate(
  kind : @model.InteractionType,
  sink : async (@model.InteractionResponse, Array[@dhttp.FileUpload]?) -> Unit,
  rejected? : (Error) -> Bool = _ => false,
) -> ResponseGate {
  { kind, state: Pending, sink, rejected, }
}

///|
/// Construct a gate that delivers its callback through Discord's REST API.
pub fn ResponseGate::rest(
  client : @dhttp.Client,
  interaction : @model.Interaction,
) -> ResponseGate {
  ResponseGate(
    interaction.typ,
    (response, files) => {
      client.create_interaction_response(
        interaction.id,
        interaction.token,
        response,
        files?,
      )
    },
    rejected=error => {
      match error {
        @dhttp.DiscordHttpError::Validation(..) => true
        @dhttp.DiscordHttpError::Api(status~, error~) =>
          status >= 400 && status < 500 && status != 429 && error.code != 40060
        _ => false
      }
    },
  )
}

///|
/// Construct an in-memory gate and a receiver for the first callback value.
pub fn ResponseGate::capture(
  interaction : @model.Interaction,
) -> (ResponseGate, ResponseCapture) {
  let queue : @aqueue.Queue[CapturedResponse] = Queue(kind=Blocking(1))
  let capture = ResponseCapture::{ queue, }
  let gate = ResponseGate(interaction.typ, (response, files) => {
    queue.put({ response, files, })
  })
  (gate, capture)
}

///|
fn callback_allowed(
  kind : @model.InteractionType,
  typ : @model.InteractionResponseType,
) -> Bool {
  match kind {
    Ping => typ == Pong
    ApplicationCommand =>
      typ == ChannelMessageWithSource ||
      typ == DeferredChannelMessageWithSource ||
      typ == Modal ||
      typ == LaunchActivity
    MessageComponent =>
      typ == ChannelMessageWithSource ||
      typ == DeferredChannelMessageWithSource ||
      typ == DeferredUpdateMessage ||
      typ == UpdateMessage ||
      typ == Modal ||
      typ == LaunchActivity
    ApplicationCommandAutocomplete => typ == Autocomplete
    ModalSubmit =>
      typ == ChannelMessageWithSource ||
      typ == DeferredChannelMessageWithSource ||
      typ == DeferredUpdateMessage ||
      typ == UpdateMessage
    Unknown(_) => true
  }
}

///|
/// Validate and deliver the one allowed initial interaction callback.
pub async fn ResponseGate::send(
  self : ResponseGate,
  response : @model.InteractionResponse,
  files? : Array[@dhttp.FileUpload],
) -> Unit {
  match self.state {
    Pending => ()
    Expired => raise ResponseGateError::Expired
    Sent(_) | Unconfirmed(_) => raise ResponseGateError::AlreadyResponded
  }
  if !callback_allowed(self.kind, response.typ) {
    raise ResponseGateError::InvalidCallback(kind=self.kind, typ=response.typ)
  }
  self.state = Sent(response.typ)
  errdefer (if self.state is Sent(_) { self.state = Unconfirmed(response.typ) })
  (self.sink)(response, files) catch {
    error if !@async.is_being_cancelled() && (self.rejected)(error) => {
      self.state = Pending
      raise error
    }
    error => raise error
  }
}

///|
/// The current state of this interaction's initial callback.
pub fn ResponseGate::state(self : ResponseGate) -> ResponseState {
  self.state
}

///|
/// Close an unused callback window. Other states remain unchanged.
///
/// ```mbt check
/// test "expire an unused callback window" {
///   let gate = @framework.ResponseGate(ApplicationCommand, (_, _) => ())
///   assert_eq(gate.state(), Pending)
///   gate.expire()
///   assert_eq(gate.state(), Expired)
///   assert_true(gate.responded())
/// }
/// ```
pub fn ResponseGate::expire(self : ResponseGate) -> Unit {
  if self.state == Pending {
    self.state = Expired
  }
}

///|
/// Whether the initial callback is no longer pending, including expiry.
pub fn ResponseGate::responded(self : ResponseGate) -> Bool {
  self.state != Pending
}

///|
/// Wait for and remove the captured callback.
pub async fn ResponseCapture::get(self : ResponseCapture) -> CapturedResponse {
  self.queue.get()
}

///|
/// Remove the captured callback without waiting, if one is available.
pub fn ResponseCapture::try_get(
  self : ResponseCapture,
) -> CapturedResponse? raise {
  self.queue.try_get()
}