modelsdev 0.11.4

A fast TUI and CLI for browsing AI models, benchmarks, and coding agents
use serde_json::Value;

use super::normalize_component_status;
use crate::status::types::{
    available_detail_state, ActiveIncident, ComponentStatus, IncidentUpdate, OfficialSnapshot,
    OfficialStatusSource, ProviderHealth, ScheduledMaintenance, StatusDetailSource,
    StatusSourceMethod,
};

fn status_io_code_to_string(code: u64) -> String {
    match code {
        100 => "operational".to_string(),
        200 => "under_maintenance".to_string(),
        300 | 400 => "degraded_performance".to_string(),
        500 => "major_outage".to_string(),
        600 => "security_event".to_string(),
        _ => format!("unknown_{code}"),
    }
}

fn status_io_code_to_health(code: u64) -> ProviderHealth {
    match code {
        100 => ProviderHealth::Operational,
        200 => ProviderHealth::Maintenance,
        300 | 400 => ProviderHealth::Degraded,
        500 => ProviderHealth::Outage,
        600 => ProviderHealth::Unknown,
        _ => ProviderHealth::Unknown,
    }
}

fn status_io_code_or_label_to_status(value: &str) -> String {
    if let Ok(code) = value.parse::<u64>() {
        return status_io_code_to_string(code);
    }

    let normalized = value.trim().to_lowercase();
    if normalized.contains("maintenance") {
        "under_maintenance".to_string()
    } else if normalized.contains("security") {
        "security_event".to_string()
    } else if normalized.contains("major") || normalized.contains("disruption") {
        "major_outage".to_string()
    } else if normalized.contains("partial")
        || normalized.contains("minor")
        || normalized.contains("degrad")
    {
        "degraded_performance".to_string()
    } else if normalized.contains("operational") {
        "operational".to_string()
    } else {
        normalize_component_status(value)
    }
}

fn status_io_datetime(value: &Value) -> Option<String> {
    value.as_str().map(str::to_string)
}

fn status_io_latest_message(messages: &[Value]) -> Option<&Value> {
    messages.iter().max_by_key(|message| {
        message
            .get("datetime")
            .and_then(|value| value.as_str())
            .and_then(crate::agents::helpers::parse_date)
            .map(|dt| dt.timestamp())
            .unwrap_or(0)
    })
}

fn status_io_collect_names(value: Option<&Value>) -> Vec<String> {
    value
        .and_then(|value| value.as_array())
        .map(|items| {
            items
                .iter()
                .filter_map(|item| {
                    item.get("name")
                        .and_then(|value| value.as_str())
                        .or_else(|| item.as_str())
                        .map(str::to_string)
                })
                .collect()
        })
        .unwrap_or_default()
}

fn status_io_state_code_to_label(code: u64) -> &'static str {
    match code {
        100 => "investigating",
        200 => "identified",
        300 => "monitoring",
        _ => "reported",
    }
}

fn status_io_message_state(message: &Value) -> Option<String> {
    // State can be numeric (100/200/300) or a string label
    message
        .get("state")
        .and_then(|value| {
            value
                .as_u64()
                .map(|code| status_io_state_code_to_label(code).to_string())
                .or_else(|| value.as_str().map(str::to_string))
        })
        .or_else(|| {
            message.get("status").and_then(|value| {
                value
                    .as_u64()
                    .map(status_io_code_to_string)
                    .or_else(|| value.as_str().map(str::to_string))
            })
        })
}

