// Some Helper functions (maybe put in utils.js)
// --- BUSY / LOADING UI ---
// Two layers:
//  1) Full-screen dim+spinner (showLoading) — multi-file combine, export, etc.
//  2) Soft doc gate (beginDocGate) — large single-PDF page render: viewing stays
//     free, but save/export/print are blocked until endDocGate.
// isAppBusy() is true for either. Doc gate is refcounted + safety-timeout so a
// failed render cannot permanently lock save/print.

window._ccsOverlayBusy = false;
window._ccsDocGateDepth = 0;
window._ccsDocGateSafetyTimer = null;
/** Max time a doc-gate can stay open without endDocGate (force clear). */
const CCS_DOC_GATE_SAFETY_MS = 30 * 60 * 1000;

window.isAppBusy = function() {
    return !!window._ccsOverlayBusy || (window._ccsDocGateDepth > 0);
};

/** Non-blocking status chip (does not capture clicks / does not dim the page). */
window.setDocLoadStatus = function(message) {
    let el = document.getElementById("ccs-doc-load-status");
    if (!message) {
        if (el && el.parentNode) el.parentNode.removeChild(el);
        return;
    }
    if (!el) {
        el = document.createElement("div");
        el.id = "ccs-doc-load-status";
        el.setAttribute("role", "status");
        el.setAttribute("aria-live", "polite");
        el.style.cssText = [
            "position:fixed", "bottom:14px", "left:50%", "transform:translateX(-50%)",
            "z-index:99990", "pointer-events:none",
            "background:rgba(20,20,20,0.9)", "color:#eee",
            "padding:8px 16px", "border-radius:8px",
            "font:13px/1.3 system-ui,sans-serif", "font-weight:600",
            "box-shadow:0 4px 16px rgba(0,0,0,0.4)",
            "border:1px solid rgba(0,170,255,0.45)",
            "max-width:90vw", "text-align:center"
        ].join(";");
        document.body.appendChild(el);
    }
    el.textContent = message;
};

window.beginDocGate = function(/* reason */) {
    window._ccsDocGateDepth = (window._ccsDocGateDepth || 0) + 1;
    if (window._ccsDocGateDepth === 1) {
        if (window._ccsDocGateSafetyTimer) {
            clearTimeout(window._ccsDocGateSafetyTimer);
        }
        window._ccsDocGateSafetyTimer = setTimeout(() => {
            if (window._ccsDocGateDepth > 0) {
                console.warn(
                    "[busy] doc gate safety clear after timeout; depth was",
                    window._ccsDocGateDepth
                );
                window._ccsDocGateDepth = 0;
                window.setDocLoadStatus(null);
            }
            window._ccsDocGateSafetyTimer = null;
        }, CCS_DOC_GATE_SAFETY_MS);
    }
};

window.endDocGate = function() {
    window._ccsDocGateDepth = Math.max(0, (window._ccsDocGateDepth || 0) - 1);
    if (window._ccsDocGateDepth === 0) {
        if (window._ccsDocGateSafetyTimer) {
            clearTimeout(window._ccsDocGateSafetyTimer);
            window._ccsDocGateSafetyTimer = null;
        }
        window.setDocLoadStatus(null);
    }
};

/** Emergency unlock (also used if anything leaves gate stuck). */
window.forceClearAppBusy = function() {
    window._ccsDocGateDepth = 0;
    if (window._ccsDocGateSafetyTimer) {
        clearTimeout(window._ccsDocGateSafetyTimer);
        window._ccsDocGateSafetyTimer = null;
    }
    window.setDocLoadStatus(null);
    window.hideLoading();
};

window.showLoading = function(message) {
    let loader = document.getElementById("ccs-export-loader");
    if (!loader) {
        loader = document.createElement("div");
        loader.id = "ccs-export-loader";
        // Below save/modals (1_000_000) but above app chrome
        loader.style.cssText = "position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.8); color:white; display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:99999; font-family:sans-serif; font-size:18px; font-weight:bold;";
        loader.innerHTML = `
            <style>@keyframes ccs-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }</style>
            <div style="margin-bottom:20px; border:5px solid rgba(255,255,255,0.2); border-top:5px solid #0af; border-radius:50%; width:50px; height:50px; animation:ccs-spin 1s linear infinite;"></div>
            <div id="ccs-export-text"></div>
        `;
        document.body.appendChild(loader);
    }
    const textEl = document.getElementById("ccs-export-text");
    if (textEl) textEl.innerText = message || "Working…";
    loader.style.display = "flex";
    loader.setAttribute("aria-busy", "true");
    loader.setAttribute("role", "alert");
    window._ccsOverlayBusy = true;
};

window.hideLoading = function() {
    const loader = document.getElementById("ccs-export-loader");
    if (loader) {
        loader.style.display = "none";
        loader.removeAttribute("aria-busy");
    }
    window._ccsOverlayBusy = false;
    // Do not clear doc gate here — large PDF renders may still be gating save/print
};

// Helper to let the browser breathe so the UI can actually update
const yieldToMain = () => new Promise(resolve => setTimeout(resolve, 15));

const stripPdfBaseName = (name) =>
    String(name || "document").replace(/(?:\.(?:pdf|ccspdf)|\s*\(\d+\)\s*)+$/ig, "").trim() || "document";

const ensureExtension = (name, ext) => {
    const clean = String(name || "").trim();
    if (!clean) return `document${ext}`;
    return clean.toLowerCase().endsWith(ext.toLowerCase()) ? clean : `${clean}${ext}`;
};

/**
 * Snapshot overlays + page metadata for CCSProject embedding
 * (Save Project / Export PDF). Does not copy PDF pages — only reads the live DOM.
 * @param {ParentNode[]|NodeList} wrappers
 * @param {{ pdfName?: string, purpose?: string }} [opts]
 * @returns {Promise<object>}
 */
window.collectProjectSchemaFromWrappers = async function(wrappers, opts) {
    const options = opts && typeof opts === "object" ? opts : {};
    const list = wrappers && wrappers.length != null ? Array.from(wrappers) : [];
    const schema = {
        version: (window.CcsProjectState && window.CcsProjectState.SCHEMA_VERSION) || "4.0",
        pdfName: options.pdfName || (stripPdfBaseName(window.currentPdfName) + ".pdf"),
        purpose: options.purpose || "project",
        pages: [],
        audit: []
    };

    for (let i = 0; i < list.length; i++) {
        const wrapper = list[i];
        if (!wrapper) continue;
        const pageData = {
            pageIndex: i,
            pageName: wrapper.dataset.pageName,
            pageId: wrapper.dataset.pageId || null,
            originalPageNum: wrapper.dataset.originalPageNum
                ? parseInt(wrapper.dataset.originalPageNum, 10)
                : null,
            overlays: []
        };
        wrapper.querySelectorAll(".overlayImg, .textOverlay, .shapeOverlay, .formFieldOverlay").forEach((overlay) => {
            const x = parseFloat(overlay.style.left);
            const y = parseFloat(overlay.style.top);
            const w = overlay.style.width === "max-content" ? "max-content" : parseFloat(overlay.style.width);
            const h = overlay.style.height === "auto" ? "auto" : parseFloat(overlay.style.height);
            const zParsed = parseInt(overlay.style.zIndex, 10);

            let objData = {
                x, y, w, h,
                id: overlay.id || crypto.randomUUID(),
                zIndex: Number.isFinite(zParsed) ? zParsed : null
            };

            if (overlay.classList.contains("formFieldOverlay")) {
                const snap = typeof window.serializeFormFieldElement === "function"
                    ? window.serializeFormFieldElement(overlay)
                    : null;
                if (snap) {
                    objData = Object.assign(objData, snap);
                    if (overlay.id) objData.id = overlay.id;
                } else {
                    objData.type = "formField";
                    objData.fieldType = overlay.dataset.fieldType || "text";
                    objData.fieldName = overlay.dataset.fieldName || "";
                    objData.required = overlay.dataset.required === "1";
                    objData.multiline = overlay.dataset.multiline === "1";
                    objData.defaultValue = overlay.dataset.defaultValue || "";
                    objData.fontSize = parseFloat(overlay.dataset.fontSize) || 12;
                    if (overlay.dataset.nativeId) objData.nativeId = overlay.dataset.nativeId;
                    if (overlay.dataset.importedNative) objData.importedNative = "1";
                    if (objData.fieldType === "radio") {
                        objData.optionValue = overlay.dataset.optionValue || "";
                    }
                    if (objData.fieldType === "dropdown") {
                        try { objData.options = JSON.parse(overlay.dataset.options || "[]"); }
                        catch (_) { objData.options = []; }
                    }
                }
            } else if (overlay.classList.contains("textOverlay")) {
                objData.type = "text";
                const textContent = overlay.querySelector(".textContent");
                objData.content = textContent ? textContent.innerHTML : overlay.innerHTML;
                objData.styles = {
                    fontFamily: overlay.style.fontFamily,
                    fontSize: parseFloat(overlay.style.fontSize),
                    color: overlay.style.color,
                    fontWeight: overlay.style.fontWeight,
                    lineHeight: overlay.style.lineHeight,
                    backgroundColor: overlay.style.backgroundColor,
                    border: overlay.style.border,
                    transform: overlay.style.transform,
                    minWidth: overlay.style.minWidth,
                    minHeight: overlay.style.minHeight,
                    boxSizing: overlay.style.boxSizing,
                    display: overlay.style.display,
                    paddingRight: overlay.style.paddingRight,
                    textAlign: textContent ? textContent.style.textAlign : overlay.style.textAlign,
                    textAlignLast: textContent ? textContent.style.textAlignLast : overlay.style.textAlignLast,
                    whiteSpace: textContent ? textContent.style.whiteSpace : overlay.style.whiteSpace,
                    overflowWrap: textContent ? textContent.style.overflowWrap : overlay.style.overflowWrap
                };
            } else if (overlay.classList.contains("overlayImg")) {
                objData.type = "image";
                const img = overlay.querySelector("img");
                if (img) objData.src = img.src;
                if (overlay.style.transform) {
                    objData.styles = { transform: overlay.style.transform };
                }
            } else if (overlay.classList.contains("shape-table")) {
                objData.type = "table";
                objData.dataset = { ...overlay.dataset };
                if (overlay.style.transform) {
                    objData.styles = { transform: overlay.style.transform };
                }
            } else if (overlay.classList.contains("shapeOverlay")) {
                objData.type = "shape";
                objData.shapeClass = overlay.className.includes("shape-circle") ? "circle"
                    : overlay.className.includes("shape-rect") ? "rect"
                    : "line";
                objData.styles = {
                    backgroundColor: overlay.style.backgroundColor,
                    border: overlay.style.border,
                    borderRadius: overlay.style.borderRadius,
                    transform: overlay.style.transform,
                    transformOrigin: overlay.style.transformOrigin
                };
            }
            pageData.overlays.push(objData);
        });
        schema.pages.push(pageData);
    }

    if (typeof window.getProjectAuditForSave === "function") {
        try {
            schema.audit = await window.getProjectAuditForSave();
        } catch (_) {
            schema.audit = [];
        }
    }

    // Mark form designer ownership when shells exist
    const formCount = schema.pages.reduce(
        (n, p) => n + (p.overlays || []).filter((o) => o && o.type === "formField").length,
        0
    );
    if (formCount > 0) schema.formFieldsManaged = true;

    return schema;
};

const canvasToPngBytes = (canvas) => new Promise((resolve, reject) => {
    canvas.toBlob(async (blob) => {
        if (!blob) return reject(new Error("canvas.toBlob returned null"));
        try {
            resolve(new Uint8Array(await blob.arrayBuffer()));
        } catch (e) {
            reject(e);
        }
    }, "image/png");
});

/**
 * Acquire a save destination WHILE user activation is still fresh.
 * Call this before long async work (html2canvas, pdf-lib save, etc).
 * @returns {null|object} null if cancelled
 */
