zc2 0.0.4

P2P compute broker with credit-based billing, WAL, and broker mesh support
//! System information and cluster detection.
//!
//! Provides diagnostics about available compute backends and network status.

use std::net::TcpStream;
use std::time::Duration;

use colored::Colorize;

/// Cluster information
#[derive(Debug, Clone)]
pub struct ClusterInfo {
    pub name: &'static str,
    pub uri: String,
    pub available: bool,
    pub version: Option<String>,
    pub nodes: Option<u32>,
}

/// Network information
#[derive(Debug, Clone)]
pub struct NetworkInfo {
    pub local_ip: Option<String>,
    pub tailscale_ip: Option<String>,
    pub hostname: String,
    pub mode: NetworkMode,
}

#[derive(Debug, Clone, Copy)]
pub enum NetworkMode {
    Tailscale,
    Local,
    Unknown,
}

impl NetworkMode {
    pub fn as_str(&self) -> &'static str {
        match self {
            NetworkMode::Tailscale => "Tailscale P2P",
            NetworkMode::Local => "Local",
            NetworkMode::Unknown => "Unknown",
        }
    }
}

/// Detect available clusters
pub fn detect_clusters() -> Vec<ClusterInfo> {
    let mut clusters = Vec::new();

    // Check Ray
    clusters.push(detect_ray());

    // Check Dask
    clusters.push(detect_dask());

    // Check Spark
    clusters.push(detect_spark());

    clusters
}

fn detect_ray() -> ClusterInfo {
    let default_uri = "ray://127.0.0.1:10001";

    // Check Ray dashboard API (more reliable than port check)
    let (available, version, nodes) = if let Ok(resp) = ureq::get("http://127.0.0.1:8265/api/cluster_status")
        .timeout(Duration::from_secs(2))
        .call()
    {
        if let Ok(body) = resp.into_string() {
            if let Ok(json) = serde_json::from_str::<serde_json::Value>(&body) {
                let nodes = json["result"]["data"]["clusterStatus"]["loadMetricsReport"]["numNodes"]
                    .as_u64()
                    .map(|n| n as u32);
                (true, Some("2.x".to_string()), nodes)
            } else {
                (false, None, None)
            }
        } else {
            (false, None, None)
        }
    } else {
        (false, None, None)
    };

    ClusterInfo {
        name: "Ray",
        uri: default_uri.to_string(),
        available,
        version,
        nodes,
    }
}

fn detect_dask() -> ClusterInfo {
    let default_uri = "dask://127.0.0.1:8786";

    // Check Dask dashboard (more reliable than port check)
    let (available, version, nodes) = if let Ok(resp) = ureq::get("http://127.0.0.1:8787/info/main/workers.html")
        .timeout(Duration::from_secs(2))
        .call()
    {
        if resp.status() == 200 {
            (true, Some("distributed".to_string()), None)
        } else {
            (false, None, None)
        }
    } else {
        (false, None, None)
    };

    ClusterInfo {
        name: "Dask",
        uri: default_uri.to_string(),
        available,
        version,
        nodes,
    }
}

fn detect_spark() -> ClusterInfo {
    let default_uri = "spark://127.0.0.1:7077";

    // Check Spark master UI (more reliable than port check)
    let (available, version, nodes) = if let Ok(resp) = ureq::get("http://127.0.0.1:8080/json/")
        .timeout(Duration::from_secs(2))
        .call()
    {
        if let Ok(body) = resp.into_string() {
            if let Ok(json) = serde_json::from_str::<serde_json::Value>(&body) {
                let workers = json["aliveworkers"].as_u64().map(|n| n as u32);
                (true, Some("3.x".to_string()), workers)
            } else {
                (false, None, None)
            }
        } else {
            (false, None, None)
        }
    } else {
        (false, None, None)
    };

    ClusterInfo {
        name: "Spark",
        uri: default_uri.to_string(),
        available,
        version,
        nodes,
    }
}

fn check_port(host: &str, port: u16) -> bool {
    TcpStream::connect_timeout(
        &format!("{}:{}", host, port).parse().unwrap(),
        Duration::from_secs(1),
    )
    .is_ok()
}

/// Get network information
pub fn get_network_info() -> NetworkInfo {
    let hostname = hostname::get()
        .map(|h| h.to_string_lossy().to_string())
        .unwrap_or_else(|_| "unknown".to_string());

    let local_ip = get_local_ip();
    let tailscale_ip = get_tailscale_ip();

    let mode = if tailscale_ip.is_some() {
        NetworkMode::Tailscale
    } else if local_ip.is_some() {
        NetworkMode::Local
    } else {
        NetworkMode::Unknown
    };

    NetworkInfo {
        local_ip,
        tailscale_ip,
        hostname,
        mode,
    }
}

fn get_local_ip() -> Option<String> {
    // Try to get the default interface IP
    if let Ok(output) = std::process::Command::new("hostname")
        .arg("-I")
        .output()
    {
        if output.status.success() {
            let ips = String::from_utf8_lossy(&output.stdout);
            return ips.split_whitespace().next().map(|s| s.to_string());
        }
    }

    // Fallback: try to bind and check
    if let Ok(socket) = std::net::UdpSocket::bind("0.0.0.0:0") {
        if socket.connect("8.8.8.8:80").is_ok() {
            if let Ok(addr) = socket.local_addr() {
                return Some(addr.ip().to_string());
            }
        }
    }

    None
}