pub(crate) fn parse_status_io(
    source: OfficialStatusSource,
    body: &str,
) -> Result<OfficialSnapshot, String> {
    let v: Value = serde_json::from_str(body).map_err(|err| err.to_string())?;

    let result = v.get("result").ok_or("missing result field")?;

    let overall_code = result
        .pointer("/status_overall/status_code")
        .and_then(|v| v.as_u64())
        .unwrap_or(100);

    let health = status_io_code_to_health(overall_code);

    // Status.io groups are the services (e.g., "Background Processing", "API"),
    // containers are infrastructure details (e.g., "Google Compute Engine").
    // Use the worst container status as the group's status.
    let components: Vec<ComponentStatus> = result
        .get("status")
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|status_group| {
                    let name = status_group.get("name").and_then(|v| v.as_str())?;
                    let worst_code = status_group
                        .get("containers")
                        .and_then(|v| v.as_array())
                        .into_iter()
                        .flatten()
                        .filter_map(|c| c.get("status_code").and_then(|v| v.as_u64()))
                        .max()
                        .unwrap_or(100);
                    Some(ComponentStatus {
                        name: name.to_string(),
                        status: status_io_code_to_string(worst_code),
                        group_name: None,
                        position: None,
                        only_show_if_degraded: false,
                    })
                })
                .collect()
        })
        .unwrap_or_default();

    let incidents: Vec<ActiveIncident> = result
        .get("incidents")
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|i| {
                    let name = i.get("name").and_then(|v| v.as_str())?;
                    let messages = i
                        .get("messages")
                        .and_then(|value| value.as_array())
                        .cloned()
                        .unwrap_or_default();
                    let latest_message = status_io_latest_message(&messages);
                    // Use components_affected (service names) not containers_affected (infra)
                    let affected_components = status_io_collect_names(i.get("components_affected"));
                    Some(ActiveIncident {
                        name: name.to_string(),
                        status: latest_message
                            .and_then(status_io_message_state)
                            .unwrap_or_else(|| "reported".to_string()),
                        impact: latest_message
                            .and_then(|message| {
                                message.get("status").and_then(|value| {
                                    value.as_u64().map(status_io_code_to_string).or_else(|| {
                                        value.as_str().map(status_io_code_or_label_to_status)
                                    })
                                })
                            })
                            .unwrap_or_default(),
                        shortlink: None,
                        created_at: i.get("datetime_open").and_then(status_io_datetime),
                        updated_at: latest_message
                            .and_then(|message| message.get("datetime"))
                            .and_then(status_io_datetime),
                        latest_update: latest_message.map(|message| IncidentUpdate {
                            status: status_io_message_state(message)
                                .unwrap_or_else(|| "reported".to_string()),
                            body: message
                                .get("details")
                                .and_then(|value| value.as_str())
                                .unwrap_or_default()
                                .to_string(),
                            created_at: message
                                .get("datetime")
                                .and_then(status_io_datetime)
                                .unwrap_or_default(),
                        }),
                        affected_components,
                    })
                })
                .collect()
        })
        .unwrap_or_default();

    let mut maintenance: Vec<ScheduledMaintenance> = Vec::new();
    if let Some(maint) = result.get("maintenance") {
        for key in &["active", "upcoming"] {
            if let Some(arr) = maint.get(*key).and_then(|v| v.as_array()) {
                for m in arr {
                    if let Some(name) = m.get("name").and_then(|v| v.as_str()) {
                        let messages = m
                            .get("messages")
                            .and_then(|value| value.as_array())
                            .cloned()
                            .unwrap_or_default();
                        let latest_message = status_io_latest_message(&messages);
                        let affected_components =
                            status_io_collect_names(m.get("components_affected"));
                        maintenance.push(ScheduledMaintenance {
                            name: name.to_string(),
                            status: latest_message
                                .and_then(status_io_message_state)
                                .unwrap_or_else(|| (*key).to_string()),
                            impact: latest_message
                                .and_then(|message| {
                                    message.get("status").and_then(|value| {
                                        value.as_u64().map(status_io_code_to_string).or_else(|| {
                                            value.as_str().map(status_io_code_or_label_to_status)
                                        })
                                    })
                                })
                                .unwrap_or_default(),
                            shortlink: None,
                            scheduled_for: m
                                .get("datetime_planned_start")
                                .and_then(status_io_datetime)
                                .or_else(|| m.get("datetime_open").and_then(status_io_datetime)),
                            scheduled_until: m
                                .get("datetime_planned_end")
                                .and_then(status_io_datetime),
                            affected_components,
                        });
                    }
                }
            }
        }
    }

    let summary = incidents
        .first()
        .map(|i| i.name.clone())
        .or_else(|| {
            result
                .pointer("/status_overall/status")
                .and_then(|value| value.as_str())
                .map(str::to_string)
        })
        .or_else(|| Some(status_io_code_to_string(overall_code)));
    let components_state = available_detail_state(&components, StatusDetailSource::Inline);
    let incidents_state = available_detail_state(&incidents, StatusDetailSource::Inline);
    let maintenance_state = available_detail_state(&maintenance, StatusDetailSource::Inline);

    Ok(OfficialSnapshot {
        label: source.label().to_string(),
        method: StatusSourceMethod::StatusIo,
        health,
        official_url: source.page_url().to_string(),
        source_updated_at: result
            .pointer("/status_overall/updated")
            .and_then(|v| v.as_str())
            .map(str::to_string),
        provider_summary: summary,
        status_note: None,
        components_state,
        components,
        incidents_state,
        incidents,
        maintenance_state,
        maintenance,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_status_io_response() {
        let json = r#"{
            "result": {
                "status_overall": {
                    "status_code": 300,
                    "status": "Minor Service Outage",
                    "updated": "2026-03-17T00:05:00Z"
                },
                "status": [
                    {
                        "id": "g1",
                        "name": "Infrastructure",
                        "containers": [
                            {"name": "Web", "status_code": 100},
                            {"name": "API", "status_code": 300}
                        ]
                    }
                ],
                "incidents": [
                    {
                        "name": "API degradation",
                        "datetime_open": "2026-03-16T23:00:00Z",
                        "components_affected": [{"name": "API"}],
                        "containers_affected": [{"name": "Web"}],
                        "messages": [
                            {
                                "state": 200,
                                "status": 300,
                                "details": "API latency is elevated in one region.",
                                "datetime": "2026-03-17T00:04:00Z"
                            }
                        ]
                    }
                ],
                "maintenance": {
                    "active": [{
                        "name": "DB migration",
                        "datetime_open": "2026-03-16T22:00:00Z",
                        "datetime_planned_start": "2026-03-16T22:00:00Z",
                        "datetime_planned_end": "2026-03-17T01:00:00Z",
                        "components_affected": [{"name": "API"}],
                        "messages": [{
                            "state": 200,
                            "status": 200,
                            "details": "Database migration in progress.",
                            "datetime": "2026-03-16T22:10:00Z"
                        }]
                    }],
                    "upcoming": [{"name": "Network upgrade"}]
                }
            }
        }"#;

        let snapshot = parse_status_io(OfficialStatusSource::GitLab, json).expect("parses ok");
        assert_eq!(snapshot.method, StatusSourceMethod::StatusIo);
        assert_eq!(snapshot.health, ProviderHealth::Degraded);
        assert_eq!(snapshot.components.len(), 1);
        assert_eq!(snapshot.components[0].name, "Infrastructure");
        assert_eq!(snapshot.components[0].status, "degraded_performance");
        assert_eq!(snapshot.incidents.len(), 1);
        assert_eq!(snapshot.incidents[0].name, "API degradation");
        assert_eq!(snapshot.incidents[0].status, "identified");
        assert_eq!(snapshot.incidents[0].impact, "degraded_performance");
        assert_eq!(
            snapshot.incidents[0].affected_components,
            vec!["API".to_string()]
        );
        assert_eq!(
            snapshot.incidents[0].updated_at.as_deref(),
            Some("2026-03-17T00:04:00Z")
        );
        assert_eq!(
            snapshot.incidents[0]
                .latest_update
                .as_ref()
                .map(|update| update.body.as_str()),
            Some("API latency is elevated in one region.")
        );
        assert_eq!(snapshot.maintenance.len(), 2);
        assert_eq!(snapshot.maintenance[0].name, "DB migration");
        assert_eq!(snapshot.maintenance[0].status, "identified");
        assert_eq!(snapshot.maintenance[0].impact, "under_maintenance");
        assert_eq!(
            snapshot.maintenance[0].scheduled_until.as_deref(),
            Some("2026-03-17T01:00:00Z")
        );
        assert_eq!(
            snapshot.provider_summary.as_deref(),
            Some("API degradation")
        );
        assert_eq!(snapshot.maintenance[1].name, "Network upgrade");
        assert_eq!(snapshot.maintenance[1].status, "upcoming");
    }

    #[test]
    fn status_io_code_mappings_cover_maintenance_and_security() {
        assert_eq!(status_io_code_to_string(200), "under_maintenance");
        assert_eq!(status_io_code_to_health(200), ProviderHealth::Maintenance);
        assert_eq!(status_io_code_to_string(600), "security_event");
        assert_eq!(status_io_code_to_health(600), ProviderHealth::Unknown);
    }
}