// ==========================================
// form-fields.js: FILLABLE ACROFORM DESIGNER
// ==========================================
// Design-time DOM widgets. On Save PDF, written as real PDF form fields
// via pdf-lib so Adobe / Edge / etc. can fill and save a copy.

window.FORM_FIELD_MODES = new Set([
    "form_text",
    "form_checkbox",
    "form_radio",
    "form_dropdown",
    "form_signature"
]);

window.isFormFieldMode = function(mode) {
    return window.FORM_FIELD_MODES.has(mode || APP.currentMode);
};

window.formFieldTypeFromMode = function(mode) {
    switch (mode) {
        case "form_checkbox": return "checkbox";
        case "form_radio": return "radio";
        case "form_dropdown": return "dropdown";
        case "form_signature": return "signature";
        default: return "text";
    }
};

/**
 * True when a form signature has been signed (DOM shell or export snapshot).
 * Used for export flatten, print, and skipping empty fillable name lines.
 */
window.isFormSignatureFieldSigned = function(elOrField) {
    if (!elOrField) return false;
    // DOM element
    if (elOrField.dataset) {
        if ((elOrField.dataset.fieldType || "") !== "signature") return false;
        if (elOrField.dataset.sigImage) return true;
        if (elOrField.dataset.sigAttestation) return true;
        const v = String(elOrField.dataset.defaultValue || "").trim();
        return !!(v && v.indexOf("data:image") !== 0);
    }
    // Snapshot object from serializeFormFieldElement
    if ((elOrField.fieldType || "") !== "signature") return false;
    if (elOrField.signatureImage) return true;
    if (elOrField.signatureAttestation) return true;
    const v = String(elOrField.defaultValue || "").trim();
    return !!(v && v.indexOf("data:image") !== 0);
};

/**
 * Mid-length hash for burned-in captions (matches on-screen caption density).
 */
window.shortHashForCaption = function(hex) {
    const s = String(hex || "").replace(/[^0-9a-fA-F]/g, "");
    if (s.length < 20) return s || "";
    // ASCII ellipsis only — StandardFonts.Helvetica is WinAnsi
    return s.slice(0, 16) + "..." + s.slice(-8);
};

/**
 * Burn signed form-signature appearance into the PDF page (ink + caption).
 * Not a fillable FT:Sig — visual + audit-friendly text for any reader / print.
 *
 * @param {*} pdfDoc
 * @param {*} page pdf-lib page
 * @param {object} f field snapshot (serializeFormFieldElement shape)
 * @param {{ x:number, y:number, width:number, height:number }} box PDF media box
 * @param {*} [font] embedded Helvetica (optional)
 * @returns {Promise<boolean>}
 */
window.stampSignedFormSignatureOnPdfPage = async function(pdfDoc, page, f, box, font) {
    if (!pdfDoc || !page || !f || !box) return false;
    const PDFLibNS = window.PDFLib || (typeof PDFLib !== "undefined" ? PDFLib : null);
    const rgb = PDFLibNS && PDFLibNS.rgb ? PDFLibNS.rgb : null;

    const att = f.signatureAttestation || null;
    let name = String(
        (att && att.signedName) || f.defaultValue || ""
    ).trim();
    if (name.indexOf("data:image") === 0) name = (att && att.signedName) || "";
    const when = (att && att.signedAt && typeof window.formatSignatureCaptionTime === "function")
        ? window.formatSignatureCaptionTime(att.signedAt)
        : (att && att.signedAt ? String(att.signedAt).slice(0, 16).replace("T", " ") : "");
    const originFull = (att && att.originHash) ? String(att.originHash) : "";
    const attFull = (att && att.attestationHash)
        ? String(att.attestationHash)
        : (f.signatureAttestationHash ? String(f.signatureAttestationHash) : "");
    const originShort = window.shortHashForCaption(originFull);
    const attShort = window.shortHashForCaption(attFull);

    const x = Number(box.x) || 0;
    const y = Number(box.y) || 0;
    const w = Math.max(8, Number(box.width) || 80);
    const h = Math.max(12, Number(box.height) || 40);
    const pad = 2;

    // Build caption lines first so we can size the strip (avoid stacked baselines)
    let primary = name || "Signed";
    if (att && att.attested !== false) primary += "  [OK]";
    if (when) primary += "  " + when;
    primary = primary.replace(/[^\x20-\x7E]/g, "?").slice(0, 120);

    const captionLines = [];
    captionLines.push({ text: primary, kind: "primary" });
    if (originShort) captionLines.push({ text: ("doc " + originShort).slice(0, 90), kind: "meta" });
    if (attShort) captionLines.push({ text: ("sig " + attShort).slice(0, 90), kind: "meta" });

    const primarySize = 6.2;
    const metaSize = 5.2;
    // Baseline-to-baseline must exceed font size or glyphs overlap
    const lineGap = 7.2;
    const captionPadY = 2.5;
    const nLines = Math.max(1, captionLines.length);
    const neededCaptionH = captionPadY * 2
        + primarySize
        + Math.max(0, nLines - 1) * lineGap
        + 1.5;

    // Prefer enough room for all lines; leave at least ~45% of box for ink
    let captionH = Math.min(h * 0.48, Math.max(neededCaptionH, 18));
    if (h - captionH < Math.min(24, h * 0.4)) {
        captionH = Math.max(neededCaptionH * 0.85, h - Math.min(28, h * 0.55));
    }
    captionH = Math.min(h - 10, Math.max(14, captionH));
    const inkH = Math.max(8, h - captionH);

    // Soft background so ink is readable on busy pages
    try {
        if (rgb) {
            page.drawRectangle({
                x: x,
                y: y,
                width: w,
                height: h,
                color: rgb(1, 1, 1),
                opacity: 0.92,
                borderColor: rgb(0.75, 0.8, 0.85),
                borderWidth: 0.5
            });
            // Divider under ink
            page.drawLine({
                start: { x: x, y: y + captionH },
                end: { x: x + w, y: y + captionH },
                thickness: 0.4,
                color: rgb(0.7, 0.75, 0.8),
                opacity: 0.9
            });
        }
    } catch (_) { /* ignore */ }

    // Ink / name image in upper region
    let drewInk = false;
    const imgData = f.signatureImage || null;
    if (imgData && typeof imgData === "string" && imgData.indexOf("data:image") === 0) {
        try {
            const comma = imgData.indexOf(",");
            const meta = imgData.slice(0, Math.max(0, comma));
            const b64 = comma >= 0 ? imgData.slice(comma + 1) : "";
            const bin = atob(b64);
            const bytes = new Uint8Array(bin.length);
            for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
            let embedded = null;
            if (/image\/jpe?g/i.test(meta)) {
                embedded = await pdfDoc.embedJpg(bytes);
            } else {
                // default PNG (cursive canvas export is usually png)
                embedded = await pdfDoc.embedPng(bytes);
            }
            if (embedded) {
                const iw = embedded.width || 1;
                const ih = embedded.height || 1;
                const maxW = w - pad * 2;
                const maxH = inkH - pad * 2;
                const scale = Math.min(maxW / iw, maxH / ih, 1);
                const dw = Math.max(1, iw * scale);
                const dh = Math.max(1, ih * scale);
                const ix = x + pad + (maxW - dw) / 2;
                const iy = y + captionH + pad + (maxH - dh) / 2;
                page.drawImage(embedded, { x: ix, y: iy, width: dw, height: dh });
                drewInk = true;
            }
        } catch (e) {
            console.warn("[stampSignedFormSignature] image embed failed", e && e.message);
        }
    }

    if (!drewInk && name && font) {
        try {
            const size = Math.min(18, Math.max(10, inkH * 0.45));
            page.drawText(name, {
                x: x + pad + 2,
                y: y + captionH + Math.max(4, (inkH - size) / 2),
                size,
                font,
                color: rgb ? rgb(0.05, 0.15, 0.35) : undefined,
                maxWidth: w - pad * 2 - 4
            });
            drewInk = true;
        } catch (e) {
            console.warn("[stampSignedFormSignature] name text failed", e && e.message);
        }
    }

    // Caption: draw top → bottom inside strip (PDF y is baseline)
    // Never clamp every line to y+2 — that stacked doc/sig on one baseline.
    if (font && captionLines.length) {
        const inkColor = rgb ? rgb(0.12, 0.16, 0.22) : undefined;
        const metaColor = rgb ? rgb(0.35, 0.4, 0.48) : undefined;
        try {
            // First baseline just below top of caption strip
            let cy = y + captionH - captionPadY - primarySize * 0.75;
            const minY = y + 1.5;
            for (let i = 0; i < captionLines.length; i++) {
                const line = captionLines[i];
                const size = line.kind === "primary" ? primarySize : metaSize;
                if (cy < minY) break;
                page.drawText(line.text, {
                    x: x + pad + 1,
                    y: cy,
                    size,
                    font,
                    color: line.kind === "primary" ? inkColor : metaColor,
                    maxWidth: w - pad * 2
                });
                cy -= lineGap;
            }
        } catch (e) {
            console.warn("[stampSignedFormSignature] caption failed", e && e.message);
        }
    }

    return drewInk || !!(name || originShort || attShort);
};

window.nextFormFieldSerial = window.nextFormFieldSerial || 1;

window.cssEscape = function(value) {
    if (window.CSS && typeof CSS.escape === "function") return CSS.escape(value);
    return String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
};

window.generateFormFieldName = function(type) {
    const prefix =
        type === "checkbox" ? "chk" :
        type === "radio" ? "radio" :
        type === "dropdown" ? "dd" :
        type === "signature" ? "sig" :
        "txt";
    let name;
    do {
        name = `${prefix}_${window.nextFormFieldSerial++}`;
    } while (document.querySelector(`.formFieldOverlay[data-field-name="${window.cssEscape(name)}"]`));
    return name;
};

window.generateRadioOptionValue = function(groupName) {
    const existing = Array.from(
        document.querySelectorAll(`.formFieldOverlay[data-field-type="radio"][data-field-name="${window.cssEscape(groupName)}"]`)
    ).map(el => el.dataset.optionValue);
    let i = 1;
    let val;
    do {
        val = `opt_${i++}`;
    } while (existing.includes(val));
    return val;
};

/**
 * PDF field names for export. Flat (no dots) so pdf-lib never throws on
 * hierarchical / empty name parts. Keep readable unique tokens.
 */
window.sanitizeFormFieldName = function(raw) {
    let cleaned = String(raw || "")
        .trim()
        .replace(/\s+/g, "_")
        .replace(/[^A-Za-z0-9._-]/g, "")
        .replace(/\.+/g, "_")          // no hierarchical names
        .replace(/_+/g, "_")
        .replace(/^[_-]+|[_-]+$/g, "");
    if (!cleaned) cleaned = window.generateFormFieldName("text");
    // pdf-lib rejects empty segments; also avoid leading digits-only weirdness
    if (!/^[A-Za-z]/.test(cleaned)) cleaned = "f_" + cleaned;
    return cleaned;
};

window.getFormFieldOverlays = function(root) {
    const scope = root || document;
    return scope.querySelectorAll(".formFieldOverlay");
};

