<?php
// Plugin Check / PHPCS: category-level exclusions with reasons.
// Line-level findings in this file are handled individually below.
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only GET routing and filter parameters (which screen/record to show); every state-changing handler verifies a nonce before writing.
/**
 * Included File: Trade Calculators
 *
 * PaintOps ties one optional "calculator" popup to each job category
 * (see the 'calculator' key on entries from paintops_job_categories()).
 * This is the trade-agnostic layer: the estimate screen doesn't know or
 * care what trade a category represents, it just asks "does this
 * category have a calculator?" and, if so, renders whatever's registered
 * under that key.
 *
 * To add a calculator for a new trade (e.g. an HVAC tonnage/ductwork
 * calculator), you do NOT need to touch estimates.php or any core file:
 *
 * 1. Write a function that returns the HTML (typically a link plus an
 *    inline <script> that opens a popup window), see
 *    paintops_render_paint_sqft_calculator() below for the pattern.
 * 2. Register it under a new key via the 'paintops_calculators' filter,
 *    e.g.:
 *        add_filter('paintops_calculators', function ($calculators) {
 *            $calculators['hvac_tonnage'] = 'my_hvac_calculator_function';
 *            return $calculators;
 *        });
 * 3. In PaintOps > Business > Job Categories, set that category's
 *    "Calculator" field to hvac_tonnage.
 */

if (!defined('ABSPATH')) exit;

/**
 * All registered calculators, keyed by the identifier stored on a job
 * category's 'calculator' field. Filterable so calculators can be added
 * (or removed/replaced) without editing this file.
 */
function paintops_calculator_registry(): array {
    return apply_filters('paintops_calculators', [
        'paint_sqft'       => 'paintops_render_paint_sqft_calculator',
        'paint_materials'  => 'paintops_render_paint_materials_calculator',
        'cabinet_trim'     => 'paintops_render_cabinet_trim_calculator',
        'labor_days'       => 'paintops_render_labor_days_calculator',
        'deck_fence_stain' => 'paintops_render_deck_fence_calculator',
    ]);
}

/**
 * Human-readable labels for everything in the registry, plus a leading
 * '' => 'None' option, built for the Job Categories admin screen's
 * Calculator dropdown (see paintops_render_business_settings_page() in
 * paintops.php), so an admin can assign any registered calculator to a
 * category by name rather than needing to know its internal key. A
 * calculator added via the 'paintops_calculators' filter without a
 * matching entry here still works (paintops_render_category_calculator()
 * doesn't consult this list at all), it just falls back to showing its
 * raw key as the label, so the dropdown never silently hides a
 * third-party calculator.
 */
function paintops_calculator_options(): array {
    $labels = [
        'paint_sqft'       => 'Paint Calculator (interior/exterior sq ft)',
        'paint_materials'  => 'Paint Quantity Calculator (gallons/liters needed)',
        'cabinet_trim'     => 'Cabinet & Trim/Door Calculator (priced per unit)',
        'labor_days'       => paintops_spelling('Labor', 'Labour') . ' Days & Crew Cost Calculator',
        'deck_fence_stain' => 'Deck/Fence Staining Calculator',
    ];

    $options = ['' => 'None'];
    foreach (array_keys(paintops_calculator_registry()) as $key) {
        $options[$key] = $labels[$key] ?? $key;
    }

    return $options;
}

/**
 * Render the calculator assigned to a job category, if any. Returns an
 * empty string if the category has no calculator set, or if the key
 * doesn't match anything registered (e.g. a calculator from a disabled
 * add-on), callers don't need to know the difference.
 *
 * $context is an optional grab-bag of job-level info a calculator might
 * want to tailor itself to, currently just 'interior'/'exterior'
 * booleans (see paintops_render_paint_sqft_calculator()), so a
 * calculator can skip sections that don't apply to this particular job.
 * Calculators that don't care about $context can just ignore the
 * parameter; PHP doesn't require a registered callback to declare it.
 */
function paintops_render_category_calculator(string $calculator_key, array $context = []): string {
    if ($calculator_key === '') {
        return '';
    }

    $registry = paintops_calculator_registry();

    if (empty($registry[$calculator_key]) || !is_callable($registry[$calculator_key])) {
        return '';
    }

    return (string) call_user_func($registry[$calculator_key], $context);
}

/**
 * Render every calculator assigned to a job category, a category can
 * now have more than one (see the 'calculators' array on entries from
 * paintops_job_categories(), and the checkbox list in the Job
 * Categories admin screen), e.g. an interior residential category
 * offering the Paint Calculator, Cabinet & Trim Calculator, and Labor
 * Days Calculator all at once, each as its own tool card + modal on the
 * New/Edit Quote screen. Just loops paintops_render_category_calculator()
 * per key and concatenates, each calculator already has its own
 * unique panel id/results id (see each one's own render function), so
 * there's no markup collision from having several on the same page.
 * Unknown/invalid keys are silently skipped (same fail-open behavior as
 * the single-calculator version), so a stale key from a
 * disabled/removed calculator doesn't break the rest of the row.
 */
function paintops_render_category_calculators(array $calculator_keys, array $context = []): string {
    $registry = paintops_calculator_registry();

    $valid_keys = [];
    foreach ($calculator_keys as $calculator_key) {
        if (is_string($calculator_key) && $calculator_key !== '' && !empty($registry[$calculator_key]) && is_callable($registry[$calculator_key])) {
            $valid_keys[] = $calculator_key;
        }
    }
    if (empty($valid_keys)) {
        return '';
    }

    $body = '';
    foreach ($valid_keys as $calculator_key) {
        $body .= (string) call_user_func($registry[$calculator_key], $context);
    }

    // All of a category's calculators now share ONE <form>, but it's
    // declared separately, up near the top of estimates.php as an empty
    // #paintopsCalcForm, NOT wrapped around this markup here. This
    // content renders visually right above the itemize card, which sits
    // inside the page's big <form method="POST"> (the actual quote-save
    // form), physically nesting a second <form> in there would be
    // invalid HTML that browsers silently mishandle (the inner form tag
    // gets dropped and its controls get adopted by the outer POST form
    // instead, so Calculate would submit the whole quote-save form
    // instead of computing anything). Every actual form control below
    // (inputs, selects, the Calculate buttons) carries
    // form="paintopsCalcForm" instead, which associates it with that
    // separate anchor form regardless of DOM nesting, standard HTML5,
    // not a hack. Clicking Calculate in any one calculator still submits
    // every calculator's current fields together in a single request,
    // and PHP computes whatever totals it has data for, that's what
    // lets someone fill in Paint, Cabinet & Trim, and Labor Days and
    // only hit Calculate once per section without losing the others'
    // numbers.
    //
    // Every non-calculator page param (job id, csaction, editEid, etc.)
    // gets replayed forward as a hidden field exactly once here, instead
    // of once per calculator, a plain <form method="get"> submit only
    // ever sends its own fields, so without this the rest of the
    // New/Edit Quote screen's routing would be lost on every submit.
    $known_prefixes = ['pc_', 'pm_', 'ct_', 'ld_', 'df_'];
    $hiddenParams = '';
    foreach ($_GET as $key => $value) {
        // Sanitize the parameter NAME first (then esc_attr on output);
        // request keys are input too. Case-preserving allowlist rather
        // than sanitize_key(), the app's routing params are camelCase
        // (viewEid, jobFinancials) and lowercasing would break replay.
        $key = preg_replace('/[^A-Za-z0-9_\-]/', '', (string) $key);
        if ($key === '') {
            continue;
        }
        if ($key === 'calc_active_panel' || $key === 'calc_used_panels' || $key === 'po_calc_nonce' || $key === '_wpnonce') {
            continue; // set explicitly below (calc_used_panels needs its
            // value freshly merged via paintops_calc_used_panels(), not
            // replayed as-is, see below)
        }
        $isCalcField = false;
        foreach ($known_prefixes as $prefix) {
            if (strpos($key, $prefix) === 0) {
                $isCalcField = true;
                break;
            }
        }
        if ($isCalcField) {
            continue; // each calculator's own fields are already in $body
        }
        if (is_array($value)) {
            continue; // not expected here (the itemize card's own overflow
            // fields ARE arrays, but they're snapshotted straight into
            // the form fresh on every submit by the JS in
            // paintops_calc_add_to_quote_script(), not replayed here)
        }
        $hiddenParams .= '<input type="hidden" form="paintopsCalcForm" name="' . esc_attr(sanitize_text_field($key)) . '" value="' . esc_attr(sanitize_text_field(wp_unslash((string) $value))) . '">';
    }

    // Carries the running "which panels have been calculated so far"
    // list forward to the next submit, see paintops_calc_used_panels()
    // for why this exists (keeps every calculated panel's results/Add-
    // to-Quote button available, not just the one just clicked).
    $usedPanels = paintops_calc_used_panels();
    $usedPanelsField = $usedPanels !== []
        ? '<input type="hidden" form="paintopsCalcForm" name="calc_used_panels" value="' . esc_attr(implode(',', $usedPanels)) . '">'
        : '';

    // Nonce for the calculator state round-trip; every state reader goes
    // through paintops_calc_nonce_ok() before trusting these params.
    $calcNonceField = '<input type="hidden" form="paintopsCalcForm" name="po_calc_nonce" value="' . esc_attr(wp_create_nonce('paintops_calc_state')) . '">';

    return $hiddenParams
        . $calcNonceField
        . $usedPanelsField
        . $body
        . paintops_calc_modal_css()
        . paintops_calc_add_to_quote_script()
        . paintops_calc_shared_cleanup_script($known_prefixes);
}

