import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  ReactFlow,
  ReactFlowProvider,
  Node,
  Edge,
  Background,
  Controls,
  Handle,
  Position,
  NodeProps,
  EdgeProps,
  useNodesState,
  useEdgesState,
  useReactFlow,
  OnConnect,
  addEdge,
  XYPosition,
  MarkerType,
  NodeResizer,
  BaseEdge,
  EdgeLabelRenderer,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import type { Project } from "../../lib/commands";
import { getCanvasState, saveCanvasState } from "../../lib/commands";
import { openUrl } from "@tauri-apps/plugin-opener";

// ── 类型 ─────────────────────────────────────────────────────────

interface GroupMember {
  project: Project;
  role: string | null | undefined;
}

interface ProjectNodeData { project: Project; role?: string | null; [key: string]: unknown; }
interface GitNodeData     { label: string; url: string; projectName: string; [key: string]: unknown; }
interface EnvNodeData     { variant: "prod" | "staging"; url: string; projectName: string; [key: string]: unknown; }
interface ServerNodeData  { note: string; projectName: string; [key: string]: unknown; }
interface FocusNodeData   { text: string; projectName: string; [key: string]: unknown; }
interface CustomNodeData          { name: string; color: string; customItemId?: string; [key: string]: unknown; }
interface ContainerChildNodeData  { name: string; color: string; [key: string]: unknown; }

const DEFAULT_CUSTOM_COLOR = "#8b5cf6";
const PRESET_COLORS = [
  "#6b7280","#ef4444","#f97316","#eab308",
  "#22c55e","#06b6d4","#3b82f6","#8b5cf6","#ec4899",
];

interface SidebarItem {
  id: string;
  nodeType: string;
  label: string;
  sublabel?: string;
  data: Record<string, unknown>;
  colorClass: string;
}

interface CustomItem {
  id: string;
  name: string;
  color: string;
  variant?: "node" | "container";   // 默认 "node"
}

// ── 节点删除按钮 ─────────────────────────────────────────────────

function DeleteBtn({ nodeId }: { nodeId: string }) {
  const { deleteElements } = useReactFlow();
  return (
    <button
      onClick={() => deleteElements({ nodes: [{ id: nodeId }] })}
      className="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-gray-400 hover:bg-red-500 text-white text-[9px] flex items-center justify-center shadow transition-colors z-10 opacity-0 group-hover:opacity-100"
    >
      ×
    </button>
  );
}

// ── 节点复制按钮（简单节点通用）──────────────────────────────────

function CopyBtn({ nodeId }: { nodeId: string }) {
  const { getNode, addNodes } = useReactFlow();
  const copy = (e: React.MouseEvent) => {
    e.stopPropagation();
    const node = getNode(nodeId);
    if (!node) return;
    addNodes([{
      ...node,
      id: `${node.type ?? "node"}-${Date.now()}-${Math.random().toString(36).slice(2, 4)}`,
      position: { x: node.position.x + 20, y: node.position.y + 20 },
      selected: false,
    }]);
  };
  return (
    <button
      onClick={copy}
      className="absolute -top-1.5 right-4 w-4 h-4 rounded-full bg-gray-400 hover:bg-blue-500 text-white text-[9px] flex items-center justify-center shadow transition-colors z-10 opacity-0 group-hover:opacity-100"
      title="复制"
    >
      ⧉
    </button>
  );
}

// ── 数据节点 ─────────────────────────────────────────────────────

function ProjectNode({ id, data }: NodeProps) {
  const { project: p, role } = data as ProjectNodeData;
  return (
    <div className="group relative bg-white border-2 border-blue-300 rounded-xl shadow-sm w-48 select-none">
      <DeleteBtn nodeId={id} />
      <CopyBtn nodeId={id} />
      <Handle type="source" position={Position.Right} style={{ opacity: 0 }} />
      <Handle type="target" position={Position.Left}  style={{ opacity: 0 }} />
      <div className="px-3 pt-2.5 pb-2">
        <p className="text-xs font-bold text-gray-800 truncate" title={p.name}>{p.name}</p>
        <div className="flex flex-wrap items-center gap-1 mt-1.5">
          <span className={`text-[10px] px-1.5 py-px rounded font-medium ${p.platform === "wsl" ? "bg-green-50 text-green-600 border border-green-200" : "bg-blue-50 text-blue-600 border border-blue-200"}`}>
            {p.platform === "wsl" ? "WSL" : "Windows"}
          </span>
          {role     && <span className="text-[10px] px-1.5 py-px rounded bg-purple-50 text-purple-600 border border-purple-100">{role}</span>}
          {p.status && <span className="text-[10px] px-1.5 py-px rounded bg-gray-50 text-gray-500 border border-gray-200">{p.status}</span>}
        </div>
        {p.tech_stack && <p className="text-[10px] text-gray-400 mt-1.5 truncate" title={p.tech_stack}>{p.tech_stack}</p>}
      </div>
    </div>
  );
}

function GitNode({ id, data }: NodeProps) {
  const { label, url, projectName } = data as GitNodeData;
  return (
    <div className="group relative bg-slate-50 border border-slate-300 rounded-lg shadow-sm w-44 select-none cursor-pointer hover:shadow-md transition-shadow"
         onClick={() => openUrl(url).catch(() => {})}>
      <DeleteBtn nodeId={id} />
      <CopyBtn nodeId={id} />
      <Handle type="source" position={Position.Right} style={{ opacity: 0 }} />
      <Handle type="target" position={Position.Left}  style={{ opacity: 0 }} />
      <div className="px-3 py-2">
        <p className="text-[10px] font-bold text-slate-400 uppercase tracking-wide mb-0.5">🔀 {label}</p>
        <p className="text-[10px] text-slate-500 truncate" title={projectName}>{projectName}</p>
        <p className="text-[9px] text-slate-400 truncate mt-0.5" title={url}>{url.replace(/^https?:\/\/(www\.)?/, "").replace(/\.git$/, "")}</p>
      </div>
    </div>
  );
}

