/**
 * ajax-nav.js
 * ---------------------------------------------------------------------
 * Lightweight AJAX navigation for the sidebar.
 *
 * WHY THIS EXISTS
 * Every page in this app is still a normal, fully server-rendered PHP page
 * (that's on purpose -- it means every page still works correctly if this
 * script fails to load, is disabled, or a page falls outside what it knows
 * how to handle). This script is a progressive enhancement layer on top of
 * that: when someone clicks a sidebar link, instead of letting the browser
 * do a full page navigation (which tears down and rebuilds the whole page,
 * including the sidebar), it fetches the target page in the background,
 * pulls out just the #main-content section, and swaps it into the current
 * page. The sidebar itself is never touched, so it never re-renders,
 * flickers, or loses its scroll/expanded-submenu state.
 *
 * SCOPE
 * Only <a> links inside #sidebar are intercepted. Everything else (forms,
 * in-page action links/buttons, modals) behaves exactly as before -- normal
 * full page loads / normal fetch() calls those pages already make on their
 * own. This keeps the blast radius small and predictable.
 *
 * SAFETY NET
 * If anything about the fetched page doesn't look like a normal page this
 * script knows how to swap (missing #main-content, a redirect to the login
 * page, a network error, a non-OK HTTP status), it always falls back to a
 * normal full navigation (`window.location.href = url`) rather than leaving
 * the user stuck. Worst case, a given click behaves exactly like it did
 * before this script existed.
 */