/**
 * Which calculator's own Calculate button was just clicked, if the
 * shared calculator form (built by paintops_render_category_calculators()
 * above) was just submitted, empty string on a plain page load or any
 * request that didn't come through that form at all. A plain HTML
 * <button type="submit" name="calc_active_panel" value="...panel id...">
 * only ever sends its own name/value pair when THAT specific button is
 * what triggered the submission (not just because it's present in the
 * form), so this doubles as both "was the shared form submitted at all"
 * (non-empty) and "which calculator's Calculate was it" (the value).
 *
 * NOTE: deliberately NOT sanitize_key(), that lowercases everything,
 * and every panel id (paintopsLaborPanel, paintopsCabinetPanel, etc.) is
 * camelCase, so a lowercased value would never match the id it's
 * compared against and no modal would ever auto-reopen after a real
 * Calculate submit (exactly what happened before this fix, clicking
 * Calculate just looked like the modal closed, since the page reloaded
 * with nothing telling it which one to reopen). This value is only ever
 * used in strict === comparisons against hardcoded panel-id literals
 * below, never echoed into HTML, so case-preserving is safe here.
 */
/**
 * The calculator state params (calc_active_panel, calc_used_panels and
 * the pc_/pm_/ct_/ld_/df_ fields) only ever come from the calculator's
 * own GET form, which now carries a nonce. Reads of that state verify
 * the nonce first; without a valid one the calculators simply render
 * in their default (empty) state.
 */
function paintops_calc_nonce_ok(): bool {
    static $ok = null;
    if ($ok === null) {
        $nonce = isset($_GET['po_calc_nonce']) ? sanitize_text_field(wp_unslash((string) $_GET['po_calc_nonce'])) : '';
        $ok = ($nonce !== '' && (bool) wp_verify_nonce($nonce, 'paintops_calc_state'));
    }
    return $ok;
}

function paintops_calc_active_panel(): string {
    if (!paintops_calc_nonce_ok() || !isset($_GET['calc_active_panel'])) {
        return '';
    }
    return preg_replace('/[^A-Za-z0-9_-]/', '', wp_unslash((string) $_GET['calc_active_panel'])); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- preg_replace reduces the value to [A-Za-z0-9_-] before it is used
}

/**
 * Whether the shared calculator form has been submitted at all this
 * page load, regardless of which specific calculator's Calculate button
 * was clicked. Every calculator's fields live in that one form now (see
 * paintops_render_category_calculators()), a <div style="display:none">
 * modal's inputs are still part of the DOM and still get submitted along
 * with everything else, so submitting ANY one calculator carries every
 * OTHER calculator's current field values (typed numbers, checked boxes)
 * along too. Each calculator uses this (instead of its own dedicated
 * xx_submit flag) to tell "checkbox absent because the form was
 * submitted and it's genuinely unchecked" apart from "checkbox absent
 * because nothing has been submitted yet, fall back to the smart
 * default", and to prefill its own text fields from $_GET even when a
 * *different* calculator's button was the one actually clicked.
 */
function paintops_calc_form_submitted(): bool {
    return paintops_calc_active_panel() !== '';
}

/**
 * Every panel that has been calculated at least once across this back-
 * and-forth of Calculate submits, not just whichever one was clicked
 * most recently (that's paintops_calc_active_panel(), still used to
 * decide which modal auto-opens). Carried forward as a single hidden
 * 'calc_used_panels' field (comma-separated panel ids, rendered by
 * paintops_render_category_calculators()) that each submit appends the
 * just-clicked panel onto.
 *
 * This is the fix for "each submit clears the last input" / "some
 * calculators don't submit to the itemizations": before this, a
 * calculator's results block (and the "Add These Numbers to Quote"
 * button living inside it) only rendered at all when it was the LAST
 * one clicked, calculating a second calculator didn't touch the
 * first one's typed field values, but it did make the first one's
 * whole results+button section disappear from the page, since that was
 * gated on "is this the panel that was just clicked" instead of "has
 * this panel been calculated yet". If the estimator worked through
 * several calculators before circling back to click Add to Quote on
 * the first one, that button was already gone. Every calculator's
 * $showResults now checks in_array(..., paintops_calc_used_panels())
 * instead, so results/Add-to-Quote stay available for every calculator
 * that's been calculated at least once, while $shouldAutoOpen (which
 * modal visually pops open on this particular page load) still checks
 * only paintops_calc_active_panel(), so submitting a second
 * calculator doesn't pop every previously-used modal open at once.
 */
function paintops_calc_used_panels(): array {
    $panels = [];
    if (paintops_calc_nonce_ok() && isset($_GET['calc_used_panels'])) {
        $raw = sanitize_text_field(wp_unslash((string) $_GET['calc_used_panels']));
        foreach (explode(',', $raw) as $panel) {
            $panel = preg_replace('/[^A-Za-z0-9_-]/', '', $panel);
            if ($panel !== '') {
                $panels[] = $panel;
            }
        }
    }
    $active = paintops_calc_active_panel();
    if ($active !== '' && !in_array($active, $panels, true)) {
        $panels[] = $active;
    }
    return $panels;
}

/**
 * Shared plumbing used by every calculator EXCEPT paint_sqft (below),
 * which predates these and keeps its own inline copy untouched to avoid
 * any regression risk to an already-shipped, already-verified feature.
 * Every calculator added after paint_sqft builds on these instead of
 * re-duplicating the same ~80 lines of modal/script boilerplate. Since
 * paintops_render_category_calculator() only ever renders ONE
 * calculator per page load (a job category has exactly one 'calculator'
 * key), there's no risk of these colliding with paint_sqft's own inline
 * versions of the same CSS classes/JS function name, at most one copy
 * of any of this is ever actually on the page at once.
 */

/**
 * Opens a calculator's modal: the tool-card link (goes in
 * estimates.php's .paintops-tool-cards row) plus the modal overlay/box
 * shell, up through its heading. No <form> tag here, every calculator
 * shares one #paintopsCalcForm, declared separately near the top of
 * estimates.php (outside the page's big <form method="POST">) so it
 * never ends up nested inside it. This modal's own inputs/selects/submit
 * button (added by the caller after this) each carry
 * form="paintopsCalcForm" instead of living inside a physical <form>
 * tag here, see paintops_render_category_calculators() for the full
 * explanation. $should_open controls whether this specific modal
 * auto-reopens (pass true only when this is the calculator whose own
 * Calculate button was just clicked, see paintops_calc_active_panel()).
 * Pair with paintops_calc_modal_close() once the calculator's own
 * fields/results have been appended.
 */
function paintops_calc_modal_open(string $panel_id, string $card_title, string $card_sub_html, string $modal_title, bool $should_open): string {
    $panel_open_class = $should_open ? ' show' : '';

    return '<a href="#" class="paintops-tool-card" onclick="document.getElementById(\'' . esc_js($panel_id) . '\').classList.add(\'show\'); return false;">'
        . '<span class="tool-card-title">' . esc_html($card_title) . '</span>'
        . '<span class="tool-card-sub">' . $card_sub_html . '</span>'
        . '</a>'
        . '<div id="' . esc_attr($panel_id) . '" class="paintops-modal-overlay' . esc_attr($panel_open_class) . '" onclick="this.classList.remove(\'show\')">'
        . '<div class="paintops-modal-box" onclick="event.stopPropagation()">'
        . '<a href="#" class="paintops-modal-close" onclick="document.getElementById(\'' . esc_js($panel_id) . '\').classList.remove(\'show\'); return false;">&times;</a>'
        . '<h2 style="margin:0 0 14px;font-size:18px;color:var(--po-text,#23282d);">' . esc_html($modal_title) . '</h2>';
}

/**
 * Closes what paintops_calc_modal_open() opened, just the two wrapper
 * divs now. The shared <form>, CSS, add-to-quote JS, and URL-cleanup
 * script are all emitted once by paintops_render_category_calculators()
 * instead of once per calculator.
 */
function paintops_calc_modal_close(): string {
    return '</div>' // .paintops-modal-box
        . '</div>'; // #<panel_id>.paintops-modal-overlay
}

function paintops_calc_modal_css(): string {
    // Shared modal/results CSS now ships as a bundled stylesheet through
    // the styles API (assets/css/trade-calc-modal.css) instead of a
    // printed <style> tag; returns '' so call sites stay unchanged.
    paintops_enqueue_style('trade-calc-modal');
    return '';
}

/**
 * "Add These Numbers to Quote": plain DOM writes + a dispatched 'input'
 * event (so the itemize card's own calculateTotal() listener picks up
 * the change), no form submit, no navigation, so no browser
 * resubmission warning. Shared by every calculator's button, including
 * paint_sqft's (which used to declare its own separate inline copy of
 * this exact function, removed once a category could have more than
 * one calculator active at once, since two different-signature
 * declarations of the same global function name on one page is a real
 * bug: whichever loads last silently wins).
 *
 * A category can now have several calculators assigned at once (see
 * paintops_render_category_calculators()), all sharing the same 5-slot
 * itemize card, so this can no longer just write to a hardcoded slot
 * per line-type the way earlier versions did (two calculators both
 * assuming they own "slot 1" would stomp on each other). Each item
 * instead carries a stable 'tag' (a fixed text prefix specific to that
 * line-type, e.g. "Exterior Painting (") alongside its 'desc' (the
 * full, numbers-included label) and 'cost'. For each item:
 *   1. Look for a slot whose current description already starts with
 *      this item's tag, that's this same line-type's own previous
 *      entry, so re-clicking after adjusting inputs updates it in
 *      place instead of leaving a stale duplicate.
 *   2. Otherwise, claim the first slot that's genuinely empty (blank
 *      description, zero/blank cost), so a second, different
 *      calculator's click lands in fresh slots rather than overwriting
 *      the first calculator's numbers.
 *   3. If neither is available (the itemize card's 5 slots are all
 *      full of unrelated data), that item is skipped and counted,
 *      surfaced afterward as a single alert() rather than silently
 *      dropped, so nothing goes missing without the estimator knowing.
 */