window.acquireSaveDestination = async function({
    defaultFileName,
    description,
    accept,
    extension,
    filterName,
    filterExtensions,
    modalTitle
}) {
    const ext = extension.startsWith(".") ? extension : `.${extension}`;

    // 1) Tauri native dialog
    if (window.__TAURI__?.dialog?.save) {
        try {
            const filePath = await window.__TAURI__.dialog.save({
                defaultPath: defaultFileName,
                filters: [{
                    name: filterName || description,
                    extensions: filterExtensions || [ext.replace(".", "")]
                }]
            });
            if (!filePath) return null; // user cancelled
            return { kind: "tauri", filePath };
        } catch (e) {
            console.warn("Tauri save dialog failed, falling back to web:", e);
        }
    }

    // 2) Chromium File System Access API (must run under user gesture)
    if (typeof window.showSaveFilePicker === "function") {
        try {
            const fileHandle = await window.showSaveFilePicker({
                suggestedName: defaultFileName,
                types: [{ description, accept }]
            });
            return { kind: "handle", fileHandle };
        } catch (err) {
            if (err && err.name === "AbortError") return null;
            // NotAllowedError after long work, unsupported options, insecure context, etc.
            console.warn("showSaveFilePicker failed, using download modal:", err);
        }
    }

    // 3) Firefox / Safari / fallback: custom name modal + <a download>
    window.hideLoading();
    const userName = await window.promptSaveAsName(
        defaultFileName,
        modalTitle || "💾 Save File As"
    );
    if (!userName) return null;
    return { kind: "download", fileName: ensureExtension(userName, ext) };
};

/**
 * Write a Blob to a previously acquired destination.
 */
window.writeSaveDestination = async function(dest, blob) {
    if (!dest) return false;

    if (dest.kind === "tauri") {
        const fsApi = window.__TAURI__.fs;
        const bytes = new Uint8Array(await blob.arrayBuffer());
        if (typeof fsApi?.writeBinaryFile === "function") {
            await fsApi.writeBinaryFile(dest.filePath, bytes);
        } else if (typeof fsApi?.writeFile === "function") {
            await fsApi.writeFile(dest.filePath, bytes);
        } else {
            throw new Error("Tauri fs write API not available");
        }
        return true;
    }

    if (dest.kind === "handle") {
        const writable = await dest.fileHandle.createWritable();
        try {
            await writable.write(blob);
        } finally {
            await writable.close();
        }
        return true;
    }

    if (dest.kind === "download") {
        const link = document.createElement("a");
        const objectUrl = URL.createObjectURL(blob);
        link.href = objectUrl;
        link.download = dest.fileName;
        link.style.display = "none";
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        setTimeout(() => URL.revokeObjectURL(objectUrl), 1500);
        return true;
    }

    return false;
};

function pdfMagicOk(buf) {
    if (!buf || buf.byteLength < 8) return false;
    const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
    return u8[0] === 0x25 && u8[1] === 0x50 && u8[2] === 0x44 && u8[3] === 0x46; // %PDF
}

/**
 * Confirm bytes look like the project we just wrote (before wiping the tab).
 */
window.peekSavedProjectBytes = async function (arrayBuffer) {
    if (!pdfMagicOk(arrayBuffer)) {
        return { ok: false, reason: "not a PDF" };
    }
    if (window.CcsProjectState && typeof window.CcsProjectState.extractProjectStateFromBytes === "function") {
        try {
            const extracted = await window.CcsProjectState.extractProjectStateFromBytes(arrayBuffer.slice(0));
            if (extracted && extracted.abort) {
                return { ok: false, reason: "integrity" };
            }
            if (!extracted || !extracted.schema) {
                return { ok: false, reason: "missing project state" };
            }
        } catch (e) {
            return { ok: false, reason: (e && e.message) || "unreadable" };
        }
    }
    return { ok: true };
};

/**
 * After Save Project: load the written file and replace the active tab with it.
 *
 *   handle   — Chromium / installed PWA / WebView2 File System Access (read back from disk)
 *   download — Firefox / no picker (PWA fallback): reload the blob we handed to <a download>
 *
 * Never wipes the working tab unless the peek succeeds.
 * @returns {{ ok: boolean, skipped?: boolean, name?: string, reason?: string }}
 */
window.rebindTabToSavedProject = async function (dest, writtenBlob, fallbackName) {
    if (!dest) return { ok: false, skipped: true };

    let file = null;
    let handle = null;

    if (dest.kind === "handle" && dest.fileHandle && typeof dest.fileHandle.getFile === "function") {
        handle = dest.fileHandle;
        try {
            file = await handle.getFile();
            if (file && file.size < 64) {
                await new Promise((r) => setTimeout(r, 80));
                file = await handle.getFile();
            }
        } catch (e) {
            console.warn("[save-project] handle.getFile failed", e);
            return { ok: false, reason: "could not read saved file" };
        }
    } else if (dest.kind === "download" && writtenBlob) {
        // PWA / Firefox: no path back from Downloads — use the bytes we just emitted.
        const name = dest.fileName || fallbackName || "project.ccspdf";
        file = new File([writtenBlob], name, { type: "application/octet-stream" });
    } else {
        return { ok: false, skipped: true };
    }

    if (!file || file.size < 64) {
        return { ok: false, reason: "saved file is empty" };
    }

    let buf;
    try {
        buf = await file.arrayBuffer();
    } catch (e) {
        return { ok: false, reason: "could not read saved bytes" };
    }

    const peek = await window.peekSavedProjectBytes(buf);
    if (!peek.ok) return peek;

    const name = file.name || fallbackName || "project.ccspdf";
    const reloadFile = new File([buf], name, { type: file.type || "application/octet-stream" });

    try {
        if (window.CcsWorkspace && typeof window.CcsWorkspace.openFilesAsTabs === "function") {
            await window.CcsWorkspace.openFilesAsTabs(
                [{ file: reloadFile, handle: handle }],
                { replaceActive: true }
            );
        } else if (typeof window.renderPDF === "function") {
            await window.renderPDF(buf, false);
            if (window.CcsWorkspace && typeof window.CcsWorkspace.setActiveTitle === "function") {
                window.CcsWorkspace.setActiveTitle(name, handle);
            } else {
                window.currentPdfName = name;
            }
        } else {
            return { ok: false, reason: "open path not ready" };
        }
    } catch (e) {
        console.error("[save-project] reopen failed", e);
        return { ok: false, reason: (e && e.message) || "reopen failed" };
    }

    if (window.historyEngine) {
        window.historyEngine.savePoint = window.historyEngine.undoStack.length;
    }
    try { window.currentPdfName = name; } catch (_) { /* ignore */ }
    if (window.CcsWorkspace && typeof window.CcsWorkspace.persistSession === "function") {
        window.CcsWorkspace.persistSession().catch(() => {});
    }
    return { ok: true, name: name };
};

// ==========================================
// io.js: JSON SERIALIZATION & DESERIALIZATION
// ==========================================

window.pendingProjectData = null;

