import {Controller} from "@hotwired/stimulus";

/**
 * Polyfill for declarative shadow DOM (`<template shadowrootmode>`) on
 * JS-inserted HTML, e.g. after Turbo navigations.
 *
 * The browser only consumes `<template shadowrootmode>` during the initial
 * HTML parse. Markup arriving via fetch/Turbo never triggers the parser path,
 * so the templates sit inert in the DOM and the host element stays empty.
 *
 * This controller walks the host's subtree on connect, manually attaches a
 * shadow root for every such template, moves the template's content into it,
 * and removes the template — the same end state the browser would produce on
 * a cold load. On cold loads the templates are already gone, so this is a
 * no-op.
 *
 * With `highlightTokens` set, `{{ merge.tokens }}` inside the rendered document
 * are wrapped in a highlighted chip — used by the notification "Platzhalter"
 * preview so the merge fields stand out in the real e-mail.
 */
export default class extends Controller {
  static values = { highlightTokens: Boolean };

  connect() {
    this.adopt(this.element);
    // On cold loads the parser already attached the shadow roots (adopt is a
    // no-op), so highlight whatever is present now.
    this.highlightAll();

    // Symfony UX Live preserves controller-bearing elements across morphs, so a
    // morph that swaps in a fresh `<template shadowrootmode>` (e.g. a live
    // preview re-render) never re-fires connect(). Watch the subtree and adopt
    // any template that arrives later, replacing stale shadow content.
    this.observer = new MutationObserver(() => {
      this.adopt(this.element);
      this.highlightAll();
    });
    this.observer.observe(this.element, { childList: true, subtree: true });
  }

  disconnect() {
    this.observer?.disconnect();
  }

  adopt(root) {
    const templates = root.querySelectorAll("template[shadowrootmode]");
    templates.forEach((template) => {
      const host = template.parentElement;
      if (!host) return;

      try {
        if (host.shadowRoot) {
          // A morph delivered fresh content into an already-adopted host:
          // replace the existing shadow content instead of bailing out.
          host.shadowRoot.replaceChildren(template.content);
          host.shadowRoot._tokensHighlighted = false;
        } else {
          const mode = template.getAttribute("shadowrootmode") || "open";
          host.attachShadow({ mode }).appendChild(template.content);
        }
        template.remove();
        // Handle nested declarative shadow roots inside the freshly attached one.
        this.adopt(host.shadowRoot);
      } catch {
        // attachShadow not allowed here — leave the template in place.
      }
    });
  }

  highlightAll() {
    if (!this.highlightTokensValue) return;
    if (this.element.shadowRoot) this.highlight(this.element.shadowRoot);
    this.element.querySelectorAll("*").forEach((el) => {
      if (el.shadowRoot) this.highlight(el.shadowRoot);
    });
  }

  // Wrap `{{ token }}` merge fields in a highlighted chip, walking text nodes so
  // the surrounding e-mail markup and styles are left untouched.
  highlight(root) {
    if (root._tokensHighlighted) return;
    root._tokensHighlighted = true;

    const tokenRe = /(\{\{\s*[\w.]+\s*\}\})/g;
    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
    const targets = [];
    let node;
    while ((node = walker.nextNode())) {
      const parent = node.parentElement;
      if (!parent || parent.tagName === "STYLE" || parent.tagName === "SCRIPT") continue;
      if (node.nodeValue && node.nodeValue.includes("{{")) targets.push(node);
    }

    targets.forEach((textNode) => {
      const text = textNode.nodeValue;
      tokenRe.lastIndex = 0;
      if (!tokenRe.test(text)) return;

      const fragment = document.createDocumentFragment();
      let last = 0;
      let match;
      tokenRe.lastIndex = 0;
      while ((match = tokenRe.exec(text)) !== null) {
        if (match.index > last) {
          fragment.appendChild(document.createTextNode(text.slice(last, match.index)));
        }
        const chip = document.createElement("span");
        chip.textContent = match[0];
        chip.style.cssText = "background:#dbeafe;color:#1d4ed8;border-radius:4px;padding:0 4px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.9em;white-space:nowrap;";
        fragment.appendChild(chip);
        last = match.index + match[0].length;
      }
      if (last < text.length) {
        fragment.appendChild(document.createTextNode(text.slice(last)));
      }
      textNode.parentNode.replaceChild(fragment, textNode);
    });
  }
}