function paintops_calc_add_to_quote_script(): string {
    // The whole add-to-quote script is static logic, so it now ships as
    // a bundled file through the scripts API
    // (assets/js/trade-calc-add-to-quote.js, printed with the footer
    // scripts) instead of a printed <script> tag; returns '' so call
    // sites stay unchanged.
    paintops_enqueue_script('trade-calc-add-to-quote');
    return '';
}

/**
 * Strips every calculator-related query param (any known prefix, plus
 * calc_active_panel) back out of the address bar right after the shared
 * form's results render, otherwise a plain page refresh would replay
 * the same submission and pop a modal back open on what looks like a
 * fresh page load. One shared cleanup now instead of one per calculator
 * (see paintops_render_category_calculators()), covering whichever
 * prefixes were actually in play.
 */
function paintops_calc_shared_cleanup_script(array $prefixes): string {
    if (!paintops_calc_form_submitted()) {
        return '';
    }

    // Auto-scroll the just-calculated panel's own "Add These Numbers to
    // Quote" button into view, the modal overlay scrolls internally
    // (see .paintops-modal-overlay's overflow-y:auto in
    // paintops_calc_modal_css()), and a calculator with several input
    // sections plus a results block routinely pushes that button below
    // the fold, so without this the estimator had to scroll down inside
    // the modal every time just to find it after clicking Calculate.
    // Only the panel that was just auto-opened has the '.show' class
    // (see $shouldAutoOpen in each calculator's own render function),
    // so this only ever targets the one the estimator is actually
    // looking at right now, not every previously-used panel.
    // block:'center' (not 'end') keeps the button comfortably in the
    // middle of the visible area instead of jammed against the bottom
    // edge of the modal.
    $autoscroll = '(function(){var p=document.querySelector(\'.paintops-modal-overlay.show\');if(!p){return;}var t=p.querySelector(\'.paintops-calc-items-holder\')||p.querySelector(\'.paintops-calc-results, [id$="Results"]\');if(t&&t.scrollIntoView){t.scrollIntoView({behavior:\'smooth\',block:\'center\'});}})();';

    // Short, fully dynamic fragment (the prefix list is built per page),
    // so it goes through the scripts API as an inline footer script
    // rather than a printed <script> tag; returns '' so the call site
    // stays unchanged.
    paintops_add_inline_script('(function(){if(window.history&&window.history.replaceState){var u=new URL(window.location.href);var prefixes=' . wp_json_encode(array_values($prefixes)) . ';Array.from(u.searchParams.keys()).forEach(function(k){if(k===\'calc_active_panel\'||k===\'calc_used_panels\'||k===\'po_calc_nonce\'||prefixes.some(function(p){return k.indexOf(p)===0;})){u.searchParams.delete(k);}});window.history.replaceState({},\'\',u.toString());}})();' . $autoscroll);
    return '';
}

/**
 * Built-in calculator: interior/exterior paint square footage & cost
 * estimator. PaintOps' original built-in calculator, registered under
 * the 'paint_sqft' key above (see paintops_calculator_registry() for
 * the rest, added later).
 *
 * Rewritten from a window.open()+document.write()-built popup (JS math,
 * a second in-memory document) to a CSS modal overlay + <form
 * method="get"> that submits back to this same page, Calculate Quote
 * is now a real HTTP request, and every number on the results box is
 * computed here in PHP, not in a browser script. That popup approach was
 * reported as "Calculate Quote does nothing, just an empty white box"
 * with no console error and no caching/minification plugin in play,
 * which points at something silently dropping the document.write()'d
 * <script> tag specifically (a known behavior of some ad-blocker/
 * privacy-extension content filters, which target document.write
 * because it's historically been abused for ad injection) rather than a
 * logic bug, a plain form submit sidesteps that whole class of
 * failure instead of chasing it further.
 *
 * Accepts an optional $context (see paintops_render_category_calculator())
 * with 'interior'/'exterior' booleans, estimates.php passes the job's
 * own Interior/Exterior checkboxes through so a job marked exterior-only
 * doesn't show or factor in Baseboards/interior fields at all.
 */
