agent-device-rec 0.1.0

Health device recommendation engine for longevity monitoring
use std::collections::HashMap;

use crate::catalog;
use crate::types::{
    DeviceCatalogFile, DeviceRecommendOutput, DeviceRecommendation, LabassessOutput, PatientInfo,
};

/// Map pattern names to device categories they call for.
fn pattern_to_categories(pattern: &str) -> Vec<&'static str> {
    match pattern {
        "insulin_resistance_cascade" | "early_dysglycemia" => vec!["cgm"],
        "metabolic_syndrome" => vec!["cgm", "scale", "blood_pressure"],
        "chronic_inflammation" => vec!["wearable"],
        "cv_risk_enhancement" => vec!["blood_pressure", "pulse_oximeter"],
        "thyroid_dysfunction" => vec!["wearable"],
        "iron_deficiency" | "anemia_chronic_inflammation" => vec!["pulse_oximeter"],
        _ => vec![],
    }
}

/// Map red flag markers to device categories for urgent monitoring.
fn red_flag_to_categories(marker: &str, urgency: &str) -> Vec<&'static str> {
    let marker_lower = marker.to_lowercase();

    // Blood pressure red flags
    if marker_lower.contains("blood_pressure")
        || marker_lower.contains("systolic")
        || marker_lower.contains("diastolic")
    {
        return vec!["blood_pressure"];
    }

    // Glucose / HbA1c red flags
    if marker_lower.contains("glucose")
        || marker_lower.contains("hba1c")
        || marker_lower.contains("fasting_glucose")
    {
        return vec!["cgm"];
    }

    // SpO2 / anemia flags
    if marker_lower.contains("hemoglobin")
        || marker_lower.contains("ferritin")
        || marker_lower.contains("spo2")
    {
        return vec!["pulse_oximeter"];
    }

    // Same-day urgency for any cardiac marker
    if urgency == "same_day" {
        return vec!["blood_pressure", "wearable"];
    }

    vec![]
}

/// Map suboptimal biomarker names to device categories.
fn biomarker_to_categories(standardized_name: &str) -> Vec<&'static str> {
    match standardized_name {
        "fasting_glucose" | "hba1c" | "fasting_insulin" => vec!["cgm"],
        "blood_pressure" | "systolic_bp" | "diastolic_bp" => vec!["blood_pressure"],
        "heart_rate" | "hrv" => vec!["wearable"],
        "spo2" | "hemoglobin" | "ferritin" => vec!["pulse_oximeter"],
        "weight" | "bmi" | "body_fat_percentage" => vec!["scale"],
        "temperature" => vec!["wearable"],
        _ => vec![],
    }
}

/// Score weight for priority tiers.
fn priority_weight(priority: &str) -> f64 {
    match priority {
        "red_flag_monitor" => 100.0,
        "pattern_monitor" => 80.0,
        "optimization" => 40.0,
        _ => 20.0,
    }
}

/// Convert confidence string to numeric.
fn confidence_to_numeric(confidence: &str) -> f64 {
    match confidence {
        "high" => 3.0,
        "moderate" => 2.0,
        "low" => 1.0,
        _ => 1.0,
    }
}

/// Convert severity string to numeric.
fn severity_to_numeric(severity: &str) -> f64 {
    match severity {
        "high" => 4.0,
        "moderate_high" => 3.0,
        "moderate" => 2.0,
        "low" => 1.0,
        "none" => 0.0,
        _ => 1.0,
    }
}

