/* ============================================================
   PPphp Designer — App bootstrap
   Orchestrates: refresh state from server -> pick initial page
   -> render -> wire autosave / explicit save / preview / keys.
   ============================================================ */
(function () {
    'use strict';
    var PPD = window.PPD = window.PPD || {};
    var store = PPD.store, model = PPD.model, reg = PPD.registry;
    var esc = reg.esc, title = reg.title;

    function qs(s, r) { return (r || document).querySelector(s); }

    var saveTimer = null;
    var saving = false;

    function setSaveStatus(text, kind) {
        var el = qs('#ppdSaveStatus');
        if (!el) return;
        el.textContent = '● ' + text;
        el.className = 'ppd-save-status ' + (kind === 'err' ? 'err' : kind === 'saving' ? 'saving' : kind === 'dirty' ? 'dirty' : 'ok');
    }
    function scheduleSave() {
        clearTimeout(saveTimer);
        saveTimer = setTimeout(doSave, 1000);
    }
    /* ---------------- incremental save ----------------
       The whole design is one JSON document, and re-POSTing all of it after
       every keystroke-sized edit gets expensive as a project grows (one page
       slot is ~3 KB; a 20-table project is ~470 KB). So we compare the state
       against the last copy the server acknowledged and send only the slots
       that differ.

       The changed slots are DIFFED FROM THE DATA rather than tracked with
       dirty flags: a missed flag would silently lose an edit, while a diff can
       only ever be too generous. Anything we cannot express as a slot change —
       a new top-level key, a structural rewrite — falls back to a full save,
       as does a `needFull` answer from the server. */
    var lastSaved = null;        // deep copy of the state the server confirmed
    var stateToken = '';         // server's token for that state

    // How deep a slot lives under each root: pages.<table>.<type>, templates.<type>
    var DELTA_ROOTS = { pages: 2, templates: 1, common: 1 };

    function stable(v) { return JSON.stringify(v === undefined ? null : v); }

    function diffSlots(prev, next, depth, basePath, ops) {
        var keys = {}, k;
        for (k in (prev || {})) keys[k] = 1;
        for (k in (next || {})) keys[k] = 1;
        for (k in keys) {
            var a = prev ? prev[k] : undefined, b = next ? next[k] : undefined;
            if (stable(a) === stable(b)) continue;
            if (depth > 1 && a && b && typeof a === 'object' && typeof b === 'object') {
                diffSlots(a, b, depth - 1, basePath.concat(k), ops);
            } else {
                ops.push({ path: basePath.concat(k), value: b === undefined ? null : b });
            }
        }
    }

    // → array of ops, or null when a full save is required
    function computeDelta(prev, next) {
        if (!prev || !next) return null;
        var ops = [], k, seen = {};
        for (k in prev) seen[k] = 1;
        for (k in next) seen[k] = 1;
        for (k in seen) {
            if (DELTA_ROOTS[k]) continue;
            // any other top-level key changed (settings, active, version…) → full save
            if (stable(prev[k]) !== stable(next[k])) return null;
        }
        for (k in DELTA_ROOTS) {
            diffSlots(prev[k] || {}, next[k] || {}, DELTA_ROOTS[k], [k], ops);
        }
        if (!ops.length) return [];
        // no point sending a "delta" that is most of the document
        if (stable(ops).length > stable(next).length * 0.6) return null;
        return ops;
    }

    function doSave() {
        if (saving) { scheduleSave(); return; }
        if (!store.isDirty()) return;
        // edits to a Default Page Template propagate to every table that has that page
        if (store.ui('scope') === 'template') PPD.model.applyTemplate(store.ui('pageType'));
        var state = store.designer();
        var ops = stateToken ? computeDelta(lastSaved, state) : null;

        if (ops && !ops.length) {          // dirty flag set but nothing actually differs
            store.markDirty(false);
            setSaveStatus('Saved', 'ok');
            return;
        }
        saving = true;
        setSaveStatus('Saving…', 'saving');

        var snapshot = JSON.parse(JSON.stringify(state));
        var req = ops ? PPD.api.saveDelta(ops, stateToken) : PPD.api.save(state);

        req.then(function (res) {
            // stored state moved on (another tab/user) → resend everything once
            if (res && res.needFull) {
                return PPD.api.save(state).then(function (full) {
                    if (full && full.success) { lastSaved = snapshot; stateToken = full.stateToken || ''; store.markDirty(false); }
                    setSaveStatus(full && full.success ? 'Saved' : 'Save failed', full && full.success ? 'ok' : 'err');
                });
            }
            if (res && res.success) {
                lastSaved = snapshot;
                stateToken = res.stateToken || '';
                store.markDirty(false);
            }
            setSaveStatus(res && res.success ? 'Saved' : 'Save failed', res && res.success ? 'ok' : 'err');
        }).catch(function () {
            // never leave a stale baseline behind — the next save resends in full
            lastSaved = null; stateToken = '';
            setSaveStatus('Offline', 'err');
        }).finally(function () { saving = false; });
    }
    // exposed for tests (the retry/token paths guard against cross-tab data loss,
    // so they need to be exercisable without a browser)
    PPD.__saveDelta = {
        compute: computeDelta, roots: DELTA_ROOTS, save: doSave,
        state: function () { return { hasBaseline: !!lastSaved, token: stateToken }; },
        reset: function () { lastSaved = null; stateToken = ''; }
    };

    function pickInitial() {
        var d = store.designer();
        var u = store.ui();
        // 1) Reopen the page this browser last worked on (per project) so the user
        //    lands back where they left off without clicking. Falls through to the
        //    server-saved "active" page, then the first table, if it's gone.
        if (PPD.panels.restoreLastPage && PPD.panels.restoreLastPage()) return;
        // honour persisted "active" if present
        var a = d.active || {};
        if (a.scope === 'template') {
            return PPD.panels.selectTemplate(a.pageType || 'list');
        }
        if (a.scope === 'common' && a.pageType && d.common && d.common[a.pageType]) {
            return PPD.panels.selectCommon(a.pageType);
        }
        var table = (a.table && d.pages && d.pages[a.table]) ? a.table : (store.tables()[0] && store.tables()[0].name);
        if (table) return PPD.panels.selectTable(table);
        // nothing — render empty
        PPD.renderer.render(); PPD.inspector.render();
    }

    function refreshFromServer(done) {
        PPD.api.load().then(function (res) {
            if (res && res.success) {
                if (res.pagesConfig) store.setPagesConfig(res.pagesConfig);
                // hydrate reference data + designer doc into the store's underlying state
                if (res.designer && res.designer.version) {
                    var d = store.designer();
                    d.version = res.designer.version;
                    d.pages = res.designer.pages || {};
                    d.common = res.designer.common || {};
                    d.active = res.designer.active || {};
                    d.templates = res.designer.templates || {};   // shared Default List Page Template
                    d.objectModel = res.designer.objectModel;
                }
            }
            done();
        }).catch(function () { done(); });
    }

    function bindGlobal() {
        store.on('request:save', doSave);
        store.on('request:preview', preview);
        store.on('model', scheduleSave);
        store.on('dirty', function (d) { if (d) setSaveStatus('Unsaved…', 'dirty'); });

        // Central navbar Save (project-shell.js) → persist the designer document.
        document.addEventListener('projectSave', function (e) {
            if (!e.detail || e.detail.module !== 'designer') return;
            e.detail.payload = store.designer();   // generic /save persists to ModuleState 'designer'
            store.markDirty(true);
            doSave();                                // also hit /designer/save for normalisation
        });

        document.addEventListener('keydown', function (e) {
            var mod = e.metaKey || e.ctrlKey;
            if (mod && e.key.toLowerCase() === 's') { e.preventDefault(); doSave(); }
            else if (mod && e.key.toLowerCase() === 'z' && !e.shiftKey) { e.preventDefault(); store.undo(); }
            else if (mod && (e.key.toLowerCase() === 'y' || (e.shiftKey && e.key.toLowerCase() === 'z'))) { e.preventDefault(); store.redo(); }
            else if (e.key === 'Delete' || e.key === 'Backspace') {
                if (/INPUT|TEXTAREA|SELECT/.test((document.activeElement || {}).tagName || '')) return;
                var sel = store.ui('selectedId'); if (sel) { e.preventDefault(); model.remove(sel); store.setUi({ selectedId: '' }); }
            }
        });
        window.addEventListener('beforeunload', function (e) { if (store.isDirty()) { e.preventDefault(); e.returnValue = ''; } });
    }

    /* ---- preview: render the active page without designer chrome ---- */
    function preview() {
        var page = model.activePage();
        if (!page) return;
        window.PPD.renderMode = 'preview';   // runtime: hidden parts are truly hidden
        var prev = document.createElement('div');
        prev.className = 'ppd-modal ppd-preview-modal';
        var ctx = { fields: store.fieldsFor(page.table), table: page.table, renderChildren: function (n) { return (n.children || []).map(function (c) { return reg.get(c.type).render(c, ctx); }).join(''); } };
        var bodyHtml = model.components(page).map(function (c) {
            if (c.visibility && c.visibility.visible === false) return '';
            try { var h = reg.get(c.type).render(c, ctx); return h ? '<div style="margin-bottom:12px">' + h + '</div>' : ''; } catch (e) { return ''; }
        }).join('');
        window.PPD.renderMode = 'design';    // restore
        prev.innerHTML = '<div class="ppd-modal-box ppd-preview-box"><div class="ppd-modal-head"><span><i class="bi bi-eye"></i> Preview — ' + esc(page.title || '') + '</span><button class="ppd-btn" data-close><i class="bi bi-x-lg"></i> Close</button></div>' +
            '<div class="ppd-preview-content" style="padding:18px">' + bodyHtml + '</div></div>';
        prev.addEventListener('click', function (e) { if (e.target === prev || e.target.closest('[data-close]')) prev.remove(); });
        document.body.appendChild(prev);
    }

    /* ---- toast ---- */
    var toastHost;
    function toast(msg, kind) {
        if (!toastHost) { toastHost = document.createElement('div'); toastHost.className = 'ppd-toast-host'; document.body.appendChild(toastHost); }
        // Normalise the tone so callers can pass 'error'/'success' or 'err'/'ok'.
        var k = kind === 'error' ? 'err' : (kind === 'success' ? 'ok' : (kind || ''));
        var el = document.createElement('div'); el.className = k; el.textContent = msg;
        toastHost.appendChild(el);
        requestAnimationFrame(function () { el.classList.add('show'); });
        setTimeout(function () { el.classList.remove('show'); setTimeout(function () { el.remove(); }, 250); }, 2200);
    }
    PPD.toast = toast;

    function boot() {
        PPD.panels.boot();
        bindGlobal();
        // start from injected bootstrap immediately, then refresh from server for freshest state
        refreshFromServer(function () {
            var healed = PPD.model.migrate();              // heal old-runtime corruption (e.g. grid actions)
            var synced = PPD.model.syncWithPages();        // reflect Pages-module page types + functionality
            var seeded = PPD.model.seedTemplates();        // create/upgrade the Default List/Add/Edit templates + apply
            store.markDirty(false);
            pickInitial();
            // persist the repair/sync/seed once, quietly, so it doesn't recompute every load
            if (healed || synced || seeded) PPD.api.save(store.designer()).then(function () { store.markDirty(false); }).catch(function () {});
        });
        document.dispatchEvent(new CustomEvent('ppphp:designer:ready', { detail: { version: 'v3' } }));
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
    else boot();
})();