function paintops_render_paint_sqft_calculator(array $context = []): string {
    // Business Settings > Company Profile > Currency's symbol.
    $currencySymbol = paintops_currency_symbol();

    // Which section(s) actually apply to this job, passed in from
    // estimates.php based on the job's own Interior/Exterior checkboxes
    // (see manage-jobs.php), same $job_scope_interior/$job_scope_exterior
    // used to filter the Scope Components checklist. An exterior-only
    // job has no use for Baseboards or any other interior field, so
    // that whole section (inputs + result lines + its contribution to
    // the total) is skipped rather than just hidden with CSS, fewer
    // fields to fill in, and the total can't accidentally include a
    // section that was never meant to apply. Fail open (show both) if
    // the context doesn't distinguish, either because the caller
    // didn't pass one at all, or because it passed both as false (an
    // older job saved before Interior/Exterior existed).
    $showInterior = !array_key_exists('interior', $context) || (bool) $context['interior'];
    $showExterior = !array_key_exists('exterior', $context) || (bool) $context['exterior'];
    if (!$showInterior && !$showExterior) {
        $showInterior = true;
        $showExterior = true;
    }

    // Business Settings > Company Profile > Measurement Units, swaps
    // this panel's field labels and starter default values from feet/
    // sq ft to meters/sq meters. The underlying math (area x rate = cost)
    // doesn't actually care which unit it's in, so this is just labels +
    // sensible starting numbers, not a real conversion of anything a user
    // has already typed, same "changes what things are called, never
    // restricts input" principle as Country/Currency.
    $metric = paintops_uses_metric();
    $areaUnit = $metric ? 'Sq Meters' : 'Sq Ft';
    $areaUnitLower = $metric ? 'sq meters' : 'sq ft';
    $lengthUnit = $metric ? 'meters' : 'ft';
    $lengthUnitLinear = $metric ? 'linear meters' : 'linear feet';
    $rateUnit = $metric ? 'sm' : 'sf';
    $rateUnitLinear = $metric ? 'm' : 'ft';

    // Starter defaults, imperial values are the original hardcoded
    // ones; metric values are the same real-world size/rate, just
    // converted (1 sq ft ~= 0.0929 sqm, 1 ft ~= 0.3048 m) and rounded to
    // something reasonable to type over, not a precise conversion of any
    // specific business's numbers.
    $defaults = [
        'pc_interior_sqft'    => $metric ? 140    : 1500,
        'pc_ceiling_height'   => $metric ? 2.4    : 8,
        'pc_auto_wall'        => 1,
        'pc_baseboard_ft'     => $metric ? 180    : 600,
        'pc_interior_rate'    => $metric ? 21.50  : 2.00,
        'pc_baseboard_rate'   => $metric ? 7.05   : 2.15,
        'pc_exterior_linear'  => $metric ? 49     : 160,
        'pc_exterior_height'  => $metric ? 2.7    : 9,
        'pc_exterior_rate'    => $metric ? 27.00  : 2.50,
        'pc_trim_level'       => 'moderate',
    ];

    // All calculators on this category now share one <form> (see
    // paintops_render_category_calculators()), $formSubmitted is true
    // once ANY of them has been submitted (every calculator's fields ride
    // along together), while $showResults is only true for THIS specific
    // calculator, i.e. only when its own Calculate button was the one
    // actually clicked (see paintops_calc_active_panel()). That split is
    // what lets every calculator's typed-in values persist across a
    // *different* calculator's submit, while only the one just used pops
    // its results open.
    $formSubmitted  = paintops_calc_form_submitted();
    $showResults    = in_array('paintopsCalcPanel', paintops_calc_used_panels(), true);
    $shouldAutoOpen = paintops_calc_active_panel() === 'paintopsCalcPanel';

    // Pull each field from $_GET once the shared form has been submitted
    // at least once (falling back to its default for anything missing/
    // malformed), otherwise just use the defaults outright for the
    // panel's first render.
    $val = function (string $key) use ($defaults, $formSubmitted) {
        if (!$formSubmitted || !paintops_calc_nonce_ok() || !isset($_GET[$key])) {
            return $defaults[$key];
        }
        // Nonce verified on the line above (paintops_calc_nonce_ok() checks
        // po_calc_nonce against the paintops_calc_state action) before any
        // request value is read.
        return sanitize_text_field(wp_unslash($_GET[$key]));
    };

    $interiorSqFt   = (float) $val('pc_interior_sqft');
    $ceilingHeight  = (float) $val('pc_ceiling_height');
    $autoWall       = ($formSubmitted && paintops_calc_nonce_ok()) ? !empty($_GET['pc_auto_wall']) : true; // unchecked boxes just don't appear in $_GET
    $baseboardFt    = (float) $val('pc_baseboard_ft');
    $interiorRate   = (float) $val('pc_interior_rate');
    $baseboardRate  = (float) $val('pc_baseboard_rate');
    $exteriorLinear = (float) $val('pc_exterior_linear');
    $exteriorHeight = (float) $val('pc_exterior_height');
    $exteriorRate   = (float) $val('pc_exterior_rate');
    $trimLevel      = (string) $val('pc_trim_level');

    $trimRates = ['none' => 0, 'light' => 0.25, 'moderate' => 0.5, 'heavy' => 1.0];
    if (!array_key_exists($trimLevel, $trimRates)) {
        $trimLevel = 'moderate';
    }

    $resultsHtml = '';
    if ($showResults) {
        // Same formulas the old client-side calculator used, estimate
        // wall area from floor size (treating the room as roughly
        // square) when "Calculate wall area from room size" is checked,
        // otherwise treat the floor number itself as the wall area.
        // Interior/baseboard math (and its line items below) only runs
        // when $showInterior, an exterior-only job never had those
        // inputs to submit in the first place, so they'd just compute
        // against stale defaults and shouldn't factor into the total.
        $interiorWallArea = 0;
        $interiorTotal = 0;
        $baseboardTotal = 0;
        if ($showInterior) {
            if ($autoWall && $interiorSqFt > 0) {
                $estLength = sqrt($interiorSqFt);
                $estWidth = $interiorSqFt / $estLength;
                $perimeter = 2 * ($estLength + $estWidth);
                $interiorWallArea = round($perimeter * $ceilingHeight);
            } else {
                $interiorWallArea = $interiorSqFt;
            }
            $interiorTotal = $interiorWallArea * $interiorRate;
            $baseboardTotal = $baseboardFt * $baseboardRate;
        }

        $exteriorWallArea = 0;
        $exteriorBaseCost = 0;
        $exteriorTrimCost = 0;
        $exteriorTotal = 0;
        if ($showExterior) {
            $exteriorWallArea = round($exteriorLinear * $exteriorHeight);
            $trimRate = $trimRates[$trimLevel];
            $exteriorBaseCost = $exteriorWallArea * $exteriorRate;
            $exteriorTrimCost = $exteriorWallArea * $trimRate;
            $exteriorTotal = $exteriorBaseCost + $exteriorTrimCost;
        }

        $grandTotal = $exteriorTotal + $interiorTotal + $baseboardTotal;

        $resultsHtml = '<div class="results" id="paintopsCalcResults">';
        if ($showInterior) {
            $resultsHtml .= '<p>Estimated Interior Wall Area: ' . esc_html(number_format($interiorWallArea, 0)) . ' ' . esc_html($areaUnitLower) . '</p>';
        }
        if ($showExterior) {
            $resultsHtml .= '<p>Estimated Exterior Wall Area: ' . esc_html(number_format($exteriorWallArea, 0)) . ' ' . esc_html($areaUnitLower) . '</p>'
                . '<p>Exterior Base: ' . esc_html($currencySymbol . number_format($exteriorBaseCost, 2)) . '</p>'
                . '<p>Exterior Trim: ' . esc_html($currencySymbol . number_format($exteriorTrimCost, 2)) . '</p>'
                . '<p>Exterior Total: ' . esc_html($currencySymbol . number_format($exteriorTotal, 2)) . '</p>';
        }
        if ($showInterior) {
            $resultsHtml .= '<p>Interior Walls: ' . esc_html($currencySymbol . number_format($interiorTotal, 2)) . '</p>'
                . '<p>Baseboards: ' . esc_html($currencySymbol . number_format($baseboardTotal, 2)) . '</p>';
        }
        $resultsHtml .= '<p style="font-size:1.2em;">Grand Total: <strong>' . esc_html($currencySymbol . number_format($grandTotal, 2)) . '</strong></p>';

        // "Add These Numbers to Quote", pushes each section's total
        // straight into the itemize card's Supply Cost fields further
        // down this same page (see the "Click to Itemize Expenses" card
        // and its calculateTotal() JS, both further down in estimates.php),
        // so the estimator doesn't have to retype these same numbers a
        // second time. Each item carries a 'tag' (a fixed prefix unique
        // to this line-type) instead of a hardcoded slot number,
        // paintopsAddCalcToQuote() (shared across every calculator, see
        // paintops_calc_add_to_quote_script()) reuses whichever slot
        // already starts with that tag (so re-clicking after tweaking
        // inputs updates in place) or claims the next empty one
        // otherwise. That matters now that a category can have more than
        // one calculator assigned (see paintops_render_category_calculators())
        //, e.g. this one and Cabinet & Trim both add to the same
        // 5-slot itemize card, so neither can just assume it owns slot 1.
        // This is still plain DOM manipulation on the already-loaded
        // page (set .value, dispatch an 'input' event so the existing
        // calculateTotal() listener picks it up), no form submit, no
        // page reload, so there's no "confirm resubmission" browser
        // prompt to worry about. Whole-dollar amounts (not cents) to
        // match how the itemize card's own Supply Cost fields already
        // behave, they're step="1" inputs saved via intval() (see the
        // save handler further up estimates.php), so a fractional value
        // would just get truncated on save anyway.
        $addToQuoteItems = [];
        if ($showExterior) {
            $addToQuoteItems[] = [
                'tag'  => 'Exterior Painting (',
                'desc' => 'Exterior Painting (' . number_format($exteriorWallArea, 0) . ' ' . $areaUnitLower . ')',
                'cost' => (int) round($exteriorTotal),
            ];
        }
        if ($showInterior) {
            $addToQuoteItems[] = [
                'tag'  => 'Interior Painting (',
                'desc' => 'Interior Painting (' . number_format($interiorWallArea, 0) . ' ' . $areaUnitLower . ')',
                'cost' => (int) round($interiorTotal),
            ];
            if ($baseboardTotal > 0) {
                $addToQuoteItems[] = [
                    'tag'  => 'Baseboards (',
                    'desc' => 'Baseboards (' . number_format($baseboardFt, 0) . ' ' . $lengthUnitLinear . ')',
                    'cost' => (int) round($baseboardTotal),
                ];
            }
        }
        if (!empty($addToQuoteItems)) {
            $resultsHtml .= '<button type="button" class="button paintops-calc-items-holder" data-calc-items="' . esc_attr(wp_json_encode($addToQuoteItems)) . '" onclick="paintopsAddAllCalcToQuote()" style="margin-top:14px;padding:10px 18px;background-color:#16a34a;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;width:100%;">Add These Numbers to Quote</button>'
                . '<p style="margin-top:8px;font-size:12px;color:#777;">Fills empty itemized Supply Cost field(s) below (or updates its own if already added) and refreshes Quote Total, review before saving.</p>';
        }

        $resultsHtml .= '</div>';
    }

    // Auto-opens (via paintops_calc_modal_open()'s $should_open param
    // below) only when THIS panel's own Calculate button was the one
    // just clicked, see $showResults above. Every other page param
    // (which job's being quoted, csaction, etc.) and the URL-cleanup
    // after a submit are now handled once, by the shared form itself
    // (see paintops_render_category_calculators()), not per calculator.
    $trimOptions = '';
    foreach (['none' => 'None', 'light' => 'Light', 'moderate' => 'Moderate', 'heavy' => 'Heavy'] as $trim_key => $trim_label) {
        $trimOptions .= '<option value="' . esc_attr($trim_key) . '"' . selected($trimLevel, $trim_key, false) . '>' . esc_html($trim_label) . '</option>';
    }

    // Tool-card subtitle reflects whichever section(s) are actually in
    // play for this job, instead of always claiming "Interior & exterior".
    if ($showInterior && $showExterior) {
        $toolCardSub = 'Interior &amp; exterior ' . esc_html($areaUnitLower) . ' + cost estimate';
    } elseif ($showInterior) {
        $toolCardSub = 'Interior ' . esc_html($areaUnitLower) . ' + cost estimate';
    } else {
        $toolCardSub = 'Exterior ' . esc_html($areaUnitLower) . ' + cost estimate';
    }

    $interiorSectionHtml = '';
    if ($showInterior) {
        $interiorSectionHtml = '<div class="calc-section" style="background:var(--po-card-bg,#fff);border:1px solid var(--po-card-border,#ddd);border-radius:8px;padding:16px 20px 20px;margin-bottom:16px;">'
            . '<h3 style="margin:0 0 12px;font-size:15px;text-transform:uppercase;letter-spacing:0.4px;color:var(--po-heading,#2563eb);">Interior</h3>'
            . '<label style="display:block;margin-top:10px;font-size:13px;">Interior Floor ' . esc_html($areaUnit) . '<input form="paintopsCalcForm" name="pc_interior_sqft" type="number" step="any" value="' . esc_attr((string) $interiorSqFt) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
            . '<label style="display:block;margin-top:10px;font-size:13px;">Ceiling Height (' . esc_html($lengthUnit) . ')<input form="paintopsCalcForm" name="pc_ceiling_height" type="number" step="any" value="' . esc_attr((string) $ceilingHeight) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
            . '<label style="display:flex;align-items:center;gap:6px;font-size:14px;margin-top:10px;"><input type="checkbox" form="paintopsCalcForm" name="pc_auto_wall" value="1"' . checked($autoWall, true, false) . ' style="width:auto;margin-top:0;"> Calculate wall area from room size (unchecked = floor size is the wall area)</label>'
            . '<label style="display:block;margin-top:10px;font-size:13px;">Interior Baseboard (' . esc_html($lengthUnitLinear) . ')<input form="paintopsCalcForm" name="pc_baseboard_ft" type="number" step="any" value="' . esc_attr((string) $baseboardFt) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
            . '<label style="display:block;margin-top:10px;font-size:13px;">Interior Rate (' . esc_html($currencySymbol) . '/' . esc_html($rateUnit) . ')<input form="paintopsCalcForm" name="pc_interior_rate" type="number" step="0.01" value="' . esc_attr((string) $interiorRate) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
            . '<label style="display:block;margin-top:10px;font-size:13px;">Baseboard Rate (' . esc_html($currencySymbol) . '/' . esc_html($rateUnitLinear) . ')<input form="paintopsCalcForm" name="pc_baseboard_rate" type="number" step="0.01" value="' . esc_attr((string) $baseboardRate) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
            . '</div>';
    }

    $exteriorSectionHtml = '';
    if ($showExterior) {
        $exteriorSectionHtml = '<div class="calc-section" style="background:var(--po-card-bg,#fff);border:1px solid var(--po-card-border,#ddd);border-radius:8px;padding:16px 20px 20px;margin-bottom:16px;">'
            . '<h3 style="margin:0 0 12px;font-size:15px;text-transform:uppercase;letter-spacing:0.4px;color:var(--po-heading,#2563eb);">Exterior</h3>'
            . '<label style="display:block;margin-top:10px;font-size:13px;">Exterior Perimeter (' . esc_html($lengthUnitLinear) . ')<input form="paintopsCalcForm" name="pc_exterior_linear" type="number" step="any" value="' . esc_attr((string) $exteriorLinear) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
            . '<label style="display:block;margin-top:10px;font-size:13px;">Exterior Wall Height (' . esc_html($lengthUnit) . ')<input form="paintopsCalcForm" name="pc_exterior_height" type="number" step="any" value="' . esc_attr((string) $exteriorHeight) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
            . '<label style="display:block;margin-top:10px;font-size:13px;">Exterior Rate (' . esc_html($currencySymbol) . '/' . esc_html($rateUnit) . ')<input form="paintopsCalcForm" name="pc_exterior_rate" type="number" step="0.01" value="' . esc_attr((string) $exteriorRate) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
            . '<label style="display:block;margin-top:10px;font-size:13px;">Exterior Trim Level<select form="paintopsCalcForm" name="pc_trim_level" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;">' . $trimOptions . '</select></label>'
            . '</div>';
    }

    // Rendered as a modal popup (position:fixed overlay, centered box)
    // rather than an inline expanding panel, an inline panel meant
    // living as a sibling inside estimates.php's .paintops-tool-cards
    // flex row, which put it beside the Google Earth/Price List cards
    // instead of cleanly below them. A fixed-position overlay sidesteps
    // that entirely (it's taken out of the flex flow regardless of where
    // it sits in the DOM) and gets back the "popup" feel Mike wanted,
    // without reintroducing window.open()/document.write(), which is
    // what actually broke the old popup. This is still just a class
    // toggle on a plain onclick attribute (same idiom as .hidden-box
    // elsewhere in the app) and a real <form method="get"> underneath,
    // so results are still computed server-side in PHP on submit.
    $html = '<a href="#" class="paintops-tool-card" onclick="document.getElementById(\'paintopsCalcPanel\').classList.add(\'show\'); return false;">'
        . '<span class="tool-card-title">Use The Paint Calculator</span>'
        . '<span class="tool-card-sub">' . $toolCardSub . '</span>'
        . '</a>'
        . '<div id="paintopsCalcPanel" class="paintops-modal-overlay' . esc_attr($shouldAutoOpen ? ' show' : '') . '" onclick="this.classList.remove(\'show\')">'
        . '<div class="paintops-modal-box" onclick="event.stopPropagation()">'
        . '<a href="#" class="paintops-modal-close" onclick="document.getElementById(\'paintopsCalcPanel\').classList.remove(\'show\'); return false;">&times;</a>'
        . '<h2 style="margin:0 0 14px;font-size:18px;color:var(--po-text,#23282d);">Paint Calculator</h2>'
        . $interiorSectionHtml
        . $exteriorSectionHtml
        . '<button type="submit" form="paintopsCalcForm" name="calc_active_panel" value="paintopsCalcPanel" class="button" style="margin-top:4px;padding:10px 18px;background-color:#2563eb;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;">Calculate Quote</button>'
        . $resultsHtml
        . '</div>' // .paintops-modal-box
        . '</div>'; // #paintopsCalcPanel.paintops-modal-overlay

    // This results div uses its own #paintopsCalcResults id (not the
    // .paintops-calc-results class the other 4 calculators share), so it
    // needs its own small style rule, shipped as a bundled stylesheet
    // (assets/css/trade-calc-sqft-results.css) through the styles API;
    // everything else (.paintops-modal-overlay etc.) is covered once by
    // the shared paintops_calc_modal_css(), called by
    // paintops_render_category_calculators() now instead of here, along
    // with the shared add-to-quote script and URL-cleanup.
    paintops_enqueue_style('trade-calc-sqft-results');

    return $html;
}

