/* =============================================================================
   portal/Questionnaire-parts.jsx — field renderer, locked read-only view,
   and the shared printable-PDF builder (used by portal + admin).
   ============================================================================= */

function QField({ field: f, value, files, onChange, onFiles, invalid, readOnly }) {
  const { C } = UI;
  const labelRow = (
    <div style={{ marginBottom: 8 }}>
      <label style={{ fontFamily: C.sans, fontWeight: 600, fontSize: 13, letterSpacing: '0.02em', color: C.carbon }}>
        {f.label}{f.required && <span style={{ color: 'var(--sch-denied-pre)', marginLeft: 4 }}>*</span>}
      </label>
      {f.help && <p style={{ fontFamily: C.serif, fontSize: 13.5, lineHeight: 1.5, color: C.carbon70, margin: '5px 0 0' }}>{f.help}</p>}
    </div>
  );
  const border = invalid ? 'var(--sch-denied-pre)' : 'var(--border-rule)';
  const inputBase = { width: '100%', fontFamily: C.sans, fontSize: 14, color: C.carbon, background: readOnly ? 'transparent' : '#fff', border: `1px solid ${border}`, borderRadius: 2, padding: '11px 13px', outline: 'none', boxSizing: 'border-box' };

  // read-only rendering
  if (readOnly) {
    if (f.type === 'schooltable') return <div>{labelRow}<QSchoolTable field={f} value={value} readOnly /></div>;
    if (f.type === 'rowtable') return <div>{labelRow}<QRowTable field={f} value={value} readOnly /></div>;
    const empty = (f.type === 'file') ? !(files && files.length) : (value == null || String(value).trim() === '');
    return (
      <div>
        {labelRow}
        {f.type === 'file'
          ? <QFileList files={files} readOnly />
          : empty
            ? <div style={{ fontFamily: C.serif, fontStyle: 'italic', fontSize: 14, color: C.carbon50 }}>Not answered</div>
            : <div style={{ fontFamily: f.type === 'textarea' ? C.serif : C.sans, fontSize: 14.5, lineHeight: 1.6, color: C.carbon, whiteSpace: 'pre-wrap' }}>{f.type === 'ack' ? '✓ Acknowledged' : String(value)}</div>}
      </div>
    );
  }

  return (
    <div>
      {labelRow}
      {(f.type === 'text' || f.type === 'email' || f.type === 'number') && (
        <input type={f.type === 'number' ? 'text' : f.type} value={value || ''} onChange={e => onChange(e.target.value)} style={inputBase}
          placeholder={f.type === 'email' ? 'you@email.com' : ''} />
      )}
      {f.type === 'date' && (
        <input type="date" value={value || ''} onChange={e => onChange(e.target.value)} style={{ ...inputBase, maxWidth: 220 }} />
      )}
      {f.type === 'textarea' && (
        <textarea value={value || ''} onChange={e => onChange(e.target.value)} rows={f.rows || 6}
          style={{ ...inputBase, fontFamily: C.serif, fontSize: 15, lineHeight: 1.6, resize: 'vertical', minHeight: f.minHeight || 120 }} />
      )}
      {f.type === 'radio' && <QRadio field={f} value={value} onChange={onChange} />}
      {f.type === 'select' && (
        <select value={value || ''} onChange={e => onChange(e.target.value)} style={{ ...inputBase, maxWidth: 320, cursor: 'pointer' }}>
          <option value="">Select…</option>
          {(f.options || []).map(o => <option key={o} value={o}>{o}</option>)}
        </select>
      )}
      {f.type === 'ack' && (
        <button onClick={() => onChange(value ? '' : 'Acknowledged')} style={{ display: 'flex', alignItems: 'flex-start', gap: 11, background: 'none', border: 'none', cursor: 'pointer', padding: 0, textAlign: 'left' }}>
          <span style={{ width: 22, height: 22, flexShrink: 0, borderRadius: 4, border: `1.5px solid ${value ? C.carbon : (invalid ? 'var(--sch-denied-pre)' : C.carbon30)}`, background: value ? C.carbon : 'transparent', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', marginTop: 1 }}>{value && <Icon name="check" size={14} color="#fff" />}</span>
          <span style={{ fontFamily: C.serif, fontSize: 14.5, lineHeight: 1.5, color: C.carbon }}>{f.label}</span>
        </button>
      )}
      {f.type === 'file' && <QFileUpload field={f} files={files} onFiles={onFiles} />}
      {f.type === 'schooltable' && <QSchoolTable field={f} value={value} onChange={onChange} invalid={invalid} />}
      {f.type === 'rowtable' && <QRowTable field={f} value={value} onChange={onChange} invalid={invalid} />}
    </div>
  );
}

function QSchoolTable({ field: f, value, onChange, readOnly, invalid }) {
  const { C } = UI;
  const cols = f.columns || [];
  const selCols = cols.filter(c => c.type === 'select');
  const textCols = cols.filter(c => c.type === 'textarea');
  const rows = Array.isArray(value) ? value : [];
  const setRows = (next) => onChange(next);
  const addRow = () => setRows([...rows, {}]);
  const removeRow = (i) => setRows(rows.filter((_, n) => n !== i));
  const setCell = (i, key, v) => setRows(rows.map((r, n) => n === i ? { ...r, [key]: v } : r));

  if (readOnly) {
    if (!rows.length) return <div style={{ fontFamily: C.serif, fontStyle: 'italic', fontSize: 14, color: C.carbon50 }}>Not answered</div>;
    return (
      <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        {rows.map((row, i) => (
          <div key={i} style={{ border: `1px solid ${C.hair}`, padding: '13px 16px' }}>
            <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
              <span style={{ fontFamily: C.sans, fontWeight: 600, fontSize: 14.5, color: C.carbon }}>{row.school || '—'}</span>
              {row.round && <span style={qChip(C)}>{row.round}</span>}
              {row.scope && <span style={qChip(C)}>{row.scope}</span>}
            </div>
            {textCols.map(c => row[c.key] ? (
              <div key={c.key} style={{ marginTop: 8 }}>
                <div style={{ fontFamily: C.sans, fontSize: 9, fontWeight: 600, letterSpacing: '0.12em', textTransform: 'uppercase', color: C.carbon50, marginBottom: 3 }}>{c.label}</div>
                <div style={{ fontFamily: C.serif, fontSize: 14, lineHeight: 1.6, color: C.carbon70, whiteSpace: 'pre-wrap' }}>{row[c.key]}</div>
              </div>
            ) : null)}
          </div>
        ))}
      </div>
    );
  }

  const sel = { fontFamily: C.sans, fontSize: 13.5, color: C.carbon, background: '#fff', border: `1px solid var(--border-rule)`, borderRadius: 2, padding: '9px 10px', outline: 'none', cursor: 'pointer' };
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      {rows.map((row, i) => (
        <div key={i} style={{ border: `1px solid ${invalid && i === 0 ? 'var(--sch-denied-pre)' : C.hair}`, background: '#fbfaf7', padding: '14px 16px', position: 'relative' }}>
          <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'flex-end', paddingRight: 28 }}>
            {selCols.map(c => (
              <label key={c.key} style={{ display: 'flex', flexDirection: 'column', gap: 5, flex: c.key === 'school' ? '1 1 220px' : '0 1 ' + (c.width || 150) + 'px' }}>
                <span style={{ fontFamily: C.sans, fontSize: 9, fontWeight: 600, letterSpacing: '0.12em', textTransform: 'uppercase', color: C.carbon50 }}>{c.label}</span>
                <select value={row[c.key] || ''} onChange={e => setCell(i, c.key, e.target.value)} style={sel}>
                  <option value="">Select…</option>
                  {(c.options || []).map(o => <option key={o} value={o}>{o}</option>)}
                </select>
              </label>
            ))}
          </div>
          {textCols.map(c => (
            <label key={c.key} style={{ display: 'block', marginTop: 12 }}>
              <span style={{ fontFamily: C.sans, fontSize: 9, fontWeight: 600, letterSpacing: '0.12em', textTransform: 'uppercase', color: C.carbon50 }}>{c.label}</span>
              <textarea value={row[c.key] || ''} onChange={e => setCell(i, c.key, e.target.value)} rows={4}
                style={{ width: '100%', marginTop: 5, fontFamily: C.serif, fontSize: 15, lineHeight: 1.6, color: C.carbon, background: '#fff', border: `1px solid var(--border-rule)`, borderRadius: 2, padding: '11px 13px', outline: 'none', resize: 'vertical', minHeight: 96, boxSizing: 'border-box' }} />
            </label>
          ))}
          <button onClick={() => removeRow(i)} title="Remove school" style={{ position: 'absolute', top: 10, right: 10, background: 'none', border: 'none', cursor: 'pointer', padding: 4 }}><Icon name="x" size={15} color="var(--kh-carbon-50)" /></button>
        </div>
      ))}
      <button onClick={addRow} style={{ display: 'inline-flex', alignItems: 'center', gap: 8, alignSelf: 'flex-start', background: '#fff', border: `1px dashed ${C.carbon30}`, padding: '10px 15px', cursor: 'pointer', fontFamily: C.sans, fontSize: 11.5, fontWeight: 600, letterSpacing: '0.04em', textTransform: 'uppercase', color: C.carbon70, borderRadius: 2 }}>
        <Icon name="plus" size={14} /> Add a school
      </button>
    </div>
  );
}

