"use client";

import Link from "next/link";
import { useEffect, useId, useRef, useState, type RefObject } from "react";
import {
  BookMarked,
  ChevronDown,
  ClipboardCheck,
  FileText,
  HelpCircle,
  MessagesSquare,
  PanelLeft,
  PanelRight,
  PenLine,
  RefreshCw,
  Sparkles,
  Square,
  Wand2,
  WrapText,
} from "lucide-react";

import { AppShell } from "@/components/AppShell";
import { Drawer } from "@/components/Drawer";
import { ThinkingIndicator } from "@/components/ThinkingIndicator";
import { useToast } from "@/components/Toast";
import { Button } from "@/components/ui/Button";
import { SectionHeader } from "@/components/ui/SectionHeader";
import { StatusNote } from "@/components/ui/StatusNote";
import { TextArea } from "@/components/ui/TextArea";
import type { ProjectResponse } from "@/lib/api/types";
import { friendlyError } from "@/lib/errors/messages";
import { useAutosave } from "@/lib/autosave/useAutosave";
import { useDraftStream } from "@/lib/stream/useDraftStream";
import {
  WORKBENCH_CHAPTER_NO,
  type ChapterEntry,
} from "@/lib/workbench/chapter";
import {
  composeDirective,
  PLOT_PRESETS,
  STYLE_PRESETS,
  type Preset,
} from "@/lib/workbench/directive";
import { applyRefinement } from "@/lib/workbench/refineApply";
import { buttonClass, focusRing } from "@/lib/ui/variants";
import { ChapterList, ChapterListContent } from "./ChapterList";
import { ChapterAssistant, AssistantContent } from "./ChapterAssistant";
import { Editor, type EditorSelection } from "./Editor";
import { RefinePanel } from "./RefinePanel";
import { ContinuePanel } from "./ContinuePanel";
import { InlineToolbox } from "./InlineToolbox";
import { ChapterRewritePanel } from "./ChapterRewritePanel";
import { ContextDrawer, CONTEXT_DRAWER_ENABLED } from "./ContextDrawer";
import {
  AiConversationDrawer,
  AI_CONVERSATION_ENABLED,
} from "./AiConversationDrawer";
import { GenrePicker } from "./GenrePicker";
import { GENRES } from "@/lib/wizard/wizard";
import { useGenreGate, toGenreDirective } from "@/lib/workbench/useGenreGate";
import { useAiConversation } from "@/lib/workbench/useAiConversation";

interface WorkbenchProps {
  project: ProjectResponse;
  // 当前章号（来自写作路由 `?chapter=N`）；缺省回退第 1 章。
  chapterNo?: number;
  // RSC 种入的已存草稿正文（GET .../draft）；无草稿（404）→ ""（空编辑器）。
  initialText?: string;
  // 大纲章节目录（GET .../outline）；无大纲 → 空数组（目录回退到仅当前章 + 去大纲提示）。
  chapters?: ChapterEntry[];
}

// 移动端右栏（目录/助手）经抽屉触达；桌面用三栏栅格。
type MobilePanel = "chapters" | "assistant" | null;

// M1：单章工作台。章节列表为占位（无章节列表端点）；默认第 1 章。
// WORKBENCH_CHAPTER_NO 移到 lib/workbench/chapter.ts（非客户端模块），供 Server Component 安全导入。