/**
 * Paint quantity calculator: how many gallons/liters of paint (and
 * primer) a job needs, given painted area, number of coats, and a
 * coverage rate, plus a rough material cost if a price per
 * gallon/liter is filled in. Complements paint_sqft (which prices
 * labor+materials together as one lump per-sqft rate) for categories
 * where the estimator wants actual quantities to buy, not just a
 * dollar figure, ties conceptually into the Paint Colors feature
 * (color/area/quantity/sheen, see includes/job-paint-colors.php)
 * without writing to it directly.
 */
function paintops_render_paint_materials_calculator(array $context = []): string {
    $currencySymbol = paintops_currency_symbol();
    $metric = paintops_uses_metric();
    $areaUnitLower = $metric ? 'sq meters' : 'sq ft';
    $volUnit = $metric ? 'liters' : 'gallons';
    $volUnitSingle = $metric ? 'liter' : 'gallon';

    $defaults = [
        'pm_wall_area'      => $metric ? 140  : 1500,
        'pm_ceiling_area'   => 0,
        'pm_coats'          => 2,
        'pm_coverage'       => $metric ? 10   : 350, // area covered per gallon/liter
        'pm_include_primer' => 1,
        'pm_price_per_vol'  => $metric ? 9.50 : 35.00,
    ];

    $formSubmitted  = paintops_calc_form_submitted();
    $showResults    = in_array('paintopsMaterialsPanel', paintops_calc_used_panels(), true);
    $shouldAutoOpen = paintops_calc_active_panel() === 'paintopsMaterialsPanel';
    $val = function (string $key) use ($defaults, $formSubmitted) {
        if (!$formSubmitted || !paintops_calc_nonce_ok() || !isset($_GET[$key])) {
            return $defaults[$key];
        }
        // Nonce verified on the line above (paintops_calc_nonce_ok() checks
        // po_calc_nonce against the paintops_calc_state action) before any
        // request value is read.
        return sanitize_text_field(wp_unslash($_GET[$key]));
    };

    $wallArea = (float) $val('pm_wall_area');
    $ceilingArea = (float) $val('pm_ceiling_area');
    $coats = max(1, (int) $val('pm_coats'));
    $coverage = max(0.01, (float) $val('pm_coverage'));
    $includePrimer = ($formSubmitted && paintops_calc_nonce_ok()) ? !empty($_GET['pm_include_primer']) : true;
    $pricePerVol = (float) $val('pm_price_per_vol');

    $resultsHtml = '';
    if ($showResults) {
        $totalArea = $wallArea + $ceilingArea;
        $paintVolume = ceil(($totalArea * $coats) / $coverage);
        $primerVolume = $includePrimer ? ceil($totalArea / $coverage) : 0;
        $totalVolume = $paintVolume + $primerVolume;
        $materialCost = $totalVolume * $pricePerVol;

        $resultsHtml = '<div class="results paintops-calc-results" id="paintopsMaterialsResults">'
            . '<p>Total Paintable Area: ' . esc_html(number_format($totalArea, 0)) . ' ' . esc_html($areaUnitLower) . '</p>'
            . '<p>Paint Needed (' . esc_html((string) $coats) . ' coat' . ($coats === 1 ? '' : 's') . '): ' . esc_html(number_format($paintVolume, 0)) . ' ' . esc_html($volUnit) . '</p>';
        if ($includePrimer) {
            $resultsHtml .= '<p>Primer Needed: ' . esc_html(number_format($primerVolume, 0)) . ' ' . esc_html($volUnit) . '</p>';
        }
        $resultsHtml .= '<p>Total ' . esc_html(ucfirst($volUnit)) . ': ' . esc_html(number_format($totalVolume, 0)) . '</p>'
            . '<p style="font-size:1.2em;">Estimated Material Cost: <strong>' . esc_html($currencySymbol . number_format($materialCost, 2)) . '</strong></p>';

        if ($totalVolume > 0) {
            $addToQuoteItems = [[
                'tag'  => 'Paint & Primer Materials (',
                'desc' => 'Paint & Primer Materials (' . number_format($totalVolume, 0) . ' ' . $volUnit . ')',
                'cost' => (int) round($materialCost),
            ]];
            $resultsHtml .= '<button type="button" class="button paintops-calc-items-holder" data-calc-items="' . esc_attr(wp_json_encode($addToQuoteItems)) . '" onclick="paintopsAddAllCalcToQuote()" style="margin-top:14px;padding:10px 18px;background-color:#16a34a;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;width:100%;">Add These Numbers to Quote</button>'
                . '<p style="margin-top:8px;font-size:12px;color:#777;">Fills an empty itemized Supply Cost field below (or updates its own if already added) and refreshes Quote Total, review before saving.</p>';
        }
        $resultsHtml .= '</div>';
    }

    $html = paintops_calc_modal_open(
        'paintopsMaterialsPanel',
        'Use The Paint Quantity Calculator',
        'Gallons/liters needed + material cost',
        'Paint Quantity Calculator',
        $shouldAutoOpen
    )
        . '<label style="display:block;margin-top:10px;font-size:13px;">Wall Area (' . esc_html($areaUnitLower) . ')<input form="paintopsCalcForm" name="pm_wall_area" type="number" step="any" value="' . esc_attr((string) $wallArea) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Ceiling Area (' . esc_html($areaUnitLower) . ', optional)<input form="paintopsCalcForm" name="pm_ceiling_area" type="number" step="any" value="' . esc_attr((string) $ceilingArea) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Number of Coats<select form="paintopsCalcForm" name="pm_coats" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;">'
        . '<option value="1"' . selected($coats, 1, false) . '>1</option>'
        . '<option value="2"' . selected($coats, 2, false) . '>2</option>'
        . '<option value="3"' . selected($coats, 3, false) . '>3</option>'
        . '</select></label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Coverage per ' . esc_html(ucfirst($volUnitSingle)) . ' (' . esc_html($areaUnitLower) . ')<input form="paintopsCalcForm" name="pm_coverage" type="number" step="any" value="' . esc_attr((string) $coverage) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<label style="display:flex;align-items:center;gap:6px;font-size:14px;margin-top:10px;"><input type="checkbox" form="paintopsCalcForm" name="pm_include_primer" value="1"' . checked($includePrimer, true, false) . ' style="width:auto;margin-top:0;"> Include a primer coat</label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Price per ' . esc_html(ucfirst($volUnitSingle)) . ' (' . esc_html($currencySymbol) . ')<input form="paintopsCalcForm" name="pm_price_per_vol" type="number" step="0.01" value="' . esc_attr((string) $pricePerVol) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<button type="submit" form="paintopsCalcForm" name="calc_active_panel" value="paintopsMaterialsPanel" class="button" style="margin-top:14px;padding:10px 18px;background-color:#2563eb;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;">Calculate</button>'
        . $resultsHtml
        . paintops_calc_modal_close();

    return $html;
}

