/**
 * Minimal, safe Markdown → HTML renderer for server-side exports (HTML / print → PDF).
 *
 * Why not react-markdown here? `react-dom/server` is not available inside App Router route handlers and the
 * remark/rehype internals are transitive (pnpm strict) dependencies. This renderer covers the GFM subset
 * that LLM output actually uses — headings, paragraphs, emphasis, inline code, fenced code, lists (nested by
 * indentation), blockquotes, tables, links, images, horizontal rules — and escapes everything else.
 * Raw HTML in the source is always escaped (never passed through). Unit-tested in tests/unit/export-html.test.ts.
 */

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

function safeUrl(url: string): string | null {
  const u = url.trim();
  if (/^(https?:|mailto:)/i.test(u)) return u;
  if (/^#/.test(u)) return u;
  return null;
}

/* ---- inline ---------------------------------------------------------------------------------- */

export function renderInline(src: string): string {
  // Protect code spans first so their content is not touched by emphasis rules.
  const codes: string[] = [];
  let s = src.replace(/(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/g, (_m, _t, code: string) => {
    codes.push(`<code>${escapeHtml(code.trim())}</code>`);
    return ` ${codes.length - 1} `;
  });
  s = escapeHtml(s);
  // images ![alt](src)
  s = s.replace(/!\[([^\]]*)\]\(((?:[^()\s]|\([^()\s]*\))+)(?:\s+&quot;[^&]*&quot;)?\)/g, (_m, alt: string, url: string) => {
    const u = safeUrl(url);
    return u ? `<img src="${escapeHtml(u)}" alt="${alt}" />` : alt;
  });
  // links [text](url) — one level of balanced parentheses allowed inside the URL
  s = s.replace(/\[([^\]]+)\]\(((?:[^()\s]|\([^()\s]*\))+)(?:\s+&quot;[^&]*&quot;)?\)/g, (_m, text: string, url: string) => {
    const u = safeUrl(url);
    return u ? `<a href="${escapeHtml(u)}" rel="noopener noreferrer nofollow">${text}</a>` : text;
  });
  // autolinks
  s = s.replace(/(^|[\s(])((?:https?:\/\/)[^\s<)]+[^\s<).,;:!?'"])/g, (_m, pre: string, url: string) => `${pre}<a href="${url}" rel="noopener noreferrer nofollow">${url}</a>`);
  // strong / em / strike
  s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/__([^_]+)__/g, "<strong>$1</strong>");
  s = s.replace(/(^|[^*\w])\*([^*\n]+)\*(?!\w)/g, "$1<em>$2</em>").replace(/(^|[^_\w])_([^_\n]+)_(?!\w)/g, "$1<em>$2</em>");
  s = s.replace(/~~([^~]+)~~/g, "<del>$1</del>");
  // hard line breaks (two trailing spaces) → <br>
  s = s.replace(/ {2,}\n/g, "<br />\n");
  // restore code spans
  s = s.replace(/ (\d+) /g, (_m, i: string) => codes[Number(i)]);
  return s;
}

/* ---- block ----------------------------------------------------------------------------------- */

type ListItem = { text: string; children: string[] };

function renderList(lines: string[], start: number): { html: string; next: number } {
  const first = lines[start];
  const baseIndent = first.match(/^\s*/)![0].length;
  const ordered = /^\s*\d+[.)]\s/.test(first);
  const items: ListItem[] = [];
  let i = start;
  while (i < lines.length) {
    const line = lines[i];
    if (!line.trim()) {
      // blank line inside a list: continue if the next non-blank line is still part of the list
      const nxt = lines[i + 1];
      if (nxt !== undefined && (nxt.match(/^\s*/)![0].length > baseIndent || /^\s*(?:[-*+]|\d+[.)])\s/.test(nxt)) && nxt.match(/^\s*/)![0].length >= baseIndent) {
        i++;
        continue;
      }
      break;
    }
    const indent = line.match(/^\s*/)![0].length;
    const marker = ordered ? /^\s*\d+[.)]\s+(.*)$/.exec(line) : /^\s*[-*+]\s+(.*)$/.exec(line);
    if (indent === baseIndent && marker) {
      items.push({ text: marker[1], children: [] });
      i++;
      continue;
    }
    if (indent > baseIndent && items.length) {
      items[items.length - 1].children.push(line);
      i++;
      continue;
    }
    break;
  }
  const tag = ordered ? "ol" : "ul";
  const body = items
    .map((it) => {
      const task = /^\[( |x|X)\]\s+/.exec(it.text);
      let text = it.text;
      let prefix = "";
      if (task) {
        text = text.slice(task[0].length);
        prefix = `<input type="checkbox" disabled${task[1] !== " " ? " checked" : ""} /> `;
      }
      const inner = it.children.length ? renderBlocks(dedent(it.children)) : "";
      return `<li>${prefix}${renderInline(text)}${inner}</li>`;
    })
    .join("");
  return { html: `<${tag}>${body}</${tag}>`, next: i };
}

