import React, { useState, useCallback, useEffect, useRef } from 'react';
import { createRoot } from 'react-dom/client';
import {
  ReactFlow,
  ReactFlowProvider,
  Background,
  Controls,
  applyNodeChanges,
  applyEdgeChanges,
  addEdge,
  Handle,
  Position,
  NodeResizer,
  NodeToolbar,
  BaseEdge,
  EdgeLabelRenderer,
  getBezierPath,
  MarkerType,
  useInternalNode,
  useReactFlow,
  useViewport,
  useStore,
} from '@xyflow/react';
import { 
  StickyNote, Type, Trash2, ArrowRight, ArrowLeftRight, Minus, Bold, Italic, Plus, 
  Grid, Square, PenTool, Baseline, AlignLeft, AlignCenter, AlignRight, Settings, 
  Eraser, Highlighter, Link as LinkIcon, Paperclip, FileImage, Type as TypeIcon,
  Scissors, Copy as CopyIcon, ClipboardPaste, CopyPlus, ArrowUpToLine, ArrowDownToLine,
  Shapes, Circle, Triangle, Hexagon, Pentagon, Diamond, Star
} from 'lucide-react';
import { getStroke } from 'perfect-freehand';

// --- 預設常見字體清單 ---
const DEFAULT_FONTS = [
  { name: '系統預設', value: 'Inter, sans-serif' },
  { name: '黑體', value: '"Noto Sans TC", sans-serif' },
  { name: '明體', value: '"Noto Serif TC", serif' },
  { name: '圓體', value: '"Nunito", sans-serif' },
  { name: '手寫風', value: '"Klee One", cursive' },
];

export const GroupContext = React.createContext({ activeGroupId: null, setActiveGroupId: () => {}, toggleTreeCollapse: () => {} });

const CollapseButton = ({ id, isCollapsed }) => {
  const edges = useStore((s) => s.edges);
  const connections = edges.filter(e => e.source === id);
  const { toggleTreeCollapse } = React.useContext(GroupContext);
  
  if (connections.length === 0) return null;
  
  return (
    <div 
      className="tree-collapse-btn" 
      onClick={(e) => { e.stopPropagation(); toggleTreeCollapse && toggleTreeCollapse(id); }}
      title={isCollapsed ? "展開分支" : "收合分支"}
    >
      {isCollapsed ? <Plus size={12}/> : <Minus size={12}/>}
    </div>
  );
};

// --- 浮動邊界演算法 (Floating Edge Math) ---
function getNodeIntersection(intersectionNode, targetNode) {
  const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured;
  const intersectionNodePosition = intersectionNode.internals.positionAbsolute;
  const targetPosition = targetNode.internals.positionAbsolute;
  
  const w = intersectionNodeWidth / 2;
  const h = intersectionNodeHeight / 2;
  const x2 = intersectionNodePosition.x + w;
  const y2 = intersectionNodePosition.y + h;
  const x1 = targetPosition.x + (targetNode.measured?.width || 0) / 2;
  const y1 = targetPosition.y + (targetNode.measured?.height || 0) / 2;
  
  const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h);
  const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h);
  const a = 1 / (Math.max(0.001, Math.abs(xx1) + Math.abs(yy1)));
  const xx3 = a * xx1;
  const yy3 = a * yy1;
  const x = w * (xx3 + yy3) + x2;
  const y = h * (-xx3 + yy3) + y2;
  return { x, y };
}

function getEdgePosition(node, intersectionPoint) {
  const n = { ...node.internals.positionAbsolute, ...node.measured };
  const nx = Math.round(n.x);
  const ny = Math.round(n.y);
  const px = Math.round(intersectionPoint.x);
  const py = Math.round(intersectionPoint.y);

  if (px <= nx + 1) return Position.Left;
  if (px >= nx + n.width - 1) return Position.Right;
  if (py <= ny + 1) return Position.Top;
  if (py >= ny + n.height - 1) return Position.Bottom;
  return Position.Top;
}

function getEdgeParams(source, target) {
  const sourceIntersectionPoint = getNodeIntersection(source, target);
  const targetIntersectionPoint = getNodeIntersection(target, source);
  const sourcePos = getEdgePosition(source, sourceIntersectionPoint);
  const targetPos = getEdgePosition(target, targetIntersectionPoint);
  return {
    sx: sourceIntersectionPoint.x,
    sy: sourceIntersectionPoint.y,
    tx: targetIntersectionPoint.x,
    ty: targetIntersectionPoint.y,
    sourcePos,
    targetPos,
  };
}

const getSvgPathFromStroke = (stroke) => {
  if (!stroke.length) return '';
  const d = stroke.reduce(
    (acc, [x0, y0], i, arr) => {
      const [x1, y1] = arr[(i + 1) % arr.length];
      acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
      return acc;
    },
    ['M', ...stroke[0], 'Q']
  );
  d.push('Z');
  return d.join(' ');
};

// --- Rich Text Utils ---
export const applyInlineStyle = (e, styleName, value, fallbackCallback) => {
  e.preventDefault();
  const selection = window.getSelection();
  if (!selection.rangeCount || selection.isCollapsed) {
    if (fallbackCallback) fallbackCallback();
    return;
  }
  
  const range = selection.getRangeAt(0);
  let container = range.commonAncestorContainer;
  if (container.nodeType === 3) container = container.parentNode;
  
  const editor = container.closest('.rich-text-editor');
  if (!editor) {
    if (fallbackCallback) fallbackCallback();
    return;
  }
  
  if (styleName === 'fontWeight') {
    document.execCommand('bold', false, null);
  } else if (styleName === 'fontStyle') {
    document.execCommand('italic', false, null);
  } else if (styleName === 'textAlign') {
    if (value === 'left') document.execCommand('justifyLeft', false, null);
    else if (value === 'center') document.execCommand('justifyCenter', false, null);
    else if (value === 'right') document.execCommand('justifyRight', false, null);
  } else {
    document.execCommand('styleWithCSS', false, true);
    if (styleName === 'color') {
      document.execCommand('foreColor', false, value);
    } else if (styleName === 'fontFamily') {
      document.execCommand('fontName', false, value);
    } else if (styleName === 'fontSize') {
      document.execCommand('fontSize', false, 7);
      const spans = editor.querySelectorAll('span[style*="xxx-large"]');
      spans.forEach(s => {
        s.style.fontSize = `${value}px`;
      });
    }
  }
  
  editor.dispatchEvent(new Event('input', { bubbles: true }));
};

const RichTextEditor = ({ html, onChange, placeholder, style, className, onClick, onKeyDown }) => {
  const contentEditableRef = useRef(null);

  useEffect(() => {
    if (contentEditableRef.current && html !== contentEditableRef.current.innerHTML) {
      contentEditableRef.current.innerHTML = html || '';
    }
  }, [html]);

  const handleInput = (e) => {
    onChange(e.target.innerHTML);
  };

  const handlePaste = (e) => {
    e.stopPropagation(); // 阻止事件冒泡到全域畫布的 paste 監聽器
  };

  return (
    <div
      ref={contentEditableRef}
      className={`nodrag rich-text-editor ${className || ''}`}
      contentEditable
      onInput={handleInput}
      onBlur={handleInput}
      onPaste={handlePaste}
      onClick={onClick}
      onKeyDown={onKeyDown}
      style={{ ...style, outline: 'none', minHeight: '1.2em', whiteSpace: 'pre-wrap', wordBreak: 'break-word', width: '100%', height: '100%' }}
      data-placeholder={placeholder}
    />
  );
};

// --- 元件區 ---
const TextFormatToolbar = ({ id, data, isPlainText = false }) => {
  const textStyle = data.textStyle || {};
  const fontSize = textStyle.fontSize || 15;
  const isBold = textStyle.fontWeight === 'bold';
  const isItalic = textStyle.fontStyle === 'italic';
  const textAlign = textStyle.textAlign || 'left';
  const fontsList = data.fonts || DEFAULT_FONTS;
  const [showFonts, setShowFonts] = useState(false);

  // 暫存游標位置供 Color Picker 使用
  const handleColorChange = (e) => {
    const color = e.target.value;
    if (window.lastSavedRange) {
      const sel = window.getSelection();
      sel.removeAllRanges();
      sel.addRange(window.lastSavedRange);
      document.execCommand('styleWithCSS', false, true);
      document.execCommand('foreColor', false, color);
      const editor = window.lastSavedRange.commonAncestorContainer.parentNode?.closest?.('.rich-text-editor');
      if (editor) editor.dispatchEvent(new Event('input', { bubbles: true }));
      window.lastSavedRange = null;
    } else {
      data.updateTextStyle(id, { color });
    }
  };

  const handleColorClick = (e) => {
    const sel = window.getSelection();
    if (sel.rangeCount > 0 && !sel.isCollapsed) {
      window.lastSavedRange = sel.getRangeAt(0);
    } else {
      window.lastSavedRange = null;
    }
  };

  return (
    <div className="text-format-toolbar">
      {!isPlainText && (
        <>
          <input type="color" className="native-color-picker" value={data.color || '#ffffff'} onChange={(e) => data.updateNodeColor(id, e.target.value)} title="卡片背景色" />
          <div className="divider"></div>
        </>
      )}
      <div style={{ position: 'relative' }}>
        <button className="format-btn" style={{ width: 100, justifyContent: 'space-between', padding: '6px 10px' }} onMouseDown={(e) => { e.preventDefault(); setShowFonts(!showFonts); }}>
          <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 14 }}>
            {fontsList.find(f => f.value === (textStyle.fontFamily || fontsList[0]?.value))?.name || '字體'}
          </span>
        </button>
        {showFonts && (
          <div className="custom-font-menu">
            {fontsList.map(f => (
              <div key={f.value} className="font-menu-item" style={{ fontFamily: f.value }} onMouseDown={(e) => applyInlineStyle(e, 'fontFamily', f.value, () => { data.updateTextStyle(id, { fontFamily: f.value }); setShowFonts(false); })}>
                {f.name}
              </div>
            ))}
            {window.queryLocalFonts && fontsList.length <= 10 && (
              <div className="font-menu-item" style={{ color: '#007aff', textAlign: 'center', borderTop: '1px solid #eee', fontSize: 12, padding: '8px' }} onMouseDown={(e) => {
                 e.preventDefault();
                 if (data.loadLocalFonts) data.loadLocalFonts();
              }}>
                載入更多本機字體...
              </div>
            )}
          </div>
        )}
      </div>
      <div className="divider"></div>
      <div className="font-size-control">
        <button onMouseDown={(e) => applyInlineStyle(e, 'fontSize', Math.max(10, fontSize - 2), () => data.updateTextStyle(id, { fontSize: Math.max(10, fontSize - 2) }))}><Minus size={14}/></button>
        <span className="font-size-label">{fontSize}</span>
        <button onMouseDown={(e) => applyInlineStyle(e, 'fontSize', Math.min(120, fontSize + 2), () => data.updateTextStyle(id, { fontSize: Math.min(120, fontSize + 2) }))}><Plus size={14}/></button>
      </div>
      <div className="divider"></div>
      <button className={`format-btn ${isBold ? 'active' : ''}`} onMouseDown={(e) => applyInlineStyle(e, 'fontWeight', 'bold', () => data.updateTextStyle(id, { fontWeight: isBold ? 'normal' : 'bold' }))}><Bold size={16} /></button>
      <button className={`format-btn ${isItalic ? 'active' : ''}`} onMouseDown={(e) => applyInlineStyle(e, 'fontStyle', 'italic', () => data.updateTextStyle(id, { fontStyle: isItalic ? 'normal' : 'italic' }))}><Italic size={16} /></button>
      <div className="divider"></div>
      <button className={`format-btn ${textAlign === 'left' ? 'active' : ''}`} onMouseDown={(e) => applyInlineStyle(e, 'textAlign', 'left', () => data.updateTextStyle(id, { textAlign: 'left' }))}><AlignLeft size={16} /></button>
      <button className={`format-btn ${textAlign === 'center' ? 'active' : ''}`} onMouseDown={(e) => applyInlineStyle(e, 'textAlign', 'center', () => data.updateTextStyle(id, { textAlign: 'center' }))}><AlignCenter size={16} /></button>
      <button className={`format-btn ${textAlign === 'right' ? 'active' : ''}`} onMouseDown={(e) => applyInlineStyle(e, 'textAlign', 'right', () => data.updateTextStyle(id, { textAlign: 'right' }))}><AlignRight size={16} /></button>
      <div className="divider"></div>
      <Baseline size={16} color="#86868b" />
      <input type="color" className="native-color-picker" style={{ width: 20, height: 20 }} value={textStyle.color || '#1d1d1f'} onClick={handleColorClick} onChange={handleColorChange} title="文字顏色" />
    </div>
  );
};