/**
 * Cabinet & trim/door pricing calculator: prices refinishing work by
 * unit count (cabinet doors/drawers, cabinet boxes, interior doors) and
 * trim/molding by linear length, instead of by square footage, the
 * pricing model most painters actually use for this kind of work, which
 * paint_sqft has no way to express at all.
 */
function paintops_render_cabinet_trim_calculator(array $context = []): string {
    $currencySymbol = paintops_currency_symbol();
    $metric = paintops_uses_metric();
    $lengthUnitLinear = $metric ? 'linear meters' : 'linear feet';
    $rateUnitLinear = $metric ? 'm' : 'ft';

    $defaults = [
        'ct_cab_doors'      => 20,
        'ct_cab_doors_rate' => 60,
        'ct_cab_boxes'      => 10,
        'ct_cab_boxes_rate' => 70,
        'ct_int_doors'      => 0,
        'ct_int_doors_rate' => 100,
        'ct_trim_length'    => $metric ? 30  : 100,
        'ct_trim_rate'      => $metric ? 8.20 : 2.50,
    ];

    $formSubmitted  = paintops_calc_form_submitted();
    $showResults    = in_array('paintopsCabinetPanel', paintops_calc_used_panels(), true);
    $shouldAutoOpen = paintops_calc_active_panel() === 'paintopsCabinetPanel';
    $val = function (string $key) use ($defaults, $formSubmitted) {
        if (!$formSubmitted || !paintops_calc_nonce_ok() || !isset($_GET[$key])) {
            return $defaults[$key];
        }
        // Nonce verified on the line above (paintops_calc_nonce_ok() checks
        // po_calc_nonce against the paintops_calc_state action) before any
        // request value is read.
        return sanitize_text_field(wp_unslash($_GET[$key]));
    };

    $cabDoors = (float) $val('ct_cab_doors');
    $cabDoorsRate = (float) $val('ct_cab_doors_rate');
    $cabBoxes = (float) $val('ct_cab_boxes');
    $cabBoxesRate = (float) $val('ct_cab_boxes_rate');
    $intDoors = (float) $val('ct_int_doors');
    $intDoorsRate = (float) $val('ct_int_doors_rate');
    $trimLength = (float) $val('ct_trim_length');
    $trimRate = (float) $val('ct_trim_rate');

    $resultsHtml = '';
    if ($showResults) {
        $cabDoorsCost = $cabDoors * $cabDoorsRate;
        $cabBoxesCost = $cabBoxes * $cabBoxesRate;
        $cabinetsCost = $cabDoorsCost + $cabBoxesCost;
        $intDoorsCost = $intDoors * $intDoorsRate;
        $trimCost = $trimLength * $trimRate;
        $grandTotal = $cabinetsCost + $intDoorsCost + $trimCost;

        $resultsHtml = '<div class="results paintops-calc-results" id="paintopsCabinetResults">'
            . '<p>Cabinet Doors &amp; Drawers: ' . esc_html($currencySymbol . number_format($cabDoorsCost, 2)) . '</p>'
            . '<p>Cabinet Boxes/Frames: ' . esc_html($currencySymbol . number_format($cabBoxesCost, 2)) . '</p>'
            . '<p>Interior Doors: ' . esc_html($currencySymbol . number_format($intDoorsCost, 2)) . '</p>'
            . '<p>Trim/Molding: ' . esc_html($currencySymbol . number_format($trimCost, 2)) . '</p>'
            . '<p style="font-size:1.2em;">Grand Total: <strong>' . esc_html($currencySymbol . number_format($grandTotal, 2)) . '</strong></p>';

        $addToQuoteItems = [];
        if ($cabinetsCost > 0) {
            $addToQuoteItems[] = [
                'tag'  => 'Cabinet Refinishing (',
                'desc' => 'Cabinet Refinishing (' . number_format($cabDoors, 0) . ' doors/drawers, ' . number_format($cabBoxes, 0) . ' boxes)',
                'cost' => (int) round($cabinetsCost),
            ];
        }
        if ($intDoorsCost > 0) {
            $addToQuoteItems[] = [
                'tag'  => 'Interior Door Painting (',
                'desc' => 'Interior Door Painting (' . number_format($intDoors, 0) . ' doors)',
                'cost' => (int) round($intDoorsCost),
            ];
        }
        if ($trimCost > 0) {
            $addToQuoteItems[] = [
                'tag'  => 'Trim & Molding (',
                'desc' => 'Trim & Molding (' . number_format($trimLength, 0) . ' ' . $lengthUnitLinear . ')',
                'cost' => (int) round($trimCost),
            ];
        }
        if (!empty($addToQuoteItems)) {
            $resultsHtml .= '<button type="button" class="button paintops-calc-items-holder" data-calc-items="' . esc_attr(wp_json_encode($addToQuoteItems)) . '" onclick="paintopsAddAllCalcToQuote()" style="margin-top:14px;padding:10px 18px;background-color:#16a34a;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;width:100%;">Add These Numbers to Quote</button>'
                . '<p style="margin-top:8px;font-size:12px;color:#777;">Fills up to 3 empty itemized Supply Cost fields below (or updates its own if already added) and refreshes Quote Total, review before saving.</p>';
        }
        $resultsHtml .= '</div>';
    }

    $html = paintops_calc_modal_open(
        'paintopsCabinetPanel',
        'Use The Cabinet & Trim Calculator',
        'Cabinets, doors &amp; trim priced per unit',
        'Cabinet & Trim Calculator',
        $shouldAutoOpen
    )
        . '<div class="calc-section" style="background:var(--po-card-bg,#fff);border:1px solid var(--po-card-border,#ddd);border-radius:8px;padding:16px 20px 20px;margin-bottom:16px;">'
        . '<h3 style="margin:0 0 12px;font-size:15px;text-transform:uppercase;letter-spacing:0.4px;color:var(--po-heading,#2563eb);">Cabinets</h3>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Doors &amp; Drawers (count)<input form="paintopsCalcForm" name="ct_cab_doors" type="number" step="1" min="0" value="' . esc_attr((string) $cabDoors) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Rate per Door/Drawer (' . esc_html($currencySymbol) . ')<input form="paintopsCalcForm" name="ct_cab_doors_rate" type="number" step="0.01" value="' . esc_attr((string) $cabDoorsRate) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Boxes/Frames (count)<input form="paintopsCalcForm" name="ct_cab_boxes" type="number" step="1" min="0" value="' . esc_attr((string) $cabBoxes) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Rate per Box/Frame (' . esc_html($currencySymbol) . ')<input form="paintopsCalcForm" name="ct_cab_boxes_rate" type="number" step="0.01" value="' . esc_attr((string) $cabBoxesRate) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '</div>'
        . '<div class="calc-section" style="background:var(--po-card-bg,#fff);border:1px solid var(--po-card-border,#ddd);border-radius:8px;padding:16px 20px 20px;margin-bottom:16px;">'
        . '<h3 style="margin:0 0 12px;font-size:15px;text-transform:uppercase;letter-spacing:0.4px;color:var(--po-heading,#2563eb);">Interior Doors</h3>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Doors (count)<input form="paintopsCalcForm" name="ct_int_doors" type="number" step="1" min="0" value="' . esc_attr((string) $intDoors) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Rate per Door (' . esc_html($currencySymbol) . ')<input form="paintopsCalcForm" name="ct_int_doors_rate" type="number" step="0.01" value="' . esc_attr((string) $intDoorsRate) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '</div>'
        . '<div class="calc-section" style="background:var(--po-card-bg,#fff);border:1px solid var(--po-card-border,#ddd);border-radius:8px;padding:16px 20px 20px;margin-bottom:16px;">'
        . '<h3 style="margin:0 0 12px;font-size:15px;text-transform:uppercase;letter-spacing:0.4px;color:var(--po-heading,#2563eb);">Trim &amp; Molding</h3>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Length (' . esc_html($lengthUnitLinear) . ')<input form="paintopsCalcForm" name="ct_trim_length" type="number" step="any" value="' . esc_attr((string) $trimLength) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Rate per ' . esc_html($rateUnitLinear) . ' (' . esc_html($currencySymbol) . ')<input form="paintopsCalcForm" name="ct_trim_rate" type="number" step="0.01" value="' . esc_attr((string) $trimRate) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '</div>'
        . '<button type="submit" form="paintopsCalcForm" name="calc_active_panel" value="paintopsCabinetPanel" class="button" style="margin-top:4px;padding:10px 18px;background-color:#2563eb;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;">Calculate</button>'
        . $resultsHtml
        . paintops_calc_modal_close();

    return $html;
}

