import React, { useCallback, useRef, useEffect, useLayoutEffect, useMemo, useState } from 'react';
import JsBarcode from 'jsbarcode';
import { QRCodeSVG } from 'qrcode.react';
import type {
  LabelTemplate,
  LabelElement,
  NutritionExtraItem,
} from '../../../types/labelTemplate';
import { canonicalElementType, isPrintInputElement } from '../../../types/labelTemplate';
import { PRESET_LABEL_SIZES } from '../../../types/labelTemplate';
import { NUTRITION_FIXED_ITEMS } from '../../../types/labelTemplate';
import { cn } from '../../ui/utils';
import { resolvePictureUrlForDisplay } from '../../../services/imageUploadService';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '../../ui/select';
import { normalizeBarcodeType, toJsBarcodeFormat } from '../../../lib/barcodeFormat';

/** 真实条形码渲染（JsBarcode），支持水平/竖排与制式 */
function BarcodeBlock({
  data,
  width,
  height,
  showText,
  orientation = 'horizontal',
  barcodeType,
  fontSize = 14,
  textAlign = 'center',
}: {
  data: string;
  width: number;
  height: number;
  showText?: boolean;
  orientation?: 'horizontal' | 'vertical';
  barcodeType?: unknown;
  fontSize?: number;
  textAlign?: 'left' | 'center' | 'right' | string;
}) {
  const svgRef = useRef<SVGSVGElement>(null);
  const isVertical = orientation === 'vertical';
  const labelReserve = showText !== false ? Math.max(12, Math.round(fontSize) + 4) : 4;
  const barHeight = Math.max(20, (isVertical ? width : height) - labelReserve);
  const jsFormat = toJsBarcodeFormat(barcodeType);
  const align =
    textAlign === 'right' ? 'flex-end' : textAlign === 'center' ? 'center' : 'flex-start';
  useEffect(() => {
    if (svgRef.current && data) {
      try {
        JsBarcode(svgRef.current, data, {
          format: jsFormat,
          width: 1,
          height: barHeight,
          displayValue: showText !== false,
          margin: 2,
          fontOptions: '',
          fontSize: Math.max(8, Math.round(fontSize)),
          textAlign: textAlign === 'right' ? 'right' : textAlign === 'center' ? 'center' : 'left',
        });
      } catch {
        // invalid data, ignore
      }
    }
  }, [data, barHeight, showText, jsFormat, fontSize, textAlign]);
  const svg = <svg ref={svgRef} className="w-full h-full min-h-0" style={{ maxHeight: isVertical ? width : height }} />;
  if (isVertical) {
    return (
      <div className="w-full h-full flex items-center justify-center">
        <div
          style={{
            transform: 'rotate(-90deg)',
            transformOrigin: 'center center',
            width: height,
            height: width,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <div className="w-full min-h-0 flex flex-col" style={{ alignItems: align }}>{svg}</div>
        </div>
      </div>
    );
  }
  return (
    <div className="w-full h-full flex flex-col justify-center" style={{ alignItems: align }}>
      <div className="w-full min-h-0 flex flex-col" style={{ alignItems: align }}>{svg}</div>
    </div>
  );
}

/** 画布网格步长（px），控件吸附到该步长 */
const GRID_SIZE = 8;

/** 将数值对齐到网格 */
function snapToGrid(value: number): number {
  return Math.round(value / GRID_SIZE) * GRID_SIZE;
}

/** 画布内安全边距（px），与主题色虚线框一致；控件不可移出 */
export const LABEL_CANVAS_SAFE_MARGIN_PX = 16;

/** 将控件矩形限制在虚线安全区内（画布像素坐标） */
export function clampLabelElementBox(
  x: number,
  y: number,
  w: number,
  h: number,
  baseW: number,
  baseH: number,
  marginPx: number = LABEL_CANVAS_SAFE_MARGIN_PX,
): { x: number; y: number; w: number; h: number } {
  const minX = marginPx;
  const minY = marginPx;
  const maxR = baseW - marginPx;
  const maxB = baseH - marginPx;
  let cw = Math.max(20, snapToGrid(w));
  let ch = Math.max(12, snapToGrid(h));
  const innerW = maxR - minX;
  const innerH = maxB - minY;
  if (innerW < GRID_SIZE || innerH < GRID_SIZE) {
    return {
      x: minX,
      y: minY,
      w: Math.max(GRID_SIZE, snapToGrid(innerW)),
      h: Math.max(GRID_SIZE, snapToGrid(innerH)),
    };
  }
  cw = Math.min(cw, snapToGrid(innerW));
  ch = Math.min(ch, snapToGrid(innerH));
  let cx = snapToGrid(x);
  let cy = snapToGrid(y);
  cx = Math.min(Math.max(cx, minX), Math.max(minX, maxR - cw));
  cy = Math.min(Math.max(cy, minY), Math.max(minY, maxB - ch));
  return { x: cx, y: cy, w: cw, h: ch };
}

/** 拖拽时仅限制位置，不改变宽高 */
function clampDragPosition(
  x: number,
  y: number,
  w: number,
  h: number,
  baseW: number,
  baseH: number,
  marginPx: number,
): { x: number; y: number } {
  const minX = marginPx;
  const minY = marginPx;
  const maxR = baseW - marginPx;
  const maxB = baseH - marginPx;
  const maxX = maxR - w;
  const maxY = maxB - h;
  if (!Number.isFinite(maxX) || !Number.isFinite(maxY) || maxX < minX || maxY < minY) {
    return { x: minX, y: minY };
  }
  let cx = snapToGrid(x);
  let cy = snapToGrid(y);
  cx = Math.min(Math.max(cx, minX), snapToGrid(maxX));
  cy = Math.min(Math.max(cy, minY), snapToGrid(maxY));
  return { x: cx, y: cy };
}

/** 1cm ≈ 37.8px (96 DPI); 1 inch = 96px */
function unitToPx(value: number, unit: 'cm' | 'inch'): number {
  return unit === 'cm' ? value * 37.8 : value * 96;
}

/** px 转单位（用于拖拽调整纸张尺寸） */
function pxToUnit(px: number, unit: 'cm' | 'inch'): number {
  return unit === 'cm' ? px / 37.8 : px / 96;
}

const RULER_H = 24;

/** 预览区标尺显示单位（与模板 `template.unit` 独立，仅影响刻度与读数） */
export type PreviewRulerDisplayUnit = "cm" | "mm" | "inch";

/** 将长度从模板单位换算到预览标尺显示单位 */
function convertTemplateLengthToDisplay(
  value: number,
  templateUnit: "cm" | "inch",
  displayUnit: PreviewRulerDisplayUnit,
): number {
  if (!Number.isFinite(value)) return 0;
  const cm = templateUnit === "cm" ? value : value * 2.54;
  if (displayUnit === "cm") return cm;
  if (displayUnit === "mm") return cm * 10;
  return cm / 2.54;
}

function formatSelectionWidthForPreviewRuler(
  lengthPx: number,
  baseW: number,
  paperWidthTemplate: number,
  templateUnit: "cm" | "inch",
  displayUnit: PreviewRulerDisplayUnit,
): string | null {
  if (!Number.isFinite(lengthPx) || !Number.isFinite(baseW) || baseW <= 0 || !Number.isFinite(paperWidthTemplate) || paperWidthTemplate <= 0) {
    return null;
  }
  const inTemplate = (lengthPx / baseW) * paperWidthTemplate;
  const d = convertTemplateLengthToDisplay(inTemplate, templateUnit, displayUnit);
  if (displayUnit === "mm") return `${Math.round(d)}mm`;
  if (displayUnit === "inch") return `${d.toFixed(3)}in`;
  return `${d.toFixed(2)}cm`;
}

/**
 * 贯穿预览区全宽的横向标尺：刻度按「预览标尺单位」绘制；与模板画布单位无关。
 */
function RulerBarHorizontal({
  rulerTotalWidthPx,
  paperWidthPx,
  paperOffsetLeftPx,
  paperWidthTemplate,
  templateUnit,
  displayUnit,
  baseW,
  selection,
}: {
  rulerTotalWidthPx: number;
  paperWidthPx: number;
  paperOffsetLeftPx: number;
  /** 模板定义的纸张宽度（模板单位） */
  paperWidthTemplate: number;
  templateUnit: "cm" | "inch";
  displayUnit: PreviewRulerDisplayUnit;
  baseW: number;
  selection: { x: number; width: number } | null;
}) {
  const displaySpan = convertTemplateLengthToDisplay(paperWidthTemplate, templateUnit, displayUnit);
  if (!Number.isFinite(displaySpan) || displaySpan <= 0 || !Number.isFinite(paperWidthPx) || paperWidthPx < 1) {
    return null;
  }
  if (!Number.isFinite(rulerTotalWidthPx) || rulerTotalWidthPx < 1) {
    return null;
  }
  const h = RULER_H;
  const pxPerDisplayUnit = paperWidthPx / displaySpan;
  /** 标尺几何中心为刻度 0，向左为负、向右为正 */
  const centerPx = rulerTotalWidthPx / 2;
  const xAtSignedUnit = (u: number) => centerPx + u * pxPerDisplayUnit;
  const nodes: React.ReactNode[] = [];

  let labelStep = 1;
  if (displayUnit === "mm") {
    if (displaySpan > 120) labelStep = 20;
    else if (displaySpan > 60) labelStep = 10;
    else if (displaySpan > 25) labelStep = 5;
  }

  const minorDivisions = displayUnit === "inch" ? 8 : 10;
  const kMin = Math.floor((0 - centerPx) / pxPerDisplayUnit) - 2;
  const kMax = Math.ceil((rulerTotalWidthPx - centerPx) / pxPerDisplayUnit) + 2;
  const kLo = Math.max(-5000, Math.min(5000, kMin));
  const kHi = Math.max(-5000, Math.min(5000, kMax));

  for (let k = kLo; k <= kHi; k++) {
    const x = xAtSignedUnit(k);
    if (x < -8 || x > rulerTotalWidthPx + 8) continue;
    const showLabel = k === 0 || k % labelStep === 0;
    nodes.push(
      <g key={`maj-${k}`}>
        <line x1={x} y1={h} x2={x} y2={4} stroke="#9ca3af" strokeWidth={1} />
        {showLabel ? (
          <text
            x={x}
            y={12}
            fontSize={8}
            fill="#4b5563"
            className="select-none font-mono"
            textAnchor="middle"
          >
            {k}
          </text>
        ) : null}
      </g>,
    );
    const midMinor = Math.floor(minorDivisions / 2);
    for (let s = 1; s < minorDivisions; s++) {
      const u = k + s / minorDivisions;
      const x2 = xAtSignedUnit(u);
      if (x2 < -4 || x2 > rulerTotalWidthPx + 4) continue;
      const y2 = s === midMinor ? 10 : 12;
      nodes.push(
        <line
          key={`min-${k}-${s}`}
          x1={x2}
          y1={h}
          x2={x2}
          y2={y2}
          stroke="#d1d5db"
          strokeWidth={0.5}
        />,
      );
    }
  }

  let selLeft = 0;
  let selW = 0;
  if (selection && Number.isFinite(baseW) && baseW > 0) {
    selLeft = paperOffsetLeftPx + (selection.x / baseW) * paperWidthPx;
    selW = (selection.width / baseW) * paperWidthPx;
  }
  const wLabel = selection
    ? formatSelectionWidthForPreviewRuler(selection.width, baseW, paperWidthTemplate, templateUnit, displayUnit)
    : null;

  return (
    <svg
      width="100%"
      height={h}
      viewBox={`0 0 ${rulerTotalWidthPx} ${h}`}
      preserveAspectRatio="none"
      className="block bg-slate-100/90 border-b border-slate-300 pointer-events-none select-none shrink-0"
      aria-hidden
    >
      <line
        x1={0}
        y1={h - 0.5}
        x2={rulerTotalWidthPx}
        y2={h - 0.5}
        stroke="#e5e7eb"
      />
      {nodes}
      {selection && Number.isFinite(selW) && selW > 0.5 && (
        <>
          <rect
            x={selLeft}
            y={1}
            width={Math.max(1, Math.min(selW, rulerTotalWidthPx - selLeft + 1))}
            height={h - 4}
            fill="rgb(59 130 246 / 0.22)"
            stroke="rgb(37 99 235 / 0.85)"
            strokeWidth={1}
            rx={2}
          />
          {wLabel && (
            <text
              x={selLeft + Math.max(2, Math.min(selW, rulerTotalWidthPx - selLeft) / 2)}
              y={h - 6}
              textAnchor="middle"
              fontSize={8}
              fill="#1d4ed8"
              className="select-none font-mono"
            >
              {wLabel}
            </text>
          )}
        </>
      )}
    </svg>
  );
}

/**
 * 多选项在画布上的文案：有 prefix 时与 App 打印一致（prefix + 答案/占位）；否则在已选字典时显示「字典名称: 内容」。
 */
function formatMultipleOptionsCanvasLine(
  cfg: Record<string, unknown>,
  text: string,
  selected: string[],
): string {
  const prefix = String(cfg.prefix ?? '').trim();
  const dictLabel = String(cfg.multipleOptionName ?? cfg.MultipleOptionName ?? '').trim();
  const answers = selected.filter(Boolean).join(', ');
  const fallback = text || '…';
  if (prefix) {
    return answers ? `${prefix}${answers}` : `${prefix}${fallback}`;
  }
  if (dictLabel) {
    const body = answers || fallback;
    return `${dictLabel}: ${body}`;
  }
  return answers || fallback;
}

function nutritionExtraRows(cfg: Record<string, unknown>): NutritionExtraItem[] {
  const raw = cfg.extraNutrients;
  if (!Array.isArray(raw)) return [];
  return raw.map((item, idx) => {
    const row = item as Record<string, unknown>;
    return {
      id: String(row.id ?? `extra-${idx}`),
      name: String(row.name ?? ''),
      value: String(row.value ?? ''),
      unit: String(row.unit ?? ''),
    };
  });
}

function nutritionFixedField(
  cfg: Record<string, unknown>,
  key: string,
  field: 'value' | 'unit',
): string {
  const directKey = field === 'value' ? key : `${key}Unit`;
  const direct = cfg[directKey];
  if (direct != null && String(direct).trim() !== '') return String(direct).trim();
  const fixedRows = Array.isArray(cfg.fixedNutrients)
    ? (cfg.fixedNutrients as Record<string, unknown>[])
    : [];
  const row = fixedRows.find((item) => String(item.key ?? '').trim() === key);
  return String(row?.[field] ?? '').trim();
}

function formatDateByPreset(format: string, date: Date): string {
  const yyyy = String(date.getFullYear());
  const yy = yyyy.slice(-2);
  const mm = String(date.getMonth() + 1).padStart(2, '0');
  const dd = String(date.getDate()).padStart(2, '0');
  const hh = String(date.getHours()).padStart(2, '0');
  const min = String(date.getMinutes()).padStart(2, '0');
  const monthLong = date.toLocaleString('en-US', { month: 'long' }).toUpperCase();
  const dayLong = date.toLocaleString('en-US', { weekday: 'long' }).toUpperCase();
  const dayShort = date.toLocaleString('en-US', { weekday: 'short' }).toUpperCase();
  const monthShort = date.toLocaleString('en-US', { month: 'short' }).toUpperCase();
  switch (format) {
    case 'DD/MM/YYYY':
      return `${dd}/${mm}/${yyyy}`;
    case 'MM/DD/YYYY':
      return `${mm}/${dd}/${yyyy}`;
    case 'DD/MM/YY':
      return `${dd}/${mm}/${yy}`;
    case 'MM/DD/YY':
      return `${mm}/${dd}/${yy}`;
    case 'MM/YY':
      return `${mm}/${yy}`;
    case 'MM/DD':
      return `${mm}/${dd}`;
    case 'MM':
      return mm;
    case 'DD':
      return dd;
    case 'YY':
      return yy;
    case 'FULLY DAY(WEDNESDAY)':
      return dayLong;
    case 'DAY (WED)':
      return dayShort;
    case 'MONTH (DECEMBER)':
      return monthLong;
    case 'YEAR (2025)':
      return yyyy;
    case 'DD MONTH YEAR (25 DECEMBER 2025)':
      return `${dd} ${monthLong} ${yyyy}`;
    default:
      return format
        .replace('YYYY', yyyy)
        .replace('YY', yy)
        .replace('MM', mm)
        .replace('DD', dd)
        .replace('HH', hh)
        .replace('mm', min);
  }
}

const DURATION_UNITS = new Set([
  'Minutes',
  'Hours',
  'Days',
  'Weeks',
  'Months (30 Day)',
  'Years',
]);

function normalizeWeightUnit(raw: unknown): 'lb' | 'kg' | 'mg' | 'g' | 'oz' {
  const unit = String(raw ?? '').trim().toLowerCase();
  if (unit === 'milligrams') return 'mg';
  if (unit === 'grams') return 'g';
  if (unit === 'ounces') return 'oz';
  if (unit === 'pounds') return 'lb';
  if (unit === 'kilograms') return 'kg';
  if (unit === 'lb' || unit === 'kg' || unit === 'mg' || unit === 'g' || unit === 'oz') return unit;
  return 'g';
}

/** 根据元素类型与 config 渲染画布上的默认内容 */
function ElementContent({ el, isAppPrintField }: { el: LabelElement; isAppPrintField?: boolean }) {
  const cfg = el.config as Record<string, unknown>;
  const type = canonicalElementType(el.type);
  const isVerticalRotation = el.rotation === 'vertical';

  // Common styles
  const commonStyle: React.CSSProperties = {
    fontSize: (cfg?.fontSize as number) ?? 14,
    fontFamily: (cfg?.fontFamily as string) ?? 'Arial',
    fontWeight: (cfg?.fontWeight as string) ?? 'normal',
    textAlign: (cfg?.textAlign as any) ?? 'left',
    color: (cfg?.color as string) ?? '#000',
  };

  // Rotation support:
  // The editor's Rotation is currently a simple horizontal/vertical toggle.
  // For text-like elements we render vertical via writing-mode to avoid layout clipping.
  const textLike =
    type === 'TEXT_STATIC' || type === 'TEXT_PRODUCT' || type === 'TEXT_PRICE';
  const textRotationStyle: React.CSSProperties =
    isVerticalRotation && textLike
      ? { writingMode: 'vertical-rl', textOrientation: 'mixed' as any }
      : {};
  const rotateBoxStyle: React.CSSProperties = isVerticalRotation
    ? { transform: 'rotate(-90deg)', transformOrigin: 'center center' }
    : {};

  // 文本类
  const inputType = cfg?.inputType as string | undefined;
  if (type === 'TEXT_STATIC') {
    const text = (cfg?.text as string) ?? 'Text';
    if (isAppPrintField) {
      if (inputType === 'options') {
        const selected = Array.isArray(cfg?.selectedOptionValues)
          ? (cfg.selectedOptionValues as string[])
          : [];
        const line = formatMultipleOptionsCanvasLine(cfg, text, selected);
        return (
          <div
            className="w-full h-full px-1 flex flex-col justify-center overflow-hidden pointer-events-none text-gray-600 italic text-[11px] leading-tight break-all"
            style={{ ...commonStyle, ...textRotationStyle }}
            title="Filled in mobile app when printing"
          >
            {line}
          </div>
        );
      }
      const display =
        inputType === 'number' ? ((cfg?.text as string) ?? '0') : text;
      return (
        <div
          className="w-full h-full px-1 flex items-center overflow-hidden pointer-events-none text-gray-600 italic text-[11px]"
          style={{ ...commonStyle, ...textRotationStyle }}
          title="Filled in mobile app when printing"
        >
          {display}
        </div>
      );
    }
    if (inputType === 'number') {
      return (
        <input
          type="number"
          readOnly
          value={(cfg?.text as string) ?? '0'}
          className="w-full h-full min-w-0 border border-gray-300 bg-white rounded px-1 pointer-events-none"
          style={{ ...commonStyle, ...textRotationStyle, textAlign: 'right' }}
        />
      );
    }
    if (inputType === 'options') {
      // 画布只展示「答案」纯文本，不出现勾选、下拉箭头等控件样式；实际选择在 APP 打印流程中完成
      const selected = Array.isArray(cfg?.selectedOptionValues)
        ? (cfg.selectedOptionValues as string[])
        : [];
      const line = formatMultipleOptionsCanvasLine(cfg, text, selected);
      const muted = selected.length === 0;
      return (
        <div
          className={cn(
            'w-full h-full px-1 overflow-hidden whitespace-pre-wrap break-all leading-tight',
            muted && 'text-gray-400',
          )}
          style={{ ...commonStyle, ...textRotationStyle }}
          title={line}
        >
          {line}
        </div>
      );
    }
    if (inputType === 'text') {
      return (
        <input
          type="text"
          readOnly
          value={text}
          className="w-full h-full min-w-0 border border-gray-300 bg-white rounded px-1 pointer-events-none"
          style={{ ...commonStyle, ...textRotationStyle }}
        />
      );
    }
    return (
      <div
        className="w-full h-full px-1 overflow-hidden whitespace-pre-wrap break-all leading-tight"
        style={{ ...commonStyle, ...textRotationStyle }}
      >
        {text}
      </div>
    );
  }
  if (type === 'TEXT_PRODUCT') {
    const text = (cfg?.text as string) ?? 'Product name';
    return (
      <div
        className="w-full h-full px-1 overflow-hidden whitespace-pre-wrap break-all leading-tight"
        style={{ ...commonStyle, ...textRotationStyle }}
      >
        {text}
      </div>
    );
  }
  if (type === 'TEXT_PRICE') {
    const text = (cfg?.text as string) ?? '0.00';
    return (
      <div
        className="w-full h-full px-1 overflow-hidden flex items-center"
        style={{
          ...commonStyle,
          ...textRotationStyle,
          justifyContent:
            commonStyle.textAlign === 'center'
              ? 'center'
              : commonStyle.textAlign === 'right'
                ? 'flex-end'
                : 'flex-start',
        }}
      >
        <span>{text}</span>
      </div>
    );
  }

  // 条码（支持水平/竖排、制式、字号与对齐）
  if (type === 'BARCODE') {
    const data = (cfg?.data as string) ?? '123456789';
    const showText = (cfg?.showText as boolean) !== false;
    const orientation = (
      el.rotation === 'vertical' || (cfg?.orientation as string) === 'vertical'
        ? 'vertical'
        : 'horizontal'
    ) as 'horizontal' | 'vertical';
    const textAlign = (cfg?.textAlign as string) ?? 'center';
    const fontSize = (cfg?.fontSize as number) ?? 14;
    return (
      <div className="flex flex-col w-full h-full overflow-hidden p-0.5">
        <div className="flex-1 w-full min-h-0 flex items-center justify-center">
          <BarcodeBlock
            data={data}
            width={el.width}
            height={el.height}
            showText={showText}
            orientation={orientation}
            barcodeType={normalizeBarcodeType(cfg?.barcodeType)}
            fontSize={fontSize}
            textAlign={textAlign}
          />
        </div>
      </div>
    );
  }

  // 二维码
  if (type === 'QRCODE') {
    const data = (cfg?.data as string) ?? 'https://example.com';
    const size = Math.min(el.width, el.height) - 4;
    return (
      <div className="w-full h-full flex items-center justify-center p-0.5">
        <QRCodeSVG value={data} size={Math.max(20, size)} level="M" includeMargin={false} />
      </div>
    );
  }

  // 图片/Logo
  if (type === 'IMAGE') {
    const src = cfg?.src as string | undefined;
    const imageRotateStyle: React.CSSProperties = isVerticalRotation
      ? { transform: 'rotate(-90deg)' }
      : {};
    if (src) {
      return (
        <div className="w-full h-full flex items-center justify-center overflow-hidden">
          <img
            src={resolvePictureUrlForDisplay(src)}
            alt=""
            className="max-w-full max-h-full object-contain"
            style={imageRotateStyle}
          />
        </div>
      );
    }
    return (
      <div
        className="w-full h-full flex flex-col items-center justify-center bg-gray-100 text-gray-500 text-[10px] border border-dashed border-gray-300"
        style={imageRotateStyle}
      >
        <span className="font-medium">Logo</span>
      </div>
    );
  }

  // 日期/时间
  if (type === 'DATE') {
    const previewFmtRaw = cfg?.__previewFormatted;
    if (typeof previewFmtRaw === 'string') {
      const previewFmt = previewFmtRaw.trim();
      return (
        <div className="w-full h-full flex items-center justify-center overflow-hidden">
          <div className="px-1 overflow-hidden whitespace-nowrap" style={{ ...commonStyle, ...rotateBoxStyle }}>
            {previewFmt || '—'}
          </div>
        </div>
      );
    }
    const it = String(cfg?.inputType ?? cfg?.InputType ?? '').toLowerCase();
    const format =
      (typeof cfg?.format === 'string' && cfg.format.trim()
        ? cfg.format
        : typeof cfg?.Format === 'string' && cfg.Format.trim()
          ? cfg.Format
          : it === 'datetime'
            ? 'YYYY-MM-DD HH:mm'
            : 'DD/MM/YYYY') ?? (it === 'datetime' ? 'YYYY-MM-DD HH:mm' : 'DD/MM/YYYY');
    const offset = Number(cfg?.offsetDays ?? cfg?.OffsetDays ?? 0) || 0;
    const d = new Date();
    d.setDate(d.getDate() + offset);
    const example = formatDateByPreset(format, d);
    const isInput = it === 'datetime' || it === 'date';
    if (isInput) {
      if (isAppPrintField) {
        return (
          <div className="w-full h-full flex items-center justify-center overflow-hidden">
            <div
              className="px-1 flex items-center justify-center overflow-hidden pointer-events-none text-[10px] text-center whitespace-nowrap"
              style={{ ...commonStyle, ...rotateBoxStyle }}
              title={`Format: ${format}`}
            >
              {format}
            </div>
          </div>
        );
      }
      return (
        <div className="w-full h-full flex items-center justify-center overflow-hidden">
          <input
            type="date"
            readOnly
            value="2025-02-01"
            className="w-full h-full min-w-0 border border-gray-300 bg-white rounded px-1 pointer-events-none text-[10px]"
            style={{ ...commonStyle, ...rotateBoxStyle }}
          />
        </div>
      );
    }
    return (
      <div className="w-full h-full flex items-center justify-center overflow-hidden">
        <div className="px-1 overflow-hidden whitespace-nowrap" style={{ ...commonStyle, ...rotateBoxStyle }}>
          {example}
        </div>
      </div>
    );
  }

  // (Simplified other types similarly for brevity, ensuring style prop is passed)
  if (type === 'TIME') {
    const previewTime = cfg?.__previewFormatted;
    if (typeof previewTime === 'string') {
      return (
        <div className="w-full h-full flex items-center justify-center overflow-hidden">
          <div className="px-1 overflow-hidden whitespace-nowrap" style={{ ...commonStyle, ...rotateBoxStyle }}>
            {previewTime.trim() || '—'}
          </div>
        </div>
      );
    }
    const format = 'HH:mm';
    const example = format.replace('HH', '12').replace('mm', '30');
    return (
      <div className="w-full h-full flex items-center justify-center overflow-hidden">
        <div className="px-1 overflow-hidden whitespace-nowrap" style={{ ...commonStyle, ...rotateBoxStyle }}>
          {example}
        </div>
      </div>
    );
  }

  if (type === 'DURATION') {
    const previewDur = cfg?.__previewFormatted;
    if (typeof previewDur === 'string') {
      return (
        <div className="w-full h-full flex items-center justify-center overflow-hidden">
          <div className="px-1 overflow-hidden whitespace-nowrap" style={{ ...commonStyle, ...rotateBoxStyle }}>
            {previewDur.trim() || '—'}
          </div>
        </div>
      );
    }
    const rawFormat =
      (typeof cfg?.format === 'string' && cfg.format.trim()
        ? cfg.format
        : typeof cfg?.Format === 'string' && cfg.Format.trim()
          ? cfg.Format
          : 'Days') ?? 'Days';
    const unit = DURATION_UNITS.has(rawFormat) ? rawFormat : 'Days';
    const rawV = cfg?.durationValue ?? cfg?.value ?? cfg?.offsetDays ?? cfg?.DurationValue ?? cfg?.Value ?? cfg?.OffsetDays;
    const durationValue = Number.isFinite(Number(rawV)) ? Number(rawV) : 3;
    const example = `${durationValue} ${unit}`;
    return (
      <div className="w-full h-full flex items-center justify-center overflow-hidden">
        <div className="px-1 overflow-hidden whitespace-nowrap" style={{ ...commonStyle, ...rotateBoxStyle }}>
          {example}
        </div>
      </div>
    );
  }

  if (type === 'WEIGHT') {
    const rawV = cfg?.value ?? cfg?.Value;
    const numVal =
      rawV == null || rawV === ''
        ? 500
        : typeof rawV === 'number'
          ? rawV
          : Number(rawV);
    const weightNum = Number.isFinite(numVal) ? numVal : 500;
    const weightUnit = normalizeWeightUnit(
      (typeof cfg?.unit === 'string' && cfg.unit.trim()
        ? cfg.unit
        : typeof cfg?.Unit === 'string' && cfg.Unit.trim()
          ? cfg.Unit
          : 'g') ?? 'g',
    );
    const weightFontSizeRaw = cfg?.fontSize ?? cfg?.FontSize;
    const weightFontSize = Number.isFinite(Number(weightFontSizeRaw)) ? Number(weightFontSizeRaw) : 14;
    const weightTextAlignRaw = String(cfg?.textAlign ?? cfg?.TextAlign ?? 'left').toLowerCase();
    const weightTextAlign: 'left' | 'center' | 'right' =
      weightTextAlignRaw === 'center' || weightTextAlignRaw === 'right' ? weightTextAlignRaw : 'left';
    return (
      <div className="w-full h-full flex items-center justify-center overflow-hidden">
        <div
          className="px-1 overflow-hidden whitespace-nowrap"
          style={{ ...commonStyle, ...rotateBoxStyle, fontSize: weightFontSize, textAlign: weightTextAlign }}
        >
          {weightNum}
          {weightUnit}
        </div>
      </div>
    );
  }

  if (type === 'WEIGHT_PRICE') {
    const unitPrice = (cfg?.unitPrice as number) ?? 10;
    const weight = (cfg?.weight as number) ?? 0.5;
    const currency = (cfg?.currency as string) ?? '$';
    return <div className="w-full h-full px-1 overflow-hidden whitespace-nowrap" style={commonStyle}>{currency}{(unitPrice * weight).toFixed(2)}</div>;
  }

  // 营养成分表
  if (type === 'NUTRITION') {
    const servingsPerContainer = String(cfg.servingsPerContainer ?? cfg.ServingsPerContainer ?? '').trim();
    const servingSize = String(cfg.servingSize ?? cfg.ServingSize ?? '').trim();
    const calories = String(cfg.calories ?? cfg.Calories ?? nutritionFixedField(cfg, 'calories', 'value') ?? '').trim();
    const nutritionTitleSize = Number(cfg.nutritionTitleFontSize ?? cfg.NutritionTitleFontSize ?? 16) || 16;
    const baseRows = NUTRITION_FIXED_ITEMS.map((item) => {
      const value = nutritionFixedField(cfg, item.key, 'value');
      const unit = nutritionFixedField(cfg, item.key, 'unit');
      if (!value) return null;
      return {
        id: item.key,
        label: item.label,
        value,
        unit,
      };
    }).filter(Boolean) as Array<{ id: string; label: string; value: string; unit: string }>;
    const extraRows = nutritionExtraRows(cfg)
      .filter((item) => item.value.trim())
      .map((item) => ({
        id: item.id,
        label: item.name.trim() || 'Other',
        value: item.value.trim(),
        unit: item.unit.trim(),
      }));
    const rows = [...baseRows, ...extraRows];
    const formatNutritionValue = (value: string, unit: string): string => {
      const v = String(value ?? '').trim();
      const u = String(unit ?? '').trim();
      if (!v && !u) return '';
      return `<${v}${u ? ` ${u}` : ''}`;
    };
    const nutritionContent = (
      <div className="text-[10px] p-1 w-full h-full overflow-hidden flex flex-col leading-tight bg-white">
        <div className="font-bold border-b border-black pb-0.5" style={{ fontSize: `${nutritionTitleSize}px` }}>
          Nutrition Facts
        </div>
        {calories ? (
          <div className="flex items-center justify-between py-0.5 mt-0.5">
            <span className="font-semibold text-[10px]">Calories</span>
            <span className="font-semibold text-[10px]">{formatNutritionValue(calories, '')}</span>
          </div>
        ) : null}
        {servingsPerContainer ? (
          <div className="flex items-center justify-between py-0.5 text-[10px]">
            <span>Servings Per Container</span>
            <span>{servingsPerContainer}</span>
          </div>
        ) : null}
        {servingSize ? (
          <div className="flex items-center justify-between pb-0.5 text-[10px]">
            <span>Serving Size</span>
            <span>{servingSize}</span>
          </div>
        ) : null}
        <div className="flex-1 min-h-0 overflow-hidden pt-0.5">
          {rows.length === 0 ? (
            <div className="text-[7px] text-gray-500">No nutrients</div>
          ) : (
            rows.slice(0, 18).map((row) => (
              <div key={row.id} className="flex items-center justify-between py-[1px] text-[10px]">
                <span className="truncate font-medium">{row.label}</span>
                <span className="shrink-0 font-medium">
                  {formatNutritionValue(row.value, row.unit)}
                </span>
              </div>
            ))
          )}
        </div>
      </div>
    );
    return (
      <div className="w-full h-full flex items-center justify-center overflow-hidden">
        <div
          className="shrink-0"
          style={
            isVerticalRotation
              ? {
                  width: el.height,
                  height: el.width,
                  transform: 'rotate(-90deg)',
                  transformOrigin: 'center center',
                }
              : { width: '100%', height: '100%' }
          }
        >
          {nutritionContent}
        </div>
      </div>
    );
  }

  // 空白占位
  if (type === 'BLANK') {
    return <div className="w-full h-full border border-dashed border-gray-200" />;
  }

  return (
    <div className="text-gray-500 text-[10px] px-1 truncate w-full flex items-center justify-center">
      {el.type.replace(/_/g, ' ')}
    </div>
  );
}

interface LabelCanvasProps {
  template: LabelTemplate;
  canvasBorder?: 'none' | 'line' | 'dotted';
  selectedId: string | null;
  onSelect: (id: string | null) => void;
  onUpdateElement: (id: string, patch: Partial<LabelElement>) => void;
  onDeleteElement: (id: string) => void;
  onTemplateChange?: (patch: Partial<LabelTemplate>) => void;
  scale?: number;
  onZoomIn?: () => void;
  onZoomOut?: () => void;
  /** 将缩放还原为 100%（与顶部标尺物理尺寸一致），并重新居中画布 */
  onResetZoom?: () => void;
  onPreview?: () => void;
  /** 为 true 时不在预览工具栏显示画布尺寸预设（改由顶部表单控制） */
  hideToolbarPresetSize?: boolean;
}

type PaperResizeEdge =
  | 'bottom'
  | 'right'
  | 'top'
  | 'left'
  | 'top-left'
  | 'top-right'
  | 'bottom-left'
  | 'bottom-right';

function cursorForPaperResizeEdge(edge: PaperResizeEdge): string {
  if (edge === 'top' || edge === 'bottom') return 'ns-resize';
  if (edge === 'left' || edge === 'right') return 'ew-resize';
  if (edge === 'top-left' || edge === 'bottom-right') return 'nwse-resize';
  return 'nesw-resize';
}

export function LabelCanvas({
  template,
  canvasBorder = 'none',
  selectedId,
  onSelect,
  onUpdateElement,
  onDeleteElement,
  onTemplateChange,
  scale = 1,
  onZoomIn,
  onZoomOut,
  onResetZoom,
  onPreview,
  hideToolbarPresetSize = false,
}: LabelCanvasProps) {
  const scrollContainerRef = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLDivElement>(null);
  const dragRef = useRef<{
    id: string;
    startX: number;
    startY: number;
    elX: number;
    elY: number;
    w: number;
    h: number;
  } | null>(null);
  const resizeRef = useRef<{ id: string; corner: string; startX: number; startY: number; w: number; h: number; elX: number; elY: number } | null>(null);
  const paperResizeRef = useRef<{
    edge: PaperResizeEdge;
    startX: number;
    startY: number;
    startW: number;
    startH: number;
    startElements: { id: string; x: number; y: number }[];
  } | null>(null);
  const lastUpdateRef = useRef<{ id: string; x?: number; y?: number; width?: number; height?: number } | null>(null);

  const nextFrameRef = useRef<number | null>(null);
  const [isSpacePressed, setIsSpacePressed] = React.useState(false);
  const [isPanning, setIsPanning] = React.useState(false);
  const [paperResizeCursor, setPaperResizeCursor] = React.useState<string | null>(null);
  const panStartRef = useRef<{ x: number; y: number; scrollLeft: number; scrollTop: number } | null>(null);
  const [panOffset, setPanOffset] = React.useState({ x: 0, y: 0 });
  const panOffsetStartRef = useRef<{ x: number; y: number; startX: number; startY: number } | null>(null);
  /** 仅影响预览区标尺刻度/读数，与顶部表单画布单位（template.unit）不同步 */
  const [previewRulerUnit, setPreviewRulerUnit] = useState<PreviewRulerDisplayUnit>("cm");

  const baseW = unitToPx(template.width, template.unit);
  const baseH = unitToPx(template.height, template.unit);
  /** 缩放后的实际占位，用于滚动区域与居中，避免放大后画布被裁切 */
  const widthPx = baseW * scale;
  const heightPx = baseH * scale;
  const showGrid = template.showGrid !== false;
  const canvasBorderClass =
    canvasBorder === 'line'
      ? 'border border-gray-500'
      : canvasBorder === 'dotted'
        ? 'border border-dotted border-gray-500'
        : 'border border-transparent';

  const labelWorkspaceLayoutRef = useRef<HTMLDivElement>(null);
  const [rulerLayoutWidth, setRulerLayoutWidth] = useState(0);
  useLayoutEffect(() => {
    const el = labelWorkspaceLayoutRef.current;
    if (!el) return;
    const ro = new ResizeObserver((entries) => {
      for (const e of entries) {
        setRulerLayoutWidth(e.contentRect.width);
      }
    });
    ro.observe(el);
    setRulerLayoutWidth(el.getBoundingClientRect().width);
    return () => ro.disconnect();
  }, []);

  const rulerTotalWidth = Math.max(1, rulerLayoutWidth, widthPx);
  const paperOffsetX = Math.max(0, (rulerTotalWidth - widthPx) / 2);
  const rulerSelection = useMemo(() => {
    if (!selectedId) return null;
    const el = template.elements.find((x) => x.id === selectedId);
    if (!el) return null;
    return { x: el.x, width: el.width };
  }, [selectedId, template.elements]);

  const handlePointerDown = useCallback(
    (e: React.PointerEvent, id: string) => {
      // 如果按住了空格，直接返回，交给外层 panning 处理
      // 允许中键 (button 1) 拖动
      if (isSpacePressed || e.button === 1) return;

      e.stopPropagation();
      onSelect(id);

      // Focus canvas for keyboard events
      canvasRef.current?.focus();

      const el = template.elements.find((x) => x.id === id);
      if (!el) return;

      const domEl = document.getElementById(`element-${id}`);
      if (domEl) {
        domEl.classList.add('z-50', 'opacity-90', 'shadow-xl', 'ring-2', 'ring-blue-400', 'ring-offset-2');
        domEl.style.cursor = 'grabbing';
      }

      dragRef.current = {
        id,
        startX: e.clientX,
        startY: e.clientY,
        elX: el.x,
        elY: el.y,
        w: el.width,
        h: el.height,
      };
      lastUpdateRef.current = { id, x: el.x, y: el.y }; // 初始化
      (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
    },
    [template.elements, onSelect, isSpacePressed]
  );

  const requestUpdate = useCallback((updateFn: () => void) => {
    if (nextFrameRef.current !== null) {
      cancelAnimationFrame(nextFrameRef.current);
    }
    nextFrameRef.current = requestAnimationFrame(() => {
      updateFn();
      nextFrameRef.current = null;
    });
  }, []);

  const beginPaperResize = useCallback((e: React.PointerEvent, edge: PaperResizeEdge) => {
    e.stopPropagation();
    paperResizeRef.current = {
      edge,
      startX: e.clientX,
      startY: e.clientY,
      startW: template.width,
      startH: template.height,
      startElements: template.elements.map((el) => ({ id: el.id, x: el.x, y: el.y })),
    };
    const cursor = cursorForPaperResizeEdge(edge);
    setPaperResizeCursor(cursor);
    document.body.style.cursor = cursor;
    (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
  }, [template.width, template.height, template.elements]);

  const handlePointerMove = useCallback(
    (e: React.PointerEvent) => {
      // 画布平移：优先处理（translate 方式，不依赖滚动）
      if (isPanning && panOffsetStartRef.current) {
        const dx = e.clientX - panOffsetStartRef.current.startX;
        const dy = e.clientY - panOffsetStartRef.current.startY;
        setPanOffset({
          x: panOffsetStartRef.current.x + dx,
          y: panOffsetStartRef.current.y + dy,
        });
        return;
      }
      if (isPanning && panStartRef.current && scrollContainerRef.current) {
        const dx = e.clientX - panStartRef.current.x;
        const dy = e.clientY - panStartRef.current.y;
        scrollContainerRef.current.scrollLeft = panStartRef.current.scrollLeft - dx;
        scrollContainerRef.current.scrollTop = panStartRef.current.scrollTop - dy;
        return;
      }
      // Drag Element
      if (dragRef.current) {
        const { id, startX, startY, elX, elY, w, h } = dragRef.current;
        const clientX = e.clientX;
        const clientY = e.clientY;

        requestUpdate(() => {
          const dx = (clientX - startX) / scale;
          const dy = (clientY - startY) / scale;
          const rawX = elX + dx;
          const rawY = elY + dy;
          const { x: snappedX, y: snappedY } = clampDragPosition(
            rawX,
            rawY,
            w,
            h,
            baseW,
            baseH,
            LABEL_CANVAS_SAFE_MARGIN_PX,
          );

          const domEl = document.getElementById(`element-${id}`);
          if (domEl) {
            domEl.style.left = `${snappedX}px`;
            domEl.style.top = `${snappedY}px`;
          }
          lastUpdateRef.current = { id, x: snappedX, y: snappedY };
        });
      }

      // Resize Element
      if (resizeRef.current) {
        const { id, corner, startX, startY, w, h, elX, elY } = resizeRef.current;
        const clientX = e.clientX;
        const clientY = e.clientY;

        requestUpdate(() => {
          const dx = (clientX - startX) / scale;
          const dy = (clientY - startY) / scale;
          let nw = w;
          let nh = h;
          let nx = elX;
          let ny = elY;
          if (corner.includes('e')) nw = Math.max(20, w + dx);
          if (corner.includes('w')) {
            nw = Math.max(20, w - dx);
            // Keep the right edge anchored when width hits min limit.
            nx = elX + (w - nw);
          }
          if (corner.includes('s')) nh = Math.max(12, h + dy);
          if (corner.includes('n')) {
            nh = Math.max(12, h - dy);
            // Keep the bottom edge anchored when height hits min limit.
            ny = elY + (h - nh);
          }
          const snappedW = snapToGrid(nw);
          const snappedH = snapToGrid(nh);
          const snappedX = snapToGrid(nx);
          const snappedY = snapToGrid(ny);
          const clamped = clampLabelElementBox(
            snappedX,
            snappedY,
            snappedW,
            snappedH,
            baseW,
            baseH,
            LABEL_CANVAS_SAFE_MARGIN_PX,
          );

          const domEl = document.getElementById(`element-${id}`);
          if (domEl) {
            domEl.style.width = `${clamped.w}px`;
            domEl.style.height = `${clamped.h}px`;
            domEl.style.left = `${clamped.x}px`;
            domEl.style.top = `${clamped.y}px`;
          }

          lastUpdateRef.current = {
            id,
            width: clamped.w,
            height: clamped.h,
            x: clamped.x,
            y: clamped.y,
          };
        });
      }

      // Resize Paper
      if (paperResizeRef.current && onTemplateChange) {
        const { edge, startX, startY, startW, startH, startElements } = paperResizeRef.current;
        const clientX = e.clientX;
        const clientY = e.clientY;

        requestUpdate(() => {
          const deltaPxX = (clientX - startX) / scale;
          const deltaPxY = (clientY - startY) / scale;

          const minPaperUnit = 1;
          const startWPx = unitToPx(startW, template.unit);
          const startHPx = unitToPx(startH, template.unit);
          const minWPx = unitToPx(minPaperUnit, template.unit);
          const minHPx = unitToPx(minPaperUnit, template.unit);

          const affectsTop = edge === 'top' || edge === 'top-left' || edge === 'top-right';
          const affectsBottom = edge === 'bottom' || edge === 'bottom-left' || edge === 'bottom-right';
          const affectsLeft = edge === 'left' || edge === 'top-left' || edge === 'bottom-left';
          const affectsRight = edge === 'right' || edge === 'top-right' || edge === 'bottom-right';

          let nextWUnit = startW;
          let nextHUnit = startH;
          let offsetContentPxX = 0;
          let offsetContentPxY = 0;

          if (affectsRight) {
            const proposedPx = Math.max(minWPx, startWPx + deltaPxX);
            nextWUnit = Math.max(minPaperUnit, Math.round(pxToUnit(proposedPx, template.unit)));
          }
          if (affectsBottom) {
            const proposedPx = Math.max(minHPx, startHPx + deltaPxY);
            nextHUnit = Math.max(minPaperUnit, Math.round(pxToUnit(proposedPx, template.unit)));
          }
          if (affectsLeft) {
            const proposedPx = Math.max(minWPx, startWPx - deltaPxX);
            nextWUnit = Math.max(minPaperUnit, Math.round(pxToUnit(proposedPx, template.unit)));
            const snappedPx = unitToPx(nextWUnit, template.unit);
            const appliedDelta = startWPx - snappedPx;
            offsetContentPxX = appliedDelta;
          }
          if (affectsTop) {
            const proposedPx = Math.max(minHPx, startHPx - deltaPxY);
            nextHUnit = Math.max(minPaperUnit, Math.round(pxToUnit(proposedPx, template.unit)));
            const snappedPx = unitToPx(nextHUnit, template.unit);
            const appliedDelta = startHPx - snappedPx;
            offsetContentPxY = appliedDelta;
          }

          const patch: Partial<LabelTemplate> = {};
          if (affectsLeft || affectsRight) patch.width = nextWUnit;
          if (affectsTop || affectsBottom) patch.height = nextHUnit;

          if ((offsetContentPxX !== 0 || offsetContentPxY !== 0) && startElements.length > 0) {
            const byId = new Map(startElements.map(s => [s.id, s]));
            patch.elements = template.elements.map((el) => {
              const s = byId.get(el.id);
              if (!s) return el;
              const nx = Math.max(0, s.x - offsetContentPxX);
              const ny = Math.max(0, s.y - offsetContentPxY);
              return (nx === el.x && ny === el.y) ? el : { ...el, x: nx, y: ny };
            });
          }

          onTemplateChange(patch);
        });
      }
    },
    [isPanning, onTemplateChange, scale, template.unit, requestUpdate, baseW, baseH]
  );

  const handlePointerUp = useCallback(() => {
    // 结束画布平移
    if (isPanning) {
      setIsPanning(false);
      panStartRef.current = null;
      panOffsetStartRef.current = null;
    }
    // Cancel pending animation frame
    if (nextFrameRef.current !== null) {
      cancelAnimationFrame(nextFrameRef.current);
      nextFrameRef.current = null;
    }

    const activeId = dragRef.current?.id || resizeRef.current?.id;
    if (activeId) {
      const domEl = document.getElementById(`element-${activeId}`);
      if (domEl) {
        domEl.classList.remove('z-50', 'opacity-90', 'shadow-xl', 'ring-2', 'ring-blue-400', 'ring-offset-2');
        domEl.style.cursor = '';
      }
    }

    if (lastUpdateRef.current) {
      const { id, ...patch } = lastUpdateRef.current;
      onUpdateElement(id, patch);
      lastUpdateRef.current = null;
    }
    dragRef.current = null;
    resizeRef.current = null;
    paperResizeRef.current = null;
    setPaperResizeCursor(null);
    document.body.style.cursor = '';
  }, [onUpdateElement]);

  useEffect(() => {
    const onKeyDown = (e: KeyboardEvent) => {
      if (e.code === 'Space' && !e.repeat) {
        setIsSpacePressed(true);
      }
    };
    const onKeyUp = (e: KeyboardEvent) => {
      if (e.code === 'Space') {
        setIsSpacePressed(false);
        setIsPanning(false);
        panStartRef.current = null;
        panOffsetStartRef.current = null;
      }
    };
    window.addEventListener('keydown', onKeyDown);
    window.addEventListener('keyup', onKeyUp);
    return () => {
      window.removeEventListener('keydown', onKeyDown);
      window.removeEventListener('keyup', onKeyUp);
    };
  }, []);

  useEffect(() => {
    if (!paperResizeCursor) return;
    document.body.style.cursor = paperResizeCursor;
    return () => {
      document.body.style.cursor = '';
    };
  }, [paperResizeCursor]);

  const centerScrollInViewport = useCallback(() => {
    const el = scrollContainerRef.current;
    if (!el) return;
    const run = () => {
      el.scrollLeft = Math.max(0, (el.scrollWidth - el.clientWidth) / 2);
      el.scrollTop = Math.max(0, (el.scrollHeight - el.clientHeight) / 2);
    };
    requestAnimationFrame(() => requestAnimationFrame(run));
  }, []);

  // 缩放或纸张尺寸变化：清空平移偏移，并把画布重新滚到视口中央，避免放大后靠边被遮挡
  useEffect(() => {
    setPanOffset({ x: 0, y: 0 });
    centerScrollInViewport();
    const t = window.setTimeout(centerScrollInViewport, 80);
    return () => window.clearTimeout(t);
  }, [scale, baseW, baseH, rulerLayoutWidth, centerScrollInViewport]);

  // Keyboard navigation for elements
  const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
    if (!selectedId) return;
    if (e.key === 'Delete' || e.key === 'Backspace') {
      // ... existing delete logic
      e.preventDefault();
      const idx = template.elements.findIndex((x) => x.id === selectedId);
      if (idx >= 0) {
        const next = template.elements.filter((x) => x.id !== selectedId);
        onDeleteElement(selectedId);
        onSelect(next[idx]?.id ?? next[idx - 1]?.id ?? null);
      }
      return;
    }

    const el = template.elements.find(x => x.id === selectedId);
    if (!el) return;

    // allow typing in inputs without triggering move? 
    // Actually our elements are not inputs (unless we implement inline edit).
    // But preventDefault is good.

    const step = e.shiftKey ? 1 : GRID_SIZE;
    let dx = 0;
    let dy = 0;

    switch (e.key) {
      case 'ArrowLeft': dx = -step; break;
      case 'ArrowRight': dx = step; break;
      case 'ArrowUp': dy = -step; break;
      case 'ArrowDown': dy = step; break;
      default: return;
    }

    e.preventDefault();
    const nx = el.x + dx;
    const ny = el.y + dy;
    const { x, y } = clampDragPosition(
      nx,
      ny,
      el.width,
      el.height,
      baseW,
      baseH,
      LABEL_CANVAS_SAFE_MARGIN_PX,
    );
    onUpdateElement(el.id, { x, y });

  }, [selectedId, template.elements, onUpdateElement, onDeleteElement, onSelect, baseW, baseH]);

  const canvasClick = () => onSelect(null);

  // 容器的 Pan 处理
  // 容器的 Pan 处理
  const handleContainerPointerDown = (e: React.PointerEvent) => {
    if (isSpacePressed || e.button === 1) {
      e.preventDefault();
      setIsPanning(true);
      panStartRef.current = {
        x: e.clientX,
        y: e.clientY,
        scrollLeft: scrollContainerRef.current?.scrollLeft || 0,
        scrollTop: scrollContainerRef.current?.scrollTop || 0
      };
      (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
    }
  };

  const handleContainerPointerMove = (e: React.PointerEvent) => {
    if (isPanning && panStartRef.current && scrollContainerRef.current) {
      const dx = e.clientX - panStartRef.current.x;
      const dy = e.clientY - panStartRef.current.y;
      scrollContainerRef.current.scrollLeft = panStartRef.current.scrollLeft - dx;
      scrollContainerRef.current.scrollTop = panStartRef.current.scrollTop - dy;
    }
  };

  const handleContainerPointerUp = (e: React.PointerEvent) => {
    if (isPanning) {
      setIsPanning(false);
      panStartRef.current = null;
    }
  };

  return (
    <div
      className="flex flex-col overflow-hidden bg-gray-100"
      style={{
        flex: "1 1 0%",
        minHeight: 0,
        display: "flex",
        flexDirection: "column",
        overflow: "hidden",
      }}
    >
      {/* Label Preview 标题 + 网格/预览/缩放 */}
      <div className="shrink-0 px-4 py-2 border-b border-gray-200 bg-white flex flex-nowrap items-center justify-between gap-3 z-10 min-h-[44px]">
        <span className="text-sm font-medium text-gray-700 shrink-0 min-w-0 truncate">Label Preview</span>
        <div className="flex flex-nowrap items-center justify-end gap-2 shrink-0 min-w-0">
          {onPreview && (
            <button
              type="button"
              onClick={onPreview}
              className="h-8 px-3 rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 text-xs font-medium shadow-sm transition-all active:scale-95 shrink-0"
            >
              Preview
            </button>
          )}
          {onTemplateChange && !hideToolbarPresetSize ? (
            <Select
              value={(() => {
                const i = PRESET_LABEL_SIZES.findIndex(
                  (p) => p.width === template.width && p.height === template.height && p.unit === template.unit
                );
                return i >= 0 ? String(i) : 'custom';
              })()}
              onValueChange={(v: string) => {
                if (v === 'custom') return;
                const p = PRESET_LABEL_SIZES[Number(v)];
                if (p) onTemplateChange({ width: p.width, height: p.height, unit: p.unit });
              }}
            >
              <SelectTrigger className="h-8 w-[130px] max-w-[130px] text-xs shrink-0">
                <SelectValue placeholder="Canvas size" />
              </SelectTrigger>
              <SelectContent>
                {PRESET_LABEL_SIZES.map((p, i) => (
                  <SelectItem key={i} value={String(i)} className="text-xs">
                    {p.name}
                  </SelectItem>
                ))}
                <SelectItem value="custom" className="text-xs text-gray-500">
                  Custom
                </SelectItem>
              </SelectContent>
            </Select>
          ) : null}
          {onTemplateChange ? (
            <button
              type="button"
              onClick={() => onTemplateChange({ showGrid: !showGrid })}
              className={cn(
                'h-8 px-3 rounded border text-xs font-medium shadow-sm transition-colors shrink-0',
                showGrid ? 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50' : 'border-gray-300 bg-gray-100 text-gray-500'
              )}
            >
              {showGrid ? 'Hide grid' : 'Show grid'}
            </button>
          ) : null}
          <div className="flex items-center gap-1 bg-white rounded border border-gray-300 p-0.5 shadow-sm h-8 shrink-0">
            <button
              type="button"
              onClick={onZoomOut}
              disabled={!onZoomOut}
              className="h-6 w-6 rounded hover:bg-gray-100 text-gray-600 disabled:opacity-50 disabled:pointer-events-none flex items-center justify-center text-sm font-medium active:scale-90 transition-transform"
              title="Zoom out"
            >
              −
            </button>
            <span className="min-w-[3rem] text-center text-xs text-gray-600 font-medium">
              {Math.round(scale * 100)}%
            </span>
            <button
              type="button"
              onClick={onZoomIn}
              disabled={!onZoomIn}
              className="h-6 w-6 rounded hover:bg-gray-100 text-gray-600 disabled:opacity-50 disabled:pointer-events-none flex items-center justify-center text-sm font-medium active:scale-90 transition-transform"
              title="Zoom in"
            >
              +
            </button>
          </div>
          {onResetZoom ? (
            <button
              type="button"
              onClick={onResetZoom}
              className="h-8 px-3 rounded border border-blue-200 bg-blue-50 text-blue-800 hover:bg-blue-100 text-xs font-medium shadow-sm transition-all active:scale-95 shrink-0"
              title="Reset zoom to 100% (match ruler canvas size, e.g. 3×2 inch)"
            >
              Restore size
            </button>
          ) : null}
          <Select
            value={previewRulerUnit}
            onValueChange={(v: PreviewRulerDisplayUnit) => setPreviewRulerUnit(v)}
          >
            <SelectTrigger
              className={cn(
                "h-8 w-auto min-w-[4.25rem] max-w-[5.5rem] px-3 text-xs font-medium shrink-0 rounded border border-gray-300 bg-white text-gray-700 shadow-sm",
                "hover:bg-gray-50 transition-all active:scale-95",
                "focus:ring-0 focus:ring-offset-0 data-[state=open]:bg-gray-50",
              )}
              title="Preview ruler unit (does not change canvas size)"
            >
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="cm" className="text-xs">
                cm
              </SelectItem>
              <SelectItem value="mm" className="text-xs">
                mm
              </SelectItem>
              <SelectItem value="inch" className="text-xs">
                inch
              </SelectItem>
            </SelectContent>
          </Select>
        </div>
      </div>

      {/* Canvas Container：底层灰底铺满可视区，内容层可滚动 */}
      <div
        ref={scrollContainerRef}
        className={cn(
          "overflow-auto relative",
          isSpacePressed ? "cursor-grab active:cursor-grabbing" : ""
        )}
        style={{ flex: "1 1 0%", minHeight: 0, overflow: "auto", position: "relative" }}
        onPointerDown={handleContainerPointerDown}
        onPointerMove={handleContainerPointerMove}
        onPointerUp={handleContainerPointerUp}
        onPointerLeave={handleContainerPointerUp}
      >
        <div
          className="pointer-events-none absolute inset-0 z-0 bg-gray-100"
          aria-hidden
        />
        <div
          className="relative z-[1] flex min-w-full w-max min-h-full flex-col box-border p-[50px]"
          style={{
            transform: `translate(${panOffset.x}px, ${panOffset.y}px)`,
          }}
        >
          <div ref={labelWorkspaceLayoutRef} className="flex w-full min-w-full flex-col">
            <div className="mb-2 w-full min-w-0">
              <RulerBarHorizontal
                rulerTotalWidthPx={rulerTotalWidth}
                paperWidthPx={widthPx}
                paperOffsetLeftPx={paperOffsetX}
                paperWidthTemplate={template.width}
                templateUnit={template.unit}
                displayUnit={previewRulerUnit}
                baseW={baseW}
                selection={rulerSelection}
              />
            </div>
            <div className="flex w-full min-w-0 justify-center">
              <div className="shrink-0 relative overflow-visible" style={{ width: widthPx, height: heightPx }}>
            <div
              ref={canvasRef}
              tabIndex={0}
              className={cn(
                'absolute left-0 top-0 bg-white shadow-lg outline-none',
                canvasBorderClass,
                isPanning ? 'cursor-grabbing' : 'cursor-grab'
              )}
              style={{
                width: baseW,
                height: baseH,
                transform: `scale(${scale})`,
                transformOrigin: 'top left',
                backgroundImage: showGrid
                  ? `linear-gradient(to right, rgba(0,0,0,0.06) 1px, transparent 1px),
                     linear-gradient(to bottom, rgba(0,0,0,0.06) 1px, transparent 1px)`
                  : undefined,
                backgroundSize: showGrid ? `${GRID_SIZE}px ${GRID_SIZE}px` : undefined,
                // 如果按住空格，禁用 canvas 内部的 pointer-events 以便拖动容器
                pointerEvents: isSpacePressed ? 'none' : 'auto',
                cursor: paperResizeCursor ?? undefined,
              }}
              onClick={(e) => {
                // 点击画布空白处取消选中
                const target = e.target as HTMLElement;
                const isOnElement = target.closest('[id^="element-"]');
                const isOnPaperResize =
                  target.closest('[data-paper-resize-handle="true"]') ||
                  target.closest('[title*="Drag to resize paper"]') ||
                  target.closest('[title*="Drag to increase paper height"]') ||
                  target.closest('[title*="Drag to increase paper width"]');
                if (!isOnElement && !isOnPaperResize) {
                  onSelect(null);
                }
              }}
              onPointerDown={(e) => {
                // 空白处按下即开始平移（在画布内且未点到元素/纸张拖拽条）
                const target = e.target as HTMLElement;
                const isOnElement = target.closest('[id^="element-"]');
                const isOnPaperResize =
                  target.closest('[data-paper-resize-handle="true"]') ||
                  target.closest('[title*="Drag to resize paper"]') ||
                  target.closest('[title*="Drag to increase paper height"]') ||
                  target.closest('[title*="Drag to increase paper width"]');
                const isOnCanvasArea = canvasRef.current?.contains(target);
                if (isOnCanvasArea && !isOnElement && !isOnPaperResize && !dragRef.current && !resizeRef.current) {
                  if (isSpacePressed || e.button === 1) {
                    e.preventDefault();
                    e.stopPropagation();
                    setIsPanning(true);
                    panOffsetStartRef.current = {
                      x: panOffset.x,
                      y: panOffset.y,
                      startX: e.clientX,
                      startY: e.clientY,
                    };
                    panStartRef.current = {
                      x: e.clientX,
                      y: e.clientY,
                      scrollLeft: scrollContainerRef.current?.scrollLeft ?? 0,
                      scrollTop: scrollContainerRef.current?.scrollTop ?? 0,
                    };
                    (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
                  }
                }
              }}
              onPointerMove={handlePointerMove}
              onPointerUp={handlePointerUp}
              onKeyDown={handleKeyDown}
            >
            {/* 选中元素对齐参考线：延伸至画布四边，蓝色虚线 */}
            {selectedId
              ? (() => {
                  const el = template.elements.find((e) => e.id === selectedId);
                  if (!el) return null;
                  const lineCls = "pointer-events-none absolute z-[2] border-blue-600";
                  return (
                    <>
                      <div
                        className={cn(lineCls, "left-0 right-0 border-t border-dashed")}
                        style={{ top: el.y }}
                        aria-hidden
                      />
                      <div
                        className={cn(lineCls, "left-0 right-0 border-t border-dashed")}
                        style={{ top: el.y + el.height }}
                        aria-hidden
                      />
                      <div
                        className={cn(lineCls, "top-0 bottom-0 border-l border-dashed")}
                        style={{ left: el.x }}
                        aria-hidden
                      />
                      <div
                        className={cn(lineCls, "top-0 bottom-0 border-l border-dashed")}
                        style={{ left: el.x + el.width }}
                        aria-hidden
                      />
                    </>
                  );
                })()
              : null}
            {/* Paper resize: top */}
            {onTemplateChange && (
              <div
                className="absolute left-0 right-0 h-3 cursor-ns-resize flex items-center justify-center bg-gray-200/80 hover:bg-blue-400/30 border-b border-gray-300 text-[10px] text-gray-500 transition-colors"
                style={{ top: 0 }}
                title="Drag to resize paper (top edge)"
                data-paper-resize-handle="true"
                onPointerDown={(e) => beginPaperResize(e, 'top')}
              >
                ⋮
              </div>
            )}
            {/* Paper resize: left */}
            {onTemplateChange && (
              <div
                className="absolute top-0 bottom-0 w-3 cursor-ew-resize flex items-center justify-center bg-gray-200/80 hover:bg-blue-400/30 border-r border-gray-300 text-[10px] text-gray-500 transition-colors"
                style={{ top: 0 }}
                title="Drag to resize paper (left edge)"
                data-paper-resize-handle="true"
                onPointerDown={(e) => beginPaperResize(e, 'left')}
              >
                ⋮
              </div>
            )}
            {/* 纸张四角拖拽缩放已关闭；宽高请在顶部表单修改 */}
            {/* 纸张尺寸拖拽：底部拉高 */}
            {onTemplateChange && (
              <div
                className="absolute bottom-0 left-0 right-0 h-3 cursor-ns-resize flex items-center justify-center bg-gray-200/80 hover:bg-blue-400/30 border-t border-gray-300 text-[10px] text-gray-500 transition-colors"
                title="Drag to resize paper (bottom edge)"
                data-paper-resize-handle="true"
                onPointerDown={(e) => beginPaperResize(e, 'bottom')}
              >
                ⋮
              </div>
            )}
            {/* 纸张尺寸拖拽：右侧拉宽 */}
            {onTemplateChange && (
              <div
                className="absolute top-0 right-0 bottom-0 w-3 cursor-ew-resize flex items-center justify-center bg-gray-200/80 hover:bg-blue-400/30 border-l border-gray-300 text-[10px] text-gray-500 transition-colors"
                title="Drag to resize paper (right edge)"
                data-paper-resize-handle="true"
                onPointerDown={(e) => beginPaperResize(e, 'right')}
              >
                ⋮
              </div>
            )}
            {template.elements.map((el) => {
              const isPrintField = isPrintInputElement(el);
              return (
              <div
                key={el.id}
                id={`element-${el.id}`}
                className={cn(
                  'absolute box-border cursor-move overflow-hidden transition-shadow',
                  el.border === 'line' && 'border border-gray-400',
                  el.border === 'dotted' && 'border border-dotted border-gray-400',
                  selectedId === el.id && 'ring-2 ring-blue-500 ring-offset-1 z-10'
                )}
                style={{
                  left: el.x,
                  top: el.y,
                  width: el.width,
                  height: el.height,
                }}
                onClick={(e) => {
                  e.stopPropagation();
                  onSelect(el.id);
                }}
                onPointerDown={(e) => handlePointerDown(e, el.id)}
              >
                <div
                  className={cn(
                    'w-full h-full min-h-0 relative',
                    isPrintField &&
                      'rounded-sm border-2 border-dashed border-amber-500/85 bg-amber-50/35',
                  )}
                >
                  <ElementContent el={el} isAppPrintField={isPrintField} />
                </div>
                {selectedId === el.id && (
                  <>
                    {/* 4 Corners */}
                    {(['nw', 'ne', 'sw', 'se'] as const).map((corner) => (
                      <div
                        key={corner}
                        className="absolute w-3.5 h-3.5 bg-white border-2 border-blue-600 rounded-none z-20 shadow-sm hover:scale-110 transition-transform"
                        style={{
                          cursor: 'nwse-resize',
                          top: corner.startsWith('n') ? -6 : undefined,
                          bottom: corner.startsWith('s') ? -6 : undefined,
                          left: corner.endsWith('w') ? -6 : undefined,
                          right: corner.endsWith('e') ? -6 : undefined,
                        }}
                        onPointerDown={(e) => {
                          e.stopPropagation();
                          const el0 = template.elements.find((x) => x.id === el.id)!;
                          resizeRef.current = {
                            id: el.id,
                            corner,
                            startX: e.clientX,
                            startY: e.clientY,
                            w: el0.width,
                            h: el0.height,
                            elX: el0.x,
                            elY: el0.y,
                          };
                          (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
                        }}
                      />
                    ))}
                    {/* 4 Edges */}
                    {(['n', 's', 'w', 'e'] as const).map((edge) => (
                      <div
                        key={edge}
                        className="absolute bg-white border-2 border-blue-600 rounded-none z-10 shadow-sm hover:bg-blue-50"
                        style={{
                          cursor: edge === 'n' || edge === 's' ? 'ns-resize' : 'ew-resize',
                          width: edge === 'n' || edge === 's' ? '20px' : '6px',
                          height: edge === 'n' || edge === 's' ? '6px' : '20px',
                          top: edge === 'n' ? -3 : edge === 's' ? undefined : '50%',
                          bottom: edge === 's' ? -3 : undefined,
                          left: edge === 'w' ? -3 : edge === 'e' ? undefined : '50%',
                          right: edge === 'e' ? -3 : undefined,
                          transform: edge === 'n' || edge === 's' ? 'translateX(-50%)' : 'translateY(-50%)',
                        }}
                        onPointerDown={(e) => {
                          e.stopPropagation();
                          const el0 = template.elements.find((x) => x.id === el.id)!;

                          const domEl = document.getElementById(`element-${el.id}`);
                          if (domEl) {
                            domEl.classList.add('z-50', 'opacity-90');
                          }

                          resizeRef.current = {
                            id: el.id,
                            corner: edge,
                            startX: e.clientX,
                            startY: e.clientY,
                            w: el0.width,
                            h: el0.height,
                            elX: el0.x,
                            elY: el0.y,
                          };
                          (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
                        }}
                      />
                    ))}
                  </>
                )}
              </div>
            );
            })}
            {baseW > LABEL_CANVAS_SAFE_MARGIN_PX * 2 && baseH > LABEL_CANVAS_SAFE_MARGIN_PX * 2 ? (
              <svg
                className="pointer-events-none absolute left-0 top-0 z-[12]"
                width={baseW}
                height={baseH}
                style={{ overflow: "visible" }}
                aria-hidden
              >
                {(() => {
                  const m = LABEL_CANVAS_SAFE_MARGIN_PX;
                  const stroke = "#2563eb";
                  const sw = 2;
                  const dash = "10 6";
                  return (
                    <>
                      <line x1={0} y1={m} x2={baseW} y2={m} stroke={stroke} strokeWidth={sw} strokeDasharray={dash} />
                      <line
                        x1={0}
                        y1={baseH - m}
                        x2={baseW}
                        y2={baseH - m}
                        stroke={stroke}
                        strokeWidth={sw}
                        strokeDasharray={dash}
                      />
                      <line x1={m} y1={0} x2={m} y2={baseH} stroke={stroke} strokeWidth={sw} strokeDasharray={dash} />
                      <line
                        x1={baseW - m}
                        y1={0}
                        x2={baseW - m}
                        y2={baseH}
                        stroke={stroke}
                        strokeWidth={sw}
                        strokeDasharray={dash}
                      />
                    </>
                  );
                })()}
              </svg>
            ) : null}
              </div>
            </div>
          </div>
        </div>
      </div>
      </div>
    </div>
  );
}

/** Preview only: no grid, no rulers, no drag; scale to fit. */
export function LabelPreviewOnly({
  template,
  canvasBorder = 'none',
  maxWidth = 480,
}: {
  template: LabelTemplate;
  canvasBorder?: 'none' | 'line' | 'dotted';
  maxWidth?: number;
}) {
  const baseW = unitToPx(template.width, template.unit);
  const baseH = unitToPx(template.height, template.unit);
  const minX = Math.min(0, ...template.elements.map((el) => el.x));
  const minY = Math.min(0, ...template.elements.map((el) => el.y));
  const maxX = Math.max(baseW, ...template.elements.map((el) => el.x + el.width));
  const maxY = Math.max(baseH, ...template.elements.map((el) => el.y + el.height));
  const contentW = Math.max(1, maxX - minX);
  const contentH = Math.max(1, maxY - minY);
  const scaleToFit = maxWidth ? Math.min(maxWidth / contentW, maxWidth / contentH, 2) : 1;
  const displayW = contentW * scaleToFit;
  const displayH = contentH * scaleToFit;
  const previewBorderClass =
    canvasBorder === 'line'
      ? 'border border-gray-500'
      : canvasBorder === 'dotted'
        ? 'border border-dotted border-gray-500'
        : 'border border-transparent';
  // 与编辑区一致：内层 baseW×baseH，transformOrigin 0 0 缩放，保证位置/样式一致
  return (
    <div
      className="inline-flex items-center justify-center p-4 bg-gray-100 rounded"
      style={{ minWidth: displayW + 32 }}
    >
      <div
        style={{ width: displayW, height: displayH }}
        className={cn("relative bg-white shadow-lg overflow-hidden", previewBorderClass)}
      >
        <div
          className="origin-top-left"
          style={{
            position: 'absolute',
            left: 0,
            top: 0,
            width: contentW,
            height: contentH,
            transform: `scale(${scaleToFit})`,
            transformOrigin: '0 0',
          }}
        >
          {template.elements.map((el) => {
            const isPrintField = isPrintInputElement(el);
            return (
            <div
              key={el.id}
              className="absolute box-border overflow-hidden pointer-events-none flex items-center justify-center text-xs"
              style={{
                left: el.x - minX,
                top: el.y - minY,
                width: el.width,
                height: el.height,
                border: el.border === 'line' ? '1px solid #999' : el.border === 'dotted' ? '1px dotted #999' : undefined,
              }}
            >
              <div
                className={cn(
                  'w-full h-full min-h-0 relative',
                  isPrintField &&
                    'rounded-sm border-2 border-dashed border-amber-500/85 bg-amber-50/35',
                )}
              >
                <ElementContent el={el} isAppPrintField={isPrintField} />
              </div>
            </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}
