//! Offline outbox — queue messages for deferred delivery.
//!
//! When the client is disconnected or an enqueue RPC fails, messages are
//! persisted in the local SQLCipher outbox table. On reconnect, `flush_outbox`
//! retries each pending entry with exponential backoff, up to `MAX_RETRIES`.

use bytes::Bytes;
use prost::Message;
use tracing::{debug, info, warn};

use quicproquo_proto::method_ids;
use quicproquo_proto::qpq::v1::{EnqueueRequest, EnqueueResponse};
use quicproquo_rpc::client::RpcClient;

use crate::conversation::{ConversationId, ConversationStore};
use crate::error::SdkError;

/// Maximum retry attempts before marking an entry as permanently failed.
const MAX_RETRIES: u32 = 10;

/// Generate a 16-byte message ID for idempotent enqueue.
///
/// Uses random bytes (no UUID v7 dependency). The server uses this for dedup.
pub fn generate_message_id() -> Vec<u8> {
    let mut id = vec![0u8; 16];
    rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut id);
    id
}

/// Queue a message for sending when connectivity is restored.
pub fn queue_outbox(
    conv_store: &ConversationStore,
    conv_id: &ConversationId,
    recipient_key: &[u8],
    payload: &[u8],
) -> Result<(), SdkError> {
    conv_store
        .enqueue_outbox(conv_id, recipient_key, payload)
        .map_err(|e| SdkError::Storage(format!("enqueue outbox: {e}")))
}

/// Process all pending outbox entries — send them to the server.
///
/// Uses exponential backoff delay between retries (1s base, max 60s).
/// Returns `(sent, failed)` counts.
pub async fn flush_outbox(
    rpc: &RpcClient,
    conv_store: &ConversationStore,
) -> Result<(usize, usize), SdkError> {
    let entries = conv_store
        .load_pending_outbox()
        .map_err(|e| SdkError::Storage(format!("load outbox: {e}")))?;

    if entries.is_empty() {
        return Ok((0, 0));
    }

    info!(pending = entries.len(), "flushing outbox");

    let mut sent = 0usize;
    let mut failed = 0usize;

    for entry in &entries {
        // Generate a message_id for idempotent retry.
        let message_id = generate_message_id();

        let req = EnqueueRequest {
            recipient_key: entry.recipient_key.clone(),
            payload: entry.payload.clone(),
            channel_id: entry.conversation_id.0.to_vec(),
            ttl_secs: 0,
            message_id,
        };
        match rpc
            .call(method_ids::ENQUEUE, Bytes::from(req.encode_to_vec()))
            .await
        {
            Ok(resp_bytes) => {
                match EnqueueResponse::decode(resp_bytes) {
                    Ok(resp) => {
                        if resp.duplicate {
                            debug!(outbox_id = entry.id, "duplicate enqueue (idempotent)");
                        }
                    }
                    Err(e) => {
                        warn!(outbox_id = entry.id, "decode enqueue response: {e}");
                    }
                }
                conv_store
                    .mark_outbox_sent(entry.id)
                    .map_err(|e| SdkError::Storage(format!("mark_outbox_sent: {e}")))?;
                sent += 1;
                debug!(outbox_id = entry.id, "outbox entry sent");
            }
            Err(e) => {
                let new_count = entry.retry_count + 1;
                if new_count > MAX_RETRIES {
                    warn!(outbox_id = entry.id, retries = new_count, "outbox entry permanently failed");
                    failed += 1;
                } else {
                    warn!(outbox_id = entry.id, retries = new_count, "outbox send failed: {e}");
                }
                conv_store
                    .mark_outbox_failed(entry.id, new_count)
                    .map_err(|e| SdkError::Storage(format!("mark_outbox_failed: {e}")))?;
            }
        }
    }

    info!(sent, failed, "outbox flush complete");
    Ok((sent, failed))
}

/// Get the number of pending outbox entries.
pub fn outbox_count(conv_store: &ConversationStore) -> Result<usize, SdkError> {
    conv_store
        .count_pending_outbox()
        .map_err(|e| SdkError::Storage(format!("count outbox: {e}")))
}

/// List pending outbox entries for display.
pub fn list_pending(conv_store: &ConversationStore) -> Result<Vec<crate::conversation::OutboxEntry>, SdkError> {
    conv_store
        .load_pending_outbox()
        .map_err(|e| SdkError::Storage(format!("load outbox: {e}")))
}

/// Clear all permanently failed outbox entries.
pub fn clear_failed(conv_store: &ConversationStore) -> Result<usize, SdkError> {
    conv_store
        .clear_failed_outbox()
        .map_err(|e| SdkError::Storage(format!("clear failed outbox: {e}")))
}
