use std::collections::HashMap;
use std::time::Instant;
use bingle_local::api::send_retry::{
RETRY_BACKOFF, is_transient_send_failure, pending_failure_reason, 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, String>;
}
#[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,
}
const SEND_RETURNED_FALSE: &str = "Send returned false";
fn classify_and_persist(
state: &mut ChatState,
timestamp: i64,
err: &str,
retries_enabled: bool,
) -> SendOutcome {
let transient = retries_enabled && is_transient_send_failure(err);
let reason = pending_failure_reason(err, transient);
if transient {
let _ = state.mark_send_failed(timestamp, &reason, false);
SendOutcome::Retrying(reason)
} else {
let _ = state.mark_send_failed(timestamp, &reason, true);
SendOutcome::Failed(reason)
}
}
fn failure_reason(result: Result<bool, String>) -> String {
match result {
Ok(false) => SEND_RETURNED_FALSE.to_string(),
Err(e) => e,
Ok(true) => unreachable!("delivered results are handled before this"),
}
}
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_reason(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 err = failure_reason(result);
let transient = is_transient_send_failure(&err);
let reason = pending_failure_reason(&err, transient);
if transient {
let _ = state.mark_send_failed(msg.timestamp, &reason, false);
retry_after.insert(msg.timestamp, now + RETRY_BACKOFF);
SendOutcome::Retrying(reason)
} else {
let _ = state.mark_send_failed(msg.timestamp, &reason, true);
retry_after.remove(&msg.timestamp);
SendOutcome::Failed(reason)
}
};
Some(RetryOutcome {
timestamp: msg.timestamp,
recipient,
outcome,
})
}