function qChip(C) { return { display: 'inline-flex', alignItems: 'center', fontFamily: C.sans, fontSize: 10, fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase', color: C.carbon70, background: C.porcelain, border: `1px solid ${C.hair}`, borderRadius: 999, padding: '3px 9px' }; }

function QRowTable({ field: f, value, onChange, readOnly, invalid }) {
  const { C } = UI;
  const cols = f.columns || [];
  const rows = Array.isArray(value) ? value : [];
  const setRows = (next) => onChange(next);
  const setCell = (i, key, v) => setRows(rows.map((r, n) => n === i ? { ...r, [key]: v } : r));
  const th = { textAlign: 'left', padding: '9px 12px', fontFamily: C.sans, fontSize: 9.5, fontWeight: 600, letterSpacing: '0.1em', textTransform: 'uppercase', color: C.carbon50, whiteSpace: 'nowrap', borderBottom: `1px solid ${C.rule}` };
  const td = { padding: readOnly ? '8px 12px' : '6px 8px', borderBottom: `1px solid ${C.hair}`, verticalAlign: 'middle' };
  const cell = { width: '100%', fontFamily: C.sans, fontSize: 13.5, color: C.carbon, background: '#fff', border: `1px solid var(--border-rule)`, borderRadius: 2, padding: '8px 9px', outline: 'none', boxSizing: 'border-box' };

  if (readOnly && !rows.length) return <div style={{ fontFamily: C.serif, fontStyle: 'italic', fontSize: 14, color: C.carbon50 }}>Not answered</div>;

  return (
    <div>
      <div style={{ border: `1px solid ${invalid ? 'var(--sch-denied-pre)' : C.hair}`, overflowX: 'auto', background: '#fff' }}>
        <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 520 }}>
          <thead><tr>
            {cols.map(c => <th key={c.key} style={{ ...th, width: c.width }}>{c.label}</th>)}
            {!readOnly && <th style={{ ...th, width: 36 }}></th>}
          </tr></thead>
          <tbody>
            {rows.map((row, i) => (
              <tr key={i}>
                {cols.map(c => (
                  <td key={c.key} style={td}>
                    {readOnly
                      ? <span style={{ fontFamily: C.sans, fontSize: 13.5, color: row[c.key] ? C.carbon : C.carbon50 }}>{row[c.key] || '—'}</span>
                      : c.type === 'select'
                        ? <select value={row[c.key] || ''} onChange={e => setCell(i, c.key, e.target.value)} style={{ ...cell, cursor: 'pointer' }}><option value="">Select…</option>{(c.options || []).map(o => <option key={o} value={o}>{o}</option>)}</select>
                        : <input type={c.type === 'date' ? 'date' : 'text'} value={row[c.key] || ''} onChange={e => setCell(i, c.key, e.target.value)} placeholder={c.key === 'composite' ? 'e.g. 720' : c.key === 'sections' ? 'e.g. Q49 · V40 · IR8 · AWA5' : ''} style={cell} />}
                  </td>
                ))}
                {!readOnly && <td style={{ ...td, textAlign: 'center' }}><button onClick={() => setRows(rows.filter((_, n) => n !== i))} title="Remove" style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4 }}><Icon name="x" size={14} color="var(--kh-carbon-50)" /></button></td>}
              </tr>
            ))}
            {!rows.length && <tr><td colSpan={cols.length + 1} style={{ padding: '18px 12px', textAlign: 'center', fontFamily: C.serif, fontStyle: 'italic', fontSize: 14, color: C.carbon50 }}>No tests added yet.</td></tr>}
          </tbody>
        </table>
      </div>
      {!readOnly && (
        <button onClick={() => setRows([...rows, {}])} style={{ display: 'inline-flex', alignItems: 'center', gap: 8, marginTop: 10, background: '#fff', border: `1px dashed ${C.carbon30}`, padding: '9px 14px', cursor: 'pointer', fontFamily: C.sans, fontSize: 11.5, fontWeight: 600, letterSpacing: '0.04em', textTransform: 'uppercase', color: C.carbon70, borderRadius: 2 }}>
          <Icon name="plus" size={14} /> Add a test
        </button>
      )}
    </div>
  );
}

