import { useState, useCallback, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import {
  ReactFlow,
  Background,
  Controls,
  type Node,
  type Edge,
  type NodeTypes,
  Handle,
  Position,
  MarkerType,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import {
  getBlueprint,
  getBlueprintStatus,
  syncBlueprintRules,
  generateBlueprintPrompt,
  getProjects,
  getFlywheelStats,
  generateMuhePrompt,
  archiveProjectDocs,
  type BlueprintData,
  type BlueprintModule,
  type BlueprintTask,
  type BlueprintArea,
  type BlueprintStatus,
  type SyncResult,
  type Project,
  type FlywheelStats,
  type StalledProject,
} from "../../lib/commands";

interface Props {
  projectName: string;
  projectPath: string;
  onClose: () => void;
}

// ── 状态色标 ─────────────────────────────────────────────────────────────────

const STATUS_CONFIG: Record<string, { color: string; bg: string; label: string }> = {
  done:        { color: "#22C55E", bg: "#F0FDF4", label: "已完成" },
  in_progress: { color: "#3B82F6", bg: "#EFF6FF", label: "进行中" },
  planned:     { color: "#EAB308", bg: "#FEFCE8", label: "已规划" },
  concept:     { color: "#9CA3AF", bg: "#F9FAFB", label: "构思中" },
  blocked:     { color: "#EF4444", bg: "#FEF2F2", label: "受阻" },
};

const TASK_PREFIX: Record<string, string> = {
  done: "✅",
  in_progress: "🔵",
  todo: "📋",
  concept: "💭",
  blocked: "🔴",
  locked: "🔒",
};

// ── 领域分组节点 ─────────────────────────────────────────────────────────────

function AreaNode({ data }: { data: { label: string; color: string } }) {
  return (
    <div
      className="rounded-xl border-2 border-dashed px-4 pt-2 pb-3 w-full h-full pointer-events-none"
      style={{
        borderColor: data.color + "60",
        background: data.color + "08",
      }}
    >
      <div className="flex items-center gap-2 mb-1">
        <div className="w-3 h-1 rounded-full" style={{ background: data.color }} />
        <span className="text-[11px] font-bold uppercase tracking-wider" style={{ color: data.color }}>
          {data.label}
        </span>
      </div>
    </div>
  );
}

// ── 模块节点 ─────────────────────────────────────────────────────────────────

function ModuleNode({ data }: { data: BlueprintModule & { areaColor?: string; dimmed?: boolean; pinned?: boolean; onClick: () => void; onHover?: (id: string | null) => void } }) {
  const cfg = STATUS_CONFIG[data.status] ?? STATUS_CONFIG.concept;
  const tasksDone = data.tasks.filter((t) => t.status === "done").length;
  const tasksTotal = data.tasks.length;

  return (
    <div
      onClick={data.onClick}
      onMouseEnter={() => data.onHover?.(data.id)}
      onMouseLeave={() => data.onHover?.(null)}
      className="cursor-pointer rounded-lg border-2 shadow-sm px-3 py-2.5 min-w-[160px] max-w-[200px] transition-all hover:shadow-md hover:scale-[1.02]"
      style={{
        borderColor: data.dimmed ? "#D1D5DB" : cfg.color,
        background: data.dimmed ? "#F9FAFB" : cfg.bg,
        borderLeftWidth: 4,
        borderLeftColor: data.dimmed ? "#E5E7EB" : (data.areaColor ?? cfg.color),
        opacity: data.dimmed ? 0.35 : 1,
        boxShadow: data.pinned ? undefined : undefined,
        animation: data.pinned ? "ssr-glow 2s linear infinite" : undefined,
        transition: "opacity 0.2s, border-color 0.2s, background 0.2s, box-shadow 0.2s",
      }}
    >
      <Handle id="left-t" type="target" position={Position.Left} className="!w-2 !h-2 !-left-1" style={{ background: cfg.color }} />
      <Handle id="left-s" type="source" position={Position.Left} className="!w-2 !h-2 !-left-1" style={{ background: cfg.color }} />
      <Handle id="right-t" type="target" position={Position.Right} className="!w-2 !h-2 !-right-1" style={{ background: cfg.color }} />
      <Handle id="right-s" type="source" position={Position.Right} className="!w-2 !h-2 !-right-1" style={{ background: cfg.color }} />
      <Handle id="top-t" type="target" position={Position.Top} className="!w-2 !h-2 !-top-1" style={{ background: cfg.color }} />
      <Handle id="top-s" type="source" position={Position.Top} className="!w-2 !h-2 !-top-1" style={{ background: cfg.color }} />
      <Handle id="bottom-t" type="target" position={Position.Bottom} className="!w-2 !h-2 !-bottom-1" style={{ background: cfg.color }} />
      <Handle id="bottom-s" type="source" position={Position.Bottom} className="!w-2 !h-2 !-bottom-1" style={{ background: cfg.color }} />

      <div className="flex items-center gap-1.5 mb-1">
        <span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ background: cfg.color }} />
        <span className="text-xs font-semibold text-gray-800 truncate">{data.name}</span>
      </div>

      {/* 进度条 */}
      <div className="w-full h-1.5 bg-gray-200 rounded-full overflow-hidden mb-1">
        <div
          className="h-full rounded-full transition-all"
          style={{ width: `${data.progress}%`, background: cfg.color }}
        />
      </div>

      <div className="flex items-center justify-between">
        <span className="text-[10px] text-gray-500">{cfg.label}</span>
        {tasksTotal > 0 && (
          <span className="text-[10px] text-gray-400">{tasksDone}/{tasksTotal}</span>
        )}
      </div>
    </div>
  );
}

const nodeTypes: NodeTypes = {
  module: ModuleNode,
  area: AreaNode,
};

// ── 布局计算 ─────────────────────────────────────────────────────────────────

const NODE_W = 180;
const NODE_H = 75;
const GAP_X = 80;
const GAP_Y = 60;
const AREA_PAD_X = 30;
const AREA_PAD_Y = 50;
const AREA_GAP = 80;
const COLS_PER_AREA = 4;

function autoLayout(
  areas: BlueprintArea[],
  modules: BlueprintModule[],
) {
  const areaNodes: Node[] = [];
  const moduleNodes: Node[] = [];
  // 绝对坐标映射，用于计算边的最佳 handle
  const absPositions: Record<string, { x: number; y: number }> = {};
  let areaOffsetY = 0;

  for (const area of areas) {
    const areaModules = modules.filter((m) => m.area === area.id);
    if (areaModules.length === 0) continue;

    const cols = Math.min(areaModules.length, COLS_PER_AREA);
    const rows = Math.ceil(areaModules.length / cols);

    const areaW = cols * (NODE_W + GAP_X) - GAP_X + AREA_PAD_X * 2;
    const areaH = rows * (NODE_H + GAP_Y) - GAP_Y + AREA_PAD_Y + AREA_PAD_X;

    areaNodes.push({
      id: `area-${area.id}`,
      type: "area",
      position: { x: 0, y: areaOffsetY },
      data: { label: area.name, color: area.color ?? "#6B7280" },
      style: { width: areaW, height: areaH },
      selectable: false,
      draggable: false,
    });

    areaModules.forEach((m, i) => {
      const col = i % cols;
      const row = Math.floor(i / cols);
      const localX = AREA_PAD_X + col * (NODE_W + GAP_X);
      const localY = AREA_PAD_Y + row * (NODE_H + GAP_Y);
      moduleNodes.push({
        id: m.id,
        type: "module",
        position: { x: localX, y: localY },
        parentId: `area-${area.id}`,
        extent: "parent" as const,
        data: { ...m, areaColor: area.color },
      });
      absPositions[m.id] = { x: localX, y: areaOffsetY + localY };
    });

    areaOffsetY += areaH + AREA_GAP;
  }

  return { areaNodes, moduleNodes, absPositions };
}