window.defaultFormFieldSize = function(type, opts) {
    if (type === "checkbox" || type === "radio") return { w: 22, h: 22 };
    if (type === "signature") return { w: 200, h: 56 };
    // Multiline needs vertical room so PDF readers / PDF.js render a real text area
    if (opts && opts.multiline) return { w: 220, h: 72 };
    return { w: 180, h: 28 };
};

/** Minimum sensible height once Multi is enabled (single-line defaults are too short). */
window.FORM_FIELD_MULTILINE_MIN_H = 56;

/**
 * Rebuild the Editor fill surface so multiline / fontSize / defaultValue changes
 * are reflected immediately (createFormFieldOverlay only builds it once).
 */
window.rebuildFormFieldFillSurface = function(el) {
    if (!el || !el.classList || !el.classList.contains("formFieldOverlay")) return null;
    const old = el.querySelector(".form-field-fill");
    if (old) old.remove();
    const fill = window.buildFormFieldFillSurface(el);
    // Keep fill first so it sits under design chrome but still hit-tests in Editor
    el.insertBefore(fill, el.firstChild);
    return fill;
};

/**
 * Apply text-field visual options to the designer shell (hint + fill control).
 * Call after Multi / Aa / default-value controls change.
 */
window.syncFormFieldTextAppearance = function(el) {
    if (!el) return;
    const multi = el.dataset.multiline === "1";
    const fontSize = Math.max(6, parseFloat(el.dataset.fontSize) || 12);
    const def = el.dataset.defaultValue || "";

    const hint = el.querySelector(".form-field-hint");
    if (hint) {
        hint.textContent = def
            ? def
            : (multi ? "Multiline text field" : "Text field");
        hint.style.whiteSpace = multi ? "pre-wrap" : "nowrap";
        hint.style.fontSize = Math.min(fontSize, 14) + "px";
    }

    // Swap input ↔ textarea and re-apply font size / value
    const prevCtrl = el.querySelector(".form-field-fill-control");
    const needTextarea = multi;
    const isTextarea = prevCtrl && prevCtrl.tagName === "TEXTAREA";
    const isInput = prevCtrl && prevCtrl.tagName === "INPUT" && prevCtrl.type === "text";
    if (!prevCtrl || (needTextarea && !isTextarea) || (!needTextarea && !isInput)) {
        window.rebuildFormFieldFillSurface(el);
        return;
    }
    prevCtrl.style.fontSize = fontSize + "px";
    if (prevCtrl.value !== def && document.activeElement !== prevCtrl) {
        prevCtrl.value = def;
    }
};

/**
 * Build Editor-mode fill controls (input/select/checkbox) for a designer field.
 * Visible only in workspace-editor + tool-select (see CSS).
 */
window.buildFormFieldFillSurface = function(el) {
    const type = el.dataset.fieldType || "text";
    const fill = document.createElement("div");
    fill.className = "form-field-fill";
    fill.addEventListener("mousedown", e => e.stopPropagation());
    fill.addEventListener("pointerdown", e => e.stopPropagation());

    const name = el.dataset.fieldName || "field";
    const fontSize = Math.max(6, parseFloat(el.dataset.fontSize) || 12);
    const def = el.dataset.defaultValue || "";

    if (type === "checkbox") {
        const input = document.createElement("input");
        input.type = "checkbox";
        input.className = "form-field-fill-control";
        input.checked = def === "1" || def === "true" || def === "Yes";
        input.addEventListener("change", () => {
            el.dataset.defaultValue = input.checked ? "1" : "";
            if (typeof window.logProjectAudit === "function") {
                window.logProjectAudit("FIELD_FILLED", {
                    fieldName: el.dataset.fieldName,
                    fieldType: "checkbox",
                    value: input.checked ? "1" : ""
                });
            }
        });
        fill.appendChild(input);
    } else if (type === "radio") {
        const input = document.createElement("input");
        input.type = "radio";
        input.className = "form-field-fill-control";
        // Scope radio group names per document tab (multi-tab safe)
        const docScope = (el.closest(".doc-viewer") && el.closest(".doc-viewer").dataset.docId)
            || (el.closest(".pageWrapper") && el.closest(".pageWrapper").dataset.pdfFingerprint)
            || "doc";
        input.name = "fill_" + String(docScope).replace(/\W+/g, "_") + "_" + name;
        input.value = el.dataset.optionValue || "opt";
        input.checked = def === "1" || def === input.value;
        input.addEventListener("change", () => {
            if (!input.checked) return;
            // Clear siblings in group
            const radioRoot = el.closest(".doc-viewer")
                || el.closest(".pageWrapper")
                || document;
            radioRoot.querySelectorAll(
                `.formFieldOverlay[data-field-type="radio"][data-field-name="${window.cssEscape(name)}"]`
            ).forEach(sib => {
                sib.dataset.defaultValue = sib === el ? "1" : "";
                const r = sib.querySelector(".form-field-fill-control");
                if (r && r !== input) r.checked = false;
            });
            el.dataset.defaultValue = "1";
            if (typeof window.logProjectAudit === "function") {
                window.logProjectAudit("FIELD_FILLED", {
                    fieldName: el.dataset.fieldName,
                    fieldType: "radio",
                    optionValue: el.dataset.optionValue
                });
            }
        });
        fill.appendChild(input);
    } else if (type === "dropdown") {
        const sel = document.createElement("select");
        sel.className = "form-field-fill-control";
        sel.style.fontSize = fontSize + "px";
        let opts = [];
        try { opts = JSON.parse(el.dataset.options || "[]"); } catch (_) { opts = []; }
        if (!opts.length) opts = ["Option 1", "Option 2"];
        const blank = document.createElement("option");
        blank.value = "";
        blank.textContent = "—";
        sel.appendChild(blank);
        opts.forEach(o => {
            const op = document.createElement("option");
            op.value = o;
            op.textContent = o;
            if (String(o) === def) op.selected = true;
            sel.appendChild(op);
        });
        sel.addEventListener("change", () => {
            el.dataset.defaultValue = sel.value;
            if (typeof window.logProjectAudit === "function") {
                window.logProjectAudit("FIELD_FILLED", {
                    fieldName: el.dataset.fieldName,
                    fieldType: "dropdown",
                    value: sel.value
                });
            }
        });
        fill.appendChild(sel);
    } else if (type === "signature") {
        // Signature *line* for forms: stores a typed name (and optional preview image).
        // Exports as a normal text field — browsers ignore PDF digital-signature (FT:Sig) widgets.
        const btn = document.createElement("button");
        btn.type = "button";
        btn.className = "form-field-fill-control form-field-fill-sig";
        const labelOf = (v) => {
            const s = String(v || "").trim();
            if (!s) return "Click to sign…";
            if (s.indexOf("data:image") === 0) return "Signed ✓";
            return s.length > 28 ? s.slice(0, 26) + "…" : s;
        };
        btn.textContent = labelOf(def);
        if (def && def.indexOf("data:image") === 0) {
            btn.style.backgroundImage = "url(" + def + ")";
            btn.style.backgroundSize = "contain";
            btn.style.backgroundRepeat = "no-repeat";
            btn.style.backgroundPosition = "center";
            btn.style.color = "transparent";
        }
        btn.addEventListener("click", () => {
            if (typeof window.openSignatureForFormField === "function") {
                window.openSignatureForFormField(el, btn);
                return;
            }
            // Fallback if signature module missing: prompt + attestation text
            const attest = window.CCS_SIGNATURE_ATTESTATION_TEXT
                || "I attest that I am the person signing this field.";
            const ok = window.confirm(attest + "\n\nClick OK to continue, then enter your name.");
            if (!ok) return;
            const sigName = window.prompt(
                "Type name for this signature line (blank clears):",
                (def && def.indexOf("data:image") !== 0) ? def : ""
            );
            if (sigName === null) return;
            Promise.resolve(
                window.applySignatureToFormField
                    ? window.applySignatureToFormField(el, btn, sigName, null, { method: "prompt" })
                    : null
            ).then(() => {
                if (!window.applySignatureToFormField) {
                    el.dataset.defaultValue = sigName;
                    btn.textContent = labelOf(sigName);
                }
            });
        });
        fill.appendChild(btn);
        // Restore caption if this field was already signed (project reload / rebuild fill)
        if (el.dataset.sigAttestation && typeof window.renderFormSignatureAttestationCaption === "function") {
            window.renderFormSignatureAttestationCaption(el, fill);
        }
    } else {
        // text / multiline (and unknown → text)
        const multi = el.dataset.multiline === "1" || type === "multiline";
        const input = document.createElement(multi ? "textarea" : "input");
        if (!multi) input.type = "text";
        input.className = "form-field-fill-control";
        input.value = def;
        input.style.fontSize = fontSize + "px";
        if (multi) {
            // Grow with the shell height so multi-line is obvious while filling
            input.rows = Math.max(2, Math.round((parseFloat(el.style.height) || 72) / (fontSize * 1.3)));
        }
        // Commit on blur (not every keystroke) so the rolling log stays useful
        input.addEventListener("blur", () => {
            const prev = el.dataset.defaultValue || "";
            el.dataset.defaultValue = input.value;
            if (prev !== input.value && typeof window.logProjectAudit === "function") {
                window.logProjectAudit("FIELD_FILLED", {
                    fieldName: el.dataset.fieldName,
                    fieldType: multi ? "text_multiline" : "text",
                    value: input.value
                });
            }
        });
        fill.appendChild(input);
    }
    return fill;
};

/**
 * Create a designer widget for a fillable field.
 * Form mode = design chrome; Editor + Pointer = fill surface.
 */