// --- STEP 1: THE SERIALIZER & BINARY APPENDER ---
window.exportProject = async function() {
    if (window.isAppBusy && window.isAppBusy()) {
        return window.customAlert(
            "Please wait for the current operation to finish before saving.",
            "⏳ Still working"
        );
    }
    const viewer = (APP.DOM && APP.DOM.viewer) || document.getElementById("viewer");
    const wrappers = viewer ? viewer.querySelectorAll(".pageWrapper") : document.querySelectorAll(".pageWrapper");
    if (wrappers.length === 0) return window.customAlert("No PDF pages found.");

    const baseName = stripPdfBaseName(window.currentPdfName);
    const defaultFileName = `${baseName}.ccspdf`;

    // Acquire destination first (user gesture still valid)
    // .ccspdf = CCS project package (valid PDF bytes + /CCSProject schema).
    // Other readers only show base pages + AcroForm — not overlay edits. Open in CCS to restore.
    const dest = await window.acquireSaveDestination({
        defaultFileName,
        description: "CCS PDF Project (re-editable in CCS PDF Editor)",
        accept: {
            "application/octet-stream": [".ccspdf"],
            "application/pdf": [".ccspdf"]
        },
        extension: ".ccspdf",
        filterName: "CCS PDF Project",
        filterExtensions: ["ccspdf"],
        modalTitle: "💾 Save Project As (.ccspdf)"
    });
    if (!dest) return;

    try {
        window.showLoading("Building project file...");
        await yieldToMain();

        const { PDFDocument } = window.PDFLib || PDFLib;
        const mergedPdf = await PDFDocument.create();
        const loadedSourceDocs = {};

        const schema = {
            // 4.0 = catalog-embedded CCSProject (+ optional dual trailer)
            version: (window.CcsProjectState && window.CcsProjectState.SCHEMA_VERSION) || "4.0",
            pdfName: defaultFileName,
            pages: [],
            // Rolling change insight (not a legal e-sign trail; see project-audit.js)
            audit: []
        };

        for (let i = 0; i < wrappers.length; i++) {
            if (i % 5 === 0) {
                window.showLoading(`Packing page ${i + 1} of ${wrappers.length}...`);
                await yieldToMain();
            }

            const wrapper = wrappers[i];
            const fingerprint = wrapper.dataset.pdfFingerprint;
            const origPageNum = parseInt(wrapper.dataset.pageId.split("_page_").pop(), 10) - 1;

            if (!loadedSourceDocs[fingerprint]) {
                const rawBytes = window.sourcePdfBuffers[fingerprint];
                if (rawBytes) {
                    try {
                        loadedSourceDocs[fingerprint] = typeof window.loadSourcePdfDocForExport === "function"
                            ? await window.loadSourcePdfDocForExport(rawBytes, fingerprint)
                            : await PDFDocument.load(rawBytes, { ignoreEncryption: true });
                    } catch (e) {
                        console.warn("Could not load source document:", e);
                    }
                }
            }

            try {
                if (loadedSourceDocs[fingerprint]) {
                    const [copiedPage] = await mergedPdf.copyPages(loadedSourceDocs[fingerprint], [origPageNum]);
                    mergedPdf.addPage(copiedPage);
                } else {
                    throw new Error("Missing source doc");
                }
            } catch (e) {
                console.warn(`Error copying page ${origPageNum}. Inserting blank fallback.`, e);
                mergedPdf.addPage([
                    parseFloat(wrapper.dataset.baseWidth) || 600,
                    parseFloat(wrapper.dataset.baseHeight) || 800
                ]);
            }

            const pageData = {
                pageIndex: i,
                pageName: wrapper.dataset.pageName,
                pageId: wrapper.dataset.pageId || null,
                originalPageNum: wrapper.dataset.originalPageNum
                    ? parseInt(wrapper.dataset.originalPageNum, 10)
                    : null,
                overlays: []
            };
            const overlays = wrapper.querySelectorAll(".overlayImg, .textOverlay, .shapeOverlay, .formFieldOverlay");

            overlays.forEach(overlay => {
                const x = parseFloat(overlay.style.left);
                const y = parseFloat(overlay.style.top);
                const w = overlay.style.width === "max-content" ? "max-content" : parseFloat(overlay.style.width);
                const h = overlay.style.height === "auto" ? "auto" : parseFloat(overlay.style.height);
                const zParsed = parseInt(overlay.style.zIndex, 10);

                let objData = {
                    x, y, w, h,
                    id: overlay.id || crypto.randomUUID(),
                    zIndex: Number.isFinite(zParsed) ? zParsed : null
                };

                if (overlay.classList.contains("formFieldOverlay")) {
                    const snap = typeof window.serializeFormFieldElement === "function"
                        ? window.serializeFormFieldElement(overlay)
                        : null;
                    if (snap) {
                        objData = Object.assign(objData, snap);
                        // geometry already on snap; keep id from overlay when present
                        if (overlay.id) objData.id = overlay.id;
                    } else {
                        objData.type = "formField";
                        objData.fieldType = overlay.dataset.fieldType || "text";
                        objData.fieldName = overlay.dataset.fieldName || "";
                        objData.required = overlay.dataset.required === "1";
                        objData.multiline = overlay.dataset.multiline === "1";
                        objData.defaultValue = overlay.dataset.defaultValue || "";
                        objData.fontSize = parseFloat(overlay.dataset.fontSize) || 12;
                        if (overlay.dataset.nativeId) objData.nativeId = overlay.dataset.nativeId;
                        if (overlay.dataset.importedNative) objData.importedNative = "1";
                        if (objData.fieldType === "radio") {
                            objData.optionValue = overlay.dataset.optionValue || "";
                        }
                        if (objData.fieldType === "dropdown") {
                            try { objData.options = JSON.parse(overlay.dataset.options || "[]"); }
                            catch (_) { objData.options = []; }
                        }
                    }
                } else if (overlay.classList.contains("textOverlay")) {
                    objData.type = "text";
                    const textContent = overlay.querySelector(".textContent");
                    objData.content = textContent ? textContent.innerHTML : overlay.innerHTML;
                    objData.styles = {
                        fontFamily: overlay.style.fontFamily,
                        fontSize: parseFloat(overlay.style.fontSize),
                        color: overlay.style.color,
                        fontWeight: overlay.style.fontWeight,
                        lineHeight: overlay.style.lineHeight,
                        backgroundColor: overlay.style.backgroundColor,
                        border: overlay.style.border,
                        transform: overlay.style.transform,
                        minWidth: overlay.style.minWidth,
                        minHeight: overlay.style.minHeight,
                        boxSizing: overlay.style.boxSizing,
                        display: overlay.style.display,
                        paddingRight: overlay.style.paddingRight,
                        textAlign: textContent ? textContent.style.textAlign : overlay.style.textAlign,
                        textAlignLast: textContent ? textContent.style.textAlignLast : overlay.style.textAlignLast,
                        whiteSpace: textContent ? textContent.style.whiteSpace : overlay.style.whiteSpace,
                        overflowWrap: textContent ? textContent.style.overflowWrap : overlay.style.overflowWrap
                    };
                } else if (overlay.classList.contains("overlayImg")) {
                    objData.type = "image";
                    const img = overlay.querySelector("img");
                    if (img) objData.src = img.src;
                    if (overlay.style.transform) {
                        objData.styles = { transform: overlay.style.transform };
                    }
                } else if (overlay.classList.contains("shape-table")) {
                    objData.type = "table";
                    objData.dataset = { ...overlay.dataset };
                    if (overlay.style.transform) {
                        objData.styles = { transform: overlay.style.transform };
                    }
                } else if (overlay.classList.contains("shapeOverlay")) {
                    objData.type = "shape";
                    objData.shapeClass = overlay.className.includes("shape-circle") ? "circle"
                        : overlay.className.includes("shape-rect") ? "rect"
                        : "line";
                    objData.styles = {
                        backgroundColor: overlay.style.backgroundColor,
                        border: overlay.style.border,
                        borderRadius: overlay.style.borderRadius,
                        transform: overlay.style.transform,
                        transformOrigin: overlay.style.transformOrigin
                    };
                }
                pageData.overlays.push(objData);
            });

            // Always record the page so restore page-matching stays stable,
            // even when all overlays were deleted.
            schema.pages.push(pageData);
        }

        // Forms: copyPages alone does not produce a reloadable AcroForm — always re-embed
        // when designer shells (or native widgets) are present.
        const wrapperListProj = Array.from(wrappers);
        const viewerProj = viewer || (APP.DOM && APP.DOM.viewer) || document;
        if (typeof window.syncDesignerFormValuesFromNatives === "function") {
            try { window.syncDesignerFormValuesFromNatives(viewerProj); } catch (_) { /* ignore */ }
        }
        let formJobsProj = typeof window.collectDesignerFormFieldsFromWrappers === "function"
            ? window.collectDesignerFormFieldsFromWrappers(wrapperListProj)
            : [];
        let designerCountProj = formJobsProj.reduce((n, j) => n + (j.fields ? j.fields.length : 0), 0);
        if (designerCountProj === 0 && typeof window.collectNativeFormFieldsFromWrappers === "function") {
            formJobsProj = window.collectNativeFormFieldsFromWrappers(wrapperListProj) || [];
            designerCountProj = formJobsProj.reduce((n, j) => n + (j.fields ? j.fields.length : 0), 0);
        }

        const domShellsProj = viewerProj.querySelectorAll
            ? viewerProj.querySelectorAll(".formFieldOverlay").length
            : 0;
        console.log("[exportProject] form snapshot=", designerCountProj, "domShells=", domShellsProj);

        if (designerCountProj > 0) {
            schema.formFieldsManaged = true;
            window.showLoading(`Writing ${designerCountProj} form field(s) into project...`);
            await yieldToMain();

            if (window.embedAllFormFields) {
                let formFont = null;
                try {
                    const { StandardFonts } = window.PDFLib || PDFLib;
                    formFont = await mergedPdf.embedFont(StandardFonts.Helvetica);
                } catch (_) { /* optional */ }
                // Project: do not burn signed signatures — keep schema re-editable
                const embedded = await window.embedAllFormFields(
                    mergedPdf, wrapperListProj, formFont, formJobsProj,
                    { burnSignedSignatures: false }
                );
                const widgets = (embedded && typeof embedded === "object")
                    ? (embedded.widgets || 0)
                    : (Number(embedded) || 0);
                console.log("[exportProject] embedded widgets=", widgets, embedded);
                if (widgets === 0 && designerCountProj > 0) {
                    console.error("[exportProject] DOM had", designerCountProj, "fields but embed wrote 0");
                }
            }
        } else if (domShellsProj > 0) {
            console.error("[exportProject] CRITICAL: dom shells but snapshot 0");
        }

        window.showLoading("Compressing project state...");
        await yieldToMain();

        if (typeof window.getProjectAuditForSave === "function") {
            schema.audit = await window.getProjectAuditForSave();
        }
        if (typeof window.logProjectAudit === "function") {
            window.logProjectAudit("SAVE_PROJECT", {
                pages: wrappers.length,
                auditEntries: Array.isArray(schema.audit) ? schema.audit.length : 0
            });
            // Include this save in the next open; re-snapshot after log
            if (typeof window.getProjectAuditForSave === "function") {
                schema.audit = await window.getProjectAuditForSave();
            }
        }

        window.showLoading("Embedding project state into PDF…");
        await yieldToMain();

        // Catalog /CCSProject only (no trailing ---CCS-SAVE-STATE--- append)
        let finalBytes;
        if (window.CcsProjectState && typeof window.CcsProjectState.savePdfWithProjectState === "function") {
            finalBytes = await window.CcsProjectState.savePdfWithProjectState(mergedPdf, schema, {
                dualWriteTrailer: false
            });
        } else if (window.CcsProjectState && typeof window.CcsProjectState.embedProjectStateInPdfDoc === "function") {
            await window.CcsProjectState.embedProjectStateInPdfDoc(mergedPdf, schema);
            finalBytes = await mergedPdf.save();
        } else {
            console.warn("[exportProject] CcsProjectState missing — saving PDF without embedded schema");
            finalBytes = await mergedPdf.save();
        }

        // octet-stream so OS/file pickers prefer .ccspdf association, not "open as PDF"
        const finalBlob = new Blob([finalBytes], { type: "application/octet-stream" });

        window.showLoading("Writing project…");
        await yieldToMain();
        await window.writeSaveDestination(dest, finalBlob);

        window.historyEngine.savePoint = window.historyEngine.undoStack.length;
        try {
            window.currentPdfName = (dest.kind === "download" && dest.fileName)
                ? dest.fileName
                : defaultFileName;
        } catch (_) { /* ignore */ }

        // PWA / Chromium: read the handle back. Download fallback: reopen our blob.
        // Only replace the tab if the written bytes actually open as a project.
        window.showLoading("Opening saved project…");
        await yieldToMain();
        const rebound = await window.rebindTabToSavedProject(dest, finalBlob, defaultFileName);
        if (!rebound.ok && !rebound.skipped) {
            window.hideLoading();
            await window.customAlert(
                "The project file was written, but it could not be re-opened to confirm the save"
                    + (rebound.reason ? " (" + rebound.reason + ")" : "")
                    + ".\n\nThis tab is still your working copy. "
                    + "Use File → Open on the new .ccspdf if it appears on disk.",
                "⚠ Save not confirmed"
            );
            return;
        }
    } catch (err) {
        console.error("exportProject failed:", err);
        window.hideLoading();
        await window.customAlert("An error occurred while saving the project file.");
        return;
    }

    window.hideLoading();
};

/**
 * Load a script once (works on file:// — script tags are not subject to wasm CORS).
 * Used to pull base64-embedded binaries without fetch/XHR.
 */
window.loadScriptOnce = function(src) {
    window._loadedScripts = window._loadedScripts || {};
    if (window._loadedScripts[src]) return window._loadedScripts[src];
    window._loadedScripts[src] = new Promise((resolve, reject) => {
        const existing = document.querySelector('script[src="' + src + '"]');
        if (existing) {
            if (existing.dataset.loaded === "1") return resolve();
            existing.addEventListener("load", () => resolve(), { once: true });
            existing.addEventListener("error", () => reject(new Error("Failed to load " + src)), { once: true });
            return;
        }
        const s = document.createElement("script");
        s.src = src;
        s.async = true;
        s.onload = () => { s.dataset.loaded = "1"; resolve(); };
        s.onerror = () => reject(new Error("Failed to load " + src));
        document.head.appendChild(s);
    });
    return window._loadedScripts[src];
};

/**
 * Build a blob: URL for qpdf.wasm from the base64 embed (file:// safe).
 * Emscripten can fetch/instantiate blob URLs; raw file:// .wasm is CORS-blocked.
 */
window.resolveQpdfWasmUrl = function() {
    if (window._qpdfWasmBlobUrl) return window._qpdfWasmBlobUrl;

    if (typeof window.__QPDF_WASM_B64 === "string" && window.__QPDF_WASM_B64.length > 0) {
        const decode = window.b64ToUint8Array || function(b64) {
            const bin = atob(b64);
            const out = new Uint8Array(bin.length);
            for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
            return out;
        };
        const bytes = decode(window.__QPDF_WASM_B64);
        const blob = new Blob([bytes], { type: "application/wasm" });
        window._qpdfWasmBlobUrl = URL.createObjectURL(blob);
        return window._qpdfWasmBlobUrl;
    }

    const scriptEl = document.querySelector('script[src*="qpdf.js"]');
    const scriptSrc = scriptEl ? scriptEl.src : "";
    if (scriptSrc) {
        return scriptSrc.replace(/qpdf\.js(\?.*)?$/i, "qpdf.wasm$1");
    }
    return new URL("lib/qpdf.wasm", window.location.href).href;
};

/**
 * Lazy-init QPDF (WASM). Content-preserving encryption — keeps AcroForm fields.
 */
window.getQpdfInstance = async function() {
    if (window._qpdfInstance) return window._qpdfInstance;

    const factory = window.QpdfModule || window.Module;
    if (typeof factory !== "function") {
        throw new Error("qpdf WASM module is not loaded.");
    }

    // file:// cannot fetch .wasm — pull the base64 embed via <script> first
    if (window.location.protocol === "file:" && !window.__QPDF_WASM_B64) {
        try {
            await window.loadScriptOnce("./lib/qpdf.wasm.embed.js");
        } catch (e) {
            console.error(e);
            throw new Error(
                "PDF encryption needs lib/qpdf.wasm.embed.js when opening via file:// " +
                "(browsers block loading .wasm from file URLs). Serve over http(s), use Tauri, " +
                "or restore the embed asset."
            );
        }
    }

    if (window.location.protocol === "file:" && !window.__QPDF_WASM_B64) {
        throw new Error(
            "PDF encryption embed loaded but window.__QPDF_WASM_B64 is empty. " +
            "Regenerate lib/qpdf.wasm.embed.js from lib/qpdf.wasm."
        );
    }

    const wasmUrl = window.resolveQpdfWasmUrl();

    window._qpdfInstance = await factory({
        locateFile: (file) => (String(file).endsWith(".wasm") ? wasmUrl : file),
        noInitialRun: true
    });
    return window._qpdfInstance;
};

/**
 * Decrypt password-protected PDF bytes with QPDF (client-side).
 * Used so export/project save can copyPages real content — pdf-lib's
 * ignoreEncryption loads structure but leaves page content blank.
 *
 * @param {Uint8Array|ArrayBuffer} pdfBytes
 * @param {string} userPassword
 * @returns {Promise<Uint8Array>}
 */