function QRadio({ field: f, value, onChange }) {
  const { C } = UI;
  const isOther = value != null && value !== '' && !f.options.includes(value);
  const [otherText, setOtherText] = React.useState(isOther ? value : '');
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
      {f.options.map(opt => {
        const on = value === opt;
        return (
          <button key={opt} onClick={() => onChange(opt)} style={radioRow(C)}>
            <Dot on={on} C={C} />{opt}
          </button>
        );
      })}
      {f.allowOther && (
        <div style={{ display: 'flex', alignItems: 'center', gap: 11 }}>
          <button onClick={() => onChange(otherText || 'Other')} style={{ ...radioRow(C), flex: 'none' }}><Dot on={isOther} C={C} />Other:</button>
          <input value={isOther ? value : otherText} onChange={e => { setOtherText(e.target.value); onChange(e.target.value); }}
            style={{ flex: 1, fontFamily: C.sans, fontSize: 14, color: C.carbon, border: 'none', borderBottom: `1px solid ${C.rule}`, padding: '6px 2px', outline: 'none', background: 'transparent' }} />
        </div>
      )}
    </div>
  );
}
function radioRow(C) { return { display: 'inline-flex', alignItems: 'center', gap: 10, background: 'none', border: 'none', cursor: 'pointer', padding: 0, fontFamily: C.sans, fontSize: 14, color: C.carbon, textAlign: 'left' }; }
function Dot({ on, C }) { return <span style={{ width: 18, height: 18, flexShrink: 0, borderRadius: 999, border: `1.5px solid ${on ? C.carbon : C.carbon30}`, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>{on && <span style={{ width: 9, height: 9, borderRadius: 999, background: C.carbon }} />}</span>; }

function QFileUpload({ field: f, files, onFiles }) {
  const { C } = UI;
  const list = files || [];
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  // Upload real bytes to Codata's file storage and keep only a small
  // {fileId, name, size, type} reference — the questionnaire's answers (this
  // field included) live inside consultantNotes, a single JSON blob on the
  // client record, and embedding file bytes there directly (this used to
  // base64-encode the whole file inline) bloats that save past Codata's size
  // limit and the PUT comes back 413. Display/PDF-export fetch the bytes back
  // by fileId on demand instead.
  async function add(fileList) {
    const incoming = [...fileList];
    setErr(''); setBusy(true);
    try {
      if (!(window.CodataAPI && CodataAPI.uploadDocumentFile)) throw new Error('File storage isn’t available right now.');
      const added = await Promise.all(incoming.map(async (file) => {
        const presign = await CodataAPI.uploadDocumentFile(file);
        return { name: file.name, size: file.size, type: file.type || '', fileId: presign.id };
      }));
      onFiles(f.multiple ? [...list, ...added] : added.slice(0, 1));
    } catch (e) {
      console.error('Questionnaire file upload failed:', e);
      setErr(e.message || 'That file could not be uploaded. Please try again.');
    } finally {
      setBusy(false);
    }
  }
  return (
    <div>
      <QFileList files={list} onRemove={(i) => onFiles(list.filter((_, n) => n !== i))} />
      <label style={{ display: 'inline-flex', alignItems: 'center', gap: 9, marginTop: list.length ? 10 : 0, border: `1.5px dashed ${C.carbon30}`, padding: '11px 16px', cursor: busy ? 'default' : 'pointer', borderRadius: 2, opacity: busy ? 0.6 : 1 }}>
        <input type="file" multiple={!!f.multiple} disabled={busy} style={{ display: 'none' }} onChange={e => { if (e.target.files.length) add(e.target.files); e.target.value = ''; }} />
        <Icon name="upload" size={16} color="var(--kh-carbon-70)" />
        <span style={{ fontFamily: C.sans, fontSize: 12.5, fontWeight: 500, color: C.carbon }}>{busy ? 'Uploading…' : (list.length && !f.multiple ? 'Replace file' : f.multiple ? 'Add file(s)' : 'Upload file')}</span>
      </label>
      {err && <div style={{ marginTop: 8, fontFamily: C.sans, fontSize: 12, color: 'var(--sch-denied-pre)' }}>{err}</div>}
    </div>
  );
}
function QFileList({ files, onRemove, readOnly }) {
  const { C } = UI;
  if (!files || !files.length) return readOnly ? <div style={{ fontFamily: C.serif, fontStyle: 'italic', fontSize: 14, color: C.carbon50 }}>No file uploaded</div> : null;
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
      {files.map((f, i) => (
        <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, border: `1px solid ${C.hair}`, background: C.porcelain, padding: '9px 12px' }}>
          <Icon name="resume" size={15} color="var(--kh-carbon-70)" />
          <span style={{ flex: 1, fontFamily: C.sans, fontSize: 13, color: C.carbon, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</span>
          <QFileDownloadLink f={f} C={C} />
          {!readOnly && onRemove && <button onClick={() => onRemove(i)} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2 }}><Icon name="x" size={14} color="var(--kh-carbon-50)" /></button>}
        </div>
      ))}
    </div>
  );
}