/** 根据源/目标的相对位置，选出最短路径的 handle 对 */
function pickHandles(
  srcPos: { x: number; y: number },
  tgtPos: { x: number; y: number },
): { sourceHandle: string; targetHandle: string } {
  const dx = tgtPos.x - srcPos.x;
  const dy = tgtPos.y - srcPos.y;

  // 以节点中心为基准，判断主要方向
  if (Math.abs(dx) >= Math.abs(dy)) {
    // 水平为主
    if (dx > 0) {
      // target 在 source 右边：source 右出 → target 左进
      return { sourceHandle: "right-s", targetHandle: "left-t" };
    } else {
      // target 在 source 左边：source 左出 → target 右进
      return { sourceHandle: "left-s", targetHandle: "right-t" };
    }
  } else {
    // 垂直为主
    if (dy > 0) {
      // target 在 source 下方：source 下出 → target 上进
      return { sourceHandle: "bottom-s", targetHandle: "top-t" };
    } else {
      // target 在 source 上方：source 上出 → target 下进
      return { sourceHandle: "top-s", targetHandle: "bottom-t" };
    }
  }
}

// ── 主组件 ───────────────────────────────────────────────────────────────────

type SidePanel = { type: "module"; module: BlueprintModule } | { type: "next" } | { type: "remaining" } | { type: "governance" } | { type: "flywheel" } | null;