window.decryptPdfBytes = async function(pdfBytes, userPassword) {
    const password = String(userPassword || "");
    if (!password) throw new Error("Password is required for decryption.");

    const qpdf = await window.getQpdfInstance();
    const inPath = "/ccs_decrypt_in.pdf";
    const outPath = "/ccs_decrypt_out.pdf";
    const bytes = pdfBytes instanceof Uint8Array
        ? pdfBytes
        : new Uint8Array(pdfBytes instanceof ArrayBuffer ? pdfBytes : pdfBytes);

    try { qpdf.FS.unlink(inPath); } catch (_) { /* ok */ }
    try { qpdf.FS.unlink(outPath); } catch (_) { /* ok */ }

    qpdf.FS.writeFile(inPath, bytes);

    const args = [
        "--password=" + password,
        "--decrypt",
        inPath,
        outPath
    ];

    let code;
    try {
        code = qpdf.callMain(args);
    } catch (e) {
        console.error("qpdf decrypt threw", e);
        throw new Error("PDF decryption failed.");
    }

    if (code !== 0 && code !== undefined && code !== null && Number(code) !== 0) {
        throw new Error("PDF decryption failed (qpdf exit " + code + ").");
    }

    let out;
    try {
        out = qpdf.FS.readFile(outPath);
    } catch (e) {
        throw new Error("PDF decryption failed: no output file produced.");
    }

    try { qpdf.FS.unlink(inPath); } catch (_) { /* ok */ }
    try { qpdf.FS.unlink(outPath); } catch (_) { /* ok */ }

    return out instanceof Uint8Array ? out : new Uint8Array(out);
};

/**
 * Load source PDF bytes for page copy on export/project save.
 * Prefers cleartext; decrypts only with a password stored for THIS fingerprint
 * (never a sticky global password from another document).
 *
 * @param {ArrayBuffer|Uint8Array} rawBytes
 * @param {string} [fingerprint]
 * @returns {Promise<*>} pdf-lib PDFDocument
 */
window.loadSourcePdfDocForExport = async function(rawBytes, fingerprint) {
    const PDFLibNS = window.PDFLib || PDFLib;
    const { PDFDocument } = PDFLibNS;
    if (!rawBytes) throw new Error("Missing source PDF bytes");

    const toU8 = (b) => {
        if (b instanceof Uint8Array) return b;
        if (b instanceof ArrayBuffer) return new Uint8Array(b);
        if (b && b.buffer) return new Uint8Array(b.buffer, b.byteOffset || 0, b.byteLength);
        return new Uint8Array(b);
    };

    const tryLoad = async (bytes, opts) => PDFDocument.load(bytes, opts || {});

    // 1) Normal load (cleartext / already-decrypted buffers)
    try {
        return await tryLoad(rawBytes);
    } catch (e1) {
        console.log("[loadSourcePdfDocForExport] cleartext load failed:", e1 && e1.message);
    }

    // 2) Decrypt only if we know THIS fingerprint was unlocked with a password
    const pwMap = window._ccsPdfPasswordByFingerprint || {};
    const pw = (fingerprint && pwMap[fingerprint]) || null;
    if (pw && typeof window.decryptPdfBytes === "function") {
        try {
            const dec = await window.decryptPdfBytes(toU8(rawBytes), pw);
            if (fingerprint && window.sourcePdfBuffers) {
                const ab = dec.buffer.slice(dec.byteOffset, dec.byteOffset + dec.byteLength);
                window.sourcePdfBuffers[fingerprint] = ab;
            }
            return await tryLoad(dec);
        } catch (e2) {
            console.warn("[loadSourcePdfDocForExport] decrypt failed", e2);
        }
    }

    // 3) Last resort — structure only (often blank page content if still encrypted)
    console.warn("[loadSourcePdfDocForExport] falling back to ignoreEncryption (pages may be blank)");
    return tryLoad(rawBytes, { ignoreEncryption: true });
};

/**
 * Password-protect finished PDF bytes using QPDF (client-side, offline).
 * Preserves structure including fillable AcroForm fields.
 *
 * @param {Uint8Array} pdfBytes  Unencrypted PDF
 * @param {string} userPassword
 * @returns {Promise<Uint8Array>}
 */
window.encryptPdfBytes = async function(pdfBytes, userPassword) {
    const password = String(userPassword || "");
    if (!password) throw new Error("Password is required for encryption.");

    const qpdf = await window.getQpdfInstance();
    const inPath = "/ccs_encrypt_in.pdf";
    const outPath = "/ccs_encrypt_out.pdf";
    const bytes = pdfBytes instanceof Uint8Array ? pdfBytes : new Uint8Array(pdfBytes);

    try { qpdf.FS.unlink(inPath); } catch (_) { /* ok */ }
    try { qpdf.FS.unlink(outPath); } catch (_) { /* ok */ }

    qpdf.FS.writeFile(inPath, bytes);

    // AES-256; keep AcroForm fillable after open.
    // Use --modify=all (not annotate-only): some readers treat annotate as
    // "comments only" and refuse interactive form fill even when widgets exist.
    // See: qpdf --encrypt user owner keylen [restrictions] -- in out
    const args = [
        "--encrypt", password, password, "256",
        "--print=full",
        "--modify=all",
        "--extract=y",
        "--accessibility=y",
        "--",
        inPath,
        outPath
    ];

    let code;
    try {
        code = qpdf.callMain(args);
    } catch (e) {
        console.error("qpdf.callMain threw", e);
        throw new Error("PDF encryption failed. Try again or export without a password.");
    }

    if (code !== 0 && code !== undefined && code !== null) {
        // Some builds return undefined on success; non-zero is failure
        if (Number(code) !== 0) {
            throw new Error("PDF encryption failed (qpdf exit " + code + ").");
        }
    }

    let out;
    try {
        out = qpdf.FS.readFile(outPath);
    } catch (e) {
        throw new Error("PDF encryption failed: no output file produced.");
    }

    try { qpdf.FS.unlink(inPath); } catch (_) { /* ok */ }
    try { qpdf.FS.unlink(outPath); } catch (_) { /* ok */ }

    return out instanceof Uint8Array ? out : new Uint8Array(out);
};

/**
 * Ensure a page has an /Annots array pdf-lib can push widgets onto.
 * After stripping widgets we used to `node.delete(Annots)`, which leaves
 * normalizedEntries().Annots === undefined and makes addToPage throw:
 *   can't access property "push", this.normalizedEntries().Annots is undefined
 *
 * @param {*} pdfDoc
 * @param {*} page pdf-lib PDFPage
 * @returns {boolean}
 */
window.ensurePdfPageAnnotsArray = function(pdfDoc, page) {
    if (!pdfDoc || !page || !page.node) return false;
    const PDFLibNS = window.PDFLib || (typeof PDFLib !== "undefined" ? PDFLib : null);
    const PDFName = PDFLibNS && PDFLibNS.PDFName;
    const PDFArray = PDFLibNS && PDFLibNS.PDFArray;
    if (!PDFName || !pdfDoc.context) return false;

    const node = page.node;
    const key = PDFName.of("Annots");

    try {
        // Already a usable array?
        let annots = null;
        try {
            if (typeof node.Annots === "function") annots = node.Annots();
        } catch (_) { /* ignore */ }
        if (!annots && typeof node.lookupMaybe === "function" && PDFArray) {
            try { annots = node.lookupMaybe(key, PDFArray); } catch (_) { /* ignore */ }
        }
        if (annots && typeof annots.push === "function") return true;
        if (annots && typeof annots.size === "function") return true;
    } catch (_) { /* recreate below */ }

    try {
        const empty = pdfDoc.context.obj([]);
        if (typeof node.set === "function") {
            node.set(key, empty);
            return true;
        }
    } catch (e) {
        console.warn("[ensurePdfPageAnnotsArray]", e);
    }
    return false;
};

/**
 * Ensure every page in the document has a pushable /Annots array.
 * Call after strip, before any addToPage / addOptionToPage.
 */
window.ensureAllPdfPagesAnnotsArrays = function(pdfDoc) {
    if (!pdfDoc || typeof pdfDoc.getPages !== "function") return 0;
    let n = 0;
    try {
        pdfDoc.getPages().forEach((page) => {
            if (window.ensurePdfPageAnnotsArray(pdfDoc, page)) n++;
        });
    } catch (e) {
        console.warn("[ensureAllPdfPagesAnnotsArrays]", e);
    }
    return n;
};

/**
 * Drop pdf-lib's cached PDFForm so the next getForm() rebuilds against the
 * live catalog. Required only after /AcroForm was removed from the catalog.
 *
 * @param {*} pdfDoc pdf-lib PDFDocument
 */
window.invalidatePdfFormCache = function(pdfDoc) {
    if (!pdfDoc) return;
    try {
        // formCache is private in TS but present at runtime on pdf-lib builds we ship.
        if (pdfDoc.formCache && typeof pdfDoc.formCache.invalidate === "function") {
            pdfDoc.formCache.invalidate();
        }
    } catch (e) {
        console.warn("[invalidatePdfFormCache]", e);
    }
};

/** True if the document catalog currently has an /AcroForm entry. */
window.pdfDocHasCatalogAcroForm = function(pdfDoc) {
    if (!pdfDoc || !pdfDoc.catalog) return false;
    try {
        const PDFLibNS = window.PDFLib || (typeof PDFLib !== "undefined" ? PDFLib : null);
        const PDFName = PDFLibNS && PDFLibNS.PDFName;
        if (!PDFName) return false;
        const key = PDFName.of("AcroForm");
        if (typeof pdfDoc.catalog.has === "function" && pdfDoc.catalog.has(key)) return true;
        if (typeof pdfDoc.catalog.get === "function" && pdfDoc.catalog.get(key)) return true;
        if (typeof pdfDoc.catalog.lookup === "function" && pdfDoc.catalog.lookup(key)) return true;
        if (typeof pdfDoc.catalog.AcroForm === "function" && pdfDoc.catalog.AcroForm()) return true;
    } catch (_) { /* ignore */ }
    return false;
};

/**
 * Return a PDFForm that is attached to the catalog (safe for create* + save).
 * Fixes the orphan-form case: formCache still holds a PDFForm after /AcroForm
 * was deleted, so createTextField would write into a dict that never saves.
 *
 * @param {*} pdfDoc
 * @returns {*} PDFForm
 */
window.getLivePdfForm = function(pdfDoc) {
    if (!pdfDoc) return null;
    try {
        const cached = pdfDoc.formCache && typeof pdfDoc.formCache.getValue === "function"
            ? pdfDoc.formCache.getValue()
            : null;
        const catalogHas = window.pdfDocHasCatalogAcroForm(pdfDoc);
        // Orphan: cache points at a form but catalog no longer has AcroForm
        if (cached && !catalogHas) {
            window.invalidatePdfFormCache(pdfDoc);
        }
        // No form yet / after full strip — getForm creates a fresh catalog AcroForm
        return pdfDoc.getForm();
    } catch (e) {
        console.warn("[getLivePdfForm]", e);
        try {
            window.invalidatePdfFormCache(pdfDoc);
            return pdfDoc.getForm();
        } catch (e2) {
            console.warn("[getLivePdfForm] retry failed", e2);
            return null;
        }
    }
};

/**
 * Remove interactive form fields / widgets from a pdf-lib document.
 *
 * copyPages keeps page Widget annotations even when getForm() shows 0 fields.
 * form.removeField() alone is not enough.
 *
 * @param {*} pdfDoc pdf-lib PDFDocument
 * @param {{ prepareForReembed?: boolean }} [opts]
 *   prepareForReembed: true  → clear fields/widgets but KEEP a live catalog
 *   AcroForm so createTextField/etc. write into the document that will be saved.
 *   prepareForReembed: false → also drop /AcroForm (delete-all / ccspdf strip-only).
 * @returns {{ fields: number, widgets: number }}
 */
