labscript 0.1.0

Prescription PDF generator with e-signature and QR verification
use genpdf::elements::Paragraph;
use genpdf::fonts::{FontData, FontFamily};
use genpdf::style;
use genpdf::{Document, Element};

use crate::errors::LabscriptError;
use crate::qr;
use crate::types::Prescription;

/// Font file search paths: (directory, regular, bold, italic, bold-italic)
const FONT_SEARCH: &[(&str, &str, &str, &str, &str)] = &[
    // Linux — Liberation Sans
    (
        "/usr/share/fonts/truetype/liberation",
        "LiberationSans-Regular.ttf",
        "LiberationSans-Bold.ttf",
        "LiberationSans-Italic.ttf",
        "LiberationSans-BoldItalic.ttf",
    ),
    (
        "/usr/share/fonts/liberation-sans",
        "LiberationSans-Regular.ttf",
        "LiberationSans-Bold.ttf",
        "LiberationSans-Italic.ttf",
        "LiberationSans-BoldItalic.ttf",
    ),
    // macOS — Arial
    (
        "/System/Library/Fonts/Supplemental",
        "Arial.ttf",
        "Arial Bold.ttf",
        "Arial Italic.ttf",
        "Arial Bold Italic.ttf",
    ),
    // macOS — Helvetica Neue (TTF collection not usable, but try)
    (
        "/Library/Fonts",
        "Arial Unicode.ttf",
        "Arial Unicode.ttf",
        "Arial Unicode.ttf",
        "Arial Unicode.ttf",
    ),
];

/// Try to load a FontFamily from well-known font paths.
fn load_font_family() -> Result<FontFamily<FontData>, LabscriptError> {
    // First try genpdf's from_files for standard Linux paths
    if let Ok(family) = genpdf::fonts::from_files(
        "/usr/share/fonts/truetype/liberation",
        "LiberationSans",
        None,
    ) {
        return Ok(family);
    }

    // Try each search path, loading files by exact name
    for (dir, regular, bold, italic, bold_italic) in FONT_SEARCH {
        let dir = std::path::Path::new(dir);
        let r_path = dir.join(regular);
        let b_path = dir.join(bold);
        let i_path = dir.join(italic);
        let bi_path = dir.join(bold_italic);

        if r_path.exists() && b_path.exists() && i_path.exists() && bi_path.exists() {
            let load = |path: &std::path::Path| -> Result<FontData, LabscriptError> {
                let data = std::fs::read(path).map_err(|e| {
                    LabscriptError::Pdf(format!("Failed to read font {}: {e}", path.display()))
                })?;
                FontData::new(data, None).map_err(|e| {
                    LabscriptError::Pdf(format!("Failed to parse font {}: {e}", path.display()))
                })
            };
            return Ok(FontFamily {
                regular: load(&r_path)?,
                bold: load(&b_path)?,
                italic: load(&i_path)?,
                bold_italic: load(&bi_path)?,
            });
        }
    }

    Err(LabscriptError::Pdf(
        "Could not find fonts. Install Liberation Sans (Linux) or ensure Arial is available (macOS)."
            .into(),
    ))
}

/// Generate a prescription PDF and write it to the given path.
pub fn generate(
    rx: &Prescription,
    _signature_path: Option<&str>,
    output_path: &str,
) -> Result<(), LabscriptError> {
    let font_family = load_font_family()?;

    let mut doc = Document::new(font_family);
    doc.set_title("Prescription");
    doc.set_minimal_conformance();
    doc.set_paper_size(genpdf::PaperSize::A4);

    let mut decorator = genpdf::SimplePageDecorator::new();
    decorator.set_margins(15);
    doc.set_page_decorator(decorator);

    // ── Header: Prescriber info ──
    doc.push(
        Paragraph::default()
            .styled_string(&rx.prescriber.name, style::Style::new().bold().with_font_size(18)),
    );
    if let Some(ref creds) = rx.prescriber.credentials {
        doc.push(Paragraph::new(creds.as_str()));
    }
    if let Some(ref license) = rx.prescriber.license {
        doc.push(Paragraph::new(format!("License: {license}")));
    }
    if let Some(ref addr) = rx.prescriber.address {
        doc.push(Paragraph::new(addr.as_str()));
    }

    // ── Horizontal line ──
    doc.push(Paragraph::new(
        "------------------------------------------------------------------------",
    ));

    // ── Patient section ──
    doc.push(
        Paragraph::default()
            .styled_string("Patient: ", style::Style::new().bold())
            .string(&rx.patient.name),
    );
    doc.push(Paragraph::new(format!("DOB: {}", rx.patient.dob)));
    if let Some(ref addr) = rx.patient.address {
        doc.push(Paragraph::new(format!("Address: {addr}")));
    }

    // ── Date ──
    doc.push(Paragraph::new(""));
    doc.push(Paragraph::new(format!("Date: {}", rx.date)));

    // ── Rx block ──
    doc.push(Paragraph::new(""));
    doc.push(
        Paragraph::default()
            .styled_string("Rx  ", style::Style::new().bold().with_font_size(16))
            .styled_string("Prescription", style::Style::new().bold().with_font_size(14)),
    );
    doc.push(Paragraph::new(""));

    for (i, med) in rx.medications.iter().enumerate() {
        doc.push(
            Paragraph::default()
                .styled_string(
                    format!("{}. {} {}", i + 1, med.drug, med.strength),
                    style::Style::new().bold(),
                ),
        );
        let mut details = String::new();
        if let Some(ref form) = med.form {
            details.push_str(&format!("Form: {form}   "));
        }
        details.push_str(&format!("Qty: {}   ", med.quantity));
        details.push_str(&format!("Sig: {}", med.sig));
        doc.push(Paragraph::new(details));
        let refills = med.refills.unwrap_or(0);
        doc.push(Paragraph::new(format!("Refills: {refills}")));
        doc.push(Paragraph::new(""));
    }

    // ── Diagnosis ──
    if let Some(ref dx) = rx.diagnosis {
        doc.push(
            Paragraph::default()
                .styled_string("Diagnosis: ", style::Style::new().bold())
                .string(dx.as_str()),
        );
    }

    // ── Notes ──
    if let Some(ref notes) = rx.notes {
        doc.push(
            Paragraph::default()
                .styled_string("Notes: ", style::Style::new().bold())
                .string(notes.as_str()),
        );
    }

    doc.push(Paragraph::new(""));

    // ── Signature ──
    if _signature_path.is_some() {
        doc.push(
            Paragraph::default()
                .styled_string("Signature: ", style::Style::new().bold())
                .string("[e-signature attached]"),
        );
    } else {
        doc.push(Paragraph::new("Signature: _______________"));
    }

    doc.push(Paragraph::new(""));

    // ── QR code (rendered as text block characters) ──
    let qr_lines = qr::render_qr_text(&rx.qr_payload)?;
    doc.push(
        Paragraph::default()
            .styled_string("Verification QR:", style::Style::new().bold()),
    );
    for line in &qr_lines {
        doc.push(
            Paragraph::new(line.as_str())
                .styled(style::Style::new().with_font_size(6)),
        );
    }

    // ── Footer ──
    doc.push(Paragraph::new(""));
    doc.push(Paragraph::new(
        "------------------------------------------------------------------------",
    ));
    doc.push(Paragraph::new(format!("Rx ID: {}", rx.id)));
    doc.push(Paragraph::new(format!(
        "Generated by labscript v{}",
        env!("CARGO_PKG_VERSION")
    )));

    doc.render_to_file(output_path)
        .map_err(|e| LabscriptError::Pdf(format!("Failed to write PDF: {e}")))?;

    Ok(())
}