/*
 * whp-nav.js — SPA-lite client navigator (progressive enhancement).
 *
 * When enabled (window.WHP_SPA_NAV === true, or the per-browser test opt-in
 * below), clicking an internal panel link (index.php?page=…) fetches just that
 * page's content region from the server's partial-render mode (X-WHP-Partial,
 * see index.php + includes/main-content.php) and swaps it into #main-content —
 * no full reload, the navbar/sidebar/scripts stay put, scroll/focus are
 * managed, and history (back/forward) works.
 *
 * DISABLED BY DEFAULT. It is opt-in because WHP pages were not written for
 * client-side re-execution. Two hazards are handled here; a third is inherent:
 *
 *   1. Inline DOMContentLoaded/load handlers won't fire after a swap (the event
 *      already fired on first load). -> we shim document/window addEventListener
 *      during script execution and invoke queued ready handlers immediately.
 *   2. Top-level const/let redeclaration errors when a script re-runs. -> inline
 *      scripts run via INDIRECT eval `(0,eval)(code)`, which gives each script
 *      its own lexical scope (no const/let clash) while still letting
 *      `function f(){}` / `var` / `window.x` attach as globals (so existing
 *      onclick="f()" handlers keep working).
 *   3. INHERENT: a page that stashes state in a top-level const/let and reads it
 *      from a SEPARATE inline block or relies on parser-order globals may not
 *      fully init under SPA nav. Such pages should be excluded (data-no-spa on a
 *      link, or the page-level allow-list a future version can add).
 *
 * Fallbacks: any non-OK response, a redirect to the login page, or a swap error
 * falls back to a normal full navigation, so the worst case is "it reloaded like
 * before" — never a dead page.
 */