window.stripAcroFormFieldsFromPdfDoc = function(pdfDoc, opts) {
    const options = opts && typeof opts === "object" ? opts : {};
    const prepareForReembed = !!options.prepareForReembed;
    const result = { fields: 0, widgets: 0 };
    if (!pdfDoc) return result;

    const PDFLibNS = window.PDFLib || (typeof PDFLib !== "undefined" ? PDFLib : null);
    const PDFName = PDFLibNS && PDFLibNS.PDFName;
    const PDFArray = PDFLibNS && PDFLibNS.PDFArray;
    const nameOf = (n) => (PDFName ? PDFName.of(n) : null);

    // --- Pass 1: high-level form API ---
    let formRef = null;
    try {
        // Prefer live form; if catalog has no AcroForm yet, getForm creates one
        // (empty). That is fine — we will clear it and either keep or delete it.
        formRef = pdfDoc.getForm();
        const fields = formRef.getFields().slice();
        for (let i = 0; i < fields.length; i++) {
            try {
                formRef.removeField(fields[i]);
                result.fields++;
            } catch (e) {
                console.warn("[stripAcroForm] removeField failed", e);
            }
        }
        try {
            formRef.getFields().slice().forEach((f) => {
                try { formRef.removeField(f); result.fields++; } catch (_) { /* ignore */ }
            });
        } catch (_) { /* ignore */ }

        // Empty /Fields so leftover names cannot block re-create
        try {
            const acro = formRef.acroForm;
            if (acro && acro.dict && PDFName) {
                acro.dict.set(nameOf("Fields"), pdfDoc.context.obj([]));
                if (typeof acro.dict.delete === "function") {
                    try { acro.dict.delete(nameOf("XFA")); } catch (_) { /* ignore */ }
                }
            }
        } catch (e) {
            console.warn("[stripAcroForm] clear Fields failed", e);
        }
    } catch (e) {
        console.warn("[stripAcroForm] getForm failed", e);
    }

    // --- Pass 2: page-level Widget annotations ---
    try {
        const pages = pdfDoc.getPages();

        for (let p = 0; p < pages.length; p++) {
            const page = pages[p];
            const node = page.node;
            if (!node) continue;

            let annots = null;
            try {
                if (typeof node.Annots === "function") annots = node.Annots();
            } catch (_) { /* ignore */ }

            if (!annots && PDFName && typeof node.lookupMaybe === "function") {
                try {
                    annots = node.lookupMaybe(nameOf("Annots"), PDFArray);
                } catch (_) { /* ignore */ }
            }

            if (!annots) continue;

            const widgetRefs = [];
            const size = typeof annots.size === "function" ? annots.size() : 0;
            for (let i = 0; i < size; i++) {
                let ref;
                try { ref = annots.get(i); } catch (_) { continue; }
                if (!ref) continue;

                let dict = null;
                try {
                    dict = pdfDoc.context.lookup(ref);
                } catch (_) {
                    try { dict = ref; } catch (__) { /* ignore */ }
                }
                if (!dict || typeof dict.get !== "function") continue;

                let subtype = null;
                try {
                    subtype = dict.get(nameOf("Subtype"));
                } catch (_) { /* ignore */ }

                let subStr = "";
                try {
                    if (subtype && typeof subtype.toString === "function") subStr = subtype.toString();
                    else if (subtype && typeof subtype.encodedName === "string") subStr = subtype.encodedName;
                    else if (subtype != null) subStr = String(subtype);
                } catch (_) { subStr = ""; }

                if (/Widget/i.test(subStr)) {
                    widgetRefs.push(ref);
                }
            }

            for (let w = widgetRefs.length - 1; w >= 0; w--) {
                const ref = widgetRefs[w];
                try {
                    if (typeof node.removeAnnot === "function") {
                        node.removeAnnot(ref);
                        result.widgets++;
                    } else if (typeof annots.indexOf === "function" && typeof annots.remove === "function") {
                        const idx = annots.indexOf(ref);
                        if (idx !== undefined && idx !== -1 && idx >= 0) {
                            annots.remove(idx);
                            result.widgets++;
                        }
                    }
                } catch (e) {
                    console.warn("[stripAcroForm] removeAnnot failed", e);
                }
            }

            // Do NOT delete /Annots when empty. pdf-lib addToPage does
            //   this.normalizedEntries().Annots.push(...)
            // and throws if Annots is undefined after a full delete.
            // Leave an empty Annots array (or recreate one) so re-embed works.
            try {
                if (typeof window.ensurePdfPageAnnotsArray === "function") {
                    window.ensurePdfPageAnnotsArray(pdfDoc, page);
                }
            } catch (_) { /* ignore */ }
        }
    } catch (e) {
        console.warn("[stripAcroForm] page Annots pass failed", e);
    }

    // --- Pass 3: catalog cleanup depends on whether we will re-embed ---
    if (prepareForReembed) {
        // KEEP catalog /AcroForm and formCache so subsequent create* writes
        // into a form that save() will actually serialize.
        // Ensure AcroForm exists (getForm creates it if strip pass1 never ran).
        try {
            if (!window.pdfDocHasCatalogAcroForm(pdfDoc)) {
                window.invalidatePdfFormCache(pdfDoc);
                pdfDoc.getForm();
            }
        } catch (e) {
            console.warn("[stripAcroForm] ensure AcroForm for re-embed", e);
        }
    } else {
        // Full wipe: drop /AcroForm and drop stale form cache (project delete-all).
        try {
            if (PDFName && pdfDoc.catalog && typeof pdfDoc.catalog.delete === "function") {
                try { pdfDoc.catalog.delete(nameOf("AcroForm")); } catch (_) { /* ignore */ }
            }
        } catch (e) {
            console.warn("[stripAcroForm] catalog cleanup", e);
        }
        window.invalidatePdfFormCache(pdfDoc);
    }

    console.log(
        "[stripAcroForm] removed fields=", result.fields,
        "widgets=", result.widgets,
        "prepareForReembed=", prepareForReembed
    );
    return result.fields + result.widgets;
};

// --- EXPORT TO PDF (FLATTEN DECORATIVE OVERLAYS + REAL ACROFORM FIELDS) ---
/**
 * html2canvas captures the PDF.js *visual* page (upright after /Rotate).
 * Page content in the file lives in MediaBox space; /Rotate only affects viewers.
 * Re-orient the capture so drawImage(0,0,mediaW,mediaH) lines up with page content
 * (otherwise signatures/text go sideways on rotated pages — print was fine because
 * print uses the visual DOM).
 *
 * @param {HTMLCanvasElement} visualCanvas
 * @param {number} pageRotate degrees clockwise (0|90|180|270), from page /Rotate
 * @returns {HTMLCanvasElement}
 */
window.reorientVisualCanvasToPdfMedia = function(visualCanvas, pageRotate) {
    const r = ((Number(pageRotate) || 0) % 360 + 360) % 360;
    if (!visualCanvas || r === 0) return visualCanvas;

    const sw = visualCanvas.width;
    const sh = visualCanvas.height;
    const out = document.createElement("canvas");
    const ctx = out.getContext("2d");
    if (!ctx) return visualCanvas;
    ctx.imageSmoothingEnabled = true;
    ctx.imageSmoothingQuality = "high";

    // Canvas positive rotate is clockwise (Y-down).
    // Display applies /Rotate CW to media → media = inverse(display) of visual.
    if (r === 90) {
        // media = visual rotated 90° CCW
        out.width = sh;
        out.height = sw;
        ctx.translate(0, out.height);
        ctx.rotate(-Math.PI / 2);
        ctx.drawImage(visualCanvas, 0, 0);
    } else if (r === 180) {
        out.width = sw;
        out.height = sh;
        ctx.translate(out.width, out.height);
        ctx.rotate(Math.PI);
        ctx.drawImage(visualCanvas, 0, 0);
    } else if (r === 270) {
        // media = visual rotated 90° CW (Canon lease PDFs, etc.)
        out.width = sh;
        out.height = sw;
        ctx.translate(out.width, 0);
        ctx.rotate(Math.PI / 2);
        ctx.drawImage(visualCanvas, 0, 0);
    } else {
        return visualCanvas;
    }
    return out;
};

/**
 * Map a point from visual CSS (top-left, y-down on the PDF.js viewport)
 * into PDF media user space (bottom-left, y-up on the MediaBox).
 */
window.visualCssPointToPdfMedia = function(xCss, yCss, visualW, visualH, mediaW, mediaH, pageRotate) {
    const r = ((Number(pageRotate) || 0) % 360 + 360) % 360;
    const vx = Number(xCss) || 0;
    const vyTop = Number(yCss) || 0;
    // Visual bottom-left style for this point
    const vxb = vx;
    const vyb = (Number(visualH) || 0) - vyTop;

    if (r === 0) {
        return { x: vxb, y: vyb };
    }
    if (r === 90) {
        // Inverse of 90 CW display ≈ 90 CCW: (vxb, vyb) → (mediaW - vyb, vxb) in BL? 
        // Match canvas reorient (90 CCW of top-left image → media):
        // media top-left (mxT, myT) = (vyTop, visualW - vx) roughly then BL...
        // Using same as canvas 90 CCW: media_x = vyb, media_y = vxb (with dims mediaW=visualH, mediaH=visualW)
        return { x: vyb, y: vxb };
    }
    if (r === 180) {
        return {
            x: (Number(mediaW) || 0) - vxb,
            y: (Number(mediaH) || 0) - vyb
        };
    }
    if (r === 270) {
        // Inverse of 270 CW display = 90 CW on visual BL points
        // canvas 90 CW: media_x = visualH - vyTop = vyb, wait:
        // From canvas: (sx,sy) top-left → (outW - sy, sx) top-left media
        // media_x_tl = visualH - vyTop = vyb... vyb = visualH - vyTop, so media_x_tl = vyb? 
        // outW = visualH (in CSS px), media_x_tl = outW - sy = visualH - vyTop = vyb
        // media_y_tl = sx = vx
        // media BL: x = media_x_tl, y = mediaH - media_y_tl = mediaH - vx
        return {
            x: vyb,
            y: (Number(mediaH) || 0) - vxb
        };
    }
    return { x: vxb, y: vyb };
};