export function Workbench({
  project,
  chapterNo = WORKBENCH_CHAPTER_NO,
  initialText = "",
  chapters = [],
}: WorkbenchProps) {
  // 用已存草稿作初值；流式开始后由下方 effect 覆盖（流不会被初值反扑——
  // stream.state.text 起始为空，仅当 streaming/done/aborted 且变化才写回）。
  const [text, setText] = useState(initialText);
  const [mobilePanel, setMobilePanel] = useState<MobilePanel>(null);
  // 本章指令（T4-b）：自由文本 + 风格预设 → 写本章时组装传入 draft 生成。
  const [directive, setDirective] = useState("");
  const [presetIds, setPresetIds] = useState<string[]>([]);
  // 选区级 AI 动作（润色/再沟通）：编辑器上报的最新选区 + 结果卡开合。
  const [selection, setSelection] = useState<EditorSelection | null>(null);
  const [refineOpen, setRefineOpen] = useState(false);
  const [continueOpen, setContinueOpen] = useState(false);
  const [toolboxOpen, setToolboxOpen] = useState(false);
  // WFW-8：整章「再沟通/重写」结果卡开合（意见 → 流式重写一版 → 版本栈 → 接受替换整章）。
  const [rewriteOpen, setRewriteOpen] = useState(false);
  // WFW-7 / P1-5：首次写章的极轻 genre 采集门。项目无题材 → 正文上方常驻一条内联「题材」选择条
  // （非弹窗），作者点一个题材再写；仅临时并入本次指令、不落库，避免无题材 slop，且第一次发送不被打断。
  const genreGate = useGenreGate(project.genre ?? null);
  // WFW-6 上下文速查抽屉：加法式、flag 灰度；与三张 AI 结果卡互不干扰。
  const [contextOpen, setContextOpen] = useState(false);
  // AC-3 AI 对话抽屉：Workbench 级单实例（append 即使抽屉关着也能触发）；flag 灰度。
  const [conversationOpen, setConversationOpen] = useState(false);
  const conversation = useAiConversation(project.id, chapterNo);
  const conversationTriggerRef = useRef<HTMLButtonElement>(null);
  const toast = useToast();
  const autosave = useAutosave(project.id, chapterNo, initialText);
  const stream = useDraftStream();
  const lastStreamText = useRef("");
  const canRefine = selection !== null && selection.text.trim().length > 0;
  const chapterTriggerRef = useRef<HTMLButtonElement>(null);
  const assistantTriggerRef = useRef<HTMLButtonElement>(null);
  const contextTriggerRef = useRef<HTMLButtonElement>(null);

  // 流式 token 累积进编辑器（打字机）；停止/结束后已生成部分留在草稿。
  useEffect(() => {
    const streamed = stream.state.text;
    if (
      (stream.state.phase === "streaming" ||
        stream.state.phase === "done" ||
        stream.state.phase === "aborted") &&
      streamed !== lastStreamText.current
    ) {
      lastStreamText.current = streamed;
      setText(streamed);
      autosave.onChange(streamed);
    }
  }, [stream.state.phase, stream.state.text, autosave]);

  // 题材 + 三槽指令合并为一条写章指令（题材前置）；effectiveGenre 无 → 仅原指令。
  const startWrite = (effectiveGenre: string | null): void => {
    const base = composeDirective(directive, presetIds);
    const merged = [toGenreDirective(effectiveGenre), base]
      .filter((part) => part.length > 0)
      .join("；");
    void stream.start(project.id, chapterNo, merged);
  };

  const onWrite = (): void => {
    // 无题材且未点选 → 引导先点上方常驻「题材」条再写（HITL：不静默生成无题材 slop）。
    // 选择条一直可见，理想路径是发送前已选好，第一次「写本章」即顺滑流式、不被弹窗打断。
    if (genreGate.needsGenre && genreGate.chosenGenre === null) {
      toast("先在上方选个题材，AI 写得更贴合、少套话。", "info");
      return;
    }
    startWrite(genreGate.effectiveGenre);
  };

  const togglePreset = (id: string): void => {
    setPresetIds((prev) =>
      prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
    );
  };

  const onEditorChange = (value: string): void => {
    setText(value);
    autosave.onChange(value);
  };

  // 接受润色/再沟通产出：按内容重锚把选段替换为产出并落草稿；原文已变则提示重选（不盲替换）。
  const onAcceptRefine = (refined: string): void => {
    if (selection) {
      const next = applyRefinement(text, selection.text, refined, {
        start: selection.start,
        end: selection.end,
      });
      if (next === null) {
        toast("正文已改动，找不到原选段，请重新选择后再润色。", "error");
      } else {
        setText(next);
        autosave.onChange(next);
      }
    }
    setRefineOpen(false);
    setSelection(null);
  };

  // 四张 AI 结果卡（润色/续写/工具箱/整章重写）互斥，避免同时占位。
  // 润色以选区为对象：无选区时不静默失败，给出可见提示引导先选段（P0-4，替代 hover-only title）。
  const openRefine = (): void => {
    if (!canRefine) {
      toast("先在正文里选中一段再润色", "info");
      return;
    }
    setContinueOpen(false);
    setToolboxOpen(false);
    setRewriteOpen(false);
    setRefineOpen(true);
  };

  // 续写前先 flush 自动保存，确保 continue 读到的是最新草稿（with_prior_chapter 读库）。
  const openContinue = (): void => {
    autosave.flush();
    setRefineOpen(false);
    setToolboxOpen(false);
    setRewriteOpen(false);
    setContinueOpen(true);
  };

  const openToolbox = (): void => {
    setRefineOpen(false);
    setContinueOpen(false);
    setRewriteOpen(false);
    setToolboxOpen(true);
  };

  const openRewrite = (): void => {
    setRefineOpen(false);
    setContinueOpen(false);
    setToolboxOpen(false);
    setRewriteOpen(true);
  };

  // 接受某一版整章重写 → 替换整章正文并落草稿（HITL，作者点选才落）。
  const onAcceptRewrite = (next: string): void => {
    setText(next);
    autosave.onChange(next);
    setRewriteOpen(false);
  };

  // 追加一段文本到章末并落草稿（续写候选 / 工具箱产物共用）。
  const appendToDraft = (chunk: string): void => {
    const base = text.trimEnd();
    const next = base.length > 0 ? `${base}\n\n${chunk}` : chunk;
    setText(next);
    autosave.onChange(next);
  };

  const onInsertContinuation = (candidate: string): void => {
    appendToDraft(candidate);
    setContinueOpen(false);
  };

  // 从「AI 对话」历史再接受（复用 DRAFT-only HITL：只回填编辑器草稿，不绕开验收事务——不变量 #3）。
  // 返回 true=成功则抽屉自关，露出改动后的编辑器 + 成功 toast；refine 找不到原选段返回 false + 漂移提示。
  const onReplaceChapterFromHistory = (content: string): boolean => {
    setText(content);
    autosave.onChange(content);
    toast("已替换整章。", "success");
    return true;
  };

  const onAppendDraftFromHistory = (content: string): boolean => {
    appendToDraft(content);
    toast("已加到章末。", "success");
    return true;
  };

  const onRefillSegmentFromHistory = (
    segment: string,
    refined: string,
  ): boolean => {
    // 合成零区间强制 slice 校验失败 → 走 indexOf 按存档的原选段重锚；找不到则不盲替换。
    const next = applyRefinement(text, segment, refined, { start: 0, end: 0 });
    if (next === null) {
      toast("正文已改动，找不到原选段，无法替换。", "error");
      return false;
    }
    setText(next);
    autosave.onChange(next);
    toast("已替换这段。", "success");
    return true;
  };

  // 字数统计非空白字符（与中文读者直觉一致：标点/空格不计入正文体量）。
  const wordCount = text.replace(/\s+/g, "").length;
  const closePanel = (): void => setMobilePanel(null);

  // 流式里程碑播报（仅终结句，非逐 token）：完成/停止/失败时写入 role=status 区。
  const liveMessage = ((): string => {
    switch (stream.state.phase) {
      case "done":
        return `本章生成完成，共 ${wordCount} 字`;
      case "aborted":
        return "已停止";
      case "error":
        return stream.state.error
          ? friendlyError(stream.state.error.code, stream.state.error.message)
              .text
          : "本章生成失败，请稍后重试。";
      default:
        return "";
    }
  })();

  return (
    <AppShell
      title={`〈${project.title}〉`}
      subtitle={`第 ${chapterNo} 章`}
      projectId={project.id}
      activeNav="write"
    >
      <div className="grid min-h-[calc(100vh-var(--chrome,4rem))] grid-cols-1 xl:h-[calc(100vh-var(--chrome,4rem))] xl:grid-cols-[12rem_1fr_18rem]">
        <ChapterList
          projectId={project.id}
          chapters={chapters}
          currentChapterNo={chapterNo}
        />

        <section className="flex min-w-0 flex-col bg-bg">
          <MobileContextBar
            chapterNo={chapterNo}
            onOpenChapters={() => setMobilePanel("chapters")}
            onOpenAssistant={() => setMobilePanel("assistant")}
            chapterTriggerRef={chapterTriggerRef}
            assistantTriggerRef={assistantTriggerRef}
          />
          {genreGate.needsGenre ? (
            // 项目无题材：正文上方常驻内联题材条（非弹窗），发送前选好即顺滑写章（P1-5）。
            <GenrePicker
              genres={GENRES}
              value={genreGate.chosenGenre}
              onPick={genreGate.setChosenGenre}
            />
          ) : null}
          <DirectivePanel
            directive={directive}
            onDirectiveChange={setDirective}
            presetIds={presetIds}
            onTogglePreset={togglePreset}
            defaultOpen={initialText.trim().length === 0}
            streaming={stream.isStreaming}
            onWrite={onWrite}
            onStop={stream.stop}
          />
          <AiVerbsRow
            streaming={stream.isStreaming}
            onContinue={openContinue}
            onRefineSelection={openRefine}
            onRewrite={openRewrite}
            onToolbox={openToolbox}
          />
          {refineOpen && canRefine && selection ? (
            <RefinePanel
              projectId={project.id}
              chapterNo={chapterNo}
              original={selection.text}
              onAccept={onAcceptRefine}
              onReaccept={onRefillSegmentFromHistory}
              conversation={conversation}
              onClose={() => setRefineOpen(false)}
            />
          ) : null}
          {continueOpen ? (
            <ContinuePanel
              projectId={project.id}
              chapterNo={chapterNo}
              onInsert={onInsertContinuation}
              appendMessages={conversation.appendMessages}
              onClose={() => setContinueOpen(false)}
            />
          ) : null}
          {toolboxOpen ? (
            <InlineToolbox
              projectId={project.id}
              chapterNo={chapterNo}
              manuscriptText={text}
              appendMessages={conversation.appendMessages}
              onInsertText={(chunk) => {
                appendToDraft(chunk);
                setToolboxOpen(false);
              }}
              onClose={() => setToolboxOpen(false)}
            />
          ) : null}
          {rewriteOpen ? (
            <ChapterRewritePanel
              projectId={project.id}
              chapterNo={chapterNo}
              currentDraft={text}
              onAccept={onAcceptRewrite}
              conversation={conversation}
              onClose={() => setRewriteOpen(false)}
            />
          ) : null}
          <div className="flex-1 overflow-auto px-6 py-8">
            {chapters.length === 0 ? (
              <StatusNote variant="info" className="mb-4">
                尚无大纲，将按设定自由生成。
              </StatusNote>
            ) : null}
            <Editor
              value={text}
              onChange={onEditorChange}
              streaming={stream.isStreaming}
              onSelectionChange={setSelection}
            />
          </div>
          <Toolbar
            projectId={project.id}
            chapterNo={chapterNo}
            wordCount={wordCount}
            savedLabel={autosave.savedLabel}
            saveStatus={autosave.status}
            streaming={stream.isStreaming}
            streamError={stream.state.error}
            liveMessage={liveMessage}
            onStop={stream.stop}
            onOpenContext={() => setContextOpen(true)}
            contextTriggerRef={contextTriggerRef}
            onOpenConversation={() => setConversationOpen(true)}
            conversationTriggerRef={conversationTriggerRef}
          />
        </section>

        <ChapterAssistant projectId={project.id} chapterNo={chapterNo} />
      </div>

      {/* 窄屏 / 中屏(1024–1280)：目录 / 本章助手经抽屉触达；≥xl 已在三栏栅格里直显。 */}
      <Drawer
        open={mobilePanel === "chapters"}
        onClose={closePanel}
        side="left"
        label="目录"
        triggerRef={chapterTriggerRef}
      >
        <ChapterListContent
          projectId={project.id}
          chapters={chapters}
          currentChapterNo={chapterNo}
        />
      </Drawer>
      <Drawer
        open={mobilePanel === "assistant"}
        onClose={closePanel}
        side="right"
        label="本章参考"
        triggerRef={assistantTriggerRef}
      >
        <AssistantContent projectId={project.id} chapterNo={chapterNo} />
      </Drawer>

      {/* WFW-6 上下文速查：视口无关的右侧 slide-over；旧侧栏/全宽页路由保留、可回滚。 */}
      <ContextDrawer
        projectId={project.id}
        open={contextOpen}
        onClose={() => setContextOpen(false)}
        triggerRef={contextTriggerRef}
      />

      {/* AC-3 AI 对话：右侧 slide-over 气泡视图；从历史再接受只回填草稿，不绕验收事务。flag 灰度。 */}
      {AI_CONVERSATION_ENABLED ? (
        <AiConversationDrawer
          open={conversationOpen}
          onClose={() => setConversationOpen(false)}
          conversation={conversation}
          currentChapterNo={chapterNo}
          onReplaceChapter={onReplaceChapterFromHistory}
          onAppendDraft={onAppendDraftFromHistory}
          onRefillSegment={onRefillSegmentFromHistory}
          triggerRef={conversationTriggerRef}
        />
      ) : null}
    </AppShell>
  );
}

