import sharp from "sharp";
import { createLogger } from "../logger.js";
const log = createLogger("extract");
const MAX_IMAGE_BYTES = 2 * 1024 * 1024;
const MAX_INPUT_PIXELS = 5e7;
const RESIZE_LONG_EDGE = 200;
const K = 5;
const MAX_ITERS = 10;
const SATURATION_SPREAD_MIN = 30;
const NEAR_WHITE = 240;
const NEAR_BLACK = 15;
const MAX_OUT_COLORS = 5;
function rgbToHex(r, g, b) {
  const clamp = (n) => Math.max(0, Math.min(255, Math.round(n)));
  const hex = (n) => clamp(n).toString(16).padStart(2, "0");
  return `#${hex(r)}${hex(g)}${hex(b)}`;
}
function hexToRgb(hex) {
  const m = hex.match(/^#([0-9a-f]{6})$/i);
  if (!m) return null;
  const n = parseInt(m[1], 16);
  return {
    r: n >> 16 & 255,
    g: n >> 8 & 255,
    b: n & 255
  };
}
function spread(r, g, b) {
  return Math.max(r, g, b) - Math.min(r, g, b);
}
function isNearWhite(r, g, b) {
  return r >= NEAR_WHITE && g >= NEAR_WHITE && b >= NEAR_WHITE;
}
function isNearBlack(r, g, b) {
  return r <= NEAR_BLACK && g <= NEAR_BLACK && b <= NEAR_BLACK;
}
function isNearGrey(r, g, b) {
  return spread(r, g, b) < SATURATION_SPREAD_MIN;
}
function isRasterMime(mime) {
  const m = mime.toLowerCase();
  if (m.includes("svg")) return false;
  if (m.includes("xml")) return false;
  return m.includes("png") || m.includes("jpeg") || m.includes("jpg") || m.includes("webp") || m.includes("gif") || m.includes("avif") || m.includes("image");
}
function pickSeeds(pixels, k) {
  const totalPx = pixels.length / 3;
  const seeds = [];
  const seen = /* @__PURE__ */ new Set();
  const stride = Math.max(1, Math.floor(totalPx / (k * 4)));
  for (let i = 0; i < totalPx && seeds.length < k; i += stride) {
    const off = i * 3;
    const r = pixels[off];
    const g = pixels[off + 1];
    const b = pixels[off + 2];
    const key = `${r},${g},${b}`;
    if (seen.has(key)) continue;
    seen.add(key);
    seeds.push([r, g, b]);
  }
  while (seeds.length < k) {
    const idx = Math.floor(Math.random() * totalPx) * 3;
    seeds.push([pixels[idx], pixels[idx + 1], pixels[idx + 2]]);
  }
  return seeds;
}
function sqDist(r1, g1, b1, r2, g2, b2) {
  const dr = r1 - r2;
  const dg = g1 - g2;
  const db = b1 - b2;
  return dr * dr + dg * dg + db * db;
}
function kmeans(pixels, k) {
  const totalPx = pixels.length / 3;
  if (totalPx === 0) return [];
  let centroids = pickSeeds(pixels, k).map(([r, g, b]) => ({ r, g, b }));
  const acc = Array.from({ length: k }, () => ({
    rSum: 0,
    gSum: 0,
    bSum: 0,
    count: 0
  }));
  for (let iter = 0; iter < MAX_ITERS; iter++) {
    for (let i = 0; i < k; i++) {
      acc[i].rSum = 0;
      acc[i].gSum = 0;
      acc[i].bSum = 0;
      acc[i].count = 0;
    }
    for (let p = 0; p < totalPx; p++) {
      const off = p * 3;
      const r = pixels[off];
      const g = pixels[off + 1];
      const b = pixels[off + 2];
      let bestIdx = 0;
      let bestDist = sqDist(r, g, b, centroids[0].r, centroids[0].g, centroids[0].b);
      for (let i = 1; i < k; i++) {
        const d = sqDist(r, g, b, centroids[i].r, centroids[i].g, centroids[i].b);
        if (d < bestDist) {
          bestDist = d;
          bestIdx = i;
        }
      }
      acc[bestIdx].rSum += r;
      acc[bestIdx].gSum += g;
      acc[bestIdx].bSum += b;
      acc[bestIdx].count++;
    }
    let moved = false;
    for (let i = 0; i < k; i++) {
      if (acc[i].count === 0) continue;
      const nr = acc[i].rSum / acc[i].count;
      const ng = acc[i].gSum / acc[i].count;
      const nb = acc[i].bSum / acc[i].count;
      if (Math.abs(nr - centroids[i].r) > 1 || Math.abs(ng - centroids[i].g) > 1 || Math.abs(nb - centroids[i].b) > 1) {
        moved = true;
      }
      centroids[i] = { r: nr, g: ng, b: nb };
    }
    if (!moved) break;
  }
  return centroids.map((c, i) => ({
    r: c.r,
    g: c.g,
    b: c.b,
    count: acc[i].count
  }));
}
function rankAndFilterClusters(clusters) {
  const populated = clusters.filter((c) => c.count > 0);
  if (populated.length === 0) return [];
  const enriched = populated.map((c) => {
    const r = Math.round(c.r);
    const g = Math.round(c.g);
    const b = Math.round(c.b);
    const mono = isNearWhite(r, g, b) || isNearBlack(r, g, b) || isNearGrey(r, g, b);
    return { ...c, r, g, b, mono };
  });
  const saturated = enriched.filter((c) => !c.mono).sort((a, b) => b.count - a.count);
  const monochrome = enriched.filter((c) => c.mono).sort((a, b) => b.count - a.count);
  const ordered = [];
  for (const c of saturated) {
    ordered.push(c);
    if (ordered.length >= MAX_OUT_COLORS) break;
  }
  if (ordered.length < 2) {
    for (const c of monochrome) {
      ordered.push(c);
      if (ordered.length >= MAX_OUT_COLORS) break;
    }
  }
  const out = [];
  const seen = [];
  const TOL = 8;
  for (const c of ordered) {
    const tooClose = seen.some(
      (s) => Math.abs(s.r - c.r) <= TOL && Math.abs(s.g - c.g) <= TOL && Math.abs(s.b - c.b) <= TOL
    );
    if (tooClose) continue;
    seen.push({ r: c.r, g: c.g, b: c.b });
    out.push(rgbToHex(c.r, c.g, c.b));
    if (out.length >= MAX_OUT_COLORS) break;
  }
  return out;
}
async function extractPaletteFromBuffer(buffer, mimeType) {
  if (buffer.length > MAX_IMAGE_BYTES) {
    log.debug("palette: input exceeds byte cap", { bytes: buffer.length, cap: MAX_IMAGE_BYTES });
    return null;
  }
  if (!isRasterMime(mimeType)) {
    log.debug("palette: non-raster MIME, skipping", { mimeType });
    return null;
  }
  let raw;
  try {
    raw = await sharp(buffer, {
      failOn: "none",
      // Decode only the first frame of animated GIF/WebP/AVIF inputs.
      // Without these, a malicious 1000-frame GIF could blow the memory
      // budget; even a legitimate animated logo would have its palette
      // averaged across frames (the dominant color of a fade animation
      // is not the brand color).
      pages: 1,
      animated: false,
      // Cap declared pixels to block decode-bomb inputs. 50M = 7000×7000;
      // any real logo fits well under that.
      limitInputPixels: MAX_INPUT_PIXELS
    }).resize(RESIZE_LONG_EDGE, RESIZE_LONG_EDGE, {
      fit: "inside",
      withoutEnlargement: true
    }).removeAlpha().raw().toBuffer({ resolveWithObject: true });
  } catch (err) {
    log.debug("palette: sharp decode failed", { error: String(err) });
    return null;
  }
  if (raw.info.channels !== 3) {
    log.debug("palette: unexpected channel count after removeAlpha", { channels: raw.info.channels });
    return null;
  }
  const clusters = kmeans(new Uint8Array(raw.data.buffer, raw.data.byteOffset, raw.data.byteLength), K);
  const colors = rankAndFilterClusters(clusters);
  if (colors.length === 0) return null;
  return { colors };
}
const __internal = {
  hexToRgb,
  rgbToHex,
  isNearWhite,
  isNearBlack,
  isNearGrey,
  isRasterMime,
  kmeans,
  rankAndFilterClusters
};
export {
  MAX_IMAGE_BYTES,
  __internal,
  extractPaletteFromBuffer
};
//# sourceMappingURL=brand-palette.js.map