import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { LabelTemplate, TemplateElement } from '../types';
import { mmToPx, MM_TO_PX, shouldRenderElement, centerElementOnLabel } from '../utils';
import { getSelectionBounds } from '../elementAlign';
import type { ElementClipboardActions } from '../hooks/useElementClipboard';
import {
  createDefaultTableElement,
  getTableColWidths,
  getTableRowHeights,
  iterTableCells,
  toggleTableCellSelection,
  scaleTableTracks,
  getCellAnchor,
  getCellRectPx,
  type TableCellCoord,
} from '../tableUtils';
import { useIsNarrowScreen } from '../hooks/useIsMobile';
import { CanvasLabelImage } from './CanvasLabelImage';
import { ElementFloatPreview, getFloatPreviewPaddingMm, renderElementFloatPreview } from './ElementFloatPreview';
import { renderLabelToDataUrl } from '../labelRenderer';
import { useAppDialog } from './AppDialog';
import { MobileMainToolButtons, type MobileToolButtonItem } from './MobileMainToolButtons';
import {
  ZoomIn,
  ZoomOut,
  Type,
  Barcode,
  QrCode,
  Image as ImageIcon,
  Move,
  Trash2,
  Table2,
} from 'lucide-react';

interface LabelDesignerProps {
  template: LabelTemplate;
  onChange: (updated: LabelTemplate) => void;
  selectedElementIds: string[];
  onSelectElements: (ids: string[]) => void;
  selectedTableCells?: TableCellCoord[];
  onSelectTableCells?: (cells: TableCellCoord[] | ((prev: TableCellCoord[]) => TableCellCoord[])) => void;
  variant?: 'desktop' | 'mobile';
  /** 微调等场景下隐藏选区框与缩放手柄 */
  suppressSelectionChrome?: boolean;
  elementClipboard?: Pick<
    ElementClipboardActions,
    'copySelectedElements' | 'pasteClipboardElements' | 'canCopy' | 'canPaste' | 'clipboardCount'
  >;
}

type ActiveTool = 'move' | 'text' | 'barcode' | 'qrcode' | 'table';
type ResizeCorner = 'nw' | 'ne' | 'sw' | 'se';

const MIN_ELEM_W = 2;
const MIN_ELEM_H = 2;

function applyCornerResize(
  startX: number,
  startY: number,
  startW: number,
  startH: number,
  deltaXMm: number,
  deltaYMm: number,
  corner: ResizeCorner
) {
  let x = startX;
  let y = startY;
  let w = startW;
  let h = startH;

  if (corner.includes('e')) w = startW + deltaXMm;
  if (corner.includes('w')) {
    w = startW - deltaXMm;
    x = startX + deltaXMm;
  }
  if (corner.includes('s')) h = startH + deltaYMm;
  if (corner.includes('n')) {
    h = startH - deltaYMm;
    y = startY + deltaYMm;
  }

  if (w < MIN_ELEM_W) {
    if (corner.includes('w')) x += w - MIN_ELEM_W;
    w = MIN_ELEM_W;
  }
  if (h < MIN_ELEM_H) {
    if (corner.includes('n')) y += h - MIN_ELEM_H;
    h = MIN_ELEM_H;
  }

  return {
    x: Math.round(x * 10) / 10,
    y: Math.round(y * 10) / 10,
    w: Math.round(w * 10) / 10,
    h: Math.round(h * 10) / 10,
  };
}

const MARQUEE_MIN_PX = 4;
const DRAG_START_THRESHOLD_PX = 4;
/** 松手前极短时间内的微小位移视为抖动，提交上一稳定位置 */
const DRAG_RELEASE_JITTER_PX = 4;
const DRAG_RELEASE_JITTER_MS = 48;
const MODERATE_ZOOM_MAX = 2.0;

function blurActiveField() {
  const active = document.activeElement;
  if (
    active instanceof HTMLInputElement ||
    active instanceof HTMLTextAreaElement ||
    active instanceof HTMLSelectElement
  ) {
    active.blur();
  }
}

/** 在容器内等比约束画布显示尺寸，避免宽/高单独触顶导致变形 */
function constrainCanvasDisplaySize(
  widthMm: number,
  heightMm: number,
  zoom: number,
  maxW: number,
  maxH: number
) {
  let displayW = mmToPx(widthMm, zoom);
  let displayH = mmToPx(heightMm, zoom);

  if (displayW > maxW) {
    displayW = maxW;
    const cappedZoom = displayW / (widthMm * MM_TO_PX);
    displayH = mmToPx(heightMm, cappedZoom);
  }
  if (displayH > maxH) {
    displayH = maxH;
    const cappedZoom = displayH / (heightMm * MM_TO_PX);
    displayW = mmToPx(widthMm, cappedZoom);
  }

  const displayZoom = displayW / (widthMm * MM_TO_PX);
  const canvasRenderScale = Math.max(
    1,
    Math.round(MM_TO_PX * displayZoom * 100) / 100
  );

  return { displayW, displayH, displayZoom, canvasRenderScale };
}

const ZOOM_MIN = 0.5;
const ZOOM_MAX = 5;

function getMaxZoomForContainer(
  widthMm: number,
  heightMm: number,
  maxW: number,
  maxH: number
) {
  if (maxW <= 0 || maxH <= 0) return ZOOM_MAX;
  const byW = maxW / (widthMm * MM_TO_PX);
  const byH = maxH / (heightMm * MM_TO_PX);
  return Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, byW, byH));
}

function clampZoomValue(value: number, maxZoom: number) {
  return Math.min(maxZoom, Math.max(ZOOM_MIN, Math.round(value * 100) / 100));
}

function rectFullyContains(
  outer: { left: number; top: number; right: number; bottom: number },
  inner: { left: number; top: number; right: number; bottom: number }
) {
  return (
    outer.left <= inner.left &&
    outer.top <= inner.top &&
    outer.right >= inner.right &&
    outer.bottom >= inner.bottom
  );
}

function applyQrcodeSquareResize(
  startX: number,
  startY: number,
  startW: number,
  startH: number,
  corner: ResizeCorner,
  deltaXMm: number,
  deltaYMm: number
) {
  const rect = applyCornerResize(
    startX,
    startY,
    startW,
    startH,
    deltaXMm,
    deltaYMm,
    corner
  );
  const size = Math.max(rect.w, rect.h, MIN_ELEM_W);

  let x = startX;
  let y = startY;
  if (corner.includes('w')) x = startX + startW - size;
  if (corner.includes('n')) y = startY + startH - size;

  return {
    x: Math.round(x * 10) / 10,
    y: Math.round(y * 10) / 10,
    w: Math.round(size * 10) / 10,
    h: Math.round(size * 10) / 10,
  };
}

