import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import Cropper from "cropperjs";
import { fabric } from "fabric";
import "cropperjs/dist/cropper.css";

/** Cap interactive edit resolution — keeps crop/mark/export responsive. */
const MAX_MARK_DIM = 2048;
const EXPORT_MIME = "image/jpeg";
const EXPORT_QUALITY = 0.92;
const HIGHLIGHT_STROKE = "rgba(255, 235, 80, 0.45)";
const HIGHLIGHT_STROKE_EDGE = "rgba(180, 120, 0, 0.35)";

function canvasToDataUrl(canvas, mime = EXPORT_MIME, quality = EXPORT_QUALITY) {
  if (!canvas) return null;
  try {
    const url = canvas.toDataURL(mime, quality);
    return url && String(url).startsWith("data:image") ? url : null;
  } catch {
    try {
      const url = canvas.toDataURL("image/png");
      return url && String(url).startsWith("data:image") ? url : null;
    } catch {
      return null;
    }
  }
}

/** Fast grayscale via canvas filter (no per-pixel JS loop). */
function grayscaleDataUrl(dataUrl) {
  return new Promise((resolve) => {
    const img = new Image();
    img.onload = () => {
      try {
        const maxDim = Math.max(img.naturalWidth, img.naturalHeight) || 1;
        const scale = Math.min(1, MAX_MARK_DIM / maxDim);
        const w = Math.max(1, Math.round(img.naturalWidth * scale));
        const h = Math.max(1, Math.round(img.naturalHeight * scale));
        const c = document.createElement("canvas");
        c.width = w;
        c.height = h;
        const ctx = c.getContext("2d");
        if (ctx.filter !== undefined) {
          ctx.filter = "grayscale(100%)";
          ctx.drawImage(img, 0, 0, w, h);
        } else {
          ctx.drawImage(img, 0, 0, w, h);
          const idata = ctx.getImageData(0, 0, w, h);
          const d = idata.data;
          for (let i = 0; i < d.length; i += 4) {
            const y = (d[i] * 0.299 + d[i + 1] * 0.587 + d[i + 2] * 0.114) | 0;
            d[i] = y;
            d[i + 1] = y;
            d[i + 2] = y;
          }
          ctx.putImageData(idata, 0, 0);
        }
        resolve(canvasToDataUrl(c) || dataUrl);
      } catch {
        resolve(dataUrl);
      }
    };
    img.onerror = () => resolve(dataUrl);
    img.src = dataUrl;
  });
}

function snapshotImageElement(img) {
  if (!img?.naturalWidth || !img.naturalHeight) return null;
  const scale = Math.min(1, MAX_MARK_DIM / Math.max(img.naturalWidth, img.naturalHeight));
  const w = Math.round(img.naturalWidth * scale);
  const h = Math.round(img.naturalHeight * scale);
  const canvas = document.createElement("canvas");
  canvas.width = w;
  canvas.height = h;
  const ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0, w, h);
  return canvasToDataUrl(canvas);
}

function eraseMarksAtPoint(canvas, x, y, radius) {
  if (!canvas) return false;
  const r2 = radius * radius;
  const pointer = new fabric.Point(x, y);
  const objects = canvas.getObjects();
  let removed = false;

  for (let i = objects.length - 1; i >= 0; i -= 1) {
    const obj = objects[i];
    let hit = false;
    try {
      if (typeof obj.containsPoint === "function" && obj.containsPoint(pointer, null, true)) {
        hit = true;
      }
    } catch {
      /* bounds fallback */
    }
    if (!hit) {
      const bounds = obj.getBoundingRect(true, true);
      const closestX = Math.max(bounds.left, Math.min(x, bounds.left + bounds.width));
      const closestY = Math.max(bounds.top, Math.min(y, bounds.top + bounds.height));
      const dx = x - closestX;
      const dy = y - closestY;
      hit = dx * dx + dy * dy <= r2;
    }
    if (hit) {
      canvas.remove(obj);
      removed = true;
    }
  }
  return removed;
}

function eraserRadiusFromMarkWidth(markWidth) {
  return Math.max(16, markWidth * 3);
}

function highlightStrokeWidth(markWidth) {
  return Math.max(18, markWidth * 4);
}

/** Flatten fabric mark canvas (cropped background + annotations) for document slots. */
function snapshotMarkCanvasToDataUrl(canvas) {
  if (!canvas) return null;
  try {
    canvas.discardActiveObject?.();
    canvas.renderAll?.();
  } catch {
    /* continue with best-effort export */
  }

  try {
    const url = canvas.toDataURL({
      format: "jpeg",
      quality: EXPORT_QUALITY,
      multiplier: 1,
      enableRetinaScaling: false,
    });
    if (url && String(url).startsWith("data:image")) return url;
  } catch {
    /* fall through */
  }

  try {
    if (typeof canvas.toCanvasElement === "function") {
      const el = canvas.toCanvasElement({
        multiplier: 1,
        enableRetinaScaling: false,
      });
      const fromEl = canvasToDataUrl(el);
      if (fromEl) return fromEl;
    }
  } catch {
    /* fall through */
  }

  return null;
}

