(function () {
  if (window.__blissInspectorLoaded) return;
  window.__blissInspectorLoaded = true;

  const ROUTE_SELECTOR = "[data-bliss-structure-id][data-bliss-route-id]";
  const TEMPLATE_SELECTOR = "[data-bliss-template-id]";
  let root = null;
  let open = false;

  function visibleElements(selector) {
    return [...document.querySelectorAll(selector)].filter(
      (element) => !element.closest("[data-bliss-ui]"),
    );
  }

  function routeElements() {
    return visibleElements(ROUTE_SELECTOR);
  }

  function templateElements() {
    return visibleElements(TEMPLATE_SELECTOR);
  }

  function templatesFor(element) {
    try {
      const templates = JSON.parse(element.dataset.blissTemplates || "[]");
      if (templates.length) return templates;
    } catch (_) {}
    return element.dataset.blissTemplateId
      ? [
          {
            id: element.dataset.blissTemplateId,
            name: element.dataset.blissTemplateName,
          },
        ]
      : [];
  }

  function inventory() {
    const items = [];
    for (const owner of routeElements()) {
      const templates = templateElements().filter(
        (element) => element.closest(ROUTE_SELECTOR) === owner,
      );
      if (!templates.length) templates.push(null);
      for (const templateElement of templates) {
        const renderedTemplates = templateElement
          ? templatesFor(templateElement)
          : [null];
        for (const template of renderedTemplates) {
          items.push({
            structureId: owner.dataset.blissStructureId,
            structureName: owner.dataset.blissStructureName,
            routeId: owner.dataset.blissRouteId,
            routeName: owner.dataset.blissRouteName,
            templateId: template?.id || null,
            templateName: template?.name || null,
          });
        }
      }
    }
    return items;
  }

  function publishInventory() {
    document.dispatchEvent(
      new CustomEvent("bliss:inventory", { detail: inventory() }),
    );
  }

  function matches(element, { kind, id }) {
    if (kind === "template") {
      return templatesFor(element).some(
        (template) => String(template.id) === String(id),
      );
    }
    const field = {
      structure: "blissStructureId",
      route: "blissRouteId",
    }[kind];
    return field && element.dataset[field] === String(id);
  }

  function highlight(detail) {
    const candidates =
      detail.kind === "template" ? templateElements() : routeElements();
    for (const element of candidates.filter((el) => matches(el, detail))) {
      element.classList.toggle("bliss-source-highlight", detail.on);
    }
  }

  async function refreshElement(element) {
    if (element.dataset.blissMethod !== "GET") return;
    const url = element.dataset.blissRequestUrl;
    if (!url) return;

    if (element === document.body) {
      const ui = [...document.body.querySelectorAll(":scope > [data-bliss-ui]")];
      const response = await fetch(url, { headers: { "HX-Request": "true" } });
      if (!response.ok) throw new Error(`GET ${url} returned ${response.status}`);
      const next = new DOMParser().parseFromString(await response.text(), "text/html");
      for (const attribute of [...document.body.attributes]) {
        document.body.removeAttribute(attribute.name);
      }
      for (const attribute of [...next.body.attributes]) {
        document.body.setAttribute(attribute.name, attribute.value);
      }
      document.body.innerHTML = next.body.innerHTML;
      for (const node of ui) document.body.appendChild(node);
      htmx.process(document.body);
      return;
    }

    await htmx.ajax("GET", url, { target: element, swap: "outerHTML" });
  }

  async function refreshSources({ kind, id, method }) {
    if (method && method !== "GET") return;
    const elements = routeElements().filter((element) => {
      if (kind === "template") {
        return templateElements().some(
          (template) =>
            matches(template, { kind: "template", id }) &&
            template.closest(ROUTE_SELECTOR) === element,
        );
      }
      return matches(element, { kind, id });
    });
    const settled = await Promise.allSettled(elements.map(refreshElement));
    const failure = settled.find((result) => result.status === "rejected");
    if (failure) console.error("Bliss live refresh failed", failure.reason);
    publishInventory();
  }

  function show() {
    if (open) return;
    open = true;
    document.documentElement.classList.add("bliss-inspector-open");
    root = document.createElement("div");
    root.id = "bliss-inspector-root";
    root.dataset.blissUi = "";
    root.setAttribute("hx-get", "/_bliss/inspector");
    root.setAttribute("hx-trigger", "load");
    root.setAttribute("hx-swap", "innerHTML");
    document.body.appendChild(root);
    htmx.process(root);
  }

  function hide() {
    open = false;
    document.documentElement.classList.remove("bliss-inspector-open");
    root?.remove();
    root = null;
    for (const element of [...routeElements(), ...templateElements()]) {
      element.classList.remove("bliss-source-highlight");
    }
  }

  document.addEventListener("DOMContentLoaded", () => {
    const style = document.createElement("style");
    style.dataset.blissUi = "";
    style.textContent = `
      html.bliss-inspector-open body > :not([data-bliss-ui]) {
        transform: translateX(min(28rem, 90vw));
        max-width: calc(100vw - min(28rem, 90vw));
      }
      [data-bliss-structure-id], [data-bliss-template-id] {
        transition: outline-color 80ms linear, background-color 80ms linear;
      }
      .bliss-source-highlight {
        outline: 4px solid #ff00a8 !important;
        outline-offset: 3px !important;
        background-color: rgba(255, 0, 168, .08) !important;
      }
    `;
    document.head.appendChild(style);
    const trigger = document.createElement("button");
    trigger.type = "button";
    trigger.textContent = "🔍";
    trigger.title = "Open Bliss live editor";
    trigger.dataset.blissUi = "";
    trigger.style.cssText =
      "position:fixed;right:.5rem;bottom:.5rem;z-index:2147483647;border:0;background:white;border-radius:999px;padding:.45rem;font-size:1.25rem;cursor:pointer;box-shadow:0 2px 8px #0004";
    trigger.onclick = () => (open ? hide() : show());
    document.body.appendChild(trigger);
  });

  document.addEventListener("bliss:highlight", (event) => highlight(event.detail));
  document.addEventListener("bliss:source-saved", (event) => refreshSources(event.detail));
  document.addEventListener("bliss:inventory-request", publishInventory);
  document.addEventListener("bliss:inspector-close", hide);
  document.addEventListener("htmx:afterSwap", () => {
    if (open) publishInventory();
  });
})();
