revoke-cli 0.3.0

Command-line interface for managing Revoke microservices infrastructure
use anyhow::{Result, Context};
use colored::Colorize;
use tabled::{Table, Tabled, settings::Style};
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc, Duration};
use crate::config::Config;
use crate::commands::TraceCommands;
use crate::utils::{print_success, print_error, print_info};

#[derive(Tabled, Serialize, Deserialize)]
struct TraceRow {
    #[tabled(rename = "Trace ID")]
    trace_id: String,
    #[tabled(rename = "Service")]
    service: String,
    #[tabled(rename = "Operation")]
    operation: String,
    #[tabled(rename = "Duration")]
    duration: String,
    #[tabled(rename = "Status")]
    status: String,
    #[tabled(rename = "Time")]
    timestamp: String,
}

#[derive(Serialize, Deserialize)]
struct SpanDetail {
    span_id: String,
    parent_span_id: Option<String>,
    operation: String,
    service: String,
    start_time: DateTime<Utc>,
    duration_ms: u64,
    status: String,
    attributes: std::collections::HashMap<String, String>,
}

pub async fn handle_command(command: TraceCommands, config: &Config) -> Result<()> {
    match command {
        TraceCommands::List { service, range, limit } => {
            list_traces(service, &range, limit, config).await?
        }
        TraceCommands::Get { trace_id } => {
            get_trace_details(&trace_id, config).await?
        }
        TraceCommands::Search { query, range } => {
            search_traces(&query, &range, config).await?
        }
        TraceCommands::Export { output, format, range } => {
            export_traces(&output, &format, &range, config).await?
        }
    }
    
    Ok(())
}

async fn list_traces(
    service_filter: Option<String>,
    range: &str,
    limit: usize,
    config: &Config,
) -> Result<()> {
    let duration = parse_duration(range)?;
    let since = Utc::now() - duration;
    
    // In a real implementation, this would query the trace backend
    let traces = vec![
        TraceRow {
            trace_id: "a1b2c3d4e5f6".to_string(),
            service: "user-service".to_string(),
            operation: "GET /api/users/:id".to_string(),
            duration: "45ms".to_string(),
            status: "success".green().to_string(),
            timestamp: "2024-01-15 10:23:45".to_string(),
        },
        TraceRow {
            trace_id: "f6e5d4c3b2a1".to_string(),
            service: "order-service".to_string(),
            operation: "POST /api/orders".to_string(),
            duration: "234ms".to_string(),
            status: "error".red().to_string(),
            timestamp: "2024-01-15 10:22:30".to_string(),
        },
    ];
    
    let filtered_traces: Vec<_> = if let Some(filter) = service_filter {
        traces.into_iter()
            .filter(|t| t.service.contains(&filter))
            .take(limit)
            .collect()
    } else {
        traces.into_iter().take(limit).collect()
    };
    
    if filtered_traces.is_empty() {
        print_info(&format!("No traces found in the last {}", range));
    } else {
        println!("{}", format!("Traces (last {})", range).bold());
        println!("{}", "=".repeat(80));
        
        let table = Table::new(&filtered_traces)
            .with(Style::modern())
            .to_string();
        println!("{}", table);
        
        println!("\n{}: {} traces shown", 
            "Total".bold(), 
            filtered_traces.len()
        );
    }
    
    Ok(())
}

