labscript 0.1.0

Prescription PDF generator with e-signature and QR verification
mod cli;
mod errors;
mod output;
mod pdf;
mod qr;
mod types;
mod verify;

use clap::Parser;

use cli::{Cli, Commands};
use errors::LabscriptError;
use types::{
    GenerateResult, Medication, PatientInfo, Prescription, PrescriptionInput, PrescriberInfo,
};

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<(), LabscriptError> {
    match &cli.command {
        Commands::AgentInfo => {
            print_agent_info();
            Ok(())
        }

        Commands::Verify { qr_string } => {
            let result = verify::verify_qr(qr_string)?;
            if use_json {
                output::json::print_success(&result);
            } else {
                output::table::render_verify_result(&result);
            }
            Ok(())
        }

        Commands::Generate {
            patient_name,
            patient_dob,
            patient_address,
            prescriber_name,
            prescriber_credentials,
            prescriber_license,
            prescriber_address,
            drug,
            strength,
            form,
            quantity,
            sig,
            refills,
            diagnosis,
            notes,
            date,
            signature,
            output: output_path,
            file,
            stdin,
        } => {
            let input = if *stdin {
                // Read from stdin
                let raw = std::io::read_to_string(std::io::stdin())?;
                serde_json::from_str::<PrescriptionInput>(&raw)?
            } else if let Some(file_path) = file {
                // Read from file
                let raw = std::fs::read_to_string(file_path)?;
                serde_json::from_str::<PrescriptionInput>(&raw)?
            } else {
                // Build from CLI args
                let p_name = patient_name.as_ref().ok_or_else(|| {
                    LabscriptError::Config("--patient-name is required".into())
                })?;
                let p_dob = patient_dob.as_ref().ok_or_else(|| {
                    LabscriptError::Config("--patient-dob is required".into())
                })?;
                let pr_name = prescriber_name.as_ref().ok_or_else(|| {
                    LabscriptError::Config("--prescriber-name is required".into())
                })?;
                let d = drug.as_ref().ok_or_else(|| {
                    LabscriptError::Config("--drug is required".into())
                })?;
                let s = strength.as_ref().ok_or_else(|| {
                    LabscriptError::Config("--strength is required".into())
                })?;
                let q = quantity.ok_or_else(|| {
                    LabscriptError::Config("--quantity is required".into())
                })?;
                let directions = sig.as_ref().ok_or_else(|| {
                    LabscriptError::Config("--sig is required".into())
                })?;

                PrescriptionInput {
                    patient: PatientInfo {
                        name: p_name.clone(),
                        dob: p_dob.clone(),
                        address: patient_address.clone(),
                    },
                    prescriber: PrescriberInfo {
                        name: pr_name.clone(),
                        credentials: prescriber_credentials.clone(),
                        license: prescriber_license.clone(),
                        address: prescriber_address.clone(),
                    },
                    medications: vec![Medication {
                        drug: d.clone(),
                        strength: s.clone(),
                        form: form.clone(),
                        quantity: q,
                        sig: directions.clone(),
                        refills: *refills,
                    }],
                    diagnosis: diagnosis.clone(),
                    notes: notes.clone(),
                    date: date.clone(),
                }
            };

            // Validate input
            if input.medications.is_empty() {
                return Err(LabscriptError::InvalidInput(
                    "At least one medication is required".into(),
                ));
            }

            // Resolve date
            let rx_date = input
                .date
                .unwrap_or_else(|| chrono::Local::now().format("%Y-%m-%d").to_string());

            // Build prescription
            let rx_id = uuid::Uuid::new_v4().to_string();
            let generated_at = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);

            let mut rx = Prescription {
                id: rx_id.clone(),
                patient: input.patient,
                prescriber: input.prescriber,
                medications: input.medications,
                diagnosis: input.diagnosis,
                notes: input.notes,
                date: rx_date,
                verification_hash: String::new(),
                qr_payload: String::new(),
                generated_at: generated_at.clone(),
            };

            // Compute hash and QR
            let hash = qr::compute_hash(&rx);
            let qr_payload = qr::build_qr_payload(&rx_id, &hash, &generated_at);
            rx.verification_hash = hash.clone();
            rx.qr_payload = qr_payload;

            // Generate PDF
            pdf::generate(&rx, signature.as_deref(), output_path)?;

            // Output result
            let result = GenerateResult {
                prescription_id: rx_id,
                output_file: output_path.clone(),
                verification_hash: hash,
                patient_name: rx.patient.name.clone(),
                prescriber_name: rx.prescriber.name.clone(),
                medication_count: rx.medications.len(),
                generated_at,
            };

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

            Ok(())
        }
    }
}

fn print_agent_info() {
    let info = serde_json::json!({
        "name": "labscript",
        "version": env!("CARGO_PKG_VERSION"),
        "description": "Prescription PDF generator with e-signature and QR verification",
        "capabilities": [
            "prescription_pdf_generation",
            "qr_code_verification",
            "e_signature_embedding",
            "multi_medication_support",
            "json_input",
            "cli_input"
        ],
        "input_formats": ["cli-args", "json-file", "json-stdin"],
        "output_formats": ["json", "table"],
        "commands": {
            "generate": "labscript generate --patient-name 'John Doe' --patient-dob 1990-01-01 --prescriber-name 'Dr. Smith' --drug Metformin --strength 500mg --quantity 60 --sig 'Take 1 tablet twice daily' --output rx.pdf",
            "generate_json": "cat prescription.json | labscript generate --stdin --output rx.pdf",
            "verify": "labscript verify 'labscript:v1:uuid:hash:timestamp'",
            "agent-info": "labscript agent-info"
        },
        "pipeline": "labparse -> labstore -> labassess -> labscript"
    });
    println!("{}", serde_json::to_string_pretty(&info).unwrap());
}