interface MobileContextBarProps {
  chapterNo: number;
  onOpenChapters: () => void;
  onOpenAssistant: () => void;
  chapterTriggerRef: RefObject<HTMLButtonElement | null>;
  assistantTriggerRef: RefObject<HTMLButtonElement | null>;
}

function MobileContextBar({
  chapterNo,
  onOpenChapters,
  onOpenAssistant,
  chapterTriggerRef,
  assistantTriggerRef,
}: MobileContextBarProps) {
  return (
    <div className="flex items-center justify-between gap-2 border-b border-line bg-panel px-4 py-2 xl:hidden">
      <Button
        ref={chapterTriggerRef}
        onClick={onOpenChapters}
        variant="secondary"
        size="sm"
      >
        <PanelLeft className="h-4 w-4" aria-hidden="true" />
        目录
      </Button>
      <span className="min-w-0 truncate font-mono text-xs text-ink-soft">
        第 {chapterNo} 章
      </span>
      <Button
        ref={assistantTriggerRef}
        onClick={onOpenAssistant}
        variant="secondary"
        size="sm"
      >
        <PanelRight className="h-4 w-4" aria-hidden="true" />
        助手
      </Button>
    </div>
  );
}

interface DirectivePanelProps {
  directive: string;
  onDirectiveChange: (value: string) => void;
  presetIds: readonly string[];
  onTogglePreset: (id: string) => void;
  // 空章默认展开（新手最需要看到输入框的时机）；满章默认收起以让出编辑视野。
  defaultOpen: boolean;
  // 发送键 = 写本章：把已选文风/剧情 + 自定义要求组装后逐字流式写进正文（不改成聊天气泡）。
  streaming: boolean;
  onWrite: () => void;
  onStop: () => void;
}

