seamless-glance 1.0.0

Fast, read-only AWS TUI for cloud infrastructure visibility and triage
use crate::{
    app::App, aws::clients::AwsClients, models::ec2::Ec2InstanceInfo,
    resources::region_aggregate::fetch_all_regions,
};
use aws_sdk_cloudwatch::{
    primitives::DateTime,
    types::{Dimension, Metric, MetricDataQuery, MetricStat, StandardUnit},
};
use aws_types::region::Region;
use chrono::Utc;
use std::collections::HashMap;

pub struct Ec2Counts {
    pub running: u32,
    pub stopped: u32,
}

async fn clients_for_region(region: &Region) -> AwsClients {
    let sdk_config = aws_config::defaults(aws_config::BehaviorVersion::v2026_01_12())
        .region(region.clone())
        .load()
        .await;

    AwsClients::new(&sdk_config)
}

async fn fetch_average_cpu_utilization(
    aws: &AwsClients,
    instances: &[Ec2InstanceInfo],
) -> Result<HashMap<String, f64>, String> {
    let running_instances = instances
        .iter()
        .filter(|instance| instance.is_running())
        .collect::<Vec<_>>();

    if running_instances.is_empty() {
        return Ok(HashMap::new());
    }

    let end = Utc::now();
    let start = end - chrono::Duration::days(Ec2InstanceInfo::LOW_CPU_LOOKBACK_DAYS);
    let mut averages_by_instance = HashMap::new();

    for chunk in running_instances.chunks(100) {
        let mut instance_ids_by_query = HashMap::new();
        let queries = chunk
            .iter()
            .enumerate()
            .map(|(index, instance)| {
                let query_id = format!("m{index}");
                instance_ids_by_query.insert(query_id.clone(), instance.id.clone());

                MetricDataQuery::builder()
                    .id(query_id)
                    .metric_stat(
                        MetricStat::builder()
                            .metric(
                                Metric::builder()
                                    .namespace("AWS/EC2")
                                    .metric_name("CPUUtilization")
                                    .dimensions(
                                        Dimension::builder()
                                            .name("InstanceId")
                                            .value(instance.id.clone())
                                            .build(),
                                    )
                                    .build(),
                            )
                            .period(Ec2InstanceInfo::LOW_CPU_PERIOD_SECONDS)
                            .stat("Average")
                            .unit(StandardUnit::Percent)
                            .build(),
                    )
                    .return_data(true)
                    .build()
            })
            .collect::<Vec<_>>();

        let response = aws
            .cw
            .get_metric_data()
            .set_metric_data_queries(Some(queries))
            .start_time(DateTime::from_secs(start.timestamp()))
            .end_time(DateTime::from_secs(end.timestamp()))
            .send()
            .await
            .map_err(|err| {
                format!("CloudWatch get_metric_data failed for EC2 CPU lookup: {err:?}")
            })?;

        for result in response.metric_data_results() {
            let Some(query_id) = result.id() else {
                continue;
            };

            let Some(instance_id) = instance_ids_by_query.get(query_id) else {
                continue;
            };

            let values = result.values();
            if values.is_empty() {
                continue;
            }

            let average = values.iter().sum::<f64>() / values.len() as f64;
            averages_by_instance.insert(instance_id.clone(), average);
        }
    }

    Ok(averages_by_instance)
}

async fn fetch_instances_for_region(
    region: Region,
    include_cpu_metrics: bool,
) -> Result<Vec<Ec2InstanceInfo>, String> {
    let aws = clients_for_region(&region).await;

    let resp = aws.ec2.describe_instances().send().await.map_err(|err| {
        format!(
            "EC2 describe_instances failed for {}: {:?}",
            region.as_ref(),
            err
        )
    })?;

    let mut instances = vec![];

    for reservation in resp.reservations() {
        for inst in reservation.instances() {
            let tag_value = |key: &str| {
                inst.tags()
                    .iter()
                    .find(|t| t.key().unwrap_or("") == key)
                    .and_then(|t| t.value().map(|v| v.to_string()))
            };

            let name = inst
                .tags()
                .iter()
                .find(|t| t.key().unwrap_or("") == "Name")
                .and_then(|t| t.value().map(|v| v.to_string()));

            instances.push(Ec2InstanceInfo {
                id: inst.instance_id().unwrap_or("").to_string(),
                name,
                owner: tag_value("Owner"),
                environment: tag_value("Environment"),
                avg_cpu_utilization: None,
                region: region.as_ref().to_string(),
                instance_type: inst
                    .instance_type()
                    .map(|t| t.as_str().to_string())
                    .unwrap_or_else(|| "unknown".to_string()),
                state: inst
                    .state()
                    .and_then(|s| s.name())
                    .map(|s| s.as_str().to_string())
                    .unwrap_or_else(|| "unknown".to_string()),
                az: inst
                    .placement()
                    .and_then(|p| p.availability_zone())
                    .unwrap_or("")
                    .to_string(),
                private_ip: inst.private_ip_address().map(|s| s.to_string()),
                public_ip: inst.public_ip_address().map(|s| s.to_string()),
                key_name: inst.key_name().map(|k| k.to_string()),
            });
        }
    }

    if include_cpu_metrics {
        let averages_by_instance = fetch_average_cpu_utilization(&aws, &instances).await?;
        for instance in &mut instances {
            instance.avg_cpu_utilization = averages_by_instance.get(&instance.id).copied();
        }
    }

    Ok(instances)
}

async fn fetch_instances_with_metrics(
    app: &App,
    include_cpu_metrics: bool,
) -> Vec<Ec2InstanceInfo> {
    let mut instances = if app.is_global_region_selected() {
        fetch_all_regions(&app.regions, move |region| {
            fetch_instances_for_region(region, include_cpu_metrics)
        })
        .await
    } else {
        match fetch_instances_for_region(app.current_region().clone(), include_cpu_metrics).await {
            Ok(items) => items,
            Err(err) => {
                eprintln!("{}", err);
                vec![]
            }
        }
    };

    instances.sort_by(|a, b| {
        a.region
            .cmp(&b.region)
            .then_with(|| a.name.cmp(&b.name))
            .then_with(|| a.id.cmp(&b.id))
    });

    instances
}

pub async fn fetch_instances(app: &App) -> Vec<Ec2InstanceInfo> {
    fetch_instances_with_metrics(app, true).await
}

pub async fn fetch_ec2_counts(app: &App) -> Ec2Counts {
    let instances = fetch_instances_with_metrics(app, false).await;

    let mut running = 0;
    let mut stopped = 0;

    for instance in instances {
        match instance.state.as_str() {
            "running" => running += 1,
            "stopped" => stopped += 1,
            _ => {}
        }
    }

    Ec2Counts { running, stopped }
}