import { parseHTML } from "linkedom";
import { createLogger } from "../logger.js";
import { extractJsonLd } from "./jsonld.js";
import { extractPaletteFromBuffer, MAX_IMAGE_BYTES } from "./brand-palette.js";
import { guardUrl } from "../watch/ssrf.js";
const log = createLogger("extract");
const PALETTE_FETCH_TIMEOUT_MS = 1500;
const PALETTE_MIN_COLORS = 2;
const COLOR_VAR_PATTERNS = [
  /--brand-primary(?:-\d+)?\b/i,
  /--color-primary(?:-\d+)?\b/i,
  /--primary(?:-\d+)?\b/i,
  /--brand(?:-\d+)?\b/i,
  /--accent(?:-\d+)?\b/i,
  /--color-accent(?:-\d+)?\b/i,
  /--theme-primary(?:-\d+)?\b/i
];
const HEX_RE = /#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g;
const RGB_RE = /rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)/g;
const HSL_RE = /hsla?\(\s*\d{1,3}(?:\.\d+)?\s*,?\s*\d{1,3}(?:\.\d+)?%\s*,?\s*\d{1,3}(?:\.\d+)?%(?:\s*[,/]\s*(?:0|1|0?\.\d+))?\s*\)/g;
const SOCIAL_PATTERNS = [
  { key: "twitter", re: /^https?:\/\/(?:www\.)?(?:twitter\.com|x\.com)\/([^/?#]+)/i },
  { key: "github", re: /^https?:\/\/(?:www\.)?github\.com\/([^/?#]+)/i },
  { key: "linkedin", re: /^https?:\/\/(?:www\.)?linkedin\.com\/(?:company|in|school)\/([^/?#]+)/i },
  { key: "youtube", re: /^https?:\/\/(?:www\.)?youtube\.com\/(?:c\/|channel\/|@)?([^/?#]+)/i },
  { key: "discord", re: /^https?:\/\/(?:www\.)?discord\.(?:com|gg)\/(?:invite\/)?([^/?#]+)/i },
  { key: "facebook", re: /^https?:\/\/(?:www\.)?facebook\.com\/([^/?#]+)/i },
  { key: "instagram", re: /^https?:\/\/(?:www\.)?instagram\.com\/([^/?#]+)/i },
  { key: "mastodon", re: /^https?:\/\/(?:[^./]+\.)*(?:mastodon|hachyderm|infosec|fosstodon)\.[^/]+\/@([^/?#]+)/i },
  { key: "tiktok", re: /^https?:\/\/(?:www\.)?tiktok\.com\/@([^/?#]+)/i }
];
const GENERIC_FONTS = /* @__PURE__ */ new Set([
  "serif",
  "sans-serif",
  "monospace",
  "cursive",
  "fantasy",
  "system-ui",
  "ui-sans-serif",
  "ui-serif",
  "ui-monospace",
  "ui-rounded",
  "inherit",
  "initial",
  "unset",
  "revert",
  "revert-layer",
  "apple-system",
  "-apple-system",
  "blinkmacsystemfont",
  "segoe ui",
  "roboto",
  "helvetica neue",
  "helvetica",
  "arial",
  "sans",
  "sans serif"
]);
const FONT_VAR_PATTERNS = {
  headings: [
    /--font-(?:heading|headings|display|brand|title|serif)\b/i
  ],
  body: [
    /--font-(?:body|sans|base|primary|text)\b/i
  ]
};
function isSvgPath(url) {
  try {
    return new URL(url).pathname.toLowerCase().endsWith(".svg");
  } catch {
    return false;
  }
}
function safeAbsoluteUrl(value, baseUrl) {
  if (!value) return void 0;
  const trimmed = value.trim();
  if (!trimmed) return void 0;
  if (/^(javascript|mailto|tel|data|file|vbscript|blob):/i.test(trimmed)) return void 0;
  try {
    if (baseUrl) return new URL(trimmed, baseUrl).href;
    return new URL(trimmed).href;
  } catch {
    return void 0;
  }
}
function getMeta(doc, nameOrProperty) {
  const el = doc.querySelector(`meta[name="${nameOrProperty}"]`) ?? doc.querySelector(`meta[property="${nameOrProperty}"]`);
  const content = el?.getAttribute("content") ?? void 0;
  return content && content.trim().length > 0 ? content.trim() : void 0;
}
function firstNonEmpty(...values) {
  for (const v of values) {
    if (v && v.trim().length > 0) return v.trim();
  }
  return void 0;
}
function findJsonLdNode(blocks, types) {
  const wantedLower = new Set(types.map((t) => t.toLowerCase()));
  function visit(node) {
    if (!node || typeof node !== "object") return void 0;
    if (Array.isArray(node)) {
      for (const item of node) {
        const hit = visit(item);
        if (hit) return hit;
      }
      return void 0;
    }
    const obj = node;
    const type = obj["@type"];
    if (typeof type === "string" && wantedLower.has(type.toLowerCase())) return obj;
    if (Array.isArray(type)) {
      for (const t of type) {
        if (typeof t === "string" && wantedLower.has(t.toLowerCase())) return obj;
      }
    }
    for (const v of Object.values(obj)) {
      const hit = visit(v);
      if (hit) return hit;
    }
    return void 0;
  }
  for (const block of blocks) {
    const hit = visit(block);
    if (hit) return hit;
  }
  return void 0;
}
function jsonLdImageUrl(value) {
  if (!value) return void 0;
  if (typeof value === "string") return value;
  if (Array.isArray(value)) {
    for (const v of value) {
      const url = jsonLdImageUrl(v);
      if (url) return url;
    }
    return void 0;
  }
  if (typeof value === "object") {
    const obj = value;
    const url = obj.url ?? obj["@id"] ?? obj.contentUrl;
    if (typeof url === "string") return url;
  }
  return void 0;
}
function extractFromJsonLd(blocks) {
  const out = { socialLinks: [], source: "unknown" };
  const node = findJsonLdNode(blocks, [
    "Organization",
    "Corporation",
    "LocalBusiness",
    "Brand",
    "WebSite"
  ]);
  if (!node) return out;
  out.source = "json-ld";
  if (typeof node.name === "string") out.name = node.name.trim();
  if (typeof node.description === "string") out.description = node.description.trim();
  if (typeof node.slogan === "string" && !out.description) {
    out.description = node.slogan.trim();
  }
  const logo = jsonLdImageUrl(node.logo) ?? jsonLdImageUrl(node.image);
  if (logo) out.logo = logo;
  const sameAs = node.sameAs;
  if (typeof sameAs === "string") out.socialLinks.push(sameAs);
  else if (Array.isArray(sameAs)) {
    for (const s of sameAs) {
      if (typeof s === "string") out.socialLinks.push(s);
    }
  }
  return out;
}
function extractFromOg(doc) {
  const out = {};
  const siteName = getMeta(doc, "og:site_name");
  if (siteName) out.name = siteName;
  const description = firstNonEmpty(
    getMeta(doc, "og:description"),
    getMeta(doc, "twitter:description"),
    getMeta(doc, "description")
  );
  if (description) out.description = description;
  const ogTitle = getMeta(doc, "og:title");
  if (ogTitle && ogTitle !== siteName) out.tagline = ogTitle;
  const ogImage = firstNonEmpty(
    getMeta(doc, "og:image"),
    getMeta(doc, "og:image:secure_url"),
    getMeta(doc, "twitter:image"),
    getMeta(doc, "twitter:image:src")
  );
  if (ogImage) out.ogImage = ogImage;
  const ogLogo = firstNonEmpty(
    getMeta(doc, "og:logo"),
    getMeta(doc, "twitter:logo")
  );
  if (ogLogo) out.ogLogo = ogLogo;
  const twitterSite = firstNonEmpty(
    getMeta(doc, "twitter:site"),
    getMeta(doc, "twitter:creator")
  );
  if (twitterSite) {
    out.twitterHandle = twitterSite.replace(/^@/, "");
  }
  return out;
}
function extractFavicon(doc, baseUrl) {
  const candidates = Array.from(
    doc.querySelectorAll(
      'link[rel~="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"], link[rel="apple-touch-icon-precomposed"], link[rel="mask-icon"]'
    )
  );
  let best;
  for (const el of candidates) {
    const href = el.getAttribute("href");
    const resolved = safeAbsoluteUrl(href, baseUrl);
    if (!resolved) continue;
    const type = (el.getAttribute("type") ?? "").toLowerCase();
    const sizes = (el.getAttribute("sizes") ?? "").toLowerCase();
    const rel = (el.getAttribute("rel") ?? "").toLowerCase();
    let weight = 1;
    if (type.includes("svg") || resolved.endsWith(".svg")) weight = 1e3;
    else if (rel.includes("apple-touch-icon")) weight = 180;
    else if (sizes === "any") weight = 256;
    else {
      const m = sizes.match(/(\d+)x\1/) ?? sizes.match(/(\d+)x(\d+)/);
      if (m) {
        const n = parseInt(m[1], 10);
        if (Number.isFinite(n)) weight = n;
      }
    }
    if (!best || weight > best.weight) {
      best = { url: resolved, weight };
    }
  }
  if (best) return { url: best.url, source: "link[rel=icon]" };
  if (baseUrl) {
    try {
      const root = new URL("/favicon.ico", baseUrl).href;
      return { url: root, source: "link[rel=icon]" };
    } catch {
      return void 0;
    }
  }
  return void 0;
}
function extractHeuristicLogo(doc, baseUrl) {
  const out = {};
  const selectors = [
    'header a[href="/"] img',
    'header [class*="logo" i] img',
    'header img[class*="logo" i]',
    '[class*="navbar" i] a[href="/"] img',
    '[class*="navbar" i] [class*="logo" i] img',
    '[class*="header" i] [class*="logo" i] img',
    'a[aria-label*="home" i] img',
    'a[href="/"] img[alt]',
    '[class*="logo" i] img',
    'img[class*="logo" i]',
    'img[alt*="logo" i]'
  ];
  for (const sel of selectors) {
    try {
      const img = doc.querySelector(sel);
      if (!img) continue;
      const src = img.getAttribute("src") ?? img.getAttribute("data-src") ?? img.getAttribute("data-lazy-src") ?? img.getAttribute("srcset")?.split(",")[0]?.trim().split(/\s+/)[0];
      const resolved = safeAbsoluteUrl(src, baseUrl);
      if (!resolved) continue;
      out.url = resolved;
      const alt = img.getAttribute("alt");
      if (alt) out.alt = alt.trim();
      return out;
    } catch {
    }
  }
  const inlineLogo = doc.querySelector('header [class*="logo" i] svg, [class*="navbar" i] [class*="logo" i] svg');
  if (inlineLogo) {
    const ariaLabel = inlineLogo.getAttribute("aria-label") ?? inlineLogo.querySelector("title")?.textContent;
    if (ariaLabel) out.alt = ariaLabel.trim();
  }
  return out;
}
function categorizeSocial(url) {
  for (const { key, re } of SOCIAL_PATTERNS) {
    if (re.test(url)) {
      const m = url.match(re);
      const handle = m ? m[1].replace(/\/$/, "") : void 0;
      switch (key) {
        case "twitter":
          return handle ? { key, canonical: `https://twitter.com/${handle}` } : void 0;
        case "github":
          return handle ? { key, canonical: `https://github.com/${handle}` } : void 0;
        case "linkedin":
          return { key, canonical: url.replace(/\?.*$/, "").replace(/\/$/, "") };
        default:
          return { key, canonical: url.replace(/\?.*$/, "").replace(/\/$/, "") };
      }
    }
  }
  return void 0;
}
function extractSocialLinks(doc, baseUrl, twitterHandle, extraUrls = []) {
  const out = {};
  const seen = /* @__PURE__ */ new Set();
  function ingest(url) {
    if (!url) return;
    const absolute = safeAbsoluteUrl(url, baseUrl);
    if (!absolute) return;
    if (seen.has(absolute)) return;
    seen.add(absolute);
    const cat = categorizeSocial(absolute);
    if (!cat) return;
    if (out[cat.key]) return;
    out[cat.key] = cat.canonical;
  }
  const anchors = doc.querySelectorAll("a[href]");
  for (const a of anchors) {
    ingest(a.getAttribute("href") ?? void 0);
  }
  for (const u of extraUrls) {
    ingest(u);
  }
  if (twitterHandle && !out.twitter) {
    const canon = `https://twitter.com/${twitterHandle.replace(/^@/, "")}`;
    out.twitter = canon;
  }
  return out;
}
function normalizeHex(hex) {
  let h = hex.replace("#", "").toLowerCase();
  if (h.length === 3) {
    h = h.split("").map((c) => c + c).join("");
  } else if (h.length === 4) {
    h = h.slice(0, 3).split("").map((c) => c + c).join("");
  } else if (h.length === 8) {
    h = h.slice(0, 6);
  } else if (h.length !== 6) {
    return void 0;
  }
  if (!/^[0-9a-f]{6}$/.test(h)) return void 0;
  return `#${h}`;
}
function rgbToHex(rgb) {
  const m = rgb.match(/(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})/);
  if (!m) return void 0;
  const [r, g, b] = [m[1], m[2], m[3]].map((s) => parseInt(s, 10));
  if (![r, g, b].every((n) => n >= 0 && n <= 255)) return void 0;
  return "#" + [r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("");
}
function hslToHex(hsl) {
  const m = hsl.match(/(\d{1,3}(?:\.\d+)?)\s*,?\s*(\d{1,3}(?:\.\d+)?)%\s*,?\s*(\d{1,3}(?:\.\d+)?)%/);
  if (!m) return void 0;
  const h = parseFloat(m[1]) % 360;
  const s = parseFloat(m[2]) / 100;
  const l = parseFloat(m[3]) / 100;
  if (![h, s, l].every(Number.isFinite)) return void 0;
  const c = (1 - Math.abs(2 * l - 1)) * s;
  const hh = h / 60;
  const x = c * (1 - Math.abs(hh % 2 - 1));
  let r = 0, g = 0, b = 0;
  if (hh < 1) [r, g, b] = [c, x, 0];
  else if (hh < 2) [r, g, b] = [x, c, 0];
  else if (hh < 3) [r, g, b] = [0, c, x];
  else if (hh < 4) [r, g, b] = [0, x, c];
  else if (hh < 5) [r, g, b] = [x, 0, c];
  else [r, g, b] = [c, 0, x];
  const m2 = l - c / 2;
  const toByte = (n) => Math.max(0, Math.min(255, Math.round((n + m2) * 255))).toString(16).padStart(2, "0");
  return `#${toByte(r)}${toByte(g)}${toByte(b)}`;
}
function parseColorToHex(raw) {
  const trimmed = raw.trim();
  if (trimmed.startsWith("#")) return normalizeHex(trimmed);
  if (trimmed.startsWith("rgb")) return rgbToHex(trimmed);
  if (trimmed.startsWith("hsl")) return hslToHex(trimmed);
  return void 0;
}
function collectInlineCss(doc) {
  const blocks = [];
  for (const style of doc.querySelectorAll("style")) {
    const text = style.textContent;
    if (text) blocks.push(text);
  }
  for (const el of doc.querySelectorAll("[style]")) {
    const text = el.getAttribute("style");
    if (text) blocks.push(text);
  }
  return blocks.join("\n");
}
function collectStyleBlockCss(doc) {
  const blocks = [];
  for (const style of doc.querySelectorAll("style")) {
    const text = style.textContent;
    if (text) blocks.push(text);
  }
  return blocks.join("\n");
}
function extractColorsFromCss(css) {
  if (!css) return { colors: [], source: "unknown" };
  const found = [];
  const seen = /* @__PURE__ */ new Set();
  const declRe = /(--[a-zA-Z0-9_-]+)\s*:\s*([^;}]+)[;}]/g;
  let match;
  while ((match = declRe.exec(css)) !== null) {
    const name = match[1];
    const value = match[2].trim();
    const nameMatches = COLOR_VAR_PATTERNS.some((re) => re.test(name));
    if (!nameMatches) continue;
    const literal = parseColorToHex(value);
    if (literal && !seen.has(literal)) {
      seen.add(literal);
      found.push(literal);
      continue;
    }
    const fallback = value.match(/var\([^,)]+,\s*([^)]+)\)/);
    if (fallback) {
      const hex = parseColorToHex(fallback[1]);
      if (hex && !seen.has(hex)) {
        seen.add(hex);
        found.push(hex);
      }
      continue;
    }
    const tripleHsl = value.match(/^(\d{1,3}(?:\.\d+)?)\s+(\d{1,3}(?:\.\d+)?)%\s+(\d{1,3}(?:\.\d+)?)%$/);
    if (tripleHsl) {
      const synthetic = `hsl(${tripleHsl[1]}, ${tripleHsl[2]}%, ${tripleHsl[3]}%)`;
      const hex = hslToHex(synthetic);
      if (hex && !seen.has(hex)) {
        seen.add(hex);
        found.push(hex);
      }
    }
  }
  return found.length > 0 ? { colors: found, source: "css-vars" } : { colors: [], source: "unknown" };
}
function splitFontFamilyList(value) {
  return value.split(",").map(
    (s) => s.trim().replace(/^['"]|['"]$/g, "").replace(/!important$/, "").trim()
  ).filter((s) => s.length > 0 && !GENERIC_FONTS.has(s.toLowerCase()));
}
function fontsFromStyleAttr(styleAttr) {
  if (!styleAttr) return [];
  const m = styleAttr.match(/font-family\s*:\s*([^;]+)/i);
  if (!m) return [];
  return splitFontFamilyList(m[1]);
}
function fontsFromCssVars(cssText) {
  const headings = /* @__PURE__ */ new Set();
  const body = /* @__PURE__ */ new Set();
  if (!cssText) return { headings: [], body: [] };
  const declRe = /(--[a-zA-Z0-9_-]+)\s*:\s*([^;}]+)[;}]/g;
  let m;
  while ((m = declRe.exec(cssText)) !== null) {
    const name = m[1];
    const value = m[2].trim();
    if (FONT_VAR_PATTERNS.headings.some((re) => re.test(name))) {
      for (const f of splitFontFamilyList(value)) headings.add(f);
    } else if (FONT_VAR_PATTERNS.body.some((re) => re.test(name))) {
      for (const f of splitFontFamilyList(value)) body.add(f);
    }
  }
  return { headings: Array.from(headings), body: Array.from(body) };
}
function fontsFromCssRules(cssText) {
  const headings = /* @__PURE__ */ new Set();
  const body = /* @__PURE__ */ new Set();
  if (!cssText) return { headings: [], body: [] };
  const ruleRe = /([^{}]+)\{([^{}]+)\}/g;
  let r;
  while ((r = ruleRe.exec(cssText)) !== null) {
    const selectorList = r[1].trim().toLowerCase();
    const decls = r[2];
    const ff = decls.match(/font-family\s*:\s*([^;}]+)/i);
    if (!ff) continue;
    const families = splitFontFamilyList(ff[1]);
    if (families.length === 0) continue;
    const selectors = selectorList.split(",").map((s) => s.trim());
    for (const selector of selectors) {
      const isHeading = /\b(h[1-6]|heading|title|display)\b/.test(selector);
      const isBody = /\b(body|html|p|main|article|prose)\b/.test(selector) || selector === ":root" || selector === "*";
      if (isHeading) {
        for (const f of families) headings.add(f);
      } else if (isBody) {
        for (const f of families) body.add(f);
      }
    }
  }
  return { headings: Array.from(headings), body: Array.from(body) };
}
function fontsFromInlineStyleAttrs(doc) {
  const headings = /* @__PURE__ */ new Set();
  const body = /* @__PURE__ */ new Set();
  for (const heading of doc.querySelectorAll("h1[style], h2[style]")) {
    for (const f of fontsFromStyleAttr(heading.getAttribute("style"))) {
      headings.add(f);
    }
  }
  const bodyEl = doc.querySelector("body[style]");
  if (bodyEl) {
    for (const f of fontsFromStyleAttr(bodyEl.getAttribute("style"))) {
      body.add(f);
    }
  }
  return { headings: Array.from(headings), body: Array.from(body) };
}
function fontsFromGoogleFontsLinks(doc) {
  const families = [];
  const seen = /* @__PURE__ */ new Set();
  for (const link of doc.querySelectorAll("link[href]")) {
    const href = link.getAttribute("href") ?? "";
    if (!/(^|\/\/)fonts\.googleapis\.com\/(css|css2)\?/.test(href)) continue;
    let url;
    try {
      url = new URL(href, "https://fonts.googleapis.com/");
    } catch {
      continue;
    }
    for (const raw of url.searchParams.getAll("family")) {
      const namePart = raw.split(":")[0];
      const name = namePart.replace(/\+/g, " ").trim();
      if (!name) continue;
      if (GENERIC_FONTS.has(name.toLowerCase())) continue;
      if (seen.has(name)) continue;
      seen.add(name);
      families.push(name);
    }
  }
  return families;
}
function extractFonts(doc, styleBlockCss) {
  const cssVars = fontsFromCssVars(styleBlockCss);
  if (cssVars.headings.length > 0 || cssVars.body.length > 0) {
    return {
      headings: cssVars.headings.slice(0, 5),
      body: cssVars.body.slice(0, 5),
      source: "css-vars"
    };
  }
  const cssRules = fontsFromCssRules(styleBlockCss);
  if (cssRules.headings.length > 0 || cssRules.body.length > 0) {
    return {
      headings: cssRules.headings.slice(0, 5),
      body: cssRules.body.slice(0, 5),
      source: "css-rule"
    };
  }
  const inline = fontsFromInlineStyleAttrs(doc);
  if (inline.headings.length > 0 || inline.body.length > 0) {
    return {
      headings: inline.headings.slice(0, 5),
      body: inline.body.slice(0, 5),
      source: "inline-style"
    };
  }
  const googleFonts = fontsFromGoogleFontsLinks(doc);
  if (googleFonts.length > 0) {
    const headings = [];
    const body = [];
    if (googleFonts.length === 1) {
      headings.push(googleFonts[0]);
      body.push(googleFonts[0]);
    } else {
      headings.push(googleFonts[0]);
      body.push(googleFonts[1]);
      for (let i = 2; i < googleFonts.length; i++) headings.push(googleFonts[i]);
    }
    return {
      headings: headings.slice(0, 5),
      body: body.slice(0, 5),
      source: "google-fonts-link"
    };
  }
  return { headings: [], body: [], source: "unknown" };
}
function extractFontsFromCss(css) {
  const cssVars = fontsFromCssVars(css);
  if (cssVars.headings.length > 0 || cssVars.body.length > 0) {
    return {
      headings: cssVars.headings.slice(0, 5),
      body: cssVars.body.slice(0, 5),
      source: "css-vars"
    };
  }
  const cssRules = fontsFromCssRules(css);
  if (cssRules.headings.length > 0 || cssRules.body.length > 0) {
    return {
      headings: cssRules.headings.slice(0, 5),
      body: cssRules.body.slice(0, 5),
      source: "css-rule"
    };
  }
  return { headings: [], body: [], source: "unknown" };
}
const MAX_REDIRECT_HOPS = 3;
const defaultImageFetcher = async (url, opts) => {
  const timeoutMs = opts?.timeoutMs ?? PALETTE_FETCH_TIMEOUT_MS;
  const signal = AbortSignal.timeout(timeoutMs);
  let currentUrl = url;
  for (let hop = 0; hop <= MAX_REDIRECT_HOPS; hop++) {
    const guard = guardUrl(currentUrl, "image_url");
    if (!guard.ok) {
      log.debug("palette fetch: SSRF guard rejected URL", {
        url: currentUrl,
        reason: guard.reason,
        hop
      });
      return null;
    }
    let response;
    try {
      response = await fetch(currentUrl, {
        headers: {
          Accept: "image/png,image/jpeg,image/webp,image/avif,image/*;q=0.8",
          "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
        },
        signal,
        redirect: "manual"
      });
    } catch (err) {
      log.debug("palette fetch failed", { url: currentUrl, error: String(err) });
      return null;
    }
    if (response.status >= 300 && response.status < 400) {
      try {
        await response.body?.cancel();
      } catch {
      }
      const location = response.headers.get("location");
      if (!location) {
        log.debug("palette fetch: 3xx without Location header", {
          url: currentUrl,
          status: response.status
        });
        return null;
      }
      let nextUrl;
      try {
        nextUrl = new URL(location, currentUrl).href;
      } catch {
        log.debug("palette fetch: unresolvable Location header", {
          url: currentUrl,
          location
        });
        return null;
      }
      if (hop === MAX_REDIRECT_HOPS) {
        log.debug("palette fetch: too many redirects", { url, hops: hop + 1 });
        return null;
      }
      currentUrl = nextUrl;
      continue;
    }
    if (!response.ok) {
      log.debug("palette fetch: non-2xx", { url: currentUrl, status: response.status });
      return null;
    }
    const contentLength = response.headers.get("content-length");
    if (contentLength && parseInt(contentLength, 10) > MAX_IMAGE_BYTES) {
      log.debug("palette fetch: content-length over cap", { url: currentUrl, contentLength });
      try {
        await response.body?.cancel();
      } catch {
      }
      return null;
    }
    const ab = await response.arrayBuffer();
    if (ab.byteLength > MAX_IMAGE_BYTES) {
      log.debug("palette fetch: body over cap", { url: currentUrl, bytes: ab.byteLength });
      return null;
    }
    return {
      buffer: Buffer.from(ab),
      contentType: response.headers.get("content-type") ?? "application/octet-stream"
    };
  }
  return null;
};
async function extractBrandAsync(html, options = {}) {
  const out = extractBrand(html, options);
  const fetcher = options.imageFetcher === void 0 ? defaultImageFetcher : options.imageFetcher;
  if (fetcher === null) return out;
  const existing = out.primary_colors ?? [];
  if (existing.length >= PALETTE_MIN_COLORS) return out;
  const candidate = out.logo_url ?? out.og_image_url;
  if (!candidate) {
    return out;
  }
  if (isSvgPath(candidate)) {
    log.debug("palette: candidate is SVG, skipping image fetch", { url: candidate });
    return out;
  }
  const fetched = await fetcher(candidate, { timeoutMs: PALETTE_FETCH_TIMEOUT_MS });
  if (!fetched) {
    log.debug("palette: fetch returned null", { url: candidate });
    return out;
  }
  const palette = await extractPaletteFromBuffer(fetched.buffer, fetched.contentType);
  if (!palette || palette.colors.length < PALETTE_MIN_COLORS) {
    log.debug("palette: quantization produced insufficient colors", {
      url: candidate,
      colorCount: palette?.colors.length ?? 0
    });
    return out;
  }
  out.primary_colors = palette.colors;
  if (out.provenance) {
    out.provenance.colors = "palette-extraction";
  } else {
    out.provenance = { colors: "palette-extraction" };
  }
  log.debug("palette extracted", { url: candidate, colors: palette.colors });
  return out;
}
function extractBrand(html, options = {}) {
  const { document: doc } = parseHTML(html);
  const baseUrl = options.baseUrl;
  const jsonldBlocks = extractJsonLd(html);
  const jsonld = extractFromJsonLd(jsonldBlocks);
  const og = extractFromOg(doc);
  const favicon = extractFavicon(doc, baseUrl);
  const heuristicLogo = extractHeuristicLogo(doc, baseUrl);
  let logoUrl;
  let logoProvenance = "unknown";
  const jsonldLogo = safeAbsoluteUrl(jsonld.logo, baseUrl);
  const ogLogoUrl = safeAbsoluteUrl(og.ogLogo, baseUrl);
  const heuristicLogoUrl = heuristicLogo.url;
  if (jsonldLogo) {
    logoUrl = jsonldLogo;
    logoProvenance = "json-ld";
  } else if (ogLogoUrl) {
    logoUrl = ogLogoUrl;
    logoProvenance = "og:logo";
  } else if (heuristicLogoUrl) {
    logoUrl = heuristicLogoUrl;
    logoProvenance = "heuristic";
  }
  const titleRaw = doc.querySelector("title")?.textContent?.trim();
  const title = titleRaw && titleRaw.length > 0 ? titleRaw : void 0;
  const name = firstNonEmpty(jsonld.name, og.name, heuristicLogo.alt);
  let tagline = og.tagline;
  if (!tagline && title && name && title.includes(name)) {
    const parts = title.split(/\s+[|·—–-]\s+/);
    const tail = parts.slice(1).join(" \u2014 ").trim();
    if (tail && tail.toLowerCase() !== name.toLowerCase()) tagline = tail;
  }
  const description = firstNonEmpty(jsonld.description, og.description);
  const socialLinks = extractSocialLinks(doc, baseUrl, og.twitterHandle, jsonld.socialLinks);
  const css = collectInlineCss(doc);
  const colorResult = extractColorsFromCss(css);
  const styleBlockCss = collectStyleBlockCss(doc);
  const fontResult = extractFonts(doc, styleBlockCss);
  const out = {
    provenance: {
      logo: logoProvenance,
      colors: colorResult.source,
      fonts: fontResult.source
    }
  };
  if (name) out.name = name;
  if (tagline) out.tagline = tagline;
  if (description) out.description = description;
  if (logoUrl) out.logo_url = logoUrl;
  if (favicon) out.favicon_url = favicon.url;
  const ogImageResolved = safeAbsoluteUrl(og.ogImage, baseUrl);
  if (ogImageResolved) out.og_image_url = ogImageResolved;
  if (colorResult.colors.length > 0) out.primary_colors = colorResult.colors;
  if (fontResult.headings.length > 0 || fontResult.body.length > 0) {
    out.fonts = {};
    if (fontResult.headings.length > 0) out.fonts.headings = fontResult.headings;
    if (fontResult.body.length > 0) out.fonts.body = fontResult.body;
  }
  if (Object.keys(socialLinks).length > 0) {
    out.social_links = socialLinks;
  }
  log.debug("brand extracted", {
    baseUrl,
    hasLogo: Boolean(logoUrl),
    hasFavicon: Boolean(favicon),
    socials: Object.keys(socialLinks),
    colorCount: colorResult.colors.length,
    fontHeadings: fontResult.headings.length,
    fontBody: fontResult.body.length
  });
  return out;
}
const __internal = {
  parseColorToHex,
  rgbToHex,
  hslToHex,
  normalizeHex,
  categorizeSocial,
  extractColorsFromCss,
  extractFontsFromCss,
  splitFontFamilyList
};
function _internalScanColors(text) {
  const hits = /* @__PURE__ */ new Set();
  for (const re of [HEX_RE, RGB_RE, HSL_RE]) {
    const matches = text.match(re) ?? [];
    for (const m of matches) {
      const hex = parseColorToHex(m);
      if (hex) hits.add(hex);
    }
  }
  return Array.from(hits);
}
export {
  __internal,
  _internalScanColors,
  defaultImageFetcher,
  extractBrand,
  extractBrandAsync
};
//# sourceMappingURL=brand.js.map