/**
 * A small allow-list sanitiser for the rich text people write.
 *
 * The blog editor stores HTML, and anyone with an account can write a
 * post — so what comes back from the browser is never trusted. Anything
 * not on this list is dropped: tags, attributes, and any URL that isn't
 * plainly http(s) or a path on this site.
 */

/** Tag -> the attributes it may keep. Everything else goes. */
const ALLOWED: Record<string, string[]> = {
  p: [], br: [], b: [], strong: [], i: [], em: [], u: [], s: [],
  h2: [], h3: [], blockquote: [], ul: [], ol: [], li: [],
  code: [], pre: [], figure: [], figcaption: [], div: [], span: [],
  a: ["href"],
  img: ["src", "alt"],
};

/** Only links that stay on the web or on this site. */
function safeUrl(value: string): boolean {
  return /^(https?:\/\/|\/)/i.test(value.trim());
}

export function sanitizeHtml(input: string): string {
  if (!input) return "";

  // Elements whose contents are dangerous too, not just their tags.
  let out = input.replace(
    /<\s*(script|style|iframe|object|embed|svg|math|template)[\s\S]*?<\s*\/\s*\1\s*>/gi,
    ""
  );
  // Unclosed versions of the same, and comments.
  out = out.replace(/<\s*(script|style|iframe|object|embed|svg|math|template)\b[^>]*>/gi, "");
  out = out.replace(/<!--[\s\S]*?-->/g, "");

  out = out.replace(
    /<(\/?)([a-zA-Z0-9-]+)((?:\s[^<>]*)?)\/?>/g,
    (_m, slash, rawName, rawAttrs) => {
      const name = String(rawName).toLowerCase();
      if (!(name in ALLOWED)) return "";
      if (slash) return `</${name}>`;

      const allowed = ALLOWED[name];
      let attrs = "";

      const re = /([a-zA-Z-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
      let match: RegExpExecArray | null;
      while ((match = re.exec(String(rawAttrs ?? "")))) {
        const key = match[1].toLowerCase();
        const value = (match[2] ?? match[3] ?? "").trim();
        if (!allowed.includes(key)) continue;
        if ((key === "href" || key === "src") && !safeUrl(value)) continue;
        attrs += ` ${key}="${value.replace(/"/g, "&quot;")}"`;
      }

      // Void elements keep their own closing form.
      if (name === "br" || name === "img") return `<${name}${attrs} />`;
      return `<${name}${attrs}>`;
    }
  );

  return out;
}

/** The same text with no markup at all - for card excerpts and search. */
export function stripTags(input: string): string {
  return sanitizeHtml(input)
    .replace(/<[^>]*>/g, " ")
    .replace(/&nbsp;/g, " ")
    .replace(/\s+/g, " ")
    .trim();
}