function dedent(lines: string[]): string[] {
  const indents = lines.filter((l) => l.trim()).map((l) => l.match(/^\s*/)![0].length);
  const min = indents.length ? Math.min(...indents) : 0;
  return lines.map((l) => l.slice(Math.min(min, l.match(/^\s*/)![0].length)));
}

function splitTableRow(row: string): string[] {
  const trimmed = row.trim().replace(/^\|/, "").replace(/\|$/, "");
  const cells: string[] = [];
  let cur = "";
  for (let i = 0; i < trimmed.length; i++) {
    const ch = trimmed[i];
    if (ch === "\\" && trimmed[i + 1] === "|") {
      cur += "|";
      i++;
    } else if (ch === "|") {
      cells.push(cur.trim());
      cur = "";
    } else cur += ch;
  }
  cells.push(cur.trim());
  return cells;
}

function isTableDelimiter(line: string): boolean {
  return /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(line) && line.includes("-");
}

function renderTable(lines: string[], start: number): { html: string; next: number } {
  const header = splitTableRow(lines[start]);
  const aligns = splitTableRow(lines[start + 1]).map((c) => (c.startsWith(":") && c.endsWith(":") ? "center" : c.endsWith(":") ? "right" : c.startsWith(":") ? "left" : null));
  let i = start + 2;
  const rows: string[][] = [];
  while (i < lines.length && lines[i].trim() && lines[i].includes("|")) {
    rows.push(splitTableRow(lines[i]));
    i++;
  }
  const align = (idx: number) => (aligns[idx] ? ` style="text-align:${aligns[idx]}"` : "");
  const thead = `<thead><tr>${header.map((c, idx) => `<th${align(idx)}>${renderInline(c)}</th>`).join("")}</tr></thead>`;
  const tbody = rows.length ? `<tbody>${rows.map((r) => `<tr>${header.map((_h, idx) => `<td${align(idx)}>${renderInline(r[idx] ?? "")}</td>`).join("")}</tr>`).join("")}</tbody>` : "";
  return { html: `<table>${thead}${tbody}</table>`, next: i };
}

export function renderBlocks(lines: string[]): string {
  const out: string[] = [];
  let i = 0;
  while (i < lines.length) {
    const line = lines[i];
    if (!line.trim()) {
      i++;
      continue;
    }
    // fenced code
    const fence = /^\s{0,3}(```+|~~~+)\s*([\w+#.-]*)\s*$/.exec(line);
    if (fence) {
      const close = fence[1];
      const lang = fence[2];
      const buf: string[] = [];
      i++;
      while (i < lines.length && !lines[i].trim().startsWith(close)) {
        buf.push(lines[i]);
        i++;
      }
      i++; // closing fence (or EOF)
      out.push(`<pre${lang ? ` data-lang="${escapeHtml(lang)}"` : ""}><code${lang ? ` class="language-${escapeHtml(lang)}"` : ""}>${escapeHtml(buf.join("\n"))}</code></pre>`);
      continue;
    }
    // heading
    const h = /^\s{0,3}(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
    if (h) {
      const level = h[1].length;
      out.push(`<h${level}>${renderInline(h[2])}</h${level}>`);
      i++;
      continue;
    }
    // hr
    if (/^\s{0,3}([-*_])(\s*\1){2,}\s*$/.test(line)) {
      out.push("<hr />");
      i++;
      continue;
    }
    // blockquote
    if (/^\s{0,3}>/.test(line)) {
      const buf: string[] = [];
      while (i < lines.length && /^\s{0,3}>/.test(lines[i])) {
        buf.push(lines[i].replace(/^\s{0,3}>\s?/, ""));
        i++;
      }
      out.push(`<blockquote>${renderBlocks(buf)}</blockquote>`);
      continue;
    }
    // list
    if (/^\s*(?:[-*+]|\d+[.)])\s+/.test(line)) {
      const r = renderList(lines, i);
      out.push(r.html);
      i = r.next;
      continue;
    }
    // table
    if (line.includes("|") && i + 1 < lines.length && isTableDelimiter(lines[i + 1])) {
      const r = renderTable(lines, i);
      out.push(r.html);
      i = r.next;
      continue;
    }
    // paragraph: consume until blank line or block start
    const buf: string[] = [line];
    i++;
    while (i < lines.length && lines[i].trim() && !/^\s{0,3}(```|~~~|#{1,6}\s|>)/.test(lines[i]) && !/^\s*(?:[-*+]|\d+[.)])\s+/.test(lines[i]) && !(lines[i].includes("|") && i + 1 < lines.length && isTableDelimiter(lines[i + 1]))) {
      buf.push(lines[i]);
      i++;
    }
    out.push(`<p>${renderInline(buf.join("\n"))}</p>`);
  }
  return out.join("\n");
}

/** Render a markdown string to sanitized HTML. */
export function markdownToHtml(md: string): string {
  return renderBlocks(md.replace(/\r\n?/g, "\n").split("\n"));
}