// 本章写作指令面板（P0-2 提升）：正文正上方常驻的「和 AI 说话」输入条。
// 标题「告诉 AI 这章想怎么写」+ HITL 微文案 +「?这是什么」常驻可见；空章默认展开露出输入框。
// 发送键即「写本章」，沿用 composeDirective + 文风/剧情 chips + 流式打字机写进正文。
function DirectivePanel({
  directive,
  onDirectiveChange,
  presetIds,
  onTogglePreset,
  defaultOpen,
  streaming,
  onWrite,
  onStop,
}: DirectivePanelProps) {
  const panelId = useId();
  const [open, setOpen] = useState(defaultOpen);
  const activeCount = presetIds.length + (directive.trim().length > 0 ? 1 : 0);
  return (
    <section className="border-b border-line bg-panel px-4 py-3 sm:px-6">
      <div className="flex items-center justify-between gap-3">
        <button
          type="button"
          onClick={() => setOpen((v) => !v)}
          aria-expanded={open}
          aria-controls={panelId}
          className={`flex min-w-0 cursor-pointer items-center gap-2 rounded font-serif text-base text-ink hover:text-cinnabar ${focusRing}`}
        >
          <ChevronDown
            className={`h-4 w-4 shrink-0 text-ink-soft transition-transform ${
              open ? "rotate-180" : ""
            }`}
            aria-hidden="true"
          />
          <span className="truncate">告诉 AI 这章想怎么写</span>
        </button>
        <div className="flex shrink-0 items-center gap-2">
          {activeCount > 0 ? (
            <span className="rounded border border-line bg-bg px-2 py-0.5 text-xs text-ink-soft">
              {activeCount} 项指令
            </span>
          ) : null}
          {/* 常驻「发送键」= 写本章：始终可见（折叠时也在），逐字流式写进正文。带可见范围提示（P1-4）。 */}
          {streaming ? (
            <Button onClick={onStop} variant="danger" size="sm">
              <Square className="h-4 w-4" aria-hidden="true" />
              停
            </Button>
          ) : (
            <Button onClick={onWrite} variant="primary" size="sm">
              <PenLine className="h-4 w-4" aria-hidden="true" />
              写本章
              <span className="text-2xs font-normal text-panel/75">
                整章·从头写
              </span>
            </Button>
          )}
          <HitlHelp />
        </div>
      </div>
      <p className="mt-1 text-xs text-ink-soft">
        AI 给草稿，你决定采不采用，原文永远你说了算。
      </p>
      {open ? (
        <div id={panelId} className="mt-3 space-y-3">
          <SectionHeader
            title="你的写作要求"
            description="选文风、点剧情需求、写自己的要求，只影响本次生成。"
          />
          <PresetGroup
            label="文风"
            presets={STYLE_PRESETS}
            presetIds={presetIds}
            onToggle={onTogglePreset}
          />
          <PresetGroup
            label="剧情需求"
            presets={PLOT_PRESETS}
            presetIds={presetIds}
            onToggle={onTogglePreset}
          />
          <label className="block">
            <span className="mb-1 block text-sm text-ink-soft">自己的要求</span>
            <TextArea
              value={directive}
              onChange={(e) => onDirectiveChange(e.target.value)}
              rows={2}
              placeholder="写点要求，AI 更懂你（留空则按大纲写）"
            />
          </label>
          {activeCount > 0 ? (
            <StatusNote variant="info">
              写本章时会把已选文风/剧情预设和你的要求合并进本次生成请求。
            </StatusNote>
          ) : null}
        </div>
      ) : null}
    </section>
  );
}

