/* global cv */

function waitForOpenCv() {
  return new Promise((resolve, reject) => {
    const started = Date.now();
    const tick = async () => {
      if (globalThis.cv && typeof globalThis.cv.then === "function") {
        try {
          globalThis.cv = await globalThis.cv;
        } catch (err) {
          return reject(err);
        }
      }
      if (globalThis.cv && typeof globalThis.cv.inpaint === "function" && typeof globalThis.cv.Mat === "function") {
        return resolve(globalThis.cv);
      }
      if (globalThis.cv && typeof globalThis.cv.onRuntimeInitialized === "function") {
        globalThis.cv.onRuntimeInitialized = () => resolve(globalThis.cv);
      }
      if (Date.now() - started > 30000) return reject(new Error("OpenCV.js init timeout"));
      setTimeout(tick, 50);
    };
    tick();
  });
}

function getAverageColor(ctx, x, y, w, h) {
  const sx = Math.min(ctx.canvas.width - 1, Math.max(0, Math.floor(x)));
  const sy = Math.min(ctx.canvas.height - 1, Math.max(0, Math.floor(y)));
  const sw = Math.max(1, Math.min(Math.floor(w), ctx.canvas.width - sx));
  const sh = Math.max(1, Math.min(Math.floor(h), ctx.canvas.height - sy));
  const data = ctx.getImageData(sx, sy, sw, sh).data;
  let r = 0, g = 0, b = 0, n = 0;
  for (let i = 0; i < data.length; i += 16) {
    r += data[i]; g += data[i + 1]; b += data[i + 2]; n++;
  }
  if (!n) return [245, 245, 245];
  return [Math.round(r / n), Math.round(g / n), Math.round(b / n)];
}

function mergeBoxes(boxes, gap) {
  const out = [];
  for (const box of boxes) {
    let merged = false;
    for (const cur of out) {
      const separate =
        box.x2 + gap < cur.x1 || cur.x2 + gap < box.x1 ||
        box.y2 + gap < cur.y1 || cur.y2 + gap < box.y1;
      if (!separate) {
        cur.x1 = Math.min(cur.x1, box.x1);
        cur.y1 = Math.min(cur.y1, box.y1);
        cur.x2 = Math.max(cur.x2, box.x2);
        cur.y2 = Math.max(cur.y2, box.y2);
        merged = true;
        break;
      }
    }
    if (!merged) out.push({ ...box });
  }
  return out;
}