window.createFormFieldOverlay = function(container, opts = {}) {
    const type = opts.type || "text";
    const multiline = !!(opts.multiline || type === "multiline");
    // Normalize "multiline" type (from native import) to text + flag
    const fieldType = type === "multiline" ? "text" : type;
    const defaults = window.defaultFormFieldSize(fieldType, { multiline });
    const x = opts.x ?? 40;
    const y = opts.y ?? 40;
    const w = opts.w ?? defaults.w;
    const h = opts.h ?? defaults.h;

    const el = document.createElement("div");
    el.className = `formFieldOverlay form-field-${fieldType}` + (opts.activate === false ? "" : " overlay-active");
    el.id = opts.id || crypto.randomUUID();
    el.dataset.fieldType = fieldType;
    el.dataset.fieldName = opts.fieldName || window.generateFormFieldName(fieldType);
    el.dataset.required = opts.required ? "1" : "0";
    el.dataset.multiline = multiline ? "1" : "0";
    el.dataset.defaultValue = opts.defaultValue != null ? String(opts.defaultValue) : "";
    el.dataset.fontSize = String(
        Number.isFinite(parseFloat(opts.fontSize)) && parseFloat(opts.fontSize) > 0
            ? Math.round(parseFloat(opts.fontSize))
            : 12
    );
    if (opts.nativeId) el.dataset.nativeId = String(opts.nativeId);

    if (fieldType === "dropdown") {
        const options = Array.isArray(opts.options) && opts.options.length
            ? opts.options
            : ["Option 1", "Option 2", "Option 3"];
        el.dataset.options = JSON.stringify(options);
    }

    if (fieldType === "radio") {
        el.dataset.optionValue = opts.optionValue
            || window.generateRadioOptionValue(el.dataset.fieldName);
        // defaultValue "1" means this radio is the group's default selection
    }

    el.style.left = x + "px";
    el.style.top = y + "px";
    el.style.width = w + "px";
    el.style.height = h + "px";

    const body = document.createElement("div");
    body.className = "form-field-body";
    body.setAttribute("aria-hidden", "true");

    const badge = document.createElement("span");
    badge.className = "form-field-badge";
    badge.textContent =
        fieldType === "checkbox" ? "☑" :
        fieldType === "radio" ? "◉" :
        fieldType === "dropdown" ? "▾" :
        fieldType === "signature" ? "✍" :
        "T";

    const label = document.createElement("span");
    label.className = "form-field-label";
    label.textContent = fieldType === "radio"
        ? `${el.dataset.fieldName} · ${el.dataset.optionValue}`
        : el.dataset.fieldName;

    if (fieldType === "checkbox") {
        body.innerHTML = `<span class="form-field-check-box"></span>`;
        body.appendChild(badge);
    } else if (fieldType === "radio") {
        body.innerHTML = `<span class="form-field-radio-dot"></span>`;
        body.appendChild(badge);
        const radioHint = document.createElement("span");
        radioHint.className = "form-field-hint";
        radioHint.style.display = "none"; // keep dataset-driven label on controls
        body.appendChild(label);
    } else if (fieldType === "dropdown") {
        body.append(badge, label);
        const chev = document.createElement("span");
        chev.className = "form-field-chevron";
        chev.textContent = "▼";
        body.appendChild(chev);
    } else if (fieldType === "signature") {
        body.append(badge, label);
        const hint = document.createElement("span");
        hint.className = "form-field-hint";
        hint.textContent = "Signature name line";
        body.appendChild(hint);
    } else {
        body.append(badge, label);
        const hint = document.createElement("span");
        hint.className = "form-field-hint";
        hint.textContent = el.dataset.defaultValue
            ? el.dataset.defaultValue
            : (el.dataset.multiline === "1" ? "Multiline text field" : "Text field");
        if (el.dataset.multiline === "1") hint.style.whiteSpace = "pre-wrap";
        body.appendChild(hint);
    }

    const fill = window.buildFormFieldFillSurface(el);

    const controls = window.buildFormFieldControls(el);
    const dragHandle = document.createElement("div");
    dragHandle.className = "textDragHandle";
    dragHandle.innerHTML = "⠿";
    const resizeHandle = document.createElement("div");
    resizeHandle.className = "resizeHandle";
    const deleteHandle = window.createDeleteHandle(el);

    el.append(fill, body, controls, dragHandle, resizeHandle, deleteHandle);
    container.appendChild(el);

    window.makeDraggable(el, resizeHandle, dragHandle);
    if (opts.activate !== false) {
        window.setActiveOverlay(el);
    }

    if (opts.pushHistory !== false && window.CcsProcessor) {
        // Stack: max+1 (or forceZ when restoring a known order into history)
        const createOpts = (opts.zIndex != null && Number.isFinite(Number(opts.zIndex)))
            ? { forceZ: opts.zIndex }
            : undefined;
        window.CcsProcessor.commit(
            window.CcsProcessor.build.createNode(el, `Add form field (${fieldType})`, createOpts)
        );
    } else if (window.CcsProcessor && typeof window.CcsProcessor.assignOverlayZIndex === "function") {
        // No history tape (e.g. project restore) — still apply stack policy here
        if (opts.zIndex != null && Number.isFinite(Number(opts.zIndex))) {
            window.CcsProcessor.assignOverlayZIndex(el, container, opts.zIndex);
        } else {
            window.CcsProcessor.assignOverlayZIndex(el, container);
        }
    }

    return el;
};

window.refreshFormFieldLabel = function(el) {
    const label = el.querySelector(".form-field-label");
    if (!label) return;
    if (el.dataset.fieldType === "radio") {
        label.textContent = `${el.dataset.fieldName} · ${el.dataset.optionValue || ""}`;
    } else {
        label.textContent = el.dataset.fieldName || "";
    }
};

window.buildFormFieldControls = function(el) {
    const controls = document.createElement("div");
    controls.className = "textControls form-field-controls";
    controls.addEventListener("mousedown", e => e.stopPropagation());

    const type = el.dataset.fieldType || "text";

    const nameInput = document.createElement("input");
    nameInput.type = "text";
    nameInput.className = "form-field-name-input";
    nameInput.title = type === "radio"
        ? "Radio group name (shared by buttons in the same group)"
        : "PDF field name (unique)";
    nameInput.placeholder = type === "radio" ? "group_name" : "field_name";
    nameInput.value = el.dataset.fieldName || "";
    nameInput.style.width = type === "radio" ? "100px" : "110px";

    nameInput.addEventListener("change", () => {
        const next = window.sanitizeFormFieldName(nameInput.value);

        if (type !== "radio") {
            const clash = document.querySelector(
                `.formFieldOverlay[data-field-name="${window.cssEscape(next)}"]:not([id="${window.cssEscape(el.id)}"])`
            );
            // Allow multiple radios to share a name; other types must be unique
            if (clash && clash.dataset.fieldType !== "radio") {
                window.customAlert(`Field name "${next}" is already used. Pick another.`);
                nameInput.value = el.dataset.fieldName;
                return;
            }
            // Also block non-radio colliding with an existing radio group name used only by radios? OK either way.
            if (clash && clash.dataset.fieldType === "radio" && type !== "radio") {
                window.customAlert(`"${next}" is a radio group name. Pick another field name.`);
                nameInput.value = el.dataset.fieldName;
                return;
            }
        }

        el.dataset.fieldName = next;
        nameInput.value = next;
        window.refreshFormFieldLabel(el);
    });

    controls.appendChild(nameInput);

    // Box size (W×H) — all field types; corner resize handle also works
    const wLabel = document.createElement("span");
    wLabel.className = "form-field-size-label";
    wLabel.textContent = "W";
    const wInput = document.createElement("input");
    wInput.type = "number";
    wInput.className = "form-field-size-input";
    // Allow tiny widgets (radios/checks often need ~8–12px on dense forms)
    wInput.min = "1";
    wInput.max = "2000";
    wInput.step = "1";
    wInput.title = "Field width (px)";
    wInput.value = String(Math.round(parseFloat(el.style.width) || 100));

    const hLabel = document.createElement("span");
    hLabel.className = "form-field-size-label";
    hLabel.textContent = "H";
    const hInput = document.createElement("input");
    hInput.type = "number";
    hInput.className = "form-field-size-input";
    hInput.min = "1";
    hInput.max = "2000";
    hInput.step = "1";
    hInput.title = "Field height (px)";
    hInput.value = String(Math.round(parseFloat(el.style.height) || 28));

    const applyBoxSize = () => {
        let w = Math.max(1, parseFloat(wInput.value) || 1);
        let h = Math.max(1, parseFloat(hInput.value) || 1);
        if (type === "checkbox" || type === "radio") {
            // Keep square; use the value the user just changed if possible
            const side = document.activeElement === hInput ? h : w;
            w = Math.max(1, side);
            h = w;
        }
        el.style.minWidth = "0";
        el.style.minHeight = "0";
        el.style.width = w + "px";
        el.style.height = h + "px";
        wInput.value = String(Math.round(w));
        hInput.value = String(Math.round(h));
        if (typeof window.syncNativeWidgetGeometryFromDesigner === "function") {
            window.syncNativeWidgetGeometryFromDesigner(el);
        }
    };
    wInput.addEventListener("change", applyBoxSize);
    hInput.addEventListener("change", applyBoxSize);

    // Keep W/H inputs in sync when user drags the corner resize handle
    const ro = new ResizeObserver(() => {
        if (document.activeElement === wInput || document.activeElement === hInput) return;
        wInput.value = String(Math.round(parseFloat(el.style.width) || el.offsetWidth || 0));
        hInput.value = String(Math.round(parseFloat(el.style.height) || el.offsetHeight || 0));
    });
    try { ro.observe(el); } catch (_) { /* ignore */ }

    controls.append(wLabel, wInput, hLabel, hInput);

    if (type === "text") {
        const fontSizeInput = document.createElement("input");
        fontSizeInput.type = "number";
        fontSizeInput.min = "6";
        fontSizeInput.max = "72";
        fontSizeInput.value = el.dataset.fontSize || "12";
        fontSizeInput.title = "Font size in the PDF (and Editor fill preview)";
        fontSizeInput.className = "form-field-size-input";
        const applyFontSize = () => {
            el.dataset.fontSize = String(Math.max(6, parseInt(fontSizeInput.value, 10) || 12));
            fontSizeInput.value = el.dataset.fontSize;
            if (typeof window.syncFormFieldTextAppearance === "function") {
                window.syncFormFieldTextAppearance(el);
            }
        };
        fontSizeInput.addEventListener("change", applyFontSize);
        fontSizeInput.addEventListener("input", applyFontSize);
        const fsLabel = document.createElement("span");
        fsLabel.className = "form-field-size-label";
        fsLabel.textContent = "Aa";
        fsLabel.title = "Font size";

        const multiLabel = document.createElement("label");
        multiLabel.className = "form-field-check-label";
        multiLabel.title = "Multiline text (textarea in Editor + PDF Multiline flag on export)";
        multiLabel.innerHTML = `<input type="checkbox" ${el.dataset.multiline === "1" ? "checked" : ""}> Multi`;
        multiLabel.querySelector("input").addEventListener("change", (e) => {
            const on = !!e.target.checked;
            el.dataset.multiline = on ? "1" : "0";
            // Grow short single-line shells so multi-line is usable
            if (on) {
                const minH = window.FORM_FIELD_MULTILINE_MIN_H || 56;
                const curH = parseFloat(el.style.height) || el.offsetHeight || 0;
                if (curH < minH) {
                    el.style.height = minH + "px";
                    // W/H are first two .form-field-size-input; Aa is the third
                    const sizeInputs = controls.querySelectorAll("input.form-field-size-input");
                    if (sizeInputs[1] && sizeInputs[1] !== fontSizeInput) {
                        sizeInputs[1].value = String(Math.round(minH));
                    }
                    if (typeof window.syncNativeWidgetGeometryFromDesigner === "function") {
                        window.syncNativeWidgetGeometryFromDesigner(el);
                    }
                }
            }
            if (typeof window.syncFormFieldTextAppearance === "function") {
                window.syncFormFieldTextAppearance(el);
            }
        });

        const defInput = document.createElement("input");
        defInput.type = "text";
        defInput.placeholder = "Default value";
        defInput.title = "Optional default text when opened";
        defInput.value = el.dataset.defaultValue || "";
        defInput.style.width = "100px";
        defInput.addEventListener("change", () => {
            el.dataset.defaultValue = defInput.value;
            if (typeof window.syncFormFieldTextAppearance === "function") {
                window.syncFormFieldTextAppearance(el);
            }
        });

        controls.append(fsLabel, fontSizeInput, multiLabel, defInput);
    }

    if (type === "dropdown") {
        const optBtn = document.createElement("button");
        optBtn.type = "button";
        optBtn.className = "form-field-opts-btn";
        optBtn.textContent = "Options…";
        optBtn.title = "Edit dropdown choices";
        optBtn.addEventListener("click", async (e) => {
            e.stopPropagation();
            let options = [];
            try { options = JSON.parse(el.dataset.options || "[]"); } catch (_) { options = []; }

            const next = await window.promptFormOptionsList(options, {
                title: "📋 Dropdown options",
                help: "Each row is one choice shown in the PDF dropdown. Press Enter to add another row.",
                minItems: 1
            });
            if (!next) return;
            el.dataset.options = JSON.stringify(next);
        });
        controls.appendChild(optBtn);
    }

    if (type === "checkbox") {
        const defLabel = document.createElement("label");
        defLabel.className = "form-field-check-label";
        defLabel.innerHTML = `<input type="checkbox" ${el.dataset.defaultValue === "1" ? "checked" : ""}> Default on`;
        defLabel.querySelector("input").addEventListener("change", (e) => {
            el.dataset.defaultValue = e.target.checked ? "1" : "0";
        });
        controls.appendChild(defLabel);
    }

    if (type === "radio") {
        const optInput = document.createElement("input");
        optInput.type = "text";
        optInput.title = "Export value for this button (unique within the group)";
        optInput.placeholder = "option value";
        optInput.value = el.dataset.optionValue || "";
        optInput.style.width = "90px";
        optInput.addEventListener("change", () => {
            const v = window.sanitizeFormFieldName(optInput.value) || window.generateRadioOptionValue(el.dataset.fieldName);
            // uniqueness within group
            const clash = Array.from(
                document.querySelectorAll(
                    `.formFieldOverlay[data-field-type="radio"][data-field-name="${window.cssEscape(el.dataset.fieldName)}"]`
                )
            ).find(other => other !== el && other.dataset.optionValue === v);
            if (clash) {
                window.customAlert(`Option value "${v}" is already used in this radio group.`);
                optInput.value = el.dataset.optionValue;
                return;
            }
            el.dataset.optionValue = v;
            optInput.value = v;
            window.refreshFormFieldLabel(el);
        });

        const defLabel = document.createElement("label");
        defLabel.className = "form-field-check-label";
        defLabel.title = "Selected by default when the PDF is opened";
        defLabel.innerHTML = `<input type="checkbox" ${el.dataset.defaultValue === "1" ? "checked" : ""}> Default`;
        defLabel.querySelector("input").addEventListener("change", (e) => {
            const on = e.target.checked;
            if (on) {
                // Only one default per group
                document.querySelectorAll(
                    `.formFieldOverlay[data-field-type="radio"][data-field-name="${window.cssEscape(el.dataset.fieldName)}"]`
                ).forEach(other => {
                    other.dataset.defaultValue = other === el ? "1" : "0";
                    const cb = other.querySelector(".form-field-controls .form-field-check-label input");
                    if (cb && other !== el) cb.checked = false;
                });
            }
            el.dataset.defaultValue = on ? "1" : "0";
        });

        controls.append(optInput, defLabel);
    }

    if (type === "signature") {
        const note = document.createElement("span");
        note.className = "form-field-check-label";
        note.style.color = "#5b21b6";
        note.textContent = "Name line + attestation";
        note.title = "In Editor fill mode: signer attests identity; name, time, and browser/device "
            + "details are stored in project state for audit/seal. "
            + "Exports as a fillable text field (browsers ignore PDF FT:Sig). "
            + "Page ink stamp: Insert Signature (N).";
        controls.appendChild(note);
    }

    return controls;
};

