<?php
/**
 * Pixel-parity PDF generation for the 4 onboarding documents (HR Information
 * Form, Form I-9, Phase A, Phase B).
 *
 * Design goal: the PDF must be the SAME template the portal shows on screen,
 * populated with the saved values — never a "Field: Value" database dump.
 *
 * To guarantee the PDF can never visually drift from the portal (the #1
 * risk with any "recreate the form in a second place" approach), this file
 * does NOT re-implement the form markup or CSS. Instead it extracts them
 * directly, at render time, from the live app.php source — the exact same
 * FORMS object, PHASES order, and <style> block the browser uses — then
 * fills in the saved field values and hands the result to wkhtmltopdf.
 * Any future edit to a form's HTML/CSS in app.php is automatically
 * reflected in the PDF with no code change needed here.
 *
 * Requires the `wkhtmltopdf` binary on the server (renders real HTML/CSS to
 * PDF, unlike simple text-drawing PDF writers). If it isn't installed,
 * onb_wkhtmltopdf() returns false and the caller should fall back /surface
 * a clear error rather than silently producing a broken file.
 */

// ---------------------------------------------------------------------
// Extraction helpers — read the single source of truth out of app.php
// ---------------------------------------------------------------------

function onb_app_php_source(): string
{
    static $src = null;
    if ($src === null) {
        $src = file_get_contents(__DIR__ . '/../Views/onboarding/app.php');
    }
    return $src;
}

/** Extracts a `const NAME = {...};` JS object literal and JSON-decodes it. */
function onb_extract_js_object(string $varName): array
{
    $src = onb_app_php_source();
    if (!preg_match('/const\s+' . preg_quote($varName, '/') . '\s*=\s*(\{.*?\});/s', $src, $m)) {
        return [];
    }
    $raw = $m[1];
    $decoded = json_decode($raw, true);
    if (!is_array($decoded)) {
        // Not directly valid JSON (e.g. PHASES uses bare keys like `A:`) —
        // quote bare JS identifier keys and retry.
        $fixed = preg_replace('/([{,]\s*)([A-Za-z_][A-Za-z0-9_]*)\s*:/', '$1"$2":', $raw);
        $decoded = json_decode($fixed, true);
    }
    return is_array($decoded) ? $decoded : [];
}

/** Returns the FORMS object: formKey => ['title'=>, 'body'=>html]. */
function onb_forms(): array
{
    static $forms = null;
    if ($forms === null) $forms = onb_extract_js_object('FORMS');
    return $forms;
}

/** Returns the PHASES object: 'A' => [formKey,...], 'B' => [formKey,...]. */
function onb_phases(): array
{
    static $phases = null;
    if ($phases === null) $phases = onb_extract_js_object('PHASES');
    return $phases;
}

/** Extracts the exact <style>...</style> block the portal uses. */
function onb_style_block(): string
{
    static $css = null;
    if ($css === null) {
        $src = onb_app_php_source();
        $css = preg_match('/<style>(.*?)<\/style>/s', $src, $m) ? $m[1] : '';
    }
    return $css;
}

// ---------------------------------------------------------------------
// Value injection — fill the extracted HTML with saved data, read-only
// ---------------------------------------------------------------------

/**
 * Takes a form's raw body HTML (from FORMS[key]['body']) and the flat
 * saved field values (name="f[KEY]" => value) and returns HTML with every
 * input/select/textarea populated and locked (no re-implementation of the
 * form — the same markup, just filled in and frozen for print).
 */
