import { Injectable } from '@nestjs/common';

/**
 * Mustache-lite renderer — no external dependency (avoids adding
 * handlebars/mustache just for this feature).
 *
 * Supported syntax:
 *   {{path.to.value}}        — plain substitution (dot walk into the data bag)
 *   {{#each items}} … {{/each}}
 *                            — iterate over an array; inside the block,
 *                              `{{this}}` binds to the current element,
 *                              and any {{prop}} walks the current item first
 *                              then falls back to the outer scope.
 *   {{#if flag}} … {{/if}}   — render the block only when `flag` is truthy
 *
 * Not supported: {{#unless}}, partials, helpers, else blocks. If we
 * ever need one, wrap the caller's data bag with a precomputed key
 * instead of expanding this grammar.
 *
 * By default output is *NOT* HTML-escaped — email templates are
 * authored as HTML and legitimately need `<b>`, `<table>`, etc. to
 * render. WhatsApp bodies are plain text, so escaping there is also
 * a no-op. Callers who need escaping wrap unsafe values themselves.
 */
@Injectable()
export class TemplateRendererService {
  render(template: string, data: Record<string, unknown>): string {
    return renderScope(template, [data]);
  }
}

function renderScope(template: string, scopeStack: Record<string, unknown>[]): string {
  // Process block sections first so nested {{var}} inside them binds
  // to the current iteration scope. Blocks may be nested; the regex
  // is intentionally non-greedy and we recurse into the body.
  let out = template;

  // {{#each name}} ... {{/each}}
  out = replaceBlock(out, 'each', (name, body) => {
    const arr = lookup(scopeStack, name);
    if (!Array.isArray(arr)) return '';
    const parts: string[] = [];
    for (const item of arr) {
      const itemScope: Record<string, unknown> =
        item && typeof item === 'object' ? (item as Record<string, unknown>) : { this: item };
      parts.push(renderScope(body, [itemScope, ...scopeStack]));
    }
    return parts.join('');
  });

  // {{#if name}} ... {{/if}}
  out = replaceBlock(out, 'if', (name, body) => {
    const v = lookup(scopeStack, name);
    return truthy(v) ? renderScope(body, scopeStack) : '';
  });

  // {{var}} — plain substitution
  out = out.replace(/\{\{\s*([^#/{][^}]*?)\s*\}\}/g, (_, key: string) => {
    const v = lookup(scopeStack, key.trim());
    return v == null ? '' : String(v);
  });

  return out;
}

function replaceBlock(
  template: string,
  keyword: 'each' | 'if',
  fn: (name: string, body: string) => string,
): string {
  const open = new RegExp(`\\{\\{\\s*#${keyword}\\s+([^}]+?)\\s*\\}\\}`, 'g');
  const close = `{{/${keyword}}}`;
  let out = '';
  let i = 0;
  while (i < template.length) {
    open.lastIndex = i;
    const m = open.exec(template);
    if (!m) {
      out += template.slice(i);
      break;
    }
    out += template.slice(i, m.index);
    const name = m[1];
    // Balanced close — count nested {{#keyword ...}} inside so
    // we jump to the MATCHING {{/keyword}}, not the first one.
    let depth = 1;
    let j = m.index + m[0].length;
    while (j < template.length && depth > 0) {
      open.lastIndex = j;
      const nextOpen = open.exec(template);
      const nextClose = template.indexOf(close, j);
      if (nextClose === -1) return template; // unbalanced — leave as-is
      if (nextOpen && nextOpen.index < nextClose) {
        depth++;
        j = nextOpen.index + nextOpen[0].length;
      } else {
        depth--;
        if (depth === 0) {
          const body = template.slice(m.index + m[0].length, nextClose);
          out += fn(name, body);
          j = nextClose + close.length;
        } else {
          j = nextClose + close.length;
        }
      }
    }
    i = j;
  }
  return out;
}

function lookup(scopeStack: Record<string, unknown>[], key: string): unknown {
  if (key === 'this' || key === '.') return scopeStack[0]?.this ?? scopeStack[0];
  const parts = key.split('.');
  for (const scope of scopeStack) {
    let cur: unknown = scope;
    let ok = true;
    for (const p of parts) {
      if (cur && typeof cur === 'object' && p in (cur as Record<string, unknown>)) {
        cur = (cur as Record<string, unknown>)[p];
      } else {
        ok = false;
        break;
      }
    }
    if (ok) return cur;
  }
  return undefined;
}

function truthy(v: unknown): boolean {
  if (v == null) return false;
  if (Array.isArray(v)) return v.length > 0;
  if (typeof v === 'string') return v.length > 0;
  if (typeof v === 'number') return v !== 0;
  return !!v;
}