/**
 * Collect form field specs for a page container (CSS top-left coords at base scale).
 */
/** Read geometry even if style.* was cleared but layout still has box. */
window.readFormFieldBox = function(el) {
    const left = parseFloat(el.style.left);
    const top = parseFloat(el.style.top);
    const w = parseFloat(el.style.width);
    const h = parseFloat(el.style.height);
    return {
        x: Number.isFinite(left) ? left : (el.offsetLeft || 0),
        y: Number.isFinite(top) ? top : (el.offsetTop || 0),
        w: Number.isFinite(w) && w > 0 ? w : (el.offsetWidth || 100),
        h: Number.isFinite(h) && h > 0 ? h : (el.offsetHeight || 24)
    };
};

window.serializeFormFieldElement = function(el) {
    if (!el || !el.classList || !el.classList.contains("formFieldOverlay")) return null;
    const type = el.dataset.fieldType || "text";
    let options = [];
    if (type === "dropdown") {
        try { options = JSON.parse(el.dataset.options || "[]"); } catch (_) { options = []; }
    }
    const box = window.readFormFieldBox(el);
    const zRaw = parseInt(el.style && el.style.zIndex, 10);
    let signatureAttestation = null;
    if (type === "signature" && el.dataset.sigAttestation) {
        try {
            signatureAttestation = JSON.parse(el.dataset.sigAttestation);
        } catch (_) {
            signatureAttestation = null;
        }
    }
    // Do not put huge data:image into schema defaultValue for seals; name only for export text
    let defaultValue = el.dataset.defaultValue || "";
    if (defaultValue.indexOf("data:image") === 0) {
        defaultValue = (signatureAttestation && signatureAttestation.signedName)
            || "";
    }
    // Ink bitmap lives separately (needed for Export PDF / print / project restore)
    const signatureImage = (type === "signature" && el.dataset.sigImage
        && String(el.dataset.sigImage).indexOf("data:image") === 0)
        ? String(el.dataset.sigImage)
        : null;
    return {
        type: "formField",
        fieldType: type,
        fieldName: el.dataset.fieldName || window.generateFormFieldName(type),
        id: el.id || crypto.randomUUID(),
        x: box.x,
        y: box.y,
        w: box.w,
        h: box.h,
        required: el.dataset.required === "1",
        multiline: el.dataset.multiline === "1",
        defaultValue,
        fontSize: parseFloat(el.dataset.fontSize) || 12,
        optionValue: el.dataset.optionValue || "",
        options,
        nativeId: el.dataset.nativeId || null,
        importedNative: el.dataset.importedNative || null,
        zIndex: Number.isFinite(zRaw) ? zRaw : null,
        // Full attestation (UA, time, hash) — included in project schema → schemaHash seal
        signatureAttestation: signatureAttestation,
        signatureAttestationHash: el.dataset.sigAttestationHash || null,
        signatureImage: signatureImage
    };
};

window.serializeFormFieldsFromContainer = function(container) {
    if (!container || !container.querySelectorAll) return [];
    const fields = [];
    container.querySelectorAll(".formFieldOverlay").forEach((el) => {
        const f = window.serializeFormFieldElement(el);
        if (f) fields.push(f);
    });
    return fields;
};

/**
 * Snapshot every designer form shell from page wrappers (KISS export input).
 * Searches the whole .pageWrapper (not only .pageContainer) so shells that
 * sit beside the container or were reparented within the page still count.
 * @returns {{ pageIndex: number, fields: object[] }[]}
 */
window.collectDesignerFormFieldsFromWrappers = function(wrappers) {
    const jobs = [];
    const list = wrappers && wrappers.length != null ? Array.from(wrappers) : [];
    const assigned = new Set();

    for (let i = 0; i < list.length; i++) {
        const w = list[i];
        if (!w || !w.querySelectorAll) continue;
        const fields = [];
        w.querySelectorAll(".formFieldOverlay").forEach((el) => {
            const f = window.serializeFormFieldElement(el);
            if (f) {
                fields.push(f);
                assigned.add(el);
            }
        });
        if (fields.length) jobs.push({ pageIndex: i, fields });
    }

    // Orphans: shells in the active viewer but not under any listed wrapper
    // (should be rare; map to nearest page by geometry, else page 0).
    try {
        const viewer = (typeof window.getActiveViewer === "function" && window.getActiveViewer())
            || (window.APP && APP.DOM && APP.DOM.viewer)
            || null;
        if (viewer && viewer.querySelectorAll) {
            viewer.querySelectorAll(".formFieldOverlay").forEach((el) => {
                if (assigned.has(el)) return;
                const f = window.serializeFormFieldElement(el);
                if (!f) return;
                let pageIndex = 0;
                const host = el.closest && el.closest(".pageWrapper");
                if (host) {
                    const idx = list.indexOf(host);
                    if (idx >= 0) pageIndex = idx;
                }
                let job = jobs.find((j) => j.pageIndex === pageIndex);
                if (!job) {
                    job = { pageIndex, fields: [] };
                    jobs.push(job);
                }
                job.fields.push(f);
                assigned.add(el);
                console.warn("[collectDesignerFormFields] orphan shell assigned to page", pageIndex, f.fieldName);
            });
        }
    } catch (e) {
        console.warn("[collectDesignerFormFields] orphan scan", e);
    }

    jobs.sort((a, b) => a.pageIndex - b.pageIndex);
    return jobs;
};

/**
 * Editor fill often types into PDF.js native widgets while designer twins are hidden.
 * Copy live control values onto designer shells before export/project serialize.
 */