function onb_fill_form_html(string $bodyHtml, array $values): string
{
    if (trim($bodyHtml) === '') return '';
    $bodyHtml = onb_strip_emoji($bodyHtml);
    if (!class_exists('DOMDocument') || !class_exists('DOMXPath')) {
        // php-xml isn't installed on this server — return the form
        // unfilled rather than crash; the caller's wkhtmltopdf step will
        // still fail gracefully and fall back to the text-based renderer.
        return $bodyHtml;
    }
    $doc = new DOMDocument();
    $doc->substituteEntities = false;
    libxml_use_internal_errors(true);
    // Wrap so DOMDocument treats it as a full (fragment-safe) document.
    $doc->loadHTML('<?xml encoding="utf-8"?><div id="__onb_root">' . $bodyHtml . '</div>', LIBXML_NOWARNING | LIBXML_NOERROR);
    libxml_clear_errors();

    $xpath = new DOMXPath($doc);

    // Text-like inputs: fill in the value attribute.
    foreach ($xpath->query('//input[@name]') as $el) {
        /** @var DOMElement $el */
        $name = onb_extract_f_key($el->getAttribute('name'));
        if ($name === null) continue;
        $type = $el->getAttribute('type') ?: 'text';
        if ($type === 'checkbox' || $type === 'radio') continue; // handled in the pass below
        if (isset($values[$name]) && $values[$name] !== '' && is_scalar($values[$name])) {
            $el->setAttribute('value', onb_print_form_value($name, (string)$values[$name]));
        }
        // dompdf silently renders no value text at all for <input> with
        // no explicit type attribute — force type="text" so this can
        // never happen regardless of how the source markup was written.
        if ($el->getAttribute('type') === '') {
            $el->setAttribute('type', 'text');
        }
    }

    // Checkboxes & radios (with or without a name): replace the native
    // <input> widget with a plain static box/dot rendered as ordinary
    // content, not a form widget — see comment above the function.
    $checkboxNodes = [];
    foreach ($xpath->query('//input[@type="checkbox" or @type="radio"]') as $el) { $checkboxNodes[] = $el; }
    foreach ($checkboxNodes as $el) {
        /** @var DOMElement $el */
        $type = $el->getAttribute('type');
        $rawName = $el->getAttribute('name');
        $name = $rawName !== '' ? onb_extract_f_key($rawName) : null;
        if ($name !== null) {
            $saved = $values[$name] ?? null;
            if ($type === 'checkbox') {
                $checked = $saved !== null && $saved !== '' && $saved !== '0' && $saved !== false;
            } else {
                $val = $el->getAttribute('value');
                $checked = $saved !== null && (string)$saved === (string)$val;
            }
        } else {
            // No name — a hardcoded/decorative checkbox (e.g. a form that
            // ships with a box pre-checked). Preserve whatever the source
            // markup already had.
            $checked = $el->hasAttribute('checked');
        }
        onb_replace_input_with_glyph($doc, $el, $type, $checked);
    }

    // Remaining text-like inputs (readonly/disabled styling + type lock).
    foreach ($xpath->query('//input[@name]') as $el) {
        /** @var DOMElement $el */
        $el->setAttribute('readonly', 'readonly');
        $el->setAttribute('disabled', 'disabled');
        $el->setAttribute('onclick', '');
        $el->setAttribute('onchange', '');
    }

    // Dropdowns.
    foreach ($xpath->query('//select[@name]') as $el) {
        /** @var DOMElement $el */
        $name = onb_extract_f_key($el->getAttribute('name'));
        $saved = $name !== null ? ($values[$name] ?? null) : null;
        foreach ($xpath->query('.//option', $el) as $opt) {
            /** @var DOMElement $opt */
            $optVal = $opt->getAttribute('value') !== '' ? $opt->getAttribute('value') : $opt->textContent;
            if ($saved !== null && (string)$saved === (string)$optVal) {
                $opt->setAttribute('selected', 'selected');
            } else {
                $opt->removeAttribute('selected');
            }
        }
        $el->setAttribute('disabled', 'disabled');
    }

    // Textareas.
    foreach ($xpath->query('//textarea[@name]') as $el) {
        /** @var DOMElement $el */
        $name = onb_extract_f_key($el->getAttribute('name'));
        if ($name !== null && isset($values[$name])) {
            while ($el->firstChild) $el->removeChild($el->firstChild);
            $el->appendChild($doc->createTextNode((string)$values[$name]));
        }
        $el->setAttribute('readonly', 'readonly');
    }

    $root = $doc->getElementById('__onb_root');
    $out = '';
    foreach ($root->childNodes as $child) {
        $out .= $doc->saveHTML($child);
    }
    return $out;
}

