nxthdr 0.2.0

Command line interface for the nxthdr platform
mod api;
mod auth;
mod config;

use clap::{Parser, Subcommand};
use clap_verbosity_flag::{InfoLevel, Verbosity};
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Parser)]
#[command(name = "nxthdr")]
#[command(about = "CLI tool to interact with nxthdr platform", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,

    #[command(flatten)]
    verbose: Verbosity<InfoLevel>,
}

#[derive(Subcommand)]
enum Commands {
    #[command(about = "Login to nxthdr platform")]
    Login,
    #[command(about = "Logout from nxthdr platform")]
    Logout,
    #[command(about = "Show authentication status")]
    Status,
    #[command(about = "Interact with peering platform")]
    Peering {
        #[command(subcommand)]
        command: PeeringCommands,
    },
    #[command(about = "Interact with probing platform")]
    Probing,
}

#[derive(Subcommand)]
enum PeeringCommands {
    #[command(about = "Get your ASN")]
    Asn,
    #[command(about = "Manage prefix leases")]
    Prefix {
        #[command(subcommand)]
        command: PrefixCommands,
    },
}

#[derive(Subcommand)]
enum PrefixCommands {
    #[command(about = "List your active prefix leases")]
    List,
    #[command(about = "Request a new prefix lease")]
    Request {
        #[arg(value_name = "HOURS", help = "Lease duration in hours (1-24)")]
        duration: u32,
    },
    #[command(about = "Revoke a prefix lease")]
    Revoke {
        #[arg(help = "Prefix to revoke (e.g., 2001:db8::/48)")]
        prefix: String,
    },
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();

    tracing_subscriber::fmt().with_max_level(cli.verbose).init();

    match cli.command {
        Commands::Login => {
            handle_login().await?;
        }
        Commands::Logout => {
            handle_logout()?;
        }
        Commands::Status => {
            handle_status()?;
        }
        Commands::Peering { command } => {
            handle_peering(command).await?;
        }
        Commands::Probing => {
            println!("Probing command not yet implemented");
        }
    }

    Ok(())
}

async fn handle_peering(command: PeeringCommands) -> anyhow::Result<()> {
    match command {
        PeeringCommands::Asn => {
            handle_asn_get().await?;
        }
        PeeringCommands::Prefix { command } => match command {
            PrefixCommands::List => {
                handle_prefix_list().await?;
            }
            PrefixCommands::Request { duration } => {
                handle_prefix_request(duration).await?;
            }
            PrefixCommands::Revoke { prefix } => {
                handle_prefix_revoke(&prefix).await?;
            }
        },
    }
    Ok(())
}

async fn handle_asn_get() -> anyhow::Result<()> {
    use serde::Deserialize;

    #[derive(Deserialize)]
    struct UserInfo {
        asn: Option<i32>,
    }

    let client = api::ApiClient::new();
    let user_info: UserInfo = client.get("/api/user/info").await?;

    if let Some(asn) = user_info.asn {
        println!("{}", asn);
    } else {
        println!("No ASN assigned yet. An ASN will be automatically assigned on first use.");
    }

    Ok(())
}

async fn handle_prefix_list() -> anyhow::Result<()> {
    use serde::Deserialize;

    #[derive(Deserialize)]
    struct PrefixLease {
        prefix: String,
        end_time: String,
    }

    #[derive(Deserialize)]
    struct UserInfo {
        active_leases: Vec<PrefixLease>,
    }

    let client = api::ApiClient::new();
    let user_info: UserInfo = client.get("/api/user/info").await?;

    if user_info.active_leases.is_empty() {
        println!("No active prefix leases.");
        println!("Run 'nxthdr peering prefix request <duration>' to request a prefix.");
    } else {
        println!("Active prefix leases:");
        for lease in user_info.active_leases {
            println!("  {} (expires: {})", lease.prefix, lease.end_time);
        }
    }

    Ok(())
}

async fn handle_prefix_request(duration: u32) -> anyhow::Result<()> {
    use serde::{Deserialize, Serialize};

    #[derive(Serialize)]
    struct PrefixRequest {
        duration_hours: u32,
    }

    #[derive(Deserialize)]
    struct PrefixResponse {
        prefix: String,
        end_time: String,
        message: String,
    }

    let client = api::ApiClient::new();
    let response: PrefixResponse = client
        .post(
            "/api/user/prefix",
            &PrefixRequest {
                duration_hours: duration,
            },
        )
        .await?;

    println!("{}", response.message);
    println!("Prefix: {}", response.prefix);
    println!("Valid until: {}", response.end_time);

    Ok(())
}

async fn handle_prefix_revoke(prefix: &str) -> anyhow::Result<()> {
    let client = api::ApiClient::new();
    let encoded_prefix = urlencoding::encode(prefix);
    let path = format!("/api/user/prefix/{}", encoded_prefix);
    client.delete(&path).await?;

    println!("✓ Prefix lease revoked successfully");

    Ok(())
}

async fn handle_login() -> anyhow::Result<()> {
    if config::tokens_exist() {
        println!(
            "You are already logged in. Run 'nxthdr logout' first if you want to login again."
        );
        return Ok(());
    }

    println!("Starting authentication...\n");

    let device_code = auth::start_device_flow().await?;

    println!("Please visit the following URL to authenticate:");
    println!("\n  {}\n", device_code.verification_uri_complete);
    println!(
        "Or go to {} and enter code: {}",
        device_code.verification_uri, device_code.user_code
    );
    println!("\nWaiting for authentication...");

    let (access_token, refresh_token, expires_at) =
        auth::poll_for_token(&device_code.device_code, device_code.interval).await?;

    let tokens = config::TokenStorage {
        access_token,
        refresh_token,
        expires_at,
    };

    config::save_tokens(&tokens)?;

    println!("\n✓ Successfully authenticated!");
    println!("Tokens saved to: {:?}", config::get_token_path()?);

    Ok(())
}

fn handle_logout() -> anyhow::Result<()> {
    if !config::tokens_exist() {
        println!("You are not logged in.");
        return Ok(());
    }

    config::delete_tokens()?;
    println!("✓ Successfully logged out.");

    Ok(())
}

fn handle_status() -> anyhow::Result<()> {
    if !config::tokens_exist() {
        println!("Status: Not logged in");
        println!("\nRun 'nxthdr login' to authenticate.");
        return Ok(());
    }

    let tokens = config::load_tokens()?;
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs() as i64;

    let is_expired = tokens.expires_at < now;
    let time_until_expiry = tokens.expires_at - now;

    println!("Status: Logged in");
    println!("Token path: {:?}", config::get_token_path()?);

    if is_expired {
        println!("Access token: Expired");
        println!(
            "\nYour access token has expired. It will be automatically refreshed on next API call."
        );
    } else {
        let hours = time_until_expiry / 3600;
        let minutes = (time_until_expiry % 3600) / 60;
        println!("Access token: Valid for {}h {}m", hours, minutes);
    }

    Ok(())
}