/**
 * htmx-reinit.js — re-initialise non-HTMX scripts after every HTMX swap.
 *
 * In the htmx-ssr rendering profile, scripts like highlight.js, cytoscape,
 * theme-init, and tag-filters run once at page load. When HTMX swaps a
 * fragment in, those scripts don't see the new DOM and the content goes
 * "dead" (unhighlighted code blocks, unwired filter pills, etc.).
 *
 * This module fixes that by exposing a registry —
 * `window.RusteloReinit.register(fn)` — and a single listener on
 * `htmx:afterSettle` that walks the registry and calls each handler with
 * the swapped element as scope.
 *
 * Handlers MUST be idempotent. We help by tracking processed nodes in a
 * WeakSet keyed by handler id, so double-decoration is impossible even if
 * a swap re-emits a region that contains an already-processed node.
 *
 * Usage from another script:
 *
 *   (function () {
 *     window.RusteloReinit.register('highlight', function (scope) {
 *       scope.querySelectorAll('pre code:not(.hljs)').forEach(hljs.highlightElement);
 *     });
 *   })();
 *
 * The registered name is used to namespace the processed-nodes WeakSet so
 * each handler manages its own idempotency.
 */
(function () {
  'use strict';

  if (window.RusteloReinit) {
    return; // already loaded
  }

  /** @type {Array<{ name: string, fn: (scope: Element) => void, seen: WeakSet }>} */
  const handlers = [];

  function findHandler(name) {
    for (let i = 0; i < handlers.length; i++) {
      if (handlers[i].name === name) {
        return handlers[i];
      }
    }
    return null;
  }

  function runHandler(handler, scope) {
    if (handler.seen.has(scope)) {
      return;
    }
    handler.seen.add(scope);
    try {
      handler.fn(scope);
    } catch (err) {
      console.error('[RusteloReinit:' + handler.name + '] handler failed', err);
    }
  }

  const api = {
    /**
     * Register a re-init handler. Calling register twice with the same
     * name replaces the previous handler.
     */
    register: function (name, fn) {
      if (typeof name !== 'string' || typeof fn !== 'function') {
        throw new TypeError('RusteloReinit.register(name, fn) — name must be string, fn must be function');
      }
      const existing = findHandler(name);
      if (existing) {
        existing.fn = fn;
        // Reset the seen set so the replacement gets a fresh chance at the DOM.
        existing.seen = new WeakSet();
      } else {
        handlers.push({ name: name, fn: fn, seen: new WeakSet() });
      }
      // Run immediately on document.body so the initial page gets processed.
      if (document.body) {
        runHandler(findHandler(name), document.body);
      } else {
        document.addEventListener('DOMContentLoaded', function () {
          runHandler(findHandler(name), document.body);
        }, { once: true });
      }
    },

    /**
     * Force-run every handler against the given scope. Used by tests and by
     * the htmx:afterSettle listener.
     */
    runAll: function (scope) {
      for (let i = 0; i < handlers.length; i++) {
        runHandler(handlers[i], scope);
      }
    },

    /** Return registered handler names (debug). */
    names: function () {
      return handlers.map(function (h) { return h.name; });
    }
  };

  window.RusteloReinit = api;

  // Hook into htmx lifecycle events. afterSettle fires after the DOM is in
  // its final state for the swap; we want this rather than afterSwap to
  // avoid running against elements that get replaced moments later.
  document.body && document.body.addEventListener('htmx:afterSettle', function (ev) {
    const target = ev.detail && ev.detail.target;
    if (target instanceof Element) {
      api.runAll(target);
    } else {
      api.runAll(document.body);
    }
  });

  // If body wasn't ready yet, wire on DOMContentLoaded.
  if (!document.body) {
    document.addEventListener('DOMContentLoaded', function () {
      document.body.addEventListener('htmx:afterSettle', function (ev) {
        const target = ev.detail && ev.detail.target;
        if (target instanceof Element) {
          api.runAll(target);
        } else {
          api.runAll(document.body);
        }
      });
    }, { once: true });
  }
})();