export function BlueprintModal({ projectName, projectPath, onClose }: Props) {
  const [sidePanel, setSidePanel] = useState<SidePanel>(null);
  const [hoveredModuleId, setHoveredModuleId] = useState<string | null>(null);
  const [pinnedModuleId, setPinnedModuleId] = useState<string | null>(null);

  // 实际高亮的模块：pin 优先，否则用 hover
  const activeModuleId = pinnedModuleId ?? hoveredModuleId;

  const { data: blueprint, isLoading, error } = useQuery<BlueprintData | null>({
    queryKey: ["blueprint", projectPath],
    queryFn: () => getBlueprint(projectPath),
  });

  const { data: bpStatus } = useQuery<BlueprintStatus>({
    queryKey: ["bp-status", projectPath],
    queryFn: () => getBlueprintStatus(projectPath),
    enabled: !!projectPath,
  });

  const handleNodeClick = useCallback((mod: BlueprintModule) => {
    setSidePanel({ type: "module", module: mod });
    setPinnedModuleId((prev) => (prev === mod.id ? null : mod.id));
  }, []);

  const handleNodeHover = useCallback((id: string | null) => {
    setHoveredModuleId(id);
  }, []);

  // 布局（稳定，只在 blueprint 变化时重建）
  const layout = useMemo(() => {
    if (!blueprint) return null;
    return autoLayout(blueprint.manifest.areas, blueprint.manifest.modules);
  }, [blueprint]);

  // 关联模块集合（仅 pin 时生效，hover 不触发节点淡化）
  const connectedIds = useMemo(() => {
    if (!pinnedModuleId || !blueprint) return null;
    const ids = new Set<string>([pinnedModuleId]);
    for (const e of blueprint.manifest.edges) {
      if (e.from === pinnedModuleId) ids.add(e.to);
      if (e.to === pinnedModuleId) ids.add(e.from);
    }
    return ids;
  }, [pinnedModuleId, blueprint]);

  // nodes 依赖 layout + connectedIds（改 dimmed 标记，不改位置）
  const { nodes, absPositions } = useMemo(() => {
    if (!blueprint || !layout) return { nodes: [] as Node[], absPositions: {} as Record<string, { x: number; y: number }> };

    const { areaNodes, moduleNodes, absPositions: pos } = layout;

    const flowModuleNodes = moduleNodes.map((n) => {
      const mod = blueprint.manifest.modules.find((m) => m.id === n.id);
      const dimmed = connectedIds != null && !connectedIds.has(n.id);
      const pinned = pinnedModuleId === n.id;
      return {
        ...n,
        data: { ...n.data, onClick: () => handleNodeClick(mod!), onHover: handleNodeHover, dimmed, pinned },
      };
    });

    // area 节点也淡化
    const flowAreaNodes = areaNodes.map((n) => {
      const areaId = n.id.replace("area-", "");
      const hasConnected = connectedIds == null || blueprint.manifest.modules.some(
        (m) => m.area === areaId && connectedIds.has(m.id),
      );
      return {
        ...n,
        style: { ...n.style, opacity: hasConnected ? 1 : 0.3, transition: "opacity 0.2s" },
      };
    });

    return { nodes: [...flowAreaNodes, ...flowModuleNodes] as Node[], absPositions: pos };
  }, [blueprint, layout, connectedIds, pinnedModuleId, handleNodeClick, handleNodeHover]);

  // edges 单独 memo，hover 只重建边的样式
  const edges = useMemo(() => {
    if (!blueprint) return [] as Edge[];

    return blueprint.manifest.edges.map((e, i) => {
      const isDep = e.edge_type === "dependency";
      const isRelated = activeModuleId != null && (e.from === activeModuleId || e.to === activeModuleId);
      const dimmed = activeModuleId != null && !isRelated;

      // 根据模块相对位置选最短路径 handle
      const srcPos = absPositions[e.from];
      const tgtPos = absPositions[e.to];
      const handles = srcPos && tgtPos ? pickHandles(srcPos, tgtPos) : { sourceHandle: "right-s", targetHandle: "left-t" };

      return {
        id: `e-${i}`,
        source: e.from,
        target: e.to,
        sourceHandle: handles.sourceHandle,
        targetHandle: handles.targetHandle,
        type: "smoothstep",
        animated: isRelated && isDep,
        style: {
          stroke: isRelated
            ? (isDep ? "#2563EB" : "#6366F1")
            : (isDep ? "#3B82F6" : "#94A3B8"),
          strokeWidth: isRelated ? (isDep ? 3.5 : 2.5) : (isDep ? 2.5 : 1.5),
          strokeDasharray: isDep ? undefined : "6 4",
          opacity: dimmed ? 0.1 : isRelated ? 1 : (isDep ? 0.7 : 0.45),
          transition: "opacity 0.2s, stroke-width 0.2s",
        },
        markerEnd: {
          type: MarkerType.ArrowClosed,
          color: isRelated
            ? (isDep ? "#2563EB" : "#6366F1")
            : (isDep ? "#3B82F6" : "#94A3B8"),
          width: isDep ? 18 : 14,
          height: isDep ? 14 : 10,
        },
        zIndex: isRelated ? 10 : 0,
      };
    });
  }, [blueprint, activeModuleId, absPositions]);

  const stats = blueprint?.stats;

  return (
    <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
      <style>{`
        @keyframes ssr-glow {
          0%   { box-shadow: 0 0 0 2px #FFD700, 0 0 12px #FFD700, 0 0 30px #FFD70060; }
          25%  { box-shadow: 0 0 0 2px #A855F7, 0 0 12px #A855F7, 0 0 30px #A855F760; }
          50%  { box-shadow: 0 0 0 2px #06B6D4, 0 0 12px #06B6D4, 0 0 30px #06B6D460; }
          75%  { box-shadow: 0 0 0 2px #F43F5E, 0 0 12px #F43F5E, 0 0 30px #F43F5E60; }
          100% { box-shadow: 0 0 0 2px #FFD700, 0 0 12px #FFD700, 0 0 30px #FFD70060; }
        }
      `}</style>
      <div className="bg-white rounded-xl shadow-2xl w-[95vw] max-w-[1200px] h-[85vh] flex flex-col">
        {/* 标题栏 */}
        <div className="px-5 py-3 border-b border-gray-200 flex items-center justify-between shrink-0">
          <div>
            <h2 className="text-sm font-semibold text-gray-800">项目蓝图</h2>
            <p className="text-xs text-gray-400 mt-0.5">
              {projectName}
              {blueprint?.manifest.updated && (
                <span className="ml-2 text-gray-300">更新于 {blueprint.manifest.updated}</span>
              )}
            </p>
          </div>
          <div className="flex items-center gap-2">
            <button
              onClick={() => setSidePanel((p) => p?.type === "flywheel" ? null : { type: "flywheel" })}
              className={`text-xs px-2.5 py-1 rounded-lg transition-colors ${
                sidePanel?.type === "flywheel"
                  ? "text-white bg-emerald-600"
                  : "text-emerald-600 bg-emerald-50 hover:bg-emerald-100"
              }`}
            >
              ♻️ 飞轮
            </button>
            <button
              onClick={() => setSidePanel((p) => p?.type === "governance" ? null : { type: "governance" })}
              className={`text-xs px-2.5 py-1 rounded-lg transition-colors ${
                sidePanel?.type === "governance"
                  ? "text-white bg-violet-600"
                  : "text-violet-600 bg-violet-50 hover:bg-violet-100"
              }`}
            >
              ⚙️ 治理
            </button>
            <button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-xl leading-none">
              ×
            </button>
          </div>
        </div>

        {/* 统计条 */}
        {stats && (
          <div className="px-5 py-2.5 border-b border-gray-100 flex items-center gap-4 shrink-0 flex-wrap">
            {blueprint?.manifest.iteration != null && blueprint.manifest.iteration > 1 && (
              <span
                className="text-[10px] px-1.5 py-0.5 rounded bg-purple-50 text-purple-600 shrink-0 font-medium"
                title="当前迭代号（模块重构时递增）"
              >
                迭代 {blueprint.manifest.iteration}
              </span>
            )}
            <StatBadge color="#22C55E" label="已完成" count={stats.done} />
            <StatBadge color="#3B82F6" label="进行中" count={stats.in_progress} />
            <StatBadge color="#EAB308" label="已规划" count={stats.planned} />
            <StatBadge color="#9CA3AF" label="构思中" count={stats.concept} />
            {stats.tasks_blocked > 0 && (
              <StatBadge color="#EF4444" label="受阻" count={stats.tasks_blocked} />
            )}
            <div className="h-4 w-px bg-gray-200" />
            <span className="text-xs text-gray-500">
              任务 {stats.tasks_done}/{stats.total_tasks}
            </span>
            {stats.total_tasks - stats.tasks_done > 0 && (
              <button
                onClick={() => setSidePanel((p) => p?.type === "remaining" ? null : { type: "remaining" })}
                className={`text-xs px-2 py-0.5 rounded-full transition-colors ${
                  sidePanel?.type === "remaining"
                    ? "text-white bg-amber-500"
                    : "text-amber-600 bg-amber-50 hover:bg-amber-100"
                }`}
              >
                余 {stats.total_tasks - stats.tasks_done} 未完成
              </button>
            )}
            {stats.dispatchable > 0 && (
              <button
                onClick={() => setSidePanel((p) => p?.type === "next" ? null : { type: "next" })}
                className={`text-xs px-2 py-0.5 rounded-full transition-colors ${
                  sidePanel?.type === "next"
                    ? "text-white bg-blue-600"
                    : "text-blue-600 bg-blue-50 hover:bg-blue-100"
                }`}
              >
                📋 {stats.dispatchable} 可派发
              </button>
            )}
            {/* 规则 & AI文档 状态指示器 */}
            {bpStatus && (
              <button
                onClick={() => setSidePanel((p) => p?.type === "governance" ? null : { type: "governance" })}
                className={`text-[10px] px-1.5 py-0.5 rounded transition-colors shrink-0 ${
                  bpStatus.status === "synced"
                    ? "text-green-600 bg-green-50 hover:bg-green-100"
                    : "text-yellow-600 bg-yellow-50 hover:bg-yellow-100"
                }`}
              >
                {bpStatus.status === "synced" ? "🟢 规则" : "🟡 规则"}
              </button>
            )}
            <button
              onClick={() => setSidePanel((p) => p?.type === "flywheel" ? null : { type: "flywheel" })}
              className={`text-[10px] px-1.5 py-0.5 rounded transition-colors shrink-0 ${
                sidePanel?.type === "flywheel"
                  ? "text-white bg-emerald-600"
                  : "text-emerald-600 bg-emerald-50 hover:bg-emerald-100"
              }`}
              title="梦核·记忆巩固 / AI文档归档"
            >
              ♻️ AI文档
            </button>
            {/* 总进度条（固定在右侧） */}
            <div className="flex-1" />
            {pinnedModuleId && (
              <button
                onClick={() => { setPinnedModuleId(null); setSidePanel(null); }}
                className="text-xs px-2 py-0.5 rounded-full text-gray-500 bg-gray-100 hover:bg-gray-200 transition-colors shrink-0"
              >
                重置视图
              </button>
            )}
            <div className="flex items-center gap-2 min-w-[120px] w-[180px] shrink-0">
              <div className="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
                <div
                  className="h-full bg-green-500 rounded-full transition-all"
                  style={{ width: `${stats.total_modules > 0 ? Math.round((stats.done / stats.total_modules) * 100) : 0}%` }}
                />
              </div>
              <span className="text-xs text-gray-400 shrink-0">
                {stats.total_modules > 0 ? Math.round((stats.done / stats.total_modules) * 100) : 0}%
              </span>
            </div>
          </div>
        )}

        {/* 图例 */}
        {blueprint && (
          <div className="px-5 py-1.5 border-b border-gray-50 flex items-center gap-5 shrink-0">
            <span className="text-[10px] text-gray-400">图例:</span>
            <div className="flex items-center gap-1.5">
              <svg width="24" height="8"><line x1="0" y1="4" x2="24" y2="4" stroke="#3B82F6" strokeWidth="2" /><polygon points="20,1 24,4 20,7" fill="#3B82F6" /></svg>
              <span className="text-[10px] text-gray-400">依赖</span>
            </div>
            <div className="flex items-center gap-1.5">
              <svg width="24" height="8"><line x1="0" y1="4" x2="24" y2="4" stroke="#D1D5DB" strokeWidth="1" strokeDasharray="4 3" /><polygon points="20,1 24,4 20,7" fill="#D1D5DB" /></svg>
              <span className="text-[10px] text-gray-400">关联</span>
            </div>
            <div className="h-3 w-px bg-gray-200" />
            {blueprint.manifest.areas.map((a) => (
              <div key={a.id} className="flex items-center gap-1">
                <span className="w-2.5 h-2.5 rounded border" style={{ background: (a.color ?? "#6B7280") + "20", borderColor: (a.color ?? "#6B7280") + "60" }} />
                <span className="text-[10px] text-gray-400">{a.name}</span>
              </div>
            ))}
          </div>
        )}

        {/* 主体区域 */}
        <div className="flex-1 flex min-h-0">
          {/* React Flow 画布 */}
          <div className="flex-1 relative">
            {isLoading ? (
              <div className="flex items-center justify-center h-full text-gray-400 text-sm">
                加载中...
              </div>
            ) : error ? (
              <div className="flex flex-col items-center justify-center h-full gap-3 px-8">
                <div className="w-16 h-16 rounded-2xl bg-red-50 flex items-center justify-center text-2xl">!</div>
                <p className="text-gray-600 text-sm font-medium">蓝图解析失败</p>
                <div className="max-w-md w-full px-4 py-3 bg-red-50 border border-red-200 rounded-lg">
                  <p className="text-xs text-red-600 font-mono break-all select-text whitespace-pre-wrap">
                    {error instanceof Error ? error.message : String(error)}
                  </p>
                </div>
                <p className="text-xs text-gray-400 text-center">
                  请检查 .blueprint/manifest.yaml 格式是否正确
                </p>
              </div>
            ) : !blueprint ? (
              <div className="flex flex-col items-center justify-center h-full gap-3">
                <div className="w-16 h-16 rounded-2xl bg-gray-50 flex items-center justify-center text-2xl">🗺️</div>
                <p className="text-gray-400 text-sm">该项目尚未创建蓝图</p>
                <p className="text-gray-300 text-xs">
                  在项目根目录创建 <code className="bg-gray-100 px-1.5 py-0.5 rounded text-gray-500">.blueprint/</code> 目录即可启用
                </p>
              </div>
            ) : (
              <ReactFlow
                nodes={nodes}
                edges={edges}
                nodeTypes={nodeTypes}
                fitView
                fitViewOptions={{ padding: 0.3 }}
                minZoom={0.2}
                maxZoom={2}
                proOptions={{ hideAttribution: true }}
                nodesDraggable={false}
              >
                <Background gap={20} size={1} color="#f0f0f0" />
                <Controls position="bottom-left" showInteractive={false} />
              </ReactFlow>
            )}
          </div>

          {/* 侧边面板 */}
          {sidePanel && (
            <div className="w-[340px] border-l border-gray-200 overflow-y-auto shrink-0 bg-gray-50/50">
              {sidePanel.type === "module" ? (
                <ModuleDetail
                  module={sidePanel.module}
                  areas={blueprint?.manifest.areas ?? []}
                  onClose={() => { setSidePanel(null); setPinnedModuleId(null); }}
                />
              ) : sidePanel.type === "remaining" ? (
                <RemainingTasksPanel
                  modules={blueprint?.manifest.modules ?? []}
                  onSelectModule={(mod) => { setSidePanel({ type: "module", module: mod }); setPinnedModuleId(mod.id); }}
                  onClose={() => { setSidePanel(null); setPinnedModuleId(null); }}
                />
              ) : sidePanel.type === "governance" ? (
                <GovernancePanel
                  projectPath={projectPath}
                  projectName={projectName}
                  onClose={() => setSidePanel(null)}
                  onOpenFlywheel={() => setSidePanel({ type: "flywheel" })}
                />
              ) : sidePanel.type === "flywheel" ? (
                <FlywheelPanel onClose={() => setSidePanel(null)} />
              ) : (
                <NextActionsPanel
                  modules={blueprint?.manifest.modules ?? []}
                  projectName={projectName}
                  onSelectModule={(mod) => { setSidePanel({ type: "module", module: mod }); setPinnedModuleId(mod.id); }}
                  onClose={() => { setSidePanel(null); setPinnedModuleId(null); }}
                />
              )}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// ── 统计小标签 ───────────────────────────────────────────────────────────────

function StatBadge({ color, label, count }: { color: string; label: string; count: number }) {
  return (
    <div className="flex items-center gap-1.5">
      <span className="w-2 h-2 rounded-full shrink-0" style={{ background: color }} />
      <span className="text-xs text-gray-500">{label}</span>
      <span className="text-xs font-semibold text-gray-700">{count}</span>
    </div>
  );
}

// ── 模块详情面板 ─────────────────────────────────────────────────────────────

function ModuleDetail({ module: mod, areas, onClose }: { module: BlueprintModule; areas: BlueprintArea[]; onClose: () => void }) {
  const cfg = STATUS_CONFIG[mod.status] ?? STATUS_CONFIG.concept;
  const area = areas.find((a) => a.id === mod.area);

  const grouped = useMemo(() => {
    const groups: Record<string, BlueprintTask[]> = {
      done: [],
      in_progress: [],
      blocked: [],
      todo: [],
      concept: [],
    };
    for (const t of mod.tasks) {
      const key = groups[t.status] ? t.status : "concept";
      groups[key].push(t);
    }
    return groups;
  }, [mod.tasks]);

  const groupLabels: Record<string, string> = {
    done: "已完成",
    in_progress: "进行中",
    blocked: "受阻",
    todo: "待开发",
    concept: "构思中",
  };

  return (
    <div className="p-4 space-y-4">
      {/* 标题 */}
      <div className="flex items-start justify-between">
        <div>
          <div className="flex items-center gap-2">
            <span className="w-3 h-3 rounded-full" style={{ background: cfg.color }} />
            <h3 className="text-sm font-semibold text-gray-800">{mod.name}</h3>
          </div>
          <div className="flex items-center gap-2 mt-1">
            <span className="text-xs text-gray-400">{cfg.label} · {mod.progress}%</span>
            {area && (
              <span
                className="text-[10px] px-1.5 py-0.5 rounded"
                style={{ background: (area.color ?? "#6B7280") + "15", color: area.color ?? "#6B7280" }}
              >
                {area.name}
              </span>
            )}
          </div>
        </div>
        <button onClick={onClose} className="text-gray-300 hover:text-gray-500 text-lg">×</button>
      </div>

      {/* 进度条 */}
      <div className="w-full h-2 bg-gray-100 rounded-full overflow-hidden">
        <div className="h-full rounded-full" style={{ width: `${mod.progress}%`, background: cfg.color }} />
      </div>

      {/* 描述 */}
      {mod.description && (
        <p className="text-xs text-gray-500 leading-relaxed bg-white rounded-lg px-3 py-2 border border-gray-100">
          {mod.description}
        </p>
      )}

      {/* 决策记录 */}
      {mod.decisions && mod.decisions.length > 0 && (
        <div className="bg-amber-50 rounded-lg px-3 py-2 border border-amber-100">
          <p className="text-[10px] font-semibold text-amber-600 uppercase tracking-wider mb-1">决策记录</p>
          <ul className="space-y-1">
            {mod.decisions.map((d, i) => (
              <li key={i} className="text-xs text-amber-700 flex gap-1.5">
                <span className="shrink-0 text-amber-400">·</span>
                <span>{d}</span>
              </li>
            ))}
          </ul>
        </div>
      )}

      {/* 任务卡列表 */}
      {mod.tasks.length > 0 && (
        <div className="space-y-3">
          {(["blocked", "in_progress", "todo", "concept", "done"] as const).map((status) => {
            const tasks = grouped[status];
            if (!tasks || tasks.length === 0) return null;
            return (
              <div key={status}>
                <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider mb-1.5">
                  {groupLabels[status]} ({tasks.length})
                </p>
                <div className="space-y-1.5">
                  {tasks.map((t, i) => (
                    <TaskCard key={i} task={t} moduleName={mod.name} />
                  ))}
                </div>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// ── 任务卡 ───────────────────────────────────────────────────────────────────

function TaskCard({ task, moduleName }: { task: BlueprintTask; moduleName?: string }) {
  const [expanded, setExpanded] = useState(false);
  const [copied, setCopied] = useState(false);
  const prefix = task.locked ? "🔒" : (TASK_PREFIX[task.status] ?? "");
  const isDispatchable =
    task.status === "todo" &&
    !task.locked &&
    task.files &&
    task.acceptance &&
    (task.complexity === "S" || task.complexity === "M");

  const handleCopyForAgent = (e: React.MouseEvent) => {
    e.stopPropagation();
    const lines = [
      `请完成以下任务（完成后更新 .blueprint/modules/ 中对应的任务卡状态为 ✅ done）：`,
      ``,
      `## ${task.title}`,
      moduleName ? `所属模块: ${moduleName}` : "",
      task.complexity ? `复杂度: ${task.complexity}` : "",
      ``,
      task.files ? `### 涉及文件\n${task.files}` : "",
      task.acceptance ? `\n### 验收标准\n${task.acceptance}` : "",
      task.depends ? `\n### 前置依赖\n${task.depends}` : "",
      task.notes ? `\n### 补充说明\n${task.notes}` : "",
    ].filter(Boolean);
    navigator.clipboard.writeText(lines.join("\n"));
    setCopied(true);
    setTimeout(() => setCopied(false), 1500);
  };

  return (
    <div
      onClick={() => setExpanded(!expanded)}
      className={`rounded-lg border px-3 py-2 cursor-pointer transition-colors ${
        task.locked
          ? "border-gray-200 bg-gray-50/80 hover:bg-gray-100 opacity-60"
          : task.status === "blocked"
            ? "border-red-200 bg-red-50/50 hover:bg-red-50"
            : isDispatchable
              ? "border-blue-200 bg-blue-50/50 hover:bg-blue-50"
              : "border-gray-100 bg-white hover:bg-gray-50"
      }`}
    >
      <div className="flex items-center gap-2">
        <span className="text-xs shrink-0">{prefix}</span>
        <span className="text-xs text-gray-700 flex-1">{task.title}</span>
        {task.complexity && (
          <span className="text-[10px] text-gray-400 bg-gray-100 px-1.5 py-0.5 rounded shrink-0">
            {task.complexity}
          </span>
        )}
        {isDispatchable && (
          <button
            onClick={handleCopyForAgent}
            className="text-[10px] text-blue-500 bg-blue-100 px-1.5 py-0.5 rounded shrink-0 hover:bg-blue-200 transition-colors"
          >
            {copied ? "已复制" : "复制派发"}
          </button>
        )}
      </div>

      {/* locked 依赖提示，始终显示 */}
      {task.locked && task.depends && (
        <div className="mt-1.5 px-2 py-1.5 bg-gray-100 border border-gray-200 rounded text-[11px] text-gray-500">
          <span className="font-semibold text-gray-400">待解锁，依赖:</span> {task.depends}
        </div>
      )}

      {/* blocked_reason 始终显示，不需要展开 */}
      {task.status === "blocked" && task.blocked_reason && (
        <div className="mt-1.5 px-2 py-1.5 bg-red-50 border border-red-100 rounded text-[11px] text-red-600">
          <span className="font-semibold text-red-500">阻塞原因:</span> {task.blocked_reason}
        </div>
      )}

      {expanded && (
        <div className="mt-2 pt-2 border-t border-gray-100 space-y-1 text-[11px] text-gray-500">
          {task.files && (
            <p><span className="text-gray-400">文件:</span> <code className="font-mono text-gray-600 bg-gray-50 px-1 rounded">{task.files}</code></p>
          )}
          {task.acceptance && (
            <p><span className="text-gray-400">验收:</span> {task.acceptance}</p>
          )}
          {task.depends && (
            <p><span className="text-gray-400">依赖:</span> {task.depends}</p>
          )}
          {task.notes && (
            <p><span className="text-gray-400">备注:</span> {task.notes}</p>
          )}
        </div>
      )}
    </div>
  );
}

// ── 下一步汇总面板 ──────────────────────────────────────────────────────────

function NextActionsPanel({
  modules,
  projectName,
  onSelectModule,
  onClose,
}: {
  modules: BlueprintModule[];
  projectName: string;
  onSelectModule: (mod: BlueprintModule) => void;
  onClose: () => void;
}) {
  const [batchCopied, setBatchCopied] = useState(false);

  const handleCopyBatchPrompt = () => {
    const prompt = [
      `当前项目：${projectName}`,
      ``,
      `读取 .blueprint/manifest.yaml 和所有 .blueprint/modules/*.md，`,
      `以及 .blueprint/CONVENTIONS.md 了解执行规则。`,
      ``,
      `【处理断点】先检查是否有 🔵 in_progress 任务卡（上次中断的），`,
      `若有，读取其 files 字段判断已完成什么、还缺什么，从断点继续。`,
      ``,
      `【批量执行】找出所有 📋 todo 任务卡，按任务卡 depends 字段及 manifest.yaml edges 综合拓扑排序后依次执行：`,
      `- 执行前：前缀改为 🔵（信息1·加锁）`,
      `- 执行依据：任务卡的 files 字段（涉及哪些文件）+ acceptance 字段（验收标准）`,
      `- 完成后：前缀改为 ✅（信息2·解锁）；若模块所有任务卡均完成，模块 status 改为 done`,
      `- 更新 manifest.yaml 的 updated 日期`,
      `- 无法独立解决：标记 🔴 blocked + blocked_reason，跳过继续下一张`,
      ``,
      `中途不需要等我确认，直接执行到底，全部完成后输出执行报告。`,
    ].join("\n");
    navigator.clipboard.writeText(prompt);
    setBatchCopied(true);
    setTimeout(() => setBatchCopied(false), 2000);
  };
  // 收集所有可派发和进行中的任务
  const actionItems = useMemo(() => {
    const items: { module: BlueprintModule; task: BlueprintTask; dispatchable: boolean }[] = [];
    for (const mod of modules) {
      for (const t of mod.tasks) {
        const dispatchable =
          t.status === "todo" &&
          !!t.files &&
          !!t.acceptance &&
          (t.complexity === "S" || t.complexity === "M");
        if (dispatchable || t.status === "in_progress" || t.status === "blocked" || t.locked) {
          items.push({ module: mod, task: t, dispatchable });
        }
      }
    }
    // blocked 最前，进行中次之，可派发再次，locked 排最后
    const priority = (item: typeof items[0]): number => {
      if (item.task.status === "blocked") return 0;
      if (item.task.status === "in_progress") return 1;
      if (item.task.locked) return 3;
      return 2; // dispatchable
    };
    items.sort((a, b) => priority(a) - priority(b));
    return items;
  }, [modules]);

  return (
    <div className="p-4 space-y-4">
      <div className="flex items-center justify-between">
        <h3 className="text-sm font-semibold text-gray-800">下一步</h3>
        <div className="flex items-center gap-2">
          <button
            onClick={handleCopyBatchPrompt}
            className="text-[11px] px-2 py-0.5 rounded-lg bg-blue-50 text-blue-600 hover:bg-blue-100 transition-colors"
            title="复制批量执行提示词，粘贴给 Claude 自动完成所有 📋 任务卡"
          >
            {batchCopied ? "已复制 ✓" : "🚀 批量执行"}
          </button>
          <button onClick={onClose} className="text-gray-300 hover:text-gray-500 text-lg">×</button>
        </div>
      </div>

      {actionItems.length === 0 ? (
        <p className="text-xs text-gray-400 text-center py-6">暂无可执行任务</p>
      ) : (
        <div className="space-y-3">
          {actionItems.filter((i) => i.task.status === "blocked").length > 0 && (
            <div>
              <p className="text-[10px] font-semibold text-red-500 uppercase tracking-wider mb-1.5">受阻 — 需架构师介入</p>
              <div className="space-y-1.5">
                {actionItems
                  .filter((i) => i.task.status === "blocked")
                  .map((item, i) => (
                    <div key={i}>
                      <button
                        onClick={() => onSelectModule(item.module)}
                        className="text-[10px] text-gray-400 hover:text-red-500 mb-0.5 transition-colors"
                      >
                        {item.module.name} →
                      </button>
                      <TaskCard task={item.task} moduleName={item.module.name} />
                    </div>
                  ))}
              </div>
            </div>
          )}
          {actionItems.filter((i) => i.task.status === "in_progress").length > 0 && (
            <div>
              <p className="text-[10px] font-semibold text-blue-500 uppercase tracking-wider mb-1.5">进行中</p>
              <div className="space-y-1.5">
                {actionItems
                  .filter((i) => i.task.status === "in_progress")
                  .map((item, i) => (
                    <div key={i}>
                      <button
                        onClick={() => onSelectModule(item.module)}
                        className="text-[10px] text-gray-400 hover:text-blue-500 mb-0.5 transition-colors"
                      >
                        {item.module.name} →
                      </button>
                      <TaskCard task={item.task} moduleName={item.module.name} />
                    </div>
                  ))}
              </div>
            </div>
          )}
          {actionItems.filter((i) => i.dispatchable).length > 0 && (
            <div>
              <p className="text-[10px] font-semibold text-amber-500 uppercase tracking-wider mb-1.5">可派发</p>
              <div className="space-y-1.5">
                {actionItems
                  .filter((i) => i.dispatchable)
                  .map((item, i) => (
                    <div key={i}>
                      <button
                        onClick={() => onSelectModule(item.module)}
                        className="text-[10px] text-gray-400 hover:text-blue-500 mb-0.5 transition-colors"
                      >
                        {item.module.name} →
                      </button>
                      <TaskCard task={item.task} moduleName={item.module.name} />
                    </div>
                  ))}
              </div>
            </div>
          )}
          {actionItems.filter((i) => i.task.locked).length > 0 && (
            <div>
              <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider mb-1.5">待解锁（依赖未完成）</p>
              <div className="space-y-1.5">
                {actionItems
                  .filter((i) => i.task.locked)
                  .map((item, i) => (
                    <div key={i}>
                      <button
                        onClick={() => onSelectModule(item.module)}
                        className="text-[10px] text-gray-400 hover:text-gray-600 mb-0.5 transition-colors"
                      >
                        {item.module.name} →
                      </button>
                      <TaskCard task={item.task} moduleName={item.module.name} />
                    </div>
                  ))}
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ── 蓝图治理面板 ────────────────────────────────────────────────────────────

function GovernancePanel({
  projectPath,
  projectName,
  onClose,
  onOpenFlywheel,
}: {
  projectPath: string;
  projectName: string;
  onClose: () => void;
  onOpenFlywheel?: () => void;
}) {
  const { data: bpStatus, isPending: statusLoading, refetch: refetchStatus } = useQuery<BlueprintStatus>({
    queryKey: ["bp-status-governance", projectPath],
    queryFn: () => getBlueprintStatus(projectPath),
  });

  const [syncing, setSyncing] = useState(false);
  const [syncResult, setSyncResult] = useState<SyncResult | null>(null);
  const [syncError, setSyncError] = useState<string | null>(null);

  const [promptMode, setPromptMode] = useState<"init" | "sync" | null>(null);
  const [prompt, setPrompt] = useState("");
  const [promptLoading, setPromptLoading] = useState(false);
  const [promptError, setPromptError] = useState<string | null>(null);
  const [copied, setCopied] = useState(false);

  const handleSyncRules = async () => {
    setSyncing(true);
    setSyncError(null);
    setSyncResult(null);
    try {
      const res = await syncBlueprintRules(projectPath);
      setSyncResult(res);
      refetchStatus();
    } catch (e) {
      setSyncError(String(e));
    } finally {
      setSyncing(false);
    }
  };

  const handleOpenPrompt = async (mode: "init" | "sync") => {
    if (promptMode === mode) {
      setPromptMode(null);
      return;
    }
    setPromptMode(mode);
    setPrompt("");
    setPromptError(null);
    setPromptLoading(true);
    try {
      const result = await generateBlueprintPrompt(projectPath, mode);
      setPrompt(result);
    } catch (e) {
      setPromptError(String(e));
    } finally {
      setPromptLoading(false);
    }
  };

  const handleCopy = () => {
    navigator.clipboard.writeText(prompt).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    });
  };

  const rulesOutdated = bpStatus?.status === "rules_outdated";
  const currentVersion = bpStatus?.conventions_version ?? "—";
  const masterVersion = bpStatus?.master_version ?? "—";
  // 用后端完整状态判断（rules_outdated 才是真正未同步，content_stale/synced 都意味着规则已同步）
  const versionSynced = bpStatus?.status === "synced" || bpStatus?.status === "content_stale";

  return (
    <div className="p-4 space-y-4">
      {/* 标题 */}
      <div className="flex items-center justify-between">
        <h3 className="text-sm font-semibold text-gray-800">蓝图治理</h3>
        <button onClick={onClose} className="text-gray-300 hover:text-gray-500 text-lg">×</button>
      </div>

      {/* 规则版本状态 */}
      <div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
        <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">CONVENTIONS 规则版本</p>
        {statusLoading ? (
          <p className="text-xs text-gray-400">检测中…</p>
        ) : (
          <div className="space-y-1.5">
            <div className="flex items-center justify-between">
              <span className="text-xs text-gray-500">当前版本</span>
              <span className="text-xs font-mono text-gray-700">{currentVersion}</span>
            </div>
            <div className="flex items-center justify-between">
              <span className="text-xs text-gray-500">最新版本</span>
              <span className="text-xs font-mono text-gray-700">{masterVersion}</span>
            </div>
            <div className="flex items-center justify-between">
              <span className="text-xs text-gray-500">状态</span>
              {versionSynced ? (
                <span className="text-xs text-green-600 bg-green-50 px-1.5 py-0.5 rounded">🟢 已同步</span>
              ) : (
                <span className="text-xs text-yellow-600 bg-yellow-50 px-1.5 py-0.5 rounded">🟡 待更新</span>
              )}
            </div>
          </div>
        )}
      </div>

      {/* 同步规则区域 */}
      {rulesOutdated && !syncResult && (
        <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 space-y-2">
          <p className="text-xs text-yellow-800 font-semibold">规则版本待更新</p>
          <p className="text-[11px] text-yellow-700 leading-relaxed">
            将覆盖写入 <code className="font-mono bg-yellow-100 px-1 rounded">.blueprint/CONVENTIONS.md</code> 并更新 CLAUDE.md 中的蓝图规则区块。
          </p>
          {syncError && (
            <p className="text-[11px] text-red-600 font-mono break-all">{syncError}</p>
          )}
          <button
            onClick={handleSyncRules}
            disabled={syncing}
            className="w-full py-1.5 rounded-lg bg-yellow-500 text-white text-xs hover:bg-yellow-600 disabled:opacity-50 transition-colors"
          >
            {syncing ? "同步中…" : "同步规则"}
          </button>
        </div>
      )}

      {syncResult && (
        <div className="bg-green-50 border border-green-200 rounded-lg p-3">
          <p className="text-xs text-green-700 font-semibold mb-1">✓ 同步完成</p>
          <p className="text-[11px] text-green-600 font-mono">{syncResult.message}</p>
        </div>
      )}

      {/* 蓝图内容提示词 */}
      <div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
        <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">蓝图内容</p>
        <p className="text-[11px] text-gray-500 leading-relaxed">
          将提示词交给 Claude Opus，由它负责理解和创作蓝图内容。
        </p>
        <div className="flex gap-2">
          <button
            onClick={() => handleOpenPrompt("init")}
            disabled={bpStatus?.status !== "none"}
            title={bpStatus?.status !== "none" ? "该项目已有蓝图，请使用「更新蓝图描绘」" : undefined}
            className={`flex-1 py-1.5 rounded-lg text-xs transition-colors ${
              bpStatus?.status !== "none"
                ? "border border-gray-100 text-gray-300 cursor-not-allowed"
                : promptMode === "init"
                ? "bg-blue-600 text-white"
                : "border border-gray-200 text-gray-600 hover:bg-gray-50"
            }`}
          >
            初始化蓝图
          </button>
          <button
            onClick={() => handleOpenPrompt("sync")}
            className={`flex-1 py-1.5 rounded-lg text-xs transition-colors ${
              promptMode === "sync"
                ? "bg-blue-600 text-white"
                : "border border-gray-200 text-gray-600 hover:bg-gray-50"
            }`}
          >
            更新蓝图描绘
          </button>
        </div>
      </div>

      {/* 提示词内容区 */}
      {promptMode && (
        <div className="space-y-2">
          {promptLoading ? (
            <p className="text-xs text-gray-400 text-center py-4">生成提示词中…</p>
          ) : promptError ? (
            <p className="text-xs text-red-500 font-mono break-all">{promptError}</p>
          ) : (
            <>
              <textarea
                readOnly
                value={prompt}
                rows={12}
                className="w-full font-mono text-[11px] text-gray-700 bg-gray-50 border border-gray-200 rounded-lg p-2.5 resize-none outline-none focus:ring-2 focus:ring-blue-200"
                onClick={(e) => (e.target as HTMLTextAreaElement).select()}
              />
              <button
                onClick={handleCopy}
                className="w-full py-1.5 rounded-lg bg-blue-600 text-white text-xs hover:bg-blue-700 transition-colors"
              >
                {copied ? "已复制 ✓" : "复制提示词"}
              </button>
              <p className="text-[10px] text-gray-400 text-center">点击文本框可全选</p>
            </>
          )}
        </div>
      )}

      {/* 梦核分析入口 */}
      {onOpenFlywheel && (
        <div className="bg-emerald-50 rounded-lg border border-emerald-100 p-3 space-y-2">
          <p className="text-[10px] font-semibold text-emerald-600 uppercase tracking-wider">梦核 · 记忆巩固</p>
          <p className="text-[11px] text-emerald-700 leading-relaxed">
            基于跨项目飞轮数据，生成梦核提示词，由 Claude Opus 完成固化 / 遗忘判断。
          </p>
          <button
            onClick={onOpenFlywheel}
            className="w-full py-1.5 rounded-lg bg-emerald-600 text-white text-xs hover:bg-emerald-700 transition-colors"
          >
            ♻️ 打开飞轮面板
          </button>
        </div>
      )}

      {/* 项目信息 */}
      <div className="pt-2 border-t border-gray-100">
        <p className="text-[10px] text-gray-300 truncate">{projectName}</p>
      </div>
    </div>
  );
}

// ── 未完成任务面板 ──────────────────────────────────────────────────────────

const REMAINING_ORDER: Record<string, number> = { blocked: 0, in_progress: 1, todo: 2, concept: 3 };
const REMAINING_LABEL: Record<string, string> = { blocked: "受阻", in_progress: "进行中", todo: "待开发", concept: "构思中" };
const REMAINING_COLOR: Record<string, string> = { blocked: "text-red-500", in_progress: "text-blue-500", todo: "text-amber-500", concept: "text-gray-400" };

function RemainingTasksPanel({
  modules,
  onSelectModule,
  onClose,
}: {
  modules: BlueprintModule[];
  onSelectModule: (mod: BlueprintModule) => void;
  onClose: () => void;
}) {
  const grouped = useMemo(() => {
    const groups: Record<string, { module: BlueprintModule; task: BlueprintTask }[]> = {};
    for (const mod of modules) {
      for (const t of mod.tasks) {
        if (t.status === "done") continue;
        const key = REMAINING_ORDER[t.status] !== undefined ? t.status : "concept";
        if (!groups[key]) groups[key] = [];
        groups[key].push({ module: mod, task: t });
      }
    }
    return groups;
  }, [modules]);

  const sortedKeys = Object.keys(grouped).sort((a, b) => (REMAINING_ORDER[a] ?? 9) - (REMAINING_ORDER[b] ?? 9));
  const totalRemaining = sortedKeys.reduce((sum, k) => sum + grouped[k].length, 0);

  return (
    <div className="p-4 space-y-4">
      <div className="flex items-center justify-between">
        <div>
          <h3 className="text-sm font-semibold text-gray-800">未完成任务</h3>
          <p className="text-[10px] text-gray-400 mt-0.5">共 {totalRemaining} 项</p>
        </div>
        <button onClick={onClose} className="text-gray-300 hover:text-gray-500 text-lg">×</button>
      </div>

      {totalRemaining === 0 ? (
        <p className="text-xs text-gray-400 text-center py-6">所有任务已完成</p>
      ) : (
        <div className="space-y-3">
          {sortedKeys.map((status) => (
            <div key={status}>
              <p className={`text-[10px] font-semibold uppercase tracking-wider mb-1.5 ${REMAINING_COLOR[status] ?? "text-gray-400"}`}>
                {REMAINING_LABEL[status] ?? status} ({grouped[status].length})
              </p>
              <div className="space-y-1.5">
                {grouped[status].map((item, i) => (
                  <div key={i}>
                    <button
                      onClick={() => onSelectModule(item.module)}
                      className="text-[10px] text-gray-400 hover:text-blue-500 mb-0.5 transition-colors"
                    >
                      {item.module.name} →
                    </button>
                    <TaskCard task={item.task} moduleName={item.module.name} />
                  </div>
                ))}
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── 飞轮健康面板 ────────────────────────────────────────────────────────────

function StalledProjectRow({ p, projectsMap, onArchive }: {
  p: StalledProject;
  projectsMap: Record<string, string>; // name → path
  onArchive?: (path: string, name: string) => void;
}) {
  const path = projectsMap[p.name];
  return (
    <div className="flex items-center justify-between gap-2">
      <div className="min-w-0">
        <p className="text-[11px] text-amber-700 truncate">· {p.name}</p>
        <p className="text-[10px] text-amber-400">最后活跃: {p.last_active}</p>
      </div>
      {onArchive && path && (
        <button
          onClick={() => onArchive(path, p.name)}
          className="text-[10px] text-amber-600 bg-amber-100 hover:bg-amber-200 px-1.5 py-0.5 rounded shrink-0 transition-colors"
        >
          归档
        </button>
      )}
    </div>
  );
}

function FlywheelPanel({ onClose }: { onClose: () => void }) {
  const { data: projects } = useQuery<Project[]>({
    queryKey: ["projects-for-flywheel"],
    queryFn: () => getProjects(),
  });

  const { projectPaths, projectsMap } = useMemo(() => {
    const paths = (projects ?? [])
      .map((p) => p.win_path || p.wsl_path || "")
      .filter(Boolean);
    const map: Record<string, string> = {};
    for (const p of (projects ?? [])) {
      const path = p.win_path || p.wsl_path || "";
      if (!path) continue;
      // 用炼境项目名做 key
      map[p.name] = path;
      // 同时用路径末尾目录名做备用 key（usage.json 里存的是目录名）
      const dirName = path.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? "";
      if (dirName && !map[dirName]) map[dirName] = path;
    }
    return { projectPaths: paths, projectsMap: map };
  }, [projects]);

  const { data: stats, isLoading } = useQuery<FlywheelStats>({
    queryKey: ["flywheel-stats", projectPaths],
    queryFn: () => getFlywheelStats(projectPaths),
    enabled: projectPaths.length > 0,
  });

  const [muhePrompt, setMuhePrompt] = useState("");
  const [generating, setGenerating] = useState(false);
  const [copied, setCopied] = useState(false);
  const [archiveMsg, setArchiveMsg] = useState<string | null>(null);
  const [archiving, setArchiving] = useState<string | null>(null);
  const [pendingArchive, setPendingArchive] = useState<{ path: string; name: string } | null>(null);

  const handleGenerateMuhe = async () => {
    if (!stats) return;
    setGenerating(true);
    try {
      const result = await generateMuhePrompt(stats);
      setMuhePrompt(result);
    } finally {
      setGenerating(false);
    }
  };

  const handleCopy = () => {
    navigator.clipboard.writeText(muhePrompt).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    });
  };

  const handleArchiveConfirm = async () => {
    if (!pendingArchive) return;
    const { path, name } = pendingArchive;
    setPendingArchive(null);
    setArchiving(name);
    setArchiveMsg(null);
    try {
      const result = await archiveProjectDocs(path);
      setArchiveMsg(`✓ 已归档 ${result.archived_files.length} 个文件 → ${result.archive_dir}`);
    } catch (e) {
      setArchiveMsg(`归档失败: ${String(e)}`);
    } finally {
      setArchiving(null);
    }
  };

  const fmtPct = (v: number) => `${(v * 100).toFixed(1)}%`;

  // Fix 2：去重——90天休眠的项目不在60天停转里重复展示
  const stalledOnlyProjects = (stats?.stalled_projects ?? []).filter(
    (p) => !(stats?.ai_doc_stale_projects ?? []).some((q) => q.name === p.name)
  );

  return (
    <div className="p-4 space-y-4">
      <div className="flex items-center justify-between">
        <div>
          <h3 className="text-sm font-semibold text-gray-800">飞轮健康</h3>
          {stats && (
            <p className="text-[10px] text-gray-400 mt-0.5">
              {stats.projects_with_data}/{stats.total_projects_scanned} 个项目有数据
            </p>
          )}
        </div>
        <button onClick={onClose} className="text-gray-300 hover:text-gray-500 text-lg">×</button>
      </div>

      {isLoading ? (
        <p className="text-xs text-gray-400 text-center py-6">聚合中…</p>
      ) : !stats || stats.projects_with_data === 0 ? (
        <div className="text-center py-6 space-y-2">
          <p className="text-xs text-gray-400">暂无飞轮数据</p>
          <p className="text-[11px] text-gray-300">打开各项目蓝图后数据将自动积累</p>
        </div>
      ) : (
        <>
          {/* CONVENTIONS 版本队列对比 */}
          {stats.cohorts.length > 0 && (
            <div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
              <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">版本队列对比</p>
              <div className="space-y-3">
                {stats.cohorts.map((c) => (
                  <div key={c.conventions_version} className="space-y-1.5">
                    <div className="flex items-center justify-between">
                      <span className="text-xs font-mono text-gray-600">v{c.conventions_version}</span>
                      <span className="text-[10px] text-gray-400">{c.project_count} 个项目</span>
                    </div>
                    {/* Fix 4：标签改为"任务完成率"，字段改为 completion_rate */}
                    <div className="grid grid-cols-2 gap-x-3 gap-y-0.5">
                      <MetricRow label="可派发率" value={fmtPct(c.dispatchable_rate)} good={c.dispatchable_rate > 0.3} />
                      <MetricRow label="任务完成率" value={fmtPct(c.completion_rate)} good={c.completion_rate > 0.5} />
                      <MetricRow label="飞轮持续率" value={fmtPct(c.continuity_rate)} good={c.continuity_rate > 0.6} />
                      <MetricRow label="阻塞密度" value={fmtPct(c.blocked_density)} good={c.blocked_density < 0.1} invert />
                    </div>
                  </div>
                ))}
              </div>
            </div>
          )}

          {/* TOP blocked reasons */}
          {stats.top_blocked_reasons.length > 0 && (
            <div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
              <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">TOP 阻塞原因</p>
              <ul className="space-y-1">
                {stats.top_blocked_reasons.map((r, i) => (
                  <li key={i} className="flex items-start gap-2">
                    <span className="text-[10px] text-gray-400 shrink-0 mt-0.5">#{i + 1}</span>
                    <span className="text-[11px] text-red-600 flex-1">{r.reason}</span>
                    <span className="text-[10px] text-gray-400 shrink-0">×{r.count}</span>
                  </li>
                ))}
              </ul>
            </div>
          )}

          {/* Fix 2：仅展示"停转但未到90天"的项目 */}
          {stalledOnlyProjects.length > 0 && (
            <div className="bg-amber-50 rounded-lg border border-amber-100 p-3 space-y-1.5">
              <p className="text-[10px] font-semibold text-amber-500 uppercase tracking-wider">
                飞轮停转（60–90天无快照）
              </p>
              {stalledOnlyProjects.map((p, i) => (
                <StalledProjectRow key={i} p={p} projectsMap={projectsMap} />
              ))}
            </div>
          )}

          {/* AI文档休眠项目（≥90天） */}
          {stats.ai_doc_stale_projects.length > 0 && (
            <div className="bg-red-50 rounded-lg border border-red-100 p-3 space-y-1.5">
              <p className="text-[10px] font-semibold text-red-500 uppercase tracking-wider">
                AI文档休眠（90天+无快照）
              </p>
              <p className="text-[10px] text-red-400 mb-1">
                建议归档 .blueprint/modules/，清空 Claude 上下文负担。
              </p>
              {/* Fix 1：归档确认弹窗 */}
              {pendingArchive ? (
                <div className="bg-red-100 border border-red-200 rounded p-2.5 space-y-2">
                  <p className="text-[11px] text-red-700 font-semibold">
                    确认归档「{pendingArchive.name}」？
                  </p>
                  <p className="text-[10px] text-red-500 leading-relaxed">
                    将删除 .blueprint/modules/*.md，文件会备份到 .blueprint/archive/，但操作本身不可一键撤销。
                  </p>
                  <div className="flex gap-2">
                    <button
                      onClick={handleArchiveConfirm}
                      disabled={!!archiving}
                      className="flex-1 py-1 rounded bg-red-600 text-white text-[11px] hover:bg-red-700 disabled:opacity-50 transition-colors"
                    >
                      确认归档
                    </button>
                    <button
                      onClick={() => setPendingArchive(null)}
                      className="flex-1 py-1 rounded border border-red-200 text-red-500 text-[11px] hover:bg-red-50 transition-colors"
                    >
                      取消
                    </button>
                  </div>
                </div>
              ) : (
                stats.ai_doc_stale_projects.map((p, i) => (
                  <StalledProjectRow
                    key={i}
                    p={p}
                    projectsMap={projectsMap}
                    onArchive={archiving ? undefined : (path, name) => setPendingArchive({ path, name })}
                  />
                ))
              )}
              {archiving && (
                <p className="text-[10px] text-red-400">归档 {archiving} 中…</p>
              )}
              {archiveMsg && (
                <p className={`text-[10px] break-all ${archiveMsg.startsWith("✓") ? "text-green-600" : "text-red-600"}`}>
                  {archiveMsg}
                </p>
              )}
            </div>
          )}

          {/* 迭代跳变项目 */}
          {(stats.iteration_jumps ?? []).length > 0 && (
            <div className="bg-purple-50 rounded-lg border border-purple-100 p-3 space-y-1.5">
              <p className="text-[10px] font-semibold text-purple-500 uppercase tracking-wider">
                迭代跳变（主动重构，完成率重置）
              </p>
              {(stats.iteration_jumps ?? []).map((j, i) => (
                <div key={i} className="flex items-center gap-2 text-[11px]">
                  <span className="text-gray-600 truncate flex-1">{j.project}</span>
                  <span className="text-purple-400 shrink-0">迭代 {j.from_iteration}→{j.to_iteration}</span>
                  <span className={`shrink-0 font-mono ${j.rate_after < j.rate_before - 0.3 ? "text-amber-500 font-semibold" : "text-gray-400"}`}>
                    {(j.rate_before * 100).toFixed(0)}%→{(j.rate_after * 100).toFixed(0)}%
                    {j.rate_after < j.rate_before - 0.3 && " ⚠️"}
                  </span>
                </div>
              ))}
            </div>
          )}

          {/* 停滞模块 */}
          {(stats.stalled_modules ?? []).length > 0 && (
            <div className="bg-gray-50 rounded-lg border border-gray-100 p-3 space-y-1.5">
              <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">
                停滞模块（in_progress 但无可推进任务）
              </p>
              <ul className="space-y-0.5">
                {(stats.stalled_modules ?? []).map((m, i) => (
                  <li key={i} className="text-[11px] text-gray-500 font-mono">{m}</li>
                ))}
              </ul>
            </div>
          )}

          {/* 梦核分析 */}
          <div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
            <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">梦核分析</p>
            <p className="text-[11px] text-gray-500 leading-relaxed">
              生成包含以上数据的提示词，交给 Claude Opus 做固化/遗忘判断。
            </p>
            {!muhePrompt ? (
              <button
                onClick={handleGenerateMuhe}
                disabled={generating}
                className="w-full py-1.5 rounded-lg bg-emerald-600 text-white text-xs hover:bg-emerald-700 disabled:opacity-50 transition-colors"
              >
                {generating ? "生成中…" : "生成梦核提示词"}
              </button>
            ) : (
              <div className="space-y-2">
                <textarea
                  readOnly
                  value={muhePrompt}
                  rows={10}
                  className="w-full font-mono text-[11px] text-gray-700 bg-gray-50 border border-gray-200 rounded-lg p-2.5 resize-none outline-none"
                  onClick={(e) => (e.target as HTMLTextAreaElement).select()}
                />
                <button
                  onClick={handleCopy}
                  className="w-full py-1.5 rounded-lg bg-emerald-600 text-white text-xs hover:bg-emerald-700 transition-colors"
                >
                  {copied ? "已复制 ✓" : "复制提示词"}
                </button>
                <button
                  onClick={() => setMuhePrompt("")}
                  className="w-full py-1 rounded-lg text-xs text-gray-400 hover:text-gray-600 transition-colors"
                >
                  重新生成
                </button>
                {/* Fix 3：说清楚实际流程——梦核不自动写入，需手动更新源码再发版 */}
                <p className="text-[10px] text-gray-400 text-center leading-relaxed">
                  复制 → 粘贴给 Claude Opus → 将建议反馈给炼境维护者 → 更新 CONVENTIONS.md 并发布新版本
                </p>
              </div>
            )}
          </div>
        </>
      )}
    </div>
  );
}

function MetricRow({ label, value, good, invert = false }: {
  label: string;
  value: string;
  good: boolean;
  invert?: boolean;
}) {
  const isGood = invert ? !good : good;
  return (
    <div className="flex items-center justify-between">
      <span className="text-[10px] text-gray-400">{label}</span>
      <span className={`text-[10px] font-mono font-semibold ${isGood ? "text-green-600" : "text-yellow-600"}`}>
        {value}
      </span>
    </div>
  );
}