/** Formats every saved onboarding date field for the Documents PDF view. */
function onb_print_form_value(string $fieldName, string $value): string
{
    return preg_match('/(?:date|dob|exp|start)/i', $fieldName) ? onb_fmt_mdy($value) : $value;
}

/** String-based version of onb_replace_input_with_glyph, for the
 *  hand-written HR Form / I-9 Section 2 templates that build HTML via
 *  string concatenation rather than the DOM filler. */
function onb_glyph_html(bool $checked, bool $radio = false): string
{
    $base = 'display:inline-block;width:12px;height:12px;border:1px solid #64748b;'
        . 'vertical-align:middle;margin-right:5px;text-align:center;line-height:11px;'
        . 'font-size:10px;font-family:sans-serif;box-sizing:border-box;';
    if ($radio) {
        $inner = $checked ? '<span style="display:inline-block;width:6px;height:6px;background:#1e293b;border-radius:50%;margin-top:2px;"></span>' : '';
        return '<span style="' . $base . 'border-radius:50%;">' . $inner . '</span>';
    }
    return '<span style="' . $base . '">' . ($checked ? 'X' : '') . '</span>';
}

/** Strips decorative emoji/pictograph characters that dompdf's font can't
 *  render (it shows a '?' tofu box instead). Only removes icon glyphs used
 *  purely for visual flair in section headers/notices — never touches any
 *  field label, value, or substantive text. */
function onb_strip_emoji(string $s): string
{
    // Common pictograph/emoji Unicode blocks used decoratively in the forms
    // (misc symbols, dingbats, transport/map symbols, supplemental symbols,
    // and variation selectors that sometimes trail them).
    $pattern = '/[\x{1F300}-\x{1FAFF}\x{2600}-\x{27BF}\x{2190}-\x{21FF}\x{2B00}-\x{2BFF}\x{FE0F}]/u';
    $out = preg_replace($pattern, '', $s);
    $out = $out ?? $s;
    // dompdf's font also has no glyph for these math relation operators
    // (found in the Meal Period Waiver and Safety Onboarding forms) — these
    // carry real meaning, so replace with the plain-text equivalent rather
    // than stripping.
    $out = str_replace(["≤", "≥"], ["<=", ">="], $out);
    return $out;
}

/** Pulls KEY out of a name="f[KEY]" attribute string; null if not that pattern. */
function onb_extract_f_key(string $name): ?string
{
    return preg_match('/^f\[(.+)\]$/', $name, $m) ? $m[1] : null;
}

/** Replaces a checkbox/radio <input> node with a plain static box/dot
 *  <span> in the same DOM position — see comment above onb_fill_form_html. */
function onb_replace_input_with_glyph(DOMDocument $doc, DOMElement $input, string $type, bool $checked): void
{
    $span = $doc->createElement('span');
    $base = 'display:inline-block;width:12px;height:12px;border:1px solid #64748b;'
        . 'vertical-align:middle;margin-right:5px;text-align:center;line-height:11px;'
        . 'font-size:10px;font-family:sans-serif;box-sizing:border-box;';
    if ($type === 'radio') {
        $span->setAttribute('style', $base . 'border-radius:50%;');
        if ($checked) {
            $dot = $doc->createElement('span');
            $dot->setAttribute('style', 'display:inline-block;width:6px;height:6px;background:#1e293b;border-radius:50%;margin-top:2px;');
            $span->appendChild($dot);
        }
    } else {
        $span->setAttribute('style', $base);
        if ($checked) {
            $span->appendChild($doc->createTextNode('X'));
        }
    }
    $input->parentNode->replaceChild($span, $input);
}

// ---------------------------------------------------------------------
// PDF rendering
// ---------------------------------------------------------------------