const Handles = () => (
  <>
    <Handle type="target" position={Position.Top} id="t" />
    <Handle type="source" position={Position.Right} id="r" />
    <Handle type="source" position={Position.Bottom} id="b" />
    <Handle type="target" position={Position.Left} id="l" />
  </>
);

const PlainTextNode = ({ id, data, selected }) => {
  const textStyle = data.textStyle || { fontSize: 32, fontWeight: 'bold', fontStyle: 'normal', color: 'inherit', fontFamily: 'Inter, sans-serif', textAlign: 'left' };
  return (
    <>
      <NodeToolbar isVisible={selected} position={Position.Top}>
        <TextFormatToolbar id={id} data={data} isPlainText={true} />
      </NodeToolbar>
      <NodeResizer color="#0071e3" isVisible={selected} minWidth={100} minHeight={40} />
      <div className={`custom-node plain-text-node ${selected ? 'selected' : ''}`}>
        <Handles />
        <CollapseButton id={id} isCollapsed={data?.treeCollapsed} />
        <RichTextEditor html={data.text} placeholder="輸入大標題..." onChange={(html) => data.updateNodeText(id, html)} style={{ ...textStyle, fontSize: `${textStyle.fontSize}px`, textAlign: textStyle.textAlign }} />
      </div>
    </>
  );
};

const StickyNode = ({ id, data, selected }) => {
  const textStyle = data.textStyle || { fontSize: 15, fontWeight: 'normal', fontStyle: 'normal', color: 'inherit', fontFamily: 'Inter, sans-serif', textAlign: 'left' };
  return (
    <>
      <NodeToolbar isVisible={selected} position={Position.Top}>
        <TextFormatToolbar id={id} data={data} />
      </NodeToolbar>
      <NodeResizer color="#0071e3" isVisible={selected} minWidth={100} minHeight={100} />
      <div className={`custom-node sticky-node ${selected ? 'selected' : ''}`} style={{ backgroundColor: data.color || '#ffecb3' }}>
        <Handles />
        <CollapseButton id={id} isCollapsed={data?.treeCollapsed} />
        <RichTextEditor html={data.text} placeholder="輸入文字..." onChange={(html) => data.updateNodeText(id, html)} style={{ ...textStyle, fontSize: `${textStyle.fontSize}px`, textAlign: textStyle.textAlign }} />
      </div>
    </>
  );
};

const CollapsibleTextNode = ({ id, data, selected }) => {
  const textStyle = data.textStyle || { fontSize: 15, fontWeight: 'normal', fontStyle: 'normal', color: 'inherit', fontFamily: 'Inter, sans-serif', textAlign: 'left' };
  const collapsed = data.collapsed || false;

  return (
    <>
      <NodeToolbar isVisible={selected} position={Position.Top}>
        <TextFormatToolbar id={id} data={data} />
      </NodeToolbar>
      {(!collapsed) && <NodeResizer color="#0071e3" isVisible={selected} minWidth={150} minHeight={100} />}
      <div className={`custom-node collapsible-node ${selected ? 'selected' : ''} ${collapsed ? 'is-collapsed' : ''}`} style={{ backgroundColor: data.color || 'rgba(255, 255, 255, 0.85)' }}>
        <Handles />
        <CollapseButton id={id} isCollapsed={data?.treeCollapsed} />
        <div className="collapsible-header" onClick={() => data.toggleNodeCollapse && data.toggleNodeCollapse(id)}>
          <RichTextEditor html={data.title} placeholder="標題..." onClick={(e) => e.stopPropagation()} onKeyDown={(e) => { if(e.key === 'Enter') e.preventDefault(); }} onChange={(html) => data.updateNodeTitle(id, html)} style={{ fontFamily: textStyle.fontFamily, textAlign: textStyle.textAlign, color: textStyle.color, fontStyle: textStyle.fontStyle, fontWeight: textStyle.fontWeight === 'bold' ? 'bold' : '600', fontSize: `${textStyle.fontSize}px`, width: 'calc(100% - 20px)' }} />
          <span style={{ transform: collapsed ? 'rotate(-90deg)' : 'rotate(0deg)', transition: 'transform 0.3s', fontSize: '12px', flexShrink: 0 }}>▼</span>
        </div>
        <div className={`collapsible-body ${collapsed ? 'collapsed' : ''}`}>
          <RichTextEditor html={data.text} onChange={(html) => data.updateNodeText(id, html)} style={{ ...textStyle, fontSize: `${textStyle.fontSize}px`, textAlign: textStyle.textAlign }} />
        </div>
      </div>
    </>
  );
};

const DrawingNode = ({ id, data, selected }) => {
  const points = Array.isArray(data.points) ? data.points : [];
  const pathData = points.length > 0 ? getSvgPathFromStroke(getStroke(points, { size: data.strokeWidth || 6, thinning: 0.2, smoothing: 0.8, streamline: 0.8 })) : '';
  const viewBox = data.originalWidth ? `0 0 ${data.originalWidth} ${data.originalHeight}` : undefined;
  const preserveAspectRatio = data.originalWidth ? "none" : undefined;

  return (
    <>
      <NodeResizer color="#0071e3" isVisible={selected} minWidth={20} minHeight={20} />
      <div className={`custom-node drawing-node ${selected ? 'selected' : ''}`} style={{ width: '100%', height: '100%', padding: 0, boxShadow: 'none', background: 'transparent' }}>
        <Handles />
        <CollapseButton id={id} isCollapsed={data?.treeCollapsed} />
        <svg width="100%" height="100%" viewBox={viewBox} preserveAspectRatio={preserveAspectRatio}>
          <path d={pathData} fill={data.color || '#000'} opacity={data.tool === 'highlighter' ? 0.5 : 1} />
        </svg>
      </div>
    </>
  );
};

const ImageNode = ({ id, data, selected }) => (
  <>
    <NodeResizer color="#0071e3" isVisible={selected} minWidth={100} minHeight={100} />
    <div className={`custom-node image-node ${selected ? 'selected' : ''}`}>
      <Handles />
      <CollapseButton id={id} isCollapsed={data?.treeCollapsed} />
      <img src={data.url} alt="" className="nodrag" />
    </div>
  </>
);

const PDFNode = ({ id, data, selected }) => (
  <>
    <NodeResizer color="#0071e3" isVisible={selected} minWidth={200} minHeight={200} />
    <div className={`custom-node pdf-node ${selected ? 'selected' : ''}`}>
      <Handles />
      <CollapseButton id={id} isCollapsed={data?.treeCollapsed} />
      <iframe src={`${data.url}#toolbar=0`} className="nodrag" />
    </div>
  </>
);

const LinkNode = ({ id, data, selected }) => (
  <>
    <NodeResizer color="#0071e3" isVisible={selected} minWidth={150} minHeight={80} />
    <div className={`custom-node link-node ${selected ? 'selected' : ''}`}>
      <Handles />
      <CollapseButton id={id} isCollapsed={data?.treeCollapsed} />
      <strong>🔗 網頁連結</strong>
      <a href={data.url} target="_blank" rel="noreferrer" className="nodrag" onClick={(e) => e.stopPropagation()}>{data.url}</a>
    </div>
  </>
);

const ShapeFormatToolbar = ({ id, data }) => {
  const [showStyleMenu, setShowStyleMenu] = useState(false);
  const textStyle = data.textStyle || { fontSize: 15, textAlign: 'center', color: '#1d1d1f' };
  return (
    <div className="text-format-toolbar">
      <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
        <input type="color" className="native-color-picker" value={data.fillColor === 'transparent' ? '#ffffff' : (data.fillColor || '#42a5f5')} onChange={(e) => data.updateShapeStyle(id, { fillColor: e.target.value })} title="填充顏色" />
        <button className={`format-btn ${data.fillColor === 'transparent' ? 'active' : ''}`} onClick={() => data.updateShapeStyle(id, { fillColor: 'transparent' })} title="無填充色">
          <div style={{ width: 14, height: 14, borderRadius: 2, border: '1px solid #c7c7cc', background: 'linear-gradient(45deg, transparent 45%, #ff3b30 45%, #ff3b30 55%, transparent 55%)' }} />
        </button>
      </div>
      <div className="divider"></div>
      <input type="color" className="native-color-picker" value={data.strokeColor || '#1e88e5'} onChange={(e) => data.updateShapeStyle(id, { strokeColor: e.target.value })} title="外框顏色" />
      <div className="divider"></div>
      
      <div style={{ position: 'relative' }}>
        <button className="format-btn" onClick={() => setShowStyleMenu(!showStyleMenu)}>外框樣式</button>
        {showStyleMenu && (
          <div className="custom-font-menu" style={{ width: 120, top: '100%', left: '50%', transform: 'translateX(-50%)', marginTop: '8px' }}>
            <div className="font-menu-item" onClick={() => { data.updateShapeStyle(id, { strokeWidth: 0 }); setShowStyleMenu(false); }}>無框線</div>
            <div className="font-menu-item" onClick={() => { data.updateShapeStyle(id, { strokeWidth: 2, strokeDasharray: 'none' }); setShowStyleMenu(false); }}>實線 (細)</div>
            <div className="font-menu-item" onClick={() => { data.updateShapeStyle(id, { strokeWidth: 4, strokeDasharray: 'none' }); setShowStyleMenu(false); }}>實線 (粗)</div>
            <div className="font-menu-item" onClick={() => { data.updateShapeStyle(id, { strokeWidth: 3, strokeDasharray: '8 8' }); setShowStyleMenu(false); }}>虛線</div>
            <div className="font-menu-item" onClick={() => { data.updateShapeStyle(id, { strokeWidth: 3, strokeDasharray: '2 6' }); setShowStyleMenu(false); }}>點點</div>
          </div>
        )}
      </div>

      <div className="divider"></div>
      <div className="font-size-control">
        <button onMouseDown={(e) => applyInlineStyle(e, 'fontSize', Math.max(10, textStyle.fontSize - 2), () => data.updateTextStyle(id, { fontSize: Math.max(10, textStyle.fontSize - 2) }))}><Minus size={14}/></button>
        <span className="font-size-label">{textStyle.fontSize}</span>
        <button onMouseDown={(e) => applyInlineStyle(e, 'fontSize', Math.min(120, textStyle.fontSize + 2), () => data.updateTextStyle(id, { fontSize: Math.min(120, textStyle.fontSize + 2) }))}><Plus size={14}/></button>
      </div>
      <button className={`format-btn ${textStyle.fontWeight === 'bold' ? 'active' : ''}`} onMouseDown={(e) => applyInlineStyle(e, 'fontWeight', 'bold', () => data.updateTextStyle(id, { fontWeight: textStyle.fontWeight === 'bold' ? 'normal' : 'bold' }))}><Bold size={16} /></button>
      <input type="color" className="native-color-picker" style={{ width: 20, height: 20 }} value={textStyle.color} 
        onClick={(e) => {
          const sel = window.getSelection();
          if (sel.rangeCount > 0 && !sel.isCollapsed) {
            window.lastSavedRange = sel.getRangeAt(0);
          } else {
            window.lastSavedRange = null;
          }
        }}
        onChange={(e) => {
          const color = e.target.value;
          if (window.lastSavedRange) {
            const sel = window.getSelection();
            sel.removeAllRanges();
            sel.addRange(window.lastSavedRange);
            document.execCommand('styleWithCSS', false, true);
            document.execCommand('foreColor', false, color);
            const editor = window.lastSavedRange.commonAncestorContainer.parentNode?.closest?.('.rich-text-editor');
            if (editor) editor.dispatchEvent(new Event('input', { bubbles: true }));
            window.lastSavedRange = null;
          } else {
            data.updateTextStyle(id, { color });
          }
        }} title="文字顏色" />
    </div>
  );
};