/**
 * Labor days & crew cost estimator: given a job's paintable area, a
 * production-rate assumption (area covered per painter per day), and
 * crew size, suggests how many calendar days the job will take and a
 * matching Total Labor Cost, an alternate, size-driven way to arrive
 * at the same number estimates.php already tries to prefill from a
 * job's assigned crew/days (see $totalLaborSuggested in estimates.php),
 * useful before an actual crew has been assigned to the job yet.
 * Defaults its daily rate from Business Settings > Pricing Rules >
 * Labor rate per day, the same source $totalLaborSuggested itself uses.
 */
function paintops_render_labor_days_calculator(array $context = []): string {
    $currencySymbol = paintops_currency_symbol();
    $metric = paintops_uses_metric();
    $areaUnitLower = $metric ? 'sq meters' : 'sq ft';
    // 'Labor'/'Labour', see paintops_spelling() in paintops.php.
    $laborWord = paintops_spelling('Labor', 'Labour');

    $pricing = function_exists('paintops_pricing_rules') ? paintops_pricing_rules() : [];
    $defaultDailyRate = isset($pricing['labor_rate_per_day']) ? (float) $pricing['labor_rate_per_day'] : 750;

    $defaults = [
        'ld_area'            => $metric ? 140 : 1500,
        'ld_production_rate' => $metric ? 28  : 300, // area per painter per day
        'ld_crew_size'       => 2,
        'ld_daily_rate'      => $defaultDailyRate,
    ];

    $formSubmitted  = paintops_calc_form_submitted();
    $showResults    = in_array('paintopsLaborPanel', paintops_calc_used_panels(), true);
    $shouldAutoOpen = paintops_calc_active_panel() === 'paintopsLaborPanel';
    $val = function (string $key) use ($defaults, $formSubmitted) {
        if (!$formSubmitted || !paintops_calc_nonce_ok() || !isset($_GET[$key])) {
            return $defaults[$key];
        }
        // Nonce verified on the line above (paintops_calc_nonce_ok() checks
        // po_calc_nonce against the paintops_calc_state action) before any
        // request value is read.
        return sanitize_text_field(wp_unslash($_GET[$key]));
    };

    $area = (float) $val('ld_area');
    $productionRate = max(0.01, (float) $val('ld_production_rate'));
    $crewSize = max(1, (int) $val('ld_crew_size'));
    $dailyRate = (float) $val('ld_daily_rate');

    $resultsHtml = '';
    if ($showResults) {
        $painterDays = $area / $productionRate;
        $estimatedDays = max(1, (int) ceil($painterDays / $crewSize));
        $laborCost = $estimatedDays * $crewSize * $dailyRate;

        $resultsHtml = '<div class="results paintops-calc-results" id="paintopsLaborResults">'
            . '<p>Estimated ' . esc_html($laborWord) . ': ' . esc_html(number_format($painterDays, 1)) . ' painter-days of work</p>'
            . '<p>With a crew of ' . esc_html((string) $crewSize) . ': ~' . esc_html((string) $estimatedDays) . ' day' . ($estimatedDays === 1 ? '' : 's') . '</p>'
            . '<p style="font-size:1.2em;">Suggested Total ' . esc_html($laborWord) . ' Cost: <strong>' . esc_html($currencySymbol . number_format($laborCost, 2)) . '</strong></p>'
            . '<p style="font-size:12px;color:#777;">' . esc_html((string) $crewSize) . ' painter(s) &times; ' . esc_html((string) $estimatedDays) . ' day(s) &times; ' . esc_html($currencySymbol . number_format($dailyRate, 2)) . '/day</p>';

        if ($laborCost > 0) {
            // 'tag' has to be a fixed prefix (not something like "N
            // Painter(s)..." where N changes) so paintopsAddCalcToQuote()
            // can recognize its own previous entry on a re-click, see
            // paintops_calc_add_to_quote_script() for the matching logic.
            $addToQuoteItems = [[
                'tag'       => $laborWord . ' Cost (',
                'desc'      => $laborWord . ' Cost (' . $crewSize . ' Painter(s), ' . $estimatedDays . ' Day(s))',
                'cost'      => (int) round($laborCost),
                // Slot 1 is always pre-filled by estimates.php with a
                // default crew guess ("N Workers On-Site" / $750) before
                // any calculator runs, that default is neither empty
                // nor tag-matched, so without forceSlot this would be
                // skipped as "already occupied" and the stale default
                // would never get overwritten. Slot 1 IS the Total Labor
                // line, so always claiming it here is correct.
                'forceSlot' => 1,
            ]];
            $resultsHtml .= '<button type="button" class="button paintops-calc-items-holder" data-calc-items="' . esc_attr(wp_json_encode($addToQuoteItems)) . '" onclick="paintopsAddAllCalcToQuote()" style="margin-top:14px;padding:10px 18px;background-color:#16a34a;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;width:100%;">Add These Numbers to Quote</button>'
                . '<p style="margin-top:8px;font-size:12px;color:#777;">Replaces the Total ' . esc_html($laborWord) . ' Description &amp; Cost field below and refreshes Quote Total, review before saving.</p>';
        }
        $resultsHtml .= '</div>';
    }

    $html = paintops_calc_modal_open(
        'paintopsLaborPanel',
        'Use The ' . esc_html($laborWord) . ' Days Calculator',
        'Estimate crew days &amp; ' . esc_html(strtolower($laborWord)) . ' cost from job size',
        esc_html($laborWord) . ' Days & Crew Cost Calculator',
        $shouldAutoOpen
    )
        . '<label style="display:block;margin-top:10px;font-size:13px;">Paintable Area (' . esc_html($areaUnitLower) . ')<input form="paintopsCalcForm" name="ld_area" type="number" step="any" value="' . esc_attr((string) $area) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Production Rate (' . esc_html($areaUnitLower) . ' per painter per day)<input form="paintopsCalcForm" name="ld_production_rate" type="number" step="any" value="' . esc_attr((string) $productionRate) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Crew Size (painters)<input form="paintopsCalcForm" name="ld_crew_size" type="number" step="1" min="1" value="' . esc_attr((string) $crewSize) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Daily Rate per Painter (' . esc_html($currencySymbol) . ')<input form="paintopsCalcForm" name="ld_daily_rate" type="number" step="0.01" value="' . esc_attr((string) $dailyRate) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<button type="submit" form="paintopsCalcForm" name="calc_active_panel" value="paintopsLaborPanel" class="button" style="margin-top:14px;padding:10px 18px;background-color:#2563eb;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;">Calculate</button>'
        . $resultsHtml
        . paintops_calc_modal_close();

    return $html;
}

/**
 * Deck/fence staining calculator: prices deck staining by area and
 * fence staining by length x height, with an optional pressure-washing
 * prep line, a different pricing model (and usually a different
 * product/rate) than wall paint, so kept as its own calculator rather
 * than folded into paint_sqft. Both the Deck and Fence sections are
 * always shown (unlike paint_sqft's interior/exterior toggle) since a
 * job could plausibly need either or both, leaving one at 0 just
 * drops it out of the total and results.
 */