// exportScaleOrOpts: number (legacy) or { exportScale, password }
window.exportPdf = async function(exportScaleOrOpts = 2) {
    if (window.isAppBusy && window.isAppBusy()) {
        return window.customAlert(
            "Please wait for the current operation to finish before exporting.",
            "⏳ Still working"
        );
    }
    const opts = (typeof exportScaleOrOpts === "object" && exportScaleOrOpts !== null)
        ? exportScaleOrOpts
        : { exportScale: exportScaleOrOpts };
    const exportScale = parseFloat(opts.exportScale) || 2;
    const exportPassword = opts.password ? String(opts.password) : "";

    const viewerEl = (APP.DOM && APP.DOM.viewer) || document.getElementById("viewer");
    const wrappers = viewerEl ? viewerEl.querySelectorAll(".pageWrapper") : document.querySelectorAll(".pageWrapper");
    if (wrappers.length === 0) return window.customAlert("No PDF pages found.");

    const hasAnyDecorative = viewerEl
        ? viewerEl.querySelector(".textOverlay, .shapeOverlay, .overlayImg")
        : document.querySelector(".textOverlay, .shapeOverlay, .overlayImg");

    if (hasAnyDecorative && !window.html2canvas) {
        return window.customAlert("Error: html2canvas is not loaded. Please check your index.html scripts.");
    }

    if (exportPassword && typeof window.QpdfModule !== "function" && typeof window.Module !== "function" && !window._qpdfInstance) {
        return window.customAlert(
            "Password protection is unavailable: the encryption module (qpdf) did not load. Export without a password, or contact IT."
        );
    }

    const baseName = stripPdfBaseName(window.currentPdfName);
    const defaultFileName = `${baseName}_Final.pdf`;

    // CRITICAL: open save picker / name modal BEFORE long async work.
    // Flattened export for any PDF reader (stamps baked in; form fields interactive)
    const dest = await window.acquireSaveDestination({
        defaultFileName,
        description: "PDF Document (flattened export)",
        accept: { "application/pdf": [".pdf"] },
        extension: ".pdf",
        filterName: "PDF Document",
        filterExtensions: ["pdf"],
        modalTitle: "📤 Export PDF As"
    });
    if (!dest) return;

    // Avoid burning Form-mode dimming (opacity) into flattened annotations
    const runExport = async () => {
        window.showLoading("Initializing PDF export...");
        await yieldToMain();

        // Always build with main PDFLib so AcroForm fields match the known-good path.
        // Password protection is a post-step via PDFLibEncrypt (see encryptPdfBytes).
        const PDFLibNS = window.PDFLib || PDFLib;
        const { PDFDocument, StandardFonts } = PDFLibNS;
        const mergedPdf = await PDFDocument.create();
        const loadedSourceDocs = {};
        let formFont = null;
        try {
            formFont = await mergedPdf.embedFont(StandardFonts.Helvetica);
        } catch (e) {
            console.warn("Could not embed Helvetica for form appearances:", e);
        }

        if (document.activeElement) document.activeElement.blur();
        if (window.setActiveOverlay) window.setActiveOverlay(null);

        let totalFormFields = 0;

        for (let i = 0; i < wrappers.length; i++) {
            const wrapper = wrappers[i];
            const container = wrapper.querySelector(".pageContainer");
            const fingerprint = wrapper.dataset.pdfFingerprint;
            const origPageNum = parseInt(wrapper.dataset.pageId.split("_page_").pop(), 10) - 1;

            const decorative = container
                ? container.querySelectorAll(".textOverlay, .shapeOverlay, .overlayImg")
                : [];
            const formFields = container
                ? container.querySelectorAll(".formFieldOverlay")
                : [];
            const hasDecorative = decorative.length > 0;
            const hasForms = formFields.length > 0;

            window.showLoading(
                hasDecorative
                    ? `Flattening page ${i + 1} of ${wrappers.length}...`
                    : hasForms
                        ? `Adding form fields (page ${i + 1} of ${wrappers.length})...`
                        : `Copying page ${i + 1} of ${wrappers.length}...`
            );
            await yieldToMain();

            if (!loadedSourceDocs[fingerprint]) {
                const rawBytes = window.sourcePdfBuffers?.[fingerprint];
                if (rawBytes) {
                    try {
                        loadedSourceDocs[fingerprint] = typeof window.loadSourcePdfDocForExport === "function"
                            ? await window.loadSourcePdfDocForExport(rawBytes, fingerprint)
                            : await PDFDocument.load(rawBytes, { ignoreEncryption: true });
                    } catch (e) {
                        console.warn("Could not load source doc:", e);
                    }
                }
            }

            let activePage;
            try {
                if (loadedSourceDocs[fingerprint]) {
                    const [copiedPage] = await mergedPdf.copyPages(loadedSourceDocs[fingerprint], [origPageNum]);
                    activePage = mergedPdf.addPage(copiedPage);
                } else {
                    throw new Error("Missing doc");
                }
            } catch (e) {
                console.warn(`Error copying page for export (fingerprint=${fingerprint}):`, e);
                activePage = mergedPdf.addPage([
                    parseFloat(wrapper.dataset.baseWidth) || 600,
                    parseFloat(wrapper.dataset.baseHeight) || 800
                ]);
            }

            if (!container) continue;

            // 1) Rasterize decorative overlays only (never burn form designer chrome into the PDF)
            if (hasDecorative) {
                const { width: pdfW, height: pdfH } = activePage.getSize();
                const pdfCanvas = container.querySelector("canvas");
                if (pdfCanvas) pdfCanvas.style.visibility = "hidden";

                // Hide form field widgets so they are not flattened as purple boxes
                formFields.forEach(f => { f.style.visibility = "hidden"; });

                const handles = container.querySelectorAll(
                    ".textControls, .textDragHandle, .rotateHandle, .resizeHandle, .deleteHandle"
                );
                handles.forEach(h => { h.style.display = "none"; });

                const originalOutlines = [];
                const originalBlobSrcs = new Map();

                try {
                    for (const o of decorative) {
                        originalOutlines.push(o.style.outline);
                        o.style.outline = "none";
                        o.classList.remove("overlay-active");

                        if (o.classList.contains("overlayImg")) {
                            const img = o.querySelector("img");
                            if (img && img.src.startsWith("blob:")) {
                                originalBlobSrcs.set(img, img.src);
                                try {
                                    const blob = await fetch(img.src).then(r => r.blob());
                                    const base64 = await new Promise((res, rej) => {
                                        const reader = new FileReader();
                                        reader.onloadend = () => res(reader.result);
                                        reader.onerror = rej;
                                        reader.readAsDataURL(blob);
                                    });
                                    img.src = base64;
                                } catch (e) {
                                    console.warn("Failed to convert blob for snapshot", e);
                                }
                            }
                        }
                    }

                    const overlayCanvas = await window.html2canvas(container, {
                        scale: exportScale,
                        backgroundColor: null,
                        useCORS: true,
                        logging: false
                    });

                    try {
                        // Page /Rotate (e.g. 270 on landscape MediaBox leases): re-orient
                        // upright capture into MediaBox space before embed.
                        let pageRotate = 0;
                        try {
                            pageRotate = activePage.getRotation
                                ? activePage.getRotation().angle
                                : 0;
                        } catch (_) { pageRotate = 0; }
                        if (!pageRotate && wrapper.dataset.pageRotate) {
                            pageRotate = parseInt(wrapper.dataset.pageRotate, 10) || 0;
                        }

                        const mediaCanvas = (typeof window.reorientVisualCanvasToPdfMedia === "function")
                            ? window.reorientVisualCanvasToPdfMedia(overlayCanvas, pageRotate)
                            : overlayCanvas;

                        if (pageRotate) {
                            console.log(
                                "[exportPdf] page", i + 1,
                                "rotate=", pageRotate,
                                "visualCanvas=", overlayCanvas.width, "x", overlayCanvas.height,
                                "mediaCanvas=", mediaCanvas.width, "x", mediaCanvas.height,
                                "pageSize=", pdfW, "x", pdfH
                            );
                        }

                        const pngBytes = await canvasToPngBytes(mediaCanvas);
                        const embeddedImg = await mergedPdf.embedPng(pngBytes);
                        activePage.drawImage(embeddedImg, {
                            x: 0,
                            y: 0,
                            width: pdfW,
                            height: pdfH
                        });
                    } catch (err) {
                        console.warn("Failed to embed layer on page", i, err);
                    }
                } finally {
                    if (pdfCanvas) pdfCanvas.style.visibility = "";
                    formFields.forEach(f => { f.style.visibility = ""; });
                    handles.forEach(h => { h.style.display = ""; });
                    decorative.forEach((o, idx) => {
                        o.style.outline = originalOutlines[idx] || "";
                        if (o.classList.contains("overlayImg")) {
                            const img = o.querySelector("img");
                            if (img && originalBlobSrcs.has(img)) {
                                img.src = originalBlobSrcs.get(img);
                            }
                        }
                    });
                }
            }

        }

        // 2) Form fields — CRITICAL:
        // pdf-lib copyPages leaves page Widget annots but getForm() is empty and
        // save/reload loses fillable fields. Always strip + re-embed from DOM shells.
        const wrapperList = Array.from(wrappers);
        const viewerForForms = viewerEl || document;

        const domShellCount = viewerForForms.querySelectorAll
            ? viewerForForms.querySelectorAll(".formFieldOverlay").length
            : 0;

        if (typeof window.syncDesignerFormValuesFromNatives === "function") {
            try { window.syncDesignerFormValuesFromNatives(viewerForForms); } catch (e) {
                console.warn("[exportPdf] syncDesignerFormValuesFromNatives", e);
            }
        }

        let formJobs = typeof window.collectDesignerFormFieldsFromWrappers === "function"
            ? window.collectDesignerFormFieldsFromWrappers(wrapperList)
            : [];
        let designerCount = formJobs.reduce((n, j) => n + (j.fields ? j.fields.length : 0), 0);

        if (designerCount === 0 && typeof window.collectNativeFormFieldsFromWrappers === "function") {
            formJobs = window.collectNativeFormFieldsFromWrappers(wrapperList) || [];
            designerCount = formJobs.reduce((n, j) => n + (j.fields ? j.fields.length : 0), 0);
            if (designerCount > 0) {
                console.log("[exportPdf] native widget snapshot:", designerCount);
            }
        }

        console.log(
            "[exportPdf] form snapshot=", designerCount,
            "domShells=", domShellCount,
            "pages=", wrapperList.length
        );

        let stampedSignatures = 0;
        let acroEligible = 0;
        if (designerCount > 0 && window.embedAllFormFields) {
            window.showLoading(`Writing form field(s)...`);
            await yieldToMain();
            // burnSignedSignatures: ink+caption on page; signed field is NOT re-importable
            const emb = await window.embedAllFormFields(
                mergedPdf, wrapperList, formFont, formJobs,
                { burnSignedSignatures: true }
            );
            if (emb && typeof emb === "object") {
                totalFormFields = emb.widgets || 0;
                stampedSignatures = emb.stamped || 0;
                acroEligible = emb.acroEligible != null ? emb.acroEligible : totalFormFields;
            } else {
                totalFormFields = Number(emb) || 0;
                acroEligible = totalFormFields;
            }
            console.log(
                "[exportPdf] widgets written:", totalFormFields,
                "stamped signatures:", stampedSignatures,
                "acroEligible:", acroEligible
            );
        } else if (domShellCount > 0) {
            console.error(
                "[exportPdf] CRITICAL: DOM has", domShellCount,
                "formFieldOverlay shell(s) but snapshot count is 0 — collection failed"
            );
        } else {
            console.warn(
                "[exportPdf] no form shells to re-embed; copyPages AcroForm alone is NOT reliable"
            );
        }

        // Embed CCS project schema (overlays + audit) so Save PDF still reloads
        // editably in this app — even though decorative layers are flattened.
        try {
            window.showLoading("Embedding project state…");
            await yieldToMain();
            if (typeof window.syncDesignerFormValuesFromNatives === "function") {
                try { window.syncDesignerFormValuesFromNatives(viewerForForms); } catch (_) { /* ignore */ }
            }
            const exportSchema = typeof window.collectProjectSchemaFromWrappers === "function"
                ? await window.collectProjectSchemaFromWrappers(wrapperList, {
                    pdfName: defaultFileName,
                    purpose: "export-pdf"
                })
                : null;
            if (exportSchema && window.CcsProjectState
                && typeof window.CcsProjectState.embedProjectStateInPdfDoc === "function") {
                if (typeof window.logProjectAudit === "function") {
                    window.logProjectAudit("SAVE_PDF", {
                        pages: wrapperList.length,
                        formFields: totalFormFields,
                        stampedSignatures,
                        flattened: true
                    });
                    if (typeof window.getProjectAuditForSave === "function") {
                        try {
                            exportSchema.audit = await window.getProjectAuditForSave();
                        } catch (_) { /* keep prior */ }
                    }
                }
                exportSchema.flattenedExport = true;
                // Signed signatures are burned into page pixels — omit fillable shells
                // so reopen does not stack a form field on top of the ink.
                if (typeof window.stripBurnedSignedSignaturesFromSchema === "function") {
                    const n = window.stripBurnedSignedSignaturesFromSchema(exportSchema);
                    if (n) console.log("[exportPdf] omitted", n, "signed signature field(s) from schema");
                }
                const meta = await window.CcsProjectState.embedProjectStateInPdfDoc(
                    mergedPdf,
                    exportSchema
                );
                console.log(
                    "[exportPdf] CCSProject embedded gzipBytes=",
                    meta && meta.bytes,
                    "hash=", meta && meta.hash && meta.hash.slice(0, 12) + "…"
                );
            }
        } catch (embErr) {
            console.warn("[exportPdf] project state embed failed (PDF still exported)", embErr);
        }

        window.showLoading("Finalizing PDF...");
        await yieldToMain();

        let mergedBytes = await mergedPdf.save({
            updateFieldAppearances: true
        });

        // Verify AcroForm widgets only when we expected fillable fields (not stamp-only)
        let verifiedFields = -1;
        if (acroEligible > 0 || totalFormFields > 0) {
            try {
                const checkDoc = await PDFDocument.load(mergedBytes);
                verifiedFields = checkDoc.getForm().getFields().length;
                console.log("[exportPdf] verify reload getForm().getFields()=", verifiedFields);
            } catch (ve) {
                console.warn("[exportPdf] verify reload failed", ve);
            }
        }

        let wasEncrypted = false;
        if (exportPassword) {
            window.showLoading("Encrypting PDF (keeping form fields)...");
            await yieldToMain();
            mergedBytes = await window.encryptPdfBytes(mergedBytes, exportPassword);
            wasEncrypted = true;
        }

        const finalBlob = new Blob([mergedBytes], { type: "application/pdf" });

        window.showLoading("Writing file...");
        await yieldToMain();
        await window.writeSaveDestination(dest, finalBlob);

        // Fail only if fillable widgets were expected and none verified.
        // Stamp-only exports (signed signatures, no other fields) are success.
        const embedFailed = acroEligible > 0 && (totalFormFields === 0 || verifiedFields === 0);
        const shouldAlert = totalFormFields > 0 || stampedSignatures > 0
            || wasEncrypted || embedFailed || domShellCount > 0;
        if (shouldAlert) {
            setTimeout(() => {
                let msg = "";
                if (embedFailed) {
                    msg = "Form fields could not be written into the PDF (wrote " +
                        totalFormFields + ", verified " + verifiedFields + ").\n\n" +
                        "Open the browser console (F12) and look for [embedAllFormFields] / [exportPdf] logs.";
                } else {
                    const parts = [];
                    if (totalFormFields > 0) {
                        parts.push(totalFormFields + " fillable form widget(s)");
                    }
                    if (stampedSignatures > 0) {
                        parts.push(stampedSignatures + " signed signature(s) burned into the page");
                    }
                    if (parts.length) {
                        msg = "Saved with " + parts.join(" and ") + ".\n\n";
                        if (totalFormFields > 0) {
                            msg += "Open the PDF in a PDF reader to fill remaining fields, then save a copy.";
                        }
                        if (stampedSignatures > 0) {
                            msg += (totalFormFields > 0 ? "\n\n" : "")
                                + "Signed form signatures are final on this export (not re-signable here). "
                                + "Use Save Project (.ccspdf) if you still need to re-sign later.";
                        }
                    } else if (domShellCount > 0 && designerCount === 0) {
                        msg = "Form shells were visible in the editor but none were collected for export.\n\n" +
                            "Open F12 console for [exportPdf] CRITICAL logs.";
                    }
                }
                if (wasEncrypted) {
                    if (msg) msg += "\n\n";
                    msg += "This file is password-protected. A password is required to open it. " +
                        "The password is not stored by CCS PDF Editor and cannot be recovered if forgotten.";
                }
                window.customAlert(
                    msg || "PDF saved.",
                    embedFailed ? "⚠ Export problem"
                        : (totalFormFields > 0 || stampedSignatures > 0 || wasEncrypted ? "✅ PDF ready" : "⚠ PDF saved")
                );
            }, 200);
        }
    };

    try {
        if (window.withCaptureSafeAppearance) {
            await window.withCaptureSafeAppearance(runExport);
        } else {
            await runExport();
        }
    } catch (err) {
        console.error("exportPdf failed:", err);
        window.hideLoading();
        await window.customAlert("An error occurred while exporting the PDF.");
        return;
    }

    window.hideLoading();
};