function detectTextLikeBoxes(canvas, ctx) {
  const maxSide = 900;
  const scale = Math.min(1, maxSide / Math.max(canvas.width, canvas.height));
  const sw = Math.max(1, Math.round(canvas.width * scale));
  const sh = Math.max(1, Math.round(canvas.height * scale));
  const sample = document.createElement("canvas");
  sample.width = sw; sample.height = sh;
  const sctx = sample.getContext("2d", { willReadFrequently: true });
  sctx.drawImage(canvas, 0, 0, sw, sh);

  const img = sctx.getImageData(0, 0, sw, sh);
  const data = img.data;
  const lum = new Uint8Array(sw * sh);
  let sum = 0;
  for (let i = 0, p = 0; i < data.length; i += 4, p++) {
    const v = Math.round(data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114);
    lum[p] = v;
    sum += v;
  }
  const mean = sum / Math.max(1, lum.length);
  let variance = 0;
  for (let i = 0; i < lum.length; i++) {
    const d = lum[i] - mean;
    variance += d * d;
  }
  const std = Math.sqrt(variance / Math.max(1, lum.length));
  const edgeThreshold = Math.max(16, Math.min(52, std * 0.52));

  const edge = new Uint8Array(sw * sh);
  for (let y = 1; y < sh - 1; y++) {
    for (let x = 1; x < sw - 1; x++) {
      const p = y * sw + x;
      const gx = Math.abs(lum[p + 1] - lum[p - 1]);
      const gy = Math.abs(lum[p + sw] - lum[p - sw]);
      const v = lum[p];
      const brightText = v > 168 && gx + gy > edgeThreshold * 0.7;
      const darkText = v < 118 && gx + gy > edgeThreshold * 0.55;
      if (gx + gy > edgeThreshold || brightText || darkText) edge[p] = 1;
    }
  }

  const mask = new Uint8Array(sw * sh);
  const rx = Math.max(3, Math.round(sw * 0.01));
  const ry = Math.max(2, Math.round(sh * 0.005));
  for (let y = 1; y < sh - 1; y++) {
    for (let x = 1; x < sw - 1; x++) {
      if (!edge[y * sw + x]) continue;
      const x1 = Math.max(0, x - rx);
      const x2 = Math.min(sw - 1, x + rx);
      const y1 = Math.max(0, y - ry);
      const y2 = Math.min(sh - 1, y + ry);
      for (let yy = y1; yy <= y2; yy++) {
        const row = yy * sw;
        for (let xx = x1; xx <= x2; xx++) mask[row + xx] = 1;
      }
    }
  }

  const visited = new Uint8Array(sw * sh);
  const boxes = [];
  const qx = [];
  const qy = [];
  for (let y = 0; y < sh; y++) {
    for (let x = 0; x < sw; x++) {
      const start = y * sw + x;
      if (!mask[start] || visited[start]) continue;
      let head = 0;
      qx.length = 0; qy.length = 0;
      qx.push(x); qy.push(y); visited[start] = 1;
      let x1 = x, x2 = x, y1 = y, y2 = y, count = 0;
      while (head < qx.length) {
        const cx = qx[head];
        const cy = qy[head++];
        count++;
        if (cx < x1) x1 = cx;
        if (cx > x2) x2 = cx;
        if (cy < y1) y1 = cy;
        if (cy > y2) y2 = cy;
        const neighbors = [[cx + 1, cy], [cx - 1, cy], [cx, cy + 1], [cx, cy - 1]];
        for (const pair of neighbors) {
          const nx = pair[0], ny = pair[1];
          if (nx < 0 || ny < 0 || nx >= sw || ny >= sh) continue;
          const np = ny * sw + nx;
          if (mask[np] && !visited[np]) {
            visited[np] = 1;
            qx.push(nx); qy.push(ny);
          }
        }
      }
      const bw = x2 - x1 + 1;
      const bh = y2 - y1 + 1;
      const area = bw * bh;
      const fill = count / Math.max(1, area);
      const imageArea = sw * sh;
      const wideEnough = bw >= Math.max(12, sw * 0.02);
      const tallEnough = bh >= Math.max(4, sh * 0.004);
      const notHuge = area < imageArea * 0.28 && bh < sh * 0.42 && bw < sw * 0.98;
      const textShape = fill > 0.03 && fill < 0.96 && bw / Math.max(1, bh) > 0.45;
      if (wideEnough && tallEnough && notHuge && textShape) boxes.push({ x1, y1, x2, y2 });
    }
  }

  let merged = boxes;
  for (let i = 0; i < 4; i++) merged = mergeBoxes(merged, Math.max(6, Math.round(sw * 0.02)));
  const padX = Math.max(6, Math.round(sw * 0.012));
  const padY = Math.max(5, Math.round(sh * 0.008));
  return merged.map((b) => ({
    x: Math.max(0, Math.round((b.x1 - padX) / scale)),
    y: Math.max(0, Math.round((b.y1 - padY) / scale)),
    w: Math.min(canvas.width, Math.round((b.x2 - b.x1 + 1 + padX * 2) / scale)),
    h: Math.min(canvas.height, Math.round((b.y2 - b.y1 + 1 + padY * 2) / scale)),
  })).filter((b) => b.w > 0 && b.h > 0);
}

function isEdgeRect(rect, canvas, marginRatio = 0.24) {
  const cx = rect.x + rect.w / 2;
  const cy = rect.y + rect.h / 2;
  return (
    cx <= canvas.width * marginRatio ||
    cx >= canvas.width * (1 - marginRatio) ||
    cy <= canvas.height * marginRatio ||
    cy >= canvas.height * (1 - marginRatio)
  );
}