export default function ImageEditorModal({
  open,
  imageSrc,
  slotIndex,
  onClose,
  onApply,
}) {
  const [activeTab, setActiveTab] = useState("crop");
  const [markColor, setMarkColor] = useState("#cc0000");
  const [markWidth, setMarkWidth] = useState(4);
  const [markTool, setMarkTool] = useState("draw");
  const [grayscale, setGrayscale] = useState(false);
  const [imageLoadError, setImageLoadError] = useState(false);
  const [markCanvasKey, setMarkCanvasKey] = useState(0);
  const [applying, setApplying] = useState(false);

  const editorImgRef = useRef(null);
  const cropperRef = useRef(null);
  const fabricCanvasRef = useRef(null);
  const markCanvasRef = useRef(null);
  const markScrollRef = useRef(null);
  const markContentSizeRef = useRef(null);
  const markBaseZoomRef = useRef(1);
  const markViewScaleRef = useRef(1);
  const shapeDragRef = useRef(null);
  const activeTabRef = useRef("crop");
  const markToolRef = useRef("draw");
  const markColorRef = useRef("#cc0000");
  const markWidthRef = useRef(4);
  const fabricHandlersRef = useRef(null);
  const interactRafRef = useRef(null);
  const toolApplyRafRef = useRef(null);
  const fitRafRef = useRef(null);

  activeTabRef.current = activeTab;
  markToolRef.current = markTool;
  markColorRef.current = markColor;
  markWidthRef.current = markWidth;

  const destroyCropper = useCallback(() => {
    if (cropperRef.current) {
      cropperRef.current.destroy();
      cropperRef.current = null;
    }
  }, []);

  const syncMarkCanvasWrapper = useCallback((canvas, cssW, cssH) => {
    const wrapper = canvas?.wrapperEl;
    if (!wrapper) return;
    wrapper.style.width = `${cssW}px`;
    wrapper.style.height = `${cssH}px`;
    wrapper.style.maxWidth = "";
    wrapper.style.maxHeight = "";
    wrapper.style.margin = "0 auto";
  }, []);

  const disposeFabric = useCallback(() => {
    shapeDragRef.current = null;
    markContentSizeRef.current = null;
    if (interactRafRef.current != null) {
      cancelAnimationFrame(interactRafRef.current);
      interactRafRef.current = null;
    }
    if (fitRafRef.current != null) {
      cancelAnimationFrame(fitRafRef.current);
      fitRafRef.current = null;
    }
    if (fabricCanvasRef.current) {
      const handlers = fabricHandlersRef.current;
      if (handlers) {
        fabricCanvasRef.current.off("mouse:down", handlers.onDown);
        fabricCanvasRef.current.off("mouse:move", handlers.onMove);
        fabricCanvasRef.current.off("mouse:up", handlers.onUp);
        fabricHandlersRef.current = null;
      }
      fabricCanvasRef.current.dispose();
      fabricCanvasRef.current = null;
    }
  }, []);

  const attachCropper = useCallback(() => {
    destroyCropper();
    const img = editorImgRef.current;
    if (!img || !imageSrc) return;

    const init = () => {
      const el = editorImgRef.current;
      if (!el?.naturalWidth || !el.naturalHeight) return;
      destroyCropper();
      cropperRef.current = new Cropper(el, {
        viewMode: 1,
        dragMode: "move",
        autoCropArea: 0.85,
        responsive: true,
        background: false,
        checkOrientation: false,
        checkCrossOrigin: false,
        restore: false,
        toggleDragModeOnDblclick: false,
      });
    };

    const onReady = () => {
      setImageLoadError(false);
      init();
    };

    const onFail = () => {
      setImageLoadError(true);
      destroyCropper();
    };

    img.onload = () => {
      onReady();
      img.onload = null;
      img.onerror = null;
    };
    img.onerror = () => {
      onFail();
      img.onload = null;
      img.onerror = null;
    };

    if (img.complete) {
      if (img.naturalWidth > 0) onReady();
      else onFail();
    }
  }, [destroyCropper, imageSrc]);

  const addArrowShape = useCallback((canvas, x1, y1, x2, y2) => {
    const color = markColorRef.current;
    const width = markWidthRef.current;
    const dx = x2 - x1;
    const dy = y2 - y1;
    const len = Math.sqrt(dx * dx + dy * dy);
    if (len < 4) return;

    const headLen = 12 + width * 0.85;
    const headHeight = headLen * 0.9;
    const halfBase = headLen * 0.42;
    const shaftLen = Math.max(0, len - headHeight);

    const line = new fabric.Line([0, 0, shaftLen, 0], {
      stroke: color,
      strokeWidth: width,
      strokeLineCap: "round",
      selectable: false,
      evented: false,
    });

    const head = new fabric.Polygon(
      [
        { x: len, y: 0 },
        { x: shaftLen, y: -halfBase },
        { x: shaftLen, y: halfBase },
      ],
      {
        fill: color,
        selectable: false,
        evented: false,
      }
    );

    const group = new fabric.Group([line, head], {
      left: x1,
      top: y1,
      angle: fabric.util.radiansToDegrees(Math.atan2(dy, dx)),
      originX: "left",
      originY: "center",
      selectable: false,
      evented: false,
    });

    canvas.add(group);
  }, []);

  const applyMarkTool = useCallback((canvas, tool) => {
    if (!canvas) return;

    const isDraw = tool === "draw";
    const isHighlight = tool === "highlight";
    const isEraser = tool === "eraser";

    canvas.isDrawingMode = isDraw || isHighlight;
    canvas.selection = tool === "text";
    canvas.skipTargetFind = isDraw || isHighlight || isEraser;
    canvas.defaultCursor = isEraser ? "crosshair" : "default";
    canvas.hoverCursor = isEraser ? "crosshair" : "move";
    canvas.freeDrawingCursor = "crosshair";

    canvas.forEachObject((o) => {
      const isText = o.type === "i-text" || o.type === "textbox";
      o.selectable = tool === "text" && isText;
      o.evented = tool === "text" && isText;
    });

    if (isDraw || isHighlight) {
      const brush = new fabric.PencilBrush(canvas);
      brush.globalCompositeOperation = "source-over";
      if (isHighlight) {
        brush.color = HIGHLIGHT_STROKE;
        brush.width = highlightStrokeWidth(markWidthRef.current);
      } else {
        brush.color = markColorRef.current;
        brush.width = markWidthRef.current;
      }
      canvas.freeDrawingBrush = brush;
    }

    canvas.calcOffset();
  }, []);

  const configureMarkPath = useCallback((path, tool) => {
    if (!path) return;
    const isHighlight = tool === "highlight";
    path.set({
      globalCompositeOperation: "source-over",
      stroke: isHighlight ? HIGHLIGHT_STROKE : markColorRef.current,
      strokeWidth: isHighlight
        ? highlightStrokeWidth(markWidthRef.current)
        : markWidthRef.current,
      strokeLineCap: "round",
      strokeLineJoin: "round",
      selectable: false,
      evented: false,
    });
  }, []);

  const fitMarkCanvasView = useCallback(
    (canvas) => {
      const size = markContentSizeRef.current;
      if (!canvas || !size) return;

      const scroll = markScrollRef.current;
      const maxW = Math.max(scroll?.clientWidth || 0, 1);
      const maxH = Math.max(
        scroll?.clientHeight || 0,
        Math.min(window.innerHeight * 0.62, 560)
      );
      const fitScale = Math.min(maxW / size.w, maxH / size.h, 1.35);
      const cssW = Math.max(1, Math.round(size.w * fitScale));
      const cssH = Math.max(1, Math.round(size.h * fitScale));

      canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
      canvas.setZoom(1);
      canvas.setDimensions({ width: size.w, height: size.h });
      canvas.setDimensions({ width: cssW, height: cssH }, { cssOnly: true });
      syncMarkCanvasWrapper(canvas, cssW, cssH);
      markBaseZoomRef.current = fitScale;
      markViewScaleRef.current = fitScale;
      canvas.calcOffset();
      canvas.requestRenderAll();
    },
    [syncMarkCanvasWrapper]
  );

  const scheduleFitMarkCanvasView = useCallback(
    (canvas) => {
      if (fitRafRef.current != null) cancelAnimationFrame(fitRafRef.current);
      fitRafRef.current = requestAnimationFrame(() => {
        fitRafRef.current = null;
        if (fabricCanvasRef.current === canvas) {
          fitMarkCanvasView(canvas);
        }
      });
    },
    [fitMarkCanvasView]
  );

  const bindFabricShapeTools = useCallback(
    (canvas) => {
      const scheduleInteractRender = (fn) => {
        if (interactRafRef.current != null) return;
        interactRafRef.current = requestAnimationFrame(() => {
          interactRafRef.current = null;
          fn();
        });
      };

      const onDown = (opt) => {
        if (!canvas || activeTabRef.current !== "mark") return;
        const tool = markToolRef.current;

        if (tool === "draw" || tool === "highlight") return;

        if (tool === "eraser") {
          const p = canvas.getPointer(opt.e);
          if (
            eraseMarksAtPoint(
              canvas,
              p.x,
              p.y,
              eraserRadiusFromMarkWidth(markWidthRef.current)
            )
          ) {
            canvas.requestRenderAll();
          }
          shapeDragRef.current = { kind: "eraser" };
          return;
        }

        if (tool === "text") {
          if (opt.target) return;
          const p = canvas.getPointer(opt.e);
          const t = new fabric.IText("Label", {
            left: p.x,
            top: p.y,
            fill: markColorRef.current,
            fontSize: 20,
            fontFamily: "Arial, Helvetica, sans-serif",
          });
          canvas.add(t);
          canvas.setActiveObject(t);
          t.enterEditing();
          t.selectAll();
          return;
        }

        const p = canvas.getPointer(opt.e);
        const mx = markColorRef.current;
        const mw = markWidthRef.current;

        if (tool === "line" || tool === "arrow") {
          const ln = new fabric.Line([p.x, p.y, p.x, p.y], {
            stroke: mx,
            strokeWidth: mw,
            strokeLineCap: "round",
            selectable: false,
            evented: false,
          });
          canvas.add(ln);
          shapeDragRef.current = { kind: tool, x0: p.x, y0: p.y, shape: ln };
        } else if (tool === "rect") {
          const r = new fabric.Rect({
            left: p.x,
            top: p.y,
            width: 0,
            height: 0,
            fill: HIGHLIGHT_STROKE,
            stroke: HIGHLIGHT_STROKE_EDGE,
            strokeWidth: 1,
            selectable: false,
            evented: false,
          });
          canvas.add(r);
          shapeDragRef.current = { kind: "rect", x0: p.x, y0: p.y, shape: r };
        } else if (tool === "rect-out") {
          const ro = new fabric.Rect({
            left: p.x,
            top: p.y,
            width: 0,
            height: 0,
            fill: "transparent",
            stroke: mx,
            strokeWidth: mw,
            selectable: false,
            evented: false,
          });
          canvas.add(ro);
          shapeDragRef.current = { kind: "rect-out", x0: p.x, y0: p.y, shape: ro };
        } else if (tool === "ellipse" || tool === "circle") {
          const el = new fabric.Ellipse({
            left: p.x,
            top: p.y,
            rx: 1,
            ry: 1,
            originX: "center",
            originY: "center",
            fill: "",
            stroke: mx,
            strokeWidth: mw,
            selectable: false,
            evented: false,
          });
          canvas.add(el);
          shapeDragRef.current = { kind: tool, x0: p.x, y0: p.y, shape: el };
        }
      };

      const onMove = (opt) => {
        const shapeDrag = shapeDragRef.current;
        if (!shapeDrag || !canvas) return;

        if (shapeDrag.kind === "eraser") {
          const p = canvas.getPointer(opt.e);
          shapeDrag.pending = {
            x: p.x,
            y: p.y,
            radius: eraserRadiusFromMarkWidth(markWidthRef.current),
          };
          scheduleInteractRender(() => {
            const drag = shapeDragRef.current;
            if (!drag?.pending || drag.kind !== "eraser") return;
            const { x, y, radius } = drag.pending;
            drag.pending = null;
            if (eraseMarksAtPoint(canvas, x, y, radius)) {
              canvas.requestRenderAll();
            }
          });
          return;
        }

        const p = canvas.getPointer(opt.e);
        shapeDrag.pending = { x: p.x, y: p.y };
        scheduleInteractRender(() => {
          const drag = shapeDragRef.current;
          if (!drag?.pending || !drag.shape) return;
          const { x, y } = drag.pending;
          drag.pending = null;

          if (drag.kind === "line" || drag.kind === "arrow") {
            drag.shape.set({ x2: x, y2: y });
            drag.lx = x;
            drag.ly = y;
          } else if (drag.kind === "rect" || drag.kind === "rect-out") {
            const xr = drag.x0;
            const yr = drag.y0;
            drag.shape.set({
              left: Math.min(xr, x),
              top: Math.min(yr, y),
              width: Math.abs(x - xr),
              height: Math.abs(y - yr),
            });
          } else if (drag.kind === "ellipse" || drag.kind === "circle") {
            let rx = Math.abs(x - drag.x0) / 2;
            let ry = Math.abs(y - drag.y0) / 2;
            if (drag.kind === "circle") {
              const rr = Math.max(rx, ry);
              rx = rr;
              ry = rr;
            }
            drag.shape.set({
              left: (drag.x0 + x) / 2,
              top: (drag.y0 + y) / 2,
              rx: Math.max(rx, 0.5),
              ry: Math.max(ry, 0.5),
            });
          }
          canvas.requestRenderAll();
        });
      };

      const onUp = () => {
        if (interactRafRef.current != null) {
          cancelAnimationFrame(interactRafRef.current);
          interactRafRef.current = null;
        }

        const shapeDrag = shapeDragRef.current;
        if (!shapeDrag || !canvas) return;

        if (shapeDrag.kind === "eraser") {
          if (shapeDrag.pending) {
            const { x, y, radius } = shapeDrag.pending;
            if (eraseMarksAtPoint(canvas, x, y, radius)) {
              canvas.requestRenderAll();
            }
          }
          shapeDragRef.current = null;
          return;
        }

        if (shapeDrag.pending && shapeDrag.shape) {
          const { x, y } = shapeDrag.pending;
          if (shapeDrag.kind === "line" || shapeDrag.kind === "arrow") {
            shapeDrag.shape.set({ x2: x, y2: y });
            shapeDrag.lx = x;
            shapeDrag.ly = y;
          } else if (shapeDrag.kind === "rect" || shapeDrag.kind === "rect-out") {
            shapeDrag.shape.set({
              left: Math.min(shapeDrag.x0, x),
              top: Math.min(shapeDrag.y0, y),
              width: Math.abs(x - shapeDrag.x0),
              height: Math.abs(y - shapeDrag.y0),
            });
          } else if (shapeDrag.kind === "ellipse" || shapeDrag.kind === "circle") {
            let rx = Math.abs(x - shapeDrag.x0) / 2;
            let ry = Math.abs(y - shapeDrag.y0) / 2;
            if (shapeDrag.kind === "circle") {
              const rr = Math.max(rx, ry);
              rx = rr;
              ry = rr;
            }
            shapeDrag.shape.set({
              left: (shapeDrag.x0 + x) / 2,
              top: (shapeDrag.y0 + y) / 2,
              rx: Math.max(rx, 0.5),
              ry: Math.max(ry, 0.5),
            });
          }
          shapeDrag.pending = null;
        }

        if (shapeDrag.kind === "line" && shapeDrag.shape) {
          shapeDrag.shape.setCoords();
        } else if (shapeDrag.kind === "arrow" && shapeDrag.shape) {
          canvas.remove(shapeDrag.shape);
          addArrowShape(
            canvas,
            shapeDrag.x0,
            shapeDrag.y0,
            shapeDrag.lx ?? shapeDrag.x0,
            shapeDrag.ly ?? shapeDrag.y0
          );
        } else if (shapeDrag.shape) {
          shapeDrag.shape.setCoords();
        }
        shapeDragRef.current = null;
        canvas.requestRenderAll();
      };

      canvas.on("mouse:down", onDown);
      canvas.on("mouse:move", onMove);
      canvas.on("mouse:up", onUp);
      fabricHandlersRef.current = { onDown, onMove, onUp };
    },
    [addArrowShape]
  );

  const initFabricFromDataUrl = useCallback(
    (dataUrl) => {
      disposeFabric();
      const canvasEl = markCanvasRef.current;
      if (!canvasEl) return;

      const fabricOpts =
        typeof dataUrl === "string" && /^https?:\/\//i.test(dataUrl)
          ? { crossOrigin: "anonymous" }
          : {};

      fabric.Image.fromURL(
        dataUrl,
        (img) => {
          const el = img.getElement?.() || img._element;
          const iw = Math.max(1, el?.naturalWidth || img.width || 1);
          const ih = Math.max(1, el?.naturalHeight || img.height || 1);
          const downscale = Math.min(1, MAX_MARK_DIM / Math.max(iw, ih));
          const cw = Math.max(1, Math.round(iw * downscale));
          const ch = Math.max(1, Math.round(ih * downscale));

          const canvas = new fabric.Canvas(canvasEl, {
            width: cw,
            height: ch,
            stopContextMenu: true,
            enableRetinaScaling: false,
            renderOnAddRemove: true,
            selection: false,
            preserveObjectStacking: true,
          });

          img.set({
            left: 0,
            top: 0,
            originX: "left",
            originY: "top",
            scaleX: cw / iw,
            scaleY: ch / ih,
            selectable: false,
            evented: false,
            objectCaching: true,
          });

          canvas.setBackgroundImage(img, () => {
            // Abort if the modal tore down or started another mark session.
            if (markCanvasRef.current !== canvasEl) {
              try {
                canvas.dispose();
              } catch {
                /* ignore */
              }
              return;
            }
            canvas.requestRenderAll();
            markContentSizeRef.current = { w: cw, h: ch };
            fabricCanvasRef.current = canvas;
            bindFabricShapeTools(canvas);
            applyMarkTool(canvas, markToolRef.current);
            canvas.on("path:created", (event) => {
              configureMarkPath(event.path, markToolRef.current);
            });
            setMarkCanvasKey((k) => k + 1);
            scheduleFitMarkCanvasView(canvas);
          });
        },
        fabricOpts
      );
    },
    [applyMarkTool, bindFabricShapeTools, configureMarkPath, disposeFabric, scheduleFitMarkCanvasView]
  );

  const snapshotCropToDataUrl = useCallback(() => {
    const cropper = cropperRef.current;
    if (cropper) {
      const canvas = cropper.getCroppedCanvas({
        maxWidth: MAX_MARK_DIM,
        maxHeight: MAX_MARK_DIM,
        imageSmoothingEnabled: true,
        imageSmoothingQuality: "medium",
      });
      const fromCropper = canvasToDataUrl(canvas);
      if (fromCropper) return fromCropper;

      const cropped = cropper.getCroppedCanvas({
        maxWidth: MAX_MARK_DIM,
        maxHeight: MAX_MARK_DIM,
      });
      const fallback = canvasToDataUrl(cropped);
      if (fallback) return fallback;
    }

    return snapshotImageElement(editorImgRef.current);
  }, []);

  const enterMarkFromCrop = useCallback(() => {
    const dataUrl = snapshotCropToDataUrl();
    if (!dataUrl) {
      window.alert(
        "Cannot open Mark mode yet. Wait for the image to finish loading, then try again."
      );
      return;
    }
    destroyCropper();
    setActiveTab("mark");
    requestAnimationFrame(() => {
      requestAnimationFrame(() => initFabricFromDataUrl(dataUrl));
    });
  }, [destroyCropper, initFabricFromDataUrl, snapshotCropToDataUrl]);

  const enterCropFromMark = useCallback(() => {
    const canvas = fabricCanvasRef.current;
    if (!canvas) return;
    const flat = snapshotMarkCanvasToDataUrl(canvas);
    if (!flat) return;
    disposeFabric();
    setActiveTab("crop");
    const img = editorImgRef.current;
    if (img) {
      img.src = flat;
      requestAnimationFrame(() => attachCropper());
    }
  }, [attachCropper, disposeFabric]);

  const handleClose = useCallback(() => {
    setActiveTab("crop");
    setGrayscale(false);
    setApplying(false);
    if (interactRafRef.current != null) {
      cancelAnimationFrame(interactRafRef.current);
      interactRafRef.current = null;
    }
    if (toolApplyRafRef.current != null) {
      cancelAnimationFrame(toolApplyRafRef.current);
      toolApplyRafRef.current = null;
    }
    if (fitRafRef.current != null) {
      cancelAnimationFrame(fitRafRef.current);
      fitRafRef.current = null;
    }
    destroyCropper();
    disposeFabric();
    if (editorImgRef.current) editorImgRef.current.removeAttribute("src");
    onClose();
  }, [destroyCropper, disposeFabric, onClose]);

  const handleApply = useCallback(async () => {
    if (applying) return;
    setApplying(true);

    // Yield so the button can show "Applying…" before heavy encode work.
    await new Promise((resolve) => requestAnimationFrame(resolve));

    let closed = false;
    try {
      let dataUrl = null;
      if (activeTab === "mark") {
        if (!fabricCanvasRef.current) {
          window.alert(
            "Mark canvas is not ready yet. Wait a moment, then click Apply again."
          );
          return;
        }
        dataUrl = snapshotMarkCanvasToDataUrl(fabricCanvasRef.current);
      } else {
        dataUrl = snapshotCropToDataUrl();
      }
      if (!dataUrl || !String(dataUrl).startsWith("data:image")) {
        window.alert(
          "Could not export the image. Try applying again, or switch back to Crop and then Mark."
        );
        return;
      }

      const finalUrl = grayscale ? await grayscaleDataUrl(dataUrl) : dataUrl;
      if (!finalUrl || !String(finalUrl).startsWith("data:image")) {
        window.alert("Could not process the image. Please try again.");
        return;
      }

      onApply(finalUrl);
      closed = true;
      handleClose();
    } finally {
      if (!closed) setApplying(false);
    }
  }, [activeTab, applying, grayscale, handleClose, onApply, snapshotCropToDataUrl]);

  const handleCropAction = (action) => {
    const cropper = cropperRef.current;
    if (!cropper) {
      attachCropper();
      return;
    }
    if (action === "rotate-left") cropper.rotate(-90);
    else if (action === "rotate-right") cropper.rotate(90);
    else if (action === "flip-h") {
      const d = cropper.getImageData();
      cropper.scaleX(d.scaleX === 1 ? -1 : 1);
    } else if (action === "flip-v") {
      const d = cropper.getImageData();
      cropper.scaleY(d.scaleY === 1 ? -1 : 1);
    } else if (action === "zoom-in") cropper.zoom(0.1);
    else if (action === "zoom-out") cropper.zoom(-0.1);
    else if (action === "reset") cropper.reset();
  };

  const handleAspect = (raw) => {
    const cropper = cropperRef.current;
    if (!cropper) return;
    const ratio = parseFloat(raw, 10);
    cropper.setAspectRatio(Number.isNaN(ratio) ? NaN : ratio);
  };

  const handleUndoMark = () => {
    const canvas = fabricCanvasRef.current;
    if (!canvas) return;
    const objs = canvas.getObjects();
    if (objs.length) {
      canvas.remove(objs[objs.length - 1]);
      canvas.requestRenderAll();
    }
  };

  const handleClearMarks = () => {
    const canvas = fabricCanvasRef.current;
    if (!canvas) return;
    canvas.getObjects().forEach((o) => canvas.remove(o));
    canvas.requestRenderAll();
  };

  const handleMarkZoom = (action) => {
    const canvas = fabricCanvasRef.current;
    const size = markContentSizeRef.current;
    if (!canvas || !size) return;

    const base = markBaseZoomRef.current || 1;
    let scale = markViewScaleRef.current || base;

    if (action === "zoom-in") {
      scale = Math.min(scale * 1.12, base * 4);
    } else if (action === "zoom-out") {
      scale = Math.max(scale / 1.12, base * 0.35);
    } else if (action === "reset") {
      scale = base;
    }

    canvas.setZoom(1);
    const cssW = Math.round(size.w * scale);
    const cssH = Math.round(size.h * scale);
    canvas.setDimensions({ width: cssW, height: cssH }, { cssOnly: true });
    syncMarkCanvasWrapper(canvas, cssW, cssH);
    markViewScaleRef.current = scale;
    canvas.calcOffset();
    canvas.requestRenderAll();
  };

  useEffect(() => {
    if (!open) return undefined;
    setActiveTab("crop");
    setGrayscale(false);
    setImageLoadError(false);
    setMarkCanvasKey(0);
    disposeFabric();
    return () => {
      destroyCropper();
      disposeFabric();
    };
  }, [open, imageSrc, destroyCropper, disposeFabric]);

  useLayoutEffect(() => {
    if (!open || !imageSrc || activeTab !== "crop") return undefined;
    attachCropper();
    return () => destroyCropper();
  }, [open, imageSrc, activeTab, attachCropper, destroyCropper]);

  useLayoutEffect(() => {
    if (!open || activeTab !== "mark") return undefined;
    const canvas = fabricCanvasRef.current;
    if (!canvas) return undefined;
    fitMarkCanvasView(canvas);
    const scroll = markScrollRef.current;
    if (!scroll || typeof ResizeObserver === "undefined") return undefined;
    const observer = new ResizeObserver(() => {
      if (fitRafRef.current != null) cancelAnimationFrame(fitRafRef.current);
      fitRafRef.current = requestAnimationFrame(() => {
        fitRafRef.current = null;
        if (fabricCanvasRef.current === canvas) fitMarkCanvasView(canvas);
      });
    });
    observer.observe(scroll);
    return () => {
      observer.disconnect();
      if (fitRafRef.current != null) {
        cancelAnimationFrame(fitRafRef.current);
        fitRafRef.current = null;
      }
    };
  }, [open, activeTab, markCanvasKey, fitMarkCanvasView]);

  useEffect(() => {
    if (!fabricCanvasRef.current) return undefined;
    if (toolApplyRafRef.current != null) {
      cancelAnimationFrame(toolApplyRafRef.current);
    }
    // Coalesce rapid color/width slider updates onto one frame.
    toolApplyRafRef.current = requestAnimationFrame(() => {
      toolApplyRafRef.current = null;
      if (fabricCanvasRef.current) {
        applyMarkTool(fabricCanvasRef.current, markTool);
      }
    });
    return () => {
      if (toolApplyRafRef.current != null) {
        cancelAnimationFrame(toolApplyRafRef.current);
        toolApplyRafRef.current = null;
      }
    };
  }, [markTool, markColor, markWidth, applyMarkTool]);

  if (!open) return null;

  const useCrossOrigin =
    typeof imageSrc === "string" &&
    /^https?:\/\//i.test(imageSrc);

  const hint =
    activeTab === "crop"
      ? "Crop mode: drag the crop box; use rotate, flip, zoom, and aspect. Switch to Mark to annotate the cropped image."
      : "Mark mode: annotate the cropped image. What you see here is what will appear in the document slot.";

  const markTools = [
    ["draw", "Pen"],
    ["highlight", "Highlight"],
    ["eraser", "Eraser"],
    ["line", "Line"],
    ["arrow", "Arrow"],
    ["rect", "Box highlight"],
    ["rect-out", "Box outline"],
    ["ellipse", "Ellipse"],
    ["circle", "Circle"],
    ["text", "Text"],
  ];

  const stageToneClass = grayscale ? " editor-stage--grayscale-preview" : "";

  return (
    <div
      className="image-editor-modal"
      role="dialog"
      aria-modal="true"
      aria-labelledby="editor-title"
    >
      <div className="image-editor-modal__backdrop" onClick={handleClose} />
      <div className="image-editor-modal__card">
        <h2 id="editor-title">Edit image</h2>
        {slotIndex != null ? (
          <p className="swi-muted small image-editor-modal__slot">
            Slot {slotIndex + 1}
          </p>
        ) : null}

        <div className="editor-mode-tabs">
          <button
            type="button"
            className={`tab-btn${activeTab === "crop" ? " active" : ""}`}
            onClick={() => {
              if (activeTab !== "crop") enterCropFromMark();
            }}
          >
            Crop &amp; transform
          </button>
          <button
            type="button"
            className={`tab-btn${activeTab === "mark" ? " active" : ""}`}
            onClick={() => {
              if (activeTab !== "mark") enterMarkFromCrop();
            }}
          >
            Mark on image
          </button>
        </div>

        <p className="swi-muted small editor-hint">{hint}</p>

        <div
          className={`editor-stage${
            activeTab === "crop" ? "" : " editor-stage--hidden"
          }${stageToneClass}`}
        >
          {imageLoadError ? (
            <p className="editor-stage__error">
              Could not load this image. Choose the file again or pick a
              different image.
            </p>
          ) : (
            <img
              key={imageSrc}
              ref={editorImgRef}
              src={imageSrc || undefined}
              alt="Crop preview"
              crossOrigin={useCrossOrigin ? "anonymous" : undefined}
            />
          )}
        </div>
        <div
          className={`editor-stage${
            activeTab === "mark" ? "" : " editor-stage--hidden"
          }${stageToneClass}`}
        >
          <div className="mark-canvas-scroll" ref={markScrollRef}>
            <canvas ref={markCanvasRef} />
          </div>
        </div>

        {activeTab === "crop" ? (
          <div className="editor-toolbar">
            <button type="button" className="swi-btn" onClick={() => handleCropAction("rotate-left")}>
              Rotate −90°
            </button>
            <button type="button" className="swi-btn" onClick={() => handleCropAction("rotate-right")}>
              Rotate +90°
            </button>
            <button type="button" className="swi-btn" onClick={() => handleCropAction("flip-h")}>
              Flip H
            </button>
            <button type="button" className="swi-btn" onClick={() => handleCropAction("flip-v")}>
              Flip V
            </button>
            <button type="button" className="swi-btn" onClick={() => handleCropAction("zoom-in")}>
              Zoom in
            </button>
            <button type="button" className="swi-btn" onClick={() => handleCropAction("zoom-out")}>
              Zoom out
            </button>
            <button type="button" className="swi-btn" onClick={() => handleCropAction("reset")}>
              Reset
            </button>
            <span className="toolbar-sep" />
            <span className="swi-muted small">Aspect:</span>
            <button type="button" className="swi-btn" onClick={() => handleAspect("NaN")}>
              Free
            </button>
            <button type="button" className="swi-btn" onClick={() => handleAspect("1")}>
              1:1
            </button>
            <button type="button" className="swi-btn" onClick={() => handleAspect("1.333")}>
              4:3
            </button>
          </div>
        ) : (
          <div className="editor-toolbar editor-toolbar-mark">
            <button type="button" className="swi-btn" onClick={() => handleMarkZoom("zoom-in")}>
              Zoom in
            </button>
            <button type="button" className="swi-btn" onClick={() => handleMarkZoom("zoom-out")}>
              Zoom out
            </button>
            <button type="button" className="swi-btn" onClick={() => handleMarkZoom("reset")}>
              Reset zoom
            </button>
            <span className="toolbar-sep" />
            <label className="toolbar-inline">
              Color
              <input
                type="color"
                value={markColor}
                onChange={(e) => setMarkColor(e.target.value)}
              />
            </label>
            <label className="toolbar-inline">
              Width
              <input
                type="range"
                min="1"
                max="28"
                value={markWidth}
                onChange={(e) => setMarkWidth(parseInt(e.target.value, 10) || 4)}
              />
            </label>
            <span className="toolbar-sep" />
            {markTools.map(([tool, label]) => (
              <button
                key={tool}
                type="button"
                className={`swi-btn mark-tool-btn${markTool === tool ? " active" : ""}`}
                onClick={() => setMarkTool(tool)}
              >
                {label}
              </button>
            ))}
            <span className="toolbar-sep" />
            <button type="button" className="swi-btn" onClick={handleUndoMark}>
              Undo
            </button>
            <button type="button" className="swi-btn" onClick={handleClearMarks}>
              Clear marks
            </button>
          </div>
        )}

        <div className="editor-output-options">
          <label className="checkbox checkbox--editor">
            <input
              type="checkbox"
              checked={grayscale}
              onChange={(e) => setGrayscale(e.target.checked)}
            />
            <span>Grayscale preview (saved as gray when you apply)</span>
          </label>
        </div>

        <div className="image-editor-modal__actions">
          <button
            type="button"
            className="swi-btn"
            onClick={handleClose}
            disabled={applying}
          >
            Cancel
          </button>
          <button
            type="button"
            className="swi-btn swi-btn--primary"
            onClick={handleApply}
            disabled={applying || (imageLoadError && activeTab === "crop")}
          >
            {applying ? "Applying…" : "Apply to slot"}
          </button>
        </div>
      </div>
    </div>
  );
}