/// Core recommendation engine.
pub fn generate_recommendations(
    assessment: &LabassessOutput,
    catalog: &DeviceCatalogFile,
    max_price: Option<u32>,
    limit: usize,
) -> DeviceRecommendOutput {
    // device_id → (priority, score, relevant_patterns, relevant_biomarkers, rationale)
    let mut candidates: HashMap<String, (String, f64, Vec<String>, Vec<String>, String)> =
        HashMap::new();

    // 1. Process red flags (highest priority)
    for rf in &assessment.data.red_flags {
        let categories = red_flag_to_categories(&rf.marker, &rf.urgency);
        for cat in &categories {
            let devices = catalog::devices_by_category(catalog, cat);
            for device in devices {
                let score = priority_weight("red_flag_monitor") + 10.0;
                let rationale = format!(
                    "Red flag: {} = {} {} (urgency: {}). {} monitoring recommended.",
                    rf.marker, rf.value, rf.unit, rf.urgency, cat
                );
                let entry = candidates.entry(device.id.clone()).or_insert_with(|| {
                    (
                        "red_flag_monitor".to_string(),
                        0.0,
                        Vec::new(),
                        Vec::new(),
                        String::new(),
                    )
                });
                if score > entry.1 {
                    entry.0 = "red_flag_monitor".to_string();
                    entry.1 = score;
                    entry.4 = rationale;
                }
                if !entry.3.contains(&rf.marker) {
                    entry.3.push(rf.marker.clone());
                }
            }
        }
    }

    // 2. Process patterns
    for pattern in &assessment.data.patterns_detected {
        let categories = pattern_to_categories(&pattern.name);
        let conf_score = confidence_to_numeric(&pattern.confidence);

        for cat in &categories {
            let devices = catalog::devices_by_category(catalog, cat);
            for device in devices {
                let score = priority_weight("pattern_monitor") + conf_score;
                let rationale = format!(
                    "Pattern '{}' detected (confidence: {}). {} monitoring helps track this condition.",
                    pattern.name, pattern.confidence, cat
                );
                let entry = candidates.entry(device.id.clone()).or_insert_with(|| {
                    (
                        "pattern_monitor".to_string(),
                        0.0,
                        Vec::new(),
                        Vec::new(),
                        String::new(),
                    )
                });
                // Only upgrade if not already red_flag_monitor
                if entry.0 != "red_flag_monitor" && score > entry.1 {
                    entry.0 = "pattern_monitor".to_string();
                    entry.1 = score;
                    entry.4 = rationale;
                }
                if !entry.2.contains(&pattern.name) {
                    entry.2.push(pattern.name.clone());
                }
            }
        }
    }

    // 3. Process suboptimal biomarkers
    for bm in &assessment.data.scored_biomarkers {
        if bm.status == "optimal" || bm.status == "good" || bm.status == "unknown" {
            continue;
        }

        let categories = biomarker_to_categories(&bm.standardized_name);
        let sev_score = severity_to_numeric(&bm.severity);

        for cat in &categories {
            let devices = catalog::devices_by_category(catalog, cat);
            for device in devices {
                let score = priority_weight("optimization") + sev_score;
                let rationale = format!(
                    "{} is {} (value: {} {}, severity: {}). {} device helps monitor progress.",
                    bm.name, bm.status, bm.value, bm.unit, bm.severity, cat
                );
                let entry = candidates.entry(device.id.clone()).or_insert_with(|| {
                    (
                        "optimization".to_string(),
                        0.0,
                        Vec::new(),
                        Vec::new(),
                        String::new(),
                    )
                });
                // Only upgrade if not already higher priority
                if entry.0 != "red_flag_monitor"
                    && entry.0 != "pattern_monitor"
                    && score > entry.1
                {
                    entry.0 = "optimization".to_string();
                    entry.1 = score;
                    entry.4 = rationale;
                }
                if !entry.3.contains(&bm.standardized_name) {
                    entry.3.push(bm.standardized_name.clone());
                }
            }
        }
    }

    // 4. Filter by max_price if set
    if let Some(max) = max_price {
        candidates.retain(|id, _| {
            catalog::find_device(catalog, id)
                .map(|d| d.price_usd.map(|p| p <= max).unwrap_or(true))
                .unwrap_or(false)
        });
    }

    // 5. Build sorted recommendations
    let mut recs: Vec<DeviceRecommendation> = candidates
        .into_iter()
        .filter_map(|(id, (priority, score, patterns, biomarkers, rationale))| {
            catalog::find_device(catalog, &id).map(|device| DeviceRecommendation {
                priority,
                device: device.clone(),
                rationale,
                relevant_patterns: patterns,
                relevant_biomarkers: biomarkers,
                score,
            })
        })
        .collect();

    // Sort: red_flag_monitor > pattern_monitor > optimization, then by score desc
    recs.sort_by(|a, b| {
        let pa = priority_weight(&a.priority);
        let pb = priority_weight(&b.priority);
        pb.partial_cmp(&pa)
            .unwrap_or(std::cmp::Ordering::Equal)
            .then(b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal))
    });

    recs.truncate(limit);

    let total = recs.len();

    DeviceRecommendOutput {
        patient: PatientInfo {
            sex: assessment.data.patient.sex.clone(),
            age: assessment.data.patient.age,
        },
        recommendations: recs,
        total_recommendations: total,
    }
}