/**
 * Resolve a schema page entry to a live .pageWrapper (multi-strategy).
 * Avoids false "page missing" when pageId drifted or pageIndex is string/out of sync.
 */
window.resolveProjectPageWrapper = function(pageData, wrappersArr, scopeRoot) {
    if (!pageData || !wrappersArr || !wrappersArr.length) return null;
    const root = scopeRoot || document;

    // 1) pageIndex (0-based; accept numeric strings)
    const idx = pageData.pageIndex;
    if (idx !== undefined && idx !== null && idx !== "") {
        const i = parseInt(idx, 10);
        if (!Number.isNaN(i) && i >= 0 && i < wrappersArr.length) {
            return wrappersArr[i];
        }
    }

    // 2) exact pageId
    if (pageData.pageId) {
        const hit = wrappersArr.find((w) => w.dataset && w.dataset.pageId === String(pageData.pageId));
        if (hit) return hit;
        try {
            const q = root.querySelector(`.pageWrapper[data-page-id="${CSS.escape ? CSS.escape(String(pageData.pageId)) : String(pageData.pageId).replace(/"/g, '\\"')}"]`);
            if (q && wrappersArr.includes(q)) return q;
        } catch (_) { /* ignore */ }
    }

    // 3) original page number
    const orig = pageData.originalPageNum != null
        ? parseInt(pageData.originalPageNum, 10)
        : (pageData.pageId && /_page_(\d+)$/.test(String(pageData.pageId))
            ? parseInt(RegExp.$1, 10)
            : NaN);
    if (!Number.isNaN(orig)) {
        const hit = wrappersArr.find((w) => parseInt(w.dataset.originalPageNum, 10) === orig);
        if (hit) return hit;
    }

    // 4) single-page document: always map to the only wrapper
    if (wrappersArr.length === 1) return wrappersArr[0];

    return null;
};