interface AiVerbsRowProps {
  streaming: boolean;
  onContinue: () => void;
  onRefineSelection: () => void;
  onRewrite: () => void;
  onToolbox: () => void;
}

// 「或让 AI:」次级动作行（P1-1）：常驻贴在中央输入条下方，把续写/润色/整章重写/工具箱
// 归为「同一个 AI 的其它用法」（act-on-chapter 与导航分家）。每个动词带可见范围提示（P1-4）。
// 生成中禁用（避免与流式写章并发），触发逻辑沿用 Workbench 既有 open* 回调（仅搬家、不改行为）。
function AiVerbsRow({
  streaming,
  onContinue,
  onRefineSelection,
  onRewrite,
  onToolbox,
}: AiVerbsRowProps) {
  return (
    <div className="flex flex-wrap items-center gap-x-3 gap-y-2 border-b border-line bg-panel px-4 py-2 sm:px-6">
      <span className="text-xs text-ink-soft">或让 AI：</span>
      <VerbButton
        icon={WrapText}
        label="续写"
        hint="接着往下写"
        disabled={streaming}
        onClick={onContinue}
      />
      <VerbButton
        icon={Wand2}
        label="润色选段"
        hint="选中段·打磨"
        disabled={streaming}
        onClick={onRefineSelection}
      />
      <VerbButton
        icon={RefreshCw}
        label="整章重写"
        hint="整章·基于现有重写"
        disabled={streaming}
        onClick={onRewrite}
      />
      <VerbButton
        icon={Sparkles}
        label="工具箱"
        disabled={streaming}
        onClick={onToolbox}
      />
    </div>
  );
}