window.syncDesignerFormValuesFromNatives = function(root) {
    const scope = root
        || (typeof window.getActiveViewer === "function" && window.getActiveViewer())
        || document;
    if (!scope || !scope.querySelectorAll) return 0;
    let n = 0;
    scope.querySelectorAll(".formFieldOverlay").forEach((el) => {
        // Prefer values already on the designer fill surface
        const designerCtrl = el.querySelector(".form-field-fill-control");
        if (designerCtrl) {
            if (designerCtrl.type === "checkbox" || designerCtrl.type === "radio") {
                el.dataset.defaultValue = designerCtrl.checked ? "1" : "";
                n++;
                return;
            }
            if (designerCtrl.tagName === "SELECT" || designerCtrl.tagName === "INPUT" || designerCtrl.tagName === "TEXTAREA") {
                el.dataset.defaultValue = String(designerCtrl.value || "");
                n++;
                return;
            }
        }
        const nativeId = el.dataset.nativeId;
        if (!nativeId) return;
        const esc = (window.CSS && CSS.escape)
            ? (s) => CSS.escape(String(s))
            : (s) => String(s).replace(/(["\\])/g, "\\$1");
        let section = null;
        try {
            // tagNativeAnnotationSections sets data-native-id on the section
            section = scope.querySelector(
                `.annotationLayer section[data-native-id="${esc(nativeId)}"]`
            );
            if (!section) {
                const fp = el.dataset.pdfFingerprint || "";
                const layers = fp
                    ? scope.querySelectorAll(`.annotationLayer[data-pdf-fingerprint="${esc(fp)}"] section`)
                    : scope.querySelectorAll(".annotationLayer section");
                for (const s of layers) {
                    if (s.dataset && s.dataset.nativeId === String(nativeId)) {
                        section = s;
                        break;
                    }
                }
            }
        } catch (_) { /* ignore */ }
        if (!section) return;
        const ctrl = section.querySelector("input, textarea, select");
        if (!ctrl) return;
        if (ctrl.type === "checkbox" || ctrl.type === "radio") {
            el.dataset.defaultValue = ctrl.checked ? "1" : "";
        } else {
            el.dataset.defaultValue = String(ctrl.value || "");
        }
        n++;
    });
    return n;
};

/**
 * Snapshot native PDF.js AcroForm widgets when no designer shells exist.
 * Geometry from section bounding box relative to pageContainer.
 */
window.collectNativeFormFieldsFromWrappers = function(wrappers) {
    const jobs = [];
    const list = wrappers && wrappers.length != null ? Array.from(wrappers) : [];
    for (let i = 0; i < list.length; i++) {
        const w = list[i];
        if (!w) continue;
        const container = (w.querySelector && w.querySelector(".pageContainer")) || w;
        const layer = container.querySelector && container.querySelector(".annotationLayer");
        if (!layer) continue;
        const cRect = container.getBoundingClientRect();
        const fields = [];
        const seen = new Set();
        layer.querySelectorAll("section").forEach((section) => {
            // Skip permanently removed / non-widget chrome
            if (section.dataset && section.dataset.formRemoved === "1") return;
            const ctrl = section.querySelector("input, textarea, select");
            if (!ctrl) return;
            const name = (ctrl.getAttribute("name") || section.dataset.fieldName || ctrl.id || "").trim();
            if (!name || seen.has(name + ":" + (ctrl.type || ""))) return;
            // Skip our uniquify prefix noise for field names when possible
            let fieldName = section.dataset.fieldName || name;
            // Strip multi-tab id prefix if present on name
            fieldName = String(fieldName).replace(/^d[a-zA-Z0-9]+_/, "");
            if (fieldName.indexOf("pdfjs_internal") === 0) {
                fieldName = (section.dataset.fieldName || ("field_" + fields.length));
            }

            const sRect = section.getBoundingClientRect();
            const x = sRect.left - cRect.left;
            const y = sRect.top - cRect.top;
            const ww = Math.max(8, sRect.width);
            const hh = Math.max(8, sRect.height);
            if (!Number.isFinite(x) || !Number.isFinite(y) || ww < 2 || hh < 2) return;

            let fieldType = "text";
            let defaultValue = "";
            let options = [];
            if (ctrl.type === "checkbox") {
                fieldType = "checkbox";
                defaultValue = ctrl.checked ? "1" : "";
            } else if (ctrl.type === "radio") {
                fieldType = "radio";
                defaultValue = ctrl.checked ? "1" : "";
            } else if (ctrl.tagName === "SELECT") {
                fieldType = "dropdown";
                defaultValue = String(ctrl.value || "");
                options = Array.from(ctrl.options || []).map((o) => o.value || o.text).filter(Boolean);
            } else if (ctrl.tagName === "TEXTAREA") {
                fieldType = "multiline";
                defaultValue = String(ctrl.value || "");
            } else {
                fieldType = "text";
                defaultValue = String(ctrl.value || "");
            }

            seen.add(name + ":" + (ctrl.type || ""));
            fields.push({
                type: "formField",
                fieldType,
                fieldName: fieldName || ("field_" + fields.length),
                id: section.dataset.nativeId || crypto.randomUUID(),
                x, y, w: ww, h: hh,
                required: !!ctrl.required,
                multiline: fieldType === "multiline",
                defaultValue,
                fontSize: 12,
                optionValue: ctrl.value || "",
                options,
                nativeId: section.dataset.nativeId || null,
                importedNative: "1"
            });
        });
        if (fields.length) jobs.push({ pageIndex: i, fields });
    }
    return jobs;
};

window.restoreFormFieldOverlay = function(container, obj) {
    const el = window.createFormFieldOverlay(container, {
        type: obj.fieldType || "text",
        fieldName: obj.fieldName,
        id: obj.id,
        x: obj.x,
        y: obj.y,
        w: obj.w,
        h: obj.h,
        required: obj.required,
        multiline: obj.multiline,
        defaultValue: obj.defaultValue,
        fontSize: obj.fontSize,
        options: obj.options,
        optionValue: obj.optionValue,
        pushHistory: false,
        activate: false,
        nativeId: obj.nativeId || null,
        zIndex: obj.zIndex
    });
    if (el && obj.importedNative) el.dataset.importedNative = "1";
    // Always treat restored fields that have a nativeId as imported twins
    if (el && obj.nativeId) el.dataset.importedNative = "1";

    // Restore signature ink + attestation (audit) from project schema
    if (el && (obj.fieldType || "") === "signature") {
        if (obj.signatureImage && String(obj.signatureImage).indexOf("data:image") === 0) {
            el.dataset.sigImage = String(obj.signatureImage);
            const btn = el.querySelector(".form-field-fill-sig");
            if (btn) {
                btn.style.backgroundImage = "url(" + obj.signatureImage + ")";
                btn.style.backgroundSize = "contain";
                btn.style.backgroundRepeat = "no-repeat";
                btn.style.backgroundPosition = "center";
                btn.style.color = "transparent";
                const nm = (obj.signatureAttestation && obj.signatureAttestation.signedName)
                    || obj.defaultValue
                    || "Signed ✓";
                btn.textContent = nm;
            }
        }
        if (obj.signatureAttestation) {
            try {
                el.dataset.sigAttestation = typeof obj.signatureAttestation === "string"
                    ? obj.signatureAttestation
                    : JSON.stringify(obj.signatureAttestation);
            } catch (_) { /* ignore */ }
            if (obj.signatureAttestationHash) {
                el.dataset.sigAttestationHash = obj.signatureAttestationHash;
            } else if (obj.signatureAttestation && obj.signatureAttestation.attestationHash) {
                el.dataset.sigAttestationHash = obj.signatureAttestation.attestationHash;
            }
        } else if (obj.signatureAttestationHash) {
            el.dataset.sigAttestationHash = obj.signatureAttestationHash;
        }
        if (typeof window.renderFormSignatureAttestationCaption === "function"
            && (el.dataset.sigAttestation || el.dataset.sigImage || el.dataset.defaultValue)) {
            window.renderFormSignatureAttestationCaption(el);
        }
    }

    const page = container && container.closest(".pageWrapper");
    if (el && page && page.dataset.pdfFingerprint) {
        el.dataset.pdfFingerprint = page.dataset.pdfFingerprint;
    }

    if (el && el.dataset.nativeId && typeof window.setNativeAnnotationVisible === "function") {
        const root = (typeof window.getActiveViewer === "function" && window.getActiveViewer())
            || (el.closest && el.closest(".doc-viewer"))
            || document;
        window.setNativeAnnotationVisible(el.dataset.nativeId, false, {
            fingerprint: el.dataset.pdfFingerprint || undefined,
            fieldName: el.dataset.fieldName || undefined,
            root
        });
        if (typeof window.syncNativeWidgetGeometryFromDesigner === "function") {
            window.syncNativeWidgetGeometryFromDesigner(el);
        }
    }
    return el;
};

/**
 * CSS top-left box (PDF.js visual viewport) → PDF media user-space box (bottom-left).
 * ALWAYS returns finite numbers — NaN boxes make pdf-lib addToPage throw AFTER
 * createTextField already registered the field (fields on form, zero page widgets).
 *
 * @param {object} f  { x, y, w, h } in visual CSS px
 * @param {number} pdfW MediaBox width (page.getSize().width)
 * @param {number} pdfH MediaBox height
 * @param {{ rotation?: number, visualW?: number, visualH?: number }} [opts]
 *   rotation — page /Rotate (0|90|180|270); visualW/H — PDF.js viewport size
 */
window.cssBoxToPdfBox = function(f, pdfW, pdfH, opts) {
    const num = (v, fallback) => {
        const n = typeof v === "number" ? v : parseFloat(v);
        return Number.isFinite(n) ? n : fallback;
    };
    let pw = num(pdfW, 612);
    let ph = num(pdfH, 792);
    if (pw < 1) pw = 612;
    if (ph < 1) ph = 792;

    const options = opts && typeof opts === "object" ? opts : {};
    const rotation = ((num(options.rotation, 0) % 360) + 360) % 360;
    let visualW = num(options.visualW, pw);
    let visualH = num(options.visualH, ph);
    if (visualW < 1) visualW = pw;
    if (visualH < 1) visualH = ph;

    let w = num(f && f.w, 100);
    let h = num(f && f.h, 24);
    if (w < 1) w = 1;
    if (h < 1) h = 1;

    let x = num(f && f.x, 0);
    let yCss = num(f && f.y, 0);

    // Unrotated fast path (same as original Y-flip)
    if (rotation === 0) {
        x = Math.max(0, Math.min(x, Math.max(0, pw - 2)));
        let y = ph - yCss - h;
        if (!Number.isFinite(y) || y < 0) y = 0;
        if (y + h > ph) y = Math.max(0, ph - h);
        return {
            x: Number(x) || 0,
            y: Number(y) || 0,
            width: Number(w) || 1,
            height: Number(h) || 1
        };
    }

    // Rotated: map all four CSS corners → media, take axis-aligned bounds
    const mapPt = (typeof window.visualCssPointToPdfMedia === "function")
        ? (cx, cy) => window.visualCssPointToPdfMedia(cx, cy, visualW, visualH, pw, ph, rotation)
        : (cx, cy) => ({ x: cx, y: ph - cy });

    const corners = [
        mapPt(x, yCss),
        mapPt(x + w, yCss),
        mapPt(x + w, yCss + h),
        mapPt(x, yCss + h)
    ];
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
    corners.forEach((p) => {
        if (!p || !Number.isFinite(p.x) || !Number.isFinite(p.y)) return;
        if (p.x < minX) minX = p.x;
        if (p.y < minY) minY = p.y;
        if (p.x > maxX) maxX = p.x;
        if (p.y > maxY) maxY = p.y;
    });
    if (!Number.isFinite(minX)) {
        return { x: 0, y: 0, width: 1, height: 1 };
    }
    let boxW = Math.max(1, maxX - minX);
    let boxH = Math.max(1, maxY - minY);
    // Clamp into media box
    minX = Math.max(0, Math.min(minX, Math.max(0, pw - 1)));
    minY = Math.max(0, Math.min(minY, Math.max(0, ph - 1)));
    if (minX + boxW > pw) boxW = Math.max(1, pw - minX);
    if (minY + boxH > ph) boxH = Math.max(1, ph - minY);

    return {
        x: Number(minX) || 0,
        y: Number(minY) || 0,
        width: Number(boxW) || 1,
        height: Number(boxH) || 1
    };
};

/**
 * Create an AcroForm signature widget (pdf-lib has no createSignature helper).
 */
window.createPdfSignatureField = function(pdfDoc, page, fieldName, box) {
    const form = (typeof window.getLivePdfForm === "function")
        ? window.getLivePdfForm(pdfDoc)
        : pdfDoc.getForm();
    if (!form) throw new Error("No live PDFForm for signature field");
    const context = pdfDoc.context;
    const PDFLibNS = window.PDFLib || PDFLib;
    const PDFHexString = PDFLibNS.PDFHexString;
    const PDFString = PDFLibNS.PDFString;

    const nameObj = PDFHexString && PDFHexString.fromText
        ? PDFHexString.fromText(fieldName)
        : (PDFString && PDFString.of ? PDFString.of(fieldName) : fieldName);

    const dict = context.obj({
        FT: "Sig",
        Type: "Annot",
        Subtype: "Widget",
        T: nameObj,
        F: 4,
        P: page.ref,
        Rect: [box.x, box.y, box.x + box.width, box.y + box.height],
    });
    const ref = context.register(dict);

    if (page.node && typeof page.node.addAnnot === "function") {
        page.node.addAnnot(ref);
    } else {
        throw new Error("page.node.addAnnot unavailable");
    }

    // Register on AcroForm
    if (form.acroForm && typeof form.acroForm.addField === "function") {
        form.acroForm.addField(ref);
    } else if (typeof form.markFieldAsDirty === "function") {
        // older paths — try ensure acroform via getForm side effects
        const acro = form.acroForm || (form.doc && form.doc.catalog.getOrCreateAcroForm && form.doc.catalog.getOrCreateAcroForm());
        if (acro && acro.addField) acro.addField(ref);
        else throw new Error("Could not register signature on AcroForm");
    }

    return ref;
};

window.getOrCreateRadioGroup = function(form, name) {
    try {
        return form.getRadioGroup(name);
    } catch (_) {
        return form.createRadioGroup(name);
    }
};

/**
 * Ensure /DA has a Tf operator and the requested font size (Acrobat-style).
 * pdf-lib's setFontSize throws if /DA is missing or has no Tf — we seed DA first.
 * Matches CCS-PAN.pdf pattern: "0 g /Helv 10 Tf" (or explicit size, never buried).
 */
window.applyPdfTextFieldDaAndSize = function(tf, fontSize, font) {
    const size = Math.max(6, parseFloat(fontSize) || 12);
    const fontName = (font && font.name) ? String(font.name) : "Helv";
    try {
        let da = "";
        try { da = tf.acroField.getDefaultAppearance() || ""; } catch (_) { da = ""; }
        if (!da || !/Tf/.test(da)) {
            // Seed a full DA so setFontSize / readers have something to work with
            tf.acroField.setDefaultAppearance(`0 0 0 rg /${fontName} ${size} Tf`);
        } else {
            // Prefer API when DA already has Tf (after addToPage appearances)
            try {
                tf.setFontSize(size);
            } catch (_) {
                tf.acroField.setDefaultAppearance(`0 0 0 rg /${fontName} ${size} Tf`);
            }
            return size;
        }
        try { tf.setFontSize(size); } catch (_) { /* DA already seeded */ }
    } catch (e) {
        console.warn("[applyPdfTextFieldDaAndSize]", e && e.message);
    }
    return size;
};

/**
 * Multiline + scrolling flags for a pdf-lib text field (Ff bit 13 = 4096).
 */
window.applyPdfTextFieldMultiline = function(tf, multiline) {
    if (!tf) return false;
    const want = !!multiline;
    try {
        if (want) {
            tf.enableMultiline();
            try { tf.enableScrolling(); } catch (_) { /* optional */ }
        } else {
            tf.disableMultiline();
        }
        return !!tf.isMultiline() === want;
    } catch (e) {
        console.warn("[applyPdfTextFieldMultiline]", e && e.message);
        // Last resort: set Ff bit directly (AcroTextFlags.Multiline = 1 << 12 = 4096)
        try {
            if (tf.acroField && typeof tf.acroField.setFlagTo === "function") {
                tf.acroField.setFlagTo(4096, want);
            }
        } catch (_) { /* ignore */ }
        return want;
    }
};

/**
 * pdf-lib throws "No /DA entry" if setFontSize is called before appearances exist.
 * Order: create → enableMultiline → addToPage(+font) → setText → DA/size → updateAppearances.
 * Appearances never throw out — only create/addToPage can fail the embed.
 */
window.embedPdfTextField = function(form, page, name, box, opts = {}) {
    const tf = form.createTextField(name);
    const multiline = !!(opts.multiline);
    window.applyPdfTextFieldMultiline(tf, multiline);

    const font = opts.font || null;
    const pageOpts = {
        x: box.x,
        y: box.y,
        width: box.width,
        height: box.height
    };
    if (font) pageOpts.font = font;
    tf.addToPage(page, pageOpts);

    const text = opts.defaultValue != null ? String(opts.defaultValue) : "";
    try { tf.setText(text); } catch (_) { /* ignore */ }

    const size = Math.max(6, parseFloat(opts.fontSize) || 12);
    window.applyPdfTextFieldDaAndSize(tf, size, font);
    if (font) {
        try { tf.updateAppearances(font); } catch (_) { /* ignore */ }
        // Re-assert size after appearance layout (layout can rewrite /DA)
        window.applyPdfTextFieldDaAndSize(tf, size, font);
        try { tf.updateAppearances(font); } catch (_) { /* ignore */ }
    }
    // Re-assert multiline after appearance work (never lose the flag)
    window.applyPdfTextFieldMultiline(tf, multiline);
    return tf;
};

/**
 * Create one field + put a widget on the page. Returns true on success.
 * If addToPage rejects the box, retries with a safe on-page rectangle.
 */
window.embedOneFormField = function(form, pdfDoc, page, f, box, font, name) {
    const type = f.fieldType || "text";

    // Repair Annots before every addToPage (strip may have deleted the key)
    if (typeof window.ensurePdfPageAnnotsArray === "function") {
        window.ensurePdfPageAnnotsArray(pdfDoc, page);
    }

    const addWithFallback = (fieldObj, addFn) => {
        try {
            addFn(box);
            return;
        } catch (e1) {
            // Retry after forcing Annots array again (common after strip)
            if (typeof window.ensurePdfPageAnnotsArray === "function") {
                window.ensurePdfPageAnnotsArray(pdfDoc, page);
            }
            let ph = 792;
            try { ph = page.getSize().height || 792; } catch (_) { /* ignore */ }
            const fallback = {
                x: 36,
                y: Math.max(36, ph - 72),
                width: Math.max(24, Number(box && box.width) || 160),
                height: Math.max(14, Number(box && box.height) || 22)
            };
            const msg = (e1 && e1.message) ? String(e1.message) : String(e1);
            // If Annots was the issue, retry original box first after repair
            if (/Annots is undefined|can't access property \"push\"/i.test(msg)) {
                try {
                    addFn(box);
                    return;
                } catch (_) { /* fall through to geometry fallback */ }
            }
            console.warn(
                "[embedOneFormField] addToPage failed for", name,
                msg, "box=", box, "→ retry", fallback
            );
            addFn(fallback);
        }
    };

    if (type === "checkbox") {
        const cb = form.createCheckBox(name);
        addWithFallback(cb, (b) => cb.addToPage(page, b));
        try {
            if (f.defaultValue === "1") cb.check();
            else cb.uncheck();
        } catch (_) { /* ignore */ }
        return true;
    }
    if (type === "dropdown") {
        const dd = form.createDropdown(name);
        const opts = (f.options && f.options.length) ? f.options : ["Option 1"];
        try { dd.addOptions(opts); } catch (_) { /* ignore */ }
        addWithFallback(dd, (b) => {
            const o = { x: b.x, y: b.y, width: b.width, height: b.height };
            if (font) o.font = font;
            dd.addToPage(page, o);
        });
        try {
            if (f.defaultValue && opts.includes(f.defaultValue)) dd.select(f.defaultValue);
            else if (opts[0]) dd.select(opts[0]);
        } catch (_) { /* ignore */ }
        const size = Math.max(6, parseFloat(f.fontSize) || 12);
        try {
            window.applyPdfTextFieldDaAndSize(dd, size, font);
        } catch (_) { /* dropdown may not support same DA helper path */ }
        if (font) {
            try { dd.updateAppearances(font); } catch (_) { /* ignore */ }
            try {
                if (typeof dd.setFontSize === "function") dd.setFontSize(size);
                dd.updateAppearances(font);
            } catch (_) { /* ignore */ }
        }
        return true;
    }
    if (type === "signature") {
        // Signed fields are burned in via stampSignedFormSignatureOnPdfPage (async path).
        // Unsigned: fillable name line (not FT:Sig — browsers ignore crypto Sig widgets).
        if (typeof window.isFormSignatureFieldSigned === "function"
            && window.isFormSignatureFieldSigned(f)) {
            // Caller (embedAllFormFields) should stamp appearance; sync path is a no-op marker.
            return "signed-pending-stamp";
        }
        let textVal = f.defaultValue != null ? String(f.defaultValue) : "";
        if (textVal.indexOf("data:image") === 0) textVal = "";
        window.embedPdfTextField(form, page, name, box, {
            multiline: false,
            fontSize: f.fontSize || 12,
            defaultValue: textVal,
            font
        });
        return true;
    }

    // text / multiline / unknown — create + addToPage with fallback
    const multiline = !!(f.multiline || type === "multiline");
    const size = Math.max(6, parseFloat(f.fontSize) || 12);
    const tf = form.createTextField(name);
    window.applyPdfTextFieldMultiline(tf, multiline);
    addWithFallback(tf, (b) => {
        const o = { x: b.x, y: b.y, width: b.width, height: b.height };
        if (font) o.font = font;
        tf.addToPage(page, o);
    });
    try { tf.setText(f.defaultValue != null ? String(f.defaultValue) : ""); } catch (_) { /* ignore */ }
    window.applyPdfTextFieldDaAndSize(tf, size, font);
    if (font) {
        try { tf.updateAppearances(font); } catch (_) { /* ignore */ }
        // Appearance layout can rewrite /DA — re-apply size, then refresh once more
        window.applyPdfTextFieldDaAndSize(tf, size, font);
        try { tf.updateAppearances(font); } catch (_) { /* ignore */ }
    }
    // Never lose Multiline flag after appearance updates
    window.applyPdfTextFieldMultiline(tf, multiline);
    return true;
};

// --- Workspace: Editor vs Form mode ---
window.applyWorkspaceMode = function(opts = {}) {
    const mode = APP.workspaceMode === "form" ? "form" : "editor";
    APP.workspaceMode = mode;
    // opts.preferredTool — optional tool to select after switch (e.g. "edit", "form_text")

    document.body.classList.toggle("workspace-form", mode === "form");
    document.body.classList.toggle("workspace-editor", mode === "editor");
    if (typeof window.syncToolBodyClass === "function") window.syncToolBodyClass();

    const sw = document.getElementById("workspaceModeSwitch");
    if (sw) {
        sw.classList.toggle("is-editor", mode === "editor");
        sw.classList.toggle("is-form", mode === "form");
        sw.setAttribute("aria-checked", mode === "form" ? "true" : "false");
        sw.title = mode === "form"
            ? "Form workspace — place fillable fields. Click for Editor (or press E)."
            : "Editor workspace — annotate / fill forms with Pointer. Click for Form (or press F).";
    }

    // Drop selection if it belongs to the other layer
    // Form fields stay selectable in Form mode only; in Editor they are fill targets (no design chrome)
    if (APP.activeOverlay) {
        const isForm = APP.activeOverlay.classList.contains("formFieldOverlay");
        if ((mode === "editor" && isForm) || (mode === "form" && !isForm)) {
            window.clearActiveOverlay(true);
        }
    }
    if (window.clearMultiSelection) window.clearMultiSelection();

    window.refreshToolModeOptionsForWorkspace();

    // Choose tool: preferred for editor; Form mode always opens on Pointer
    let tool = opts.preferredTool || null;
    if (mode === "form") {
        tool = "select";
    } else if (tool) {
        // Editor: reject form-only tools
        if (window.isFormFieldMode(tool)) tool = "select";
    } else if (window.isFormFieldMode(APP.currentMode)) {
        tool = "select";
    }

    if (tool) {
        APP.currentMode = tool;
        if (APP.DOM.toolModeSelect) {
            APP.DOM.toolModeSelect.value = tool;
            if (window.updateToolModeSelectStyle) window.updateToolModeSelectStyle(tool);
        }
        if (APP.DOM.viewer) {
            APP.DOM.viewer.style.cursor = tool === "select" ? "default" : "crosshair";
        }
    } else if (window.updateToolModeSelectStyle) {
        window.updateToolModeSelectStyle(APP.currentMode);
    }
};

window.setWorkspaceMode = function(mode, opts = {}) {
    APP.workspaceMode = mode === "form" ? "form" : "editor";
    window.applyWorkspaceMode(opts);
};

window.refreshToolModeOptionsForWorkspace = function() {
    const select = APP.DOM.toolModeSelect || document.getElementById("toolModeSelect");
    if (!select) return;
    const mode = APP.workspaceMode === "form" ? "form" : "editor";

    select.querySelectorAll("option").forEach(opt => {
        const ws = opt.dataset.workspace || "both";
        const show = ws === "both" || ws === mode;
        opt.hidden = !show;
        opt.disabled = !show;
    });

    select.querySelectorAll("optgroup").forEach(group => {
        const anyVisible = Array.from(group.querySelectorAll("option")).some(o => !o.hidden);
        group.hidden = !anyVisible;
        // Some browsers ignore optgroup.hidden — also collapse label via disabled
        group.disabled = !anyVisible;
    });

    // If current value is hidden, fall back to select
    const current = select.querySelector(`option[value="${CSS.escape ? CSS.escape(select.value) : select.value}"]`);
    if (!current || current.hidden) {
        select.value = "select";
        APP.currentMode = "select";
        if (window.updateToolModeSelectStyle) window.updateToolModeSelectStyle("select");
    } else if (window.updateToolModeSelectStyle) {
        window.updateToolModeSelectStyle(APP.currentMode);
    }
};

window.toggleWorkspaceMode = function() {
    if (window.ccsConfig && window.ccsConfig("enableFormMode", true) === false) {
        window.setWorkspaceMode("editor", { preferredTool: "select" });
        return;
    }
    if (APP.workspaceMode === "form") {
        // Back to editor on Pointer (same as first load)
        window.setWorkspaceMode("editor", { preferredTool: "select" });
    } else {
        // Always enter Form on Pointer so fields aren't hard to select
        window.setWorkspaceMode("form", { preferredTool: "select" });
    }
};

/**
 * Save/print capture must not burn Form-mode / non-pointer dimming into output.
 * - Adds body.capture-safe (full opacity; see style.css)
 * - Temporarily leaves Form workspace chrome so export/print see full-strength layers
 * Does not change APP.workspaceMode / tools permanently.
 */
window.withCaptureSafeAppearance = async function(fn) {
    const inForm = APP.workspaceMode === "form"
        || document.body.classList.contains("workspace-form");
    const hadCaptureSafe = document.body.classList.contains("capture-safe");
    document.body.classList.add("capture-safe");
    if (inForm) {
        document.body.classList.remove("workspace-form");
        document.body.classList.add("workspace-editor");
    }
    // Let the browser apply styles before html2canvas / print clone
    await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
    try {
        return await fn();
    } finally {
        if (!hadCaptureSafe) document.body.classList.remove("capture-safe");
        if (inForm) {
            document.body.classList.add("workspace-form");
            document.body.classList.remove("workspace-editor");
        }
    }
};

// Scripts load after DOM via config loader — safe to init chrome here
(function initWorkspaceModeUi() {
    const formEnabled = !(window.ccsConfig && window.ccsConfig("enableFormMode", true) === false);
    if (!formEnabled) {
        APP.workspaceMode = "editor";
    }
    window.setWorkspaceMode(APP.workspaceMode || "editor");
    if (typeof window.syncToolBodyClass === "function") window.syncToolBodyClass();
    if (typeof window.updateToolModeSelectStyle === "function") {
        window.updateToolModeSelectStyle(APP.currentMode || "select");
    }
    const sw = document.getElementById("workspaceModeSwitch");
    if (sw && !sw.dataset.bound) {
        sw.dataset.bound = "1";
        if (!formEnabled) {
            sw.style.display = "none";
        } else {
            sw.addEventListener("click", () => window.toggleWorkspaceMode());
            sw.addEventListener("keydown", (e) => {
                if (e.key === " " || e.key === "Enter") {
                    e.preventDefault();
                    window.toggleWorkspaceMode();
                }
            });
        }
    }
})();

/**
 * KISS form write path:
 *  1) Snapshot designer shells from the DOM (plain data)
 *  2) Strip every page Widget annotation
 *  3) Drop /AcroForm + form cache so we never write into an orphan form
 *  4) create* fields on a brand-new catalog AcroForm
 *
 * @param {*} pdfDoc pdf-lib PDFDocument (pages already copied)
 * @param {ParentNode[]|NodeList} wrappers .pageWrapper list aligned with pdf pages
 * @param {*} [font] optional embedded font for appearances
 * @param {{ pageIndex: number, fields: object[] }[]} [precollectedJobs] optional snapshot (native fallback)
 * @param {{ burnSignedSignatures?: boolean }} [opts]
 *   burnSignedSignatures: true on Export PDF — ink+caption burned, no AcroForm widget.
 *   false/omit on Save Project — keep a fillable name line (schema still holds full sign state).
 * @returns {Promise<{ widgets: number, stamped: number, failed: number, designerTotal: number, acroEligible: number }>}
 */
window.embedAllFormFields = async function(pdfDoc, wrappers, font, precollectedJobs, opts) {
    const emptyResult = () => ({
        widgets: 0, stamped: 0, failed: 0, designerTotal: 0, acroEligible: 0
    });
    if (!pdfDoc) return emptyResult();

    const options = (opts && typeof opts === "object") ? opts : {};
    const burnSigned = !!options.burnSignedSignatures;

    let pageJobs = Array.isArray(precollectedJobs) && precollectedJobs.length
        ? precollectedJobs
        : null;
    if (!pageJobs) {
        pageJobs = typeof window.collectDesignerFormFieldsFromWrappers === "function"
            ? window.collectDesignerFormFieldsFromWrappers(wrappers)
            : [];
    }
    if ((!pageJobs || !pageJobs.length) && typeof window.collectNativeFormFieldsFromWrappers === "function") {
        pageJobs = window.collectNativeFormFieldsFromWrappers(wrappers) || [];
    }
    const designerTotal = pageJobs.reduce((n, j) => n + (j.fields ? j.fields.length : 0), 0);
    let signedEligible = 0;
    pageJobs.forEach((j) => {
        (j.fields || []).forEach((f) => {
            if (f.fieldType === "signature"
                && typeof window.isFormSignatureFieldSigned === "function"
                && window.isFormSignatureFieldSigned(f)) {
                signedEligible++;
            }
        });
    });
    const acroEligible = Math.max(0, designerTotal - (burnSigned ? signedEligible : 0));
    console.log(
        "[embedAllFormFields] snapshot fields=", designerTotal,
        "pagesWithFields=", pageJobs.length,
        "pdfPages=", (pdfDoc.getPages() || []).length,
        "sample=", pageJobs[0] && pageJobs[0].fields && pageJobs[0].fields[0]
            ? JSON.stringify({
                name: pageJobs[0].fields[0].fieldName,
                x: pageJobs[0].fields[0].x,
                y: pageJobs[0].fields[0].y,
                w: pageJobs[0].fields[0].w,
                h: pageJobs[0].fields[0].h,
                type: pageJobs[0].fields[0].fieldType
            })
            : null
    );

    if (designerTotal === 0) {
        return emptyResult();
    }

    const pages = pdfDoc.getPages();
    if (!pages || !pages.length) {
        console.error("[embedAllFormFields] pdfDoc has 0 pages");
        return emptyResult();
    }

    // Full strip of old widgets/AcroForm, then brand-new getForm().
    // Do NOT manually poke Fields=[] after this — that desyncs pdf-lib internals.
    if (typeof window.stripAcroFormFieldsFromPdfDoc === "function") {
        window.stripAcroFormFieldsFromPdfDoc(pdfDoc, { prepareForReembed: false });
    }
    if (typeof window.invalidatePdfFormCache === "function") {
        window.invalidatePdfFormCache(pdfDoc);
    }

    let form;
    try {
        form = pdfDoc.getForm(); // creates fresh catalog AcroForm after strip
    } catch (e) {
        console.error("[embedAllFormFields] getForm failed", e);
        return emptyResult();
    }

    // pdf-lib addToPage requires page.normalizedEntries().Annots to be an array.
    // Stripping used to delete /Annots entirely → "can't access property push".
    if (typeof window.ensureAllPdfPagesAnnotsArrays === "function") {
        window.ensureAllPdfPagesAnnotsArrays(pdfDoc);
    }

    let count = 0;
    let stampedCount = 0;
    let failCount = 0;
    const usedNames = new Set();
    const radioDefaults = new Map();
    const radioGroups = new Map();
    const radioSrcToName = new Map();
    /** @type {Map<string, { multiline: boolean, fontSize: number }>} */
    const embeddedTextOpts = new Map();

    const uniqueName = (raw) => {
        let name = "ccs_" + window.sanitizeFormFieldName(raw);
        if (usedNames.has(name)) {
            let i = 2;
            while (usedNames.has(name + "_" + i)) i++;
            name = name + "_" + i;
        }
        usedNames.add(name);
        return name;
    };

    const listWrappers = wrappers && wrappers.length != null ? Array.from(wrappers) : [];
    const pageBoxOpts = (page, pageIndex) => {
        let pdfW = 612;
        let pdfH = 792;
        let rotation = 0;
        try {
            const sz = page.getSize();
            pdfW = sz.width;
            pdfH = sz.height;
        } catch (_) { /* defaults */ }
        try {
            rotation = page.getRotation ? page.getRotation().angle : 0;
        } catch (_) { rotation = 0; }
        const w = listWrappers[pageIndex];
        let visualW = pdfW;
        let visualH = pdfH;
        if (w && w.dataset) {
            const bw = parseFloat(w.dataset.baseWidth);
            const bh = parseFloat(w.dataset.baseHeight);
            if (Number.isFinite(bw) && bw > 0) visualW = bw;
            if (Number.isFinite(bh) && bh > 0) visualH = bh;
            if (!rotation && w.dataset.pageRotate) {
                rotation = parseInt(w.dataset.pageRotate, 10) || 0;
            }
        }
        return { pdfW, pdfH, rotation, visualW, visualH };
    };

    // Radios first (shared group names across options)
    for (const job of pageJobs) {
        const page = pages[job.pageIndex];
        if (!page) continue;
        if (typeof window.ensurePdfPageAnnotsArray === "function") {
            window.ensurePdfPageAnnotsArray(pdfDoc, page);
        }
        const { pdfW, pdfH, rotation, visualW, visualH } = pageBoxOpts(page, job.pageIndex);
        for (const f of job.fields) {
            if (f.fieldType !== "radio") continue;
            const srcKey = window.sanitizeFormFieldName(f.fieldName);
            let groupName = radioSrcToName.get(srcKey);
            if (!groupName) {
                groupName = uniqueName(f.fieldName);
                try {
                    radioGroups.set(groupName, form.createRadioGroup(groupName));
                    radioSrcToName.set(srcKey, groupName);
                } catch (e) {
                    failCount++;
                    console.warn("[embedAllFormFields] radio group", groupName, e && e.message);
                    continue;
                }
            }
            const group = radioGroups.get(groupName);
            if (!group) continue;
            const optVal = window.sanitizeFormFieldName(f.optionValue || ("opt_" + (count + 1)));
            const box = window.cssBoxToPdfBox(f, pdfW, pdfH, { rotation, visualW, visualH });
            try {
                if (typeof window.ensurePdfPageAnnotsArray === "function") {
                    window.ensurePdfPageAnnotsArray(pdfDoc, page);
                }
                try {
                    group.addOptionToPage(optVal, page, box);
                } catch (e1) {
                    if (typeof window.ensurePdfPageAnnotsArray === "function") {
                        window.ensurePdfPageAnnotsArray(pdfDoc, page);
                    }
                    // Retry original box after Annots repair, then geometry fallback
                    try {
                        group.addOptionToPage(optVal, page, box);
                    } catch (e2) {
                        const fallback = { x: 36, y: 36, width: 18, height: 18 };
                        console.warn("[embedAllFormFields] radio addOption retry", e2 && e2.message);
                        group.addOptionToPage(optVal, page, fallback);
                    }
                }
                count++;
                if (f.defaultValue === "1") radioDefaults.set(groupName, optVal);
            } catch (err) {
                failCount++;
                console.warn("[embedAllFormFields] radio option", groupName, optVal, err && err.message);
            }
        }
    }
    for (const [groupName, optVal] of radioDefaults) {
        try { radioGroups.get(groupName).select(optVal); } catch (_) { /* ignore */ }
    }

    // Non-radio
    for (const job of pageJobs) {
        const page = pages[job.pageIndex];
        if (!page) {
            failCount += job.fields.filter((f) => f.fieldType !== "radio").length;
            console.warn("[embedAllFormFields] missing pdf page index", job.pageIndex);
            continue;
        }
        if (typeof window.ensurePdfPageAnnotsArray === "function") {
            window.ensurePdfPageAnnotsArray(pdfDoc, page);
        }
        const { pdfW, pdfH, rotation, visualW, visualH } = pageBoxOpts(page, job.pageIndex);

        for (const f of job.fields) {
            if (f.fieldType === "radio") continue;
            const name = uniqueName(f.fieldName);
            const box = window.cssBoxToPdfBox(f, pdfW, pdfH, { rotation, visualW, visualH });
            try {
                const isSignedSig = f.fieldType === "signature"
                    && typeof window.isFormSignatureFieldSigned === "function"
                    && window.isFormSignatureFieldSigned(f);

                // Export PDF: burn ink + caption; do NOT create AcroForm (avoids re-import stack)
                if (isSignedSig && burnSigned
                    && typeof window.stampSignedFormSignatureOnPdfPage === "function") {
                    const ok = await window.stampSignedFormSignatureOnPdfPage(
                        pdfDoc, page, f, box, font
                    );
                    if (ok) stampedCount++;
                    else failCount++;
                    continue;
                }

                // Save Project / unsigned: fillable name line (not FT:Sig)
                if (isSignedSig && !burnSigned) {
                    let textVal = f.defaultValue != null ? String(f.defaultValue) : "";
                    if (textVal.indexOf("data:image") === 0) {
                        textVal = (f.signatureAttestation && f.signatureAttestation.signedName) || "";
                    }
                    if (!textVal && f.signatureAttestation && f.signatureAttestation.signedName) {
                        textVal = String(f.signatureAttestation.signedName);
                    }
                    window.embedPdfTextField(form, page, name, box, {
                        multiline: false,
                        fontSize: f.fontSize || 12,
                        defaultValue: textVal,
                        font
                    });
                    count++;
                    continue;
                }

                const emb = window.embedOneFormField(form, pdfDoc, page, f, box, font, name);
                if (emb === "signed-pending-stamp") {
                    // Should not hit when burnSigned handled above; treat as stamp fallback
                    if (burnSigned && typeof window.stampSignedFormSignatureOnPdfPage === "function") {
                        const ok = await window.stampSignedFormSignatureOnPdfPage(
                            pdfDoc, page, f, box, font
                        );
                        if (ok) stampedCount++;
                        else failCount++;
                    } else {
                        failCount++;
                    }
                    continue;
                }
                if (f.fieldType === "text" || f.fieldType === "multiline" || f.fieldType === "dropdown" || !f.fieldType) {
                    embeddedTextOpts.set(name, {
                        multiline: !!(f.multiline || f.fieldType === "multiline"),
                        fontSize: Math.max(6, parseFloat(f.fontSize) || 12)
                    });
                }
                count++;
            } catch (err) {
                failCount++;
                // Field may already be on the form without a page widget — try remove
                try {
                    const orphan = form.getFieldMaybe && form.getFieldMaybe(name);
                    if (orphan && form.removeField) form.removeField(orphan);
                } catch (_) { /* ignore */ }
                console.warn(
                    "[embedAllFormFields] failed",
                    name,
                    f.fieldType,
                    "box=", JSON.stringify(box),
                    "pageSize=", pdfW, "x", pdfH,
                    "src=", f.x, f.y, f.w, f.h,
                    err && (err.message || err)
                );
            }
        }
    }

    try {
        if (font) form.updateFieldAppearances(font);
    } catch (e) {
        console.warn("[embedAllFormFields] updateFieldAppearances", e);
    }

    // Final pass: layout can rewrite /DA — re-stamp multiline + font size from snapshot
    // so Acrobat / PDF.js see the same options the designer set (matches CCS-PAN style).
    try {
        for (const [name, want] of embeddedTextOpts) {
            let field = null;
            try {
                field = form.getFieldMaybe
                    ? form.getFieldMaybe(name)
                    : form.getField(name);
            } catch (_) { field = null; }
            if (!field) continue;
            try {
                if (typeof field.enableMultiline === "function") {
                    window.applyPdfTextFieldMultiline(field, want.multiline);
                }
            } catch (_) { /* ignore */ }
            try {
                window.applyPdfTextFieldDaAndSize(field, want.fontSize, font);
            } catch (_) { /* ignore */ }
            if (font && typeof field.updateAppearances === "function") {
                try { field.updateAppearances(font); } catch (_) { /* ignore */ }
                try { window.applyPdfTextFieldDaAndSize(field, want.fontSize, font); } catch (_) { /* ignore */ }
                // Multiline after last appearance write
                try {
                    if (typeof field.enableMultiline === "function") {
                        window.applyPdfTextFieldMultiline(field, want.multiline);
                    }
                } catch (_) { /* ignore */ }
            }
        }
    } catch (e) {
        console.warn("[embedAllFormFields] final option re-stamp", e);
    }

    // Truth: fields on the form that have at least one widget on a page
    let live = 0;
    let withWidgets = 0;
    try {
        const fields = form.getFields();
        live = fields.length;
        fields.forEach((field) => {
            try {
                const widgets = field.acroField && field.acroField.getWidgets
                    ? field.acroField.getWidgets()
                    : [];
                if (widgets && widgets.length) withWidgets++;
            } catch (_) { /* ignore */ }
        });
    } catch (_) { live = -1; }

    // Prefer real widget count; fall back to our AcroForm success counter (not stamps)
    const written = withWidgets > 0 ? withWidgets : count;
    console.log(
        "[embedAllFormFields] widgets=", written,
        "stampedSigs=", stampedCount,
        "burnSigned=", burnSigned,
        "okSteps=", count,
        "failed=", failCount,
        "form.getFields()=", live,
        "fieldsWithPageWidgets=", withWidgets,
        "acroEligible=", acroEligible,
        "designerTotal=", designerTotal
    );

    if (written === 0 && acroEligible > 0) {
        console.error(
            "[embedAllFormFields] expected", acroEligible,
            "AcroForm field(s) but 0 page widgets — see failed logs above"
        );
    }
    return {
        widgets: written,
        stamped: stampedCount,
        failed: failCount,
        designerTotal,
        acroEligible
    };
};

/**
 * Drop signed form-signature shells from export schema so reopening Export PDF
 * does not re-import a fillable field on top of burned-in ink.
 * Optionally records a lightweight burnedSignatures[] audit list on the schema.
 * @param {object} schema
 * @returns {number} number of signed signature overlays removed
 */
window.stripBurnedSignedSignaturesFromSchema = function(schema) {
    if (!schema || !Array.isArray(schema.pages)) return 0;
    const burned = [];
    let removed = 0;
    schema.pages.forEach((p, pageIndex) => {
        if (!p || !Array.isArray(p.overlays)) return;
        const kept = [];
        p.overlays.forEach((o) => {
            if (!o) return;
            const isSigField = (o.type === "formField" || o.fieldType)
                && (o.fieldType === "signature" || o.type === "signature");
            const signed = isSigField
                && typeof window.isFormSignatureFieldSigned === "function"
                && window.isFormSignatureFieldSigned(o);
            if (signed) {
                removed++;
                burned.push({
                    pageIndex: pageIndex,
                    fieldName: o.fieldName || null,
                    fieldId: o.id || null,
                    signedName: (o.signatureAttestation && o.signatureAttestation.signedName)
                        || o.defaultValue
                        || null,
                    signedAt: (o.signatureAttestation && o.signatureAttestation.signedAt) || null,
                    attestationHash: (o.signatureAttestation && o.signatureAttestation.attestationHash)
                        || o.signatureAttestationHash
                        || null,
                    originHash: (o.signatureAttestation && o.signatureAttestation.originHash) || null,
                    burnedIn: true
                });
                return; // omit from overlays
            }
            kept.push(o);
        });
        p.overlays = kept;
    });
    if (burned.length) {
        schema.burnedSignatures = burned;
    }
    return removed;
};

/** @deprecated per-page helper — prefer embedAllFormFields */
window.embedFormFieldsOnPage = async function(pdfDoc, page, container, font) {
    // Minimal compatibility: wrap single page
    const fakeWrapper = { querySelector: () => container };
    // Need page index match — not reliable alone; callers should use embedAllFormFields
    const form = pdfDoc.getForm();
    const fields = window.serializeFormFieldsFromContainer(container);
    if (!fields.length) return 0;
    const pages = pdfDoc.getPages();
    const pageIndex = pages.indexOf(page);
    if (pageIndex < 0) return 0;
    // Build temp list of wrappers aligned to pages
    const wrappers = pages.map((_, i) => (i === pageIndex ? fakeWrapper : { querySelector: () => null }));
    return window.embedAllFormFields(pdfDoc, wrappers, font);
};
