//! Shared utilities for displaying a printer's SGD-queried
//! resolution + calibrated label dimensions across the Printers
//! and Test Prints pages.
//!
//! The actual SGD round-trips live in `crate::dbus::AgentClient`
//! (`get_printer_dpi` + `get_media_size`). This module only owns the
//! struct that bundles the three values together and the
//! human-readable formatter that turns them into a one-line subtitle.

use crate::dbus::{AgentClient, PrinterEntry};

/// Bundle of SGD-queried printer metadata. Zero values represent
/// "unknown" — every formatter degrades gracefully when one or both
/// of the dim fields is 0.
#[derive(Debug, Clone, Default)]
pub struct PrinterDims {
    /// Resolution in dpi. 0 if SGD didn't respond.
    pub dpi: u32,
    /// PHYSICAL printhead width in dots — hardware constant per
    /// model, parsed from `^HH`'s RESOLUTION line. 0 if unknown
    /// (older ZD220/230 firmware mostly answers; new agents return
    /// this via GetPrinterDimensions).
    pub phys_head_dots: u32,
    /// STORED PRINT WIDTH in dots — the user-settable `^PW` value
    /// the printer is currently using. Caps the printable area at
    /// or below `phys_head_dots`. A non-zero value LESS than
    /// `phys_head_dots` indicates someone `^PW<n>^JUS`-ed the
    /// printer down at some point (sticky in flash) — the
    /// renderer surfaces a warning in that case.
    pub head_w_dots: u32,
    /// Calibrated label length in dots from the most recent
    /// `~JC` (or auto-calibrate). 0 if unknown.
    pub label_len_dots: u32,
    /// `Some(false)` when `~HQES` reported active errors;
    /// `Some(true)` when the printer is healthy; `None` when the
    /// status query failed or didn't return.
    pub status_ok: Option<bool>,
    /// Human-readable error labels from `~HQES`. Empty when status
    /// is ok or unknown.
    pub errors: Vec<String>,
    /// Human-readable warning labels from `~HQES`. Independent of
    /// errors — warnings can fire on a healthy printer.
    pub warnings: Vec<String>,
}

/// Run all SGD queries for a single printer and bundle the results
/// (dpi, media size, and live status flags). Errors are logged and
/// converted to zero / None values; callers treat empties as "the
/// printer didn't answer, render the row without that info".
pub fn query_printer_dims(agent: &AgentClient, uid: &str) -> PrinterDims {
    let mut d = PrinterDims::default();
    match agent.get_printer_dpi(uid) {
        Ok(v) => d.dpi = v as u32,
        Err(e) => tracing::warn!("get_printer_dpi({uid}): {e:#}"),
    }
    // Prefer the newer GetPrinterDimensions (returns physical head +
    // stored PW + label length in one ^HH round-trip). Fall back to
    // legacy get_media_size when the agent is older than this UI.
    match agent.get_printer_dimensions(uid) {
        Ok((phys, stored_pw, label_len)) => {
            d.phys_head_dots = phys;
            d.head_w_dots = stored_pw;
            d.label_len_dots = label_len;
        }
        Err(e) => {
            tracing::warn!("get_printer_dimensions({uid}): {e:#} — falling back");
            if let Ok((head_w, label_len)) = agent.get_media_size(uid) {
                d.head_w_dots = head_w;
                d.label_len_dots = label_len;
            }
        }
    }
    match agent.get_printer_status(uid) {
        Ok((ok, errors, warnings)) => {
            d.status_ok = Some(ok);
            d.errors = errors;
            d.warnings = warnings;
        }
        Err(e) => tracing::warn!("get_printer_status({uid}): {e:#}"),
    }
    d
}

/// Format the live status as a one-line subtitle fragment. Returns
/// an empty string when status is unknown or fully healthy with no
/// warnings. Caller prepends it on its own line for visibility.
pub fn status_subtitle(dims: &PrinterDims) -> String {
    let mut parts: Vec<String> = Vec::new();
    if !dims.errors.is_empty() {
        parts.push(format!("Error: {}", dims.errors.join(", ")));
    }
    if !dims.warnings.is_empty() {
        parts.push(format!("Warning: {}", dims.warnings.join(", ")));
    }
    parts.join(" \u{00b7} ")
}

