const BLOCK_TAGS = [
  "script", "style", "noscript", "svg", "canvas", "iframe", "template",
  "nav", "footer", "aside", "form", "button"
];

function extractReadableHtml(html, options = {}) {
  const maximum = Math.max(500, Number(options.maxChars) || 12000);
  const source = String(html || "");
  const title = firstContent(source, /<title\b[^>]*>([\s\S]*?)<\/title>/i, 300);
  const description = metaContent(source, ["description", "og:description"], 600);
  const canonicalUrl = linkHref(source, "canonical");
  const publishedAt = firstMeta(source, [
    "article:published_time", "date", "datepublished", "publishdate", "pubdate"
  ]);
  const updatedAt = firstMeta(source, [
    "article:modified_time", "datemodified", "last-modified", "lastmodified"
  ]);
  const headings = [...source.matchAll(/<h[1-3]\b[^>]*>([\s\S]*?)<\/h[1-3]>/gi)]
    .map((match) => sanitizeText(match[1], 300))
    .filter(Boolean)
    .slice(0, 20);
  const preferred = firstRaw(source, /<(main|article)\b[^>]*>([\s\S]*?)<\/\1>/i, 2) || source;
  const stripped = stripNonContent(preferred);
  const extractedText = sanitizeText(
    stripped
      .replace(/<(br|hr)\b[^>]*\/?>/gi, "\n")
      .replace(/<\/(p|div|section|article|main|li|h[1-6]|tr)>/gi, "\n")
      .replace(/<[^>]+>/g, " "),
    maximum
  );
  return {
    title,
    description,
    headings,
    canonical_url: canonicalUrl,
    published_at: normalizeDate(publishedAt),
    updated_at: normalizeDate(updatedAt),
    extracted_text: extractedText,
    extraction_status: extractedText ? "ok" : "empty"
  };
}

function extractPlainText(text, options = {}) {
  const maximum = Math.max(500, Number(options.maxChars) || 12000);
  return {
    title: "",
    description: "",
    headings: [],
    canonical_url: null,
    published_at: null,
    updated_at: null,
    extracted_text: sanitizeText(text, maximum),
    extraction_status: String(text || "").trim() ? "ok" : "empty"
  };
}

function stripNonContent(value) {
  let output = String(value || "");
  for (const tag of BLOCK_TAGS) {
    output = output.replace(new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, "gi"), " ");
  }
  output = output.replace(
    /<([a-z0-9]+)\b[^>]*(?:hidden|aria-hidden\s*=\s*["']?true|style\s*=\s*["'][^"']*display\s*:\s*none)[^>]*>[\s\S]*?<\/\1>/gi,
    " "
  );
  return output;
}

function sanitizeText(value, maximum = 12000) {
  return decodeEntities(String(value || "").replace(/<[^>]+>/g, " "))
    .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ")
    .replace(/[ \t]+/g, " ")
    .replace(/\s*\n\s*/g, "\n")
    .replace(/\n{3,}/g, "\n\n")
    .trim()
    .slice(0, maximum);
}

function decodeEntities(value) {
  return value
    .replace(/&#(\d+);/g, (_match, code) => safeCharacter(Number(code)))
    .replace(/&#x([0-9a-f]+);/gi, (_match, code) => safeCharacter(Number.parseInt(code, 16)))
    .replaceAll("&amp;", "&")
    .replaceAll("&lt;", "<")
    .replaceAll("&gt;", ">")
    .replaceAll("&quot;", "\"")
    .replaceAll("&#39;", "'")
    .replaceAll("&apos;", "'")
    .replaceAll("&nbsp;", " ");
}

function safeCharacter(code) {
  return Number.isInteger(code) && code > 0 && code <= 0x10ffff
    ? String.fromCodePoint(code)
    : "";
}

function firstContent(source, expression, maximum) {
  const match = source.match(expression);
  return sanitizeText(match?.[1], maximum);
}

function firstRaw(source, expression, group) {
  return source.match(expression)?.[group] || "";
}

function metaContent(source, names, maximum) {
  return sanitizeText(firstMeta(source, names), maximum);
}

function firstMeta(source, names) {
  for (const name of names) {
    const escaped = escapeExpression(name);
    const patterns = [
      new RegExp(`<meta\\b[^>]*(?:name|property)\\s*=\\s*["']${escaped}["'][^>]*content\\s*=\\s*["']([^"']*)["'][^>]*>`, "i"),
      new RegExp(`<meta\\b[^>]*content\\s*=\\s*["']([^"']*)["'][^>]*(?:name|property)\\s*=\\s*["']${escaped}["'][^>]*>`, "i")
    ];
    for (const pattern of patterns) {
      const match = source.match(pattern);
      if (match?.[1]) return decodeEntities(match[1]).trim();
    }
  }
  return "";
}

function linkHref(source, rel) {
  const escaped = escapeExpression(rel);
  const patterns = [
    new RegExp(`<link\\b[^>]*rel\\s*=\\s*["'][^"']*${escaped}[^"']*["'][^>]*href\\s*=\\s*["']([^"']+)["'][^>]*>`, "i"),
    new RegExp(`<link\\b[^>]*href\\s*=\\s*["']([^"']+)["'][^>]*rel\\s*=\\s*["'][^"']*${escaped}[^"']*["'][^>]*>`, "i")
  ];
  for (const pattern of patterns) {
    const match = source.match(pattern);
    if (match?.[1]) return decodeEntities(match[1]).trim();
  }
  return null;
}

function normalizeDate(value) {
  if (!value) return null;
  const date = new Date(value);
  return Number.isNaN(date.getTime()) ? null : date.toISOString();
}

function escapeExpression(value) {
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

module.exports = {
  decodeEntities,
  extractPlainText,
  extractReadableHtml,
  sanitizeText,
  stripNonContent
};
