/**
 * Client-side navigation runtime, served at `/__wrnexus/nav.js`.
 *
 * Progressive enhancement over normal links: intercepts same-origin `<a>`
 * clicks, fetches the target page's HTML, swaps the `#app` container in place,
 * updates history/title/scroll, ensures any framework runtimes the new page
 * needs are present, and re-hydrates.
 *
 * Before replacing the current page, component lifecycle behaviors are
 * explicitly disposed. This ensures `unmount` hooks and watcher cleanups run
 * before the old DOM is removed.
 *
 * Programmatic navigation is exposed as:
 *
 *   window.__wrnexusNavigate(url)
 */

export const NAV_RUNTIME = String.raw`
(function () {
  if (
    !window.history ||
    !history.pushState ||
    !window.fetch ||
    !window.DOMParser
  ) {
    return;
  }

  if (window.__wrnexusNavInstalled) {
    return;
  }

  window.__wrnexusNavInstalled = true;

  var APP_ID = "app";
  var inFlight = null;
  var memory = Object.create(null);
  var keepAlive = Object.create(null);
  var keepAliveOrder = [];

  function keepAliveKey(node) {
    return String(node && node.getAttribute("data-wrn-keepalive") || "");
  }

  function retainKeepAlive(root) {
    Array.prototype.forEach.call(root.querySelectorAll("[data-wrn-keepalive]"), function (node) {
      var key = keepAliveKey(node);
      if (!key) return;
      if (!keepAlive[key]) keepAliveOrder.push(key);
      keepAlive[key] = node;
      if (node.parentNode) node.parentNode.removeChild(node);
    });
    while (keepAliveOrder.length > 32) {
      var expired = keepAliveOrder.shift();
      if (expired && keepAlive[expired]) {
        dispose(keepAlive[expired]);
        delete keepAlive[expired];
      }
    }
  }

  function restoreKeepAlive(root) {
    var placeholders = [];
    if (root.matches && root.matches("[data-wrn-keepalive]")) placeholders.push(root);
    Array.prototype.forEach.call(root.querySelectorAll("[data-wrn-keepalive]"), function (node) { placeholders.push(node); });
    placeholders.forEach(function (placeholder) {
      var saved = keepAlive[keepAliveKey(placeholder)];
      if (saved && placeholder.parentNode) placeholder.parentNode.replaceChild(saved, placeholder);
    });
  }

  function preservationPolicy(doc) {
    var meta = doc.querySelector('meta[name="wrnexus-preserve"]');
    var out = Object.create(null);
    String(meta && meta.getAttribute("content") || "").split(",").forEach(function (name) {
      if (name) out[name] = true;
    });
    return out;
  }

  function syncPreservationPolicy(nextDocument) {
    var selector = 'meta[name="wrnexus-preserve"]';
    var current = document.querySelector(selector);
    var next = nextDocument.querySelector(selector);
    if (!next) {
      if (current) current.remove();
      return;
    }
    if (!current) {
      current = document.createElement("meta");
      current.setAttribute("name", "wrnexus-preserve");
      document.head.appendChild(current);
    }
    current.setAttribute("content", next.getAttribute("content") || "");
  }

  function stateKey(url) {
    try {
      var parsed = new URL(url, location.href);
      return "wrnexus:navigation:" + parsed.pathname + parsed.search;
    } catch (_) {
      return "wrnexus:navigation:" + String(url);
    }
  }

  function safeField(field) {
    var type = String(field.type || "").toLowerCase();
    var name = String(field.name || field.id || "").toLowerCase();
    return type !== "password" && type !== "file" && type !== "hidden" &&
      !field.hasAttribute("data-no-preserve") &&
      !/(?:csrf|token|secret|password|credential)/.test(name);
  }

  function capturePage(url) {
    var policy = preservationPolicy(document);
    var state = { fields: Object.create(null), elements: Object.create(null) };
    if (policy.scroll) state.scroll = [window.scrollX || 0, window.scrollY || 0];
    if (policy.forms || policy.filters || policy.pagination || policy.workflow) {
      Array.prototype.forEach.call(document.querySelectorAll("#app input,#app select,#app textarea"), function (field, index) {
        if (!safeField(field)) return;
        var key = field.name || field.id || String(index);
        state.fields[key] = { value: field.value, checked: !!field.checked, selectedIndex: field.selectedIndex };
      });
    }
    if (policy.tabs || policy.expanded || policy.component) {
      Array.prototype.forEach.call(document.querySelectorAll("#app [data-wrn-preserve]"), function (node, index) {
        var key = node.getAttribute("data-wrn-preserve") || node.id || String(index);
        state.elements[key] = {
          selected: node.getAttribute("aria-selected"),
          expanded: node.getAttribute("aria-expanded"),
          value: "value" in node ? node.value : null,
        };
      });
    }
    memory[stateKey(url)] = state;
    try { sessionStorage.setItem(stateKey(url), JSON.stringify(state)); } catch (_) {}
  }

  function restorePage(url, isPop) {
    var policy = preservationPolicy(document);
    var state = memory[stateKey(url)];
    if (!state) {
      try { state = JSON.parse(sessionStorage.getItem(stateKey(url)) || "null"); } catch (_) {}
    }
    if (state && (policy.forms || policy.filters || policy.pagination || policy.workflow)) {
      Array.prototype.forEach.call(document.querySelectorAll("#app input,#app select,#app textarea"), function (field, index) {
        if (!safeField(field)) return;
        var saved = state.fields && state.fields[field.name || field.id || String(index)];
        if (!saved) return;
        if (field.type === "checkbox" || field.type === "radio") field.checked = !!saved.checked;
        else field.value = saved.value;
        if (field.tagName === "SELECT") field.selectedIndex = saved.selectedIndex;
        field.dispatchEvent(new Event("input", { bubbles: true }));
      });
    }
    if (state && (policy.tabs || policy.expanded || policy.component)) {
      Array.prototype.forEach.call(document.querySelectorAll("#app [data-wrn-preserve]"), function (node, index) {
        var key = node.getAttribute("data-wrn-preserve") || node.id || String(index);
        var saved = state.elements && state.elements[key];
        if (!saved) return;
        if (saved.selected != null) node.setAttribute("aria-selected", saved.selected);
        if (saved.expanded != null) node.setAttribute("aria-expanded", saved.expanded);
        if (saved.value != null && "value" in node) node.value = saved.value;
      });
    }
    if (state && policy.scroll && state.scroll) window.scrollTo(state.scroll[0], state.scroll[1]);
    else if (!isPop) window.scrollTo(0, 0);
  }

  function pathOf(src) {
    return String(src).split("?")[0];
  }

  function isLocalLink(anchor) {
    if (
      !anchor ||
      anchor.hasAttribute("download") ||
      anchor.hasAttribute("data-no-nav")
    ) {
      return false;
    }

    if (anchor.target && anchor.target !== "_self") {
      return false;
    }

    if (anchor.origin !== location.origin) {
      return false;
    }

    var href = anchor.getAttribute("href");

    if (!href || href.charAt(0) === "#") {
      return false;
    }

    var rel = (
      anchor.getAttribute("rel") || ""
    ).toLowerCase();

    return rel.indexOf("external") === -1;
  }

  function currentDocumentNonce() {
    var node = document.querySelector("script[nonce],style[nonce]");
    if (!node) return "";
    return node.nonce || node.getAttribute("nonce") || "";
  }

  function syncWrnStyles(nextDocument) {
    var current = Array.prototype.slice.call(
      document.querySelectorAll("style[data-wrnexus-style-id]"),
    );
    var next = Array.prototype.slice.call(
      nextDocument.querySelectorAll("style[data-wrnexus-style-id]"),
    );
    var currentById = Object.create(null);
    var nextIds = Object.create(null);
    var nonce = currentDocumentNonce();

    current.forEach(function (style) {
      var id = style.getAttribute("data-wrnexus-style-id");
      if (id) currentById[id] = style;
    });

    next.forEach(function (nextStyle) {
      var id = nextStyle.getAttribute("data-wrnexus-style-id");
      if (!id) return;
      nextIds[id] = true;

      var currentStyle = currentById[id];
      if (!currentStyle) {
        currentStyle = document.createElement("style");
        currentStyle.setAttribute("data-wrnexus-style-id", id);
      }

      ["data-wrnexus-style-owner", "data-wrnexus-style-kind"].forEach(function (name) {
        var value = nextStyle.getAttribute(name);
        if (value == null) currentStyle.removeAttribute(name);
        else currentStyle.setAttribute(name, value);
      });

      if (nonce) currentStyle.setAttribute("nonce", nonce);
      if (currentStyle.textContent !== nextStyle.textContent) {
        currentStyle.textContent = nextStyle.textContent;
      }

      document.head.appendChild(currentStyle);
    });

    current.forEach(function (style) {
      var id = style.getAttribute("data-wrnexus-style-id");
      if (id && !nextIds[id]) style.remove();
    });
  }

  function loadedScriptPaths() {
    var loaded = {};

    document
      .querySelectorAll("script[src]")
      .forEach(function (script) {
        var src = script.getAttribute("src");

        if (src) {
          loaded[pathOf(src)] = true;
        }
      });

    return loaded;
  }

  function runtimeIds(root) {
    var ids = {};
    if (!root || !root.querySelectorAll) return ids;
    var nodes = [];
    if (root.matches && root.matches("[data-wrnexus-runtime]")) nodes.push(root);
    root.querySelectorAll("[data-wrnexus-runtime]").forEach(function (node) { nodes.push(node); });
    nodes.forEach(function (node) {
      String(node.getAttribute("data-wrnexus-runtime") || "")
        .split(/[\s,]+/)
        .forEach(function (id) { if (id) ids[id] = true; });
    });
    return ids;
  }

  function packageRuntimeRegistry() {
    return window.__wrnexusRuntimes || {};
  }

  function mountPackageRuntimes(root) {
    var ids = runtimeIds(root);
    var registry = packageRuntimeRegistry();
    Object.keys(ids).forEach(function (id) {
      var runtime = registry[id];
      if (!runtime || typeof runtime.mount !== "function") return;
      try { runtime.mount(root); }
      catch (error) { console.error("[wrnexus] failed to mount runtime", id, error); }
    });
  }

  function unmountPackageRuntimes(root) {
    var ids = runtimeIds(root);
    var registry = packageRuntimeRegistry();
    Object.keys(ids).forEach(function (id) {
      var runtime = registry[id];
      if (!runtime || typeof runtime.unmount !== "function") return;
      try { runtime.unmount(root); }
      catch (error) { console.error("[wrnexus] failed to unmount runtime", id, error); }
    });
  }

  /**
   * Append framework runtimes declared by the incoming document but not
   * currently loaded.
   */
  function ensureScripts(doc) {
    var loaded = loadedScriptPaths();

    doc
      .querySelectorAll("script[src]")
      .forEach(function (script) {
        var src = script.getAttribute("src");

        if (!src || loaded[pathOf(src)]) {
          return;
        }

        loaded[pathOf(src)] = true;

        var element =
          document.createElement("script");

        Array.prototype.forEach.call(
          script.attributes,
          function (attribute) {
            if (attribute.name === "src") return;
            element.setAttribute(
              attribute.name,
              attribute.value,
            );
          },
        );
        element.src = src;
        if (!script.hasAttribute("async")) {
          element.async = false;
        }
        element.addEventListener("load", function () {
          try {
            mountPackageRuntimes(document);
            window.dispatchEvent(
              new CustomEvent(
                "wrnexus:runtime-loaded",
                { detail: { src: src } },
              ),
            );
          } catch (_) {}
        });
        element.addEventListener("error", function () {
          console.error(
            "[wrnexus] failed to load client runtime",
            src,
          );
        });

        document.body.appendChild(element);
      });
  }

  /**
   * Run explicit component cleanup before removing the existing page.
   *
   * The reactive runtime also observes removed DOM nodes, but explicit
   * disposal here guarantees that unmount hooks run before replacement.
   */
  function dispose(root) {
    unmountPackageRuntimes(root);
    try {
      var stores = window.__wrnexusStoreContainer;
      if (stores && typeof stores.disposePageStores === "function") {
        Promise.resolve(stores.disposePageStores()).catch(function (error) {
          console.error("[wrnexus] failed to dispose page stores", error);
        });
      }
    } catch (error) {
      console.error("[wrnexus] failed to access page stores", error);
    }
    try {
      if (
        typeof window.__wrnexusDisposeBehaviors ===
        "function"
      ) {
        window.__wrnexusDisposeBehaviors(root);
      }
    } catch (error) {
      console.error(
        "[wrnexus] failed to dispose page behaviors",
        error,
      );
    }
  }

  /**
   * Re-hydrate already-loaded runtimes against newly inserted DOM.
   *
   * All framework hydration functions must remain idempotent.
   */
  function rehydrate(root) {
    mountPackageRuntimes(root);
    try {
      if (
        typeof window.__wrnexusHydrateScopes ===
        "function"
      ) {
        window.__wrnexusHydrateScopes(root);
      }
    } catch (error) {
      console.error(
        "[wrnexus] failed to hydrate reactive scopes",
        error,
      );
    }

    try {
      if (
        typeof window.__wrnexusHydrateCsrFetches ===
        "function"
      ) {
        window.__wrnexusHydrateCsrFetches(root);
      }
    } catch (error) {
      console.error(
        "[wrnexus] failed to hydrate CSR bindings",
        error,
      );
    }

    try {
      if (
        window.__wireValidate &&
        typeof window.__wireValidate.init ===
          "function"
      ) {
        window.__wireValidate.init(document);
      }
    } catch (error) {
      console.error(
        "[wrnexus] failed to hydrate validation",
        error,
      );
    }

    try {
      if (
        window.wireTheme &&
        typeof window.wireTheme.bind ===
          "function"
      ) {
        window.wireTheme.bind(document);
      }
    } catch (error) {
      console.error(
        "[wrnexus] failed to hydrate theme bindings",
        error,
      );
    }
  }

  function dispatchNavigationEvent(url) {
    try {
      window.dispatchEvent(
        new CustomEvent("wrnexus:navigated", {
          detail: {
            url: url,
          },
        }),
      );
    } catch (_) {
      // CustomEvent may not be available in very old browsers.
    }
  }

  function hardNavigate(url) {
    location.href = url;
  }

  function render(html, url, isPop) {
    var doc;

    try {
      doc = new DOMParser().parseFromString(
        html,
        "text/html",
      );
    } catch (_) {
      hardNavigate(url);
      return;
    }

    capturePage(location.href);

    var incomingApp =
      doc.getElementById(APP_ID);

    var currentApp =
      document.getElementById(APP_ID);

    if (!incomingApp || !currentApp) {
      hardNavigate(url);
      return;
    }

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

    syncPreservationPolicy(doc);

    syncWrnStyles(doc);

    var importedNodes = [];

    for (
      var index = 0;
      index < incomingApp.childNodes.length;
      index++
    ) {
      importedNodes.push(
        document.importNode(
          incomingApp.childNodes[index],
          true,
        ),
      );
    }

    /*
     * Run unmount hooks and watcher cleanup while the current DOM is still
     * connected. This is important for components that remove window or
     * document listeners during lifecycle.unmount.
     */
    retainKeepAlive(currentApp);
    dispose(currentApp);

    try {
      /*
       * Avoid innerHTML so navigation works with strict Trusted Types and CSP
       * configurations.
       */
      currentApp.replaceChildren.apply(
        currentApp,
        importedNodes,
      );
      restoreKeepAlive(currentApp);
    } catch (_) {
      hardNavigate(url);
      return;
    }

    /*
     * Load any runtime required only by the incoming page. Fresh scripts
     * initialise themselves when loaded.
     */
    ensureScripts(doc);

    /*
     * Hydrate runtimes that are already available. Hydration is scoped to
     * #app instead of the entire document for less DOM traversal.
     */
    rehydrate(currentApp);

    if (!isPop) {
      history.pushState(
        {
          wrnexusNav: true,
        },
        "",
        url,
      );

    }

    restorePage(url, isPop);

    dispatchNavigationEvent(url);
  }

  function navigate(url, isPop) {
    var token = {};

    inFlight = token;
    window.dispatchEvent(new CustomEvent("wrnexus:navigation-start", { detail: { url: url } }));

    var pending = prefetched.get(url) || requestDocument(url, false);
    prefetched.delete(url);
    pending
      .then(function (result) {
        if (inFlight !== token) return null;
        if (result.redirectedUrl) url = result.redirectedUrl;
        if (result.contentType.indexOf("text/html") === -1) {
          hardNavigate(url);
          return null;
        }
        render(result.text, url, isPop);
        return null;
      })
      .catch(function () {
        if (inFlight === token) hardNavigate(url);
      });
  }

  var prefetched = new Map();
  function requestDocument(url, isPrefetch) {
    return fetch(url, {
      headers: {
        "x-wrnexus-nav": "1",
        ...(isPrefetch ? { "x-wrnexus-prefetch": "1" } : {}),
        accept: "text/html",
      },

      credentials: "same-origin",
    }).then(function (response) {
      var contentType = response.headers.get("content-type") || "";
      return response.text().then(function (text) {
        return {
          text: text,
          contentType: contentType,
          redirectedUrl: response.redirected && response.url ? response.url : "",
        };
      });
    });
  }

  function prefetch(anchor) {
    if (!isLocalLink(anchor) || anchor.hasAttribute("data-no-prefetch")) return;
    var url = anchor.href;
    if (url === location.href || prefetched.has(url)) return;
    prefetched.set(url, requestDocument(url, true));
    while (prefetched.size > 20) prefetched.delete(prefetched.keys().next().value);
  }

  // Prefetch only on explicit pointer intent. Delegated pointerover fires when
  // scrolling moves links underneath a stationary pointer, which can otherwise
  // download every link on a long index page.
  document.addEventListener("pointerdown", function (event) {
    var anchor = event.target && event.target.closest ? event.target.closest("a") : null;
    prefetch(anchor);
  }, { passive: true });
  document.addEventListener("focusin", function (event) {
    var anchor = event.target && event.target.closest ? event.target.closest("a") : null;
    prefetch(anchor);
  });

  document.addEventListener(
    "click",
    function (event) {
      if (
        event.defaultPrevented ||
        event.button !== 0 ||
        event.metaKey ||
        event.ctrlKey ||
        event.shiftKey ||
        event.altKey
      ) {
        return;
      }

      var anchor =
        event.target &&
        event.target.closest
          ? event.target.closest("a")
          : null;

      if (!isLocalLink(anchor)) {
        return;
      }

      if (anchor.href === location.href) {
        event.preventDefault();
        return;
      }

      event.preventDefault();

      navigate(anchor.href, false);
    },
    false,
  );

  window.addEventListener(
    "popstate",
    function () {
      navigate(location.href, true);
    },
  );

  /**
   * Programmatic navigation for forms, actions and application code.
   */
  window.__wrnexusNavigate = function (url) {
    navigate(
      new URL(url, location.href).href,
      false,
    );
  };

  /** Re-render the current route without adding a duplicate history entry. */
  window.__wrnexusRefresh = function () {
    navigate(location.href, true);
  };
})();
`.trim();
