seamless-glance 1.0.0

Fast, read-only AWS TUI for cloud infrastructure visibility and triage
use async_trait::async_trait;

use crate::{
    aws::clients::AwsClients,
    models::describable::{shell_quote, DescribableResource},
};

#[derive(Debug, Clone)]
pub struct Ec2InstanceInfo {
    pub id: String,
    pub name: Option<String>,
    pub owner: Option<String>,
    pub environment: Option<String>,
    pub avg_cpu_utilization: Option<f64>,
    pub instance_type: String,
    pub state: String,
    pub region: String,
    pub az: String,
    pub private_ip: Option<String>,
    pub public_ip: Option<String>,
    pub key_name: Option<String>,
}

impl Ec2InstanceInfo {
    pub const PRODUCTION_NAME_HINTS: [&str; 7] = [
        "prod",
        "production",
        "live",
        "critical",
        "primary",
        "main",
        "customer",
    ];
    pub const LOW_CPU_THRESHOLD_PERCENT: f64 = 5.0;
    pub const LOW_CPU_LOOKBACK_DAYS: i64 = 7;
    pub const LOW_CPU_PERIOD_SECONDS: i32 = 3600;

    pub fn is_stopped(&self) -> bool {
        self.state == "stopped"
    }

    pub fn is_running(&self) -> bool {
        self.state == "running"
    }

    pub fn has_public_ip(&self) -> bool {
        self.public_ip.is_some()
    }

    pub fn has_production_like_name(&self) -> bool {
        let Some(name) = &self.name else {
            return false;
        };

        let normalized = name.to_ascii_lowercase();
        Self::PRODUCTION_NAME_HINTS
            .iter()
            .any(|hint| normalized.contains(hint))
    }

    pub fn needs_stopped_review(&self) -> bool {
        self.is_stopped() && (self.has_public_ip() || self.has_production_like_name())
    }

    pub fn missing_required_tags(&self) -> Vec<&'static str> {
        let mut missing = Vec::new();

        if self
            .name
            .as_deref()
            .map(|value| value.trim().is_empty())
            .unwrap_or(true)
        {
            missing.push("Name");
        }

        if self
            .owner
            .as_deref()
            .map(|value| value.trim().is_empty())
            .unwrap_or(true)
        {
            missing.push("Owner");
        }

        if self
            .environment
            .as_deref()
            .map(|value| value.trim().is_empty())
            .unwrap_or(true)
        {
            missing.push("Environment");
        }

        missing
    }

    pub fn has_tag_coverage_gap(&self) -> bool {
        !self.missing_required_tags().is_empty()
    }

    pub fn has_sustained_low_cpu(&self) -> bool {
        self.is_running()
            && self
                .avg_cpu_utilization
                .is_some_and(|cpu| cpu < Self::LOW_CPU_THRESHOLD_PERCENT)
    }

    pub fn formatted_avg_cpu(&self) -> String {
        self.avg_cpu_utilization
            .map(|cpu| format!("{cpu:.1}%"))
            .unwrap_or_else(|| "-".to_string())
    }

    pub fn review_signals(&self) -> Vec<&'static str> {
        let mut signals = Vec::new();

        if self.has_public_ip() {
            signals.push("public-ip");
        }

        if self.has_production_like_name() {
            signals.push("prod-name");
        }

        if self.has_tag_coverage_gap() {
            signals.push("missing-tags");
        }

        if self.has_sustained_low_cpu() {
            signals.push("low-cpu");
        }

        signals
    }
}

#[async_trait]
impl DescribableResource for Ec2InstanceInfo {
    fn resource_name(&self) -> String {
        self.name.clone().unwrap_or_else(|| self.id.clone())
    }

    fn action_region(&self) -> Option<&str> {
        Some(&self.region)
    }

    async fn describe(&self, clients: &AwsClients) -> anyhow::Result<String> {
        let resp = clients
            .ec2
            .describe_instances()
            .instance_ids(&self.id)
            .send()
            .await?;

        Ok(format!("{:#?}", resp))
    }

    fn console_url(&self, region: &str) -> Option<String> {
        Some(format!(
            "https://console.aws.amazon.com/ec2/v2/home?region={region}#InstanceDetails:instanceId={}",
            self.id
        ))
    }

    fn cli_command(&self, region: &str) -> Option<String> {
        Some(format!(
            "aws ec2 describe-instances --instance-ids {} --region {}",
            shell_quote(&self.id),
            shell_quote(region)
        ))
    }
}