interface VerbButtonProps {
  icon: typeof WrapText;
  label: string;
  // 可见（非 hover-only）范围提示：让作者点前就能预判对草稿的影响（P1-4）。工具箱无范围提示。
  hint?: string;
  disabled: boolean;
  onClick: () => void;
}

function VerbButton({ icon: Icon, label, hint, disabled, onClick }: VerbButtonProps) {
  return (
    <Button onClick={onClick} disabled={disabled} variant="secondary" size="sm">
      <Icon className="h-4 w-4" aria-hidden="true" />
      {label}
      {hint ? <span className="text-2xs text-ink-soft">{hint}</span> : null}
    </Button>
  );
}

// 「?这是什么」：常驻、可再发现的说明（非一次性 coach）——解释「和 AI 说话」的闭环。
// 仿 AiToolbarMoreMenu：按钮 + 轻量弹层，Esc / 点击外部关闭，还原焦点。
function HitlHelp() {
  const [open, setOpen] = useState(false);
  const popId = useId();
  const rootRef = useRef<HTMLDivElement>(null);
  const buttonRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    if (!open) return;
    const onKeyDown = (e: KeyboardEvent): void => {
      if (e.key === "Escape") {
        setOpen(false);
        buttonRef.current?.focus();
      }
    };
    const onPointerDown = (e: PointerEvent): void => {
      const target = e.target;
      if (target instanceof Node && !rootRef.current?.contains(target)) {
        setOpen(false);
      }
    };
    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("pointerdown", onPointerDown);
    return () => {
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("pointerdown", onPointerDown);
    };
  }, [open]);

  return (
    <div ref={rootRef} className="relative">
      <button
        ref={buttonRef}
        type="button"
        aria-haspopup="dialog"
        aria-expanded={open}
        aria-controls={popId}
        onClick={() => setOpen((v) => !v)}
        className={buttonClass({
          variant: "ghost",
          size: "sm",
          className: "gap-1 text-xs text-ink-soft",
        })}
      >
        <HelpCircle className="h-4 w-4" aria-hidden="true" />
        这是什么
      </button>
      {open ? (
        <div
          id={popId}
          role="dialog"
          aria-label="怎么和 AI 说话"
          className="absolute right-0 z-30 mt-2 w-72 max-w-[80vw] rounded border border-line bg-panel p-3 text-sm text-ink-soft shadow-paper"
        >
          <p className="mb-1 font-serif text-sm text-ink">怎么和 AI 说话</p>
          <p>
            在这条写下要求，点「写本章」——AI 会把草稿逐字写进正文。草稿随时能改；
            满意了去「审稿」验收。原文永远你说了算。
          </p>
        </div>
      ) : null}
    </div>
  );
}