async fn get_trace_details(trace_id: &str, config: &Config) -> Result<()> {
    print_info(&format!("Fetching details for trace: {}", trace_id));
    
    // In a real implementation, this would query the trace backend
    let spans = vec![
        SpanDetail {
            span_id: "span-1".to_string(),
            parent_span_id: None,
            operation: "HTTP GET /api/users/123".to_string(),
            service: "gateway".to_string(),
            start_time: Utc::now() - Duration::milliseconds(100),
            duration_ms: 95,
            status: "ok".to_string(),
            attributes: [
                ("http.method".to_string(), "GET".to_string()),
                ("http.status_code".to_string(), "200".to_string()),
                ("user.id".to_string(), "123".to_string()),
            ].into_iter().collect(),
        },
        SpanDetail {
            span_id: "span-2".to_string(),
            parent_span_id: Some("span-1".to_string()),
            operation: "database.query".to_string(),
            service: "user-service".to_string(),
            start_time: Utc::now() - Duration::milliseconds(80),
            duration_ms: 45,
            status: "ok".to_string(),
            attributes: [
                ("db.statement".to_string(), "SELECT * FROM users WHERE id = ?".to_string()),
                ("db.type".to_string(), "postgresql".to_string()),
            ].into_iter().collect(),
        },
    ];
    
    println!("\n{}", "Trace Details".bold());
    println!("{}", "=".repeat(80));
    println!("{}: {}", "Trace ID".bold(), trace_id);
    println!("{}: {} spans", "Total Spans".bold(), spans.len());
    println!("{}: {}ms", "Total Duration".bold(), spans.iter().map(|s| s.duration_ms).max().unwrap_or(0));
    
    println!("\n{}", "Span Tree".bold());
    println!("{}", "-".repeat(80));
    
    for (i, span) in spans.iter().enumerate() {
        let indent = if span.parent_span_id.is_some() { "  └─ " } else { "" };
        
        println!("{}{} {} ({} ms) {}", 
            indent,
            span.service.cyan(),
            span.operation,
            span.duration_ms,
            if span.status == "ok" { 
                "✓".green().to_string() 
            } else { 
                "✗".red().to_string() 
            }
        );
        
        if !span.attributes.is_empty() {
            let attr_indent = if span.parent_span_id.is_some() { "      " } else { "  " };
            for (key, value) in &span.attributes {
                println!("{}{}: {}", attr_indent, key.dimmed(), value);
            }
        }
    }
    
    Ok(())
}

async fn search_traces(query: &str, range: &str, config: &Config) -> Result<()> {
    print_info(&format!("Searching traces for: {}", query));
    
    let duration = parse_duration(range)?;
    let since = Utc::now() - duration;
    
    // In a real implementation, this would search the trace backend
    let results = vec![
        TraceRow {
            trace_id: "search-result-1".to_string(),
            service: "user-service".to_string(),
            operation: "GET /api/users/search".to_string(),
            duration: "123ms".to_string(),
            status: "success".green().to_string(),
            timestamp: "2024-01-15 09:45:23".to_string(),
        },
    ];
    
    if results.is_empty() {
        print_info(&format!("No traces found matching '{}' in the last {}", query, range));
    } else {
        println!("\n{}", format!("Search Results for '{}' (last {})", query, range).bold());
        println!("{}", "=".repeat(80));
        
        let table = Table::new(&results)
            .with(Style::modern())
            .to_string();
        println!("{}", table);
        
        println!("\n{}: {} traces found", 
            "Total".bold(), 
            results.len()
        );
    }
    
    Ok(())
}

async fn export_traces(
    output: &str,
    format: &str,
    range: &str,
    config: &Config,
) -> Result<()> {
    print_info(&format!("Exporting traces from the last {} to {}", range, output));
    
    let duration = parse_duration(range)?;
    let since = Utc::now() - duration;
    
    // In a real implementation, this would query and export traces
    let traces = vec![
        TraceRow {
            trace_id: "export-1".to_string(),
            service: "user-service".to_string(),
            operation: "GET /api/users".to_string(),
            duration: "32ms".to_string(),
            status: "success".to_string(),
            timestamp: Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
        },
    ];
    
    let content = match format {
        "json" => serde_json::to_string_pretty(&traces)?,
        "csv" => {
            let mut wtr = csv::Writer::from_writer(vec![]);
            for trace in &traces {
                wtr.serialize(trace)?;
            }
            String::from_utf8(wtr.into_inner()?)?
        }
        _ => return Err(anyhow::anyhow!("Unsupported export format: {}", format)),
    };
    
    std::fs::write(output, content)?;
    
    print_success(&format!(
        "Exported {} traces to {} in {} format",
        traces.len(),
        output,
        format
    ));
    
    Ok(())
}

fn parse_duration(duration_str: &str) -> Result<Duration> {
    let len = duration_str.len();
    if len < 2 {
        return Err(anyhow::anyhow!("Invalid duration format"));
    }
    
    let (num_str, unit) = duration_str.split_at(len - 1);
    let num: i64 = num_str.parse()
        .context("Invalid duration number")?;
    
    let duration = match unit {
        "s" => Duration::seconds(num),
        "m" => Duration::minutes(num),
        "h" => Duration::hours(num),
        "d" => Duration::days(num),
        _ => return Err(anyhow::anyhow!("Invalid duration unit. Use s, m, h, or d")),
    };
    
    Ok(duration)
}