agent-supplements-rec 0.1.0

Curated supplement recommendation engine for longevity biomarker optimization
use std::collections::HashMap;

use crate::catalog;
use crate::types::*;

/// Pattern name → recommended product IDs
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"]),
];

/// Generate supplement recommendations from a labassess assessment.
pub fn generate_recommendations(
    assessment: &LabassessOutput,
    catalog_file: &CatalogFile,
    min_evidence: &str,
    limit: usize,
) -> RecommendOutput {
    let min_ev = evidence_to_numeric(min_evidence);

    // Step 1: Build map of flagged biomarkers
    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();

    // Keyed by product ID → best Recommendation so far
    let mut recs: HashMap<String, Recommendation> = HashMap::new();

    // Step 2: Process red flags (priority = "red_flag")
    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);
        }
    }

    // Step 3: Process patterns (priority = "pattern")
    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,
                );
            }
        }
    }

    // Step 4: Process suboptimal biomarkers (priority = "suboptimal")
    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,
            );
        }
    }

    // Step 5: Deduplicate, sort, and truncate
    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);

    // Step 6: Check interactions among final set
    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,
    }
}