zc2 0.0.12

P2P compute broker with credit-based billing, WAL, and broker mesh support
//! Transaction flush buffer for batched API writes.
//!
//! Instead of writing each transaction to the dashboard API synchronously on the hot path,
//! completed transactions are queued here and flushed periodically (every 30s)
//! in a single batch. This removes the dashboard API call from the critical path when P2P mode
//! is enabled.

use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicU64, Ordering};

/// A buffered transaction waiting to be flushed to the dashboard API.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BufferedTransaction {
    pub request_id: String,
    pub user_id: String,
    pub tx_type: String,
    pub amount: f64,
    pub balance_after: f64,
    pub worker_id: String,
    pub duration_ms: f64,
    /// Name of the broker node that processed this transaction (executor origin)
    pub source_node: Option<String>,
    /// Name of the worker that executed the job (executor destination)
    pub worker_name: Option<String>,
    /// Worker URI / address (destination endpoint)
    pub worker_uri: Option<String>,
    /// Worker price in credits per hour (zkcr/hr)
    pub price_per_hour: f64,
}

/// Accumulates completed transactions and balance snapshots for periodic API flush.
pub struct TransactionBuffer {
    /// Pending transactions keyed by request_id.
    pending: DashMap<String, BufferedTransaction>,
    /// Authoritative balance snapshots keyed by user_id (latest value wins).
    balance_snapshots: DashMap<String, f64>,
    /// Count of transactions flushed since start.
    total_flushed: AtomicU64,
}

impl TransactionBuffer {
    pub fn new() -> Self {
        Self {
            pending: DashMap::new(),
            balance_snapshots: DashMap::new(),
            total_flushed: AtomicU64::new(0),
        }
    }

    /// Queue a completed transaction for the next flush.
    pub fn push_transaction(&self, tx: BufferedTransaction) {
        self.pending.insert(tx.request_id.clone(), tx);
    }

    /// Record an authoritative balance snapshot for a user.
    pub fn snapshot_balance(&self, user_id: &str, balance: f64) {
        self.balance_snapshots.insert(user_id.to_string(), balance);
    }

    /// Number of pending transactions.
    pub fn pending_count(&self) -> usize {
        self.pending.len()
    }

    /// Total transactions flushed since start.
    pub fn total_flushed(&self) -> u64 {
        self.total_flushed.load(Ordering::Relaxed)
    }

    /// Drain all pending items, returning them.
    fn drain(&self) -> (Vec<BufferedTransaction>, Vec<(String, f64)>) {
        // Drain transactions
        let mut txs = Vec::with_capacity(self.pending.len());
        let keys: Vec<String> = self.pending.iter().map(|e| e.key().clone()).collect();
        for key in keys {
            if let Some((_, tx)) = self.pending.remove(&key) {
                txs.push(tx);
            }
        }

        // Drain balance snapshots
        let mut balances = Vec::with_capacity(self.balance_snapshots.len());
        let bkeys: Vec<String> = self.balance_snapshots.iter().map(|e| e.key().clone()).collect();
        for key in bkeys {
            if let Some((uid, bal)) = self.balance_snapshots.remove(&key) {
                balances.push((uid, bal));
            }
        }

        (txs, balances)
    }