interface PresetGroupProps {
  label: string;
  presets: readonly Preset[];
  presetIds: readonly string[];
  onToggle: (id: string) => void;
}

// 一组预设 chips（文风 / 剧情需求）：点选 toggle，选中高亮。id 统一进 presetIds。
function PresetGroup({ label, presets, presetIds, onToggle }: PresetGroupProps) {
  return (
    <div>
      <p className="mb-1 text-sm text-ink-soft">{label}</p>
      <div className="flex flex-wrap gap-2" role="group" aria-label={label}>
        {presets.map((preset) => {
          const active = presetIds.includes(preset.id);
          return (
            <Button
              key={preset.id}
              type="button"
              aria-pressed={active}
              onClick={() => onToggle(preset.id)}
              variant={active ? "outline" : "secondary"}
              size="sm"
            >
              {preset.label}
            </Button>
          );
        })}
      </div>
    </div>
  );
}

// 写章失败提示：友好文案（不暴露 raw code）+ 可选「去设置」动作（F4）。
function StreamErrorNote({
  streamError,
}: {
  streamError: { code: string; message: string };
}) {
  const friendly = friendlyError(streamError.code, streamError.message);
  return (
    <p className="mb-2 text-sm text-conflict">
      {friendly.text}
      {friendly.actionHref ? (
        <>
          {" "}
          <Link
            href={friendly.actionHref}
            className="underline hover:text-cinnabar"
          >
            {friendly.actionLabel}
          </Link>
        </>
      ) : null}
    </p>
  );
}