/// Render the calibrated-dimensions subtitle line for a printer row.
/// Empty when nothing useful is known (caller should omit the line
/// entirely in that case).
///
/// When both physical-head and stored-PW are known AND they differ,
/// the head reading is shown first with a "Print width capped: X"
/// suffix and a ⚠ marker so the operator can see *at a glance* that
/// the printer's been ^PW-capped below its hardware capability — the
/// failure mode that used to hide as "Printhead: 2.00" on a 4″ ZD220
/// (someone ^PW406^JUS-ed it once, label print would clip the right
/// half of every 4″ design forever after, calibration didn't fix it
/// because calibration only re-detects gap+length).
pub fn calibrated_dims_subtitle(dims: &PrinterDims) -> String {
    // dpi == 0 means SGD failed; fall back to 203 so we at least get
    // a plausible inch conversion (most ZD-series are 203 dpi).
    let dpi = if dims.dpi > 0 { dims.dpi } else { 203 };
    // Pick the canonical head dimension to display: prefer physical,
    // fall back to stored PW when the agent is too old to answer
    // GetPrinterDimensions (legacy GetMediaSize only returns PW).
    let head_display = if dims.phys_head_dots > 0 {
        dims.phys_head_dots
    } else {
        dims.head_w_dots
    };
    // Cap warning fires only when stored PW is materially LESS than
    // physical head — `> 50 dots` (~0.25″ at 203 dpi). Zebra factory
    // default sets PW a hair below physical (812 vs 832 on a ZD220 4″
    // head) for tear-bar alignment; that 20-dot gap is normal and
    // would false-positive a literal `<` comparison on every healthy
    // printer.
    const CAP_TOLERANCE_DOTS: u32 = 50;
    let capped_warning = if dims.phys_head_dots > 0
        && dims.head_w_dots > 0
        && dims.head_w_dots + CAP_TOLERANCE_DOTS < dims.phys_head_dots
    {
        Some(format!(
            " \u{26a0} Print width capped at {:.2}\" (^PW{})",
            dots_to_inches(dims.head_w_dots, dpi),
            dims.head_w_dots
        ))
    } else {
        None
    };
    let warn = capped_warning.as_deref().unwrap_or("");
    match (head_display, dims.label_len_dots) {
        (0, 0) => String::new(),
        (0, l) => format!(
            "Label length: {:.2}\"  ({l} dots @ {dpi} dpi)",
            dots_to_inches(l, dpi)
        ),
        (w, 0) => format!(
            "Printhead: {:.2}\" wide  ({w} dots @ {dpi} dpi)  · length not calibrated{warn}",
            dots_to_inches(w, dpi)
        ),
        (w, l) => format!(
            "Label length: {:.2}\"  \u{00b7}  Printhead: {:.2}\" wide  ({l} \u{00d7} {w} dots @ {dpi} dpi){warn}",
            dots_to_inches(l, dpi),
            dots_to_inches(w, dpi)
        ),
    }
}

/// Convert printer dots to inches at the given dpi.
pub fn dots_to_inches(dots: u32, dpi: u32) -> f32 {
    if dpi == 0 {
        return 0.0;
    }
    dots as f32 / dpi as f32
}

/// The "always-on" first line of a printer-row subtitle: serial,
/// manufacturer, UID. Doesn't depend on SGD output so it can be
/// shown immediately while the dims/status are still loading.
pub fn base_serial_subtitle(p: &PrinterEntry) -> String {
    if p.serial.is_empty() {
        format!("UID: {} · {}", p.uid, p.manufacturer)
    } else {
        format!("Serial: {} · {} · UID: {}", p.serial, p.manufacturer, p.uid)
    }
}

/// Compose the full subtitle for a printer row: serial line + (when
/// available) calibrated dims line + (when active) status line.
/// When `dims` is None the row hasn't been queried yet — caller
/// pairs this with a visible spinner.
pub fn full_subtitle(p: &PrinterEntry, dims: Option<&PrinterDims>) -> String {
    let mut s = base_serial_subtitle(p);
    let Some(d) = dims else { return s };
    let dl = calibrated_dims_subtitle(d);
    if !dl.is_empty() {
        s.push('\n');
        s.push_str(&dl);
    }
    let sl = status_subtitle(d);
    if !sl.is_empty() {
        s.push('\n');
        s.push_str(&sl);
    }
    s
}

/// Pick the badge icon name + accent CSS class for a row's status,
/// or None when no badge should be shown (healthy printer or
/// status not yet queried). Caller hides the badge slot entirely
/// in the None case.
pub fn status_badge(dims: &PrinterDims) -> Option<(&'static str, &'static str)> {
    if dims.status_ok == Some(false) {
        Some(("dialog-error-symbolic", "error"))
    } else if !dims.warnings.is_empty() {
        Some(("dialog-warning-symbolic", "warning"))
    } else {
        None
    }
}