(function () {
    'use strict';

    if (window.__ajaxNavInitialized) return;
    window.__ajaxNavInitialized = true;

    var currentController = null;
    var progressCreepTimer = null;

    // fetch() has no built-in timeout -- on a flaky connection (satellite
    // internet is the classic case: brief mid-flight stalls from handoffs
    // or obstructions, not outright connection failures) a request can hang
    // indefinitely with no error at all. Without this, the only way to
    // recover is the user manually clicking again or cancelling + reloading
    // in the browser. This makes that recovery automatic.
    var NAV_TIMEOUT_MS = 12000;

    // -----------------------------------------------------------------
    // Loading indicator: a slim top progress bar + a spinner over the
    // content area, so it's obvious a page is loading even on a slow link.
    // -----------------------------------------------------------------
    function ensureLoaderStyles() {
        if (document.getElementById('ajax-nav-styles')) return;

        var style = document.createElement('style');
        style.id = 'ajax-nav-styles';
        style.textContent =
            '#ajax-nav-progress{position:fixed;top:0;left:0;height:3px;width:0%;' +
            'background:linear-gradient(90deg,#6366f1,#22d3ee);z-index:9999;' +
            'opacity:0;transition:width .2s ease,opacity .3s ease;pointer-events:none;}' +
            '#ajax-nav-progress.is-active{opacity:1;}' +
            '#ajax-nav-spinner-overlay{position:absolute;inset:0;' +
            'background:rgba(255,255,255,.7);display:none;align-items:center;' +
            'justify-content:center;z-index:40;min-height:160px;}' +
            '#ajax-nav-spinner-overlay.is-active{display:flex;}' +
            '#ajax-nav-spinner-overlay .ajax-nav-spinner{width:40px;height:40px;' +
            'border-radius:9999px;border:4px solid #c7d2fe;border-top-color:#4f46e5;' +
            'animation:ajax-nav-spin .7s linear infinite;}' +
            '@keyframes ajax-nav-spin{to{transform:rotate(360deg);}}';
        document.head.appendChild(style);

        var bar = document.createElement('div');
        bar.id = 'ajax-nav-progress';
        document.body.appendChild(bar);
    }

    function showLoading() {
        ensureLoaderStyles();

        var bar = document.getElementById('ajax-nav-progress');
        clearInterval(progressCreepTimer);
        bar.style.transition = 'none';
        bar.style.width = '0%';
        bar.classList.add('is-active');
        void bar.offsetWidth; // force reflow so the transition below re-triggers
        bar.style.transition = 'width .2s ease,opacity .3s ease';
        bar.style.width = '25%';

        // Slowly creep the bar forward while we wait, like a classic
        // top-loading-bar pattern, without ever quite reaching 100%.
        progressCreepTimer = setInterval(function () {
            var current = parseFloat(bar.style.width) || 0;
            if (current < 88) {
                bar.style.width = (current + (88 - current) * 0.1) + '%';
            }
        }, 300);

        var main = document.getElementById('main-content');
        if (main) {
            if (getComputedStyle(main).position === 'static') {
                main.style.position = 'relative';
            }
            var overlay = document.createElement('div');
            overlay.id = 'ajax-nav-spinner-overlay';
            overlay.setAttribute('data-ajax-nav-overlay', '1');
            overlay.innerHTML = '<div class="ajax-nav-spinner"></div>';
            main.appendChild(overlay);
            // Force reflow before adding the class so the (currently
            // instant) display:flex is what we actually see.
            void overlay.offsetWidth;
            overlay.classList.add('is-active');
        }
    }

    function finishLoading() {
        clearInterval(progressCreepTimer);
        var bar = document.getElementById('ajax-nav-progress');
        if (bar) {
            bar.style.width = '100%';
            setTimeout(function () {
                bar.classList.remove('is-active');
                setTimeout(function () { bar.style.width = '0%'; }, 300);
            }, 150);
        }
        // Any overlay still present (e.g. an error path that didn't swap
        // main-content's innerHTML) should be removed explicitly; on the
        // success path it's already gone since we replace main-content's
        // innerHTML wholesale.
        var overlay = document.querySelector('[data-ajax-nav-overlay]');
        if (overlay) overlay.remove();
    }

    // -----------------------------------------------------------------
    // Link eligibility
    // -----------------------------------------------------------------
    function isEligibleLink(link) {
        if (!link || !link.getAttribute) return false;
        var rawHref = link.getAttribute('href');
        if (!rawHref || rawHref.charAt(0) === '#') return false;
        if (link.hasAttribute('download')) return false;
        if (link.hasAttribute('data-no-ajax')) return false;
        if (link.target && link.target !== '' && link.target !== '_self') return false;

        var url;
        try {
            url = new URL(link.href, window.location.href);
        } catch (e) {
            return false;
        }
        if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
        if (url.origin !== window.location.origin) return false;
        // Let logout go through as a normal full navigation so session
        // teardown and the resulting redirect happen the plain, reliable way.
        if (/\/logout\.php(?:[/?#]|$)/.test(url.pathname)) return false;

        return true;
    }

    // -----------------------------------------------------------------
    // Script re-execution
    // Scripts inserted via innerHTML never run on their own -- we have to
    // recreate and re-insert each one for it to actually execute.
    // -----------------------------------------------------------------
    function runScriptsSequentially(scripts, baseUrl) {
        return scripts.reduce(function (chain, oldScript) {
            return chain.then(function () { return execScript(oldScript, baseUrl); });
        }, Promise.resolve());
    }

    // Re-running the same inline script more than once (revisiting a page,
    // or two different pages happening to reuse a variable name) throws
    // "Identifier has already been declared" if that script has a
    // top-level `const`/`let` -- those create permanent bindings in the
    // shared global scope that persist across fake navigations, unlike a
    // real page load which gets a fresh scope every time. Demoting only the
    // TOP-LEVEL (brace-depth 0) const/let to var avoids that crash; var
    // redeclaration is always legal. const/let *inside* a function are left
    // untouched since each call already gets its own fresh scope there --
    // no collision risk. Function declarations are already safely
    // re-runnable as-is (redeclaring `function foo(){}` just overwrites the
    // previous one), which is also what keeps inline onclick="foo()"
    // attributes elsewhere on the page working after this transform.
    function demoteTopLevelDeclarations(code) {
        var depth = 0;
        var out = '';
        var i = 0;
        var n = code.length;
        while (i < n) {
            var ch = code[i];

            if (ch === '/' && code[i + 1] === '/') {
                var lineEnd = code.indexOf('\n', i);
                var lineStop = lineEnd === -1 ? n : lineEnd;
                out += code.slice(i, lineStop);
                i = lineStop;
                continue;
            }
            if (ch === '/' && code[i + 1] === '*') {
                var blockEnd = code.indexOf('*/', i + 2);
                var blockStop = blockEnd === -1 ? n : blockEnd + 2;
                out += code.slice(i, blockStop);
                i = blockStop;
                continue;
            }
            if (ch === '"' || ch === "'") {
                var quote = ch;
                var j = i + 1;
                while (j < n && code[j] !== quote) {
                    if (code[j] === '\\') j++;
                    j++;
                }
                j = Math.min(j + 1, n);
                out += code.slice(i, j);
                i = j;
                continue;
            }
            if (ch === '`') {
                var k = i + 1;
                while (k < n && code[k] !== '`') {
                    if (code[k] === '\\') k++;
                    k++;
                }
                k = Math.min(k + 1, n);
                out += code.slice(i, k);
                i = k;
                continue;
            }
            if (ch === '{') { depth++; out += ch; i++; continue; }
            if (ch === '}') { depth--; out += ch; i++; continue; }

            if (depth === 0) {
                var rest = code.slice(i, i + 6);
                var m = rest.match(/^(const|let)\b/);
                if (m) {
                    var prevChar = i === 0 ? '' : code[i - 1];
                    if (prevChar === '' || /[\s;{}()]/.test(prevChar)) {
                        out += 'var';
                        i += m[1].length;
                        continue;
                    }
                }
            }

            out += ch;
            i++;
        }
        return out;
    }

    function execScript(oldScript, baseUrl) {
        return new Promise(function (resolve) {
            var rawSrc = oldScript.getAttribute('src');
            var newScript = document.createElement('script');
            Array.prototype.forEach.call(oldScript.attributes, function (attr) {
                if (attr.name.toLowerCase() === 'src') return;
                newScript.setAttribute(attr.name, attr.value);
            });

            if (rawSrc) {
                var abs;
                try {
                    abs = new URL(rawSrc, baseUrl).href;
                } catch (e) {
                    resolve();
                    return;
                }
                var alreadyLoaded = Array.prototype.some.call(document.scripts, function (s) {
                    return s.src === abs;
                });
                if (alreadyLoaded) { resolve(); return; }
                newScript.src = abs;
                newScript.onload = function () { resolve(); };
                newScript.onerror = function () { resolve(); }; // don't block the chain on one failed asset
                document.body.appendChild(newScript);
            } else {
                newScript.textContent = demoteTopLevelDeclarations(oldScript.textContent);
                document.body.appendChild(newScript);
                resolve();
            }
        });
    }

    function syncHeadAssets(doc, baseUrl) {
        // New stylesheets referenced by the target page that aren't already
        // loaded on the current page.
        Array.prototype.forEach.call(doc.querySelectorAll('head link[rel="stylesheet"][href]'), function (link) {
            var raw = link.getAttribute('href');
            var abs;
            try { abs = new URL(raw, baseUrl).href; } catch (e) { return; }
            var already = Array.prototype.some.call(document.querySelectorAll('link[rel="stylesheet"][href]'), function (l) {
                try { return new URL(l.getAttribute('href'), window.location.href).href === abs; }
                catch (e) { return false; }
            });
            if (!already) {
                var newLink = document.createElement('link');
                newLink.rel = 'stylesheet';
                newLink.href = abs;
                document.head.appendChild(newLink);
            }
        });

        // External head scripts (CDN libraries etc). These need to finish
        // loading before body scripts run, since body scripts often depend
        // on them (e.g. a chart library before the code that draws a chart).
        var headScripts = Array.prototype.filter.call(doc.querySelectorAll('head script[src]'), function (s) {
            return !!s.getAttribute('src');
        });
        return runScriptsSequentially(headScripts, baseUrl);
    }

    function collectBodyScripts(doc) {
        var sidebarInDoc = doc.getElementById('sidebar');
        return Array.prototype.filter.call(doc.body.querySelectorAll('script'), function (s) {
            return !(sidebarInDoc && sidebarInDoc.contains(s));
        });
    }

    // -----------------------------------------------------------------
    // Sidebar active-state sync
    // The fetched page's own server-rendered sidebar already has the
    // correct "active" classes computed for it. Rather than re-implement
    // that route-matching logic in JS (and risk it drifting out of sync
    // with the PHP), we just copy the class names for matching links from
    // the fetched sidebar onto the live one.
    // -----------------------------------------------------------------
    function syncSidebarActiveState(doc) {
        var liveSidebar = document.getElementById('sidebar');
        var fetchedSidebar = doc.getElementById('sidebar');
        if (!liveSidebar || !fetchedSidebar) return;

        // The same href can legitimately appear more than once in the
        // sidebar (e.g. a submenu entry plus a shortcut elsewhere). Both
        // sidebars are rendered from the same template in the same order,
        // so pair up same-href links positionally (1st fetched <-> 1st
        // live, 2nd <-> 2nd, ...) rather than always grabbing the first
        // live match, which would mis-assign classes when there's more
        // than one link to the same page.
        var liveLinksByHref = {};
        Array.prototype.forEach.call(liveSidebar.querySelectorAll('a[href]'), function (a) {
            var href = a.getAttribute('href');
            if (!href) return;
            (liveLinksByHref[href] = liveLinksByHref[href] || []).push(a);
        });
        var consumedIndex = {};

        Array.prototype.forEach.call(fetchedSidebar.querySelectorAll('a[href]'), function (fetchedLink) {
            var href = fetchedLink.getAttribute('href');
            if (!href || !liveLinksByHref[href]) return;

            var idx = consumedIndex[href] || 0;
            var liveLink = liveLinksByHref[href][idx];
            if (!liveLink) return;
            consumedIndex[href] = idx + 1;

            liveLink.className = fetchedLink.className;

            var fetchedSvg = fetchedLink.querySelector('svg');
            var liveSvg = liveLink.querySelector('svg');
            if (fetchedSvg && liveSvg) {
                liveSvg.setAttribute('class', fetchedSvg.getAttribute('class') || '');
            }
        });

        expandActiveSubmenus();
    }

    function expandActiveSubmenus() {
        var sidebar = document.getElementById('sidebar');
        if (!sidebar || sidebar.classList.contains('sidebar-collapsed')) return;

        Array.prototype.forEach.call(sidebar.querySelectorAll('[id$="-sub"]'), function (menu) {
            var hasActiveChild = menu.querySelector('a.text-indigo-600');
            if (!hasActiveChild) return;
            menu.classList.remove('hidden');
            var arrow = document.getElementById(menu.id.replace(/-sub$/, '-arrow'));
            if (arrow) arrow.style.transform = 'rotate(180deg)';
            try { localStorage.setItem('menu_' + menu.id, 'open'); } catch (e) { /* ignore */ }
        });
    }

    // -----------------------------------------------------------------
    // Core navigation
    // -----------------------------------------------------------------
    // -----------------------------------------------------------------
    // Extra body content (modals, toasts, overlays placed outside
    // #main-content)
    // Every page in this app that has a modal places it as a sibling AFTER
    // #main-content closes (a deliberate, reasonable pattern to avoid
    // z-index/stacking-context issues from nested containers) -- but that
    // means swapping only #main-content's innerHTML, as done above, never
    // inserts that markup into the live page at all. The modal's trigger
    // button (inside #main-content) gets swapped in fine and looks normal,
    // but clicking it calls a toggle function that can't find its modal
    // element, throwing "Cannot read properties of null". This syncs
    // everything in <body> outside the persistent sidebar+main-content
    // region to match the freshly fetched page, the same way the
    // #main-content swap already does for the content region itself.
    // -----------------------------------------------------------------
    function getChromeRoot(doc) {
        doc = doc || document;
        var sidebar = doc.getElementById('sidebar');
        var main = doc.getElementById('main-content');
        if (!sidebar || !main) return null;
        var node = sidebar.parentElement;
        while (node && !node.contains(main)) {
            node = node.parentElement;
        }
        return node;
    }

    function syncExtraBodyContent(doc) {
        var liveChromeRoot = getChromeRoot(document);
        var fetchedChromeRoot = getChromeRoot(doc);
        if (!liveChromeRoot || !fetchedChromeRoot) return;

        // Remove the previous page's extra content (stale modals etc.) --
        // but never touch the persistent chrome, or infrastructure this
        // script itself injected (the loading progress bar).
        Array.prototype.slice.call(document.body.children).forEach(function (child) {
            if (child === liveChromeRoot) return;
            if (child.id === 'ajax-nav-progress') return;
            child.remove();
        });

        // Insert the freshly fetched page's own extra content. Scripts are
        // deliberately skipped here -- they're already collected and
        // re-executed (with the const/let-safety transform) by
        // collectBodyScripts()/runScriptsSequentially() below; inserting a
        // raw copy here too would execute it a second time, untransformed.
        Array.prototype.slice.call(doc.body.children).forEach(function (child) {
            if (child === fetchedChromeRoot) return;
            if (child.tagName === 'SCRIPT') return;
            document.body.appendChild(document.importNode(child, true));
        });
    }

    function applyNavigationResult(html, finalUrl, push) {
        var doc = new DOMParser().parseFromString(html, 'text/html');
        var newMain = doc.getElementById('main-content');
        var liveMain = document.getElementById('main-content');

        if (!newMain || !liveMain) {
            window.location.href = finalUrl;
            return;
        }

        var sidebarEl = document.getElementById('sidebar');
        var wasCollapsed = sidebarEl && sidebarEl.classList.contains('sidebar-collapsed');

        liveMain.innerHTML = newMain.innerHTML;
        liveMain.className = newMain.className;

        // main-content's collapsed-sidebar width classes are toggled
        // client-side and aren't part of the freshly-fetched markup, so
        // reapply them or the layout would visually snap back to full width.
        if (wasCollapsed) {
            liveMain.classList.remove('md:pl-64');
            liveMain.classList.add('md:pl-[64px]');
        }

        syncExtraBodyContent(doc);

        if (doc.title) document.title = doc.title;

        if (push) {
            history.pushState({ ajaxNav: true }, '', finalUrl);
        }

        window.scrollTo({ top: 0, behavior: 'auto' });

        // Close the mobile sidebar drawer if it was open, matching the
        // normal "navigating closes the menu" feel on small screens.
        if (window.innerWidth < 768 && sidebarEl && !sidebarEl.classList.contains('-translate-x-full')) {
            sidebarEl.classList.add('-translate-x-full');
            var overlay = document.getElementById('sidebar-overlay');
            if (overlay) overlay.classList.add('hidden');
        }

        syncSidebarActiveState(doc);

        var bodyScripts = collectBodyScripts(doc);
        syncHeadAssets(doc, finalUrl)
            .then(function () { return runScriptsSequentially(bodyScripts, finalUrl); })
            .then(finishLoading)
            .catch(finishLoading);
    }

    function navigate(url, opts) {
        var push = !opts || opts.push !== false;

        if (currentController) currentController.abort();
        var controller = new AbortController();
        currentController = controller;

        showLoading();

        var timeoutId = setTimeout(function () {
            controller.abort();
        }, NAV_TIMEOUT_MS);

        fetch(url, {
            headers: { 'X-Requested-With': 'XMLHttpRequest' },
            credentials: 'same-origin',
            signal: controller.signal
        })
            .then(function (response) {
                clearTimeout(timeoutId);
                if (!response.ok) throw new Error('ajax-nav:bad-status');

                var finalUrl = response.url || url;
                var finalPath;
                try { finalPath = new URL(finalUrl).pathname; } catch (e) { finalPath = ''; }

                if (response.redirected && /\/login\.php(?:[/?#]|$)/.test(finalPath)) {
                    window.location.href = finalUrl;
                    throw new Error('ajax-nav:redirected-to-login');
                }

                return response.text().then(function (html) {
                    return { html: html, finalUrl: finalUrl };
                });
            })
            .then(function (result) {
                applyNavigationResult(result.html, result.finalUrl, push);
            })
            .catch(function (err) {
                clearTimeout(timeoutId);
                // If a newer navigate() call has already taken over (a fresh
                // click, or popstate), currentController now points at ITS
                // controller, not this one -- let that one handle things and
                // do nothing here. Otherwise, whatever just happened -- our
                // own timeout firing, or any other network failure -- falls
                // through to a normal full navigation rather than leaving
                // the user stuck on a silently-hung page.
                if (controller !== currentController) return;
                if (err && err.message === 'ajax-nav:redirected-to-login') return;
                window.location.href = url;
            });
    }

    // -----------------------------------------------------------------
    // Wire up click + back/forward handling
    // -----------------------------------------------------------------
    document.addEventListener('click', function (e) {
        if (e.defaultPrevented || e.button !== 0) return;
        if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;

        var sidebar = document.getElementById('sidebar');
        if (!sidebar) return;

        var link = e.target.closest ? e.target.closest('a') : null;
        if (!link || !sidebar.contains(link)) return;
        if (!isEligibleLink(link)) return;

        e.preventDefault();
        navigate(link.href, { push: true });
    });

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

    try {
        history.replaceState({ ajaxNav: true }, '', window.location.href);
    } catch (e) { /* ignore */ }
})();