// New-style entries only carry a fileId (bytes fetched on demand from Codata's
// file storage); older, already-saved entries may still carry a base64/blob
// `url` from before this fix, so keep that path working too.
function QFileDownloadLink({ f, C }) {
  const [busy, setBusy] = React.useState(false);
  const linkStyle = { fontFamily: C.sans, fontSize: 10.5, letterSpacing: '0.08em', textTransform: 'uppercase', color: C.carbon, display: 'inline-flex', alignItems: 'center', gap: 5, background: 'none', border: 'none', cursor: busy ? 'default' : 'pointer', padding: 0, opacity: busy ? 0.5 : 1 };
  if (f.fileId) {
    return (
      <button style={linkStyle} disabled={busy} onClick={async () => {
        const api = window.CodataAPI || window.CodataAdmin;
        if (!api || !api.downloadDocumentFile) return;
        setBusy(true);
        try { await api.downloadDocumentFile(f.fileId, f.name); }
        catch (e) { console.error('File download failed:', e); alert("This file couldn't be downloaded right now. Please try again."); }
        finally { setBusy(false); }
      }}><Icon name="download" size={13} /></button>
    );
  }
  if (f.url) return <a href={f.url} download={f.name} style={linkStyle}><Icon name="download" size={13} /></a>;
  return null;
}