/** Renders an HTML string to a PDF file. Tries dompdf (pure PHP, no
 *  external binary or exec() needed) first, then falls back to shelling
 *  out to wkhtmltopdf if dompdf isn't available for some reason. Returns
 *  true on success. */
function onb_wkhtmltopdf(string $html, string $outPath): bool
{
    $dir = dirname($outPath);
    if (!is_dir($dir)) @mkdir($dir, 0755, true);

    if (onb_render_pdf_with_dompdf($html, $outPath)) {
        return true;
    }
    return onb_render_pdf_with_wkhtmltopdf($html, $outPath);
}

/** Pure-PHP HTML->PDF rendering via the vendored dompdf library. No
 *  exec()/shell_exec()/external binary required, so this works even on
 *  hosts that disable those (a common reason wkhtmltopdf silently fails). */
function onb_render_pdf_with_dompdf(string $html, string $outPath): bool
{
    $autoload = __DIR__ . '/../../vendor_bundled/autoload.php';
    if (!is_file($autoload)) return false;
    if (!class_exists('DOMDocument')) return false; // dompdf itself needs ext-dom
    require_once $autoload;
    if (!class_exists(\Dompdf\Dompdf::class)) return false;
    try {
        $dompdf = new \Dompdf\Dompdf(['isHtml5ParserEnabled' => true, 'isRemoteEnabled' => false]);
        $dompdf->loadHtml($html);
        $dompdf->setPaper('Letter', 'portrait');
        $dompdf->render();
        $bytes = $dompdf->output();
        if ($bytes === null || $bytes === '') return false;
        file_put_contents($outPath, $bytes);
        return is_file($outPath) && filesize($outPath) > 0;
    } catch (\Throwable $e) {
        error_log('dompdf render failed: ' . $e->getMessage());
        return false;
    }
}

/** Fallback: shells out to the wkhtmltopdf binary if it's installed and
 *  exec() isn't disabled. Kept as a secondary path only — many hosts
 *  disable exec()/shell_exec() or don't have the binary installed, which
 *  is why this must never be the only rendering path. */
function onb_render_pdf_with_wkhtmltopdf(string $html, string $outPath): bool
{
    if (!function_exists('exec') || !function_exists('shell_exec')) return false;

    $tmpHtml = tempnam(sys_get_temp_dir(), 'onb_') . '.html';
    file_put_contents($tmpHtml, $html);

    $bin = trim((string)@shell_exec('command -v wkhtmltopdf 2>/dev/null'));
    if ($bin === '') $bin = '/usr/bin/wkhtmltopdf';
    if (!is_file($bin)) { @unlink($tmpHtml); return false; }

    $cmd = escapeshellcmd($bin)
        . ' --quiet --enable-local-file-access --page-size Letter'
        . ' --margin-top 12mm --margin-bottom 12mm --margin-left 12mm --margin-right 12mm '
        . escapeshellarg($tmpHtml) . ' ' . escapeshellarg($outPath) . ' 2>&1';
    exec($cmd, $outLines, $code);
    @unlink($tmpHtml);
    return $code === 0 && is_file($outPath) && filesize($outPath) > 0;
}