function EnvNode({ id, data }: NodeProps) {
  const { variant, url, projectName } = data as EnvNodeData;
  const isProd = variant === "prod";
  return (
    <div className={`group relative rounded-lg shadow-sm border w-44 select-none cursor-pointer hover:shadow-md transition-shadow ${isProd ? "bg-emerald-50 border-emerald-300" : "bg-amber-50 border-amber-300"}`}
         onClick={() => openUrl(url).catch(() => {})}>
      <DeleteBtn nodeId={id} />
      <CopyBtn nodeId={id} />
      <Handle type="source" position={Position.Right} style={{ opacity: 0 }} />
      <Handle type="target" position={Position.Left}  style={{ opacity: 0 }} />
      <div className="px-3 py-2">
        <p className={`text-[10px] font-bold uppercase tracking-wide mb-0.5 ${isProd ? "text-emerald-600" : "text-amber-600"}`}>{isProd ? "🌐 生产" : "🧪 测试"}</p>
        <p className="text-[10px] text-gray-600 truncate" title={projectName}>{projectName}</p>
        <p className={`text-[9px] truncate mt-0.5 ${isProd ? "text-emerald-700" : "text-amber-700"}`} title={url}>{url.replace(/^https?:\/\//, "")}</p>
      </div>
    </div>
  );
}

function ServerNode({ id, data }: NodeProps) {
  const { note, projectName } = data as ServerNodeData;
  return (
    <div className="group relative bg-slate-50 border border-slate-300 rounded-lg shadow-sm w-44 select-none">
      <DeleteBtn nodeId={id} />
      <CopyBtn nodeId={id} />
      <Handle type="source" position={Position.Right} style={{ opacity: 0 }} />
      <Handle type="target" position={Position.Left}  style={{ opacity: 0 }} />
      <div className="px-3 py-2">
        <p className="text-[10px] font-bold text-slate-500 uppercase tracking-wide mb-0.5">🖥 Server</p>
        <p className="text-[10px] text-slate-500 truncate" title={projectName}>{projectName}</p>
        <p className="text-[10px] text-slate-600 mt-0.5 line-clamp-2" title={note}>{note}</p>
      </div>
    </div>
  );
}

function FocusNode({ id, data }: NodeProps) {
  const { text, projectName } = data as FocusNodeData;
  return (
    <div className="group relative bg-indigo-50 border border-indigo-200 rounded-lg shadow-sm w-48 select-none">
      <DeleteBtn nodeId={id} />
      <CopyBtn nodeId={id} />
      <Handle type="source" position={Position.Right} style={{ opacity: 0 }} />
      <Handle type="target" position={Position.Left}  style={{ opacity: 0 }} />
      <div className="px-3 py-2">
        <p className="text-[10px] font-bold text-indigo-500 uppercase tracking-wide mb-0.5">📌 当前焦点</p>
        <p className="text-[10px] text-indigo-600 truncate" title={projectName}>{projectName}</p>
        <p className="text-[10px] text-indigo-700 mt-0.5 line-clamp-3" title={text}>{text}</p>
      </div>
    </div>
  );
}

// ── 自定义节点（画布上）──────────────────────────────────────────

function CustomNode({ id, data }: NodeProps) {
  const { name, color } = data as CustomNodeData;
  const { updateNodeData, addNodes, addEdges, getNode, deleteElements } = useReactFlow();
  const [editing, setEditing]     = useState(false);
  const [editName, setEditName]   = useState(name);
  const [editColor, setEditColor] = useState(color || DEFAULT_CUSTOM_COLOR);

  const c = color || DEFAULT_CUSTOM_COLOR;

  const startEdit = () => {
    setEditName(name);
    setEditColor(color || DEFAULT_CUSTOM_COLOR);
    setEditing(true);
  };

  const commit = () => {
    const n = editName.trim();
    if (!n) return;
    updateNodeData(id, { name: n, color: editColor });
    setEditing(false);
  };

  const cancel = () => setEditing(false);

  // 添加子节点：在当前节点正下方创建一个新自定义节点并连线
  const addChildNode = (e: React.MouseEvent) => {
    e.stopPropagation();
    const parent = getNode(id);
    if (!parent) return;
    const childId = `custom-${Date.now()}-${Math.random().toString(36).slice(2, 4)}`;
    addNodes([{
      id: childId,
      type: "custom",
      position: { x: parent.position.x, y: parent.position.y + 130 },
      data: { name: "子节点", color: c },
    }]);
    addEdges([{
      id: `e-${id}-${childId}`,
      source: id,
      target: childId,
      sourceHandle: "bottom",
      targetHandle: "top",
      type: "labeled",
      data: { label: "", _smooth: true },
      style: { stroke: c, strokeWidth: 1.5 },
      markerEnd: { type: MarkerType.ArrowClosed, color: c, width: 12, height: 12 },
    }]);
  };

  const handleStyle = {
    width: 8, height: 8,
    background: c,
    border: "2px solid white",
    boxShadow: "0 0 0 1px " + c,
  };

  if (editing) {
    return (
      <div
        className="relative rounded-xl shadow-md w-44 bg-white"
        style={{ border: `2px dashed ${editColor}` }}
      >
        {/* 编辑态保留隐形 handle 以免断线 */}
        <Handle id="top"    type="target" position={Position.Top}    style={{ opacity: 0 }} />
        <Handle id="bottom" type="source" position={Position.Bottom} style={{ opacity: 0 }} />
        <Handle id="left"   type="target" position={Position.Left}   style={{ opacity: 0 }} />
        <Handle id="right"  type="source" position={Position.Right}  style={{ opacity: 0 }} />
        <div className="px-3 py-2.5 flex flex-col gap-2">
          <input
            autoFocus
            value={editName}
            onChange={(e) => setEditName(e.target.value)}
            onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Enter") commit(); if (e.key === "Escape") cancel(); }}
            onPointerDown={(e) => e.stopPropagation()}
            className="text-[11px] border border-gray-200 rounded px-1.5 py-0.5 outline-none focus:ring-1 bg-white w-full"
          />
          <div className="flex items-center gap-1 flex-wrap">
            {PRESET_COLORS.map((pc) => (
              <button
                key={pc}
                onPointerDown={(e) => e.stopPropagation()}
                onClick={() => setEditColor(pc)}
                className="w-4 h-4 rounded-full transition-all"
                style={{
                  backgroundColor: pc,
                  outline: editColor === pc ? `2px solid ${pc}` : "2px solid transparent",
                  outlineOffset: "2px",
                  transform: editColor === pc ? "scale(1.2)" : "scale(1)",
                }}
              />
            ))}
          </div>
          <div className="flex gap-1">
            <button onPointerDown={(e) => e.stopPropagation()} onClick={commit}
              className="flex-1 text-[10px] text-white rounded py-0.5" style={{ background: editColor }}>
              保存
            </button>
            <button onPointerDown={(e) => e.stopPropagation()} onClick={cancel}
              className="flex-1 text-[10px] border border-gray-200 text-gray-500 rounded py-0.5 hover:bg-gray-50">
              取消
            </button>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div
      className="group relative rounded-xl shadow-sm w-40 select-none"
      style={{ border: `2px dashed ${c}`, background: `${c}14` }}
    >
      {/* 顶部：接受父节点连线（target） */}
      <Handle id="top" type="target" position={Position.Top}
        className="opacity-30 group-hover:opacity-100 transition-opacity"
        style={{ ...handleStyle, top: -4 }} />
      {/* 底部：发起子节点连线（source） */}
      <Handle id="bottom" type="source" position={Position.Bottom}
        className="opacity-30 group-hover:opacity-100 transition-opacity"
        style={{ ...handleStyle, bottom: -4 }} />
      {/* 左右保留隐形 handle 供通用连线使用 */}
      <Handle id="left"  type="target" position={Position.Left}  style={{ opacity: 0 }} />
      <Handle id="right" type="source" position={Position.Right} style={{ opacity: 0 }} />

      {/* hover 时顶部工具栏：编辑 / 复制 / 删除 */}
      <div className="absolute -top-6 left-1/2 -translate-x-1/2 flex items-center gap-px bg-white border border-gray-200 rounded-full shadow px-1 py-0.5 opacity-0 group-hover:opacity-100 transition-opacity z-20">
        <button
          onClick={startEdit}
          className="w-5 h-5 rounded-full hover:bg-violet-50 text-gray-400 hover:text-violet-500 text-[10px] flex items-center justify-center transition-colors"
          title="编辑"
        >✎</button>
        <button
          onClick={(e) => {
            e.stopPropagation();
            const node = getNode(id);
            if (!node) return;
            addNodes([{ ...node, id: `custom-${Date.now()}-${Math.random().toString(36).slice(2,4)}`, position: { x: node.position.x + 20, y: node.position.y + 20 }, selected: false }]);
          }}
          className="w-5 h-5 rounded-full hover:bg-blue-50 text-gray-400 hover:text-blue-500 text-[10px] flex items-center justify-center transition-colors"
          title="复制"
        >⧉</button>
        <button
          onClick={(e) => { e.stopPropagation(); deleteElements({ nodes: [{ id }] }); }}
          className="w-5 h-5 rounded-full hover:bg-red-50 text-gray-400 hover:text-red-400 text-[10px] flex items-center justify-center transition-colors"
          title="删除"
        >×</button>
      </div>

      <div className="px-3 py-2.5 flex items-center gap-2">
        <span className="text-sm shrink-0" style={{ color: c }}>✦</span>
        <p className="text-[11px] font-semibold truncate" style={{ color: c }} title={name}>{name}</p>
      </div>

      {/* 底部：添加子节点快捷按钮 */}
      <button
        onClick={addChildNode}
        className="absolute -bottom-3.5 left-1/2 -translate-x-1/2 w-5 h-5 rounded-full bg-white border-2 text-[11px] font-bold flex items-center justify-center shadow transition-all z-10 opacity-0 group-hover:opacity-100 hover:scale-110"
        style={{ borderColor: c, color: c }}
        title="添加子节点"
      >
        +
      </button>
    </div>
  );
}

// ── 容器节点（大图，子节点在内部）────────────────────────────────

function ContainerNode({ id, data, selected }: NodeProps) {
  const { name, color } = data as CustomNodeData;
  const { updateNodeData, addNodes, getNodes, deleteElements } = useReactFlow();

  const copyContainer = (e: React.MouseEvent) => {
    e.stopPropagation();
    const allNodes = getNodes();
    const self = allNodes.find((n) => n.id === id);
    if (!self) return;
    const newContainerId = `container-${Date.now()}-${Math.random().toString(36).slice(2, 4)}`;
    const children = allNodes.filter((n) => n.parentId === id);
    addNodes([
      {
        ...self,
        id: newContainerId,
        position: { x: self.position.x + 20, y: self.position.y + 20 },
        selected: false,
      },
      ...children.map((child, i) => ({
        ...child,
        id: `cchild-${Date.now()}-${i}-${Math.random().toString(36).slice(2, 4)}`,
        parentId: newContainerId,
      })),
    ]);
  };
  const [editing, setEditing] = useState(false);
  const [editName, setEditName] = useState(name);
  const c = color || DEFAULT_CUSTOM_COLOR;

  const commitEdit = () => {
    const n = editName.trim();
    if (!n) { setEditing(false); return; }
    updateNodeData(id, { name: n });
    setEditing(false);
  };

  const addChild = (e: React.MouseEvent) => {
    e.stopPropagation();
    const siblings = getNodes().filter((n) => n.parentId === id);
    const col = Math.floor(siblings.length / 3);
    const row = siblings.length % 3;
    addNodes([{
      id: `cchild-${Date.now()}-${Math.random().toString(36).slice(2, 4)}`,
      type: "containerChild",
      position: { x: 16 + col * 152, y: 52 + row * 52 },
      parentId: id,
      extent: "parent" as const,
      data: { name: "子节点", color: c },
    }]);
  };

  return (
    <div
      className="group/container relative rounded-2xl overflow-visible"
      style={{ width: "100%", height: "100%", background: `${c}0c`, border: `2px solid ${c}` }}
    >
      <NodeResizer
        color={c}
        isVisible={selected}
        minWidth={220}
        minHeight={160}
        handleStyle={{ width: 8, height: 8 }}
      />
      <Handle type="target" position={Position.Top}    style={{ opacity: 0 }} />
      <Handle type="source" position={Position.Bottom} style={{ opacity: 0 }} />

      {/* 标题栏 */}
      <div
        className="flex items-center gap-2 px-3 py-2 rounded-t-2xl"
        style={{ background: `${c}22`, borderBottom: `1px solid ${c}44` }}
      >
        <span className="text-base leading-none" style={{ color: c }}>▣</span>
        {editing ? (
          <input
            autoFocus
            value={editName}
            onChange={(e) => setEditName(e.target.value)}
            onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Enter") commitEdit(); if (e.key === "Escape") setEditing(false); }}
            onBlur={commitEdit}
            onPointerDown={(e) => e.stopPropagation()}
            className="flex-1 min-w-0 text-[12px] font-bold bg-transparent border-b outline-none"
            style={{ color: c, borderColor: c }}
          />
        ) : (
          <span
            className="flex-1 min-w-0 text-[12px] font-bold truncate cursor-text"
            style={{ color: c }}
            onDoubleClick={() => { setEditName(name); setEditing(true); }}
          >
            {name}
          </span>
        )}

        {/* 颜色色板（绝对定位，不占 flex 空间，hover 显示） */}
        {!editing && (
          <div className="absolute left-8 top-full mt-1 flex items-center gap-0.5 px-1.5 py-1 rounded-lg shadow-md opacity-0 group-hover/container:opacity-100 transition-opacity pointer-events-none group-hover/container:pointer-events-auto z-20"
               style={{ background: `${c}22`, border: `1px solid ${c}44` }}>
            {PRESET_COLORS.map((pc) => (
              <button
                key={pc}
                onPointerDown={(e) => e.stopPropagation()}
                onClick={() => updateNodeData(id, { color: pc })}
                className="w-3 h-3 rounded-full transition-transform hover:scale-125"
                style={{
                  backgroundColor: pc,
                  outline: c === pc ? `2px solid ${pc}` : "none",
                  outlineOffset: "1px",
                }}
              />
            ))}
          </div>
        )}

        {/* 操作按钮 */}
        <button
          onPointerDown={(e) => e.stopPropagation()}
          onClick={addChild}
          className="shrink-0 w-5 h-5 rounded-full text-white text-sm font-bold flex items-center justify-center transition-colors"
          style={{ background: c }}
          title="添加子区域"
        >+</button>
        <button
          onPointerDown={(e) => e.stopPropagation()}
          onClick={copyContainer}
          className="shrink-0 w-4 h-4 rounded-full bg-gray-200 hover:bg-blue-400 text-white text-[9px] flex items-center justify-center transition-colors"
          title="复制容器"
        >⧉</button>
        <button
          onPointerDown={(e) => e.stopPropagation()}
          onClick={() => deleteElements({ nodes: [{ id }] })}
          className="shrink-0 w-4 h-4 rounded-full bg-gray-200 hover:bg-red-400 text-white text-[9px] flex items-center justify-center transition-colors"
          title="删除"
        >×</button>
      </div>
    </div>
  );
}

// ── 容器子节点（在容器内部）──────────────────────────────────────

function ContainerChildNode({ id, data }: NodeProps) {
  const { name, color } = data as ContainerChildNodeData;
  const { updateNodeData, deleteElements, getNode, addNodes } = useReactFlow();

  const copyChild = (e: React.MouseEvent) => {
    e.stopPropagation();
    const node = getNode(id);
    if (!node) return;
    addNodes([{
      ...node,
      id: `cchild-${Date.now()}-${Math.random().toString(36).slice(2, 4)}`,
      position: { x: node.position.x + 16, y: node.position.y + 16 },
      selected: false,
    }]);
  };
  const [editing, setEditing] = useState(false);
  const [editName, setEditName] = useState(name);
  const c = color || DEFAULT_CUSTOM_COLOR;

  const commit = () => {
    const n = editName.trim();
    if (n) updateNodeData(id, { name: n });
    setEditing(false);
  };

  return (
    <div
      className="group/child relative flex items-center gap-2 px-3 py-2 rounded-xl select-none"
      style={{ border: `1.5px solid ${c}`, background: `${c}18`, minWidth: 120 }}
    >
      <Handle type="target" position={Position.Left}  style={{ opacity: 0 }} />
      <Handle type="source" position={Position.Right} style={{ opacity: 0 }} />
      <span className="text-[10px] shrink-0" style={{ color: c }}>◈</span>
      {editing ? (
        <input
          autoFocus
          value={editName}
          onChange={(e) => setEditName(e.target.value)}
          onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Enter" || e.key === "Escape") commit(); }}
          onBlur={commit}
          onPointerDown={(e) => e.stopPropagation()}
          className="flex-1 min-w-0 text-[11px] font-medium bg-transparent border-b outline-none"
          style={{ color: c, borderColor: c }}
        />
      ) : (
        <span
          className="flex-1 min-w-0 text-[11px] font-medium truncate cursor-text"
          style={{ color: c }}
          onDoubleClick={() => { setEditName(name); setEditing(true); }}
          title={name}
        >
          {name}
        </span>
      )}
      <button
        onPointerDown={(e) => e.stopPropagation()}
        onClick={copyChild}
        className="shrink-0 w-3.5 h-3.5 rounded-full bg-gray-200 hover:bg-blue-400 text-white text-[8px] flex items-center justify-center transition-colors opacity-0 group-hover/child:opacity-100"
        title="复制"
      >⧉</button>
      <button
        onPointerDown={(e) => e.stopPropagation()}
        onClick={() => deleteElements({ nodes: [{ id }] })}
        className="shrink-0 w-3.5 h-3.5 rounded-full bg-gray-200 hover:bg-red-400 text-white text-[8px] flex items-center justify-center transition-colors opacity-0 group-hover/child:opacity-100"
      >×</button>
    </div>
  );
}

// 必须定义在模块顶层
const nodeTypes = {
  project:        ProjectNode,
  git:            GitNode,
  env:            EnvNode,
  server:         ServerNode,
  focus:          FocusNode,
  custom:         CustomNode,
  container:      ContainerNode,
  containerChild: ContainerChildNode,
};

// ── 可编辑标签边（支持拖动路径 + 文本标签）─────────────────────

function LabeledEdge({
  id, sourceX, sourceY, targetX, targetY,
  style, markerEnd, data,
}: EdgeProps) {
  const { setEdges, screenToFlowPosition } = useReactFlow();
  const [editing, setEditing] = useState(false);
  const [draft, setDraft]     = useState((data?.label as string) ?? "");
  const label      = (data?.label    as string)                     ?? "";
  const cpOffset   = (data?.cpOffset as { x: number; y: number })   ?? { x: 0, y: 0 };
  const strokeColor = (style?.stroke as string) ?? "#9ca3af";

  // 控制点 = 中点 + 用户拖动偏移（flow 坐标）
  const cpX = (sourceX + targetX) / 2 + cpOffset.x;
  const cpY = (sourceY + targetY) / 2 + cpOffset.y;

  // 二次贝塞尔路径
  const edgePath = `M ${sourceX} ${sourceY} Q ${cpX} ${cpY} ${targetX} ${targetY}`;

  // t=0.5 时的曲线坐标（标签位置）
  const labelX = (sourceX + 2 * cpX + targetX) / 4;
  const labelY = (sourceY + 2 * cpY + targetY) / 4;

  // 拖动状态（ref 避免重渲染）
  const dragStartFlow = useRef<{ x: number; y: number } | null>(null);
  const dragStartCp   = useRef<{ x: number; y: number }>({ x: 0, y: 0 });

  const onHandlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
    e.stopPropagation();
    dragStartFlow.current = screenToFlowPosition({ x: e.clientX, y: e.clientY });
    dragStartCp.current   = { ...cpOffset };
    (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
  };

  const onHandlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
    if (!dragStartFlow.current) return;
    const cur = screenToFlowPosition({ x: e.clientX, y: e.clientY });
    const dx  = cur.x - dragStartFlow.current.x;
    const dy  = cur.y - dragStartFlow.current.y;
    setEdges((eds) =>
      eds.map((edge) =>
        edge.id === id
          ? { ...edge, data: { ...edge.data, cpOffset: { x: dragStartCp.current.x + dx, y: dragStartCp.current.y + dy } } }
          : edge,
      ),
    );
  };

  const onHandlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
    dragStartFlow.current = null;
    (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
  };

  const commitLabel = (value: string) => {
    setEdges((eds) =>
      eds.map((e) => e.id === id ? { ...e, data: { ...e.data, label: value.trim() } } : e),
    );
    setEditing(false);
  };

  return (
    <>
      <BaseEdge id={id} path={edgePath} style={style} markerEnd={markerEnd} />
      <EdgeLabelRenderer>
        {/* 路径拖动把手（悬停显示，双击恢复直线） */}
        <div
          style={{
            transform: `translate(-50%,-50%) translate(${cpX}px,${cpY}px)`,
            pointerEvents: "all",
            borderColor: strokeColor,
          }}
          className="absolute nodrag nopan w-3 h-3 rounded-full bg-white border-2 cursor-grab active:cursor-grabbing opacity-20 hover:opacity-100 transition-opacity z-10"
          onPointerDown={onHandlePointerDown}
          onPointerMove={onHandlePointerMove}
          onPointerUp={onHandlePointerUp}
          onDoubleClick={(e) => {
            e.stopPropagation();
            setEdges((eds) =>
              eds.map((edge) =>
                edge.id === id ? { ...edge, data: { ...edge.data, cpOffset: { x: 0, y: 0 } } } : edge,
              ),
            );
          }}
          title="拖动调整路径 · 双击恢复直线"
        />

        {/* 文本标签 */}
        <div
          style={{
            transform: `translate(-50%,-50%) translate(${labelX}px,${labelY}px)`,
            pointerEvents: "all",
          }}
          className="absolute nodrag nopan"
        >
          {editing ? (
            <input
              autoFocus
              value={draft}
              onChange={(e) => setDraft(e.target.value)}
              onBlur={() => commitLabel(draft)}
              onKeyDown={(e) => {
                e.stopPropagation();
                if (e.key === "Enter")  commitLabel(draft);
                if (e.key === "Escape") { setDraft(label); setEditing(false); }
              }}
              onPointerDown={(e) => e.stopPropagation()}
              className="text-[10px] px-1.5 py-0.5 rounded border border-gray-300 bg-white outline-none focus:ring-1 focus:ring-blue-400 shadow-sm min-w-[60px] max-w-[160px]"
              style={{ color: strokeColor }}
            />
          ) : label ? (
            <span
              onDoubleClick={() => { setDraft(label); setEditing(true); }}
              className="text-[10px] px-1.5 py-0.5 rounded bg-white border border-gray-200 shadow-sm cursor-text select-none whitespace-nowrap"
              style={{ color: strokeColor }}
              title="双击编辑"
            >
              {label}
            </span>
          ) : (
            <span
              onDoubleClick={() => { setDraft(""); setEditing(true); }}
              className="text-[10px] px-1 py-0.5 rounded text-gray-300 hover:text-gray-400 hover:bg-white hover:border hover:border-gray-200 cursor-text select-none transition-colors"
              title="双击添加标签"
            >
              ✎
            </span>
          )}
        </div>
      </EdgeLabelRenderer>
    </>
  );
}

const edgeTypes = {
  labeled: LabeledEdge,
};

// ── 持久化类型 ────────────────────────────────────────────────────

interface PersistedCanvas {
  nodes: {
    id: string; type?: string; position: XYPosition; data: Record<string, unknown>;
    style?: React.CSSProperties;
    parentId?: string;
    extent?: "parent";
  }[];
  edges: { id: string; source: string; target: string; style?: React.CSSProperties; type?: string; data?: Record<string, unknown>; markerEnd?: unknown }[];
  customItems: CustomItem[];
}

// ── 生成资料列表 ──────────────────────────────────────────────────

function buildSidebarItems(members: GroupMember[]): Record<string, SidebarItem[]> {
  const projects: SidebarItem[] = [];
  const gitItems: SidebarItem[] = [];
  const envItems: SidebarItem[] = [];
  const serverItems: SidebarItem[] = [];
  const focusItems: SidebarItem[] = [];

  members.forEach(({ project: p, role }) => {
    projects.push({ id: `s-proj-${p.id}`,   nodeType: "project", label: p.name,     sublabel: role ?? p.type ?? undefined, data: { project: p, role },                                     colorClass: "border-blue-200" });
    if (p.repo_url)     gitItems.push({ id: `s-git-${p.id}-o`,  nodeType: "git",    label: "origin",   sublabel: p.name, data: { label: "origin",   url: p.repo_url,     projectName: p.name }, colorClass: "border-slate-200" });
    if (p.upstream_url) gitItems.push({ id: `s-git-${p.id}-u`,  nodeType: "git",    label: "upstream", sublabel: p.name, data: { label: "upstream", url: p.upstream_url, projectName: p.name }, colorClass: "border-slate-200" });
    if (p.prod_url)     envItems.push({ id: `s-env-${p.id}-p`,  nodeType: "env",    label: "生产",     sublabel: p.name, data: { variant: "prod",    url: p.prod_url,     projectName: p.name }, colorClass: "border-emerald-200" });
    if (p.staging_url)  envItems.push({ id: `s-env-${p.id}-s`,  nodeType: "env",    label: "测试",     sublabel: p.name, data: { variant: "staging", url: p.staging_url,  projectName: p.name }, colorClass: "border-amber-200" });
    if (p.server_note)  serverItems.push({ id: `s-srv-${p.id}`, nodeType: "server", label: "Server",   sublabel: p.name, data: { note: p.server_note, projectName: p.name },                colorClass: "border-slate-200" });
    if (p.recent_focus) focusItems.push({ id: `s-foc-${p.id}`,  nodeType: "focus",  label: "当前焦点", sublabel: p.name, data: { text: p.recent_focus, projectName: p.name },               colorClass: "border-indigo-200" });
  });

  const result: Record<string, SidebarItem[]> = {};
  if (projects.length)    result["🗂 项目"]    = projects;
  if (gitItems.length)    result["🔀 Git 仓库"] = gitItems;
  if (envItems.length)    result["🌐 环境"]    = envItems;
  if (serverItems.length) result["🖥 服务器"]  = serverItems;
  if (focusItems.length)  result["📌 焦点"]    = focusItems;
  return result;
}

// ── 拖拽核心逻辑（pointer events，供两种卡片复用）────────────────

interface DragHandlers {
  onPointerDown: (e: React.PointerEvent<HTMLDivElement>) => void;
  onPointerMove: (e: React.PointerEvent<HTMLDivElement>) => void;
  onPointerUp:   (e: React.PointerEvent<HTMLDivElement>) => void;
  onPointerCancel: (e: React.PointerEvent<HTMLDivElement>) => void;
}

function useDragToCanvas(
  ghostLabel: string,
  canvasRef: React.RefObject<HTMLDivElement | null>,
  screenToFlowPosition: (p: XYPosition) => XYPosition,
  onDrop: (nodeType: string, data: Record<string, unknown>, position: XYPosition) => void,
  nodeType: string,
  data: Record<string, unknown>,
): DragHandlers {
  const ghostRef  = useRef<HTMLDivElement | null>(null);
  const activeRef = useRef(false);

  const cleanup = (el: HTMLDivElement | null) => {
    if (el && document.body.contains(el)) document.body.removeChild(el);
  };

  const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
    e.currentTarget.setPointerCapture(e.pointerId);
    activeRef.current = true;
    const ghost = document.createElement("div");
    ghost.style.cssText = [
      "position:fixed","pointer-events:none","z-index:9999",
      "background:white","border:1.5px solid #a78bfa","border-radius:8px",
      "padding:4px 10px","font-size:11px","color:#1f2937",
      "box-shadow:0 4px 16px rgba(0,0,0,0.15)","white-space:nowrap",
      `left:${e.clientX + 12}px`,`top:${e.clientY + 12}px`,
    ].join(";");
    ghost.textContent = ghostLabel;
    document.body.appendChild(ghost);
    ghostRef.current = ghost;
  };

  const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
    if (!activeRef.current || !ghostRef.current) return;
    ghostRef.current.style.left = `${e.clientX + 12}px`;
    ghostRef.current.style.top  = `${e.clientY + 12}px`;
  };

  const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
    if (!activeRef.current) return;
    e.currentTarget.releasePointerCapture(e.pointerId);
    activeRef.current = false;
    cleanup(ghostRef.current);
    ghostRef.current = null;
    const canvas = canvasRef.current;
    if (!canvas) return;
    const rect = canvas.getBoundingClientRect();
    if (e.clientX >= rect.left && e.clientX <= rect.right &&
        e.clientY >= rect.top  && e.clientY <= rect.bottom) {
      onDrop(nodeType, data, screenToFlowPosition({ x: e.clientX, y: e.clientY }));
    }
  };

  const onPointerCancel = (e: React.PointerEvent<HTMLDivElement>) => {
    activeRef.current = false;
    cleanup(ghostRef.current);
    ghostRef.current = null;
    e.currentTarget.releasePointerCapture(e.pointerId);
  };

  return { onPointerDown, onPointerMove, onPointerUp, onPointerCancel };
}

