/**
 * Geteilte Merge-Tag-Substitution für Worker (Send-Pfad) und Preview (Standalone-Tab).
 *
 * Whitespace-Toleranz: matched `{{KEY}}`, `{{ KEY }}`, `{{  KEY  }}` etc.
 * RegExp-Sonderzeichen in Keys werden escaped, damit Tags wie `{{addr.line1}}`
 * oder `{{order(2024)}}` nicht versehentlich als Pattern interpretiert werden.
 *
 * HTML vs. Plain-Text:
 *  - `substituteTagsHtml`: escaped Werte mit `&`, `<`, `>` → für MJML/HTML-Bodies.
 *  - `substituteTagsText`: setzt Werte 1:1 ein → für Plain-Text-Alternative.
 *
 * Beide nutzen denselben Iterations-Loop und respektieren Reservierungs-Verhalten:
 * Wenn `reservedKeys` gesetzt ist, werden Keys aus `tags` übersprungen, die bereits
 * als System-Tag belegt sind. Damit kann der Worker erst System-Tags applien und dann
 * customData-Spalten ohne Risiko von Doppel-Substitution drüberlegen.
 */

export function escapeRegExp(s: string): string {
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/**
 * Minimal-Escape — exakt das was der Worker bisher gemacht hat. Bewusst KEIN
 * Quote-Escape: Werte landen typischerweise in Text-Knoten oder URL-Params, die
 * vom MJML-Renderer/Click-Wrapper bereits korrekt enkodiert werden. Quote-Escape
 * würde bestehende `<a href="…{{TAG}}…">`-Bodies aufbrechen.
 */
export function escapeHtmlForTag(s: string): string {
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

function tagRegex(key: string): RegExp {
  return new RegExp('\\{\\{\\s*' + escapeRegExp(key) + '\\s*\\}\\}', 'g');
}

export interface SubstituteOptions {
  /** Keys die übersprungen werden (z.B. wenn customData einen System-Tag-Key trägt). */
  reservedKeys?: ReadonlySet<string>;
}

function toStringValue(raw: unknown): string {
  if (raw == null) return '';
  if (typeof raw === 'string') return raw;
  if (typeof raw === 'number' || typeof raw === 'boolean') return String(raw);
  // Objekte/Arrays werden als JSON-String eingesetzt — selten der gewünschte
  // Output, aber besser als '[object Object]'. CRM-Spalten sind in der Regel
  // skalar; nested Werte gehören als flache `addr.line1`-Keys ins customData.
  try {
    return JSON.stringify(raw);
  } catch {
    return String(raw);
  }
}

/** Substituiert Tags in einem HTML-Body. Werte werden mit `escapeHtmlForTag` enkodiert. */
export function substituteTagsHtml(
  html: string,
  tags: Record<string, unknown>,
  opts: SubstituteOptions = {},
): string {
  let out = html;
  for (const [key, raw] of Object.entries(tags)) {
    if (opts.reservedKeys?.has(key)) continue;
    out = out.replace(tagRegex(key), escapeHtmlForTag(toStringValue(raw)));
  }
  return out;
}

/** Substituiert Tags in Plain-Text. Werte werden 1:1 eingesetzt (kein Escape). */
export function substituteTagsText(
  text: string,
  tags: Record<string, unknown>,
  opts: SubstituteOptions = {},
): string {
  let out = text;
  for (const [key, raw] of Object.entries(tags)) {
    if (opts.reservedKeys?.has(key)) continue;
    out = out.replace(tagRegex(key), toStringValue(raw));
  }
  return out;
}