function paintops_render_deck_fence_calculator(array $context = []): string {
    $currencySymbol = paintops_currency_symbol();
    $metric = paintops_uses_metric();
    $areaUnitLower = $metric ? 'sq meters' : 'sq ft';
    $lengthUnit = $metric ? 'meters' : 'ft';
    $lengthUnitLinear = $metric ? 'linear meters' : 'linear feet';
    $rateUnit = $metric ? 'sm' : 'sf';

    $defaults = [
        'df_deck_area'    => 0,
        'df_deck_rate'    => $metric ? 24.00 : 2.25,
        'df_fence_length' => 0,
        'df_fence_height' => $metric ? 1.8   : 6,
        'df_fence_rate'   => $metric ? 19.00 : 1.75,
        'df_include_prep' => 0,
        'df_prep_rate'    => $metric ? 3.75  : 0.35,
    ];

    $formSubmitted  = paintops_calc_form_submitted();
    $showResults    = in_array('paintopsDeckFencePanel', paintops_calc_used_panels(), true);
    $shouldAutoOpen = paintops_calc_active_panel() === 'paintopsDeckFencePanel';
    $val = function (string $key) use ($defaults, $formSubmitted) {
        if (!$formSubmitted || !paintops_calc_nonce_ok() || !isset($_GET[$key])) {
            return $defaults[$key];
        }
        // Nonce verified on the line above (paintops_calc_nonce_ok() checks
        // po_calc_nonce against the paintops_calc_state action) before any
        // request value is read.
        return sanitize_text_field(wp_unslash($_GET[$key]));
    };

    $deckArea = (float) $val('df_deck_area');
    $deckRate = (float) $val('df_deck_rate');
    $fenceLength = (float) $val('df_fence_length');
    $fenceHeight = (float) $val('df_fence_height');
    $fenceRate = (float) $val('df_fence_rate');
    $includePrep = ($formSubmitted && paintops_calc_nonce_ok()) ? !empty($_GET['df_include_prep']) : false;
    $prepRate = (float) $val('df_prep_rate');

    $resultsHtml = '';
    if ($showResults) {
        $deckCost = $deckArea * $deckRate;
        $fenceArea = round($fenceLength * $fenceHeight);
        $fenceCost = $fenceArea * $fenceRate;
        $prepCost = $includePrep ? ($deckArea + $fenceArea) * $prepRate : 0;
        $grandTotal = $deckCost + $fenceCost + $prepCost;

        $resultsHtml = '<div class="results paintops-calc-results" id="paintopsDeckFenceResults">';
        if ($deckArea > 0) {
            $resultsHtml .= '<p>Deck Staining (' . esc_html(number_format($deckArea, 0)) . ' ' . esc_html($areaUnitLower) . '): ' . esc_html($currencySymbol . number_format($deckCost, 2)) . '</p>';
        }
        if ($fenceLength > 0) {
            $resultsHtml .= '<p>Fence Area: ' . esc_html(number_format($fenceArea, 0)) . ' ' . esc_html($areaUnitLower) . '</p>'
                . '<p>Fence Staining: ' . esc_html($currencySymbol . number_format($fenceCost, 2)) . '</p>';
        }
        if ($includePrep) {
            $resultsHtml .= '<p>Pressure Washing Prep: ' . esc_html($currencySymbol . number_format($prepCost, 2)) . '</p>';
        }
        $resultsHtml .= '<p style="font-size:1.2em;">Grand Total: <strong>' . esc_html($currencySymbol . number_format($grandTotal, 2)) . '</strong></p>';

        $addToQuoteItems = [];
        if ($deckCost > 0) {
            $addToQuoteItems[] = [
                'tag'  => 'Deck Staining (',
                'desc' => 'Deck Staining (' . number_format($deckArea, 0) . ' ' . $areaUnitLower . ')',
                'cost' => (int) round($deckCost),
            ];
        }
        if ($fenceCost > 0) {
            $addToQuoteItems[] = [
                'tag'  => 'Fence Staining (',
                'desc' => 'Fence Staining (' . number_format($fenceLength, 0) . ' ' . $lengthUnitLinear . ')',
                'cost' => (int) round($fenceCost),
            ];
        }
        if ($prepCost > 0) {
            $addToQuoteItems[] = [
                'tag'  => 'Pressure Washing Prep',
                'desc' => 'Pressure Washing Prep',
                'cost' => (int) round($prepCost),
            ];
        }
        if (!empty($addToQuoteItems)) {
            $resultsHtml .= '<button type="button" class="button paintops-calc-items-holder" data-calc-items="' . esc_attr(wp_json_encode($addToQuoteItems)) . '" onclick="paintopsAddAllCalcToQuote()" style="margin-top:14px;padding:10px 18px;background-color:#16a34a;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;width:100%;">Add These Numbers to Quote</button>'
                . '<p style="margin-top:8px;font-size:12px;color:#777;">Fills up to 3 empty itemized Supply Cost fields below (or updates its own if already added) and refreshes Quote Total, review before saving.</p>';
        }
        $resultsHtml .= '</div>';
    }

    $html = paintops_calc_modal_open(
        'paintopsDeckFencePanel',
        'Use The Deck/Fence Staining Calculator',
        'Deck &amp; fence staining, ' . esc_html($areaUnitLower) . '-based',
        'Deck/Fence Staining Calculator',
        $shouldAutoOpen
    )
        . '<div class="calc-section" style="background:var(--po-card-bg,#fff);border:1px solid var(--po-card-border,#ddd);border-radius:8px;padding:16px 20px 20px;margin-bottom:16px;">'
        . '<h3 style="margin:0 0 12px;font-size:15px;text-transform:uppercase;letter-spacing:0.4px;color:var(--po-heading,#2563eb);">Deck</h3>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Deck Area (' . esc_html($areaUnitLower) . ')<input form="paintopsCalcForm" name="df_deck_area" type="number" step="any" value="' . esc_attr((string) $deckArea) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Rate (' . esc_html($currencySymbol) . '/' . esc_html($rateUnit) . ')<input form="paintopsCalcForm" name="df_deck_rate" type="number" step="0.01" value="' . esc_attr((string) $deckRate) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '</div>'
        . '<div class="calc-section" style="background:var(--po-card-bg,#fff);border:1px solid var(--po-card-border,#ddd);border-radius:8px;padding:16px 20px 20px;margin-bottom:16px;">'
        . '<h3 style="margin:0 0 12px;font-size:15px;text-transform:uppercase;letter-spacing:0.4px;color:var(--po-heading,#2563eb);">Fence</h3>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Fence Length (' . esc_html($lengthUnitLinear) . ')<input form="paintopsCalcForm" name="df_fence_length" type="number" step="any" value="' . esc_attr((string) $fenceLength) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Fence Height (' . esc_html($lengthUnit) . ')<input form="paintopsCalcForm" name="df_fence_height" type="number" step="any" value="' . esc_attr((string) $fenceHeight) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Rate (' . esc_html($currencySymbol) . '/' . esc_html($rateUnit) . ')<input form="paintopsCalcForm" name="df_fence_rate" type="number" step="0.01" value="' . esc_attr((string) $fenceRate) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '</div>'
        . '<div class="calc-section" style="background:var(--po-card-bg,#fff);border:1px solid var(--po-card-border,#ddd);border-radius:8px;padding:16px 20px 20px;margin-bottom:16px;">'
        . '<h3 style="margin:0 0 12px;font-size:15px;text-transform:uppercase;letter-spacing:0.4px;color:var(--po-heading,#2563eb);">Prep</h3>'
        . '<label style="display:flex;align-items:center;gap:6px;font-size:14px;margin-top:10px;"><input type="checkbox" form="paintopsCalcForm" name="df_include_prep" value="1"' . checked($includePrep, true, false) . ' style="width:auto;margin-top:0;"> Include pressure washing prep</label>'
        . '<label style="display:block;margin-top:10px;font-size:13px;">Prep Rate (' . esc_html($currencySymbol) . '/' . esc_html($rateUnit) . ')<input form="paintopsCalcForm" name="df_prep_rate" type="number" step="0.01" value="' . esc_attr((string) $prepRate) . '" style="padding:8px;margin-top:4px;width:100%;box-sizing:border-box;border:1px solid var(--po-input-border,#ccd0d4);border-radius:4px;"></label>'
        . '</div>'
        . '<button type="submit" form="paintopsCalcForm" name="calc_active_panel" value="paintopsDeckFencePanel" class="button" style="margin-top:4px;padding:10px 18px;background-color:#2563eb;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;">Calculate</button>'
        . $resultsHtml
        . paintops_calc_modal_close();

    return $html;
}

/**
 * Price List quick-reference popup, shown as a link on the New/Edit
 * Quote screen. Deliberately just a reference table (not another
 * calculator), see paintops_price_list() in paintops.php, managed
 * under PaintOps > Business, seeded with rough US national-average
 * painting costs. Still uses the window.open()+document.write() popup
 * pattern paintops_render_paint_sqft_calculator() above was just moved
 * away from, this one's read-only (no results to compute, nothing to
 * submit), so it doesn't carry the same document.write-gets-silently-
 * dropped risk, but if it's ever reported as showing a blank popup, the
 * same inline-panel rewrite applies here too.
 */
function paintops_render_price_list_popup_link(): string {
    if (!function_exists('paintops_price_list')) {
        return '';
    }

    $items = paintops_price_list();
    if (empty($items)) {
        return '';
    }

    $units = function_exists('paintops_price_list_units') ? paintops_price_list_units() : [];
    $rows = '';
    $row_index = 0;
    foreach ($items as $item) {
        $label = esc_html($item['label'] ?? '');
        $unit_label = esc_html($units[$item['unit'] ?? 'flat'] ?? '');
        $price = number_format((float) ($item['price'] ?? 0), 2);
        $note = esc_html($item['note'] ?? '');
        $row_bg = ($row_index % 2 === 1) ? 'background:#fafafa;' : '';
        $rows .= "<tr style='{$row_bg}'><td style='padding:8px 12px;border-bottom:1px solid #eee;'>{$label}</td>"
            . "<td style='padding:8px 12px;border-bottom:1px solid #eee;white-space:nowrap;font-weight:600;color:#2563eb;'>" . esc_html(paintops_currency_symbol()) . "{$price} <span style='font-weight:400;color:#555;'>{$unit_label}</span></td>"
            . "<td style='padding:8px 12px;border-bottom:1px solid #eee;color:#777;font-size:12px;'>{$note}</td></tr>";
        $row_index++;
    }

    $table_html = "<div style='border:1px solid var(--po-card-border,#ddd);border-radius:8px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.06);'>"
        . "<table style='width:100%;border-collapse:collapse;font-family:sans-serif;'>"
        . "<thead><tr style='text-align:left;background:#2563eb;color:#fff;'>"
        . "<th style='padding:10px 12px;font-weight:600;'>Item</th><th style='padding:10px 12px;font-weight:600;'>Reference Price</th><th style='padding:10px 12px;font-weight:600;'>Note</th>"
        . "</tr></thead><tbody>{$rows}</tbody></table></div>";

    // openPaintopsPriceList() (the window.open()+document.write() popup
    // builder, including the popup document's own inline <style>, which
    // lives in a JS string there) now ships as a bundled file through
    // the scripts API (assets/js/trade-calc-price-list.js, printed with
    // the footer scripts, fine for an onclick-referenced function); the
    // one dynamic value, the rendered table HTML, rides along as a JSON
    // config fragment attached before it.
    paintops_enqueue_script('trade-calc-price-list', 'var paintopsPriceListCfg = ' . wp_json_encode(['tableHtml' => $table_html]) . ';');

    ob_start();
    ?>
<a class="paintops-tool-card" onclick="openPaintopsPriceList()">
    <span class="tool-card-title">Price List (Reference)</span>
    <span class="tool-card-sub">Quick lookup while quoting</span>
</a>
    <?php
    return (string) ob_get_clean();
}