const ShapeNode = ({ id, data, selected }) => {
  const t = data.shapeType || 'rectangle';
  const fill = data.fillColor || '#42a5f5';
  const stroke = data.strokeColor || '#1e88e5';
  const sw = data.strokeWidth !== undefined ? data.strokeWidth : 2;
  const sda = data.strokeDasharray || 'none';
  const textStyle = data.textStyle || { fontSize: 15, fontWeight: 'normal', color: '#1d1d1f', fontFamily: 'Inter, sans-serif', textAlign: 'center' };

  const renderShape = () => {
    const props = { fill, stroke, strokeWidth: sw, strokeDasharray: sda };
    if (t === 'rectangle') return <rect x="0" y="0" width="100%" height="100%" {...props} />;
    if (t === 'rounded-rectangle') return <rect x="0" y="0" width="100%" height="100%" rx="16" {...props} />;
    if (t === 'circle') return <ellipse cx="50%" cy="50%" rx="50%" ry="50%" {...props} />;
    if (t === 'triangle') return <polygon points="50,0 100,100 0,100" preserveAspectRatio="none" viewBox="0 0 100 100" {...props} />;
    if (t === 'diamond') return <polygon points="50,0 100,50 50,100 0,50" preserveAspectRatio="none" viewBox="0 0 100 100" {...props} />;
    if (t === 'hexagon') return <polygon points="50,0 100,25 100,75 50,100 0,75 0,25" preserveAspectRatio="none" viewBox="0 0 100 100" {...props} />;
    if (t === 'pentagon') return <polygon points="50,0 100,38 82,100 18,100 0,38" preserveAspectRatio="none" viewBox="0 0 100 100" {...props} />;
    if (t === 'star') return <polygon points="50,0 61,35 98,35 68,57 79,91 50,70 21,91 32,57 2,35 39,35" preserveAspectRatio="none" viewBox="0 0 100 100" {...props} />;
    if (t === 'parallelogram') return <polygon points="25,0 100,0 75,100 0,100" preserveAspectRatio="none" viewBox="0 0 100 100" {...props} />;
    return <rect x="0" y="0" width="100%" height="100%" {...props} />;
  };

  return (
    <>
      <NodeToolbar isVisible={selected} position={Position.Top}>
        <ShapeFormatToolbar id={id} data={data} />
      </NodeToolbar>
      <NodeResizer color="#0071e3" isVisible={selected} minWidth={40} minHeight={40} />
      <div className={`shape-node ${selected ? 'selected' : ''}`}>
        <svg width="100%" height="100%" preserveAspectRatio="none" viewBox={t !== 'rectangle' && t !== 'rounded-rectangle' && t !== 'circle' ? "0 0 100 100" : undefined}>
          {renderShape()}
        </svg>
        <div className="text-overlay">
          <RichTextEditor html={data.text} onChange={(html) => data.updateNodeText(id, html)} style={{ ...textStyle, fontSize: `${textStyle.fontSize}px` }} />
        </div>
        <Handles />
        <CollapseButton id={id} isCollapsed={data?.treeCollapsed} />
      </div>
    </>
  );
};

const GroupNode = ({ id, selected }) => {
  const { activeGroupId, setActiveGroupId } = React.useContext(GroupContext);
  const isActive = activeGroupId === id;
  
  const handleDoubleClick = (e) => {
    e.stopPropagation();
    setActiveGroupId(id);
  };

  return (
    <>
      <NodeResizer color="#0071e3" isVisible={selected && !isActive} minWidth={100} minHeight={100} />
      <div 
        className={`custom-node group-node ${selected ? 'selected' : ''} ${isActive ? 'active' : ''}`}
        style={{ width: '100%', height: '100%', backgroundColor: 'transparent', border: isActive ? '2px dashed #0071e3' : '1px solid transparent', borderRadius: 8 }}
      >
        <div 
          className="group-overlay" 
          onDoubleClick={handleDoubleClick}
          style={{ 
            position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, 
            pointerEvents: isActive ? 'none' : 'auto',
            zIndex: isActive ? -1 : 1 
          }}
        />
        {isActive && (
          <div className="group-active-label" style={{ position: 'absolute', top: -28, left: -2, background: '#0071e3', color: '#fff', padding: '4px 8px', borderRadius: 4, fontSize: 12, fontWeight: 600, pointerEvents: 'none' }}>編輯群組中... (點擊畫布空白處退出)</div>
        )}
      </div>
    </>
  );
};

