use std::collections::HashMap;
use std::time::Instant;
use bingle_core::api::bingle_api::{BingleError, SendFailureKind};
use bingle_local::api::send_retry::{
RETRY_BACKOFF, SendFailure, classify_send_error, select_sendable_message,
};
use serde_json::{Value, json};
use crate::chat_state::ChatState;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SendTarget {
Handle(String),
Id(String),
}
impl SendTarget {
pub fn label(&self) -> &str {
match self {
SendTarget::Handle(h) => h,
SendTarget::Id(id) => id,
}
}
}
pub trait MessageSender {
fn send_text(&self, target: &SendTarget, message: &Value) -> Result<bool, BingleError>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SendOutcome {
Delivered,
Retrying(String),
Failed(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RetryOutcome {
pub timestamp: i64,
pub recipient: String,
pub outcome: SendOutcome,
}
fn failure_of(result: &Result<bool, BingleError>) -> SendFailure {
classify_send_error(result).unwrap_or_else(|| SendFailure {
kind: SendFailureKind::Unknown,
reason: "unknown send failure".to_string(),
})
}
fn classify_and_persist(
state: &mut ChatState,
timestamp: i64,
failure: SendFailure,
retries_enabled: bool,
) -> SendOutcome {
let transient = retries_enabled && failure.kind.is_retryable();
if transient {
let _ = state.mark_send_failed(timestamp, &failure.reason, Some(failure.kind), false);
SendOutcome::Retrying(failure.reason)
} else {
let _ = state.mark_send_failed(timestamp, &failure.reason, Some(failure.kind), true);
SendOutcome::Failed(failure.reason)
}
}
pub fn send_once(
sender: &dyn MessageSender,
state: &mut ChatState,
target: &SendTarget,
text: &str,
retries_enabled: bool,
) -> SendOutcome {
let ts = match state.queue_outbound(target.label(), text) {
Ok(ts) => ts,
Err(e) => return SendOutcome::Failed(format!("could not queue message: {e}")),
};
let result = sender.send_text(target, &json!({ "text": text }));
if matches!(result, Ok(true)) {
let _ = state.mark_delivered(ts);
return SendOutcome::Delivered;
}
classify_and_persist(state, ts, failure_of(&result), retries_enabled)
}
pub fn retry_pending(
sender: &dyn MessageSender,
state: &mut ChatState,
retry_after: &mut HashMap<i64, Instant>,
now: Instant,
) -> Option<RetryOutcome> {
let pending = state.pending_outbound().ok()?;
retry_after.retain(|ts, _| pending.iter().any(|m| m.timestamp == *ts));
let msg = select_sendable_message(pending, retry_after, now)?;
let recipient = msg.recipient_handles.first().cloned().unwrap_or_default();
let target = SendTarget::Handle(recipient.clone());
let result = sender.send_text(&target, &json!({ "text": msg.text }));
let outcome = if matches!(result, Ok(true)) {
let _ = state.mark_delivered(msg.timestamp);
retry_after.remove(&msg.timestamp);
SendOutcome::Delivered
} else {
let failure = failure_of(&result);
if failure.kind.is_retryable() {
let _ =
state.mark_send_failed(msg.timestamp, &failure.reason, Some(failure.kind), false);
retry_after.insert(msg.timestamp, now + RETRY_BACKOFF);
SendOutcome::Retrying(failure.reason)
} else {
let _ =
state.mark_send_failed(msg.timestamp, &failure.reason, Some(failure.kind), true);
retry_after.remove(&msg.timestamp);
SendOutcome::Failed(failure.reason)
}
};
Some(RetryOutcome {
timestamp: msg.timestamp,
recipient,
outcome,
})
}