import "server-only";

import sanitizeHtml from "sanitize-html";

import { trimTrailingEmptyParagraphs } from "@/lib/html";

const pxValue = [/^\d{1,5}px$/];

/**
 * Allowlist for article HTML. Anything the editor can't produce is removed,
 * including every event handler, `style` outside the few layout properties
 * the editor sets, inline data: images and javascript: links.
 */
const CONTENT_OPTIONS: sanitizeHtml.IOptions = {
  allowedTags: [
    "h1",
    "h2",
    "h3",
    "h4",
    "p",
    "br",
    "strong",
    "em",
    "u",
    "s",
    "code",
    "pre",
    "ul",
    "ol",
    "li",
    "blockquote",
    "hr",
    "a",
    "figure",
    "figcaption",
    "img",
    "table",
    "colgroup",
    "col",
    "thead",
    "tbody",
    "tr",
    "th",
    "td",
  ],
  allowedAttributes: {
    a: ["href", "title", "target", "rel"],
    img: ["src", "alt", "title", "width", "height"],
    figure: [
      {
        name: "data-align",
        multiple: false,
        values: ["left", "center", "right", "full"],
      },
      "style",
    ],
    ol: ["start"],
    code: ["class"],
    table: ["style"],
    col: ["style"],
    th: ["colspan", "rowspan", "colwidth"],
    td: ["colspan", "rowspan", "colwidth"],
  },
  allowedClasses: { code: [/^language-[\w+#-]{1,30}$/] },
  allowedStyles: {
    // Image width as a share of the column, set by the resize control.
    figure: { width: [/^(?:100|[1-9]\d?)(?:\.\d{1,2})?%$/] },
    table: { "min-width": pxValue, width: pxValue },
    col: { "min-width": pxValue, width: pxValue },
  },
  allowedSchemes: ["http", "https", "mailto", "tel"],
  allowedSchemesByTag: { img: ["http", "https"] },
  allowedSchemesAppliedToAttributes: ["href", "src"],
  allowProtocolRelative: false,
  disallowedTagsMode: "discard",
  nonTextTags: [
    "script",
    "style",
    "textarea",
    "option",
    "noscript",
    "iframe",
    "object",
    "embed",
    "svg",
    "math",
    "template",
  ],
  transformTags: {
    b: "strong",
    i: "em",
    strike: "s",
    del: "s",
    a: (tagName, attribs) => {
      const next = { ...attribs };
      if (next.target === "_blank") {
        next.rel = "noopener noreferrer";
      } else {
        delete next.target;
        delete next.rel;
      }
      return { tagName, attribs: next };
    },
  },
  // Images must point at a real URL; an empty paragraph-only figure is dropped.
  exclusiveFilter: (frame) =>
    (frame.tag === "img" && !frame.attribs.src) ||
    (frame.tag === "a" && !frame.attribs.href && !frame.text.trim()),
};

/** Sanitizes article HTML. Run on every save; the public API also trusts nothing else. */
export function sanitizeContentHtml(html: string) {
  return trimTrailingEmptyParagraphs(sanitizeHtml(html, CONTENT_OPTIONS));
}

/** Plain text of an HTML fragment, with block boundaries kept as spaces. */
export function htmlToText(html: string) {
  return sanitizeHtml(html.replace(/<\/(p|h\d|li|td|th|figcaption|blockquote|pre)>/gi, " $&"), {
    allowedTags: [],
    allowedAttributes: {},
  })
    .replace(/&nbsp;/g, " ")
    .replace(/&amp;/g, "&")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/\s+/g, " ")
    .trim();
}

const WORDS_PER_MINUTE = 225;

export function readingMinutes(html: string) {
  const words = htmlToText(html).split(" ").filter(Boolean).length;
  return Math.max(1, Math.round(words / WORDS_PER_MINUTE));
}

/** True when the editor holds nothing but empty paragraphs. */
export function isContentEmpty(html: string) {
  return !htmlToText(html) && !/<(img|table|hr)\b/i.test(html);
}
