/**
 * ReactiveStatManager — mounts as a child of <Article> and reactively
 * replaces ${key} template patterns in the rendered markdown DOM with
 * live stat values from the journal stream.
 *
 * On mount, walks all text nodes in the article content looking for
 * ${key} placeholders. Each match is wrapped in a <span> with a
 * data-reactive-stat attribute so that subsequent updates (driven by
 * a Solid effect) only touch those spans.
 */

import { Component, createEffect, onCleanup, createMemo } from "solid-js";
import { useArticleDom } from "../Article";
import { useJournalStream } from "../stores/journalStream";
import { useJournalCompletions } from "../journal/completions";
import { fullKey } from "../journal/stat-helpers";
import type { StatDef } from "../journal/completions";

const TEMPLATE_RE = /\$\{(\w+)\}/g;

/** Tag name used for marker spans — must stay in sync with the scan pass. */
const MARKER = "data-reactive-stat";

export const ReactiveStatManager: Component = () => {
  const contentDom = useArticleDom();
  const stream = useJournalStream();
  const comp = useJournalCompletions();

  const statDefs = createMemo(() => comp.data.stats);
  const values = createMemo(() => stream.stats);

  /** Build a lookup: bare key -> StatDef (for scope-aware resolution) */
  const bareDefMap = createMemo(() => {
    const map = new Map<string, StatDef>();
    for (const def of statDefs()) {
      if (!map.has(def.key)) {
        map.set(def.key, def);
      }
    }
    return map;
  });

  /**
   * Resolve a bare key from markdown template to its runtime value.
   * Uses StatDef scoping when available, falls back to player-first, global-second.
   */
  function resolveBareKey(bareKey: string): string {
    const bareDef = bareDefMap().get(bareKey);
    if (bareDef) {
      const fk = fullKey(bareDef, stream.myName);
      return values()[fk] ?? bareDef.default ?? "";
    }
    const pk = `${stream.myName}:${bareKey}`;
    if (values()[pk] !== undefined) return values()[pk];
    return values()[bareKey] ?? "";
  }

  // ---- Initial scan: wrap ${key} text in marker spans ----

  createEffect(() => {
    const dom = contentDom();
    if (!dom) return;

    // Only scan once — after the first scan, the spans exist and we
    // switch to the reactive update effect below.
    if (dom.querySelector(`[${MARKER}]`)) return;

    scanAndWrap(dom);
  });

  onCleanup(() => {
    const dom = contentDom();
    if (dom) unwrapAll(dom);
  });

  // ---- Reactive updates: whenever stat values change, update all spans ----

  createEffect(() => {
    const dom = contentDom();
    if (!dom) return;

    const v = values();
    const spans = dom.querySelectorAll<HTMLElement>(`[${MARKER}]`);

    for (const span of spans) {
      const template = span.getAttribute(MARKER) ?? "";
      let result = template;
      TEMPLATE_RE.lastIndex = 0;
      let m: RegExpExecArray | null;
      while ((m = TEMPLATE_RE.exec(template)) !== null) {
        result = result.replace(`\${${m[1]}}`, resolveBareKey(m[1]));
      }
      if (span.textContent !== result) {
        span.textContent = result;
      }
    }
  });

  return null; // renders nothing — works purely via DOM manipulation
};

// ---------------------------------------------------------------------------
// DOM helpers
// ---------------------------------------------------------------------------

/**
 * Walk all text nodes in a subtree and wrap `${key}` patterns in
 * `<span data-reactive-stat="originalText">` elements.
 */
function scanAndWrap(root: Element): void {
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
  const replacements: { node: Text; html: string }[] = [];

  let textNode: Text | null;
  while ((textNode = walker.nextNode() as Text | null)) {
    const text = textNode.textContent;
    if (!text) continue;

    TEMPLATE_RE.lastIndex = 0;
    if (!TEMPLATE_RE.test(text)) continue;

    // Build replacement HTML: split on ${key} and wrap each key span
    TEMPLATE_RE.lastIndex = 0;
    const parts: string[] = [];
    let lastIndex = 0;
    let m: RegExpExecArray | null;
    while ((m = TEMPLATE_RE.exec(text)) !== null) {
      if (m.index > lastIndex) {
        parts.push(escapeHtml(text.slice(lastIndex, m.index)));
      }
      parts.push(
        `<span ${MARKER}="${escapeAttr(m[0])}">${escapeHtml(m[0])}</span>`,
      );
      lastIndex = TEMPLATE_RE.lastIndex;
    }
    if (lastIndex < text.length) {
      parts.push(escapeHtml(text.slice(lastIndex)));
    }

    replacements.push({ node: textNode, html: parts.join("") });
  }

  for (const { node, html } of replacements) {
    const wrapper = document.createElement("span");
    wrapper.innerHTML = html;
    node.replaceWith(wrapper);
  }
}

/**
 * Reverse scanAndWrap — remove marker spans, restoring plain text.
 * Called on cleanup so subsequent remounts get a clean slate.
 */
function unwrapAll(root: Element): void {
  const spans = root.querySelectorAll<HTMLElement>(`[${MARKER}]`);
  // Process in reverse to avoid DOM mutation issues
  for (let i = spans.length - 1; i >= 0; i--) {
    const span = spans[i];
    const parent = span.parentNode;
    if (!parent) continue;
    const text = span.getAttribute(MARKER) ?? span.textContent ?? "";
    span.replaceWith(document.createTextNode(text));
  }
  // Normalize to merge adjacent text nodes
  root.normalize();
}

function escapeHtml(s: string): string {
  return s
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

function escapeAttr(s: string): string {
  return s
    .replace(/&/g, "&amp;")
    .replace(/"/g, "&quot;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

export default ReactiveStatManager;