function onb_pdf_wrap_html(string $title, string $bodyHtml): string
{
    $css = onb_style_block();
    // Print overrides: static A4/Letter-friendly layout, one form starts on
    // its own page, and anything interactive-only (calendar/signature-pad
    // buttons, "Next"/"Back" wizard buttons) is hidden — the saved VALUES
    // are already baked into the markup by onb_fill_form_html(), so hiding
    // the widget chrome does not remove any information from the PDF.
    $printCss = '
      body{background:#fff;margin:0;}
      .onb-print-page{page-break-after:always;padding:6px 0;}
      .onb-print-page:last-child{page-break-after:auto;}
      .onb-print-title{font-size:16px;font-weight:800;color:var(--navy,#0f2038);margin:0 0 10px;}
      input,select,textarea{background:#fff !important;}
      input[readonly],input[disabled],select[disabled],textarea[readonly]{color:#1e293b;opacity:1;-webkit-text-fill-color:#1e293b;}
      button,.btn,.wiznav,#nextReqBtn{display:none !important;}
      .fld{display:inline-block !important;width:31%;vertical-align:top;margin:5px 1.5% 10px 0 !important;}
      .fld input[type="text"],.fld input:not([type]),.fld select,.fld textarea{width:100%;box-sizing:border-box;}
      .row,.row3-compact{display:block !important;}
    ';
    $safeTitle = htmlspecialchars($title, ENT_QUOTES);
    return '<!DOCTYPE html><html><head><meta charset="utf-8">'
        . '<style>' . $css . "\n" . $printCss . '</style>'
        . '</head><body><h1 class="onb-print-title">' . $safeTitle . '</h1>' . $bodyHtml . '</body></html>';
}

/** Small signature block matching the portal's on-screen "signed" state. */
function onb_signature_block_html(string $signerName, string $signedAt): string
{
    if ($signerName === '') return '';
    $name = htmlspecialchars($signerName, ENT_QUOTES);
    $when = $signedAt ? htmlspecialchars(onb_fmt_mdy(substr($signedAt, 0, 10)) . ' ' . substr($signedAt, 11, 5), ENT_QUOTES) : '';
    return '<div style="margin-top:10px;padding-top:8px;border-top:1px solid #e2e8f0;">'
        . '<div class="muted" style="font-size:11px;text-transform:uppercase;">Signature</div>'
        . '<div style="font-family:\'Brush Script MT\',cursive,\'Segoe Script\',cursive;font-size:24px;color:#1e293b;">' . $name . '</div>'
        . ($when ? '<div class="muted" style="font-size:11px;">Signed ' . $when . '</div>' : '')
        . '</div>';
}

/**
 * Renders Phase A or Phase B exactly as the portal wizard shows it: the
 * real FORMS[key].body markup, filled with the saved per-form field values
 * and each form's own signature, one form per page, in the portal's own
 * PHASES order.
 *
 * $formsData: ordered list of ['key'=>, 'title'=>(optional override),
 * 'fields'=>[...], 'signedBy'=>, 'signedAt'=>] — this is exactly the shape
 * the wizard already sends to submitPhase (see app.php submitApplicant()).
 */
function onb_render_wizard_pdf(string $docTitle, array $formsData, string $outPath): bool
{
    try {
        $forms = onb_forms();
        $sections = '';
        foreach ($formsData as $entry) {
            $key = $entry['key'] ?? '';
            $def = $forms[$key] ?? null;
            $title = $entry['title'] ?? ($def['title'] ?? $key);
            $filled = $def ? onb_fill_form_html($def['body'], $entry['fields'] ?? []) : '<p><em>Form not found: ' . htmlspecialchars($key) . '</em></p>';
            $sig = onb_signature_block_html((string)($entry['signedBy'] ?? ''), (string)($entry['signedAt'] ?? ''));
            $sections .= '<div class="onb-print-page"><div class="card">'
                . '<h2 style="margin-top:0;">' . htmlspecialchars($title, ENT_QUOTES) . '</h2>'
                . $filled . $sig
                . '</div></div>';
        }
        $html = onb_pdf_wrap_html($docTitle, $sections);
        return onb_wkhtmltopdf($html, $outPath);
    } catch (\Throwable $e) {
        error_log('onb_render_wizard_pdf failed, falling back: ' . $e->getMessage());
        return false;
    }
}

/**
 * Renders Form I-9 exactly as the portal shows it in two parts: Section 1
 * (the real FORMS['i9'].body the applicant filled) and Section 2 (a
 * template mirroring the HR "I-9 Section 2 — Employer Verification"
 * screen — that screen is HR-only chrome outside the FORMS object, so its
 * layout is reproduced here using the identical CSS classes).
 */
function onb_render_i9_pdf(array $hire, array $section1Fields, array $section2, string $signerName, string $outPath): bool
{
    try {
    $forms = onb_forms();
    $def = $forms['i9'] ?? null;
    $section1Html = $def ? onb_fill_form_html($def['body'], $section1Fields) : '';

    $esc = fn($v) => htmlspecialchars((string)($v ?? ''), ENT_QUOTES);
    $docPath = $section2['doc_path'] ?? 'BC';
    $isA = $docPath === 'A';

    $listA = '<div class="row"><div class="fld"><label>Document title</label><input type="text" readonly disabled value="' . $esc($isA ? ($section2['listA_title'] ?? '') : '') . '"></div><div class="fld"><label>Issuing authority</label><input type="text" readonly disabled value="' . $esc($isA ? ($section2['listA_auth'] ?? '') : '') . '"></div></div>'
        . '<div class="row"><div class="fld"><label>Document number</label><input type="text" readonly disabled value="' . $esc($isA ? ($section2['listA_num'] ?? '') : '') . '"></div><div class="fld"><label>Expiration date (if any)</label><input type="text" readonly disabled value="' . $esc($isA ? onb_fmt_mdy($section2['listA_exp'] ?? '') : '') . '"></div></div>';

    $listBC = '<div class="sub">List B — identity</div>'
        . '<div class="row"><div class="fld"><label>Document title</label><input type="text" readonly disabled value="' . $esc(!$isA ? ($section2['listB_title'] ?? '') : '') . '"></div><div class="fld"><label>Issuing authority</label><input type="text" readonly disabled value="' . $esc(!$isA ? ($section2['listB_auth'] ?? '') : '') . '"></div></div>'
        . '<div class="row"><div class="fld"><label>Document number</label><input type="text" readonly disabled value="' . $esc(!$isA ? ($section2['listB_num'] ?? '') : '') . '"></div><div class="fld"><label>Expiration date (if any)</label><input type="text" readonly disabled value="' . $esc(!$isA ? onb_fmt_mdy($section2['listB_exp'] ?? '') : '') . '"></div></div>'
        . '<div class="sub">List C — employment authorization</div>'
        . '<div class="row"><div class="fld"><label>Document title</label><input type="text" readonly disabled value="' . $esc(!$isA ? ($section2['listC_title'] ?? '') : '') . '"></div><div class="fld"><label>Issuing authority</label><input type="text" readonly disabled value="' . $esc(!$isA ? ($section2['listC_auth'] ?? '') : '') . '"></div></div>'
        . '<div class="row"><div class="fld"><label>Document number</label><input type="text" readonly disabled value="' . $esc(!$isA ? ($section2['listC_num'] ?? '') : '') . '"></div><div class="fld"><label>Expiration date (if any)</label><input type="text" readonly disabled value="' . $esc(!$isA ? onb_fmt_mdy($section2['listC_exp'] ?? '') : '') . '"></div></div>';

    $rep = $esc($section2['employer_rep'] ?? '');
    $section2Html = '<div class="sh">Documentation the employee presented</div>'
        . '<div class="rg"><label>' . onb_glyph_html($isA, true) . ' List A (identity <b>and</b> work authorization)</label>'
        . '<label>' . onb_glyph_html(!$isA, true) . ' List B + List C</label></div>'
        . ($isA ? $listA : $listBC)
        . '<label style="margin-top:8px;display:block;">Additional information (optional)</label><input type="text" readonly disabled value="' . $esc($section2['additional_info'] ?? '') . '">'
        . '<div class="sh">Employer / Company <span style="font-weight:400;text-transform:none;">— used on all government forms (W-4, DE 4, I-9, 8850)</span></div>'
        . '<div class="row"><div class="fld"><label>First day of employment</label><input type="text" readonly disabled value="' . $esc(onb_fmt_mdy($section2['first_day'] ?? $hire['hire_date'] ?? '')) . '"></div><div class="fld"><label>Company legal name</label><input type="text" readonly disabled value="' . $esc($section2['employer_business_name'] ?? 'Hammer Head Security') . '"></div></div>'
        . '<div class="row"><div class="fld"><label>Company address</label><input type="text" readonly disabled value="' . $esc($section2['employer_address'] ?? '4551 South B Street, Stockton, CA 95206') . '"></div><div class="fld"><label>Name &amp; title of employer representative</label><input type="text" readonly disabled value="' . $rep . '"></div></div>'
        . '<div class="row"><div class="fld"><label>Federal EIN (W-4 &amp; 8850)</label><input type="text" readonly disabled value="' . $esc($section2['employer_ein'] ?? '') . '"></div><div class="fld"><label>CA Employer Payroll Tax Account # (DE 4)</label><input type="text" readonly disabled value="' . $esc($section2['ca_payroll_acct'] ?? '') . '"></div></div>'
        . '<label class="ck" style="margin-top:10px;">' . onb_glyph_html(true) . ' I have physically examined the original document(s); they appear genuine and relate to this employee.</label>'
        . onb_signature_block_html($signerName, '');

    $html = onb_pdf_wrap_html('Form I-9 — Employment Eligibility Verification',
        '<div class="onb-print-page"><div class="card"><h2 style="margin-top:0;">Section 1 — Employee Information and Attestation</h2>' . $section1Html . '</div></div>'
        . '<div class="onb-print-page"><div class="card"><h2 style="margin-top:0;">Section 2 — Employer Review and Verification</h2>' . $section2Html . '</div></div>'
    );
    return onb_wkhtmltopdf($html, $outPath);
    } catch (\Throwable $e) {
        error_log('onb_render_i9_pdf failed, falling back: ' . $e->getMessage());
        return false;
    }
}

/**
 * Renders the "HR Information Form" exactly as the portal's New Hire modal
 * (the actual on-screen form Rachel fills), instead of a database dump.
 * Field grouping/labels mirror showAdd() in app.php.
 */
function onb_render_hr_form_pdf(array $hire, string $outPath): bool
{
    try {
    $esc = fn($v) => htmlspecialchars((string)($v ?? ''), ENT_QUOTES);
    $sched = json_decode($hire['schedule_json'] ?? '{}', true) ?: [];
    $days = [['mon', 'Mon'], ['tue', 'Tue'], ['wed', 'Wed'], ['thu', 'Thu'], ['fri', 'Fri'], ['sat', 'Sat'], ['sun', 'Sun']];
    $schedRows = '';
    foreach ($days as [$k, $label]) {
        $d = $sched[$k] ?? ['on' => false, 'start' => '', 'end' => ''];
        $on = !empty($d['on']);
        $schedRows .= '<div class="row3-compact"><div class="fld"><label>' . onb_glyph_html($on) . ' ' . $label . '</label></div>'
            . '<div class="fld"><label>Start</label><input type="text" readonly disabled value="' . $esc($d['start'] ?? '') . '"></div>'
            . '<div class="fld"><label>End</label><input type="text" readonly disabled value="' . $esc($d['end'] ?? '') . '"></div></div>';
    }

    $body = '<div class="card">'
        . '<div class="hrsect" style="margin-top:0;">Employee Info</div>'
        . '<div class="row3-compact"><div class="fld"><label>First name</label><input type="text" readonly disabled value="' . $esc(($hire['first'] ?? '')) . '"></div><div class="fld"><label>Middle name</label><input type="text" readonly disabled value="' . $esc(($hire['middle'] ?? '')) . '"></div><div class="fld"><label>Last name</label><input type="text" readonly disabled value="' . $esc(($hire['last'] ?? '')) . '"></div></div>'
        . '<div class="row3-compact"><div class="fld"><label>Phone number</label><input type="text" readonly disabled value="' . $esc(($hire['phone'] ?? '')) . '"></div><div class="fld"><label>Email address</label><input type="text" readonly disabled value="' . $esc(($hire['email'] ?? '')) . '"></div><div class="fld"><label>Job title</label><input type="text" readonly disabled value="' . $esc(($hire['position'] ?? '')) . '"></div></div>'
        . '<label>Address</label><input type="text" readonly disabled value="' . $esc(($hire['address'] ?? '')) . '">'
        . '<div class="row3-compact"><div class="fld"><label>City</label><input type="text" readonly disabled value="' . $esc(($hire['city'] ?? '')) . '"></div><div class="fld"><label>State</label><input type="text" readonly disabled value="' . $esc(($hire['state'] ?? '')) . '"></div><div class="fld"><label>Zip</label><input type="text" readonly disabled value="' . $esc(($hire['zip'] ?? '')) . '"></div></div>'
        . '<div class="hrsect">Licensing</div>'
        . '<div class="row3-compact"><div class="fld"><label>BSIS Guard Card #</label><input type="text" readonly disabled value="' . $esc(($hire['guard_card'] ?? '')) . '"></div><div class="fld"><label>Guard Card Expiry</label><input type="text" readonly disabled value="' . $esc(onb_fmt_mdy($hire['guard_card_exp'] ?? '')) . '"></div><div class="fld"><label>Firearms Permit</label><input type="text" readonly disabled value="' . $esc(($hire['firearms_permit'] ?? '')) . '"></div></div>'
        . '<div class="row3-compact"><div class="fld"><label>Driver\'s License #</label><input type="text" readonly disabled value="' . $esc(($hire['dl'] ?? '')) . '"></div><div class="fld"><label>DL Expiry</label><input type="text" readonly disabled value="' . $esc(onb_fmt_mdy($hire['dl_exp'] ?? '')) . '"></div><div></div></div>'
        . '<div class="hrsect">Employment</div>'
        . '<div class="row3-compact"><div class="fld"><label>Hire type</label><input type="text" readonly disabled value="' . $esc(($hire['hire_type'] ?? '')) . '"></div><div class="fld"><label>Employment status</label><input type="text" readonly disabled value="' . $esc(($hire['emp_status'] ?? '')) . '"></div><div class="fld"><label>Date of hire</label><input type="text" readonly disabled value="' . $esc(onb_fmt_mdy($hire['hire_date'] ?? '')) . '"></div></div>'
        . '<div class="row3-compact"><div class="fld"><label>Pay rate</label><input type="text" readonly disabled value="' . $esc(($hire['pay_rate'] ?? '')) . '"></div><div class="fld"><label>Pay frequency</label><input type="text" readonly disabled value="' . $esc(($hire['pay_freq'] ?? '')) . '"></div><div class="fld"><label>Job site</label><input type="text" readonly disabled value="' . $esc(($hire['site'] ?? '')) . '"></div></div>'
        . '<div class="hrsect">Work Schedule</div>' . $schedRows
        . '<div class="hrsect">HR Representative</div>'
        . '<div class="row3-compact"><div class="fld"><label>Employer / HR representative name</label><input type="text" readonly disabled value="' . $esc(($hire['hr_rep'] ?? '')) . '"></div><div></div><div></div></div>'
        . '<div class="hrsect">Company Details</div>'
        . '<div class="row3-compact"><div class="fld"><label>Company Name</label><input type="text" readonly disabled value="' . $esc($hire['company_name'] ?? '') . '"></div><div class="fld"><label>EIN</label><input type="text" readonly disabled value="' . $esc($hire['company_ein'] ?? '') . '"></div><div class="fld"><label>State Tax ID (Entity ID)</label><input type="text" readonly disabled value="' . $esc($hire['company_state_tax_id'] ?? '') . '"></div></div>'
        . '</div>';

    $html = onb_pdf_wrap_html('HR Information Form', $body);
    return onb_wkhtmltopdf($html, $outPath);
    } catch (\Throwable $e) {
        error_log('onb_render_hr_form_pdf failed, falling back: ' . $e->getMessage());
        return false;
    }
}