// --- STEP 2: THE DESERIALIZER ---
window.applyProjectData = function(schema, rootViewer) {
    let totalApplied = 0; let missingPages = 0;
    
    const viewerEl = rootViewer
        || (APP.DOM && APP.DOM.viewer)
        || document.getElementById("viewer");
    const allWrappers = Array.from(
        viewerEl
            ? viewerEl.querySelectorAll(".pageWrapper")
            : document.querySelectorAll(".pageWrapper")
    );

    if (typeof window.loadProjectAudit === "function") {
        window.loadProjectAudit(schema);
    }

    // Project saved after form import/delete — mark managed so further exports strip natives
    if (schema && schema.formFieldsManaged && typeof window.markFormFieldsManaged === "function") {
        window.markFormFieldsManaged(viewerEl || undefined);
    }

    const pages = Array.isArray(schema && schema.pages) ? schema.pages : [];

    pages.forEach((pageData) => {
        if (!pageData) return;

        const targetWrapper = window.resolveProjectPageWrapper(pageData, allWrappers, viewerEl || document);

        // Empty page records with no overlays — never treat as "missing"
        const overlayCount = Array.isArray(pageData.overlays) ? pageData.overlays.length : 0;
        if (!targetWrapper) {
            if (overlayCount > 0) missingPages++;
            return;
        }

        const container = targetWrapper.querySelector(".pageContainer");
        if (!container) {
            if (overlayCount > 0) missingPages++;
            return;
        }

        // Restore optional page labels
        if (pageData.pageName) targetWrapper.dataset.pageName = pageData.pageName;
        if (pageData.hoverTitle) targetWrapper.dataset.hoverTitle = pageData.hoverTitle;

        const skipBurnedDecor = !!(schema && schema.flattenedExport);
        (pageData.overlays || []).forEach(obj => {
            if (!obj) return;

            // Save PDF burns text/shapes/images into page pixels — do not re-spawn them
            // (would double-draw). Remaining form fields stay interactive and are re-applied.
            if (skipBurnedDecor && obj.type !== "formField") return;

            // Signed form signatures were burned on Export PDF and stripped from schema;
            // if an older export still has them, skip so we do not stack over ink.
            if (skipBurnedDecor && obj.type === "formField" && obj.fieldType === "signature"
                && typeof window.isFormSignatureFieldSigned === "function"
                && window.isFormSignatureFieldSigned(obj)) {
                return;
            }

            // Skip only if this id already exists in THIS viewer (not another tab).
            // document.getElementById caused intermittent multi-tab / stale-DOM skips.
            if (obj.id) {
                const esc = (window.CSS && CSS.escape)
                    ? CSS.escape(String(obj.id))
                    : String(obj.id).replace(/(["\\])/g, "\\$1");
                let existsHere = false;
                try {
                    existsHere = !!(viewerEl
                        ? viewerEl.querySelector("#" + esc)
                        : document.getElementById(obj.id));
                } catch (_) {
                    existsHere = !!(viewerEl
                        ? viewerEl.querySelector(`[id="${String(obj.id).replace(/"/g, '\\"')}"]`)
                        : document.getElementById(obj.id));
                }
                if (existsHere) {
                    totalApplied++;
                    return;
                }
            }

            const el = document.createElement("div");
            el.style.left = obj.x + "px"; el.style.top = obj.y + "px";
            el.style.width = obj.w === "max-content" ? "max-content" : obj.w + "px";
            el.style.height = obj.h === "auto" ? "auto" : obj.h + "px";
            el.id = obj.id;
            if (obj.zIndex != null && Number.isFinite(Number(obj.zIndex))) {
                el.style.zIndex = String(Math.round(Number(obj.zIndex)));
            }

            if (obj.type === "formField") {
                if (window.restoreFormFieldOverlay) {
                    window.restoreFormFieldOverlay(container, obj);
                    totalApplied++;
                }
                return;
            }
            else if (obj.type === "text") {
                el.className = "textOverlay";
                el.style.fontFamily = obj.styles.fontFamily;
                el.style.fontSize = obj.styles.fontSize + "px"; 
                el.style.color = obj.styles.color; 
                el.style.fontWeight = obj.styles.fontWeight; 
                el.style.lineHeight = obj.styles.lineHeight;
                el.style.backgroundColor = obj.styles.backgroundColor || "transparent"; 
                el.style.border = obj.styles.border || "none";

                if (obj.styles.transform) el.style.transform = obj.styles.transform;
                if (window.syncOverlayChrome) window.syncOverlayChrome(el);

                if (obj.styles.minWidth) el.style.minWidth = obj.styles.minWidth;
                if (obj.styles.minHeight) el.style.minHeight = obj.styles.minHeight;
                if (obj.styles.boxSizing) el.style.boxSizing = obj.styles.boxSizing;
                if (obj.styles.display) el.style.display = obj.styles.display;
                if (obj.styles.paddingRight) el.style.paddingRight = obj.styles.paddingRight;

                const textContent = document.createElement("div"); 
                textContent.className = "textContent"; 
                textContent.contentEditable = "true";
                textContent.innerHTML = window.sanitizeHTML(obj.content); 
                textContent.style.width = "100%"; 
                textContent.style.outline = "none"; 
                
                textContent.style.textAlign = obj.styles.textAlign || "left";
                if (obj.styles.textAlignLast) textContent.style.textAlignLast = obj.styles.textAlignLast;
                if (obj.styles.whiteSpace) textContent.style.whiteSpace = obj.styles.whiteSpace;
                if (obj.styles.overflowWrap) textContent.style.overflowWrap = obj.styles.overflowWrap;
                
                const controls = document.createElement("div"); 
                controls.className = "textControls"; 
                controls.addEventListener("mousedown", e => e.stopPropagation());
                
                const toolbarObj = window.createRichTextToolbar(textContent, el); 
                controls.appendChild(toolbarObj.element);
                toolbarObj.sync(obj.styles.fontFamily, obj.styles.fontSize, obj.styles.color, obj.styles.fontWeight, obj.styles.lineHeight);

                const dragHandle = document.createElement("div"); 
                dragHandle.className = "textDragHandle"; 
                dragHandle.innerHTML = "⠿";
                
                // ✨ THE FIX: Add the Rotate Handle
                const rotateHandle = document.createElement("div"); 
                rotateHandle.className = "rotateHandle"; 
                rotateHandle.innerHTML = "&#8635;"; 
                rotateHandle.style.cursor = "grab";
                
                const resizeHandle = document.createElement("div"); 
                resizeHandle.className = "resizeHandle";
                
                const deleteHandle = window.createDeleteHandle(el);
                
                // ✨ THE FIX: Append rotateHandle to the text overlay
                el.append(controls, dragHandle, rotateHandle, deleteHandle, textContent, resizeHandle);
                window.makeDraggable(el, resizeHandle, dragHandle);
            } 
            else if (obj.type === "image") {
                el.className = "overlayImg";
                const img = document.createElement("img"); img.src = obj.src; img.style.width = "100%"; img.style.height = "100%"; img.style.pointerEvents = "none";
                
                if (obj.styles && obj.styles.transform) el.style.transform = obj.styles.transform;
                if (window.syncOverlayChrome) window.syncOverlayChrome(el);

                const dragHandle = document.createElement("div"); dragHandle.className = "textDragHandle"; dragHandle.innerHTML = "⠿";
                
                const rotateHandle = document.createElement("div"); rotateHandle.className = "rotateHandle"; rotateHandle.innerHTML = "&#8635;"; rotateHandle.style.cursor = "grab";
                
                const resizeHandle = document.createElement("div"); resizeHandle.className = "resizeHandle"; const deleteHandle = window.createDeleteHandle(el);
                
                el.append(img, dragHandle, rotateHandle, resizeHandle, deleteHandle); window.makeDraggable(el, resizeHandle, dragHandle);
            } 
            else if (obj.type === "table") {
                el.className = "shapeOverlay shape-table";
                Object.assign(el.dataset, obj.dataset);
                window.renderTableGrid(el);
                window.buildTableControls(el);
                
                if (obj.styles && obj.styles.transform) el.style.transform = obj.styles.transform;
                if (window.syncOverlayChrome) window.syncOverlayChrome(el);

                const dragHandle = document.createElement("div"); dragHandle.className = "textDragHandle"; dragHandle.innerHTML = "⠿";
                
                const rotateHandle = document.createElement("div"); rotateHandle.className = "rotateHandle"; rotateHandle.innerHTML = "&#8635;"; rotateHandle.style.cursor = "grab";
                
                const deleteHandle = window.createDeleteHandle(el);
                
                el.append(dragHandle, rotateHandle, deleteHandle); window.makeDraggable(el, null, dragHandle);
            }
            else if (obj.type === "shape") {
                const shapeKind = obj.shapeClass || "rect";
                el.className = `shapeOverlay shape-${shapeKind}`;
                el.style.backgroundColor = obj.styles.backgroundColor; el.style.border = obj.styles.border; el.style.borderRadius = obj.styles.borderRadius;
                if (shapeKind === "circle") el.style.borderRadius = el.style.borderRadius || "50%";
                if (obj.styles.transform) el.style.transform = obj.styles.transform;
                if (obj.styles.transformOrigin) el.style.transformOrigin = obj.styles.transformOrigin;
                if (window.syncOverlayChrome) window.syncOverlayChrome(el);
                window.buildShapeControls(el, shapeKind);
                
                const dragHandle = document.createElement("div"); dragHandle.className = "textDragHandle"; dragHandle.innerHTML = "⠿";
                
                // ✨ THE FIX: Add Rotate Handle to Shapes
                const rotateHandle = document.createElement("div"); rotateHandle.className = "rotateHandle"; rotateHandle.innerHTML = "&#8635;"; rotateHandle.style.cursor = "grab";
                
                const resizeHandle = document.createElement("div"); resizeHandle.className = "resizeHandle"; const deleteHandle = window.createDeleteHandle(el);
                
                // ✨ THE FIX: Append rotateHandle
                el.append(dragHandle, rotateHandle, resizeHandle, deleteHandle); window.makeDraggable(el, resizeHandle, dragHandle);
            }

            container.appendChild(el);
            // Legacy saves without zIndex: place restored items on top in schema order
            // (no CREATE_NODE tape during bulk restore — use processor stack API directly)
            if ((obj.zIndex == null || !Number.isFinite(Number(obj.zIndex)))
                && window.CcsProcessor && typeof window.CcsProcessor.assignOverlayZIndex === "function") {
                window.CcsProcessor.assignOverlayZIndex(el, container);
            }
            totalApplied++;
        });
    });

    if (totalApplied > 0 && window.CcsProcessor) {
        // Snapshot all applied overlay nodes as a compound CREATE tape
        const steps = [];
        schema.pages.forEach((p) => {
            (p.overlays || []).forEach((o) => {
                const target = o.id ? document.getElementById(o.id) : null;
                if (target && target.isConnected) {
                    steps.push(window.CcsProcessor.build.createNode(target, "Restore overlay"));
                }
            });
        });
        if (steps.length) {
            window.CcsProcessor.commit(
                window.CcsProcessor.build.compound(steps, "Loaded Save State")
            );
        }
    }

    // Post-restore: hide native AcroForm widgets that have designer twins
    // (otherwise original PDF positions reappear next to moved shells)
    if (typeof window.scheduleSuppressNativesForDesignerTwins === "function") {
        window.scheduleSuppressNativesForDesignerTwins(viewerEl || undefined);
    } else if (typeof window.suppressNativesForDesignerTwins === "function") {
        window.suppressNativesForDesignerTwins(viewerEl || undefined);
    }

    // Only warn when overlays were expected but their host pages are truly absent
    if (missingPages > 0 && totalApplied === 0 && allWrappers.length === 0) {
        window.customAlert(
            `Could not restore project overlays — no pages are loaded in the viewer.`,
            "⚠️ Restore incomplete"
        );
    } else if (missingPages > 0 && totalApplied > 0) {
        window.customAlert(
            `Loaded ${totalApplied} edit(s), but ${missingPages} page(s) from the save file could not be matched to the current document.`,
            "⚠️ Partial restore"
        );
    } else if (missingPages > 0 && totalApplied === 0 && allWrappers.length > 0) {
        // Pages are on screen; schema page keys just didn't match — quiet skip (no scary false alarm)
        console.info(
            "[applyProjectData] page key mismatch but viewer has",
            allWrappers.length,
            "page(s); restored",
            totalApplied,
            "overlay(s). missingPageRecords=",
            missingPages
        );
    }

    if (typeof window.syncPageThumbnails === "function") window.syncPageThumbnails();
    window.historyEngine.savePoint = window.historyEngine.undoStack.length;
};

const projectInput = document.getElementById("projectInput");
const pendingModal = document.getElementById("pendingProjectModal");
const fileMenuSelect = document.getElementById("fileMenuSelect");

fileMenuSelect.addEventListener("change", async (e) => {
    const val = e.target.value;
    
    if (val === "open") {
        // Prefer showOpenFilePicker so we get FileSystemFileHandles for session restore
        if (typeof window.showOpenFilePicker === "function" && window.isSecureContext) {
            try {
                const handles = await window.showOpenFilePicker({
                    multiple: true,
                    types: [{
                        description: "PDF / CCS Project",
                        accept: {
                            "application/pdf": [".pdf"],
                            "application/octet-stream": [".ccspdf"]
                        }
                    }]
                });
                const items = [];
                for (const handle of handles) {
                    try {
                        const file = await handle.getFile();
                        items.push({ file, handle });
                    } catch (err) {
                        console.warn("open picker getFile failed", err);
                    }
                }
                if (items.length) {
                    // Always go through processPdfFiles so Append vs New Tab is offered
                    // when the active document already has pages (and multi-file choices).
                    await window.processPdfFiles(items.map((i) => i.file), false, {
                        handles: items.map((i) => i.handle)
                    });
                    e.target.value = "default";
                    return;
                }
            } catch (err) {
                if (err && err.name === "AbortError") {
                    e.target.value = "default";
                    return;
                }
                console.warn("showOpenFilePicker failed, falling back to <input>", err);
            }
        }
        document.getElementById("fileInput").click();
    } else if (val === "save_project") {
        window.exportProject(); 
    } else if (val === "save_pdf") {
        // ✨ Show the modal instead of exporting immediately
        document.getElementById("exportQualityModal").style.display = "flex";
    } else if (val === "load") {
        projectInput.click();
    } else if (val === "print") {
        window.printBtn();
    } else if (val === "close") {
        if (window.CcsWorkspace && typeof window.CcsWorkspace.closeActiveDocument === "function") {
            await window.CcsWorkspace.closeActiveDocument();
        } else if (APP.DOM && APP.DOM.viewer) {
            // Classic single-doc: clear canvas
            const dirty = window.historyEngine
                && (historyEngine.undoStack.length || 0) !== (historyEngine.savePoint || 0);
            if (dirty && typeof window.customConfirm === "function") {
                const ok = await window.customConfirm(
                    "Close this document? Unsaved changes will be lost.",
                    "⚠️ Close"
                );
                if (!ok) {
                    e.target.value = "default";
                    return;
                }
            }
            APP.DOM.viewer.innerHTML = "";
            window.currentPdfName = "Untitled";
            if (window.historyEngine) {
                historyEngine.undoStack = [];
                historyEngine.redoStack = [];
                historyEngine.savePoint = 0;
            }
            if (typeof window.updateHistoryUI === "function") window.updateHistoryUI();
            if (window.CcsWorkspace && typeof window.CcsWorkspace.appTitleWithVersion === "function") {
                document.title = window.CcsWorkspace.appTitleWithVersion();
            } else {
                const base = (window.ccsConfig && window.ccsConfig("appTitle", "CCS PDF Editor")) || "CCS PDF Editor";
                const raw = String((window.ccsConfig && window.ccsConfig("version", "")) || "");
                const m = raw.match(/^(\d+(?:\.\d+)*)/);
                document.title = m ? `${base} ${m[1]}` : base;
            }
        }
    } else if (val === "close_all") {
        if (window.CcsWorkspace && typeof window.CcsWorkspace.closeAllDocuments === "function") {
            await window.CcsWorkspace.closeAllDocuments();
        } else if (window.CcsWorkspace && typeof window.CcsWorkspace.closeActiveDocument === "function") {
            await window.CcsWorkspace.closeActiveDocument();
        }
    }
    
    e.target.value = "default";
});

// launchQueue consumer lives in workspace.js (keeps FileSystemFileHandle + multi-tab).
// combine-mode query is handled there / via processPdfFiles forceAppend.


document.getElementById("pendingProjectCancelBtn").addEventListener("click", () => { window.pendingProjectData = null; pendingModal.style.display = "none"; });
document.getElementById("pendingProjectOpenBtn").addEventListener("click", () => { document.getElementById("fileInput").click(); });
// Password fields show/hide on export modal
(function wireExportPasswordUi() {
    const enable = document.getElementById("exportPasswordEnable");
    const fields = document.getElementById("exportPasswordFields");
    if (!enable || !fields) return;
    enable.addEventListener("change", () => {
        fields.style.display = enable.checked ? "block" : "none";
        if (enable.checked) {
            const inp = document.getElementById("exportPasswordInput");
            if (inp) setTimeout(() => inp.focus(), 0);
        } else {
            const a = document.getElementById("exportPasswordInput");
            const b = document.getElementById("exportPasswordConfirm");
            if (a) a.value = "";
            if (b) b.value = "";
        }
    });
})();

document.getElementById("exportQualityCancelBtn").addEventListener("click", () => {
    document.getElementById("exportQualityModal").style.display = "none";
    const enable = document.getElementById("exportPasswordEnable");
    const a = document.getElementById("exportPasswordInput");
    const b = document.getElementById("exportPasswordConfirm");
    if (enable) enable.checked = false;
    if (a) a.value = "";
    if (b) b.value = "";
    const fields = document.getElementById("exportPasswordFields");
    if (fields) fields.style.display = "none";
});

document.getElementById("exportQualityConfirmBtn").addEventListener("click", () => {
    const selectedScale = parseFloat(document.getElementById("exportQualitySelect").value) || 2;
    const enablePw = document.getElementById("exportPasswordEnable");
    const pwInput = document.getElementById("exportPasswordInput");
    const pwConfirm = document.getElementById("exportPasswordConfirm");

    let password = "";
    if (enablePw && enablePw.checked) {
        const p1 = (pwInput && pwInput.value) || "";
        const p2 = (pwConfirm && pwConfirm.value) || "";
        if (!p1) {
            window.customAlert("Enter a password, or uncheck password protection.");
            if (pwInput) pwInput.focus();
            return;
        }
        if (p1.length < 4) {
            window.customAlert("Use a password of at least 4 characters.");
            if (pwInput) pwInput.focus();
            return;
        }
        if (p1 !== p2) {
            window.customAlert("Passwords do not match.");
            if (pwConfirm) pwConfirm.focus();
            return;
        }
        password = p1;
    }

    document.getElementById("exportQualityModal").style.display = "none";
    // Clear password fields from the modal immediately (not stored elsewhere)
    if (pwInput) pwInput.value = "";
    if (pwConfirm) pwConfirm.value = "";
    if (enablePw) enablePw.checked = false;
    const fields = document.getElementById("exportPasswordFields");
    if (fields) fields.style.display = "none";

    // Fire immediately so showSaveFilePicker still has user activation
    window.exportPdf({ exportScale: selectedScale, password });
});