// ── 资料列表卡片 ──────────────────────────────────────────────────

interface DataItemCardProps {
  item: SidebarItem;
  canvasRef: React.RefObject<HTMLDivElement | null>;
  onDrop: (nodeType: string, data: Record<string, unknown>, position: XYPosition) => void;
  screenToFlowPosition: (p: XYPosition) => XYPosition;
}

function DataItemCard({ item, canvasRef, onDrop, screenToFlowPosition }: DataItemCardProps) {
  const handlers = useDragToCanvas(
    item.sublabel ? `${item.label}  ${item.sublabel}` : item.label,
    canvasRef, screenToFlowPosition, onDrop, item.nodeType, item.data,
  );
  return (
    <div
      {...handlers}
      style={{ cursor: "grab", touchAction: "none", userSelect: "none" }}
      className={`flex flex-col px-2.5 py-1.5 rounded-lg border bg-white hover:shadow-sm hover:border-blue-300 transition-shadow ${item.colorClass}`}
    >
      <span className="text-[11px] font-semibold text-gray-700 truncate">{item.label}</span>
      {item.sublabel && <span className="text-[10px] text-gray-400 truncate mt-px">{item.sublabel}</span>}
    </div>
  );
}

// ── 自定义节点列表卡片（支持编辑名称和颜色）────────────────────────