const CustomEdge = ({ id, source, target, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style = {}, markerEnd, markerStart, selected, data }) => {
  const sourceNode = useInternalNode(source);
  const targetNode = useInternalNode(target);
  
  // 訂閱寬高改變，確保卡片收合時線條會強制重新渲染
  useStore(useCallback(s => s.nodeLookup.get(source)?.measured?.height, [source]));
  useStore(useCallback(s => s.nodeLookup.get(target)?.measured?.height, [target]));
  useStore(useCallback(s => s.nodeLookup.get(source)?.measured?.width, [source]));
  useStore(useCallback(s => s.nodeLookup.get(target)?.measured?.width, [target]));

  const [showArrowMenu, setShowArrowMenu] = useState(false);
  
  let sx = sourceX, sy = sourceY, tx = targetX, ty = targetY, sourcePos = sourcePosition, targetPos = targetPosition;
  if (sourceNode && targetNode) {
    const params = getEdgeParams(sourceNode, targetNode);
    sx = params.sx; sy = params.sy; tx = params.tx; ty = params.ty;
    sourcePos = params.sourcePos; targetPos = params.targetPos;
  }
  
  const [edgePath, labelX, labelY] = getBezierPath({ sourceX: sx, sourceY: sy, sourcePosition: sourcePos, targetX: tx, targetY: ty, targetPosition: targetPos });
  const edgeColor = data?.color || style.stroke || '#000';
  const edgeStrokeWidth = data?.strokeWidth || 2;

  const parsedMarkerEnd = markerEnd === 'custom-dot' ? `custom-dot-${id}` : markerEnd;
  const parsedMarkerStart = markerStart === 'custom-dot' ? `custom-dot-${id}` : markerStart;

  return (
    <>
      <svg style={{ position: 'absolute', width: 0, height: 0 }}>
        <defs>
          <marker id={`custom-dot-${id}`} viewBox="0 0 10 10" refX="5" refY="5" markerWidth="3" markerHeight="3" orient="auto-start-reverse">
            <circle cx="5" cy="5" r="5" fill={edgeColor} />
          </marker>
        </defs>
      </svg>
      <BaseEdge path={edgePath} markerEnd={parsedMarkerEnd} markerStart={parsedMarkerStart} style={{ ...style, stroke: edgeColor, strokeWidth: edgeStrokeWidth }} id={id} />
      <path d={edgePath} fill="none" strokeOpacity={0} strokeWidth={20} className="react-flow__edge-interaction" />
      {selected && (
        <EdgeLabelRenderer>
          <div
            className="nodrag nopan edge-toolbar"
            style={{ position: 'absolute', transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`, pointerEvents: 'all' }}
          >
            <button onClick={() => data.updateEdgeStyle(id, { strokeWidth: 2 })}>細</button>
            <button onClick={() => data.updateEdgeStyle(id, { strokeWidth: 4 })}>中</button>
            <button onClick={() => data.updateEdgeStyle(id, { strokeWidth: 8 })}>粗</button>
            <div className="divider"></div>
            
            <div style={{ position: 'relative' }}>
              <button onClick={() => setShowArrowMenu(!showArrowMenu)} title="線條結尾">
                <ArrowRight size={14}/>
              </button>
              {showArrowMenu && (
                <div className="custom-font-menu" style={{ width: 120, top: '100%', left: '50%', transform: 'translateX(-50%)', marginTop: '8px' }}>
                  <div className="font-menu-item" onClick={() => { data.updateEdgeArrow(id, 'none'); setShowArrowMenu(false); }}>無結尾</div>
                  <div className="font-menu-item" onClick={() => { data.updateEdgeArrow(id, 'end'); setShowArrowMenu(false); }}>單向箭頭</div>
                  <div className="font-menu-item" onClick={() => { data.updateEdgeArrow(id, 'both'); setShowArrowMenu(false); }}>雙向箭頭</div>
                  <div className="font-menu-item" onClick={() => { data.updateEdgeArrow(id, 'end-dot'); setShowArrowMenu(false); }}>單向圓點</div>
                  <div className="font-menu-item" onClick={() => { data.updateEdgeArrow(id, 'both-dot'); setShowArrowMenu(false); }}>雙向圓點</div>
                </div>
              )}
            </div>
            
            <div className="divider"></div>
            <input type="color" className="native-color-picker" value={edgeColor} onChange={(e) => data.updateEdgeColor(id, e.target.value)} title="連線顏色" />
          </div>
        </EdgeLabelRenderer>
      )}
    </>
  );
};

const nodeTypes = { sticky: StickyNode, plainText: PlainTextNode, collapsible: CollapsibleTextNode, drawing: DrawingNode, image: ImageNode, pdf: PDFNode, link: LinkNode, shape: ShapeNode, group: GroupNode };
const edgeTypes = { custom: CustomEdge };

let idCounter = 3;
const getId = () => `${idCounter++}`;

function FlowApp() {
  const [nodes, setNodes] = useState([]);
  const [edges, setEdges] = useState([]);
  const [localFonts, setLocalFonts] = useState(DEFAULT_FONTS);
  
  // 雲端同步設定
  const KV_BUCKET = 'WMgTaeNQNsJ6knUcqk1Gwb';
  const urlParams = new URLSearchParams(window.location.search);
  const boardId = urlParams.get('board');
  const isMigrating = urlParams.get('migrate') === 'true';
  const [syncStatus, setSyncStatus] = useState(boardId ? '載入中...' : '本機模式');
  const [boardTitle, setBoardTitle] = useState('未命名白板');
  
  const [bgSettings, setBgSettings] = useState({ color: '#f5f5f7', gap: 30, lineWidth: 1, gridColor: '#c7c7cc', variant: 'lines' });
  const [showBgSettings, setShowBgSettings] = useState(false);
  
  const [showAttachMenu, setShowAttachMenu] = useState(false);
  const [showShapeMenu, setShowShapeMenu] = useState(false);
  const fileInputRef = useRef(null);

  const [drawMode, setDrawMode] = useState(false);
  const [drawTool, setDrawTool] = useState('pen');
  const [drawColor, setDrawColor] = useState('#000000');
  const [drawSize, setDrawSize] = useState(6);
  
  const currentStrokeRef = useRef(null);
  const drawingSvgRef = useRef(null);
  
  // 右鍵選單與剪貼簿
  const [contextMenu, setContextMenu] = useState(null);
  const clipboardRef = useRef({ nodes: [], edges: [] });
  const [activeGroupId, setActiveGroupId] = useState(null);

  const { screenToFlowPosition, setViewport, updateNodeInternals } = useReactFlow();
  const viewport = useViewport();

  const loadLocalFonts = useCallback(async () => {
    if ('queryLocalFonts' in window) {
      try {
        const fonts = await window.queryLocalFonts();
        const uniqueFonts = Array.from(new Set(fonts.map(f => f.family))).map(f => ({ name: f, value: `"${f}"` }));
        const allFonts = [...DEFAULT_FONTS, ...uniqueFonts].filter((v,i,a)=>a.findIndex(t=>(t.name === v.name))===i);
        setLocalFonts(allFonts);
        setNodes(nds => nds.map(n => ({ ...n, data: { ...n.data, fonts: allFonts } })));
      } catch (err) {
        console.error('Font access denied or unsupported.', err);
        alert('無法取得字體權限，請確認瀏覽器是否允許。');
      }
    }
  }, []);

  const updateNodeColor = useCallback((id, color) => setNodes((nds) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, color } } : n))), []);
  const updateNodeText = useCallback((id, text) => setNodes((nds) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, text } } : n))), []);
  const updateNodeTitle = useCallback((id, title) => setNodes((nds) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, title } } : n))), []);
  const updateTextStyle = useCallback((id, style) => setNodes((nds) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, textStyle: { ...(n.data.textStyle || {}), ...style } } } : n))), []);
  const updateShapeStyle = useCallback((id, style) => setNodes((nds) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, ...style } } : n))), []);
  
  const toggleNodeCollapse = useCallback((id) => {
    setNodes((nds) => nds.map((n) => {
      if (n.id === id) {
        const isCollapsed = !n.data.collapsed;
        const newStyle = { ...n.style };
        const newData = { ...n.data, collapsed: isCollapsed };
        if (isCollapsed) {
          newData.expandedHeight = n.height || n.measured?.height || n.style?.height || 150;
          const fontSize = n.data.textStyle?.fontSize || 15;
          const collapseHeight = Math.max(45, fontSize * 1.5 + 24);
          newStyle.height = collapseHeight;
          newStyle.minHeight = collapseHeight;
          return { ...n, style: newStyle, data: newData, height: collapseHeight };
        } else {
          const expandedH = n.data.expandedHeight || 150;
          newStyle.height = expandedH;
          newStyle.minHeight = 100;
          return { ...n, style: newStyle, data: newData, height: expandedH };
        }
      }
      return n;
    }));
    [10, 50, 100, 300].forEach(t => setTimeout(() => updateNodeInternals(id), t));
  }, [updateNodeInternals]);
  
  const updateEdgeStyle = useCallback((id, style) => setEdges((eds) => eds.map((e) => (e.id === id ? { ...e, data: { ...e.data, ...style } } : e))), []);
  const updateEdgeColor = useCallback((id, color) => setEdges((eds) => eds.map((e) => (e.id === id ? { ...e, data: { ...e.data, color } } : e))), []);
  const updateEdgeArrow = useCallback((id, type) => {
    setEdges((eds) => eds.map((e) => {
      if (e.id !== id) return e;
      let markerStart = undefined;
      let markerEnd = undefined;
      if (type === 'end') markerEnd = { type: MarkerType.ArrowClosed, color: e.data?.color || '#000' };
      if (type === 'both') {
        markerEnd = { type: MarkerType.ArrowClosed, color: e.data?.color || '#000' };
        markerStart = { type: MarkerType.ArrowClosed, color: e.data?.color || '#000' };
      }
      if (type === 'end-dot') markerEnd = 'custom-dot';
      if (type === 'both-dot') {
        markerEnd = 'custom-dot';
        markerStart = 'custom-dot';
      }
      return { ...e, markerStart, markerEnd, data: { ...e.data, arrowType: type } };
    }));
  }, []);

  useEffect(() => {
    setEdges(eds => eds.map(e => {
      const edgeColor = e.data?.color || '#000';
      const needsUpdate = (e.markerEnd && e.markerEnd.color !== edgeColor) || (e.markerStart && e.markerStart.color !== edgeColor);
      if (needsUpdate) {
        return {
          ...e,
          markerEnd: (e.markerEnd && typeof e.markerEnd === 'object') ? { ...e.markerEnd, color: edgeColor } : e.markerEnd,
          markerStart: (e.markerStart && typeof e.markerStart === 'object') ? { ...e.markerStart, color: edgeColor } : e.markerStart
        };
      }
      return e;
    }));
  }, [edges.map(e => e.data?.color).join(',')]);

  // --- 排版與群組 ---
  const handleAlign = useCallback((type) => {
    setNodes(nds => {
      const selected = nds.filter(n => n.selected);
      if (selected.length < 2) return nds;
      
      const minX = Math.min(...selected.map(n => n.position.x));
      const maxX = Math.max(...selected.map(n => n.position.x + (n.measured?.width || 0)));
      const minY = Math.min(...selected.map(n => n.position.y));
      const maxY = Math.max(...selected.map(n => n.position.y + (n.measured?.height || 0)));
      const centerX = minX + (maxX - minX) / 2;
      const centerY = minY + (maxY - minY) / 2;

      return nds.map(n => {
        if (!n.selected) return n;
        let newX = n.position.x;
        let newY = n.position.y;
        const w = n.measured?.width || 0;
        const h = n.measured?.height || 0;

        switch(type) {
          case 'left': newX = minX; break;
          case 'center': newX = centerX - w / 2; break;
          case 'right': newX = maxX - w; break;
          case 'top': newY = minY; break;
          case 'middle': newY = centerY - h / 2; break;
          case 'bottom': newY = maxY - h; break;
        }
        return { ...n, position: { x: newX, y: newY } };
      });
    });
  }, [setNodes]);

  const handleDistribute = useCallback((type) => {
    setNodes(nds => {
      const selected = nds.filter(n => n.selected);
      if (selected.length < 3) return nds;

      if (type === 'horizontal') {
        const sorted = [...selected].sort((a, b) => a.position.x - b.position.x);
        const first = sorted[0];
        const last = sorted[sorted.length - 1];
        const totalWidths = sorted.slice(1, -1).reduce((sum, n) => sum + (n.measured?.width || 0), 0);
        const availableSpace = last.position.x - (first.position.x + (first.measured?.width || 0)) - totalWidths;
        const gap = availableSpace / (sorted.length - 1);
        
        let currentX = first.position.x + (first.measured?.width || 0) + gap;
        const newPositions = {};
        for (let i = 1; i < sorted.length - 1; i++) {
          newPositions[sorted[i].id] = currentX;
          currentX += (sorted[i].measured?.width || 0) + gap;
        }
        return nds.map(n => newPositions[n.id] !== undefined ? { ...n, position: { ...n.position, x: newPositions[n.id] } } : n);
      } else {
        const sorted = [...selected].sort((a, b) => a.position.y - b.position.y);
        const first = sorted[0];
        const last = sorted[sorted.length - 1];
        const totalHeights = sorted.slice(1, -1).reduce((sum, n) => sum + (n.measured?.height || 0), 0);
        const availableSpace = last.position.y - (first.position.y + (first.measured?.height || 0)) - totalHeights;
        const gap = availableSpace / (sorted.length - 1);
        
        let currentY = first.position.y + (first.measured?.height || 0) + gap;
        const newPositions = {};
        for (let i = 1; i < sorted.length - 1; i++) {
          newPositions[sorted[i].id] = currentY;
          currentY += (sorted[i].measured?.height || 0) + gap;
        }
        return nds.map(n => newPositions[n.id] !== undefined ? { ...n, position: { ...n.position, y: newPositions[n.id] } } : n);
      }
    });
  }, [setNodes]);

  const handleGroup = useCallback(() => {
    setNodes(nds => {
      const selected = nds.filter(n => n.selected);
      if (selected.length < 2) return nds;

      const minX = Math.min(...selected.map(n => n.position.x));
      const maxX = Math.max(...selected.map(n => n.position.x + (n.measured?.width || 0)));
      const minY = Math.min(...selected.map(n => n.position.y));
      const maxY = Math.max(...selected.map(n => n.position.y + (n.measured?.height || 0)));

      const padding = 20;
      const groupPos = { x: minX - padding, y: minY - padding };
      const groupWidth = maxX - minX + padding * 2;
      const groupHeight = maxY - minY + padding * 2;
      const groupId = 'group-' + Date.now();

      const groupNode = {
        id: groupId,
        type: 'group',
        position: groupPos,
        style: { width: groupWidth, height: groupHeight, backgroundColor: 'transparent', border: 'none' },
        zIndex: -1
      };

      const selectedIds = new Set(selected.map(n => n.id));

      return [
        groupNode,
        ...nds.map(n => {
          if (selectedIds.has(n.id)) {
            return { 
              ...n, 
              parentId: groupId, 
              position: { x: n.position.x - groupPos.x, y: n.position.y - groupPos.y },
              selected: false,
              expandParent: true
            };
          }
          return n;
        })
      ];
    });
  }, [setNodes]);

  const handleUngroup = useCallback(() => {
    setNodes(nds => {
      const selectedGroups = nds.filter(n => n.selected && n.type === 'group');
      if (selectedGroups.length === 0) return nds;

      const groupIds = new Set(selectedGroups.map(n => n.id));
      const withoutGroups = nds.filter(n => !groupIds.has(n.id));
      
      return withoutGroups.map(n => {
        if (n.parentId && groupIds.has(n.parentId)) {
          const group = selectedGroups.find(g => g.id === n.parentId);
          return {
            ...n,
            parentId: undefined,
            position: { x: n.position.x + group.position.x, y: n.position.y + group.position.y },
            selected: true
          };
        }
        return n;
      });
    });
  }, [setNodes]);

  // --- 圖層控制與剪貼簿 (Layers & Clipboard) ---
  const handleBringToFront = useCallback(() => {
    setNodes(nds => {
      const maxZ = Math.max(...nds.map(n => n.zIndex || 0), 0);
      return nds.map(n => n.selected ? { ...n, zIndex: maxZ + 1 } : n);
    });
  }, [setNodes]);

  const handleSendToBack = useCallback(() => {
    setNodes(nds => {
      const minZ = Math.min(...nds.map(n => n.zIndex || 0), 0);
      return nds.map(n => n.selected ? { ...n, zIndex: minZ - 1 } : n);
    });
  }, [setNodes]);

  const copyToClipboard = useCallback(() => {
    const selectedNodes = nodes.filter(n => n.selected);
    const selectedIds = new Set(selectedNodes.map(n => n.id));
    const selectedEdges = edges.filter(e => selectedIds.has(e.source) && selectedIds.has(e.target));
    clipboardRef.current = { nodes: selectedNodes, edges: selectedEdges };
  }, [nodes, edges]);

  const deleteSelectedNodes = useCallback(() => {
    setNodes(nds => nds.filter(n => !n.selected));
    setEdges(eds => eds.filter(e => !e.selected));
  }, [setNodes, setEdges]);

  const createGroup = useCallback(() => {
    const selectedNodes = nodes.filter(n => n.selected);
    if (selectedNodes.length < 2) return;
    
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
    selectedNodes.forEach(n => {
      minX = Math.min(minX, n.position.x);
      minY = Math.min(minY, n.position.y);
      const width = n.measured?.width || n.width || 100;
      const height = n.measured?.height || n.height || 100;
      maxX = Math.max(maxX, n.position.x + width);
      maxY = Math.max(maxY, n.position.y + height);
    });
    
    const padding = 20;
    const groupId = getId();
    const groupNode = {
      id: groupId,
      type: 'group',
      position: { x: minX - padding, y: minY - padding },
      style: { width: maxX - minX + padding * 2, height: maxY - minY + padding * 2 },
      data: { isGroup: true }
    };
    
    setNodes(nds => {
      const newNds = nds.map(n => {
        if (n.selected) {
          return {
            ...n,
            parentId: groupId,
            position: {
              x: n.position.x - (minX - padding),
              y: n.position.y - (minY - padding)
            },
            selected: false
          };
        }
        return n;
      });
      return [...newNds, { ...groupNode, selected: true }];
    });
  }, [nodes]);

  const ungroup = useCallback((groupId) => {
    const groupNode = nodes.find(n => n.id === groupId);
    if (!groupNode) return;
    
    setNodes(nds => {
      const withoutGroup = nds.filter(n => n.id !== groupId);
      return withoutGroup.map(n => {
        if (n.parentId === groupId) {
          return {
            ...n,
            parentId: undefined,
            position: {
              x: n.position.x + groupNode.position.x,
              y: n.position.y + groupNode.position.y
            },
            selected: true
          };
        }
        return { ...n, selected: false };
      });
    });
  }, [nodes]);

  const cutToClipboard = useCallback(() => {
    copyToClipboard();
    deleteSelectedNodes();
  }, [copyToClipboard, deleteSelectedNodes]);

  const pasteFromClipboard = useCallback((mousePos) => {
    const { nodes: clipNodes, edges: clipEdges } = clipboardRef.current;
    if (clipNodes.length === 0) return;

    let offsetX = 30;
    let offsetY = 30;

    if (mousePos) {
      const flowPos = screenToFlowPosition({ x: mousePos.left, y: mousePos.top }, { snapToGrid: false });
      const minX = Math.min(...clipNodes.map(n => n.position.x));
      const minY = Math.min(...clipNodes.map(n => n.position.y));
      offsetX = flowPos.x - minX;
      offsetY = flowPos.y - minY;
    }

    const idMap = {};
    const newNodes = clipNodes.map(n => {
      const newId = getId();
      idMap[n.id] = newId;
      return {
        ...n,
        id: newId,
        position: { x: n.position.x + offsetX, y: n.position.y + offsetY },
        selected: true
      };
    });

    const newEdges = clipEdges.map(e => ({
      ...e,
      id: `e${idMap[e.source]}-${idMap[e.target]}-${Date.now()}`,
      source: idMap[e.source],
      target: idMap[e.target],
      selected: true
    }));

    setNodes(nds => [...nds.map(n => ({...n, selected: false})), ...newNodes]);
    setEdges(eds => [...eds.map(e => ({...e, selected: false})), ...newEdges]);
  }, [setNodes, setEdges, screenToFlowPosition]);

  const duplicateSelected = useCallback(() => {
    copyToClipboard();
    pasteFromClipboard();
  }, [copyToClipboard, pasteFromClipboard]);

  // --- 鍵盤快捷鍵 ---
  useEffect(() => {
    const handleKeyDown = (e) => {
      if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
      if (e.metaKey || e.ctrlKey) {
        if (e.key === 'c') { e.preventDefault(); copyToClipboard(); }
        else if (e.key === 'x') { e.preventDefault(); cutToClipboard(); }
        // 注意：這裡移除了 v 的預設阻擋，讓系統的 paste 事件能正常觸發
        else if (e.key === 'd') { e.preventDefault(); duplicateSelected(); }
      }
    };
    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [copyToClipboard, cutToClipboard, duplicateSelected]);

  // --- 右鍵選單 Handlers ---
  const onNodeContextMenu = useCallback((e, node) => {
    e.preventDefault();
    if (!node.selected) {
      setNodes(nds => nds.map(n => ({ ...n, selected: n.id === node.id })));
    }
    setContextMenu({ top: e.clientY, left: e.clientX, type: 'node' });
  }, [setNodes]);

  const onPaneContextMenu = useCallback((e) => {
    e.preventDefault();
    setContextMenu({ top: e.clientY, left: e.clientX, type: 'pane' });
  }, []);

  const closeContextMenu = useCallback(() => setContextMenu(null), []);

  const onPaneClick = useCallback(() => {
    setActiveGroupId(null);
  }, []);

  const selectedNodes = nodes.filter(n => n.selected);
  const [isLoaded, setIsLoaded] = useState(false);

  useEffect(() => {
    const loadState = async () => {
      let savedState = null;
      if (isMigrating) {
        savedState = localStorage.getItem('freeform_app_state');
        setSyncStatus('轉移舊資料中...');
      } else if (boardId) {
        try {
          const res = await fetch(`https://kvdb.io/${KV_BUCKET}/${boardId}`);
          if (res.ok) {
            const data = await res.json();
            savedState = JSON.stringify(data);
            setSyncStatus('已同步 ☁️');
          } else {
            setSyncStatus('新雲端畫布 ☁️');
          }
        } catch (e) {
          console.error('Cloud load failed', e);
          setSyncStatus('連線失敗 ⚠️');
        }
      } else {
        savedState = localStorage.getItem('freeform_app_state');
      }

      if (savedState) {
        try {
          const parsed = JSON.parse(savedState) || {};
          if (parsed.title) setBoardTitle(parsed.title);
        const savedNodes = Array.isArray(parsed.nodes) ? parsed.nodes : null;
        const savedEdges = Array.isArray(parsed.edges) ? parsed.edges : [];
        const savedBg = parsed.bgSettings;
        const savedViewport = parsed.viewport;
        
        if (savedNodes) {
          let maxId = idCounter;
          savedNodes.forEach(n => {
            const num = parseInt(n.id, 10);
            if (!isNaN(num) && num >= maxId) maxId = num + 1;
          });
          idCounter = maxId;

          const restoredNodes = savedNodes.map(n => ({
            ...n,
            data: { ...n.data, updateNodeColor, updateNodeText, updateNodeTitle, updateTextStyle, updateShapeStyle, toggleNodeCollapse, loadLocalFonts, fonts: localFonts }
          }));
          
          const restoredEdges = savedEdges.map(e => ({
            ...e,
            data: { ...e.data, updateEdgeStyle, updateEdgeColor, updateEdgeArrow }
          }));
          
          setNodes(restoredNodes);
          setEdges(restoredEdges);
        } else {
          // 如果沒有節點資料，載入預設
          const initialNodes = [
            {
              id: '1', type: 'plainText', position: { x: 250, y: 150 }, style: { width: 350, height: 160 },
              data: { text: '體驗右鍵選單功能', textStyle: { fontSize: 32 }, fonts: localFonts },
            }
          ].map(n => ({ ...n, data: { ...n.data, updateNodeColor, updateNodeText, updateNodeTitle, updateTextStyle, updateShapeStyle, toggleNodeCollapse, loadLocalFonts, fonts: localFonts } }));
          setNodes(initialNodes);
        }

        if (savedBg) setBgSettings(prev => ({ ...prev, ...savedBg }));
        if (savedViewport) setTimeout(() => setViewport(savedViewport), 50);
      } catch (e) {
        console.error('Failed to load state', e);
        const initialNodes = [
          {
            id: '1', type: 'plainText', position: { x: 250, y: 150 }, style: { width: 350, height: 160 },
            data: { text: '體驗右鍵選單功能', textStyle: { fontSize: 32 }, fonts: localFonts },
          }
        ].map(n => ({ ...n, data: { ...n.data, updateNodeColor, updateNodeText, updateNodeTitle, updateTextStyle, updateShapeStyle, toggleNodeCollapse, loadLocalFonts, fonts: localFonts } }));
        setNodes(initialNodes);
      }
    } else {
      const initialNodes = [
        {
          id: '1', type: 'plainText', position: { x: 250, y: 150 }, style: { width: 350, height: 160 },
          data: { text: '體驗右鍵選單功能', textStyle: { fontSize: 32 }, fonts: localFonts },
        }
      ].map(n => ({ ...n, data: { ...n.data, updateNodeColor, updateNodeText, updateNodeTitle, updateTextStyle, updateShapeStyle, toggleNodeCollapse, loadLocalFonts, fonts: localFonts } }));
      setNodes(initialNodes);
    }
      setIsLoaded(true);
    };
    loadState();
  }, [setViewport, updateNodeColor, updateNodeText, updateNodeTitle, updateTextStyle, updateShapeStyle, toggleNodeCollapse, updateEdgeStyle, updateEdgeColor, updateEdgeArrow, boardId, isMigrating]);

  // 更新 Dashboard 紀錄
  useEffect(() => {
    if (!isLoaded || !boardId) return;
    try {
      const recent = JSON.parse(localStorage.getItem('freeform_recent_boards') || '[]');
      const existing = recent.find(b => b.id === boardId);
      if (existing) {
        existing.title = boardTitle;
        existing.timestamp = Date.now();
      } else {
        const colors = ['#007aff', '#34c759', '#ff9500', '#af52de', '#ff2d55'];
        const color = colors[Math.floor(Math.random() * colors.length)];
        recent.push({ id: boardId, title: boardTitle, timestamp: Date.now(), color });
      }
      localStorage.setItem('freeform_recent_boards', JSON.stringify(recent));
      
      const syncKey = localStorage.getItem('freeform_catalog_sync_key');
      if (syncKey) {
        fetch(`https://kvdb.io/${KV_BUCKET}/catalog_${syncKey}`, {
          method: 'POST',
          body: JSON.stringify(recent)
        }).catch(console.error);
      }
      
      if (isMigrating) {
        localStorage.removeItem('freeform_app_state'); // 轉換成功後清除舊的
      }
    } catch(e) {}
  }, [isLoaded, boardId, boardTitle, isMigrating]);

  // --- 全域貼上事件 (Global Paste) ---
  useEffect(() => {
    const handlePaste = (e) => {
      // 避免在輸入框內貼上時觸發新增節點
      if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT' || e.target.isContentEditable) return;
      
      const items = e.clipboardData?.items;
      if (!items) {
        // 如果是按 Cmd+V 但沒有剪貼簿內容，退回原本的畫布內節點複製功能
        pasteFromClipboard();
        return;
      }

      let handled = false;
      for (let i = 0; i < items.length; i++) {
        const item = items[i];
        if (item.type.indexOf('image') !== -1) {
          const file = item.getAsFile();
          if (file) {
            const reader = new FileReader();
            reader.onload = (event) => {
              const base64 = event.target.result;
              const newNode = {
                id: getId(), type: 'image',
                position: { x: window.innerWidth / 2 - 150, y: window.innerHeight / 2 - 150 },
                style: { width: 300, height: 300 },
                data: { url: base64, title: '貼上的圖片', updateNodeTitle }
              };
              setNodes(nds => [...nds, newNode]);
            };
            reader.readAsDataURL(file);
            handled = true;
          }
          break;
        } else if (item.type === 'text/plain') {
          item.getAsString((text) => {
            if (text.trim() && !text.startsWith('{"nodes":')) { // 避免誤判為內部 json 複製
              const newNode = {
                id: getId(), type: 'plainText',
                position: { x: window.innerWidth / 2 - 100, y: window.innerHeight / 2 - 50 },
                style: { width: 300, height: 'auto' },
                data: { text, updateNodeText, updateTextStyle, fonts: localFonts }
              };
              setNodes(nds => [...nds, newNode]);
            } else if (text.startsWith('{"nodes":')) {
               pasteFromClipboard(); // 交給原本的畫布內貼上功能
            }
          });
          handled = true;
          break;
        }
      }
      
      if (!handled) {
         pasteFromClipboard();
      }
    };
    
    window.addEventListener('paste', handlePaste);
    return () => window.removeEventListener('paste', handlePaste);
  }, [setNodes, updateNodeTitle, updateNodeText, updateTextStyle, pasteFromClipboard]);

  useEffect(() => {
    if (!isLoaded) return;
    const timer = setTimeout(() => {
      try {
        const stateToSave = {
          title: boardTitle,
          nodes: nodes.map(n => ({ ...n, data: { ...n.data, fonts: undefined } })),
          edges,
          bgSettings
        };
        const jsonStr = JSON.stringify(stateToSave);
        
        if (boardId) {
          setSyncStatus('儲存中...');
          fetch(`https://kvdb.io/${KV_BUCKET}/${boardId}`, {
            method: 'POST',
            body: jsonStr
          }).then(res => {
            if (res.ok) setSyncStatus('已同步 ☁️');
            else setSyncStatus('儲存失敗 ⚠️');
          }).catch(() => setSyncStatus('儲存失敗 ⚠️'));
        } else {
          localStorage.setItem('freeform_app_state', jsonStr);
        }
      } catch (err) {
        console.warn('Failed to save state', err);
      }
    }, 1500); // 延長到 1.5 秒以避免過度頻繁打 API
    return () => clearTimeout(timer);
  }, [nodes, edges, bgSettings, isLoaded, boardId, boardTitle]);

  const handleMoveEnd = useCallback((e, viewport) => {
    if (!isLoaded || boardId) return; // 雲端模式下不儲存 Viewport 到 LocalStorage
    try {
      const raw = localStorage.getItem('freeform_app_state');
      if (!raw) return;
      const currentState = JSON.parse(raw);
      if (currentState.nodes) {
        currentState.viewport = viewport;
        localStorage.setItem('freeform_app_state', JSON.stringify(currentState));
      }
    } catch (err) {}
  }, [isLoaded, boardId]);

  const onNodesChange = useCallback((changes) => setNodes((nds) => applyNodeChanges(changes, nds)), [setNodes]);
  const onEdgesChange = useCallback((changes) => setEdges((eds) => applyEdgeChanges(changes, eds)), [setEdges]);
  const onConnect = useCallback((connection) => {
    const newEdge = { 
      ...connection, id: `e${connection.source}-${connection.target}-${Date.now()}`, type: 'custom',
      data: { strokeWidth: 2, color: '#000', updateEdgeStyle, updateEdgeColor, updateEdgeArrow },
      markerEnd: { type: MarkerType.ArrowClosed, color: '#000' }
    };
    setEdges((eds) => addEdge(newEdge, eds));
  }, [updateEdgeStyle, updateEdgeColor, updateEdgeArrow, setEdges]);

  const addPlainText = () => {
    const newNode = { id: getId(), type: 'plainText', position: { x: window.innerWidth / 2 - 100, y: window.innerHeight / 2 - 100 }, style: { width: 250, height: 80 }, data: { text: '', updateNodeColor, updateNodeText, updateNodeTitle, updateTextStyle, fonts: localFonts } };
    setNodes((nds) => [...nds, newNode]);
  };
  const addStickyNote = () => {
    const newNode = { id: getId(), type: 'sticky', position: { x: window.innerWidth / 2 - 100, y: window.innerHeight / 2 - 100 }, style: { width: 200, height: 150 }, data: { text: '', color: '#ffecb3', updateNodeColor, updateNodeText, updateNodeTitle, updateTextStyle, fonts: localFonts } };
    setNodes((nds) => [...nds, newNode]);
  };
  const addCollapsibleText = () => {
    const newNode = { id: getId(), type: 'collapsible', position: { x: window.innerWidth / 2 - 125, y: window.innerHeight / 2 - 100 }, style: { width: 250, height: 150 }, data: { title: '新文字框', text: '', collapsed: false, color: 'rgba(255, 255, 255, 0.85)', updateNodeColor, updateNodeText, updateNodeTitle, updateTextStyle, toggleNodeCollapse, fonts: localFonts } };
    setNodes((nds) => [...nds, newNode]);
  };
  const addShape = (shapeType) => {
    const newNode = { 
      id: getId(), type: 'shape', position: { x: window.innerWidth / 2 - 50, y: window.innerHeight / 2 - 50 }, 
      style: { width: 100, height: 100 }, 
      data: { shapeType, fillColor: '#d0e8ff', strokeColor: '#0071e3', strokeWidth: 2, text: '', updateNodeText, updateTextStyle, updateShapeStyle } 
    };
    setNodes((nds) => [...nds, newNode]);
    setShowShapeMenu(false);
  };

  // --- 高效能 Drawing Handlers ---
  const onPointerDown = useCallback((e) => {
    closeContextMenu(); // 點擊時關閉右鍵選單
    if (!drawMode) return;
    const pos = screenToFlowPosition({ x: e.clientX, y: e.clientY }, { snapToGrid: false });
    
    if (drawTool === 'eraser') {
      const nodesToErase = nodes.filter(n => n.type === 'drawing' && pos.x >= n.position.x && pos.x <= n.position.x + n.measured.width && pos.y >= n.position.y && pos.y <= n.position.y + n.measured.height);
      if (nodesToErase.length > 0) setNodes(nds => nds.filter(n => !nodesToErase.find(ne => ne.id === n.id)));
      return;
    }

    currentStrokeRef.current = [[pos.x, pos.y, e.pressure || 0.5]];
    
    if (drawingSvgRef.current) {
      const size = drawTool === 'highlighter' ? drawSize * 3 : drawSize;
      const stroke = getStroke(currentStrokeRef.current, { size, thinning: 0.2, smoothing: 0.8, streamline: 0.8 });
      drawingSvgRef.current.setAttribute('d', getSvgPathFromStroke(stroke));
      drawingSvgRef.current.setAttribute('fill', drawColor);
      drawingSvgRef.current.setAttribute('opacity', drawTool === 'highlighter' ? '0.5' : '1');
    }
  }, [drawMode, drawTool, drawSize, drawColor, screenToFlowPosition, nodes, closeContextMenu]);

  const onPointerMove = useCallback((e) => {
    if (!drawMode || !currentStrokeRef.current) return;
    const pos = screenToFlowPosition({ x: e.clientX, y: e.clientY }, { snapToGrid: false });
    currentStrokeRef.current.push([pos.x, pos.y, e.pressure || 0.5]);
    
    if (drawingSvgRef.current) {
      const size = drawTool === 'highlighter' ? drawSize * 3 : drawSize;
      const stroke = getStroke(currentStrokeRef.current, { size, thinning: 0.2, smoothing: 0.8, streamline: 0.8 });
      drawingSvgRef.current.setAttribute('d', getSvgPathFromStroke(stroke));
    }
  }, [drawMode, drawSize, drawTool, screenToFlowPosition]);

  const onPointerUp = useCallback(() => {
    if (!drawMode || !currentStrokeRef.current) return;
    const pts = currentStrokeRef.current;
    if (pts.length > 2) {
      const xs = pts.map(p => p[0]);
      const ys = pts.map(p => p[1]);
      const minX = Math.min(...xs);
      const maxX = Math.max(...xs);
      const minY = Math.min(...ys);
      const maxY = Math.max(...ys);
      const w = Math.max(10, maxX - minX);
      const h = Math.max(10, maxY - minY);
      const relativePoints = pts.map(p => [p[0] - minX, p[1] - minY, p[2]]);
      
      const newNode = {
        id: getId(),
        type: 'drawing',
        position: { x: minX, y: minY },
        style: { width: w, height: h },
        data: { 
          points: relativePoints, 
          originalWidth: w,
          originalHeight: h,
          color: drawColor, 
          strokeWidth: drawTool === 'highlighter' ? drawSize * 3 : drawSize, 
          tool: drawTool 
        },
      };
      setNodes(nds => [...nds, newNode]);
    }
    currentStrokeRef.current = null;
    if (drawingSvgRef.current) drawingSvgRef.current.setAttribute('d', ''); // 清除預覽
  }, [drawMode, drawColor, drawSize, drawTool]);

  const handleFileUpload = (e) => {
    const file = e.target.files[0];
    if (!file) return;
    if (file.size > 2 * 1024 * 1024) {
      alert('檔案超過 2MB，受限於瀏覽器儲存空間，重新整理網頁後可能無法完整保存。建議使用壓縮後的檔案！');
    }
    const reader = new FileReader();
    reader.onload = (event) => {
      const url = event.target.result;
      const isPDF = file.type === 'application/pdf';
      const newNode = {
        id: getId(), type: isPDF ? 'pdf' : 'image', position: { x: window.innerWidth / 2 - 100, y: window.innerHeight / 2 - 100 },
        style: isPDF ? { width: 400, height: 500 } : { width: 300, height: 200 }, data: { url }
      };
      setNodes(nds => [...nds, newNode]);
    };
    reader.readAsDataURL(file);
    setShowAttachMenu(false);
  };
  
  const handleLinkInsert = () => {
    const url = prompt("請輸入網址 (包含 https://):");
    if (url) {
      const newNode = { id: getId(), type: 'link', position: { x: window.innerWidth / 2 - 100, y: window.innerHeight / 2 - 100 }, style: { width: 250, height: 100 }, data: { url } };
      setNodes(nds => [...nds, newNode]);
    }
    setShowAttachMenu(false);
  };

  const toggleTreeCollapse = useCallback((id) => {
    setNodes(nds => nds.map(n => 
      n.id === id ? { ...n, data: { ...n.data, treeCollapsed: !n.data?.treeCollapsed } } : n
    ));
  }, [setNodes]);

  const processedNodes = React.useMemo(() => {
    // BFS to find hidden nodes
    const collapsedRoots = nodes.filter(n => n.data?.treeCollapsed).map(n => n.id);
    const hiddenNodeIds = new Set();
    
    if (collapsedRoots.length > 0) {
      const queue = [...collapsedRoots];
      while (queue.length > 0) {
        const currentId = queue.shift();
        const childrenIds = edges.filter(e => e.source === currentId).map(e => e.target);
        for (const childId of childrenIds) {
          if (!hiddenNodeIds.has(childId)) {
            hiddenNodeIds.add(childId);
            queue.push(childId);
          }
        }
      }
    }

    return nodes.map(n => {
      let hidden = hiddenNodeIds.has(n.id);
      
      let selectable = true;
      let draggable = true;
      let pointerEvents = 'auto';
      let className = (n.className || '').replace('inactive-child', '').trim();
      
      if (n.parentId && n.parentId !== activeGroupId) {
        selectable = false;
        draggable = false;
        pointerEvents = 'none';
        className += ' inactive-child';
      } else if (n.parentId && n.parentId === activeGroupId) {
        selectable = true;
        draggable = true;
      }

      return {
        ...n,
        hidden: hidden || n.hidden,
        selectable,
        draggable,
        className,
        style: { ...n.style, pointerEvents }
      };
    });
  }, [nodes, edges, activeGroupId]);

  return (
    <GroupContext.Provider value={{ activeGroupId, setActiveGroupId, toggleTreeCollapse }}>
      <div 
        className={drawMode ? 'drawing-mode' : ''} 
        style={{ width: '100%', height: '100%', backgroundColor: bgSettings.color }}
        onPointerDown={onPointerDown}
        onPointerMove={onPointerMove}
        onPointerUp={onPointerUp}
        onPointerCancel={onPointerUp}
      >
        <ReactFlow
          nodes={processedNodes} edges={edges}
          onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect}
          nodeTypes={nodeTypes} edgeTypes={edgeTypes}
          fitView fitViewOptions={{ padding: 2 }} minZoom={0.2} maxZoom={4}
          panOnDrag={false} // 改為 Space 平移
          selectionOnDrag={!drawMode} // 畫布拖曳改為框選
          panActivationKeyCode="Space"
          selectionMode="partial"
          connectionMode="loose"
          onNodeContextMenu={onNodeContextMenu}
          onPaneContextMenu={onPaneContextMenu}
          onPaneClick={onPaneClick}
          onMoveEnd={handleMoveEnd}
        >
        {bgSettings.variant !== 'none' && <Background color={bgSettings.gridColor} gap={bgSettings.gap} lineWidth={bgSettings.lineWidth} variant={bgSettings.variant} />}
        <Controls showInteractive={false} position="bottom-right" style={{ bottom: 100 }} />
      </ReactFlow>

      {/* 雲端同步狀態提示 */}
      <div className="cloud-sync-status" style={{
        position: 'absolute', top: 20, left: 20, zIndex: 1000, 
        background: 'rgba(255,255,255,0.9)', padding: '8px 16px', 
        borderRadius: '20px', boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
        fontSize: '14px', fontWeight: '500', display: 'flex', alignItems: 'center', gap: '8px',
        backdropFilter: 'blur(10px)', border: '1px solid rgba(0,0,0,0.05)'
      }}>
        <button onClick={() => window.location.href = '/'} style={{ background: 'transparent', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', padding: 0, color: '#007aff', fontWeight: '600', marginRight: '8px' }}>
          &lt; 目錄
        </button>
        <div style={{ width: '1px', height: '16px', background: '#ccc' }} />
        <input 
          value={boardTitle} 
          onChange={e => setBoardTitle(e.target.value)} 
          style={{ border: 'none', background: 'transparent', fontWeight: '600', fontSize: '16px', width: '120px', outline: 'none' }}
          placeholder="白板名稱"
        />
        <div style={{ width: '1px', height: '16px', background: '#ccc' }} />
        <div style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: syncStatus.includes('已同步') || syncStatus.includes('本機') ? '#34c759' : (syncStatus.includes('失敗') ? '#ff3b30' : '#ff9500') }} title={syncStatus} />
        {boardId && (
          <button 
            onClick={() => { navigator.clipboard.writeText(window.location.href); alert('網址已複製！您可以將這個網址分享給其他設備或朋友，他們就能看到一樣的畫布。'); }}
            style={{ marginLeft: 8, padding: '4px 8px', fontSize: 12, background: '#0071e3', color: 'white', border: 'none', borderRadius: 12, cursor: 'pointer' }}
          >
            複製分享連結
          </button>
        )}
      </div>

      {/* 右鍵選單 (Context Menu) */}
      {contextMenu && (
        <div className="context-menu" style={{ top: contextMenu.top, left: contextMenu.left }}>
          {contextMenu.type === 'node' && (
            <>
              {selectedNodes.length >= 2 && (
                <button onClick={() => { createGroup(); closeContextMenu(); }}><Grid size={16}/> 群組</button>
              )}
              {selectedNodes.length === 1 && selectedNodes[0].type === 'group' && (
                <button onClick={() => { ungroup(selectedNodes[0].id); closeContextMenu(); }}><Grid size={16}/> 取消群組</button>
              )}
              {selectedNodes.length >= 2 || (selectedNodes.length === 1 && selectedNodes[0].type === 'group') ? <div className="divider"></div> : null}
              <button onClick={() => { handleBringToFront(); closeContextMenu(); }}><ArrowUpToLine size={16}/> 移至最前</button>
              <button onClick={() => { handleSendToBack(); closeContextMenu(); }}><ArrowDownToLine size={16}/> 移至最後</button>
              <div className="divider"></div>
              <button onClick={() => { cutToClipboard(); closeContextMenu(); }}><Scissors size={16}/> 剪下</button>
              <button onClick={() => { copyToClipboard(); closeContextMenu(); }}><CopyIcon size={16}/> 拷貝</button>
              <button onClick={() => { pasteFromClipboard(contextMenu); closeContextMenu(); }}><ClipboardPaste size={16}/> 貼上</button>
              <button onClick={() => { duplicateSelected(); closeContextMenu(); }}><CopyPlus size={16}/> 複製</button>
            </>
          )}
          {contextMenu.type === 'pane' && (
            <button onClick={() => { pasteFromClipboard(contextMenu); closeContextMenu(); }}><ClipboardPaste size={16}/> 貼上</button>
          )}
        </div>
      )}

      {/* 高效能繪圖 SVG Layer */}
      <svg style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none', zIndex: 1000, display: drawMode ? 'block' : 'none' }}>
        <g transform={`translate(${viewport.x}, ${viewport.y}) scale(${viewport.zoom})`}>
          <path ref={drawingSvgRef} d="" fill="transparent" />
        </g>
      </svg>

      {/* 多選排版工具列 */}
      {selectedNodes.length >= 2 && !drawMode && (
        <div className="multi-select-toolbar">
          <span>排版</span>
          <div className="divider" />
          <button onClick={() => handleAlign('left')} title="齊左">齊左</button>
          <button onClick={() => handleAlign('center')} title="水平置中">置中</button>
          <button onClick={() => handleAlign('right')} title="齊右">齊右</button>
          <div className="divider" />
          <button onClick={() => handleAlign('top')} title="齊上">齊上</button>
          <button onClick={() => handleAlign('middle')} title="垂直置中">置中</button>
          <button onClick={() => handleAlign('bottom')} title="齊下">齊下</button>
          <div className="divider" />
          <button onClick={() => handleDistribute('horizontal')} title="水平分散">平分寬度</button>
          <button onClick={() => handleDistribute('vertical')} title="垂直分散">平分高度</button>
          <div className="divider" />
          <button onClick={handleGroup} style={{ fontWeight: 'bold', color: 'var(--accent-color)' }}>群組</button>
        </div>
      )}

      {/* 解散群組工具列 */}
      {selectedNodes.length === 1 && selectedNodes[0].type === 'group' && !drawMode && (
        <div className="multi-select-toolbar">
          <button onClick={handleUngroup} style={{ fontWeight: 'bold', color: '#ff3b30' }}>解散群組</button>
        </div>
      )}

      {/* 繪圖專屬工具列 */}
      {drawMode && (
        <div className="drawing-toolbar">
          <button className={`drawing-tool-btn ${drawTool === 'pen' ? 'active' : ''}`} onClick={() => setDrawTool('pen')} title="鋼筆"><PenTool size={20}/></button>
          <button className={`drawing-tool-btn ${drawTool === 'highlighter' ? 'active' : ''}`} onClick={() => setDrawTool('highlighter')} title="螢光筆"><Highlighter size={20}/></button>
          <button className={`drawing-tool-btn ${drawTool === 'eraser' ? 'active' : ''}`} onClick={() => setDrawTool('eraser')} title="橡皮擦 (點擊刪除)"><Eraser size={20}/></button>
          <div style={{ width: '1px', height: '24px', background: 'var(--toolbar-border)', margin: '0 8px' }}></div>
          <input type="range" min="2" max="30" value={drawSize} onChange={e => setDrawSize(parseInt(e.target.value))} style={{ width: 80 }} title="粗細"/>
          <div style={{ width: '1px', height: '24px', background: 'var(--toolbar-border)', margin: '0 8px' }}></div>
          <input type="color" className="native-color-picker" value={drawColor} onChange={e => setDrawColor(e.target.value)} title="顏色" />
        </div>
      )}

      {/* 畫布設定面板 */}
      {showBgSettings && (
        <div className="bg-settings-popover">
          <h4>畫布設定</h4>
          <label>背景顏色 <input type="color" className="native-color-picker" value={bgSettings.color} onChange={e => setBgSettings(s => ({ ...s, color: e.target.value }))} /></label>
          <label>格線樣式 
            <select value={bgSettings.variant} onChange={e => setBgSettings(s => ({ ...s, variant: e.target.value }))}>
              <option value="lines">方格</option>
              <option value="dots">點陣</option>
              <option value="none">無網格</option>
            </select>
          </label>
          {bgSettings.variant !== 'none' && (
            <>
              <label>格線顏色 <input type="color" className="native-color-picker" value={bgSettings.gridColor} onChange={e => setBgSettings(s => ({ ...s, gridColor: e.target.value }))} /></label>
              <label>格線大小 <input type="range" min="10" max="100" value={bgSettings.gap} onChange={e => setBgSettings(s => ({ ...s, gap: parseInt(e.target.value) }))} /></label>
              <label>格線粗細 <input type="range" min="1" max="10" step="0.5" value={bgSettings.lineWidth} onChange={e => setBgSettings(s => ({ ...s, lineWidth: parseFloat(e.target.value) }))} /></label>
            </>
          )}
        </div>
      )}

      {/* Main Toolbar */}
      <div className="toolbar-container">
        <button className={`toolbar-btn ${drawMode ? 'active' : ''}`} style={drawMode ? { backgroundColor: 'var(--accent-color)', color: 'white' } : {}} onClick={() => setDrawMode(!drawMode)} title="畫筆模式 (支援 iPad)">
          <PenTool size={20} />
        </button>
        <div style={{ width: '1px', background: 'var(--toolbar-border)', margin: '0 8px' }}></div>
        
        <button className="toolbar-btn" onClick={addPlainText} title="純文字"><TypeIcon size={20} /></button>
        <button className="toolbar-btn" onClick={addStickyNote} title="新增便條紙"><StickyNote size={20} /></button>
        <button className="toolbar-btn" onClick={addCollapsibleText} title="新增折疊文字"><Type size={20} /></button>
        
        {/* 圖形選單 */}
        <div style={{ position: 'relative' }}>
          <button className="toolbar-btn" onClick={() => { setShowShapeMenu(!showShapeMenu); setShowAttachMenu(false); setShowBgSettings(false); }} title="加入圖形"><Shapes size={20} /></button>
          {showShapeMenu && (
            <div className="shape-picker-menu">
              <div className="shape-btn" onClick={() => addShape('rectangle')}><Square size={32} /></div>
              <div className="shape-btn" onClick={() => addShape('rounded-rectangle')}><rect width="32" height="32" rx="8" fill="currentColor" opacity="0.8" viewBox="0 0 32 32" /></div>
              <div className="shape-btn" onClick={() => addShape('circle')}><Circle size={32} fill="currentColor" opacity="0.8" /></div>
              <div className="shape-btn" onClick={() => addShape('triangle')}><Triangle size={32} fill="currentColor" opacity="0.8" /></div>
              <div className="shape-btn" onClick={() => addShape('diamond')}><Diamond size={32} fill="currentColor" opacity="0.8" /></div>
              <div className="shape-btn" onClick={() => addShape('hexagon')}><Hexagon size={32} fill="currentColor" opacity="0.8" /></div>
              <div className="shape-btn" onClick={() => addShape('pentagon')}><Pentagon size={32} fill="currentColor" opacity="0.8" /></div>
              <div className="shape-btn" onClick={() => addShape('star')}><Star size={32} fill="currentColor" opacity="0.8" /></div>
              <div className="shape-btn" onClick={() => addShape('parallelogram')}>
                <svg width="32" height="32" viewBox="0 0 100 100"><polygon points="25,10 90,10 75,90 10,90" fill="currentColor" opacity="0.8" /></svg>
              </div>
              <div className="shape-btn" onClick={() => addShape('cylinder')}>
                <svg width="32" height="32" viewBox="0 0 100 100">
                  <path d="M10,25 Q50,0 90,25 L90,75 Q50,100 10,75 Z" fill="currentColor" opacity="0.8" />
                  <ellipse cx="50" cy="25" rx="40" ry="12" fill="none" stroke="currentColor" strokeWidth="4" />
                </svg>
              </div>
            </div>
          )}
        </div>

        {/* 附件選單 */}
        <div style={{ position: 'relative' }}>
          <button className="toolbar-btn" onClick={() => setShowAttachMenu(!showAttachMenu)} title="加入附件"><Paperclip size={20} /></button>
          {showAttachMenu && (
            <div className="attach-menu">
              <button onClick={() => fileInputRef.current.click()}><FileImage size={16}/> 圖片或 PDF</button>
              <button onClick={handleLinkInsert}><LinkIcon size={16}/> 網頁連結</button>
            </div>
          )}
          <input type="file" ref={fileInputRef} style={{ display: 'none' }} accept="image/*,application/pdf" onChange={handleFileUpload} />
        </div>

        <div style={{ width: '1px', background: 'var(--toolbar-border)', margin: '0 8px' }}></div>
        <button className="toolbar-btn" onClick={() => setShowBgSettings(!showBgSettings)} title="畫布設定"><Settings size={20} /></button>
        <div style={{ width: '1px', background: 'var(--toolbar-border)', margin: '0 8px' }}></div>
        <button className="toolbar-btn" onClick={deleteSelectedNodes} title="刪除選取的項目" style={{ color: '#ff3b30' }}><Trash2 size={20} /></button>
      </div>
    </div>
    </GroupContext.Provider>
  );
}

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  render() {
    if (this.state.hasError) {
      return (
        <div style={{ padding: 40, fontFamily: 'sans-serif', background: '#f8d7da', color: '#721c24', height: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
          <h2>Oops! 白板發生了崩潰 😭</h2>
          <p style={{ marginBottom: 20 }}>很可能是讀取舊的暫存檔時格式不相容。請點擊下方按鈕清除暫存並重新載入：</p>
          <button 
            style={{ padding: '10px 20px', fontSize: 16, cursor: 'pointer', background: '#dc3545', color: 'white', border: 'none', borderRadius: 8 }}
            onClick={() => { localStorage.removeItem('freeform_app_state'); window.location.reload(); }}
          >
            清除存檔並修復 (Clear Save & Reload)
          </button>
          <pre style={{ marginTop: 20, fontSize: 12, opacity: 0.7, maxWidth: '80%', overflow: 'auto' }}>{this.state.error?.toString()}</pre>
        </div>
      );
    }
    return this.props.children; 
  }
}

const Dashboard = () => {
  const [boards, setBoards] = useState([]);
  const [syncKey, setSyncKey] = useState(localStorage.getItem('freeform_catalog_sync_key') || '');
  const [isSyncing, setIsSyncing] = useState(false);

  useEffect(() => {
    const recent = JSON.parse(localStorage.getItem('freeform_recent_boards') || '[]');
    
    // Check if there is an old local state to migrate
    const localState = localStorage.getItem('freeform_app_state');
    if (localState && !recent.find(b => b.id === 'local-migration')) {
      recent.unshift({
        id: 'local-migration',
        title: '舊版未命名白板',
        timestamp: Date.now(),
        color: '#ff9500'
      });
      localStorage.setItem('freeform_recent_boards', JSON.stringify(recent));
    }
    
    recent.sort((a, b) => b.timestamp - a.timestamp);
    setBoards(recent);
  }, []);

  const createNewBoard = () => {
    const id = Math.random().toString(36).substring(2, 10);
    window.location.href = `?board=${id}`;
  };

  const openBoard = (id) => {
    if (id === 'local-migration') {
       window.location.href = `?board=${Math.random().toString(36).substring(2, 10)}&migrate=true`;
    } else {
       window.location.href = `?board=${id}`;
    }
  };

  const handleSync = async () => {
    if (!syncKey) return;
    setIsSyncing(true);
    try {
      const res = await fetch(`https://kvdb.io/WMgTaeNQNsJ6knUcqk1Gwb/catalog_${syncKey}`);
      let merged = JSON.parse(localStorage.getItem('freeform_recent_boards') || '[]');
      if (res.ok) {
         const cloudData = await res.json();
         cloudData.forEach(cb => {
            const existingIdx = merged.findIndex(lb => lb.id === cb.id);
            if (existingIdx >= 0) {
               if (cb.timestamp > merged[existingIdx].timestamp) merged[existingIdx] = cb;
            } else {
               merged.push(cb);
            }
         });
         merged.sort((a,b) => b.timestamp - a.timestamp);
      }
      localStorage.setItem('freeform_recent_boards', JSON.stringify(merged));
      setBoards(merged);
      await fetch(`https://kvdb.io/WMgTaeNQNsJ6knUcqk1Gwb/catalog_${syncKey}`, {
         method: 'POST',
         body: JSON.stringify(merged)
      });
      localStorage.setItem('freeform_catalog_sync_key', syncKey);
      alert('目錄同步成功！');
    } catch (e) {
      alert('同步失敗，請檢查網路。');
    }
    setIsSyncing(false);
  };

  return (
    <div className="dashboard-container">
      <div className="dashboard-header">
        <h1>所有白板</h1>
        <div className="dashboard-actions" style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
          <div style={{ display: 'flex', alignItems: 'center', background: 'white', padding: '4px 12px', borderRadius: '20px', boxShadow: '0 2px 8px rgba(0,0,0,0.05)' }}>
            <span style={{ fontSize: 12, color: '#86868b', marginRight: 8, whiteSpace: 'nowrap' }}>同步密碼:</span>
            <input 
              value={syncKey} 
              onChange={e => setSyncKey(e.target.value)} 
              placeholder="設定密碼" 
              style={{ border: 'none', outline: 'none', width: '80px', fontSize: 14 }}
            />
            <button onClick={handleSync} disabled={isSyncing} style={{ background: 'transparent', color: '#007aff', boxShadow: 'none', padding: '4px 0', fontSize: 14, marginLeft: 8 }}>
              {isSyncing ? '同步中...' : '同步'}
            </button>
          </div>
          <button onClick={createNewBoard}>+ 新增白板</button>
        </div>
      </div>
      <div className="boards-grid">
        {boards.map(b => (
          <div className="board-card" key={b.id} onClick={() => openBoard(b.id)}>
            <div className="board-card-thumbnail" style={{ backgroundColor: b.color || '#34c759' }}>
              {b.title ? b.title.charAt(0).toUpperCase() : '未'}
            </div>
            <div className="board-card-info">
              <input 
                className="board-card-title" 
                value={b.title} 
                onClick={(e) => e.stopPropagation()} 
                onChange={(e) => {
                  const newTitle = e.target.value;
                  const newBoards = boards.map(board => board.id === b.id ? { ...board, title: newTitle, timestamp: Date.now() } : board);
                  setBoards(newBoards);
                  localStorage.setItem('freeform_recent_boards', JSON.stringify(newBoards));
                  if (syncKey) {
                    fetch(`https://kvdb.io/WMgTaeNQNsJ6knUcqk1Gwb/catalog_${syncKey}`, {
                      method: 'POST',
                      body: JSON.stringify(newBoards)
                    }).catch(console.error);
                  }
                }}
              />
              <span className="board-card-date">{new Date(b.timestamp).toLocaleString()}</span>
            </div>
          </div>
        ))}
        {boards.length === 0 && (
          <div style={{ color: '#86868b', gridColumn: '1 / -1' }}>目前還沒有任何白板，點擊右上角新增一個吧！</div>
        )}
      </div>
    </div>
  );
};

const App = () => {
  const urlParams = new URLSearchParams(window.location.search);
  const boardId = urlParams.get('board');

  if (!boardId) {
    return <Dashboard />;
  }

  return (
    <ErrorBoundary>
      <ReactFlowProvider>
        <FlowApp />
      </ReactFlowProvider>
    </ErrorBoundary>
  );
};

const root = createRoot(document.getElementById('root'));
root.render(<App />);
