// Render-time excerpt sanitizer. Some imported / AI-generated excerpts were
// hard-truncated at a fixed byte count, leaving sentences clipped mid-word
// (e.g. "Since 1976, the co"). Others arrive with raw HTML still in the
// string (e.g. "<p>In today's fast-paced..."), which renders as visible
// "<p>" text on the card. This helper handles both at render time without
// mutating stored data.

const SENTENCE_END = /[.!?…]['")\]]?$/;
const TRAILING_JUNK = /[\s,;:—–\-—_]+$/;

export function cleanExcerpt(raw: string | null | undefined): string {
  if (!raw) return "";
  // Strip any HTML tags first — excerpts are rendered as plain text, so
  // <p>, <br>, <em>, etc. should be removed rather than escaped.
  // Replace tags with a space so adjacent words don't collide when
  // formerly-block-level boundaries are removed.
  let s = raw.replace(/<[^>]*>/g, " ");
  // Decode the common entities that show up in WP/AI-imported excerpts.
  s = s
    .replace(/&nbsp;/g, " ")
    .replace(/&amp;/g, "&")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"')
    .replace(/&#039;/g, "'")
    .replace(/&apos;/g, "'");
  // Collapse multi-whitespace runs left behind by tag removal.
  s = s.replace(/\s+/g, " ").trim();
  if (!s) return "";
  if (SENTENCE_END.test(s)) return s;
  // Drop the trailing partial token.
  const lastSpace = s.lastIndexOf(" ");
  if (lastSpace > 20) s = s.slice(0, lastSpace);
  s = s.replace(TRAILING_JUNK, "");
  return s + "…";
}
