use std::collections::HashMap;
use crate::catalog;
use crate::types::{
DeviceCatalogFile, DeviceRecommendOutput, DeviceRecommendation, LabassessOutput, PatientInfo,
};
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![],
}
}
fn red_flag_to_categories(marker: &str, urgency: &str) -> Vec<&'static str> {
let marker_lower = marker.to_lowercase();
if marker_lower.contains("blood_pressure")
|| marker_lower.contains("systolic")
|| marker_lower.contains("diastolic")
{
return vec!["blood_pressure"];
}
if marker_lower.contains("glucose")
|| marker_lower.contains("hba1c")
|| marker_lower.contains("fasting_glucose")
{
return vec!["cgm"];
}
if marker_lower.contains("hemoglobin")
|| marker_lower.contains("ferritin")
|| marker_lower.contains("spo2")
{
return vec!["pulse_oximeter"];
}
if urgency == "same_day" {
return vec!["blood_pressure", "wearable"];
}
vec![]
}
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![],
}
}
fn priority_weight(priority: &str) -> f64 {
match priority {
"red_flag_monitor" => 100.0,
"pattern_monitor" => 80.0,
"optimization" => 40.0,
_ => 20.0,
}
}
fn confidence_to_numeric(confidence: &str) -> f64 {
match confidence {
"high" => 3.0,
"moderate" => 2.0,
"low" => 1.0,
_ => 1.0,
}
}
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,
}
}
pub fn generate_recommendations(
assessment: &LabassessOutput,
catalog: &DeviceCatalogFile,
max_price: Option<u32>,
limit: usize,
) -> DeviceRecommendOutput {
let mut candidates: HashMap<String, (String, f64, Vec<String>, Vec<String>, String)> =
HashMap::new();
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());
}
}
}
}
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(),
)
});
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());
}
}
}
}
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(),
)
});
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());
}
}
}
}
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)
});
}
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();
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,
}
}