import { esc } from "./html";

/**
 * GitHub-Flavored-ish markdown renderer for README/about pages. Supported:
 * headings, fenced code, lists (incl. task lists), blockquotes, hr, links,
 * images, emphasis, strikethrough, inline code, pipe tables, autolinks, and a
 * conservative allowlist of raw HTML (everything else escaped/stripped).
 * Relative link/image URLs are rewritten against the repo's raw/file routes.
 * The page CSP (default-src 'none') is the primary XSS backstop; the tag
 * sanitizer here is defense in depth.
 */

export interface MarkdownCtx {
  repo: string;
  ref?: string;
}

const ALLOWED_TAGS = new Set([
  "a", "img", "p", "br", "hr", "b", "i", "strong", "em", "del", "ins", "s",
  "sub", "sup", "kbd", "code", "pre", "blockquote", "h1", "h2", "h3", "h4",
  "h5", "h6", "ul", "ol", "li", "table", "thead", "tbody", "tr", "th", "td",
  "details", "summary", "span", "div", "dl", "dt", "dd", "abbr", "mark", "q",
  "samp", "var", "cite", "figure", "figcaption", "picture", "source",
]);
const ALLOWED_ATTRS = new Set([
  "href", "src", "alt", "title", "align", "width", "height", "colspan",
  "rowspan", "start", "type", "id", "open", "srcset", "name",
]);
const URL_ATTRS = new Set(["href", "src", "srcset"]);
const VOID_TAGS = new Set(["br", "hr", "img", "source"]);
const HTML_BLOCK = new Set([
  "table", "thead", "tbody", "tr", "th", "td", "div", "p", "ul", "ol", "li",
  "blockquote", "pre", "hr", "br", "details", "summary", "figure",
  "figcaption", "dl", "dt", "dd", "h1", "h2", "h3", "h4", "h5", "h6",
  "picture", "source",
]);