    /// Flush all pending transactions to production dashboard via API.
    /// Returns the number of transactions flushed.
    pub fn flush_to_api(&self, api_url: &str, api_key: &str) -> usize {
        let (txs, _balances) = self.drain();

        if txs.is_empty() {
            return 0;
        }

        let tx_count = txs.len();

        // Build JSON payload for batch-sync API
        let payload: Vec<serde_json::Value> = txs
            .iter()
            .map(|tx| {
                // Map internal tx_type to dashboard transaction_type
                let dashboard_type = match tx.tx_type.as_str() {
                    "commit" => "job_execution",
                    "cancel" => "job_execution",
                    "credit" => "credit_purchase",
                    other => other,
                };
                let (job_name, credits_amount, status) = match tx.tx_type.as_str() {
                    "cancel" => (
                        format!("Cancelled Job ({})", tx.worker_name.as_deref().unwrap_or(&tx.worker_id)),
                        0.0f64,
                        "failed",
                    ),
                    _ => (
                        format!("Compute Job ({})", tx.worker_name.as_deref().unwrap_or(&tx.worker_id)),
                        tx.amount,
                        "completed",
                    ),
                };
                serde_json::json!({
                    "zakuro_user_id": tx.user_id,
                    "job_name": job_name,
                    "transaction_type": dashboard_type,
                    "credits_amount": credits_amount,
                    "status": status,
                    "duration_ms": if tx.duration_ms > 0.0 { serde_json::Value::Number(serde_json::Number::from_f64(tx.duration_ms).unwrap()) } else { serde_json::Value::Null },
                    "worker_id": tx.worker_name,
                    "source_node": tx.source_node,
                    "compute_hours": if tx.duration_ms > 0.0 { Some(tx.duration_ms / 3_600_000.0) } else { None::<f64> },
                    "executor": tx.worker_name,
                    "destination": tx.worker_uri,
                    "price_per_hour": if tx.price_per_hour > 0.0 { Some(tx.price_per_hour) } else { None::<f64> },
                    "metadata": null
                })
            })
            .collect();

        let endpoint = format!("{}/api/broker/batch-sync", api_url.trim_end_matches('/'));

        let payload_str = match serde_json::to_string(&payload) {
            Ok(s) => s,
            Err(e) => {
                eprintln!("  [FLUSH] Failed to serialize payload: {}", e);
                // Return transactions to buffer on failure
                for tx in txs {
                    self.pending.insert(tx.request_id.clone(), tx);
                }
                return 0;
            }
        };

        match ureq::post(&endpoint)
            .set("X-Broker-Api-Key", api_key)
            .set("Content-Type", "application/json")
            .send_string(&payload_str)
        {
            Ok(resp) if resp.status() == 200 => {
                let body = resp.into_string().unwrap_or_default();
                let parsed: serde_json::Value = serde_json::from_str(&body).unwrap_or_default();
                let inserted = parsed["inserted"].as_u64().unwrap_or(tx_count as u64);
                let failed   = parsed["failed"].as_u64().unwrap_or(0);
                let success  = parsed["success"].as_bool().unwrap_or(true);

                self.total_flushed.fetch_add(inserted, Ordering::Relaxed);

                if !success || failed > 0 {
                    eprintln!("  [FLUSH] Partial sync to {}: {} inserted, {} failed",
                              api_url, inserted, failed);
                    if let Some(errors) = parsed["errors"].as_array() {
                        for e in errors.iter().take(3) {
                            eprintln!("  [FLUSH]   → {}: {}",
                                      e["job_name"].as_str().unwrap_or("?"),
                                      e["error"].as_str().unwrap_or("unknown"));
                        }
                        if errors.len() > 3 {
                            eprintln!("  [FLUSH]   … and {} more errors", errors.len() - 3);
                        }
                    }
                } else {
                    println!("  [FLUSH] Synced {} transactions to {} via API", inserted, api_url);
                }
                inserted as usize
            }
            Ok(resp) => {
                eprintln!("  [FLUSH] API sync failed: status {}", resp.status());
                // Return transactions to buffer on failure
                for tx in txs {
                    self.pending.insert(tx.request_id.clone(), tx);
                }
                0
            }
            Err(e) => {
                eprintln!("  [FLUSH] API sync failed: {}", e);
                // Return transactions to buffer on failure
                for tx in txs {
                    self.pending.insert(tx.request_id.clone(), tx);
                }
                0
            }
        }
    }
}

impl std::fmt::Debug for TransactionBuffer {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("TransactionBuffer")
            .field("pending", &self.pending.len())
            .field("balance_snapshots", &self.balance_snapshots.len())
            .field("total_flushed", &self.total_flushed())
            .finish()
    }
}