/**
 * Minimal markdown -> HTML email conversion + safe sanitization.
 *
 * Used by the Phase 7 sales send-out flow (`document-sends.service.ts`) to
 * render rep-authored bodies into the email-safe HTML we hand to nodemailer.
 *
 * § §14.7 critical mitigation: "Body markdown with HTML/script injection".
 * This module is the choke point - every code path that turns rep-authored
 * markdown into an email body MUST go through `renderEmailBody()`. Direct
 * passthrough to `transporter.sendMail({ html })` from a user-supplied string
 * is a code-review block.
 *
 * The implementation is intentionally tiny (no DOMPurify, no marked) for two
 * reasons:
 *
 *   1. Email clients render a strict subset of HTML anyway - paragraphs,
 *      bold/italic, line breaks, links and code spans cover ~99% of what
 *      reps actually write. Anything more complex (tables, images, lists)
 *      goes via the admin-editable HTML body templates.
 *   2. Adding a transitive deps surface for the markdown→HTML conversion
 *      doubles the attack surface for the very mitigation it implements.
 *
 * The renderer:
 *   - HTML-escapes every input character before applying any markdown rules.
 *   - Whitelists exactly: paragraphs, line breaks, **bold**, _italic_, `code`,
 *     and `[text](https://...)` links (https only).
 *   - Strips any other markdown / HTML constructs by virtue of being
 *     escape-first-then-rule-replace.
 *
 * Tested against the standard XSS vector list (`<script>`, `<img onerror>`,
 * `javascript:` URLs, `<iframe>`, etc.) - see
 * `tests/unit/markdown-email-sanitization.test.ts`.
 */

const MAX_BODY_BYTES = 50 * 1024; // 50 KB hard cap matching §14.7

/** Re-export for the sender service so it doesn't have to remember the cap. */
export const EMAIL_BODY_MAX_BYTES = MAX_BODY_BYTES;

/**
 * Escape every HTML-significant character. Run on raw input BEFORE any
 * markdown rules so user-supplied HTML can never reach the rendered body.
 */
function escapeHtml(input: string): string {
  return input
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

/**
 * The list of allowed URL schemes. We deliberately reject `javascript:`,
 * `data:`, `vbscript:`, and bare-word schemes (which some browsers resolve
 * to `http:`) - only fully-qualified `https://` and `mailto:` make it through.
 */
function isSafeHref(href: string): boolean {
  const trimmed = href.trim().toLowerCase();
  return trimmed.startsWith('https://') || trimmed.startsWith('mailto:');
}

/**
 * Apply inline markdown rules to an already-HTML-escaped string. The order
 * matters: code spans win over emphasis, links win over emphasis (so
 * `[**foo**](url)` works), and emphasis is greedy-non-greedy aware.
 */
function applyInlineRules(escaped: string): string {
  let out = escaped;

  // `code` (single backticks; nothing fancier - no fenced blocks here)
  out = out.replace(/`([^`\n]+)`/g, '<code>$1</code>');

  // [text](href) - href validated. Bracket text is already escaped so we
  // pass it through verbatim. Use a non-greedy match so two links on one
  // line don't collapse.
  out = out.replace(/\[([^\]\n]+?)\]\(([^)\n]+?)\)/g, (_full, text: string, href: string) => {
    // The href captured group came from already-escaped input, so the
    // entity-encoded chars need to survive into the attribute. We DO NOT
    // unescape here; the escaped form (`&amp;`, `&#39;` etc.) is the safe
    // representation for attribute values.
    if (!isSafeHref(href.replace(/&amp;/g, '&'))) {
      // Drop the link entirely; render the text as plain.
      return text;
    }
    return `<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`;
  });

  // **bold** then *italic* / _italic_
  out = out.replace(/\*\*([^*\n]+?)\*\*/g, '<strong>$1</strong>');
  out = out.replace(/(?<!\*)\*([^*\n]+?)\*(?!\*)/g, '<em>$1</em>');
  out = out.replace(/(^|\s)_([^_\n]+?)_(?=\s|$)/g, '$1<em>$2</em>');

  return out;
}