fn get_tailscale_ip() -> Option<String> {
    // Check for tailscale0 interface
    if let Ok(output) = std::process::Command::new("ip")
        .args(["addr", "show", "tailscale0"])
        .output()
    {
        if output.status.success() {
            let output_str = String::from_utf8_lossy(&output.stdout);
            for line in output_str.lines() {
                if line.contains("inet ") && !line.contains("inet6") {
                    // Extract IP from "inet 100.x.x.x/32 ..."
                    if let Some(inet_part) = line.split("inet ").nth(1) {
                        if let Some(ip) = inet_part.split('/').next() {
                            return Some(ip.trim().to_string());
                        }
                    }
                }
            }
        }
    }

    // Try tailscale CLI
    if let Ok(output) = std::process::Command::new("tailscale")
        .args(["ip", "-4"])
        .output()
    {
        if output.status.success() {
            let ip = String::from_utf8_lossy(&output.stdout).trim().to_string();
            if !ip.is_empty() {
                return Some(ip);
            }
        }
    }

    None
}

/// Print system information
pub fn print_info() {
    println!();
    println!("  {}", "╔═══════════════════════════════════════════╗".cyan());
    println!("  {}          {}              {}", "".cyan(), "Zakuro System Info".bold().white(), "".cyan());
    println!("  {}", "╚═══════════════════════════════════════════╝".cyan());
    println!();

    // Network info
    let network = get_network_info();
    println!("  {}", "Network".bold());
    println!("  {}", "".repeat(50));
    println!("    Hostname:      {}", network.hostname);
    println!("    Mode:          {}", match network.mode {
        NetworkMode::Tailscale => network.mode.as_str().green(),
        NetworkMode::Local => network.mode.as_str().yellow(),
        NetworkMode::Unknown => network.mode.as_str().red(),
    });
    if let Some(ip) = &network.local_ip {
        println!("    Local IP:      {}", ip);
    }
    if let Some(ip) = &network.tailscale_ip {
        println!("    Tailscale IP:  {}", ip.cyan());
    }
    println!();

    // Cluster detection
    let clusters = detect_clusters();
    println!("  {}", "Compute Clusters".bold());
    println!("  {}", "".repeat(50));

    for cluster in &clusters {
        let status = if cluster.available {
            "".green()
        } else {
            "".red()
        };

        let version_str = cluster.version.as_deref().unwrap_or("-");
        let nodes_str = cluster.nodes.map(|n| format!("{} nodes", n)).unwrap_or("-".to_string());

        println!(
            "    {} {:8} {:20} {} {}",
            status,
            cluster.name,
            cluster.uri.dimmed(),
            version_str,
            if cluster.available { nodes_str } else { "not running".dimmed().to_string() }
        );
    }
    println!();

    // Environment
    println!("  {}", "Configuration".bold());
    println!("  {}", "".repeat(50));

    // Show only essential environment variables
    let zakuro_auth = std::env::var("ZAKURO_API_KEY").ok();
    let api_url = std::env::var("ZAKURO_API_URL")
        .unwrap_or_else(|_| "https://my.zakuro-ai.com".to_string());
    let tailscale_key = std::env::var("TAILSCALE_AUTHKEY").ok();

    println!("    API URL:           {}", api_url.cyan());
    println!("    ZAKURO_API_KEY:       {}",
        if zakuro_auth.is_some() { "✓ set".green() } else { "✗ not set".red() });
    println!("    TAILSCALE_AUTHKEY: {}",
        if tailscale_key.is_some() { "✓ set".green() } else { "○ optional".dimmed() });
    println!();

    // Show broker status
    println!("  {}", "Services".bold());
    println!("  {}", "".repeat(50));

    let services = [
        ("Production Broker", "my.zakuro-ai.com", 443_u16, true),  // Always show as available (HTTPS endpoint)
        ("Local Broker", "127.0.0.1", 9000, false),
        ("Local Worker", "127.0.0.1", 3960, false),
    ];

    for (name, host, port, is_https) in services {
        let available = if is_https {
            true
        } else {
            check_port(host, port)
        };
        let status = if available { "".green() } else { "".dimmed() };
        let port_display = if is_https { "https".to_string() } else { format!(":{}", port) };
        println!("    {} {:20} {}{}", status, name, host.dimmed(), port_display.dimmed());
    }
    println!();

    // Detect and show mesh nodes (ports 9001–9009)
    let mesh_ports: Vec<u16> = (9001..=9009)
        .filter(|&p| check_port("127.0.0.1", p))
        .collect();

    if !mesh_ports.is_empty() {
        println!("  {}", "Mesh Nodes".bold());
        println!("  {}", "".repeat(50));

        for port in mesh_ports {
            let url = format!("http://127.0.0.1:{}/health", port);
            match ureq::get(&url).timeout(Duration::from_millis(500)).call() {
                Ok(resp) => {
                    if let Ok(body) = resp.into_string() {
                        if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
                            let node_name = v["node_name"].as_str().unwrap_or("unknown");
                            let ts_ip = v["tailscale_ip"].as_str();
                            let ts_connected = v["tailscale_connected"].as_bool().unwrap_or(false);

                            let ts_status = if ts_connected {
                                format!("Tailscale {}", ts_ip.unwrap_or("")).green().to_string()
                            } else {
                                "no Tailscale".red().to_string()
                            };

                            println!(
                                "    {} {:20} :{} {}",
                                "".green(),
                                node_name,
                                port,
                                ts_status
                            );
                            continue;
                        }
                    }
                    println!("    {} :{}", "".green(), port);
                }
                Err(_) => {
                    println!("    {} :{}", "".dimmed(), port);
                }
            }
        }
        println!();
    }
}