function safeUrl(url: string, allowDataImage = false): string | null {
  const u = url.trim();
  if (/^(javascript|vbscript):/i.test(u)) return null;
  if (/^data:/i.test(u)) return allowDataImage && /^data:image\//i.test(u) ? u : null;
  if (/^(https?:)?\/\//i.test(u) || u.startsWith("#") || /^mailto:/i.test(u) || /^[\w./?#-]/.test(u)) return u;
  return null;
}

/** Rewrite a relative link/image URL to the repo's file/raw route. */
function rewriteUrl(url: string, ctx: MarkdownCtx | undefined, image: boolean): string | null {
  const u = safeUrl(url, image);
  if (u === null) return null;
  if (!ctx) return u;
  // anchors, absolute, protocol-relative, root-relative, mailto, data: left as-is
  if (/^(#|mailto:|data:|https?:|\/\/|\/)/i.test(u)) return u;
  let rel = u.replace(/^\.\//, "");
  let hash = "";
  const hi = rel.indexOf("#");
  if (hi >= 0) { hash = rel.slice(hi); rel = rel.slice(0, hi); }
  let query = "";
  const qi = rel.indexOf("?");
  if (qi >= 0) { query = rel.slice(qi + 1); rel = rel.slice(0, qi); }
  const r = `/${encodeURIComponent(ctx.repo)}`;
  const kind = image ? "plain" : "tree";
  const encPath = rel.split("/").map(encodeURIComponent).join("/");
  const params = [query, ctx.ref ? `h=${encodeURIComponent(ctx.ref)}` : ""].filter(Boolean).join("&");
  return `${r}/${kind}/${encPath}${params ? "?" + params : ""}${hash}`;
}

function rewriteSrcset(val: string, ctx: MarkdownCtx | undefined): string | null {
  const parts: (string | null)[] = val.split(",").map((p) => {
    const seg = p.trim();
    if (!seg) return "";
    const sp = seg.search(/\s/);
    const url = sp >= 0 ? seg.slice(0, sp) : seg;
    const desc = sp >= 0 ? seg.slice(sp) : "";
    const u = rewriteUrl(url, ctx, true);
    return u === null ? null : u + desc;
  });
  if (parts.some((p) => p === null)) return null;
  return parts.filter(Boolean).join(", ");
}

function parseAttrs(s: string, tag: string, ctx: MarkdownCtx | undefined): string {
  const out: string[] = [];
  const re = /([a-zA-Z][a-zA-Z0-9-]*)(?:\s*=\s*("[^"]*"|'[^']*'|[^\s>]+))?/g;
  let m: RegExpExecArray | null;
  while ((m = re.exec(s))) {
    const name = m[1].toLowerCase();
    if (!ALLOWED_ATTRS.has(name)) continue;
    let val = m[2] ?? "";
    if (val && (val[0] === '"' || val[0] === "'")) val = val.slice(1, -1);
    if (URL_ATTRS.has(name)) {
      if (name === "srcset") {
        const rw = rewriteSrcset(val, ctx);
        if (rw === null) continue;
        val = rw;
      } else {
        const u = rewriteUrl(val, ctx, name === "src" && (tag === "img" || tag === "source"));
        if (u === null) continue;
        val = u;
      }
    }
    out.push(` ${name}='${esc(val)}'`);
  }
  return out.join("");
}

/** Sanitize a single raw HTML tag; escape it entirely if not allowlisted. */
function sanitizeTag(raw: string, ctx: MarkdownCtx | undefined): string {
  const m = raw.match(/^<(\/?)([a-zA-Z][a-zA-Z0-9]*)([\s\S]*?)(\/?)>$/);
  if (!m) return esc(raw);
  const tag = m[2].toLowerCase();
  if (!ALLOWED_TAGS.has(tag)) return esc(raw);
  if (m[1] === "/") return `</${tag}>`;
  const attrs = parseAttrs(m[3], tag, ctx);
  const selfClose = m[4] === "/" || VOID_TAGS.has(tag);
  return `<${tag}${attrs}${selfClose ? "/" : ""}>`;
}

const TAG_RE = /`[^`]+`|<\/?[a-zA-Z][a-zA-Z0-9]*(?:\s+[a-zA-Z-]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?)*\s*\/?>/g;

function inline(text: string, ctx?: MarkdownCtx): string {
  const stash: string[] = [];
  const keep = (html: string) => ` ${stash.push(html) - 1} `;
  // protect inline code spans and raw HTML tags from markdown/escaping
  let s = text.replace(TAG_RE, (m) =>
    m[0] === "`" ? keep(`<code>${esc(m.slice(1, -1))}</code>`) : keep(sanitizeTag(m, ctx))
  );
  s = esc(s);
  // images
  s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+&quot;[^&]*&quot;)?\)/g, (_m, alt, url) => {
    const u = rewriteUrl(url, ctx, true);
    return u ? `<img src='${esc(u)}' alt='${alt}' style='max-width:100%'/>` : alt;
  });
  // links
  s = s.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+&quot;[^&]*&quot;)?\)/g, (_m, txt, url) => {
    const u = rewriteUrl(url, ctx, false);
    return u ? `<a href='${esc(u)}'>${txt}</a>` : txt;
  });
  // strikethrough, bold, italics
  s = s.replace(/~~([^~]+)~~/g, "<del>$1</del>");
  s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
  s = s.replace(/(^|[\s(])\*([^*\s][^*]*)\*/g, "$1<em>$2</em>");
  s = s.replace(/(^|[\s(])_([^_\s][^_]*)_/g, "$1<em>$2</em>");
  // autolinks: bare http(s), www., email
  s = s.replace(/(^|[\s(])(https?:\/\/[^\s<)]+)/g, "$1<a href='$2'>$2</a>");
  s = s.replace(/(^|[\s(])(www\.[^\s<)]+)/g, "$1<a href='http://$2'>$2</a>");
  s = s.replace(/(^|[\s(])([\w.+-]+@[\w-]+(?:\.[\w-]+)+)(?=$|[\s.,;:)])/g, "$1<a href='mailto:$2'>$2</a>");
  // restore protected tokens
  s = s.replace(/ (\d+) /g, (_m, i) => stash[+i]);
  return s;
}

const DELIM_RE = /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?\s*$/;

function splitRow(s: string): string[] {
  let t = s.trim();
  if (t.startsWith("|")) t = t.slice(1);
  if (t.endsWith("|") && !t.endsWith("\\|")) t = t.slice(0, -1);
  return t.split(/(?<!\\)\|/).map((c) => c.trim().replace(/\\\|/g, "|"));
}

function alignOf(cell: string): string {
  const l = cell.trim().startsWith(":");
  const r = cell.trim().endsWith(":");
  if (l && r) return " align='center'";
  if (r) return " align='right'";
  if (l) return " align='left'";
  return "";
}

export function renderMarkdown(src: string, ctx?: MarkdownCtx): string {
  const lines = src.replaceAll("\r\n", "\n").split("\n");
  const out: string[] = [];
  let para: string[] = [];
  let listStack: ("ul" | "ol")[] = [];
  let quote = false;

  const flushPara = () => {
    if (para.length) {
      out.push(`<p>${inline(para.join("\n"), ctx)}</p>`);
      para = [];
    }
  };
  const closeLists = (depth: number) => {
    while (listStack.length > depth) out.push(`</${listStack.pop()}>`);
  };
  const closeQuote = () => {
    if (quote) {
      out.push("</blockquote>");
      quote = false;
    }
  };
  const resetBlocks = () => { flushPara(); closeLists(0); closeQuote(); };

  for (let i = 0; i < lines.length; i++) {
    const raw = lines[i];
    const fence = raw.match(/^\s*(```|~~~)\s*(\S*)\s*$/);
    if (fence) {
      resetBlocks();
      const code: string[] = [];
      i++;
      for (; i < lines.length; i++) {
        if (/^\s*(```|~~~)\s*$/.test(lines[i])) break;
        code.push(lines[i]);
      }
      out.push(`<pre class='md-code'><code>${esc(code.join("\n"))}</code></pre>`);
      continue;
    }
    const heading = raw.match(/^(#{1,6})\s+(.*)$/);
    if (heading) {
      resetBlocks();
      const level = heading[1].length;
      out.push(`<h${level}>${inline(heading[2].replace(/\s+#+\s*$/, ""), ctx)}</h${level}>`);
      continue;
    }
    if (/^\s*([-*_])\s*\1\s*\1[\s\-*_]*$/.test(raw)) {
      resetBlocks();
      out.push("<hr/>");
      continue;
    }
    // pipe table: a row with '|' followed by a delimiter row
    if (raw.includes("|") && i + 1 < lines.length && DELIM_RE.test(lines[i + 1]) && lines[i + 1].includes("-")) {
      resetBlocks();
      const headers = splitRow(raw);
      const aligns = splitRow(lines[i + 1]).map(alignOf);
      const body: string[] = [];
      i += 2;
      for (; i < lines.length && lines[i].includes("|") && lines[i].trim(); i++) {
        body.push(lines[i]);
      }
      i--;
      const th = headers.map((c, j) => `<th${aligns[j] ?? ""}>${inline(c, ctx)}</th>`).join("");
      const rows = body
        .map((r) => {
          const cells = splitRow(r);
          return `<tr>${cells.map((c, j) => `<td${aligns[j] ?? ""}>${inline(c, ctx)}</td>`).join("")}</tr>`;
        })
        .join("");
      out.push(`<table><thead><tr>${th}</tr></thead><tbody>${rows}</tbody></table>`);
      continue;
    }
    // raw HTML block: a line starting with an allowed block-level tag
    const htmlBlock = raw.match(/^\s*<\/?([a-zA-Z][a-zA-Z0-9]*)/);
    if (htmlBlock && HTML_BLOCK.has(htmlBlock[1].toLowerCase())) {
      resetBlocks();
      out.push(inline(raw, ctx));
      continue;
    }
    const bq = raw.match(/^\s*>\s?(.*)$/);
    if (bq) {
      flushPara();
      closeLists(0);
      if (!quote) {
        out.push("<blockquote>");
        quote = true;
      }
      out.push(inline(bq[1], ctx) + "<br/>");
      continue;
    }
    const li = raw.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
    if (li) {
      flushPara();
      closeQuote();
      const depth = Math.min(Math.floor(li[1].length / 2), 3) + 1;
      const kind: "ul" | "ol" = /^[-*+]$/.test(li[2]) ? "ul" : "ol";
      while (listStack.length < depth) {
        out.push(`<${kind}>`);
        listStack.push(kind);
      }
      closeLists(depth);
      const task = li[3].match(/^\[([ xX])\]\s+(.*)$/);
      if (task) {
        const checked = task[1].toLowerCase() === "x" ? " checked" : "";
        out.push(`<li class='task'><input type='checkbox' disabled${checked}/> ${inline(task[2], ctx)}</li>`);
      } else {
        out.push(`<li>${inline(li[3], ctx)}</li>`);
      }
      continue;
    }
    if (/^\s*$/.test(raw)) {
      resetBlocks();
      continue;
    }
    closeLists(0);
    closeQuote();
    para.push(raw);
  }
  resetBlocks();
  return out.join("\n");
}
