use anyhow::Result;
use tabled::{Table, Tabled, settings::Style};

use crate::api;
use crate::util::{format_optional_price_per_hour, format_price_per_hour};

#[derive(Tabled)]
struct InstanceTypeRow {
    #[tabled(rename = "Name")]
    name: String,
    #[tabled(rename = "FID")]
    fid: String,
    #[tabled(rename = "GPUs")]
    gpus: String,
    #[tabled(rename = "Region")]
    region: String,
    #[tabled(rename = "Capacity")]
    capacity: String,
    #[tabled(rename = "Spot Price")]
    spot_price: String,
    #[tabled(rename = "Win Price")]
    win_price: String,
}

#[derive(Tabled)]
struct InstanceTypeRowVerbose {
    #[tabled(rename = "Name")]
    name: String,
    #[tabled(rename = "FID")]
    fid: String,
    #[tabled(rename = "GPU Type")]
    gpu_type: String,
    #[tabled(rename = "GPU Count")]
    gpu_count: String,
    #[tabled(rename = "GPU Mem")]
    gpu_memory: String,
    #[tabled(rename = "RAM")]
    ram: String,
    #[tabled(rename = "Region")]
    region: String,
    #[tabled(rename = "Capacity")]
    capacity: String,
    #[tabled(rename = "Spot Price")]
    spot_price: String,
    #[tabled(rename = "Win Price")]
    win_price: String,
}

struct InstanceTypeData {
    name: String,
    fid: String,
    gpu_type: String,
    num_gpus: i32,
    gpu_memory_gb: i32,
    ram_gb: i32,
    region: String,
    capacity: i32,
    spot_price: String,
    win_price: Option<String>,
}

impl From<&InstanceTypeData> for InstanceTypeRow {
    fn from(data: &InstanceTypeData) -> Self {
        Self {
            name: data.name.clone(),
            fid: data.fid.clone(),
            gpus: format!("{}x {}", data.num_gpus, data.gpu_type),
            region: data.region.clone(),
            capacity: data.capacity.to_string(),
            spot_price: format_price_per_hour(&data.spot_price),
            win_price: format_optional_price_per_hour(&data.win_price),
        }
    }
}

impl From<&InstanceTypeData> for InstanceTypeRowVerbose {
    fn from(data: &InstanceTypeData) -> Self {
        Self {
            name: data.name.clone(),
            fid: data.fid.clone(),
            gpu_type: data.gpu_type.clone(),
            gpu_count: data.num_gpus.to_string(),
            gpu_memory: format!("{}GB", data.gpu_memory_gb),
            ram: format!("{}GB", data.ram_gb),
            region: data.region.clone(),
            capacity: data.capacity.to_string(),
            spot_price: format_price_per_hour(&data.spot_price),
            win_price: format_optional_price_per_hour(&data.win_price),
        }
    }
}

pub async fn run(region: Option<String>, verbose: bool, json: bool) -> Result<()> {
    let client = api::Client::load()?;

    println!("Fetching instance types...");
    let (instance_types, auctions) = tokio::try_join!(
        client.fetch_instance_types(),
        client.fetch_spot_availability(),
    )?;

    let auctions: Vec<_> = match &region {
        Some(r) => auctions.into_iter().filter(|a| &a.region == r).collect(),
        None => auctions,
    };

    if auctions.is_empty() {
        if let Some(r) = region {
            println!("No instance types available in region '{r}'");
        } else {
            println!("No instance types available");
        }
        return Ok(());
    }

    if json {
        let output: Vec<_> = auctions
            .iter()
            .filter_map(|auction| {
                instance_types.get(&auction.instance_type).map(|it| {
                    serde_json::json!({
                        "name": it.name,
                        "fid": it.fid,
                        "gpu_type": it.gpu_type,
                        "gpu_count": it.num_gpus,
                        "gpu_memory_gb": it.gpu_memory_gb,
                        "ram_gb": it.ram_gb,
                        "region": auction.region,
                        "capacity": auction.capacity,
                        "spot_price": auction.last_instance_price,
                        "win_price": auction.lowest_allocated_price,
                    })
                })
            })
            .collect();
        println!("{}", serde_json::to_string_pretty(&output)?);
        return Ok(());
    }

    // Build common data
    let mut data: Vec<InstanceTypeData> = auctions
        .iter()
        .filter_map(|auction| {
            instance_types
                .get(&auction.instance_type)
                .map(|it| InstanceTypeData {
                    name: it.name.clone(),
                    fid: it.fid.clone(),
                    gpu_type: it.gpu_type.clone(),
                    num_gpus: it.num_gpus,
                    gpu_memory_gb: it.gpu_memory_gb,
                    ram_gb: it.ram_gb,
                    region: auction.region.clone(),
                    capacity: auction.capacity,
                    spot_price: auction.last_instance_price.clone(),
                    win_price: auction.lowest_allocated_price.clone(),
                })
        })
        .collect();

    data.sort_by(|a, b| {
        natord::compare(&a.name, &b.name).then_with(|| natord::compare(&a.region, &b.region))
    });

    let count = data.len();

    if verbose {
        let rows: Vec<InstanceTypeRowVerbose> = data.iter().map(Into::into).collect();
        let table = Table::new(&rows).with(Style::rounded()).to_string();
        println!("{table}");
    } else {
        let rows: Vec<InstanceTypeRow> = data.iter().map(Into::into).collect();
        let table = Table::new(&rows).with(Style::rounded()).to_string();
        println!("{table}");
    }

    println!("\nTotal: {count} entries");
    Ok(())
}