/**
 * Convert rep-authored markdown into email-safe HTML.
 *
 * Throws when the input exceeds {@link EMAIL_BODY_MAX_BYTES}.
 */
export function renderEmailBody(markdown: string): string {
  if (Buffer.byteLength(markdown, 'utf8') > MAX_BODY_BYTES) {
    throw new Error(`Email body exceeds maximum length (${MAX_BODY_BYTES} bytes)`);
  }

  // 1. HTML-escape EVERYTHING first - there is no path through the rules
  //    below that lets an unescaped angle bracket reach the output.
  const escaped = escapeHtml(markdown);

  // 2. Split into paragraphs on blank lines; within each paragraph,
  //    single newlines become <br>.
  const paragraphs = escaped.split(/\n{2,}/);
  const rendered = paragraphs
    .map((para) => {
      const trimmed = para.trim();
      if (!trimmed) return '';
      const inline = applyInlineRules(trimmed).replace(/\n/g, '<br>');
      return `<p>${inline}</p>`;
    })
    .filter(Boolean)
    .join('\n');

  return rendered;
}

/**
 * Find every `{{token}}` reference in a body. Returns the raw token strings
 * (with braces) for caller-side validation against the merge-field catalog.
 */
export function extractTokens(markdown: string): string[] {
  const matches = markdown.match(/\{\{[^{}\n]+?\}\}/g);
  return matches ? Array.from(new Set(matches)) : [];
}

/**
 * Markdown-significant characters that need to be neutralized before a merge
 * value is substituted into the rep-authored body. Without this, a value
 * like `[click here](https://attacker.tld)` stored on a client/company would
 * survive `renderEmailBody`'s HTML escape (escapeHtml leaves `[`, `]`, `(`,
 * `)` intact) and produce a real `<a href>` in the outbound email - a
 * phishing lure delivered from the legitimate sales account.
 *
 * Each char is replaced with its HTML entity. The entity encoding survives
 * `escapeHtml` (which only re-escapes `&`) and renders as the original
 * literal character - visually the user still sees their data verbatim,
 * but the markdown rules (link, emphasis, code) no longer fire on it.
 */
const MERGE_VALUE_ESCAPE_MAP: Record<string, string> = {
  '\\': '&#92;',
  '`': '&#96;',
  '*': '&#42;',
  _: '&#95;',
  '[': '&#91;',
  ']': '&#93;',
  '(': '&#40;',
  ')': '&#41;',
  '{': '&#123;',
  '}': '&#125;',
};

function escapeMergeValue(value: string): string {
  return value.replace(/[\\`*_[\](){}]/g, (ch) => MERGE_VALUE_ESCAPE_MAP[ch] ?? ch);
}

/**
 * Replace `{{token}}` references with values from the supplied map. Tokens
 * not present in the map are left intact so the dry-run reporter can flag
 * them. Values are markdown-escaped before substitution so a malicious
 * field cannot inject a link, emphasis, or another `{{token}}` form.
 */
export function expandMergeTokens(
  markdown: string,
  values: Record<string, string | number | null | undefined>,
): string {
  return markdown.replace(/\{\{([^{}\n]+?)\}\}/g, (full, raw: string) => {
    const key = `{{${raw}}}`;
    const value = values[key];
    if (value === null || value === undefined || value === '') return full;
    return escapeMergeValue(String(value));
  });
}

/**
 * Returns the list of `{{token}}` references in `markdown` that aren't
 * present (or are blank/null) in the provided value map. Used by the
 * pre-send dry-run UI per §14.7 ("Body markdown contains unresolved merge
 * fields - Send blocked until resolved").
 */
export function findUnresolvedTokens(
  markdown: string,
  values: Record<string, string | number | null | undefined>,
): string[] {
  return extractTokens(markdown).filter((token) => {
    const v = values[token];
    return v === undefined || v === null || v === '';
  });
}