// 底栏瘦身（P1-1）：act-on-chapter 动词与纯导航都已上移/移除，底栏只余
// 状态（字数/保存/播报）+ 流式时的「停」+ 唯一强调的「审稿」前进 CTA + 速查/对话记录抽屉入口。
interface ToolbarProps {
  projectId: string;
  chapterNo: number;
  wordCount: number;
  savedLabel: string | null;
  saveStatus: ReturnType<typeof useAutosave>["status"];
  streaming: boolean;
  streamError: { code: string; message: string } | null;
  // 流式里程碑播报句（完成/停止/失败）；仅在 role=status 区出现，不逐 token 刷新。
  liveMessage: string;
  // 流式时的「停」：底栏 sticky，滚到长正文下方也够得着（中央条会滚出视野）。
  onStop: () => void;
  // 打开上下文速查抽屉（WFW-6）。
  onOpenContext: () => void;
  contextTriggerRef: RefObject<HTMLButtonElement | null>;
  // 打开对话记录抽屉（AC-3）。
  onOpenConversation: () => void;
  conversationTriggerRef: RefObject<HTMLButtonElement | null>;
}

function Toolbar({
  projectId,
  chapterNo,
  wordCount,
  savedLabel,
  saveStatus,
  streaming,
  streamError,
  liveMessage,
  onStop,
  onOpenContext,
  contextTriggerRef,
  onOpenConversation,
  conversationTriggerRef,
}: ToolbarProps) {
  return (
    <div className="sticky bottom-0 z-10 border-t border-line bg-panel/95 px-4 py-2 backdrop-blur sm:px-6 sm:py-3">
      {streamError ? <StreamErrorNote streamError={streamError} /> : null}
      <div className="grid gap-2 sm:flex sm:flex-wrap sm:items-center sm:gap-3">
        <div className="flex min-w-0 flex-wrap items-center gap-2">
        {streaming ? (
          <Button onClick={onStop} variant="danger" size="sm">
            <Square className="h-4 w-4" aria-hidden="true" />
            停
          </Button>
        ) : null}
        {streaming ? (
          // ThinkingIndicator 自带 role=status，开始时播报一次「生成中」；
          // 易变字数 aria-hidden，仅供视觉，不逐 token 打扰屏读。
          <span className="inline-flex items-center gap-2">
            <ThinkingIndicator label="生成中" className="text-xs text-cinnabar" />
            <span
              aria-hidden="true"
              className="font-mono text-xs text-cinnabar"
            >
              {wordCount} 字
            </span>
          </span>
        ) : (
          <span className="font-mono text-xs text-ink-soft">
            字数 {wordCount}
          </span>
        )}
        {/* 终结句播报区：完成/停止/失败时写入，屏读只播报里程碑。 */}
        <span className="sr-only" role="status" aria-live="polite">
          {liveMessage}
        </span>
        {CONTEXT_DRAWER_ENABLED ? (
          <Button
            ref={contextTriggerRef}
            onClick={onOpenContext}
            variant="secondary"
            size="sm"
            title="打开上下文速查（设定库/大纲/伏笔/规则/文风）"
          >
            <BookMarked className="h-4 w-4" aria-hidden="true" />
            速查
          </Button>
        ) : null}
        {AI_CONVERSATION_ENABLED ? (
          <Button
            ref={conversationTriggerRef}
            onClick={onOpenConversation}
            variant="secondary"
            size="sm"
            title="打开对话记录（润色/重写/续写/工具箱往复留痕的只读日志）"
          >
            <MessagesSquare className="h-4 w-4" aria-hidden="true" />
            对话记录
          </Button>
        ) : null}
        {/* 唯一强调的「审稿」= 写→审→验收的前进 CTA（导航去重后仅此一处强调，左栏另有入口）。 */}
        <Link
          href={`/projects/${projectId}/review?chapter=${chapterNo}`}
          className={buttonClass({ variant: "outline", size: "sm" })}
        >
          <ClipboardCheck className="h-4 w-4" aria-hidden="true" />
          审稿
        </Link>
        </div>
        <span
          role="status"
          aria-live="polite"
          className="min-w-0 font-mono text-xs text-ink-soft sm:ml-auto"
        >
          <FileText className="mr-1 inline h-3.5 w-3.5" aria-hidden="true" />
          {saveStatus === "saving"
            ? "保存中…"
            : saveStatus === "error"
              ? "保存失败"
              : (savedLabel ?? "尚未保存")}
        </span>
      </div>
    </div>
  );
}
