agent-device-rec 0.1.0

Health device recommendation engine for longevity monitoring
mod catalog;
mod cli;
mod compare;
mod db;
mod errors;
mod list;
mod output;
mod recommend;
mod search;
mod show;
mod types;

use clap::Parser;

use cli::{Cli, Commands};
use errors::DeviceError;
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<(), DeviceError> {
    if matches!(cli.command, Commands::AgentInfo) {
        print_agent_info();
        return Ok(());
    }

    let catalog = catalog::load_catalog();

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

            let mut result =
                recommend::generate_recommendations(&assessment, &catalog, *max_price, *limit);

            // Filter by category if specified
            if let Some(cat) = category {
                let lower = cat.to_lowercase();
                result
                    .recommendations
                    .retain(|r| r.device.category.to_lowercase() == lower);
                result.total_recommendations = result.recommendations.len();
            }

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

        Commands::List {
            category,
            brand,
            tracks,
            price_range,
            limit,
        } => {
            let devices = list::list_devices(
                &catalog,
                category.as_deref(),
                brand.as_deref(),
                tracks.as_deref(),
                price_range.as_deref(),
                *limit,
            );

            if use_json {
                let device_refs: Vec<_> = devices.into_iter().cloned().collect();
                output::json::print_success(&device_refs);
            } else {
                output::table::render_device_list(&devices, &catalog.brand);
            }
        }

        Commands::Show { device_id } => {
            let (device, brand_name) = show::show_device(&catalog, device_id)?;

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

        Commands::Search { query, limit } => {
            let devices = search::search_devices(&catalog, query, *limit);

            if use_json {
                let device_refs: Vec<_> = devices.into_iter().cloned().collect();
                output::json::print_success(&device_refs);
            } else {
                output::table::render_device_list(&devices, &catalog.brand);
            }
        }

        Commands::Brands => {
            if use_json {
                output::json::print_success(&catalog.brand);
            } else {
                output::table::render_brands(&catalog.brand);
            }
        }

        Commands::Compare { device_ids } => {
            let comparison = compare::compare_devices(&catalog, device_ids)?;

            if use_json {
                output::json::print_success(&comparison);
            } else {
                output::table::render_comparison(&comparison);
            }
        }

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

    Ok(())
}

/// Read labassess JSON from stdin.
fn read_assessment_stdin() -> Result<LabassessOutput, DeviceError> {
    let input = std::io::read_to_string(std::io::stdin())?;
    let assessment: LabassessOutput = serde_json::from_str(&input)?;
    Ok(assessment)
}

/// Load a minimal assessment from the labstore DB for a given patient slug.
/// This constructs a simplified LabassessOutput from raw biomarker data.
/// For full pattern/red-flag detection, users should pipe through labassess.
fn load_assessment_from_db(cli: &Cli, slug: &str) -> Result<LabassessOutput, DeviceError> {
    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 assessment — no patterns or red flags without 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: "unknown".to_string(),
        })
        .collect();

    let total = scored.len();

    Ok(LabassessOutput {
        version: "1".to_string(),
        status: "success".to_string(),
        data: types::LabassessData {
            mode: "minimal".to_string(),
            patient: types::PatientInfo {
                sex: Some(sex),
                age: None,
            },
            scored_biomarkers: scored,
            derived_values: Vec::new(),
            patterns_detected: Vec::new(),
            red_flags: Vec::new(),
            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 = catalog::load_catalog();
    let device_count = catalog.device.len();
    let brand_count = catalog.brand.len();
    let categories: Vec<String> = {
        let mut cats: Vec<String> = catalog
            .device
            .iter()
            .map(|d| d.category.clone())
            .collect();
        cats.sort();
        cats.dedup();
        cats
    };

    let info = serde_json::json!({
        "name": "agent-device-rec",
        "version": env!("CARGO_PKG_VERSION"),
        "description": "Health device recommendation engine for longevity monitoring",
        "capabilities": [
            "device_recommendation",
            "device_catalog_browsing",
            "device_comparison",
            "pattern_to_device_mapping",
            "red_flag_monitoring"
        ],
        "catalog": {
            "devices": device_count,
            "brands": brand_count,
            "categories": categories
        },
        "db_path": "~/.labstore/labstore.db",
        "commands": {
            "recommend_stdin": "labassess ... --json | agent-device-rec recommend --stdin",
            "recommend_db": "agent-device-rec recommend <slug>",
            "list": "agent-device-rec list [--category cgm] [--brand withings]",
            "show": "agent-device-rec show <device-id>",
            "search": "agent-device-rec search <query>",
            "brands": "agent-device-rec brands",
            "compare": "agent-device-rec compare <id1> <id2> [<id3>...]"
        },
        "pipeline": "labparse -> labstore -> labassess -> agent-device-rec"
    });
    println!("{}", serde_json::to_string_pretty(&info).unwrap());
}