export const LabelDesigner: React.FC<LabelDesignerProps> = ({
  template,
  onChange,
  selectedElementIds,
  onSelectElements,
  selectedTableCells = [],
  onSelectTableCells,
  variant = 'desktop',
  suppressSelectionChrome = false,
  elementClipboard,
}) => {
  const { showConfirm } = useAppDialog();
  const selectedElementId = selectedElementIds[0] || null;
  const isMobileLayout = variant === 'mobile';
  const isNarrow = useIsNarrowScreen() || isMobileLayout;
  const [zoom, setZoom] = useState<number>(MODERATE_ZOOM_MAX);
  const [canvasAreaSize, setCanvasAreaSize] = useState({ w: 0, h: 0 });
  const [activeTool, setActiveTool] = useState<ActiveTool>('move');
  const canvasAreaRef = useRef<HTMLDivElement | null>(null);
  const canvasRef = useRef<HTMLDivElement | null>(null);
  const imageInputRef = useRef<HTMLInputElement | null>(null);

  const previewDefaults = useMemo(
    () => template.variableDefaults ?? null,
    [template.variableDefaults]
  );
  const printColorMode = template.paper?.printColorMode ?? 'cmyk';

  const showReferencePreview = useMemo(
    () =>
      !!template.previewReferenceBackground &&
      template.enablePreviewReferenceBackground !== false,
    [template.previewReferenceBackground, template.enablePreviewReferenceBackground]
  );

  const localElementsRef = useRef<TemplateElement[] | null>(null);
  /** 交互开始时的元素快照：拖动底图分层、缩放节流重绘 */
  const [interactionSnapshot, setInteractionSnapshot] = useState<TemplateElement[] | null>(
    null
  );
  /** 缩放时仅重绘当前元素的浮层内容 */
  const [interactionFloatElements, setInteractionFloatElements] = useState<
    TemplateElement[] | null
  >(null);
  const dragRafRef = useRef<number | null>(null);
  const pendingDragElementsRef = useRef<TemplateElement[] | null>(null);
  const pendingDragDeltaRef = useRef({ x: 0, y: 0 });
  type DragCommitSnapshot = {
    elements: TemplateElement[];
    delta: { x: number; y: number };
    time: number;
  };
  const dragCommitHistoryRef = useRef<DragCommitSnapshot | null>(null);
  const dragCommitHistoryPrevRef = useRef<DragCommitSnapshot | null>(null);
  const dragFloatLayerRef = useRef<HTMLDivElement | null>(null);
  const canvasWrapperRef = useRef<HTMLDivElement | null>(null);
  const interactionPortalRef = useRef<HTMLDivElement | null>(null);
  const dragFloatImgRefs = useRef<Map<string, HTMLImageElement>>(new Map());
  const dragFloatUrlsRef = useRef<Record<string, string>>({});
  const dragFloatElementsRef = useRef<TemplateElement[]>([]);
  const dragVisualActivatedRef = useRef(false);
  const dragTransformLoopRef = useRef<number | null>(null);
  const [dragFloatElements, setDragFloatElements] = useState<TemplateElement[]>([]);
  const [dragInteractionVisualActive, setDragInteractionVisualActive] = useState(false);
  const [interactionCanvasReady, setInteractionCanvasReady] = useState(false);
  const interactionFloatPaintRef = useRef(0);
  const interactionFloatTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  const [dragState, setDragState] = useState<{
    elementId: string;
    type: 'drag' | 'resize';
    resizeCorner?: ResizeCorner;
    startX: number;
    startY: number;
    startElemX: number;
    startElemY: number;
    startElemW: number;
    startElemH: number;
    startTableRowHeights?: number[];
    startTableColWidths?: number[];
    groupStartPositions?: Record<string, { x: number; y: number }>;
  } | null>(null);

  const [marqueeState, setMarqueeState] = useState<{
    startX: number;
    startY: number;
    currentX: number;
    currentY: number;
    additive: boolean;
  } | null>(null);

  const ignoreCanvasClickRef = useRef(false);
  const dragMovedRef = useRef(false);
  const tableCellPointerRef = useRef<{
    row: number;
    col: number;
    elementId: string;
    startX: number;
    startY: number;
    pointerId: number;
    dragStarted: boolean;
  } | null>(null);
  const multiTouchActiveRef = useRef(false);
  const touchPointerMapRef = useRef<Map<number, { x: number; y: number }>>(new Map());
  const marqueeRef = useRef(marqueeState);
  marqueeRef.current = marqueeState;
  const selectedIdsRef = useRef(selectedElementIds);
  selectedIdsRef.current = selectedElementIds;
  const templateElementsRef = useRef(template.elements);
  templateElementsRef.current = template.elements;
  const templateRef = useRef(template);
  templateRef.current = template;

  const addImageElement = useCallback(
    (src: string = '') => {
      const current = templateRef.current;
      const id = `image_${Date.now()}`;
      const n = current.elements.length + 1;
      const newElement: TemplateElement = {
        id,
        type: 'image',
        name: `图片 ${n}`,
        ...centerElementOnLabel(current.width, current.height, 30, 30),
        width: 30,
        height: 30,
        content: src,
        fontSize: 12,
        textAlign: 'center',
        fontWeight: 'normal',
        barcodeFormat: 'CODE128',
        showText: false,
        fontFamily: 'sans',
        backgroundColor: '#ffffff',
        backgroundOpacity: 0,
        imageFit: 'contain',
      };
      onChange({
        ...current,
        elements: [...current.elements, newElement],
      });
      onSelectElements([id]);
      setActiveTool('move');
    },
    [onChange, onSelectElements]
  );

  const addElement = useCallback(
    (type: 'text' | 'barcode' | 'qrcode' | 'table') => {
      const current = templateRef.current;
      let newElement: TemplateElement;
      const id = `${type}_${Date.now()}`;
      const n = current.elements.length + 1;

      if (type === 'table') {
        newElement = createDefaultTableElement(id, n);
      } else if (type === 'text') {
        newElement = {
          id,
          type: 'text',
          name: `文本 ${n}`,
          x: 5,
          y: 5,
          width: 30,
          height: 8,
          content: '文本内容',
          fontSize: 12,
          textAlign: 'left',
          verticalAlign: 'middle',
          wordWrap: true,
          fontWeight: 'normal',
          barcodeFormat: 'CODE128',
          showText: false,
          fontFamily: 'sans',
          backgroundColor: '#ffffff',
          backgroundOpacity: 0,
        };
      } else if (type === 'barcode') {
        newElement = {
          id,
          type: 'barcode',
          name: `条形码 ${n}`,
          x: 5,
          y: 15,
          width: 50,
          height: 12,
          content: '123456789',
          fontSize: 10,
          textAlign: 'center',
          fontWeight: 'normal',
          barcodeFormat: 'CODE128',
          showText: true,
          fontFamily: 'mono',
          backgroundColor: '#ffffff',
          backgroundOpacity: 0,
        };
      } else {
        newElement = {
          id,
          type: 'qrcode',
          name: `二维码 ${n}`,
          x: 40,
          y: 10,
          width: 20,
          height: 20,
          content: 'https://example.com',
          fontSize: 10,
          textAlign: 'center',
          fontWeight: 'normal',
          barcodeFormat: 'CODE128',
          showText: false,
          fontFamily: 'sans',
          backgroundColor: '#ffffff',
          backgroundOpacity: 0,
        };
      }

      const centered = centerElementOnLabel(
        current.width,
        current.height,
        newElement.width,
        newElement.height
      );
      newElement = { ...newElement, ...centered };

      onChange({
        ...current,
        elements: [...current.elements, newElement],
      });
      onSelectElements([id]);
      setActiveTool('move');
    },
    [onChange, onSelectElements]
  );

  const deleteSelectedElements = useCallback(async () => {
    const current = templateRef.current;
    const ids =
      selectedElementIds.length > 0
        ? selectedElementIds
        : selectedElementId
          ? [selectedElementId]
          : [];
    const deletable = ids.filter((id) => {
      const el = current.elements.find((item) => item.id === id);
      return el && !el.locked;
    });
    if (deletable.length === 0) return;

    const message =
      deletable.length === 1
        ? `确定删除「${current.elements.find((item) => item.id === deletable[0])?.name ?? '未命名'}」吗？此操作无法撤销。`
        : `确定删除选中的 ${deletable.length} 个元素吗？此操作无法撤销。`;

    const confirmed = await showConfirm(message, {
      title: '删除元素',
      confirmLabel: '删除',
      variant: 'error',
    });
    if (!confirmed) return;

    const idSet = new Set(deletable);
    const remaining = current.elements.filter((el) => !idSet.has(el.id));
    onChange({ ...templateRef.current, elements: remaining });
    onSelectElements(selectedElementIds.filter((id) => remaining.some((el) => el.id === id)));
  }, [selectedElementIds, selectedElementId, onChange, onSelectElements, showConfirm]);

  const handleToolClick = (tool: ActiveTool) => {
    if (tool === 'move') {
      setActiveTool('move');
    } else {
      addElement(tool);
    }
  };

  const handleImageToolClick = () => {
    imageInputRef.current?.click();
  };

  const handleImageFile = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file || !file.type.startsWith('image/')) {
      addImageElement('');
      e.target.value = '';
      return;
    }
    const reader = new FileReader();
    reader.onload = () => addImageElement(reader.result as string);
    reader.onerror = () => addImageElement('');
    reader.readAsDataURL(file);
    e.target.value = '';
  };

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      const activeTag = document.activeElement?.tagName.toLowerCase();
      if (activeTag === 'input' || activeTag === 'textarea' || activeTag === 'select') return;

      if (e.key === 'v' || e.key === 'V') setActiveTool('move');
      if (e.key === 't' || e.key === 'T') addElement('text');
      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'a') {
        e.preventDefault();
        setActiveTool('move');
        onSelectElements(templateRef.current.elements.filter((el) => !el.locked).map((el) => el.id));
        return;
      }
      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'c') {
        e.preventDefault();
        elementClipboard?.copySelectedElements();
        return;
      }
      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'v') {
        e.preventDefault();
        elementClipboard?.pasteClipboardElements();
        setActiveTool('move');
        return;
      }
      if (e.key === 'Delete' || e.key === 'Backspace') {
        e.preventDefault();
        void deleteSelectedElements();
        return;
      }

      const moveIds =
        selectedElementIds.length > 0 ? selectedElementIds : selectedElementId ? [selectedElementId] : [];
      if (moveIds.length === 0) return;

      const increment = e.shiftKey ? 0.5 : 0.1;
      let deltaX = 0;
      let deltaY = 0;

      if (e.key === 'ArrowUp') deltaY = -increment;
      else if (e.key === 'ArrowDown') deltaY = increment;
      else if (e.key === 'ArrowLeft') deltaX = -increment;
      else if (e.key === 'ArrowRight') deltaX = increment;
      else return;

      const idSet = new Set(moveIds);
      e.preventDefault();
      const current = templateRef.current;
      onChange({
        ...current,
        elements: current.elements.map((el) => {
          if (!idSet.has(el.id) || el.locked) return el;
          return {
            ...el,
            x: Math.round((el.x + deltaX) * 10) / 10,
            y: Math.round((el.y + deltaY) * 10) / 10,
          };
        }),
      });
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [
    selectedElementId,
    selectedElementIds,
    template,
    onChange,
    addElement,
    onSelectElements,
    deleteSelectedElements,
    elementClipboard,
  ]);

  const clearInteractionCanvasState = useCallback(() => {
    if (interactionFloatTimerRef.current !== null) {
      clearTimeout(interactionFloatTimerRef.current);
      interactionFloatTimerRef.current = null;
    }
    interactionFloatPaintRef.current = 0;
    if (dragFloatLayerRef.current) {
      dragFloatLayerRef.current.style.transform = '';
    }
    if (dragTransformLoopRef.current !== null) {
      cancelAnimationFrame(dragTransformLoopRef.current);
      dragTransformLoopRef.current = null;
    }
    setInteractionSnapshot(null);
    setInteractionFloatElements(null);
    setDragFloatElements([]);
    dragFloatElementsRef.current = [];
    dragVisualActivatedRef.current = false;
    setDragInteractionVisualActive(false);
    setInteractionCanvasReady(false);
    dragFloatImgRefs.current.clear();
    dragFloatUrlsRef.current = {};
    pendingDragDeltaRef.current = { x: 0, y: 0 };
    dragCommitHistoryRef.current = null;
    dragCommitHistoryPrevRef.current = null;
  }, []);

  const cancelActiveInteraction = useCallback(() => {
    if (dragRafRef.current !== null) {
      cancelAnimationFrame(dragRafRef.current);
      dragRafRef.current = null;
    }
    pendingDragElementsRef.current = null;
    localElementsRef.current = null;
    clearInteractionCanvasState();
    setDragState(null);
    dragMovedRef.current = false;
    setMarqueeState(null);
  }, [clearInteractionCanvasState]);

  const shouldIgnoreTouchPointer = (e: { pointerType: string; isPrimary: boolean }) => {
    if (multiTouchActiveRef.current) return true;
    if (e.pointerType === 'touch' && !e.isPrimary) return true;
    return false;
  };

  useEffect(() => {
    if (!isMobileLayout && !isNarrow) return;

    const onPointerDown = (e: PointerEvent) => {
      if (e.pointerType !== 'touch') return;
      const target = e.target as Node | null;
      if (!canvasAreaRef.current?.contains(target)) return;

      touchPointerMapRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });

      if (touchPointerMapRef.current.size > 1) {
        multiTouchActiveRef.current = true;
        cancelActiveInteraction();
      }
    };

    const onPointerUp = (e: PointerEvent) => {
      if (e.pointerType !== 'touch') return;
      touchPointerMapRef.current.delete(e.pointerId);
      if (touchPointerMapRef.current.size <= 1) {
        multiTouchActiveRef.current = false;
      }
    };

    window.addEventListener('pointerdown', onPointerDown, true);
    window.addEventListener('pointerup', onPointerUp, true);
    window.addEventListener('pointercancel', onPointerUp, true);
    return () => {
      window.removeEventListener('pointerdown', onPointerDown, true);
      window.removeEventListener('pointerup', onPointerUp, true);
      window.removeEventListener('pointercancel', onPointerUp, true);
      touchPointerMapRef.current.clear();
      multiTouchActiveRef.current = false;
    };
  }, [isMobileLayout, isNarrow, cancelActiveInteraction]);

  const applyModerateZoom = useCallback(() => {
    const area = canvasAreaRef.current;
    if (!area) return;
    const pad = isNarrow ? 32 : 80;
    const availW = area.clientWidth - pad;
    const availH = area.clientHeight - pad;
    if (availW <= 0 || availH <= 0) return;
    const docW = mmToPx(template.width, 1);
    const docH = mmToPx(template.height, 1);
    const fitZoom = Math.min(availW / docW, availH / docH, MODERATE_ZOOM_MAX);
    setZoom(Math.max(0.5, Math.round(fitZoom * 100) / 100));
  }, [template.width, template.height, isNarrow]);

  useEffect(() => {
    applyModerateZoom();
  }, [template.id, applyModerateZoom]);

  useEffect(() => {
    const area = canvasAreaRef.current;
    if (!area) return;

    const updateSize = () => {
      setCanvasAreaSize({ w: area.clientWidth, h: area.clientHeight });
    };
    updateSize();
    const observer = new ResizeObserver(updateSize);
    observer.observe(area);
    return () => observer.disconnect();
  }, []);

  const canvasPad = 32;
  const canvasLayout = useMemo(() => {
    const maxW = canvasAreaSize.w - canvasPad;
    const maxH = canvasAreaSize.h - canvasPad;
    const maxZoom = getMaxZoomForContainer(template.width, template.height, maxW, maxH);
    if (maxW <= 0 || maxH <= 0) {
      const effectiveZoom = clampZoomValue(zoom, maxZoom);
      const displayW = mmToPx(template.width, effectiveZoom);
      const displayH = mmToPx(template.height, effectiveZoom);
      return {
        displayW,
        displayH,
        displayZoom: effectiveZoom,
        canvasRenderScale: Math.max(1, Math.round(MM_TO_PX * effectiveZoom * 100) / 100),
        maxZoom,
      };
    }
    const constrained = constrainCanvasDisplaySize(
      template.width,
      template.height,
      zoom,
      maxW,
      maxH
    );
    return { ...constrained, maxZoom };
  }, [template.width, template.height, zoom, canvasAreaSize.w, canvasAreaSize.h]);
  const {
    displayW: canvasDisplayW,
    displayH: canvasDisplayH,
    displayZoom,
    canvasRenderScale,
    maxZoom,
  } = canvasLayout;
  const displayZoomRef = useRef(displayZoom);
  displayZoomRef.current = displayZoom;
  const maxZoomRef = useRef(maxZoom);
  maxZoomRef.current = maxZoom;

  const prefetchDragFloatImages = useCallback(
    (elements: TemplateElement[]) => {
      if (elements.length === 0) return;
      void Promise.all(
        elements.map(async (el) => {
          try {
            const url = await renderElementFloatPreview(
              el,
              canvasRenderScale,
              previewDefaults
            );
            dragFloatUrlsRef.current[el.id] = url;
            const img = dragFloatImgRefs.current.get(el.id);
            if (img) {
              img.src = url;
              if (img.complete) {
                img.style.opacity = '1';
                const placeholder = img.previousElementSibling as HTMLElement | null;
                if (placeholder) placeholder.style.display = 'none';
              }
            }
          } catch (err) {
            console.error('Drag float preview error:', err);
          }
        })
      );
    },
    [canvasRenderScale, previewDefaults]
  );

  useEffect(() => {
    setZoom((z) => (z > maxZoom ? clampZoomValue(z, maxZoom) : z));
  }, [maxZoom]);

  useEffect(() => {
    const area = canvasAreaRef.current;
    if (!area) return;

    const handleWheel = (e: WheelEvent) => {
      if (e.ctrlKey || e.metaKey) {
        e.preventDefault();
        const delta = e.deltaY > 0 ? -0.25 : 0.25;
        setZoom((z) => clampZoomValue(z + delta, maxZoomRef.current));
      }
    };

    area.addEventListener('wheel', handleWheel, { passive: false });
    return () => area.removeEventListener('wheel', handleWheel);
  }, []);

  const marqueeActive = marqueeState !== null;

  useEffect(() => {
    if (!marqueeActive) return;

    const handlePointerMove = (e: PointerEvent) => {
      if (multiTouchActiveRef.current) return;
      const canvas = canvasRef.current;
      if (!canvas) return;
      const rect = canvas.getBoundingClientRect();
      const currentX = e.clientX - rect.left;
      const currentY = e.clientY - rect.top;
      setMarqueeState((prev) => (prev ? { ...prev, currentX, currentY } : null));
    };

    const handlePointerUp = () => {
      if (multiTouchActiveRef.current) {
        setMarqueeState(null);
        return;
      }
      const state = marqueeRef.current;
      setMarqueeState(null);

      if (!state) return;

      const left = Math.min(state.startX, state.currentX);
      const top = Math.min(state.startY, state.currentY);
      const right = Math.max(state.startX, state.currentX);
      const bottom = Math.max(state.startY, state.currentY);
      const isTiny =
        right - left < MARQUEE_MIN_PX && bottom - top < MARQUEE_MIN_PX;

      ignoreCanvasClickRef.current = true;

      if (isTiny) {
        if (!state.additive) {
          requestAnimationFrame(() => onSelectElements([]));
        }
        return;
      }

      const marqueeRect = { left, top, right, bottom };
      const elements = localElementsRef.current || templateElementsRef.current;
      const hitIds = elements
        .filter((el) => {
          if (el.locked) return false;
          const elRect = {
            left: mmToPx(el.x, displayZoomRef.current),
            top: mmToPx(el.y, displayZoomRef.current),
            right: mmToPx(el.x + el.width, displayZoomRef.current),
            bottom: mmToPx(el.y + el.height, displayZoomRef.current),
          };
          return rectFullyContains(marqueeRect, elRect);
        })
        .map((el) => el.id);

      if (state.additive) {
        const merged = new Set([...selectedIdsRef.current, ...hitIds]);
        onSelectElements([...merged]);
      } else {
        onSelectElements(hitIds);
      }
    };

    window.addEventListener('pointermove', handlePointerMove);
    window.addEventListener('pointerup', handlePointerUp);
    window.addEventListener('pointercancel', handlePointerUp);
    return () => {
      window.removeEventListener('pointermove', handlePointerMove);
      window.removeEventListener('pointerup', handlePointerUp);
      window.removeEventListener('pointercancel', handlePointerUp);
    };
  }, [marqueeActive, displayZoom, onSelectElements]);

  useEffect(() => {
    if (!dragState) return;
    dragMovedRef.current = false;

    const syncInteractionPortalLayout = () => {
      const wrapper = canvasWrapperRef.current;
      const portal = interactionPortalRef.current;
      if (wrapper && portal) {
        const rect = wrapper.getBoundingClientRect();
        portal.style.left = `${rect.left}px`;
        portal.style.top = `${rect.top}px`;
        portal.style.width = `${rect.width}px`;
        portal.style.height = `${rect.height}px`;
      }
    };

    const runDragTransformLoop = () => {
      const tick = () => {
        syncInteractionPortalLayout();
        const layer = dragFloatLayerRef.current;
        if (layer) {
          const { x, y } = pendingDragDeltaRef.current;
          layer.style.transform = `translate3d(${x}px, ${y}px, 0)`;
        }
        dragTransformLoopRef.current = requestAnimationFrame(tick);
      };
      dragTransformLoopRef.current = requestAnimationFrame(tick);
    };

    if (dragState.type === 'drag') {
      runDragTransformLoop();
    }

    const handlePointerMove = (e: PointerEvent) => {
      if (multiTouchActiveRef.current) return;

      const deltaX = e.clientX - dragState.startX;
      const deltaY = e.clientY - dragState.startY;

      if (dragState.type === 'drag') {
        pendingDragDeltaRef.current = { x: deltaX, y: deltaY };
      }

      const pastDragThreshold =
        Math.abs(deltaX) > DRAG_START_THRESHOLD_PX || Math.abs(deltaY) > DRAG_START_THRESHOLD_PX;

      if (dragState.type === 'drag' && !pastDragThreshold) {
        return;
      }
      if (dragState.type === 'drag' && pastDragThreshold) {
        dragMovedRef.current = true;
        if (!dragVisualActivatedRef.current) {
          dragVisualActivatedRef.current = true;
          setDragInteractionVisualActive(true);
          prefetchDragFloatImages(dragFloatElementsRef.current);
        }
      }
      const MM_PER_PX = 1 / (3.78 * displayZoomRef.current);
      const deltaXMm = deltaX * MM_PER_PX;
      const deltaYMm = deltaY * MM_PER_PX;

      if (dragState.type === 'drag') {
        const groupStarts = dragState.groupStartPositions;
        if (!groupStarts || Object.keys(groupStarts).length === 0) return;

        const updated = template.elements.map((el) => {
          const start = groupStarts[el.id];
          if (!start || el.locked) return el;
          return {
            ...el,
            x: Math.round((start.x + deltaXMm) * 10) / 10,
            y: Math.round((start.y + deltaYMm) * 10) / 10,
          };
        });
        pendingDragElementsRef.current = updated;
        dragCommitHistoryPrevRef.current = dragCommitHistoryRef.current;
        dragCommitHistoryRef.current = {
          elements: updated,
          delta: { x: deltaX, y: deltaY },
          time: performance.now(),
        };
      } else if (dragState.type === 'resize') {
        const elem = template.elements.find((el) => el.id === dragState.elementId);
        if (!elem || elem.locked) return;

        dragMovedRef.current = true;
        const corner = dragState.resizeCorner ?? 'se';
        const rect =
          elem.type === 'qrcode'
            ? applyQrcodeSquareResize(
                dragState.startElemX,
                dragState.startElemY,
                dragState.startElemW,
                dragState.startElemH,
                corner,
                deltaXMm,
                deltaYMm
              )
            : applyCornerResize(
                dragState.startElemX,
                dragState.startElemY,
                dragState.startElemW,
                dragState.startElemH,
                deltaXMm,
                deltaYMm,
                corner
              );

        const updated = template.elements.map((el) => {
          if (el.id !== dragState.elementId) return el;
          if (
            el.type === 'table' &&
            dragState.startTableRowHeights &&
            dragState.startTableColWidths
          ) {
            return scaleTableTracks(
              { ...el, x: rect.x, y: rect.y },
              rect.w,
              rect.h,
              dragState.startElemW,
              dragState.startElemH,
              dragState.startTableRowHeights,
              dragState.startTableColWidths
            );
          }
          return { ...el, x: rect.x, y: rect.y, width: rect.w, height: rect.h };
        });
        pendingDragElementsRef.current = updated;

        const paintInteractionFloat = () => {
          interactionFloatPaintRef.current = performance.now();
          interactionFloatTimerRef.current = null;
          const pending = pendingDragElementsRef.current?.find(
            (el) => el.id === dragState.elementId
          );
          if (pending) setInteractionFloatElements([pending]);
        };

        if (dragRafRef.current === null) {
          dragRafRef.current = requestAnimationFrame(() => {
            dragRafRef.current = null;
            const next = pendingDragElementsRef.current;
            if (next) {
              localElementsRef.current = next;
              const now = performance.now();
              const elapsed = now - interactionFloatPaintRef.current;
              if (elapsed >= 66 || interactionFloatPaintRef.current === 0) {
                paintInteractionFloat();
              } else if (interactionFloatTimerRef.current === null) {
                interactionFloatTimerRef.current = setTimeout(
                  paintInteractionFloat,
                  66 - elapsed
                );
              }
            }
          });
        }
      }
    };

    const resolveDragCommitElements = (): TemplateElement[] | null => {
      if (dragState.type !== 'drag') {
        return pendingDragElementsRef.current ?? localElementsRef.current;
      }
      const cur = dragCommitHistoryRef.current;
      const prev = dragCommitHistoryPrevRef.current;
      if (cur && prev) {
        const micro = Math.hypot(cur.delta.x - prev.delta.x, cur.delta.y - prev.delta.y);
        const dt = cur.time - prev.time;
        if (micro > 0 && micro <= DRAG_RELEASE_JITTER_PX && dt <= DRAG_RELEASE_JITTER_MS) {
          return prev.elements;
        }
        return cur.elements;
      }
      return cur?.elements ?? pendingDragElementsRef.current ?? localElementsRef.current;
    };

    const handlePointerUp = () => {
      if (multiTouchActiveRef.current) {
        cancelActiveInteraction();
        return;
      }
      if (dragRafRef.current !== null) {
        cancelAnimationFrame(dragRafRef.current);
        dragRafRef.current = null;
      }
      const commitElements = resolveDragCommitElements();
      if (commitElements) {
        localElementsRef.current = commitElements;
      }
      pendingDragElementsRef.current = null;
      if (localElementsRef.current) {
        const shouldCommit =
          dragState.type === 'resize' || dragMovedRef.current;
        if (shouldCommit) {
          onChange({ ...templateRef.current, elements: localElementsRef.current });
        }
      }
      localElementsRef.current = null;
      clearInteractionCanvasState();
      setDragState(null);
    };

    window.addEventListener('pointermove', handlePointerMove);
    window.addEventListener('pointerup', handlePointerUp);
    window.addEventListener('pointercancel', handlePointerUp);
    return () => {
      if (dragTransformLoopRef.current !== null) {
        cancelAnimationFrame(dragTransformLoopRef.current);
        dragTransformLoopRef.current = null;
      }
      window.removeEventListener('pointermove', handlePointerMove);
      window.removeEventListener('pointerup', handlePointerUp);
      window.removeEventListener('pointercancel', handlePointerUp);
    };
  }, [dragState, template, displayZoom, onChange, cancelActiveInteraction, clearInteractionCanvasState, prefetchDragFloatImages]);

  const buildGroupStartPositions = (element: TemplateElement) => {
    const moveIds =
      selectedElementIds.includes(element.id) && selectedElementIds.length > 1
        ? selectedElementIds
        : [element.id];
    const groupStartPositions: Record<string, { x: number; y: number }> = {};
    for (const id of moveIds) {
      const el = template.elements.find((item) => item.id === id);
      if (el && !el.locked) {
        groupStartPositions[id] = { x: el.x, y: el.y };
      }
    }
    return groupStartPositions;
  };

  const startMarquee = (e: React.PointerEvent) => {
    if (activeTool !== 'move' || e.button !== 0) return;
    if (shouldIgnoreTouchPointer(e)) return;

    const canvas = canvasRef.current;
    if (!canvas) return;

    blurActiveField();

    const rect = canvas.getBoundingClientRect();
    const startX = e.clientX - rect.left;
    const startY = e.clientY - rect.top;

    e.preventDefault();
    e.stopPropagation();
    e.currentTarget.setPointerCapture(e.pointerId);

    setMarqueeState({
      startX,
      startY,
      currentX: startX,
      currentY: startY,
      additive: e.shiftKey || e.ctrlKey || e.metaKey,
    });
  };

  const handleWorkspacePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
    if (activeTool !== 'move' || e.button !== 0) return;
    if (shouldIgnoreTouchPointer(e)) return;
    const target = e.target as HTMLElement;
    if (target.closest('[data-label-element]') || target.closest('.ps-handle')) return;

    startMarquee(e);
  };

  const handleElementPointerDown = (
    e: React.PointerEvent,
    element: TemplateElement,
    actionType: 'drag' | 'resize'
  ) => {
    if (e.button !== 0) return;
    if (shouldIgnoreTouchPointer(e)) return;
    e.stopPropagation();

    if (e.shiftKey || e.ctrlKey || e.metaKey) {
      return;
    }

    handleDragStart(e, element, actionType);
  };

  const handleElementClick = (e: React.MouseEvent, element: TemplateElement) => {
    e.stopPropagation();
    if (dragMovedRef.current) {
      dragMovedRef.current = false;
      return;
    }

    const isMulti = e.shiftKey || e.ctrlKey || e.metaKey;
    if (isMulti) {
      if (selectedElementIds.includes(element.id)) {
        onSelectElements(selectedElementIds.filter((x) => x !== element.id));
      } else {
        onSelectElements([...selectedElementIds, element.id]);
      }
    } else {
      onSelectElements([element.id]);
    }
  };

  const handleDragStart = (
    e: React.PointerEvent,
    element: TemplateElement,
    actionType: 'drag' | 'resize'
  ) => {
    if (shouldIgnoreTouchPointer(e)) return;
    e.preventDefault();
    if (!selectedElementIds.includes(element.id)) {
      onSelectElements([element.id]);
    }
    if (element.locked) return;

    e.currentTarget.setPointerCapture(e.pointerId);
    const groupStartPositions = buildGroupStartPositions(element);
    const moveIds = new Set(Object.keys(groupStartPositions));
    const floating = template.elements.filter(
      (el) =>
        moveIds.has(el.id) &&
        shouldRenderElement(el, null, 0, previewDefaults, 'design')
    );

    setInteractionSnapshot(template.elements);
    setInteractionFloatElements(null);
    interactionFloatPaintRef.current = 0;
    dragFloatUrlsRef.current = {};
    dragFloatImgRefs.current.clear();
    dragCommitHistoryRef.current = null;
    dragCommitHistoryPrevRef.current = null;
    dragVisualActivatedRef.current = false;
    setDragInteractionVisualActive(false);
    setInteractionCanvasReady(false);
    dragFloatElementsRef.current = floating;
    setDragFloatElements(floating);
    setDragState({
      elementId: element.id,
      type: actionType,
      startX: e.clientX,
      startY: e.clientY,
      startElemX: element.x,
      startElemY: element.y,
      startElemW: element.width,
      startElemH: element.height,
      groupStartPositions,
    });
  };

  const handleTableCellPointerDown = (
    e: React.PointerEvent,
    element: TemplateElement,
    row: number,
    col: number
  ) => {
    if (e.button !== 0 || shouldIgnoreTouchPointer(e) || element.locked) return;
    e.stopPropagation();
    e.currentTarget.setPointerCapture(e.pointerId);
    tableCellPointerRef.current = {
      row,
      col,
      elementId: element.id,
      startX: e.clientX,
      startY: e.clientY,
      pointerId: e.pointerId,
      dragStarted: false,
    };
  };

  const handleTableCellPointerMove = (
    e: React.PointerEvent,
    element: TemplateElement
  ) => {
    const pending = tableCellPointerRef.current;
    if (!pending || pending.pointerId !== e.pointerId || pending.elementId !== element.id) {
      return;
    }
    if (pending.dragStarted || element.locked) return;

    const dx = e.clientX - pending.startX;
    const dy = e.clientY - pending.startY;
    if (
      Math.abs(dx) > DRAG_START_THRESHOLD_PX ||
      Math.abs(dy) > DRAG_START_THRESHOLD_PX
    ) {
      pending.dragStarted = true;
      dragMovedRef.current = true;
      tableCellPointerRef.current = null;
      try {
        (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
      } catch {
        /* ignore */
      }
      handleDragStart(e, element, 'drag');
    }
  };

  const handleTableCellPointerUp = (e: React.PointerEvent) => {
    const pending = tableCellPointerRef.current;
    if (!pending || pending.pointerId !== e.pointerId) return;

    if (!pending.dragStarted && onSelectTableCells) {
      onSelectTableCells((prev) =>
        toggleTableCellSelection(prev, { row: pending.row, col: pending.col })
      );
    }
    tableCellPointerRef.current = null;
    try {
      (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
    } catch {
      /* ignore */
    }
  };

  const handleResizeStart = (
    e: React.PointerEvent,
    element: TemplateElement,
    corner: ResizeCorner
  ) => {
    if (shouldIgnoreTouchPointer(e)) return;
    e.stopPropagation();
    e.preventDefault();
    onSelectElements([element.id]);
    if (element.locked) return;

    e.currentTarget.setPointerCapture(e.pointerId);
    setInteractionSnapshot(template.elements);
    setInteractionFloatElements([element]);
    interactionFloatPaintRef.current = 0;
    setInteractionCanvasReady(false);
    setDragState({
      elementId: element.id,
      type: 'resize',
      resizeCorner: corner,
      startX: e.clientX,
      startY: e.clientY,
      startElemX: element.x,
      startElemY: element.y,
      startElemW: element.width,
      startElemH: element.height,
      startTableRowHeights:
        element.type === 'table' ? getTableRowHeights(element) : undefined,
      startTableColWidths:
        element.type === 'table' ? getTableColWidths(element) : undefined,
    });
  };

  const fitToView = () => {
    const area = canvasAreaRef.current;
    if (!area) return;
    const pad = isMobileLayout ? 32 : 80;
    const availW = area.clientWidth - pad;
    const availH = area.clientHeight - pad;
    const docW = mmToPx(template.width, 1);
    const docH = mmToPx(template.height, 1);
    const fitZoom = Math.min(availW / docW, availH / docH, 5);
    setZoom(Math.max(0.5, Math.round(fitZoom * 100) / 100));
  };

  useEffect(() => {
    if (!isMobileLayout) return;
    const timer = window.setTimeout(() => fitToView(), 0);
    return () => window.clearTimeout(timer);
  }, [isMobileLayout, template.width, template.height]);

  const selectedElemObj = template.elements.find((el) => el.id === selectedElementId);

  const filterVisibleElements = useCallback(
    (elements: TemplateElement[]) =>
      elements.filter((el) =>
        shouldRenderElement(el, null, 0, previewDefaults, 'design')
      ),
    [previewDefaults]
  );

  const dragMovingIdSet = useMemo(() => {
    if (dragState?.type !== 'drag' || !dragState.groupStartPositions) return null;
    return new Set(Object.keys(dragState.groupStartPositions));
  }, [dragState]);

  const liftSelectedAboveDim = activeTool === 'move' && selectedElementIds.length > 0;

  const staticCanvasElements = useMemo(() => {
    let elements: TemplateElement[];
    if (
      dragState?.type === 'drag' &&
      interactionSnapshot &&
      dragMovingIdSet &&
      dragInteractionVisualActive &&
      interactionCanvasReady
    ) {
      elements = filterVisibleElements(
        interactionSnapshot.filter((el) => !dragMovingIdSet.has(el.id))
      );
    } else if (dragState?.type === 'resize' && interactionSnapshot && interactionCanvasReady) {
      elements = filterVisibleElements(
        interactionSnapshot.filter((el) => el.id !== dragState.elementId)
      );
    } else {
      elements = filterVisibleElements(template.elements);
    }

    if (liftSelectedAboveDim) {
      const selectedSet = new Set(selectedElementIds);
      elements = elements.filter((el) => !selectedSet.has(el.id));
    }

    return elements;
  }, [
    dragState?.type,
    dragState?.elementId,
    interactionSnapshot,
    dragMovingIdSet,
    dragInteractionVisualActive,
    interactionCanvasReady,
    template.elements,
    filterVisibleElements,
    liftSelectedAboveDim,
    selectedElementIds,
  ]);

  const selectedLiftElements = useMemo(() => {
    if (!liftSelectedAboveDim) return null;

    let elements = filterVisibleElements(
      template.elements.filter((el) => selectedElementIds.includes(el.id))
    );

    if (dragState?.type === 'drag' && dragInteractionVisualActive && dragMovingIdSet) {
      elements = elements.filter((el) => !dragMovingIdSet.has(el.id));
    }
    if (dragState?.type === 'resize' && interactionCanvasReady && dragState.elementId) {
      elements = elements.filter((el) => el.id !== dragState.elementId);
    }

    return elements.length > 0 ? elements : null;
  }, [
    liftSelectedAboveDim,
    selectedElementIds,
    template.elements,
    filterVisibleElements,
    dragState?.type,
    dragState?.elementId,
    dragInteractionVisualActive,
    dragMovingIdSet,
    interactionCanvasReady,
  ]);

  const resizeFloatElements = useMemo(() => {
    if (dragState?.type !== 'resize' || !interactionFloatElements?.length) return null;
    return filterVisibleElements(interactionFloatElements);
  }, [dragState?.type, interactionFloatElements, filterVisibleElements]);

  const allowInteractionOverflow = dragState !== null;

  const syncInteractionPortalLayout = useCallback(() => {
    const wrapper = canvasWrapperRef.current;
    const portal = interactionPortalRef.current;
    if (!wrapper || !portal) return;
    const rect = wrapper.getBoundingClientRect();
    portal.style.left = `${rect.left}px`;
    portal.style.top = `${rect.top}px`;
    portal.style.width = `${rect.width}px`;
    portal.style.height = `${rect.height}px`;
  }, []);

  useEffect(() => {
    if (!allowInteractionOverflow) return;
    syncInteractionPortalLayout();
    window.addEventListener('scroll', syncInteractionPortalLayout, true);
    window.addEventListener('resize', syncInteractionPortalLayout);
    return () => {
      window.removeEventListener('scroll', syncInteractionPortalLayout, true);
      window.removeEventListener('resize', syncInteractionPortalLayout);
    };
  }, [allowInteractionOverflow, syncInteractionPortalLayout]);

  useEffect(() => {
    const shouldPrepareCanvas =
      (dragState?.type === 'drag' && dragInteractionVisualActive) ||
      dragState?.type === 'resize';
    if (!shouldPrepareCanvas) {
      setInteractionCanvasReady(false);
      return;
    }
    setInteractionCanvasReady(false);
    let cancelled = false;
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        if (!cancelled) setInteractionCanvasReady(true);
      });
    });
    return () => {
      cancelled = true;
    };
  }, [dragState, dragInteractionVisualActive]);

  const hideSelectionChrome =
    suppressSelectionChrome || (dragState?.type === 'drag' && dragInteractionVisualActive);
  const showUnselectedDimLayer = activeTool === 'move';
  const showUnselectedDim = showUnselectedDimLayer && selectedElementIds.length > 0;

  const multiSelectionBounds = useMemo(() => {
    if (selectedElementIds.length < 2 || hideSelectionChrome) return null;
    const selected = template.elements.filter(
      (el) => selectedElementIds.includes(el.id) && !el.locked
    );
    if (selected.length < 2) return null;
    return getSelectionBounds(selected);
  }, [selectedElementIds, template.elements, hideSelectionChrome]);

  const dragMultiSelectionBounds = useMemo(() => {
    if (
      selectedElementIds.length < 2 ||
      dragState?.type !== 'drag' ||
      !dragInteractionVisualActive ||
      dragFloatElements.length < 2
    ) {
      return null;
    }
    return getSelectionBounds(dragFloatElements);
  }, [
    selectedElementIds.length,
    dragState?.type,
    dragInteractionVisualActive,
    dragFloatElements,
  ]);

  const resizingPreviewElement =
    dragState?.type === 'resize'
      ? interactionFloatElements?.find((el) => el.id === dragState.elementId)
      : null;

  const overlayElements = filterVisibleElements(template.elements);

  const tools: { id: ActiveTool; icon: React.ReactNode; label: string; shortcut?: string }[] = [
    { id: 'move', icon: <Move className="w-[18px] h-[18px]" />, label: '移动', shortcut: 'V' },
    { id: 'text', icon: <Type className="w-[18px] h-[18px]" />, label: '文本', shortcut: 'T' },
    { id: 'table', icon: <Table2 className="w-[18px] h-[18px]" />, label: '表格' },
    { id: 'barcode', icon: <Barcode className="w-[18px] h-[18px]" />, label: '条形码' },
    { id: 'qrcode', icon: <QrCode className="w-[18px] h-[18px]" />, label: '二维码' },
  ];

  const mobileToolItems = useMemo<MobileToolButtonItem[]>(
    () => [
      ...tools.map((tool) => ({
        id: tool.id,
        icon: tool.icon,
        label: tool.label,
        shortcut: tool.shortcut,
        active: activeTool === tool.id,
        onClick: () => handleToolClick(tool.id),
      })),
      {
        id: 'image',
        icon: <ImageIcon className="w-[18px] h-[18px]" />,
        label: '图片',
        active: false,
        onClick: handleImageToolClick,
      },
    ],
    // eslint-disable-next-line react-hooks/exhaustive-deps -- tools 为静态配置
    [activeTool, handleToolClick, handleImageToolClick]
  );

  const mainToolButtons = (
    <>
      {tools.map((tool) => (
        <button
          key={tool.id}
          className={`ps-tool-btn ${activeTool === tool.id ? 'active' : ''} ${isMobileLayout ? 'mobile-design-tool-btn' : ''}`}
          onClick={() => handleToolClick(tool.id)}
          title={`${tool.label}${tool.shortcut ? ` (${tool.shortcut})` : ''}`}
        >
          {tool.icon}
        </button>
      ))}
      <button
        className={`ps-tool-btn ${isMobileLayout ? 'mobile-design-tool-btn' : ''}`}
        onClick={handleImageToolClick}
        title="图片工具"
      >
        <ImageIcon className="w-[18px] h-[18px]" />
      </button>
      <input
        ref={imageInputRef}
        type="file"
        accept="image/*"
        className="hidden"
        onChange={handleImageFile}
      />
    </>
  );

  const deleteToolButton =
    selectedElementIds.length > 0 ? (
      <button
        className={`ps-tool-btn hover:!bg-red-900/40 hover:!text-red-400 ${isMobileLayout ? 'mobile-design-tool-btn' : ''}`}
        onClick={() => void deleteSelectedElements()}
        title="删除选中元素"
      >
        <Trash2 className="w-[18px] h-[18px]" />
      </button>
    ) : null;

  const zoomControls = (
    <div className="flex items-center gap-1.5 shrink-0">
      <div className="ps-zoom-control">
        <button
          className="ps-zoom-btn"
          onClick={() => setZoom((z) => clampZoomValue(z - 0.25, maxZoom))}
          title="缩小"
        >
          <ZoomOut className="w-3 h-3" />
        </button>
        <span className="px-1 text-[10px] font-mono text-[#ccc] min-w-[32px] text-center select-none">
          {Math.round(displayZoom * 100)}%
        </span>
        <button
          className="ps-zoom-btn"
          onClick={() => setZoom((z) => clampZoomValue(z + 0.25, maxZoom))}
          disabled={displayZoom >= maxZoom - 0.001}
          title="放大"
        >
          <ZoomIn className="w-3 h-3" />
        </button>
      </div>
      <button
        type="button"
        onClick={fitToView}
        className="h-[24px] text-[10px] text-[#aaa] hover:text-white px-1.5 py-0.5 border border-[#4a4a4a] hover:border-[#666] transition cursor-pointer"
        title="适合窗口"
      >
        适合
      </button>
    </div>
  );

  const showInteractionFloatPortal =
    typeof document !== 'undefined' &&
    ((dragState?.type === 'drag' &&
      dragInteractionVisualActive &&
      dragFloatElements.length > 0) ||
      (resizeFloatElements && resizeFloatElements.length > 0));

  const interactionFloatPortal = showInteractionFloatPortal
    ? createPortal(
        <div
          ref={interactionPortalRef}
          className="pointer-events-none"
          style={{
            position: 'fixed',
            left: 0,
            top: 0,
            width: 0,
            height: 0,
            overflow: 'visible',
            zIndex: 10000,
          }}
        >
          {dragState?.type === 'drag' && dragFloatElements.length > 0 && (
            <div
              ref={dragFloatLayerRef}
              className="absolute inset-0 pointer-events-none"
              style={{
                overflow: 'visible',
                willChange: 'transform',
              }}
            >
              {dragFloatElements.map((el) => {
                const padMm = getFloatPreviewPaddingMm(el);
                const padPx = mmToPx(padMm, displayZoom);
                const boxW = mmToPx(el.width, displayZoom);
                const boxH = mmToPx(el.height, displayZoom);
                return (
                <div
                  key={el.id}
                  className="absolute pointer-events-none"
                  style={{
                    left: `${mmToPx(el.x, displayZoom)}px`,
                    top: `${mmToPx(el.y, displayZoom)}px`,
                    width: `${boxW}px`,
                    height: `${boxH}px`,
                    transform: el.rotation ? `rotate(${el.rotation}deg)` : undefined,
                    transformOrigin: 'center center',
                  }}
                >
                  <div
                    data-drag-float-placeholder={el.id}
                    className="absolute inset-0 bg-white shadow-[0_1px_4px_rgba(0,0,0,0.12)]"
                  />
                  <img
                    ref={(node) => {
                      if (node) {
                        dragFloatImgRefs.current.set(el.id, node);
                        const url = dragFloatUrlsRef.current[el.id];
                        if (url) {
                          node.src = url;
                          if (node.complete) {
                            node.style.opacity = '1';
                            const placeholder = node.previousElementSibling as HTMLElement | null;
                            if (placeholder) placeholder.style.display = 'none';
                          }
                        }
                      } else {
                        dragFloatImgRefs.current.delete(el.id);
                      }
                    }}
                    alt=""
                    className="absolute pointer-events-none select-none opacity-0"
                    style={{
                      left: `${-padPx}px`,
                      top: `${-padPx}px`,
                      width: `${boxW + padPx * 2}px`,
                      height: `${boxH + padPx * 2}px`,
                    }}
                    onLoad={(e) => {
                      const img = e.currentTarget;
                      img.style.opacity = '1';
                      const placeholder = img.previousElementSibling as HTMLElement | null;
                      if (placeholder) placeholder.style.display = 'none';
                    }}
                  />
                  {selectedElementIds.length === 1 && (
                    <div
                      className="absolute inset-0 ps-resize-outline pointer-events-none z-[3]"
                      aria-hidden
                    />
                  )}
                </div>
              );
              })}
              {dragMultiSelectionBounds && (
                <div
                  className="absolute ps-resize-outline pointer-events-none z-[5]"
                  style={{
                    left: `${mmToPx(dragMultiSelectionBounds.minX, displayZoom)}px`,
                    top: `${mmToPx(dragMultiSelectionBounds.minY, displayZoom)}px`,
                    width: `${mmToPx(
                      dragMultiSelectionBounds.maxX - dragMultiSelectionBounds.minX,
                      displayZoom
                    )}px`,
                    height: `${mmToPx(
                      dragMultiSelectionBounds.maxY - dragMultiSelectionBounds.minY,
                      displayZoom
                    )}px`,
                  }}
                  aria-hidden
                />
              )}
            </div>
          )}
          {resizeFloatElements && resizeFloatElements.length > 0 && (
            <div
              className="absolute inset-0 pointer-events-none"
              style={{ overflow: 'visible' }}
            >
              {resizeFloatElements.map((el) => {
                const left = mmToPx(el.x, displayZoom);
                const top = mmToPx(el.y, displayZoom);
                const width = mmToPx(el.width, displayZoom);
                const height = mmToPx(el.height, displayZoom);
                return (
                  <div
                    key={el.id}
                    className="absolute pointer-events-none"
                    style={{
                      left: `${left}px`,
                      top: `${top}px`,
                      width: `${width}px`,
                      height: `${height}px`,
                      transform: el.rotation ? `rotate(${el.rotation}deg)` : undefined,
                      transformOrigin: 'center center',
                    }}
                  >
                    <ElementFloatPreview
                      element={el}
                      renderScale={canvasRenderScale}
                      displayZoom={displayZoom}
                      variableDefaults={previewDefaults}
                    />
                    <div className="absolute inset-0 ps-resize-outline z-[2]" aria-hidden />
                  </div>
                );
              })}
            </div>
          )}
        </div>,
        document.body
      )
    : null;

  return (
    <>
    <div
      className={
        isMobileLayout
          ? `mobile-design-canvas-shell flex flex-col flex-1 min-w-0 min-h-0${
              allowInteractionOverflow ? ' mobile-design-canvas-shell-interaction' : ''
            }`
          : 'flex flex-1 min-w-0 min-h-0'
      }
      data-interaction-active={allowInteractionOverflow ? '' : undefined}
    >
      {!isMobileLayout && (
        <div className="ps-toolbar flex flex-col">
          {mainToolButtons}
          <div className="flex-1" />
          {deleteToolButton}
        </div>
      )}

      <div className="flex flex-col flex-1 min-w-0 min-h-0">
        <div
          ref={canvasAreaRef}
          className={`flex-1 ps-workspace-bg ps-design-canvas-touch flex items-center justify-center min-h-0 min-w-0 p-4 ${
            allowInteractionOverflow ? 'overflow-visible' : 'overflow-auto scrollbar-hide overscroll-contain'
          }`}
          onPointerDown={handleWorkspacePointerDown}
          onClick={(e) => {
            if (ignoreCanvasClickRef.current) {
              ignoreCanvasClickRef.current = false;
              return;
            }
            if (e.target === e.currentTarget) {
              requestAnimationFrame(() => onSelectElements([]));
            }
          }}
        >
          <div
            ref={canvasWrapperRef}
            className="relative shrink-0 grow-0"
            style={{
              width: `${canvasDisplayW}px`,
              height: `${canvasDisplayH}px`,
              overflow: allowInteractionOverflow ? 'visible' : 'hidden',
            }}
          >
          <div
            ref={canvasRef}
            className={`absolute inset-0 shadow-[0_2px_20px_rgba(0,0,0,0.5)] select-none overflow-hidden ${
              showReferencePreview ? '' : 'bg-white'
            }`}
          >
            {showReferencePreview && (
              <img
                src={template.previewReferenceBackground}
                alt=""
                className="absolute inset-0 w-full h-full pointer-events-none select-none z-0"
                style={{
                  objectFit: template.previewReferenceBackgroundFit ?? 'cover',
                  opacity: Math.min(
                    1,
                    Math.max(0, (template.previewReferenceBackgroundOpacity ?? 100) / 100)
                  ),
                }}
              />
            )}

            {/* 整页统一渲染：半透明背景按图层层级与下方元素真实合成 */}
            <div className="absolute inset-0 overflow-hidden pointer-events-none z-[1]">
              <CanvasLabelImage
                widthMm={template.width}
                heightMm={template.height}
                elements={staticCanvasElements}
                rowData={null}
                rowIndex={0}
                showBorder={false}
                transparentBackground={showReferencePreview}
                variableDefaults={previewDefaults}
                printColorMode={printColorMode}
                renderScale={canvasRenderScale}
              />
            </div>

            {showUnselectedDimLayer && (
              <div
                className={`ps-unselected-dim-layer${
                  showUnselectedDim ? ' ps-unselected-dim-layer--visible' : ''
                }`}
                aria-hidden={!showUnselectedDim}
              />
            )}

            {selectedLiftElements && (
              <div className="ps-selected-lift-layer">
                <CanvasLabelImage
                  widthMm={template.width}
                  heightMm={template.height}
                  elements={selectedLiftElements}
                  rowData={null}
                  rowIndex={0}
                  showBorder={false}
                  transparentBackground
                  variableDefaults={previewDefaults}
                  printColorMode={printColorMode}
                  renderScale={canvasRenderScale}
                />
              </div>
            )}

            {activeTool === 'move' && (
              <div
                className="absolute inset-0 z-[1] cursor-crosshair touch-none"
                aria-hidden
                onPointerDown={startMarquee}
              />
            )}

            {marqueeState && (
              <div
                className="ps-marquee-select"
                style={{
                  left: `${Math.min(marqueeState.startX, marqueeState.currentX)}px`,
                  top: `${Math.min(marqueeState.startY, marqueeState.currentY)}px`,
                  width: `${Math.abs(marqueeState.currentX - marqueeState.startX)}px`,
                  height: `${Math.abs(marqueeState.currentY - marqueeState.startY)}px`,
                }}
              />
            )}

            {overlayElements.map((element, index) => {
              const isLocked = !!element.locked;
              const isSelected = !isLocked && selectedElementIds.includes(element.id);
              const isPrimary = selectedElementIds.indexOf(element.id) === 0;
              const isBeingDragged =
                dragInteractionVisualActive &&
                dragState?.type === 'drag' &&
                !!dragMovingIdSet?.has(element.id);
              const isBeingResized =
                dragState?.type === 'resize' && dragState.elementId === element.id;
              if (isBeingDragged) {
                return null;
              }
              if (isBeingResized && interactionCanvasReady && resizingPreviewElement) {
                return null;
              }

              const overlayElement =
                isBeingResized && resizingPreviewElement ? resizingPreviewElement : element;

              const showResizeHandles =
                isSelected &&
                !hideSelectionChrome &&
                !isLocked &&
                selectedElementIds.length === 1 &&
                dragState?.type !== 'resize';
              const elemX = mmToPx(overlayElement.x, displayZoom);
              const elemY = mmToPx(overlayElement.y, displayZoom);
              const elemW = mmToPx(overlayElement.width, displayZoom);
              const elemH = mmToPx(overlayElement.height, displayZoom);

              return (
                <div
                  key={element.id}
                  data-label-element
                  className={`absolute group touch-none ${
                    isLocked
                      ? 'pointer-events-none'
                      : isSelected
                      ? 'cursor-move'
                      : 'cursor-move hover:outline hover:outline-1 hover:outline-[#31a8ff]/40'
                  }`}
                  style={{
                    left: `${elemX}px`,
                    top: `${elemY}px`,
                    width: `${elemW}px`,
                    height: `${elemH}px`,
                    zIndex: isBeingResized
                      ? index + 2000
                      : isSelected
                      ? index + 1000
                      : index + 10,
                    transform: overlayElement.rotation
                      ? `rotate(${overlayElement.rotation}deg)`
                      : undefined,
                    transformOrigin: 'center center',
                    pointerEvents: isBeingResized ? 'none' : undefined,
                  }}
                  onPointerDown={
                    isLocked || isBeingResized
                      ? undefined
                      : (e) => handleElementPointerDown(e, element, 'drag')
                  }
                  onClick={isLocked ? undefined : (e) => handleElementClick(e, element)}
                >
                  {selectedElementIds.length > 1 && isSelected && (
                    <div
                      className={`absolute -top-5 right-0 text-[9px] font-bold px-1 py-0.5 z-50 text-white min-w-[14px] text-center ${
                        isPrimary ? 'bg-[#31a8ff]' : 'bg-emerald-600'
                      }`}
                    >
                      {selectedElementIds.indexOf(element.id) + 1}
                    </div>
                  )}

                  {isBeingResized && (
                    <div className="absolute inset-0 ps-resize-outline pointer-events-none z-[60]" aria-hidden />
                  )}

                  {isSelected &&
                    !hideSelectionChrome &&
                    !isLocked &&
                    selectedElementIds.length === 1 &&
                    !isBeingResized && (
                      <div
                        className="absolute inset-0 ps-resize-outline pointer-events-none z-[60]"
                        aria-hidden
                      />
                    )}

                  {showResizeHandles && (
                    <>
                      <div
                        className="ps-handle top-0 left-0 -translate-x-1/2 -translate-y-1/2 cursor-nw-resize"
                        onPointerDown={(e) => handleResizeStart(e, element, 'nw')}
                        title="拖拽调整尺寸"
                      />
                      <div
                        className="ps-handle top-0 right-0 translate-x-1/2 -translate-y-1/2 cursor-ne-resize"
                        onPointerDown={(e) => handleResizeStart(e, element, 'ne')}
                        title="拖拽调整尺寸"
                      />
                      <div
                        className="ps-handle bottom-0 left-0 -translate-x-1/2 translate-y-1/2 cursor-sw-resize"
                        onPointerDown={(e) => handleResizeStart(e, element, 'sw')}
                        title="拖拽调整尺寸"
                      />
                      <div
                        className="ps-handle bottom-0 right-0 translate-x-1/2 translate-y-1/2 cursor-se-resize"
                        onPointerDown={(e) => handleResizeStart(e, element, 'se')}
                        title="拖拽调整尺寸"
                      />
                    </>
                  )}

                  {!isLocked && (
                    <div className="absolute top-[-16px] left-0 bg-[#3c3c3c] text-[#ccc] text-[9px] px-1 py-px opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-50">
                      {element.name}
                    </div>
                  )}

                  {isSelected &&
                    !hideSelectionChrome &&
                    !isLocked &&
                    selectedElementIds.length === 1 &&
                    overlayElement.type === 'table' &&
                    onSelectTableCells && (
                      <div className="absolute inset-0 z-10 pointer-events-none">
                        {iterTableCells(overlayElement).map(({ row, col }) => {
                          const { x: cellX, y: cellY, width: cellW, height: cellH } =
                            getCellRectPx(overlayElement, row, col, canvasRenderScale);
                          const isCellSelected = selectedTableCells.some((c) => {
                            const anchor = getCellAnchor(overlayElement, c.row, c.col);
                            return anchor.row === row && anchor.col === col;
                          });
                          return (
                            <div
                              key={`cell-${row}-${col}`}
                              className={`absolute pointer-events-auto cursor-grab active:cursor-grabbing touch-none ${
                                isCellSelected
                                  ? 'bg-[#31a8ff]/25'
                                  : 'hover:bg-[#31a8ff]/10'
                              }`}
                              style={{
                                left: `${cellX}px`,
                                top: `${cellY}px`,
                                width: `${cellW}px`,
                                height: `${cellH}px`,
                                boxShadow: isCellSelected
                                  ? 'inset 0 0 0 1px #31a8ff'
                                  : undefined,
                              }}
                              onPointerDown={(e) =>
                                handleTableCellPointerDown(e, overlayElement, row, col)
                              }
                              onPointerMove={(e) =>
                                handleTableCellPointerMove(e, overlayElement)
                              }
                              onPointerUp={(e) => handleTableCellPointerUp(e)}
                              onPointerCancel={(e) => handleTableCellPointerUp(e)}
                              title={`单元格 ${row + 1},${col + 1}（拖动移动表格）`}
                            />
                          );
                        })}
                      </div>
                    )}
                </div>
              );
            })}

            {multiSelectionBounds && (
              <div
                className="absolute ps-resize-outline pointer-events-none z-[1005]"
                style={{
                  left: `${mmToPx(multiSelectionBounds.minX, displayZoom)}px`,
                  top: `${mmToPx(multiSelectionBounds.minY, displayZoom)}px`,
                  width: `${mmToPx(
                    multiSelectionBounds.maxX - multiSelectionBounds.minX,
                    displayZoom
                  )}px`,
                  height: `${mmToPx(
                    multiSelectionBounds.maxY - multiSelectionBounds.minY,
                    displayZoom
                  )}px`,
                }}
                aria-hidden
              />
            )}
          </div>

          </div>
        </div>

        {isMobileLayout ? (
          <div className="mobile-design-toolstrip">
            <MobileMainToolButtons items={mobileToolItems} />
            <input
              ref={imageInputRef}
              type="file"
              accept="image/*"
              className="hidden"
              onChange={handleImageFile}
            />
            {zoomControls}
          </div>
        ) : (
          <div className={`ps-status-bar ${isNarrow ? 'ps-status-bar-compact' : ''}`}>
            <div className="flex items-center gap-2 min-w-0 truncate">
              {isNarrow ? (
                <span className="truncate">
                  {template.width}×{template.height}
                  {selectedElemObj ? ` · ${selectedElemObj.name}` : ''}
                  {selectedElementIds.length > 1 ? ` · ${selectedElementIds.length}选` : ''}
                </span>
              ) : (
                <>
                  <span>
                    文档: {template.width} × {template.height} mm
                  </span>
                  {selectedElemObj && selectedElementIds.length === 1 && (
                    <span className="text-[#31a8ff]">
                      | {selectedElemObj.name} — X:{selectedElemObj.x} Y:{selectedElemObj.y} W:
                      {selectedElemObj.width} H:{selectedElemObj.height} mm
                    </span>
                  )}
                  {multiSelectionBounds && (
                    <span className="text-emerald-400">
                      | 已选 {selectedElementIds.length} 个 — X:{multiSelectionBounds.minX} Y:
                      {multiSelectionBounds.minY} mm
                    </span>
                  )}
                  {activeTool === 'move' && (
                    <span className="text-[#888] hidden sm:inline">
                      | 框选 · Shift/Ctrl+点选 · Ctrl+A 全选 · Ctrl+C/V 复制粘贴
                    </span>
                  )}
                </>
              )}
            </div>
            <div className="flex items-center gap-2 shrink-0">
              {!isNarrow && (
                <span className="hidden sm:inline text-[10px]">
                  预览使用变量默认值 · 方向键微调 · Ctrl+滚轮缩放
                </span>
              )}
              {zoomControls}
              <button
                type="button"
                onClick={applyModerateZoom}
                className="text-[10px] text-[#aaa] hover:text-white px-1.5 py-0.5 border border-[#4a4a4a] hover:border-[#666] transition cursor-pointer"
                title="适中缩放"
              >
                适中
              </button>
            </div>
          </div>
        )}
      </div>
    </div>
    {interactionFloatPortal}
    </>
  );
};
