firecloud-cli 0.2.0

Command-line interface for FireCloud P2P messaging and file sharing
use anyhow::{Context, Result};
use colored::Colorize;
use std::path::PathBuf;
use firecloud_net::{FireCloudNode, NodeConfig, NodeEvent};
use comfy_table::{Table, presets::UTF8_FULL};
use std::time::Duration;

pub async fn run(data_dir: PathBuf, live: bool) -> Result<()> {
    loop {
        if live {
            print!("\x1B[2J\x1B[1;1H"); // Clear screen
        }
        
        println!("\n{}", "🌐 FireCloud Network Topology".bold().cyan());
        println!("{}", "".repeat(50).dimmed());
        
        // Initialize temporary node to query network
        let config = NodeConfig {
            port: 0,
            enable_mdns: true,
            bootstrap_peers: Vec::new(),
            bootstrap_relays: vec![],
        };
        
        let mut node = FireCloudNode::new(config)
            .await
            .context("Failed to initialize node")?;
        
        println!("\n{} Your Peer ID: {}", "".blue(), node.local_peer_id().to_string().yellow());
        
        // Discover peers for a few seconds
        println!("\n{} Discovering network...", "🔍".cyan());
        let mut all_peers = Vec::new();
        let discovery_timeout = tokio::time::Instant::now() + Duration::from_secs(5);

        while tokio::time::Instant::now() < discovery_timeout {
            tokio::select! {
                _ = tokio::time::sleep(Duration::from_millis(100)) => {}
                event = node.poll_event() => {
                    if let Some(event) = event {
                        match event {
                            NodeEvent::PeerDiscovered(peer_id) => {
                                if peer_id != node.local_peer_id() {
                                    all_peers.push(peer_id);
                                }
                            }
                            NodeEvent::Listening(addr) => {
                                println!("{} Listening on {}", "".green(), addr.to_string().dimmed());
                            }
                            _ => {}
                        }
                    }
                }
            }
        }
        
        if all_peers.is_empty() {
            println!("\n{} No peers connected", "".yellow());
            println!("  Start the network node: {}", "firecloud node".cyan());
            if !live {
                return Ok(());
            } else {
                println!("\n{} Retrying in 5 seconds... Press Ctrl+C to exit.", "".blue());
                tokio::time::sleep(Duration::from_secs(5)).await;
                continue;
            }
        }
        
        println!("\n{} Connected Peers: {}", "".green(), all_peers.len());
        
        // Query storage providers from DHT
        println!("{} Querying for storage providers...", "🔍".cyan());
        let providers = match node.find_storage_providers(10).await {
            Ok(p) => p,
            Err(e) => {
                eprintln!("{} Warning: Could not query storage providers: {}", "".yellow(), e);
                vec![]
            }
        };
        
        // Create network topology visualization
        println!("\n{}", "Network Topology:".bold());
        println!("┌─ {} (you)", "Local Node".cyan());
        
        for (idx, peer_id) in all_peers.iter().enumerate() {
            let is_provider = providers.iter().any(|p| p.peer_id == *peer_id);
            let symbol = if idx == all_peers.len() - 1 { "" } else { "" };
            let role = if is_provider { "📦 Provider" } else { "👤 User" };
            
            println!("{}{} {}", 
                     symbol, 
                     truncate_peer_id(&peer_id.to_string()).dimmed(), 
                     role.yellow());
        }
        
        // Storage provider details
        if !providers.is_empty() {
            println!("\n{}", "Storage Providers:".bold());
            
            let mut table = Table::new();
            table.load_preset(UTF8_FULL);
            table.set_header(vec!["Peer ID", "Available Space", "Status"]);
            
            for provider in &providers {
                let peer_id_str = truncate_peer_id(&provider.peer_id.to_string());
                let space = format_size(provider.available_space);
                let status = "🟢 Online";
                
                table.add_row(vec![peer_id_str, space, status.to_string()]);
            }
            
            println!("{}\n", table);
        }
        
        // Network statistics
        println!("{}", "Network Statistics:".bold());
        println!("  Total Nodes: {}", all_peers.len() + 1);
        println!("  Storage Providers: {}", providers.len());
        
        let total_storage: u64 = providers.iter()
            .map(|p| p.available_space)
            .sum();
        
        if total_storage > 0 {
            println!("  Total Available Storage: {}", format_size(total_storage).green());
        }
        
        // Live update mode
        if live {
            println!("\n{} Live mode enabled. Press Ctrl+C to exit. Refreshing in 5s...", "".blue());
            tokio::time::sleep(Duration::from_secs(5)).await;
            // Loop will continue and refresh
        } else {
            println!();
            break;
        }
    }
    
    Ok(())
}

/// Truncate peer ID to show first 8 and last 6 characters
fn truncate_peer_id(peer_id: &str) -> String {
    if peer_id.len() > 20 {
        format!("{}...{}", &peer_id[..8], &peer_id[peer_id.len()-6..])
    } else {
        peer_id.to_string()
    }
}

/// Format bytes to human-readable string
fn format_size(bytes: u64) -> String {
    const KB: u64 = 1_024;
    const MB: u64 = 1_024 * 1_024;
    const GB: u64 = 1_024 * 1_024 * 1_024;
    const TB: u64 = 1_024 * 1_024 * 1_024 * 1_024;
    
    if bytes >= TB {
        format!("{:.2} TB", bytes as f64 / TB as f64)
    } else if bytes >= GB {
        format!("{:.2} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.2} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.2} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} B", bytes)
    }
}