interface CustomItemCardProps {
  item: CustomItem;
  canvasRef: React.RefObject<HTMLDivElement | null>;
  onDrop: (nodeType: string, data: Record<string, unknown>, position: XYPosition) => void;
  screenToFlowPosition: (p: XYPosition) => XYPosition;
  onRemove: (id: string) => void;
  onUpdate: (id: string, name: string, color: string) => void;
}

function CustomItemCard({ item, canvasRef, onDrop, screenToFlowPosition, onRemove, onUpdate }: CustomItemCardProps) {
  const [editing, setEditing]     = useState(false);
  const [editName, setEditName]   = useState(item.name);
  const [editColor, setEditColor] = useState(item.color);

  const c = item.color || DEFAULT_CUSTOM_COLOR;

  const isContainer = item.variant === "container";
  const nodeType = isContainer ? "container" : "custom";
  const handlers = useDragToCanvas(
    `${isContainer ? "▣" : "✦"} ${item.name}`,
    canvasRef, screenToFlowPosition, onDrop,
    nodeType, { name: item.name, color: item.color, customItemId: item.id },
  );

  const commit = () => {
    const name = editName.trim();
    if (!name) return;
    onUpdate(item.id, name, editColor);
    setEditing(false);
  };

  const cancel = () => {
    setEditName(item.name);
    setEditColor(item.color);
    setEditing(false);
  };

  if (editing) {
    return (
      <div className="flex flex-col gap-1.5 px-2.5 py-2 rounded-lg border-2 border-dashed bg-white" style={{ borderColor: editColor }}>
        {/* 名称输入 */}
        <input
          autoFocus
          value={editName}
          onChange={(e) => setEditName(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Enter") commit(); if (e.key === "Escape") cancel(); }}
          className="text-[11px] border border-gray-200 rounded px-1.5 py-0.5 outline-none focus:ring-1 focus:ring-violet-300 bg-white w-full"
        />
        {/* 颜色色板 */}
        <div className="flex items-center gap-1 flex-wrap">
          {PRESET_COLORS.map((pc) => (
            <button
              key={pc}
              onClick={() => setEditColor(pc)}
              className="w-4 h-4 rounded-full transition-all"
              style={{
                backgroundColor: pc,
                outline: editColor === pc ? `2px solid ${pc}` : "2px solid transparent",
                outlineOffset: "2px",
                transform: editColor === pc ? "scale(1.2)" : "scale(1)",
              }}
            />
          ))}
        </div>
        {/* 操作按钮 */}
        <div className="flex gap-1">
          <button
            onClick={commit}
            className="flex-1 text-[10px] text-white rounded py-0.5 transition-colors"
            style={{ background: editColor }}
          >
            保存
          </button>
          <button
            onClick={cancel}
            className="flex-1 text-[10px] border border-gray-200 text-gray-500 rounded py-0.5 hover:bg-gray-50"
          >
            取消
          </button>
        </div>
      </div>
    );
  }

  return (
    <div className="flex items-center gap-1 group/custom">
      {/* 可拖拽主体 */}
      <div
        {...handlers}
        style={{ cursor: "grab", touchAction: "none", userSelect: "none", borderColor: c }}
        className="flex-1 min-w-0 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border-2 border-dashed hover:shadow-sm transition-shadow"
      >
        <span className="text-[10px] shrink-0" style={{ color: c }}>{isContainer ? "▣" : "✦"}</span>
        <span className="text-[11px] font-medium truncate" style={{ color: c }}>{item.name}</span>
      </div>
      {/* 编辑按钮 */}
      <button
        onClick={() => { setEditName(item.name); setEditColor(item.color); setEditing(true); }}
        className="shrink-0 w-4 h-4 flex items-center justify-center text-gray-300 hover:text-violet-500 opacity-0 group-hover/custom:opacity-100 transition-opacity text-xs"
        title="编辑"
      >
        ✎
      </button>
      {/* 删除按钮 */}
      <button
        onClick={() => onRemove(item.id)}
        className="shrink-0 w-4 h-4 flex items-center justify-center text-gray-300 hover:text-red-400 opacity-0 group-hover/custom:opacity-100 transition-opacity text-xs"
        title="移除"
      >
        ×
      </button>
    </div>
  );
}

// ── Sidebar ───────────────────────────────────────────────────────

interface SidebarProps {
  groups: Record<string, SidebarItem[]>;
  customItems: CustomItem[];
  canvasRef: React.RefObject<HTMLDivElement | null>;
  onDrop: (nodeType: string, data: Record<string, unknown>, position: XYPosition) => void;
  screenToFlowPosition: (p: XYPosition) => XYPosition;
  onAddCustom: (name: string, variant?: "node" | "container") => void;
  onRemoveCustom: (id: string) => void;
  onUpdateCustom: (id: string, name: string, color: string) => void;
}

function ResourceSidebar({ groups, customItems, canvasRef, onDrop, screenToFlowPosition, onAddCustom, onRemoveCustom, onUpdateCustom }: SidebarProps) {
  const [input, setInput] = useState("");

  const commit = (variant?: "node" | "container") => {
    const name = input.trim();
    if (!name) return;
    onAddCustom(name, variant);
    setInput("");
  };

  const onKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Enter") commit("node");
  };

  return (
    <div className="w-44 shrink-0 border-r border-gray-100 overflow-y-auto flex flex-col bg-gray-50/60">

      {/* ── 资料列表 ── */}
      <div className="flex flex-col gap-3 p-3">
        <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-widest">资料列表</p>
        {Object.entries(groups).map(([category, items]) => (
          <div key={category} className="flex flex-col gap-1.5">
            <p className="text-[10px] font-semibold text-gray-500">{category}</p>
            {items.map((item) => (
              <DataItemCard
                key={item.id}
                item={item}
                canvasRef={canvasRef}
                onDrop={onDrop}
                screenToFlowPosition={screenToFlowPosition}
              />
            ))}
          </div>
        ))}
        {Object.keys(groups).length === 0 && (
          <p className="text-[10px] text-gray-400 text-center mt-2">暂无项目资料</p>
        )}
      </div>

      {/* ── 分割线 ── */}
      <div className="flex items-center gap-2 px-3 py-2">
        <div className="flex-1 h-px bg-gray-200" />
        <span className="text-[9px] text-gray-300 shrink-0">自定义</span>
        <div className="flex-1 h-px bg-gray-200" />
      </div>

      {/* ── 自定义节点 ── */}
      <div className="flex flex-col gap-2 px-3 pb-4">
        <p className="text-[10px] font-semibold text-violet-400 uppercase tracking-widest">自定义节点</p>

        {/* 添加输入框 */}
        <div className="flex flex-col gap-1">
          <div className="flex items-center gap-1">
            <input
              value={input}
              onChange={(e) => setInput(e.target.value)}
              onKeyDown={onKey}
              placeholder="名称…"
              className="flex-1 min-w-0 border border-violet-200 rounded-md px-2 py-1 text-[11px] outline-none focus:ring-1 focus:ring-violet-300 bg-white placeholder-gray-300"
            />
            <button
              onClick={() => commit("node")}
              disabled={!input.trim()}
              title="添加普通节点"
              className="shrink-0 w-6 h-6 rounded-md bg-violet-500 hover:bg-violet-600 disabled:bg-gray-200 disabled:cursor-not-allowed text-white text-sm flex items-center justify-center transition-colors"
            >✦</button>
          </div>
          <button
            onClick={() => commit("container")}
            disabled={!input.trim()}
            className="flex items-center justify-center gap-1 text-[10px] border border-dashed border-violet-300 rounded-md py-1 text-violet-500 hover:bg-violet-50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
          >
            <span>▣</span> 加为容器节点
          </button>
        </div>

        {/* 自定义节点列表 */}
        {customItems.map((item) => (
          <CustomItemCard
            key={item.id}
            item={item}
            canvasRef={canvasRef}
            onDrop={onDrop}
            screenToFlowPosition={screenToFlowPosition}
            onRemove={onRemoveCustom}
            onUpdate={onUpdateCustom}
          />
        ))}

        {customItems.length === 0 && (
          <p className="text-[10px] text-gray-300 text-center mt-1">输入名称后回车添加</p>
        )}
      </div>
    </div>
  );
}

// ── 树形布局算法（仅针对自定义节点） ─────────────────────────────────

function computeTreeLayout(nodes: Node[], edges: Edge[]): Map<string, XYPosition> {
  const customIds = new Set(nodes.filter((n) => n.type === "custom").map((n) => n.id));
  if (customIds.size === 0) return new Map();

  const childrenOf = new Map<string, string[]>();
  const parentsOf  = new Map<string, string[]>();

  for (const e of edges) {
    if (customIds.has(e.source) && customIds.has(e.target)) {
      childrenOf.set(e.source, [...(childrenOf.get(e.source) ?? []), e.target]);
      parentsOf.set(e.target,  [...(parentsOf.get(e.target)  ?? []), e.source]);
    }
  }

  // 根节点 = 没有自定义父节点的自定义节点
  const roots = [...customIds].filter((id) => !parentsOf.has(id));
  if (roots.length === 0) return new Map(); // 存在环，跳过

  // BFS 分配层级
  const level = new Map<string, number>();
  const queue: string[] = [...roots];
  roots.forEach((r) => level.set(r, 0));
  while (queue.length > 0) {
    const cur = queue.shift()!;
    for (const child of childrenOf.get(cur) ?? []) {
      if (!level.has(child)) {
        level.set(child, level.get(cur)! + 1);
        queue.push(child);
      }
    }
  }

  // 按层分组，居中排列
  const byLevel = new Map<number, string[]>();
  for (const [id, lv] of level) {
    byLevel.set(lv, [...(byLevel.get(lv) ?? []), id]);
  }

  const H_GAP = 200, V_GAP = 130;
  const positions = new Map<string, XYPosition>();
  for (const [lv, ids] of byLevel) {
    const totalW = (ids.length - 1) * H_GAP;
    ids.forEach((id, i) => positions.set(id, { x: i * H_GAP - totalW / 2, y: lv * V_GAP }));
  }
  return positions;
}

// ── CanvasInner ───────────────────────────────────────────────────

function CanvasInner({ groups, groupId }: { groups: Record<string, SidebarItem[]>; groupId: string }) {
  const [initialized, setInitialized] = useState(false);
  const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
  const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
  const [customItems, setCustomItems] = useState<CustomItem[]>([]);
  const { screenToFlowPosition, getNode, deleteElements } = useReactFlow();
  const canvasRef = useRef<HTMLDivElement>(null);
  const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const [edgeMenu, setEdgeMenu] = useState<{ edgeId: string; x: number; y: number } | null>(null);

  // 点击其他地方关闭右键菜单
  useEffect(() => {
    const close = () => setEdgeMenu(null);
    window.addEventListener("click", close);
    return () => window.removeEventListener("click", close);
  }, []);

  // 初始加载（从数据库）
  useEffect(() => {
    getCanvasState(groupId).then((raw) => {
      if (raw) {
        try {
          const parsed = JSON.parse(raw) as PersistedCanvas;
          setNodes((parsed.nodes ?? []) as Node[]);
          setEdges((parsed.edges ?? []) as Edge[]);
          setCustomItems(parsed.customItems ?? []);
        } catch { /* 数据损坏时忽略 */ }
      }
      setInitialized(true);
    }).catch(() => setInitialized(true));
  }, [groupId]);

  // 防抖保存（加载完成后才监听变化）
  useEffect(() => {
    if (!initialized) return;
    if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
    saveTimerRef.current = setTimeout(() => {
      const state: PersistedCanvas = {
        nodes: nodes.map(({ id, type, position, data, style, parentId, extent }) => ({
          id, type, position, data: data as Record<string, unknown>,
          ...(style    ? { style }    : {}),
          ...(parentId ? { parentId } : {}),
          ...(extent === "parent" ? { extent: "parent" as const } : {}),
        })),
        edges: edges.map(({ id, source, target, style, type, data, markerEnd }) => ({ id, source, target, style, type, data: data as Record<string, unknown> | undefined, markerEnd })),
        customItems,
      };
      saveCanvasState(groupId, JSON.stringify(state)).catch(() => {});
    }, 800);
    return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current); };
  }, [nodes, edges, customItems, groupId, initialized]);

  // 自定义节点之间的连线用彩色箭头，其余用灰色
  const onConnect: OnConnect = useCallback(
    (conn) => {
      if (conn.source === conn.target) return;   // 禁止自环
      const src = getNode(conn.source);
      const tgt = getNode(conn.target);
      const bothCustom = src?.type === "custom" && tgt?.type === "custom";
      const edgeColor  = bothCustom ? ((src?.data as CustomNodeData).color || DEFAULT_CUSTOM_COLOR) : "#d1d5db";
      setEdges((eds) => addEdge({
        ...conn,
        type: "labeled",
        data: { label: "", _smooth: bothCustom },
        style: { stroke: edgeColor, strokeWidth: 1.5 },
        markerEnd: bothCustom
          ? { type: MarkerType.ArrowClosed, color: edgeColor, width: 12, height: 12 }
          : undefined,
      }, eds));
    },
    [setEdges, getNode],
  );

  // 一键整理：将所有自定义节点按父子层级重新排布
  const applyTreeLayout = useCallback(() => {
    const positions = computeTreeLayout(nodes, edges);
    if (positions.size === 0) return;
    setNodes((nds) => nds.map((n) => {
      const pos = positions.get(n.id);
      return pos ? { ...n, position: pos } : n;
    }));
  }, [nodes, edges, setNodes]);

  const handleDrop = useCallback(
    (nodeType: string, data: Record<string, unknown>, position: XYPosition) => {
      const isContainer = nodeType === "container";
      setNodes((nds) => nds.concat({
        id: `${nodeType}-${Date.now()}-${Math.random().toString(36).slice(2, 5)}`,
        type: nodeType,
        position,
        data,
        // 容器节点需要初始尺寸，子节点才能正确定位
        ...(isContainer ? { style: { width: 300, height: 220 } } : {}),
      }));
    },
    [setNodes],
  );

  const addCustomItem = useCallback((name: string, variant?: "node" | "container") => {
    setCustomItems((prev) => [...prev, { id: `ci-${Date.now()}`, name, color: DEFAULT_CUSTOM_COLOR, variant: variant ?? "node" }]);
  }, []);

  const removeCustomItem = useCallback((id: string) => {
    setCustomItems((prev) => prev.filter((c) => c.id !== id));
  }, []);

  // 更新名称/颜色，同步更新画布上已放置的同源节点
  const updateCustomItem = useCallback((id: string, name: string, color: string) => {
    setCustomItems((prev) => prev.map((c) => c.id === id ? { ...c, name, color } : c));
    setNodes((nds) => nds.map((n) =>
      n.type === "custom" && (n.data as CustomNodeData).customItemId === id
        ? { ...n, data: { ...n.data, name, color } }
        : n
    ));
  }, [setCustomItems, setNodes]);

  return (
    <div className="flex flex-1 min-w-0 h-full">
      <ResourceSidebar
        groups={groups}
        customItems={customItems}
        canvasRef={canvasRef}
        onDrop={handleDrop}
        screenToFlowPosition={screenToFlowPosition}
        onAddCustom={addCustomItem}
        onRemoveCustom={removeCustomItem}
        onUpdateCustom={updateCustomItem}
      />
      <div ref={canvasRef} className="flex-1 relative">
        <ReactFlow
          nodes={nodes}
          edges={edges}
          nodeTypes={nodeTypes}
          edgeTypes={edgeTypes}
          onNodesChange={onNodesChange}
          onEdgesChange={onEdgesChange}
          onConnect={onConnect}
          onEdgeContextMenu={(e, edge) => {
            e.preventDefault();
            setEdgeMenu({ edgeId: edge.id, x: e.clientX, y: e.clientY });
          }}
          nodesDraggable
          panOnDrag
          zoomOnScroll
          minZoom={0.3}
          maxZoom={2}
          proOptions={{ hideAttribution: true }}
          deleteKeyCode="Delete"
        >
          <Background color="#e5e7eb" gap={18} size={1} />
          <Controls showInteractive={false} position="bottom-right" />
          {nodes.length === 0 && (
            <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
              <div className="flex flex-col items-center gap-2 text-gray-300">
                <p className="text-3xl">🗂</p>
                <p className="text-xs">从左侧拖入资源，自由排列</p>
              </div>
            </div>
          )}
        </ReactFlow>

        {/* 边右键菜单 */}
        {edgeMenu && (
          <div
            style={{ position: "fixed", left: edgeMenu.x, top: edgeMenu.y, zIndex: 9999 }}
            className="bg-white border border-gray-200 rounded-lg shadow-lg py-1 min-w-[110px]"
            onClick={(e) => e.stopPropagation()}
          >
            <button
              className="w-full text-left px-3 py-1.5 text-[12px] text-red-500 hover:bg-red-50 transition-colors rounded-lg"
              onClick={() => {
                deleteElements({ edges: [{ id: edgeMenu.edgeId }] });
                setEdgeMenu(null);
              }}
            >
              删除连线
            </button>
          </div>
        )}

        {nodes.length > 0 && (
          <div className="absolute top-3 right-3 flex gap-1.5 z-10">
            {nodes.some((n) => n.type === "custom") && (
              <button
                onClick={applyTreeLayout}
                className="text-[10px] text-violet-500 hover:text-violet-700 border border-violet-200 hover:border-violet-400 px-2 py-1 rounded-md bg-white transition-colors"
                title="将自定义节点按父子层级自动排列"
              >
                整理树形
              </button>
            )}
            <button
              onClick={() => { setNodes([]); setEdges([]); saveCanvasState(groupId, JSON.stringify({ nodes: [], edges: [], customItems })).catch(() => {}); }}
              className="text-[10px] text-gray-400 hover:text-red-500 border border-gray-200 hover:border-red-300 px-2 py-1 rounded-md bg-white transition-colors"
            >
              清空画布
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

// ── 导出 ─────────────────────────────────────────────────────────

interface Props {
  members: GroupMember[];
  groupId: string;
}

export function GroupResourceCanvas({ members, groupId }: Props) {
  const groups = useMemo(() => buildSidebarItems(members), [members]);
  return (
    <div style={{ height: 480 }} className="w-full flex">
      <ReactFlowProvider>
        <CanvasInner groups={groups} groupId={groupId} />
      </ReactFlowProvider>
    </div>
  );
}
