agent-supplements-rec 0.1.0

Curated supplement recommendation engine for longevity biomarker optimization
mod catalog;
mod cli;
mod db;
mod errors;
mod interactions;
mod list;
mod output;
mod recommend;
mod search;
mod show;
mod types;

use clap::Parser;

use cli::{Cli, Commands};
use errors::SupplementsError;
use types::LabassessOutput;

fn main() {
    let cli = Cli::parse();
    let use_json = cli.json || !output::is_tty();

    match run(&cli, use_json) {
        Ok(()) => {}
        Err(e) => {
            if use_json {
                output::json::print_error(&e.to_string());
            } else {
                eprintln!("error: {e}");
            }
            e.exit();
        }
    }
}

fn run(cli: &Cli, use_json: bool) -> Result<(), SupplementsError> {
    if matches!(cli.command, Commands::AgentInfo) {
        print_agent_info();
        return Ok(());
    }

    let catalog_file = catalog::load_catalog();

    match &cli.command {
        Commands::Recommend {
            slug,
            stdin,
            sex,
            age,
            category: _category,
            limit,
            min_evidence,
        } => {
            let assessment = if *stdin {
                read_assessment_stdin()?
            } else if let Some(s) = slug {
                load_assessment_from_db(cli, s, sex.as_deref(), *age)?
            } else {
                return Err(SupplementsError::NoInput);
            };

            let result = recommend::generate_recommendations(
                &assessment,
                &catalog_file,
                min_evidence,
                *limit,
            );

            if use_json {
                output::json::print_success(&result);
            } else {
                output::table::render_recommendations(&result);
            }
        }

        Commands::List {
            category,
            brand,
            biomarker,
            limit,
        } => {
            let products = list::filter_products(
                &catalog_file,
                category.as_deref(),
                brand.as_deref(),
                biomarker.as_deref(),
                *limit,
            );

            if use_json {
                let refs: Vec<_> = products.iter().copied().collect();
                output::json::print_success(&refs);
            } else {
                output::table::render_product_list(&products);
            }
        }

        Commands::Show { product_id } => {
            let (product, brand_name) = show::get_product_detail(&catalog_file, product_id)?;

            if use_json {
                output::json::print_success(product);
            } else {
                output::table::render_product_detail(product, &brand_name);
            }
        }

        Commands::Search { query, limit } => {
            let products = search::search(&catalog_file, query, *limit);

            if use_json {
                let refs: Vec<_> = products.iter().copied().collect();
                output::json::print_success(&refs);
            } else {
                output::table::render_product_list(&products);
            }
        }

        Commands::Brands => {
            let brands: Vec<&types::Brand> = catalog_file.brand.iter().collect();

            if use_json {
                output::json::print_success(&brands);
            } else {
                output::table::render_brands(&brands);
            }
        }

        Commands::Interactions { product_ids } => {
            let found = interactions::check_interactions(&catalog_file, product_ids)?;

            if use_json {
                output::json::print_success(&found);
            } else {
                output::table::render_interactions(&found);
            }
        }

        Commands::AgentInfo => unreachable!(),
    }

    Ok(())
}

fn read_assessment_stdin() -> Result<LabassessOutput, SupplementsError> {
    let input = std::io::read_to_string(std::io::stdin())?;
    let assessment: LabassessOutput = serde_json::from_str(&input)?;
    Ok(assessment)
}

fn load_assessment_from_db(
    cli: &Cli,
    slug: &str,
    _sex: Option<&str>,
    _age: Option<u8>,
) -> Result<LabassessOutput, SupplementsError> {
    let db_path = db::resolve_db_path(cli.db.as_deref())?;
    let conn = db::open(&db_path)?;
    let (patient_id, _name, sex, _dob) = db::get_patient(&conn, slug)?;
    let biomarkers = db::load_latest_biomarkers(&conn, patient_id)?;

    // Build a minimal labassess-compatible structure from raw biomarkers.
    // For full pattern/red-flag detection, users should pipe through labassess.
    let scored: Vec<types::ScoredBiomarker> = biomarkers
        .into_iter()
        .map(|(name, value, unit)| types::ScoredBiomarker {
            name: name.clone(),
            standardized_name: name,
            value,
            unit,
            status: "unknown".to_string(),
            severity: "none".to_string(),
        })
        .collect();

    let total = scored.len();

    Ok(LabassessOutput {
        version: "1".to_string(),
        status: "success".to_string(),
        data: types::LabassessData {
            mode: "db_minimal".to_string(),
            patient: types::PatientInfo {
                sex: Some(sex),
                age: None,
            },
            scored_biomarkers: scored,
            derived_values: vec![],
            patterns_detected: vec![],
            red_flags: vec![],
            summary: types::Summary {
                total_markers: total,
                scored: 0,
                unknown: total,
                optimal: 0,
                good: 0,
                attention: 0,
                marginal: 0,
                concern: 0,
            },
        },
    })
}

fn print_agent_info() {
    let catalog_file = catalog::load_catalog();
    let product_count = catalog_file.product.len();
    let brand_count = catalog_file.brand.len();
    let categories: Vec<String> = {
        let mut cats: Vec<String> = catalog_file
            .product
            .iter()
            .map(|p| p.category.clone())
            .collect();
        cats.sort();
        cats.dedup();
        cats
    };
    let biomarkers: Vec<String> = {
        let mut bios: Vec<String> = catalog_file
            .product
            .iter()
            .flat_map(|p| p.targets.iter().map(|t| t.biomarker.clone()))
            .collect();
        bios.sort();
        bios.dedup();
        bios
    };

    let info = serde_json::json!({
        "name": "agent-supplements-rec",
        "version": env!("CARGO_PKG_VERSION"),
        "description": "Curated supplement recommendation engine for longevity biomarker optimization",
        "capabilities": [
            "supplement_recommendation",
            "catalog_browsing",
            "interaction_checking",
            "biomarker_targeting",
            "evidence_filtering"
        ],
        "catalog": {
            "products": product_count,
            "brands": brand_count,
            "categories": categories,
            "targeted_biomarkers": biomarkers,
        },
        "input_modes": [
            "stdin (pipe labassess JSON)",
            "db (patient slug → labstore)"
        ],
        "db_path": "~/.labstore/labstore.db",
        "commands": {
            "recommend_stdin": "labassess ... --json | agent-supplements-rec recommend --stdin",
            "recommend_slug": "agent-supplements-rec recommend <patient-slug>",
            "list": "agent-supplements-rec list [--category vitamin] [--brand thorne]",
            "show": "agent-supplements-rec show <product-id>",
            "search": "agent-supplements-rec search <query>",
            "brands": "agent-supplements-rec brands",
            "interactions": "agent-supplements-rec interactions <id1> <id2> [...]",
        },
        "pipeline": "labparse -> labstore -> labassess -> agent-supplements-rec"
    });
    println!("{}", serde_json::to_string_pretty(&info).unwrap());
}