function inpaintTextRegions(canvas, ctx, options = {}) {
  const edgeOnly = !!options.edgeOnly;
  const cv = globalThis.cv;
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const srcRgba = cv.matFromImageData(imageData);
  const src = new cv.Mat();
  const dst = new cv.Mat();
  const dstRgba = new cv.Mat();
  const gray = new cv.Mat();
  const bg = new cv.Mat();
  const diff = new cv.Mat();
  const contrast = new cv.Mat();
  const dark = new cv.Mat();
  const darkText = new cv.Mat();
  const merged = new cv.Mat();
  const filtered = cv.Mat.zeros(canvas.height, canvas.width, cv.CV_8UC1);

  try {
    cv.cvtColor(srcRgba, src, cv.COLOR_RGBA2RGB);
    cv.cvtColor(src, gray, cv.COLOR_RGB2GRAY);

    const blurSize = Math.max(21, Math.min(71, Math.round(Math.min(canvas.width, canvas.height) / 35) | 1));
    cv.GaussianBlur(gray, bg, new cv.Size(blurSize, blurSize), 0);
    cv.absdiff(gray, bg, diff);
    cv.threshold(diff, contrast, 10, 255, cv.THRESH_BINARY);
    cv.threshold(gray, dark, 115, 255, cv.THRESH_BINARY_INV);
    cv.bitwise_and(dark, contrast, darkText);

    let kept = 0;

    const addExactContours = (mat) => {
      const vector = new cv.MatVector();
      const hier = new cv.Mat();
      try {
        cv.findContours(mat, vector, hier, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE);
        for (let i = 0; i < vector.size(); i++) {
          const rect = cv.boundingRect(vector.get(i));
          const area = rect.width * rect.height;
          const imageArea = canvas.width * canvas.height;
          if (
            rect.width < Math.max(12, canvas.width * 0.006) ||
            rect.height < Math.max(8, canvas.height * 0.003) ||
            rect.height > canvas.height * 0.18 ||
            area > imageArea * 0.08
          ) continue;
          if (rect.width > canvas.width * 0.12 || rect.width / Math.max(1, rect.height) > 2.2) {
            const padX = Math.max(10, Math.round(canvas.width * 0.01));
            const padY = Math.max(8, Math.round(canvas.height * 0.008));
            const x = Math.max(0, rect.x - padX);
            const y = Math.max(0, rect.y - padY);
            const w = Math.min(canvas.width - x, rect.width + padX * 2);
            const h = Math.min(canvas.height - y, rect.height + padY * 2);
            const target = { x, y, w, h };
            if (!edgeOnly || isEdgeRect(target, canvas)) {
              cv.rectangle(filtered, new cv.Point(x, y), new cv.Point(x + w, y + h), new cv.Scalar(255), -1);
              kept++;
            }
          } else {
            const target = { x: rect.x, y: rect.y, w: rect.width, h: rect.height };
            if (!edgeOnly || isEdgeRect(target, canvas)) {
              cv.drawContours(filtered, vector, i, new cv.Scalar(255), -1);
              kept++;
            }
          }
        }
      } finally {
        vector.delete();
        hier.delete();
      }
    };

    const darkK = cv.getStructuringElement(
      cv.MORPH_RECT,
      new cv.Size(Math.max(11, Math.round(canvas.width * 0.01)), Math.max(9, Math.round(canvas.height * 0.006)))
    );
    try {
      cv.morphologyEx(darkText, merged, cv.MORPH_CLOSE, darkK);
      cv.dilate(merged, merged, darkK);
      addExactContours(merged);
    } finally {
      darkK.delete();
    }

    const finalK = cv.getStructuringElement(cv.MORPH_RECT, new cv.Size(5, 5));
    try {
      cv.dilate(filtered, filtered, finalK);
    } finally {
      finalK.delete();
    }

    if (!kept) return 0;
    const maskRatio = cv.countNonZero(filtered) / Math.max(1, canvas.width * canvas.height);
    if (maskRatio > 0.1) return 0;

    cv.inpaint(src, filtered, dst, 4, cv.INPAINT_TELEA);
    cv.cvtColor(dst, dstRgba, cv.COLOR_RGB2RGBA);
    const out = new ImageData(new Uint8ClampedArray(dstRgba.data), dstRgba.cols, dstRgba.rows);
    ctx.putImageData(out, 0, 0);
    return kept;
  } finally {
    srcRgba.delete();
    src.delete();
    dst.delete();
    dstRgba.delete();
    gray.delete();
    bg.delete();
    diff.delete();
    contrast.delete();
    dark.delete();
    darkText.delete();
    merged.delete();
    filtered.delete();
  }
}