/* ---- Locked, read-only final view (client side) -------------------------- */
function QuestionnaireLocked({ data, onNavigate }) {
  const { C } = UI;
  const submitted = data.submittedAt ? new Date(data.submittedAt).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) : '';
  return (
    <div className="kh-fade" style={{ maxWidth: 880, margin: '0 auto', padding: '4px 4px 60px' }}>
      <div style={{ marginBottom: 22 }}>
        <div style={{ fontFamily: C.sans, fontSize: 10, fontWeight: 600, letterSpacing: '0.2em', textTransform: 'uppercase', color: C.carbon50, marginBottom: 10 }}>Kickoff Questionnaire</div>
        <h1 className="kh-display" style={{ fontSize: 'clamp(28px,3.4vw,42px)', margin: 0 }}>{Q.title}</h1>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, background: 'color-mix(in oklab, var(--sch-completed) 12%, #fff)', border: `1px solid color-mix(in oklab, var(--sch-completed) 32%, #fff)`, padding: '14px 18px', marginBottom: 24, flexWrap: 'wrap' }}>
        <Icon name="lock" size={17} color="var(--sch-completed)" />
        <div style={{ flex: 1, minWidth: 220 }}>
          <div style={{ fontFamily: C.sans, fontWeight: 600, fontSize: 14, color: C.carbon }}>Submitted{submitted ? ' on ' + submitted : ''}</div>
          <div style={{ fontFamily: C.serif, fontSize: 13.5, lineHeight: 1.5, color: C.carbon70 }}>Karen has your responses. This is your final copy — it’s locked, so it can’t be edited. Reach out to Karen if something needs to change.</div>
        </div>
        <button onClick={() => window.qPrintResponses && window.qPrintResponses(data)} style={qBtn(C, 'light')}><Icon name="download" size={15} /> Download my copy</button>
      </div>

      {Q.sections.map(sec => (
        <div key={sec.id} style={{ marginBottom: 14, background: '#fff', border: `1px solid ${C.hair}` }}>
          <div style={{ padding: '13px 20px', borderBottom: `1px solid ${C.hair}`, background: '#fbfaf7' }}>
            <span style={{ fontFamily: C.sans, fontWeight: 600, fontSize: 11, letterSpacing: '0.14em', textTransform: 'uppercase', color: C.carbon }}>{sec.title}</span>
          </div>
          <div style={{ padding: 'clamp(18px,3vw,28px)', display: 'flex', flexDirection: 'column', gap: 22 }}>
            {sec.fields.map(f => (
              <QField key={f.id} field={f} value={data.answers[f.id]} files={data.files[f.id]} readOnly />
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

/* ---- Rendering uploaded files into the printout ------------------------- */
// Rasterize / render each uploaded questionnaire file so it appears visually in
// the downloaded PDF. Images embed as-is; PDFs are rasterized to page images
// via pdf.js; Word docs are rendered to HTML via docx-preview. Every branch is
// guarded — if a render fails we just fall back to showing the filename.
function qEnsurePdfWorker() {
  const lib = window.pdfjsLib;
  if (!lib) return Promise.resolve();
  if (lib.__khWorkerReady) return lib.__khWorkerReady;
  const SRC = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.worker.min.js';
  lib.__khWorkerReady = fetch(SRC).then(r => r.text())
    .then(code => { lib.GlobalWorkerOptions.workerSrc = URL.createObjectURL(new Blob([code], { type: 'text/javascript' })); })
    .catch(() => { lib.GlobalWorkerOptions.workerSrc = SRC; });
  return lib.__khWorkerReady;
}
async function qRasterizePdf(blob) {
  const lib = window.pdfjsLib;
  if (!lib) return null;
  await qEnsurePdfWorker();
  const buf = await blob.arrayBuffer();
  const pdf = await lib.getDocument({ data: buf }).promise;
  const imgs = [];
  const pages = Math.min(pdf.numPages, 15);
  for (let n = 1; n <= pages; n++) {
    const page = await pdf.getPage(n);
    const base = page.getViewport({ scale: 1 });
    const scale = Math.min(1400 / base.width, 2);
    const vp = page.getViewport({ scale });
    const canvas = document.createElement('canvas');
    canvas.width = vp.width; canvas.height = vp.height;
    await page.render({ canvasContext: canvas.getContext('2d'), viewport: vp }).promise;
    imgs.push(canvas.toDataURL('image/jpeg', 0.85));
  }
  return imgs;
}
async function qRenderDocx(blob) {
  if (!window.docx || !window.docx.renderAsync) return null;
  const holder = document.createElement('div');
  holder.style.cssText = 'position:fixed;left:-99999px;top:0;width:760px;';
  document.body.appendChild(holder);
  try {
    await window.docx.renderAsync(blob, holder, null, { inWrapper: true, ignoreWidth: false, ignoreHeight: true, className: 'kh-docx' });
    return holder.innerHTML;
  } finally { holder.remove(); }
}
function qBlobToDataURL(blob) {
  return new Promise((resolve, reject) => {
    const fr = new FileReader();
    fr.onload = () => resolve(fr.result);
    fr.onerror = reject;
    fr.readAsDataURL(blob);
  });
}
// Get the file's bytes as a Blob regardless of which shape it was saved in:
// new entries carry only a fileId (fetched from Codata's file storage on
// demand); older, already-saved entries may still carry a base64/blob `url`
// from before uploads stopped embedding bytes into consultantNotes.
async function qFileBlob(x) {
  if (x.fileId) {
    const api = window.CodataAPI || window.CodataAdmin;
    if (api && api.fetchDocumentFileBlob) return api.fetchDocumentFileBlob(x.fileId);
  }
  if (x.url) return fetch(x.url).then(r => r.blob());
  return null;
}
async function qRenderFileAssets(data) {
  const Q = window.QUESTIONNAIRE || { sections: [] };
  const out = {};
  for (const sec of Q.sections) {
    for (const f of sec.fields) {
      if (f.type !== 'file') continue;
      const fl = (data.files && data.files[f.id]) || [];
      if (!fl.length) continue;
      const rendered = [];
      for (const x of fl) {
        const nm = (x.name || '').toLowerCase();
        const mime = x.type || '';
        const entry = { name: x.name, images: null, html: null };
        try {
          const blob = await qFileBlob(x);
          if (blob) {
            const isImage = /^image\//.test(mime) || /\.(png|jpe?g|gif|webp)$/.test(nm);
            const isPdf = /^application\/pdf/.test(mime) || nm.endsWith('.pdf');
            const isDoc = nm.endsWith('.docx') || nm.endsWith('.doc');
            if (isImage) entry.images = [await qBlobToDataURL(blob)];
            else if (isPdf) entry.images = await qRasterizePdf(blob);
            else if (isDoc) entry.html = await qRenderDocx(blob);
          }
        } catch (e) { console.warn('Questionnaire file render failed:', x.name, e); }
        rendered.push(entry);
      }
      out[f.id] = rendered;
    }
  }
  return out;
}

/* ---- Shared printable responses (portal "download my copy" + admin PDF) --- */
function qBuildPrintDoc(data, assets) {
  // Resolve the questionnaire schema from the global so this works in the admin
  // console too (which doesn't load Questionnaire.jsx, where `Q` is defined).
  const Q = window.QUESTIONNAIRE || { sections: [] };
  const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  const name = esc(data.answers && data.answers.name || 'Client');
  const submitted = data.submittedAt ? new Date(data.submittedAt).toLocaleString('en-US', { dateStyle: 'long', timeStyle: 'short' }) : 'Draft (not yet submitted)';
  let body = '';
  Q.sections.forEach(sec => {
    body += `<h2>${esc(sec.title)}</h2>`;
    sec.fields.forEach(f => {
      let v;
      if (f.type === 'file') {
        const fl = (data.files && data.files[f.id]) || [];
        if (!fl.length) { v = '<em>No file uploaded</em>'; }
        else {
          const ass = (assets && assets[f.id]) || [];
          v = fl.map((x, i) => {
            const a = ass[i] || {};
            let inner = '';
            if (a.images && a.images.length) inner = a.images.map(src => `<img class="qfile-img" src="${src}"/>`).join('');
            else if (a.html) inner = `<div class="qfile-doc">${a.html}</div>`;
            return `<div class="qfile"><div class="qfile-name">📎 ${esc(x.name)}</div>${inner}</div>`;
          }).join('');
        }
      }
      else if (f.type === 'ack') { v = data.answers[f.id] ? '\u2713 Acknowledged' : '<em>Not acknowledged</em>'; }
      else if (f.type === 'schooltable' || f.type === 'rowtable') {
        const rowsv = data.answers && data.answers[f.id];
        if (Array.isArray(rowsv) && rowsv.length) {
          const cols = f.columns || [];
          v = `<table class="qtbl"><thead><tr>${cols.map(c => `<th>${esc(c.label)}</th>`).join('')}</tr></thead><tbody>${rowsv.map(r => `<tr>${cols.map(c => `<td>${esc(r[c.key] || '')}</td>`).join('')}</tr>`).join('')}</tbody></table>`;
        } else v = '<em>Not answered</em>';
      }
      else { const a = data.answers && data.answers[f.id]; v = (a != null && String(a).trim() !== '') ? esc(a).replace(/\n/g, '<br/>') : '<em>Not answered</em>'; }
      body += `<div class="q"><div class="lbl">${esc(f.label)}</div><div class="ans">${v}</div></div>`;
    });
  });
  return `<!doctype html><html><head><meta charset="utf-8"/><title>${name} — Kickoff Questionnaire</title>
  <style>
    @page { size: A4; margin: 1.6cm; }
    * { box-sizing: border-box; }
    body { font-family: Georgia, 'Times New Roman', serif; color: #1d1d1d; line-height: 1.5; max-width: 760px; margin: 0 auto; padding: 28px; }
    .head { border-bottom: 2px solid #1d1d1d; padding-bottom: 14px; margin-bottom: 22px; }
    .eyebrow { font-family: Arial, sans-serif; font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase; color: #6b6b6b; }
    h1 { font-family: Arial, sans-serif; font-size: 24px; margin: 8px 0 6px; }
    .meta { font-family: Arial, sans-serif; font-size: 12px; color: #6b6b6b; }
    h2 { font-family: Arial, sans-serif; font-size: 13px; letter-spacing: 0.12em; text-transform: uppercase; border-bottom: 1px solid #1d1d1d; padding-bottom: 4px; margin: 26px 0 14px; }
    .q { margin-bottom: 16px; break-inside: avoid; }
    .lbl { font-family: Arial, sans-serif; font-weight: 700; font-size: 12px; color: #1d1d1d; margin-bottom: 3px; }
    .ans { font-size: 13.5px; white-space: normal; }
    em { color: #9a9a9a; }
    .qtbl { width: 100%; border-collapse: collapse; margin-top: 4px; }
    .qtbl th { font-family: Arial, sans-serif; font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; text-align: left; color: #6b6b6b; border-bottom: 1px solid #1d1d1d; padding: 4px 8px; }
    .qtbl td { font-size: 12px; border-bottom: 1px solid #ddd; padding: 5px 8px; vertical-align: top; }
    .qfile { margin-top: 8px; }
    .qfile-name { font-family: Arial, sans-serif; font-size: 11px; color: #6b6b6b; margin-bottom: 6px; }
    .qfile-img { display: block; width: 100%; max-width: 100%; margin: 0 0 12px; border: 1px solid #ddd; break-inside: avoid; page-break-inside: avoid; }
    .qfile-doc { border: 1px solid #ddd; padding: 10px; margin-bottom: 12px; break-inside: avoid; }
    .qfile-doc .docx-wrapper { background: transparent !important; padding: 0 !important; }
    .qfile-doc .docx { box-shadow: none !important; margin: 0 auto !important; }
  </style></head><body>
    <div class="head"><div class="eyebrow">Karen Hamou Consulting · Client Kickoff Questionnaire</div><h1>${name}</h1><div class="meta">Submitted: ${esc(submitted)}</div></div>
    ${body}
    <script>window.addEventListener('load', function(){ setTimeout(function(){ try { window.focus(); window.print(); } catch (e) {} }, 400); });<\/script>
  </body></html>`;
}
async function qPrintResponses(data) {
  // Open the window synchronously inside the click handler so it isn't treated
  // as a pop-up, then render the uploaded files (async) before writing the doc.
  const w = window.open('', '_blank');
  if (!w) { try { alert('Please allow pop-ups for this site to download the PDF.'); } catch (e) {} return; }
  w.document.open();
  w.document.write('<!doctype html><meta charset="utf-8"><title>Preparing…</title><body style="font-family:Arial,sans-serif;color:#6b6b6b;padding:44px;font-size:14px">Preparing your document…</body>');
  w.document.close();
  let assets = {};
  try { assets = await qRenderFileAssets(data); } catch (e) { console.warn('Questionnaire asset render failed:', e); }
  const html = qBuildPrintDoc(data, assets);
  w.document.open(); w.document.write(html); w.document.close();
}
window.qBuildPrintDoc = qBuildPrintDoc;
window.qPrintResponses = qPrintResponses;
