use std::collections::HashMap;
use crate::catalog;
use crate::types::*;
const PATTERN_SUPPLEMENTS: &[(&str, &[&str])] = &[
(
"insulin_resistance_cascade",
&[
"berberine-500",
"chromium-picolinate",
"magnesium-glycinate",
"omega3-epa-dha",
],
),
(
"chronic_inflammation",
&["curcumin-meriva", "omega3-epa-dha", "vitamin-d3-5000"],
),
(
"b12_folate_macrocytosis",
&["vitamin-b12-methyl", "methylfolate-1000"],
),
("iron_deficiency", &["iron-bisglycinate"]),
(
"early_dysglycemia",
&[
"berberine-500",
"chromium-picolinate",
"magnesium-glycinate",
],
),
("cv_risk_enhancement", &["omega3-epa-dha"]),
];
pub fn generate_recommendations(
assessment: &LabassessOutput,
catalog_file: &CatalogFile,
min_evidence: &str,
limit: usize,
) -> RecommendOutput {
let min_ev = evidence_to_numeric(min_evidence);
let flagged: HashMap<String, (&ScoredBiomarker, f64, String)> = assessment
.data
.scored_biomarkers
.iter()
.filter(|b| b.status != "optimal" && b.status != "unknown" && b.status != "good")
.map(|b| {
(
b.standardized_name.clone(),
(b, b.value, b.unit.clone()),
)
})
.collect();
let mut recs: HashMap<String, Recommendation> = HashMap::new();
for rf in &assessment.data.red_flags {
let products = catalog::products_for_biomarker(catalog_file, &rf.marker);
for product in products {
if !meets_evidence_threshold(product, min_ev) {
continue;
}
let score = 100.0 + urgency_rank(&rf.urgency);
let evidence = best_evidence(product);
let targets = build_targets_for_product(product, &flagged);
let rationale = format!(
"{} addresses red flag: {} (value: {:.1} {}, urgency: {}). {}",
product.name, rf.marker, rf.value, rf.unit, rf.urgency, rf.action
);
insert_if_higher(&mut recs, product, "red_flag", score, &evidence, &rationale, targets);
}
}
for pattern in &assessment.data.patterns_detected {
let product_ids = pattern_to_products(&pattern.name);
let conf_rank = confidence_rank(&pattern.confidence);
for pid in product_ids {
if let Some(product) = catalog::find_product(catalog_file, pid) {
if !meets_evidence_threshold(product, min_ev) {
continue;
}
let score = 80.0 + conf_rank;
let evidence = best_evidence(product);
let targets = build_targets_for_product(product, &flagged);
let rationale = format!(
"{} recommended for pattern '{}' (confidence: {}). {}",
product.name, pattern.name, pattern.confidence, pattern.action
);
insert_if_higher(
&mut recs, product, "pattern", score, &evidence, &rationale, targets,
);
}
}
}
for (bio_name, (scored, _value, _unit)) in &flagged {
let products = catalog::products_for_biomarker(catalog_file, bio_name);
for product in products {
if !meets_evidence_threshold(product, min_ev) {
continue;
}
let sev = severity_to_numeric(&scored.severity);
let ev = best_evidence_numeric(product);
let score = sev * ev;
let evidence = best_evidence(product);
let targets = build_targets_for_product(product, &flagged);
let rationale = format!(
"{} targets {} {} (current: {:.1} {}, status: {}).",
product.name,
scored.status,
bio_name,
scored.value,
scored.unit,
scored.status,
);
insert_if_higher(
&mut recs, product, "suboptimal", score, &evidence, &rationale, targets,
);
}
}
let mut sorted: Vec<Recommendation> = recs.into_values().collect();
sorted.sort_by(|a, b| {
let pa = priority_rank(&a.priority);
let pb = priority_rank(&b.priority);
pb.partial_cmp(&pa)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
})
});
sorted.truncate(limit);
let product_ids: Vec<&str> = sorted.iter().map(|r| r.product.id.as_str()).collect();
let interactions = catalog::find_interactions(catalog_file, &product_ids);
let warnings: Vec<InteractionWarning> = interactions
.into_iter()
.map(|i| InteractionWarning {
products: vec![i.product_a.clone(), i.product_b.clone()],
severity: i.severity.clone(),
description: i.description.clone(),
})
.collect();
let total = sorted.len();
RecommendOutput {
patient: assessment.data.patient.clone(),
assessment_summary: assessment.data.summary.clone(),
recommendations: sorted,
interaction_warnings: warnings,
total_recommendations: total,
}
}
fn insert_if_higher(
recs: &mut HashMap<String, Recommendation>,
product: &Product,
priority: &str,
score: f64,
evidence: &str,
rationale: &str,
targets: Vec<TargetedBiomarker>,
) {
let existing = recs.get(&product.id);
let dominated = match existing {
None => true,
Some(ex) => {
let new_pr = priority_rank(priority);
let old_pr = priority_rank(&ex.priority);
new_pr > old_pr || (new_pr == old_pr && score > ex.score)
}
};
if dominated {
recs.insert(
product.id.clone(),
Recommendation {
priority: priority.to_string(),
product: product.clone(),
rationale: rationale.to_string(),
target_biomarkers: targets,
evidence_level: evidence.to_string(),
score,
},
);
}
}
fn build_targets_for_product(
product: &Product,
flagged: &HashMap<String, (&ScoredBiomarker, f64, String)>,
) -> Vec<TargetedBiomarker> {
product
.targets
.iter()
.filter_map(|t| {
flagged.get(&t.biomarker).map(|(scored, value, unit)| {
TargetedBiomarker {
biomarker: t.biomarker.clone(),
current_value: *value,
current_unit: unit.clone(),
current_status: scored.status.clone(),
expected_direction: t.direction.clone(),
}
})
})
.collect()
}
fn pattern_to_products(pattern_name: &str) -> Vec<&'static str> {
for (name, ids) in PATTERN_SUPPLEMENTS {
if *name == pattern_name {
return ids.to_vec();
}
}
Vec::new()
}
fn priority_rank(priority: &str) -> f64 {
match priority {
"red_flag" => 3.0,
"pattern" => 2.0,
"suboptimal" => 1.0,
_ => 0.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,
_ => 0.0,
}
}
fn evidence_to_numeric(evidence: &str) -> f64 {
match evidence {
"strong" => 3.0,
"moderate" => 2.0,
"emerging" => 1.0,
_ => 0.0,
}
}
fn best_evidence_numeric(product: &Product) -> f64 {
product
.targets
.iter()
.map(|t| evidence_to_numeric(&t.evidence))
.fold(0.0_f64, f64::max)
}
fn best_evidence(product: &Product) -> String {
let max = best_evidence_numeric(product);
if max >= 3.0 {
"strong".to_string()
} else if max >= 2.0 {
"moderate".to_string()
} else {
"emerging".to_string()
}
}
fn meets_evidence_threshold(product: &Product, min_ev: f64) -> bool {
best_evidence_numeric(product) >= min_ev
}
fn urgency_rank(urgency: &str) -> f64 {
match urgency {
"same_day" => 3.0,
"72_hour" => 2.0,
"routine" => 1.0,
_ => 0.0,
}
}
fn confidence_rank(confidence: &str) -> f64 {
match confidence {
"high" => 3.0,
"moderate" => 2.0,
"low" => 1.0,
_ => 0.0,
}
}