import { useState, useCallback, useMemo, useEffect, useRef } from "react";
import { AgentInfraPanel } from "./AgentInfraPanel";
import { GiteaPanel } from "./GiteaPanel";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { listen } from "@tauri-apps/api/event";
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,
  applyOnboardingPack,
  getCommitStats,
  removeBlueprintBuff,
  getBuffStatus,
  type BlueprintData,
  type BlueprintModule,
  type BlueprintTask,
  type BlueprintArea,
  type BlueprintStatus,
  type SyncResult,
  type BuffStatus,
  type TemplateResult,
  type CommitStats,
} from "../../lib/commands";

interface Props {
  projectName: string;
  projectPath: string;
  projectId?: 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: "受阻" },
  abandoned:   { color: "#52525B", bg: "#FAFAFA", 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" } | null;

export function BlueprintModal({ projectName, projectPath, projectId, onClose }: Props) {
  const [sidePanel, setSidePanel] = useState<SidePanel>(null);
  const [hoveredModuleId, setHoveredModuleId] = useState<string | null>(null);
  const [pinnedModuleId, setPinnedModuleId] = useState<string | null>(null);
  const [recentlyUpdated, setRecentlyUpdated] = useState(false);
  const recentlyUpdatedTimer = useRef<ReturnType<typeof setTimeout> | null>(null);

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

  const queryClient = useQueryClient();

  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,
  });

  // 监听 blueprint://changed 事件，路径匹配时刷新数据并高亮提示
  useEffect(() => {
    let unlisten: (() => void) | undefined;
    listen<{ project_path: string }>("blueprint://changed", (event) => {
      const norm = (s: string) => s.replace(/\\/g, "/").replace(/\/$/, "");
      if (norm(event.payload.project_path) !== norm(projectPath)) return;
      queryClient.invalidateQueries({ queryKey: ["blueprint", projectPath] });
      setRecentlyUpdated(true);
      if (recentlyUpdatedTimer.current) clearTimeout(recentlyUpdatedTimer.current);
      recentlyUpdatedTimer.current = setTimeout(() => setRecentlyUpdated(false), 3000);
    }).then((fn) => { unlisten = fn; });
    return () => {
      unlisten?.();
      if (recentlyUpdatedTimer.current) clearTimeout(recentlyUpdatedTimer.current);
    };
  }, [projectPath, queryClient]);

  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>
            <div className="flex items-center gap-2">
              <h2 className="text-sm font-semibold text-gray-800">项目蓝图</h2>
              {recentlyUpdated && (
                <span className="text-[10px] text-purple-600 bg-purple-50 border border-purple-200 px-1.5 py-0.5 rounded-full animate-pulse">
                  ⚡ 刚刚更新
                </span>
              )}
            </div>
            <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 === "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>
            )}
            {/* 总进度条（固定在右侧） */}
            <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}
                  projectId={projectId}
                  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,
  projectId,
  onClose,
}: {
  projectPath: string;
  projectName: string;
  projectId?: string;
  onClose: () => void;
}) {
  const { data: bpStatus, isPending: statusLoading, refetch: refetchStatus } = useQuery<BlueprintStatus>({
    queryKey: ["bp-status-governance", projectPath],
    queryFn: () => getBlueprintStatus(projectPath),
  });

  const { data: buffStatus, refetch: refetchBuff } = useQuery<BuffStatus | null>({
    queryKey: ["buff-status-governance", projectPath],
    queryFn: () => getBuffStatus(projectPath),
    staleTime: 5000,
  });

  const { data: commitStats } = useQuery<CommitStats | null>({
    queryKey: ["commit-stats-governance", projectId],
    queryFn: () => (projectId ? getCommitStats(projectId) : Promise.resolve(null)),
    enabled: !!projectId,
    staleTime: 5000,
  });

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

  const [buffLoading, setBuffLoading] = useState(false);
  const [buffError, setBuffError] = useState<string | null>(null);
  const [buffInitPrompt, setBuffInitPrompt] = useState<string | null>(null);
  const [buffInitCopied, setBuffInitCopied] = useState(false);
  const [packTemplates, setPackTemplates] = useState<TemplateResult[] | 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 handleApplyBuff = async () => {
    if (!projectId) return;
    setBuffLoading(true);
    setBuffError(null);
    setBuffInitPrompt(null);
    setPackTemplates(null);
    try {
      const report = await applyOnboardingPack(projectPath, projectId);
      setPackTemplates(report.templates);
      refetchBuff();
      // 只有尚无蓝图时才展示 init prompt；已有蓝图则 Buff 直接激活无需初始化
      if (bpStatus?.status === "none") {
        const p = await generateBlueprintPrompt(projectPath, "init").catch(() => null);
        if (p) setBuffInitPrompt(p);
      }
    } catch (e) {
      setBuffError(String(e));
    } finally {
      setBuffLoading(false);
    }
  };

  const handleRemoveBuff = async () => {
    setBuffLoading(true);
    setBuffError(null);
    try {
      await removeBlueprintBuff(projectPath);
      refetchBuff();
      setBuffInitPrompt(null);
      setPackTemplates(null);
    } catch (e) {
      setBuffError(String(e));
    } finally {
      setBuffLoading(false);
    }
  };

  const handleCopyBuffPrompt = () => {
    if (!buffInitPrompt) return;
    navigator.clipboard.writeText(buffInitPrompt).then(() => {
      setBuffInitCopied(true);
      setTimeout(() => setBuffInitCopied(false), 2000);
    });
  };

  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>
      )}

      {/* 梦核分析入口 */}
      {/* ── 蓝图 Buff ─────────────────────────────────────── */}
      <div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
        <div className="flex items-center justify-between">
          <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">接入包</p>
          {buffStatus?.status === "active" && (
            <span className="text-[10px] text-purple-600 bg-purple-50 border border-purple-200 px-1.5 py-0.5 rounded-full">⚡ 已激活</span>
          )}
        </div>
        <p className="text-[11px] text-gray-500 leading-relaxed">
          应用接入包：种入 AGENTS.md / lefthook / git-workflow 标准模板（不覆盖已有），并挂 buff 自动监听 git 提交、采集飞轮数据。
        </p>
        {buffStatus?.last_sync_at && (
          <p className="text-[10px] text-gray-400">上次同步：{buffStatus.last_sync_at}</p>
        )}
        {buffError && (
          <p className="text-[11px] text-red-600 font-mono break-all">{buffError}</p>
        )}
        {packTemplates && (
          <div className="space-y-0.5 pt-0.5">
            <p className="text-[10px] text-gray-500 font-semibold">模板分发</p>
            {packTemplates.map((t) => (
              <p key={t.name} className="text-[10px] font-mono flex items-center gap-1">
                <span
                  className={
                    t.action === "written"
                      ? "text-green-600"
                      : t.action === "skipped"
                      ? "text-gray-400"
                      : "text-red-600"
                  }
                >
                  {t.action === "written" ? "✓" : t.action === "skipped" ? "○" : "✗"}
                </span>
                <span className="text-gray-600 truncate flex-1">{t.name}</span>
                <span className="text-gray-400">
                  {t.action === "written" ? "已写入" : t.action === "skipped" ? "已存在" : t.detail}
                </span>
              </p>
            ))}
          </div>
        )}
        {buffStatus?.status === "active" ? (
          <button
            onClick={handleRemoveBuff}
            disabled={buffLoading}
            className="w-full py-1.5 rounded-lg border border-red-200 text-red-600 text-xs hover:bg-red-50 disabled:opacity-50 transition-colors"
          >
            {buffLoading ? "处理中…" : "撤销 Buff"}
          </button>
        ) : (
          <button
            onClick={handleApplyBuff}
            disabled={buffLoading || !projectId}
            title={!projectId ? "需要传入 projectId" : undefined}
            className="w-full py-1.5 rounded-lg bg-purple-600 text-white text-xs hover:bg-purple-700 disabled:opacity-50 transition-colors"
          >
            {buffLoading ? "施加中…" : "⚡ 施加 Buff"}
          </button>
        )}
        {/* 已有蓝图时施加 Buff：直接激活，无需初始化 */}
        {buffStatus?.status === "active" && bpStatus?.status !== "none" && !buffInitPrompt && (
          <p className="text-[10px] text-purple-500 leading-relaxed">
            已激活。炼境将每 5 秒监听 git 提交，自动将命中任务文件的任务卡标记为 🔵 进行中。
          </p>
        )}
        {/* 无蓝图时施加 Buff：展示 init prompt 供初始化 */}
        {buffInitPrompt && (
          <div className="space-y-1.5 pt-1">
            <p className="text-[10px] text-purple-600 font-semibold">将以下提示词交给 Claude 初始化蓝图：</p>
            <textarea
              readOnly
              value={buffInitPrompt}
              rows={8}
              className="w-full font-mono text-[11px] text-gray-700 bg-gray-50 border border-gray-200 rounded-lg p-2 resize-none outline-none"
              onClick={(e) => (e.target as HTMLTextAreaElement).select()}
            />
            <button
              onClick={handleCopyBuffPrompt}
              className="w-full py-1.5 rounded-lg bg-purple-600 text-white text-xs hover:bg-purple-700 transition-colors"
            >
              {buffInitCopied ? "已复制 ✓" : "复制提示词"}
            </button>
          </div>
        )}
        {buffStatus?.status === "active" && commitStats && commitStats.total > 0 && (
          <div className="pt-1.5 border-t border-gray-100 space-y-0.5">
            <p className="text-[10px] text-gray-500">
              commit 规范化 {commitStats.conventional}/{commitStats.total}（
              {Math.round((commitStats.conventional / commitStats.total) * 100)}%） · 返工 {commitStats.rework}
            </p>
            {commitStats.conventional / commitStats.total < 0.6 && (
              <p className="text-[10px] text-amber-600 leading-relaxed">
                ⚠ 规范化偏低，飞轮数据可信度低——种 lefthook（接入包已含）后逐步转准
              </p>
            )}
          </div>
        )}
      </div>

      {/* ── Agent 基础设施（接入包 v2）───────────────────────── */}
      <AgentInfraPanel projectPath={projectPath} projectId={projectId} />

      {/* ── Gitea 集成（统一 git 后端）─────────────────────────── */}
      {projectId && <GiteaPanel projectId={projectId} projectName={projectName} projectPath={projectPath} />}

      {/* 项目信息 */}
      <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>
  );
}

