/**
 * Client realtime runtime, served at `/__wrnexus/realtime.js`.
 *
 * Two ways to use it — no hand-written WebSocket code either way:
 *
 * 1. Declarative (zero JS). Put `data-room="<name>"` on a container; the runtime
 *    connects, appends incoming messages to `[data-room-log]` using a
 *    `<template data-room-item="<type>">` (fields via `%field%`, HTML-escaped),
 *    reflects connection state on `[data-room-status]`, and sends a
 *    `<form data-room-send>`'s named fields as JSON on submit (fields marked
 *    `data-room-reset` clear after send). Optional `data-room-user` identifies
 *    the connection.
 *
 * 2. Programmatic: `const room = wire.room("chat"); room.on("chat", fn);
 *    room.send({ type: "chat", text })`. Handles connect, JSON, reconnect.
 *
 * Rebinds on `wrnexus:navigated` (client-side nav) and closes rooms whose
 * container has left the page.
 */

export const REALTIME_RUNTIME = String.raw`
(function () {
  if (!("WebSocket" in window)) return;
  var wire = (window.wire = window.wire || {});
  if (wire.room) return; // already installed
  var open = {}; // normalized room+query key -> room connection

  function normalizedQuery(query) {
    var params = new URLSearchParams(String(query || "").replace(/^\?/, ""));
    var pairs = [];
    params.forEach(function (value, name) { pairs.push([name, value]); });
    pairs.sort(function (a, b) {
      return a[0] === b[0] ? String(a[1]).localeCompare(String(b[1])) : String(a[0]).localeCompare(String(b[0]));
    });
    var normalized = new URLSearchParams();
    pairs.forEach(function (pair) { normalized.append(pair[0], pair[1]); });
    return normalized.toString();
  }

  function roomKey(name, query) {
    var normalized = normalizedQuery(query);
    return String(name || "") + (normalized ? "?" + normalized : "");
  }

  function openRoom(name, query) {
    query = normalizedQuery(query);
    var key = roomKey(name, query);
    if (open[key]) return open[key];
    var ws = null, queue = [], listeners = [], attempts = 0, timer = null, closed = false;

    function url() {
      var proto = location.protocol === "https:" ? "wss" : "ws";
      var q = query ? "?" + query : "";
      return proto + "://" + location.host + "/realtime/" + name + q;
    }
    function emit(msg) {
      for (var i = 0; i < listeners.length; i++) {
        var l = listeners[i];
        if (!l.type || l.type === "*" || l.type === msg.type) {
          try { l.cb(msg); } catch (e) { console.error("[wrnexus] room '" + name + "' listener error", e); }
        }
      }
    }
    function connect() {
      ws = new WebSocket(url());
      ws.onopen = function () {
        attempts = 0;
        for (var i = 0; i < queue.length; i++) ws.send(queue[i]);
        queue = [];
        emit({ type: "__open" });
      };
      ws.onclose = function () {
        ws = null;
        emit({ type: "__close" });
        if (!closed) {
          var delay = Math.min(5000, 400 * Math.pow(2, attempts++));
          clearTimeout(timer);
          timer = setTimeout(connect, delay);
        }
      };
      ws.onerror = function () { emit({ type: "__error" }); };
      ws.onmessage = function (e) {
        var msg;
        try { msg = JSON.parse(e.data); } catch (_) { msg = { type: "__raw", data: e.data }; }
        emit(msg);
      };
    }

    var api = {
      name: name,
      key: key,
      query: query,
      send: function (obj) {
        var payload = typeof obj === "string" ? obj : JSON.stringify(obj);
        if (ws && ws.readyState === 1) ws.send(payload);
        else queue.push(payload);
        return api;
      },
      on: function (type, cb) {
        if (typeof type === "function") { cb = type; type = "*"; }
        listeners.push({ type: type, cb: cb });
        return api;
      },
      close: function () { closed = true; clearTimeout(timer); if (ws) try { ws.close(); } catch (_) {} ws = null; delete open[key]; },
    };
    open[key] = api;
    connect();
    return api;
  }
  wire.room = openRoom;

  // --- Declarative binding ---------------------------------------------------

  // Fill %field% placeholders in a cloned template fragment WITHOUT innerHTML
  // (setting text/attr values, never parsing HTML) — so it works under a strict
  // Trusted-Types CSP, and message text can never be interpreted as markup.
  function subst(str, msg) {
    return str.replace(/%(\w+)%/g, function (_, k) {
      return msg[k] == null ? "" : String(msg[k]);
    });
  }
  function fillNode(node, msg) {
    if (node.nodeType === 3) {
      if (node.nodeValue.indexOf("%") !== -1) node.nodeValue = subst(node.nodeValue, msg);
      return;
    }
    if (node.nodeType === 1 && node.attributes) {
      for (var i = 0; i < node.attributes.length; i++) {
        var a = node.attributes[i];
        if (a.value.indexOf("%") !== -1) a.value = subst(a.value, msg);
      }
    }
    var kids = node.childNodes;
    for (var j = 0; j < kids.length; j++) fillNode(kids[j], msg);
  }

  function bindContainer(el) {
    var name = el.getAttribute("data-room");
    var user = el.getAttribute("data-room-user");
    var query = el.getAttribute("data-room-query") || (user ? "user=" + encodeURIComponent(user) : "");
    var key = roomKey(name, query);
    if (el.__wireRoomBound && el.__wireRoomKey === key) return;
    el.__wireRoomBound = true;
    el.__wireRoomKey = key;
    var room = openRoom(name, query);
    el.__wireRoom = room;

    var log = el.querySelector("[data-room-log]");
    var status = el.querySelector("[data-room-status]");
    var templates = {};
    var tnodes = el.querySelectorAll("template[data-room-item]");
    for (var i = 0; i < tnodes.length; i++) {
      templates[tnodes[i].getAttribute("data-room-item") || ""] = tnodes[i];
    }

    function setStatus(text, variant) {
      if (!status) return;
      status.textContent = text;
      if (status.hasAttribute("data-room-status-class")) {
        status.className = status.getAttribute("data-room-status-class") + " " + variant;
      }
    }

    room.on("*", function (msg) {
      if (msg.type === "__open") return setStatus("connected", "is-connected");
      if (msg.type === "__close") return setStatus("disconnected", "is-disconnected");
      if (msg.type === "__error") return setStatus("error", "is-error");
      if (!log) return;
      var tpl = templates[msg.type];
      if (tpl == null) tpl = templates[""];
      if (tpl == null || !tpl.content) return; // no template for this type
      var frag = tpl.content.cloneNode(true);
      fillNode(frag, msg);
      log.appendChild(frag);
      log.scrollTop = log.scrollHeight;
    });

    var form = el.querySelector("form[data-room-send]");
    if (form && !form.__wireRoomForm) {
      form.__wireRoomForm = true;
      form.addEventListener("submit", function (e) {
        e.preventDefault();
        var data = {};
        for (var i = 0; i < form.elements.length; i++) {
          var input = form.elements[i];
          if (input.name) data[input.name] = input.value;
        }
        if (el.__wireRoom) el.__wireRoom.send(data);
        for (var j = 0; j < form.elements.length; j++) {
          if (form.elements[j].hasAttribute("data-room-reset")) form.elements[j].value = "";
        }
      });
    }
  }

  function bindAll(root) {
    var containers = (root || document).querySelectorAll("[data-room]");
    var present = {};
    for (var i = 0; i < containers.length; i++) {
      bindContainer(containers[i]);
      if (containers[i].__wireRoomKey) present[containers[i].__wireRoomKey] = true;
    }
    // Close rooms whose container has left the page (client-side navigation).
    for (var nm in open) if (!present[nm]) open[nm].close();
  }

  wire.bindRooms = bindAll;
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", function () { bindAll(document); });
  else bindAll(document);
  window.addEventListener("wrnexus:navigated", function () { bindAll(document); });
})();
`.trim();
