use crate::components::common::{MessageActivityMsg, Msg, PopupActivityMsg};
use crate::error::AppError;
use quetty_server::bulk_operations::BulkOperationResult;
use std::sync::mpsc::Sender;
#[derive(Debug, Clone)]
pub struct BulkOperationContext {
pub operation_type: BulkOperationType,
pub successful_count: usize,
pub failed_count: usize,
pub total_count: usize,
pub message_ids: Vec<String>,
pub should_remove_from_state: bool,
pub reload_threshold: usize,
pub current_message_count: usize,
pub selected_from_current_page: usize,
}
#[derive(Debug, Clone)]
pub enum BulkOperationType {
Delete,
Send {
from_queue_display: String,
to_queue_display: String,
should_delete: bool,
},
}
#[derive(Debug, Clone)]
pub enum ReloadStrategy {
ForceReload { reason: String },
LocalRemoval,
CompletionOnly,
}
pub struct BulkOperationPostProcessor;
impl BulkOperationPostProcessor {
pub fn determine_reload_strategy(context: &BulkOperationContext) -> ReloadStrategy {
let large_operation = context.successful_count >= context.reload_threshold;
match &context.operation_type {
BulkOperationType::Delete => {
let all_current_deleted =
context.selected_from_current_page >= context.current_message_count;
if all_current_deleted && large_operation {
let reason = format!(
"Complete current page deletion in large operation ({} messages) - ensuring UI consistency",
context.successful_count
);
ReloadStrategy::ForceReload { reason }
} else if context.successful_count > 0 {
ReloadStrategy::LocalRemoval
} else {
ReloadStrategy::CompletionOnly
}
}
BulkOperationType::Send { should_delete, .. } => {
if *should_delete && context.successful_count > 0 {
let queue_would_be_empty =
context.successful_count >= context.current_message_count;
if queue_would_be_empty {
let reason = format!(
"Queue emptied by send-with-delete operation ({} messages) - reloading fresh messages",
context.successful_count
);
ReloadStrategy::ForceReload { reason }
} else {
ReloadStrategy::LocalRemoval
}
} else {
ReloadStrategy::CompletionOnly
}
}
}
}
pub fn handle_completion(
context: &BulkOperationContext,
tx_to_main: &Sender<Msg>,
error_reporter: &crate::error::ErrorReporter,
) -> Result<(), AppError> {
let strategy = Self::determine_reload_strategy(context);
log::info!(
"Processing bulk operation completion: type={:?}, strategy={:?}",
context.operation_type,
strategy
);
match strategy {
ReloadStrategy::ForceReload { reason } => {
log::info!("Forcing message reload: {reason}");
if let Err(e) = tx_to_main.send(Msg::MessageActivity(
MessageActivityMsg::ForceReloadMessages,
)) {
error_reporter.report_send_error("force reload message", &e);
return Err(AppError::Component(e.to_string()));
}
if let Err(e) = tx_to_main.send(Msg::MessageActivity(
MessageActivityMsg::RefreshQueueStatistics,
)) {
error_reporter.report_send_error("refresh queue statistics", &e);
}
Self::send_completion_message(context, tx_to_main, error_reporter)?;
}
ReloadStrategy::LocalRemoval => {
if context.should_remove_from_state && !context.message_ids.is_empty() {
log::info!(
"Smart local removal: removing {} messages from state while preserving others",
context.message_ids.len()
);
if let Err(e) = tx_to_main.send(Msg::MessageActivity(
MessageActivityMsg::BulkRemoveMessagesFromState(
context.message_ids.clone(),
),
)) {
error_reporter.report_send_error("remove messages from state", &e);
return Err(AppError::Component(e.to_string()));
}
}
log::info!("Refreshing queue statistics after smart local removal");
if let Err(e) = tx_to_main.send(Msg::MessageActivity(
MessageActivityMsg::RefreshQueueStatistics,
)) {
error_reporter.report_send_error("refresh queue statistics", &e);
}
Self::send_completion_message(context, tx_to_main, error_reporter)?;
}
ReloadStrategy::CompletionOnly => {
if let Err(e) = tx_to_main.send(Msg::MessageActivity(
MessageActivityMsg::RefreshQueueStatistics,
)) {
error_reporter.report_send_error("refresh queue statistics", &e);
}
Self::send_completion_message(context, tx_to_main, error_reporter)?;
}
}
if matches!(context.operation_type, BulkOperationType::Delete)
|| matches!(
context.operation_type,
BulkOperationType::Send {
should_delete: true,
..
}
)
{
if let Err(e) =
tx_to_main.send(Msg::MessageActivity(MessageActivityMsg::ClearAllSelections))
{
error_reporter.report_send_error("clear selections", &e);
}
}
Ok(())
}
pub fn format_bulk_operation_result_message(
operation: &str,
queue_name: &str,
successful_count: usize,
failed_count: usize,
not_found_count: usize,
total_count: usize,
is_delete: bool,
) -> String {
if successful_count == 0 {
if failed_count > 0 {
format!(
"❌ Bulk {operation} failed: No messages were processed from {queue_name}\n\n\
📊 Results:\n\
• ❌ Failed: {failed_count} messages\n\
• ⚠️ Not found: {not_found_count} messages\n\
• 📦 Total requested: {total_count}\n\n\
💡 Messages may have been already processed, moved, or deleted by another process."
)
} else {
let unavailable_hint = if is_delete {
format!(
"💡 The {not_found_count} messages you selected were not available for deletion.\n\
This typically happens when:\n\
• Messages were processed by another consumer\n\
• Messages were moved or deleted by another process\n\
• Selected messages are only visible in preview but not available for consumption\n\n\
🔄 Try refreshing the queue to see the current available messages."
)
} else {
format!(
"💡 The {not_found_count} messages you selected were not available for moving.\n\
This typically happens when:\n\
• Messages were processed by another consumer\n\
• Messages were moved or deleted by another process\n\
• Selected messages are only visible in preview but not available for consumption\n\n\
🔄 Try refreshing the queue to see the current available messages."
)
};
format!(
"⚠️ No messages were processed from {queue_name}
📊 Results:
• ⚠️ Not found: {not_found_count} messages
• 📦 Total requested: {total_count}
{unavailable_hint}"
)
}
} else if failed_count > 0 || not_found_count > 0 {
format!(
"⚠️ Bulk {operation} operation completed with mixed results
{queue_name}
📊 Results:
• ✅ Successfully processed: {successful_count} messages
• ❌ Failed: {failed_count} messages
• ⚠️ Not found: {not_found_count} messages
• 📦 Total requested: {total_count}
💡 Some messages may have been processed by another process during the operation."
)
} else {
let operation_word = if is_delete { "move" } else { "copy" };
let past_tense = if is_delete { "moved" } else { "copied" };
let queue_wording = if queue_name.contains('→') {
queue_name.replace('→', "to")
} else {
queue_name.to_string()
};
format!(
"✅ Bulk {op} operation completed successfully!\n\n{count} message{plural} processed from {queue_wording}\n\nAll messages {past_tense} successfully",
op = operation_word,
count = successful_count,
plural = if successful_count == 1 { "" } else { "s" },
queue_wording = queue_wording,
past_tense = past_tense,
)
}
}
fn send_completion_message(
context: &BulkOperationContext,
tx_to_main: &Sender<Msg>,
error_reporter: &crate::error::ErrorReporter,
) -> Result<(), AppError> {
match &context.operation_type {
BulkOperationType::Delete => {
if let Err(e) = tx_to_main.send(Msg::MessageActivity(
MessageActivityMsg::BulkDeleteCompleted {
successful_count: context.successful_count,
failed_count: context.failed_count,
total_count: context.total_count,
},
)) {
error_reporter.report_send_error("bulk delete completion message", &e);
return Err(AppError::Component(e.to_string()));
}
}
BulkOperationType::Send {
from_queue_display,
to_queue_display,
should_delete,
} => {
let not_found_count = context
.total_count
.saturating_sub(context.successful_count + context.failed_count);
let queue_name_combined = format!("{from_queue_display} → {to_queue_display}");
let operation = if *should_delete { "move" } else { "copy" };
let is_delete = *should_delete;
let message = Self::format_bulk_operation_result_message(
operation,
&queue_name_combined,
context.successful_count,
context.failed_count,
not_found_count,
context.total_count,
is_delete,
);
if let Err(e) =
tx_to_main.send(Msg::PopupActivity(PopupActivityMsg::ShowSuccess(message)))
{
error_reporter.report_send_error("success popup message", &e);
return Err(AppError::Component(e.to_string()));
}
}
}
Ok(())
}
pub fn create_delete_context(
result: &BulkOperationResult,
message_ids: Vec<String>,
reload_threshold: usize,
current_message_count: usize,
selected_from_current_page: usize,
) -> BulkOperationContext {
BulkOperationContext {
operation_type: BulkOperationType::Delete,
successful_count: result.successful,
failed_count: result.failed,
total_count: message_ids.len(),
message_ids,
should_remove_from_state: true,
reload_threshold,
current_message_count,
selected_from_current_page,
}
}
#[allow(clippy::too_many_arguments)]
pub fn create_send_context(
result: &BulkOperationResult,
message_ids_to_remove: Vec<String>,
reload_threshold: usize,
from_queue_display: String,
to_queue_display: String,
should_delete: bool,
current_message_count: usize,
selected_from_current_page: usize,
) -> BulkOperationContext {
BulkOperationContext {
operation_type: BulkOperationType::Send {
from_queue_display,
to_queue_display,
should_delete,
},
successful_count: result.successful,
failed_count: result.failed,
total_count: result.total_requested,
message_ids: message_ids_to_remove,
should_remove_from_state: should_delete,
reload_threshold,
current_message_count,
selected_from_current_page,
}
}
pub fn extract_successfully_processed_message_ids(
bulk_data: &crate::app::updates::messages::bulk_execution::task_manager::BulkSendData,
successful_count: usize,
) -> Vec<String> {
use crate::app::updates::messages::bulk_execution::task_manager::BulkSendData;
match bulk_data {
BulkSendData::MessageIds(message_ids) => {
message_ids
.iter()
.take(successful_count)
.map(|id| id.id.clone())
.collect()
}
BulkSendData::MessageData(messages_data) => {
messages_data
.iter()
.take(successful_count)
.map(|(id, _)| id.id.clone())
.collect()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_delete_strategy_large_operation_with_all_current_deleted() {
let context = BulkOperationContext {
operation_type: BulkOperationType::Delete,
successful_count: 100,
failed_count: 0,
total_count: 100,
message_ids: vec![],
should_remove_from_state: true,
reload_threshold: 50,
current_message_count: 20,
selected_from_current_page: 20, };
match BulkOperationPostProcessor::determine_reload_strategy(&context) {
ReloadStrategy::ForceReload { reason } => {
assert!(reason.contains("Complete current page deletion in large operation"));
}
_ => panic!(
"Expected ForceReload strategy for large operation that deletes all current page messages"
),
}
}
#[test]
fn test_delete_strategy_large_operation_partial_current() {
let context = BulkOperationContext {
operation_type: BulkOperationType::Delete,
successful_count: 100,
failed_count: 0,
total_count: 100,
message_ids: vec![],
should_remove_from_state: true,
reload_threshold: 50,
current_message_count: 20,
selected_from_current_page: 10, };
match BulkOperationPostProcessor::determine_reload_strategy(&context) {
ReloadStrategy::LocalRemoval => {}
_ => panic!(
"Expected LocalRemoval strategy for large operation that preserves some current page messages"
),
}
}
#[test]
fn test_delete_strategy_small_operation_all_current_deleted() {
let context = BulkOperationContext {
operation_type: BulkOperationType::Delete,
successful_count: 5,
failed_count: 0,
total_count: 5,
message_ids: vec![],
should_remove_from_state: true,
reload_threshold: 50,
current_message_count: 5,
selected_from_current_page: 5, };
match BulkOperationPostProcessor::determine_reload_strategy(&context) {
ReloadStrategy::LocalRemoval => {}
_ => panic!(
"Expected LocalRemoval strategy for small operation (even if all current deleted)"
),
}
}
#[test]
fn test_delete_strategy_small_local_removal() {
let context = BulkOperationContext {
operation_type: BulkOperationType::Delete,
successful_count: 3,
failed_count: 0,
total_count: 3,
message_ids: vec!["1".to_string(), "2".to_string(), "3".to_string()],
should_remove_from_state: true,
reload_threshold: 50,
current_message_count: 20,
selected_from_current_page: 3,
};
match BulkOperationPostProcessor::determine_reload_strategy(&context) {
ReloadStrategy::LocalRemoval => {}
_ => panic!("Expected LocalRemoval strategy for typical small operation"),
}
}
#[test]
fn test_send_strategy_large_move() {
let context = BulkOperationContext {
operation_type: BulkOperationType::Send {
from_queue_display: "Main".to_string(),
to_queue_display: "DLQ".to_string(),
should_delete: true,
},
successful_count: 2000,
failed_count: 0,
total_count: 2000,
message_ids: vec![],
should_remove_from_state: true,
reload_threshold: 50,
current_message_count: 1000, selected_from_current_page: 1000,
};
match BulkOperationPostProcessor::determine_reload_strategy(&context) {
ReloadStrategy::ForceReload { reason } => {
assert!(reason.contains("Queue emptied by send-with-delete operation"));
}
_ => {
panic!("Expected ForceReload strategy for large send operation that empties queue")
}
}
}
#[test]
fn test_send_strategy_copy_only() {
let context = BulkOperationContext {
operation_type: BulkOperationType::Send {
from_queue_display: "Main".to_string(),
to_queue_display: "Other".to_string(),
should_delete: false, },
successful_count: 50,
failed_count: 0,
total_count: 50,
message_ids: vec![],
should_remove_from_state: false,
reload_threshold: 10,
current_message_count: 1000,
selected_from_current_page: 50,
};
match BulkOperationPostProcessor::determine_reload_strategy(&context) {
ReloadStrategy::CompletionOnly => {}
_ => panic!("Expected CompletionOnly strategy for copy operation"),
}
}
#[test]
fn test_send_strategy_local_removal() {
let context = BulkOperationContext {
operation_type: BulkOperationType::Send {
from_queue_display: "Main".to_string(),
to_queue_display: "DLQ".to_string(),
should_delete: true,
},
successful_count: 2000,
failed_count: 0,
total_count: 2000,
message_ids: vec![],
should_remove_from_state: true,
reload_threshold: 50,
current_message_count: 3000, selected_from_current_page: 1000, };
match BulkOperationPostProcessor::determine_reload_strategy(&context) {
ReloadStrategy::LocalRemoval => {}
_ => panic!(
"Expected LocalRemoval strategy for large send operation that doesn't empty the queue"
),
}
}
#[test]
fn test_send_strategy_force_reload_when_queue_emptied() {
let context = BulkOperationContext {
operation_type: BulkOperationType::Send {
from_queue_display: "Main".to_string(),
to_queue_display: "DLQ".to_string(),
should_delete: true,
},
successful_count: 100,
failed_count: 0,
total_count: 100,
message_ids: vec![],
should_remove_from_state: true,
reload_threshold: 50,
current_message_count: 100, selected_from_current_page: 100,
};
match BulkOperationPostProcessor::determine_reload_strategy(&context) {
ReloadStrategy::ForceReload { reason } => {
assert!(reason.contains("Queue emptied by send-with-delete operation"));
}
_ => panic!(
"Expected ForceReload strategy when send-with-delete empties the entire queue"
),
}
}
}