(function () {
    'use strict';

    // ---- activation gate --------------------------------------------------
    function spaEnabled() {
        if (window.WHP_SPA_NAV === true) { return true; }
        // Per-browser test opt-in so it can be exercised on staging without
        // flipping the server default: visit any page with ?_spatest=1 once.
        try {
            if (/[?&]_spatest=1\b/.test(location.search)) { localStorage.setItem('whp_spa_test', '1'); }
            return localStorage.getItem('whp_spa_test') === '1';
        } catch (e) { return false; }
    }
    if (!spaEnabled()) { return; }

    var mainSel = '#main-content';
    var inFlight = null;       // AbortController for the current navigation
    var navToken = 0;          // monotonically increasing; stale responses are dropped

    // ---- tiny top progress bar -------------------------------------------
    var bar = null;
    function progress(state) {
        if (!bar) {
            bar = document.createElement('div');
            bar.id = 'whp-nav-progress';
            bar.setAttribute('aria-hidden', 'true');
            document.body.appendChild(bar);
        }
        if (state === 'start') { bar.className = 'active'; bar.style.width = '35%'; }
        else if (state === 'mid') { bar.style.width = '75%'; }
        else { bar.style.width = '100%'; setTimeout(function () { bar.className = ''; bar.style.width = '0'; }, 250); }
    }

    // ---- DOMContentLoaded / load shim ------------------------------------
    // Returns a controller you arm before running swapped scripts and disarm
    // after; queued ready handlers are then fired in registration order.
    function readyShim() {
        var queued = [];
        var origDoc = document.addEventListener;
        var origWin = window.addEventListener;
        function patch(orig, isWin) {
            return function (type, listener, opts) {
                if (type === 'DOMContentLoaded' || (isWin && type === 'load')) {
                    if (listener) { queued.push(listener); }
                    return;
                }
                return orig.call(this, type, listener, opts);
            };
        }
        return {
            arm: function () {
                document.addEventListener = patch(origDoc, false);
                window.addEventListener = patch(origWin, true);
            },
            disarm: function () {
                document.addEventListener = origDoc;
                window.addEventListener = origWin;
            },
            fire: function () {
                queued.forEach(function (fn) {
                    try {
                        var ev = new Event('DOMContentLoaded');
                        (typeof fn === 'function' ? fn : fn.handleEvent).call(document, ev);
                    } catch (e) { console.error('[whp-nav] ready handler failed', e); }
                });
            }
        };
    }

    // ---- external-script dedupe ------------------------------------------
    // Shared page scripts (table-pagination.js, whp-table.js, dns-manager.js…)
    // declare top-level class/const globals. Re-adding the same <script src> on
    // a later navigation re-executes it in global scope -> "Identifier X already
    // declared". So we load each external src at most once per document; the
    // global it defines persists, and the page's inline `new Foo()` (re-run
    // fresh via indirect eval each nav) still finds it. Idempotent
    // auto-enhancers (whp-table) are re-applied to the new content below.
    var loadedSrc = null;
    function normSrc(src) { try { return new URL(src, location.href).pathname; } catch (e) { return src; } }
    function seedLoaded() {
        loadedSrc = {};
        Array.prototype.forEach.call(document.scripts, function (s) { if (s.src) { loadedSrc[normSrc(s.src)] = true; } });
    }

    // ---- sequential (re-)execution of swapped <script> nodes --------------
    function runScripts(scripts) {
        if (loadedSrc === null) { seedLoaded(); }
        var shim = readyShim();
        shim.arm();
        var chain = Promise.resolve();
        scripts.forEach(function (old) {
            chain = chain.then(function () {
                if (old.src) {
                    var key = normSrc(old.src);
                    if (loadedSrc[key]) { return; }   // already loaded — global persists, don't redeclare
                    loadedSrc[key] = true;
                    return new Promise(function (resolve) {
                        var s = document.createElement('script');
                        for (var i = 0; i < old.attributes.length; i++) {
                            s.setAttribute(old.attributes[i].name, old.attributes[i].value);
                        }
                        s.onload = s.onerror = function () { resolve(); };
                        document.body.appendChild(s);
                    });
                }
                // Inline script: indirect eval for scope isolation (see header).
                var code = old.textContent || '';
                if (code.trim()) {
                    try { (0, eval)(code); }   // eslint-disable-line no-eval
                    catch (e) { console.error('[whp-nav] inline script failed (page may not fully init)', e); }
                }
            });
        });
        return chain.then(function () {
            shim.disarm();
            shim.fire();
            // Re-apply idempotent global enhancers to the swapped content, since a
            // deduped external enhancer script won't have re-run its own auto-init.
            try {
                if (window.WhpTable) {
                    document.querySelectorAll('#main-content table[data-whp-table]').forEach(function (t) { window.WhpTable.enhance(t); });
                }
            } catch (e) { console.error('[whp-nav] enhancer pass failed', e); }
        }, function (e) { shim.disarm(); console.error('[whp-nav]', e); });
    }

    // ---- sidebar active-state sync ---------------------------------------
    function syncSidebar(pageKey) {
        var links = document.querySelectorAll('#sidebarMenu a.list-group-item');
        links.forEach(function (a) {
            var href = a.getAttribute('href') || '';
            var m = href.match(/[?&]page=([a-z0-9_-]+)/i);
            var isActive = m && m[1] === pageKey;
            a.classList.toggle('active', !!isActive);
            if (isActive) {
                a.setAttribute('aria-current', 'page');
                // Expand the parent collapsible group if this link lives in one.
                var grp = a.closest('.collapse');
                if (grp && !grp.classList.contains('show')) {
                    grp.classList.add('show');
                    var toggle = document.querySelector('[aria-controls="' + grp.id + '"]');
                    if (toggle) { toggle.setAttribute('aria-expanded', 'true'); toggle.classList.add('active'); }
                }
            } else {
                a.removeAttribute('aria-current');
            }
        });
    }

    function closeOffcanvasIfOpen() {
        var oc = document.getElementById('sidebarMenu');
        if (oc && oc.classList.contains('show') && window.bootstrap && bootstrap.Offcanvas) {
            var inst = bootstrap.Offcanvas.getInstance(oc);
            if (inst) { inst.hide(); }
        }
    }

    // ---- the navigation itself -------------------------------------------
    function isInternalPageLink(a) {
        if (!a || a.target === '_blank' || a.hasAttribute('download') || a.hasAttribute('data-no-spa')) { return false; }
        var href = a.getAttribute('href') || '';
        if (!/(^|\/)index\.php\?(?=.*\bpage=)/.test(href) && !/^\?(?=.*\bpage=)/.test(href)) { return false; }
        if (/whp-action=logout/.test(href)) { return false; }   // let logout do a real GET
        // Same-origin only.
        try { var u = new URL(a.href, location.href); if (u.origin !== location.origin) { return false; } } catch (e) { return false; }
        return true;
    }

    function navigate(url, opts) {
        opts = opts || {};
        var myToken = ++navToken;
        if (inFlight) { try { inFlight.abort(); } catch (e) {} }
        inFlight = new AbortController();
        progress('start');

        fetch(url, { headers: { 'X-WHP-Partial': '1' }, credentials: 'same-origin', signal: inFlight.signal, redirect: 'follow' })
            .then(function (r) {
                // Session expired / bounced to login → fall back to full load.
                if (!r.ok || /\/login\.php/.test(r.url)) { throw { fallback: true }; }
                progress('mid');
                var title = r.headers.get('X-WHP-Title');
                var pageKey = r.headers.get('X-WHP-Page');
                return r.text().then(function (html) { return { html: html, title: title, pageKey: pageKey }; });
            })
            .then(function (data) {
                if (myToken !== navToken) { return; }   // a newer nav superseded us
                var main = document.querySelector(mainSel);
                if (!main) { throw { fallback: true }; }

                // Parse, lift out scripts, swap the rest, then re-run scripts.
                var tmp = document.createElement('div');
                tmp.innerHTML = data.html;
                var scripts = Array.prototype.slice.call(tmp.querySelectorAll('script'));
                scripts.forEach(function (s) { s.parentNode.removeChild(s); });
                main.innerHTML = tmp.innerHTML;

                if (data.title) { document.title = decodeURIComponent(data.title); }
                var pageKey = data.pageKey ? decodeURIComponent(data.pageKey) : (url.match(/[?&]page=([a-z0-9_-]+)/i) || [])[1];

                return runScripts(scripts).then(function () {
                    if (myToken !== navToken) { return; }
                    if (opts.push !== false) { history.pushState({ whpPage: pageKey }, '', url); }
                    syncSidebar(pageKey);
                    closeOffcanvasIfOpen();
                    // a11y: move focus to the freshly-loaded region + reset scroll.
                    main.focus();
                    window.scrollTo(0, 0);
                    if (window.whpRefreshTimes) { try { window.whpRefreshTimes(); } catch (e) {} }
                    document.dispatchEvent(new CustomEvent('whp:navigated', { detail: { page: pageKey } }));
                    progress('done');
                });
            })
            .catch(function (err) {
                if (err && err.name === 'AbortError') { return; }
                // Any failure → honest full navigation (never a dead page).
                if (opts.push === false) { location.reload(); } else { window.location.href = url; }
            });
    }

    // ---- wiring -----------------------------------------------------------
    document.addEventListener('click', function (ev) {
        if (ev.defaultPrevented || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) { return; }
        var a = ev.target.closest && ev.target.closest('a');
        if (!a || !isInternalPageLink(a)) { return; }
        ev.preventDefault();
        navigate(a.href, { push: true });
    });

    window.addEventListener('popstate', function () {
        if (!spaEnabled()) { return; }
        navigate(location.href, { push: false });
    });

    // Seed history state for the initial page so the first Back works cleanly.
    try { history.replaceState({ whpPage: (location.search.match(/[?&]page=([a-z0-9_-]+)/i) || [])[1] || 'dashboard' }, '', location.href); } catch (e) {}

    window.WhpNav = { navigate: navigate };
})();