function inpaintBottomRightLite(canvas, ctx) {
  const w = canvas.width;
  const h = canvas.height;
  const x = Math.floor(w * 0.85);
  const y = Math.floor(h * 0.92);
  const sample = getAverageColor(ctx, Math.max(0, x - Math.floor(w * 0.12)), Math.max(0, y - Math.floor(h * 0.08)), Math.floor(w * 0.1), Math.floor(h * 0.06));
  ctx.save();
  ctx.fillStyle = `rgb(${sample[0]}, ${sample[1]}, ${sample[2]})`;
  ctx.fillRect(x, y, w - x, h - y);
  ctx.restore();
}

function borderCanvas(canvas, ctx, op) {
  const parts = String(op || "").split(":");
  const px = Math.max(0, parseInt(parts[1] || "50", 10) || 50);
  if (!px) return { canvas, ctx };
  let color = parts[2] || "auto";
  if (color === "auto") {
    const avg = getAverageColor(ctx, 0, 0, canvas.width, canvas.height).map(v => Math.min(255, v + 60));
    color = `rgb(${avg[0]}, ${avg[1]}, ${avg[2]})`;
  }
  const next = document.createElement("canvas");
  next.width = canvas.width + px * 2;
  next.height = canvas.height + px * 2;
  const nctx = next.getContext("2d", { willReadFrequently: true });
  nctx.fillStyle = color;
  nctx.fillRect(0, 0, next.width, next.height);
  nctx.drawImage(canvas, px, px);
  return { canvas: next, ctx: nctx };
}

function watermarkCanvas(canvas, ctx, op) {
  const text = String(op || "").slice("watermark:".length) || "ecommclaw";
  const fontSize = Math.max(8, Math.min(18, Math.floor(canvas.width / 90)));
  ctx.save();
  ctx.font = `${fontSize}px sans-serif`;
  ctx.fillStyle = "rgba(255,255,255,0.32)";
  ctx.textBaseline = "bottom";
  const metrics = ctx.measureText(text);
  ctx.fillText(text, canvas.width - metrics.width - 12, canvas.height - 12);
  ctx.textBaseline = "middle";
  ctx.fillText(text, Math.max(12, canvas.width / 2 - metrics.width / 2), canvas.height / 2);
  ctx.restore();
}

function blobToBase64(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(String(reader.result || "").split(",")[1] || "");
    reader.onerror = () => reject(reader.error || new Error("blob read failed"));
    reader.readAsDataURL(blob);
  });
}

async function processImage(task) {
  await waitForOpenCv();
  const res = await fetch(task.source_url, { credentials: "omit", cache: "no-store" });
  if (!res.ok) throw new Error(`image fetch failed ${res.status}: ${task.source_url}`);
  const blob = await res.blob();
  const bitmap = await createImageBitmap(blob);
  let canvas = document.createElement("canvas");
  canvas.width = bitmap.width;
  canvas.height = bitmap.height;
  let ctx = canvas.getContext("2d", { willReadFrequently: true });
  ctx.drawImage(bitmap, 0, 0);

  const ops = Array.isArray(task.ops) ? task.ops : [];
  for (const op of ops) {
    if (op === "download" || !op) continue;
    if (op === "inpaint_bottom_right") {
      inpaintBottomRightLite(canvas, ctx);
    } else if (op === "ocr_inpaint_text") {
      inpaintTextRegions(canvas, ctx);
    } else if (op === "ocr_inpaint_text_edges") {
      inpaintTextRegions(canvas, ctx, { edgeOnly: true });
    } else if (String(op).startsWith("border:")) {
      const next = borderCanvas(canvas, ctx, op);
      canvas = next.canvas;
      ctx = next.ctx;
    } else if (String(op).startsWith("watermark:")) {
      watermarkCanvas(canvas, ctx, op);
    }
  }

  const outBlob = await new Promise((resolve, reject) => {
    canvas.toBlob((b) => b ? resolve(b) : reject(new Error("canvas.toBlob failed")), "image/jpeg", 0.88);
  });
  return {
    content_base64: await blobToBase64(outBlob),
    width: canvas.width,
    height: canvas.height,
  };
}

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  if (!message || message.type !== "rb_process_image_offscreen") return false;
  processImage(message.task || {})
    .then((result) => sendResponse({ ok: true, result }))
    .catch((err) => sendResponse({ ok: false, error: err && err.message ? err.message : String(err) }));
  return true;
});
