import { useState, useEffect, useMemo, useRef } from "react";
import { useParams, useNavigate } from "react-router";
import {
  Plus, Wand2, Play, Trash2,
  Loader2, ArrowRight, Layers, Download, X,
  ChevronDown, ChevronUp, ChevronRight, Volume2, VolumeX,
  RefreshCw, Image as ImageIcon, Info, Check, Sparkles,
  MoreHorizontal,
} from "lucide-react";
import {
  useStoryboards,
  useCreateStoryboard,
  useGenerateStoryboards,
  useDeleteStoryboard,
  useAssemblyTask,
  useStartAssembly,
  useGenerateStructuredVideo,
  useVideoTasks,
  useCharacters,
  useScenes,
  useEpisodes,
  useSaveCharacter,
  useGenerateCharacterImage,
} from "../../hooks/useAi";
import type { Storyboard, Character, Scene, Episode, AiTask } from "../../lib/api/ai";
import { useOperationCost } from "../../hooks/useUsage";

const DURATION_OPTIONS = [5, 10, 15, 20, 30];
const RATIO_OPTIONS = [
  { value: "16:9", label: "16:9 横屏" },
  { value: "9:16", label: "9:16 竖屏" },
  { value: "1:1", label: "1:1 方形" },
];
const VIDEO_MODEL_OPTIONS = [
  { value: "doubao-seedance-2-0-fast-260128", label: "doubao-seedance-2-0-fast" },
  { value: "doubao-seedance-2-0-260128", label: "doubao-seedance-2-0" },
];
const DEFAULT_VIDEO_MODEL = VIDEO_MODEL_OPTIONS[0].value;
const MAX_CHARACTERS = 5;
const MAX_PROPS = 5;
const PROMPT_PREVIEW_LIMIT = 80;

// 分镜编辑器草稿存到 localStorage，按 (project, episode, storyboard) 分 key。
// 跨页面切换/刷新都不会丢；展开分镜时优先用草稿，没草稿才回退到上一次任务。
// v2: duration 默认 5s（之前 15s），audioOn 默认 true（之前 false）。bump 版本号让旧草稿失效。
const SB_DRAFT_KEY_PREFIX = "yaoai:sb-draft:v2";
const sbDraftKey = (pid: string, eid: string, sbId: string) =>
  `${SB_DRAFT_KEY_PREFIX}:${pid}:${eid}:${sbId}`;

interface SbDraft {
  shortDescription: string;
  characterKeys: string[];
  sceneKey: string;
  propKeys: string[];
  freeformPrompt: string;
  selectedDuration: number;
  selectedRatio: string;
  selectedModel: string;
  audioOn: boolean;
}

function loadSbDraft(key: string): SbDraft | null {
  try {
    const raw = localStorage.getItem(key);
    return raw ? (JSON.parse(raw) as SbDraft) : null;
  } catch {
    return null;
  }
}

function saveSbDraft(key: string, d: SbDraft) {
  try {
    localStorage.setItem(key, JSON.stringify(d));
  } catch {
    // quota / private mode — 静默失败
  }
}
const FOCUS_RING = "focus:outline-none focus:ring-2 focus:ring-[#1c64ff]/20 focus:border-[#1c64ff]/45";
const CONTROL_SURFACE = "border border-slate-200/80 bg-white shadow-[0_1px_2px_rgba(15,23,42,0.04)]";
const SOFT_PANEL = "border border-slate-200/80 bg-white/95 shadow-[0_8px_24px_rgba(15,23,42,0.05)]";

const FREEFORM_PLACEHOLDER = `【镜头1】 时长2s | 蒙太奇快切 | 固定机位快速切出
画面定格在角色刚转头的瞬间，背景虚化为暖色光斑

【镜头2】 时长3s | 推近 | 缓慢推到面部特写
角色微微抬眼，眼神坚定，发丝随风轻拂

【镜头3】 时长2s | 拉远 | 从面部拉到全景
镜头快速拉远，呈现完整环境氛围`;

function characterFrontKey(c: Character): string | null {
  return c.frontImageTosKey ?? c.imageTosKey ?? null;
}
function characterFrontUrl(c: Character): string | null {
  return c.frontImageUrl ?? c.imageUrl ?? null;
}

function refTokens(value?: string | null): string[] {
  if (!value) return [];
  return value.split(",").map((token) => token.trim()).filter(Boolean);
}

function inferSceneRefFromCharacters(characters: string | null | undefined, scenes: Scene[]): string | null {
  const sceneRefs = new Set(
    scenes
      .map((scene) => scene.name?.trim())
      .filter(Boolean)
      .map((name) => `@${name}`)
  );
  return refTokens(characters).find((token) => sceneRefs.has(token)) ?? null;
}

export function StoryboardWorkspace() {
  const { episodeId: urlEpisodeId, projectId } = useParams<{ projectId: string; episodeId?: string }>();
  const navigate = useNavigate();
  const pid = projectId ?? "";

  const { data: episodes = [], isLoading: episodesLoading } = useEpisodes(pid);
  const [activeEpisodeId, setActiveEpisodeId] = useState<string>("");

  useEffect(() => {
    if (urlEpisodeId) {
      setActiveEpisodeId(urlEpisodeId);
    } else if (episodes.length > 0 && !activeEpisodeId) {
      setActiveEpisodeId(episodes[0].id);
    }
  }, [urlEpisodeId, episodes.length]);

  const eid = activeEpisodeId;

  const { data: storyboards = [], isLoading } = useStoryboards(pid, eid);
  const { data: videoTasks = [], isFetched: videoTasksFetched } = useVideoTasks(pid);
  const { data: characters = [] } = useCharacters(pid);
  const { data: scenes = [] } = useScenes(pid);
  const { data: assemblyTask } = useAssemblyTask(pid, eid);
  const generateSbs = useGenerateStoryboards(pid);
  const createSb = useCreateStoryboard(pid, eid);
  const deleteSb = useDeleteStoryboard(pid, eid);
  const generateStructured = useGenerateStructuredVideo(pid);
  const startAssembly = useStartAssembly(pid, eid);
  const videoCredits = useOperationCost("video_generate");

  // ─── Editor state (bound to currently expanded card) ───
  const [expandedId, setExpandedId] = useState<string | null>(null);
  const [characterKeys, setCharacterKeys] = useState<string[]>([]);
  const [sceneKey, setSceneKey] = useState<string>("");
  const [propKeys, setPropKeys] = useState<string[]>([]);
  const [styleKey] = useState<string>("");  // reserved; not exposed in this UI
  const [shortDescription, setShortDescription] = useState<string>("");
  const [freeformPrompt, setFreeformPrompt] = useState<string>("");
  const [selectedDuration, setSelectedDuration] = useState(5);
  const [selectedRatio, setSelectedRatio] = useState<string>("16:9");
  const [selectedModel, setSelectedModel] = useState<string>(DEFAULT_VIDEO_MODEL);
  const [audioOn, setAudioOn] = useState(true);
  const [generatingVideoId, setGeneratingVideoId] = useState<string | null>(null);

  // Modals
  const [showCharacterPicker, setShowCharacterPicker] = useState(false);
  const [showPromptDetails, setShowPromptDetails] = useState(false);
  const [activeVideoSbId, setActiveVideoSbId] = useState<string | null>(null);

  const expanded = useMemo(
    () => storyboards.find((s) => s.id === expandedId) ?? null,
    [storyboards, expandedId]
  );

  // key → Character lookup (using front-image key)
  const keyToCharacter = useMemo(() => {
    const m = new Map<string, Character>();
    characters.forEach((c) => {
      const k = characterFrontKey(c);
      if (k) m.set(k, c);
    });
    return m;
  }, [characters]);

  const sceneByKey = useMemo(() => {
    const m = new Map<string, Scene>();
    scenes.forEach((s) => { if (s.imageTosKey) m.set(s.imageTosKey, s); });
    return m;
  }, [scenes]);

  const getTaskForSb = (sb: Storyboard): AiTask | undefined => {
    const byId = videoTasks.filter((t) => t.storyboardId === sb.id);
    return byId.find((t) => t.status === "succeeded")
      ?? byId.find((t) => t.status === "running" || t.status === "submitted" || t.status === "pending")
      ?? byId[0];
  };

  const getDotColor = (sb: Storyboard) => {
    const task = getTaskForSb(sb);
    if (!task) return "bg-gray-300 dark:bg-gray-600";
    if (task.status === "succeeded") return "bg-green-500";
    if (task.status === "failed") return "bg-red-400";
    return "bg-orange-400 animate-pulse";
  };

  // 编辑器状态只在切换分镜卡片时从最近任务回填一次。
  // 之后输入框完全归用户掌控，不会被 videoTasks 刷新（如点击生成后 mutation 失效）覆盖。
  const lastSyncedExpandedIdRef = useRef<string | null>(null);
  // sync effect 触发 setStuff 后，本轮 render 内 save effect 会用陈旧 state 误存。
  // 设这个 ref 让 save effect 跳过 sync 后的第一次触发，等下一轮 render 拿到新 state 再存。
  const skipNextDraftSaveRef = useRef(false);

  // Reset on episode change
  useEffect(() => {
    setExpandedId(null);
    setFreeformPrompt("");
    setCharacterKeys([]);
    setSceneKey("");
    setPropKeys([]);
    setShortDescription("");
    setActiveVideoSbId(null);
    lastSyncedExpandedIdRef.current = null;
  }, [activeEpisodeId]);

  // Auto-expand first storyboard on load
  useEffect(() => {
    if (storyboards.length > 0 && !expandedId) {
      setExpandedId(storyboards[0].id);
      setActiveVideoSbId(storyboards[0].id);
    }
  }, [storyboards.length]);

  useEffect(() => {
    if (!expanded) return;
    if (!videoTasksFetched) return; // 等首次拉到任务列表再回填，避免空数据回填后被锁
    if (lastSyncedExpandedIdRef.current === expandedId) return;
    lastSyncedExpandedIdRef.current = expandedId;
    skipNextDraftSaveRef.current = true; // 跳过本轮陈旧 state 的误存

    // 优先从 localStorage 草稿恢复（跨页面切换/刷新都不丢）
    const dKey = pid && eid ? sbDraftKey(pid, eid, expanded.id) : null;
    const draft = dKey ? loadSbDraft(dKey) : null;
    if (draft) {
      setShortDescription(draft.shortDescription);
      setCharacterKeys(draft.characterKeys);
      setSceneKey(draft.sceneKey);
      setPropKeys(draft.propKeys);
      setFreeformPrompt(draft.freeformPrompt);
      setSelectedDuration(draft.selectedDuration);
      setSelectedRatio(draft.selectedRatio);
      setSelectedModel(draft.selectedModel);
      setAudioOn(draft.audioOn);
      return;
    }

    // 没草稿才回退到上一次任务
    setShortDescription(expanded.shortDescription ?? "");
    const lastTask = getTaskForSb(expanded);
    const initialChars = lastTask?.characterImageKeys
      ?? (lastTask?.characterImageKey ? [lastTask.characterImageKey] : []);
    setCharacterKeys(initialChars);
    setSceneKey(lastTask?.sceneImageKey ?? "");
    setPropKeys(lastTask?.propImageKeys ?? []);
    setFreeformPrompt(expanded.videoPrompt?.trim() || (lastTask?.userPrompt?.freeformPrompt ?? ""));
    if (typeof lastTask?.videoDuration === "number") setSelectedDuration(lastTask.videoDuration);
    if (lastTask?.videoRatio) setSelectedRatio(lastTask.videoRatio);
    if (typeof lastTask?.generateAudio === "boolean") setAudioOn(lastTask.generateAudio);
  }, [expandedId, videoTasksFetched]);

  // 编辑器任意字段变化即写入 localStorage 草稿
  useEffect(() => {
    if (!expandedId || !pid || !eid) return;
    if (skipNextDraftSaveRef.current) {
      skipNextDraftSaveRef.current = false;
      return;
    }
    saveSbDraft(sbDraftKey(pid, eid, expandedId), {
      shortDescription,
      characterKeys,
      sceneKey,
      propKeys,
      freeformPrompt,
      selectedDuration,
      selectedRatio,
      selectedModel,
      audioOn,
    });
  }, [
    expandedId, pid, eid,
    shortDescription, characterKeys, sceneKey, propKeys, freeformPrompt,
    selectedDuration, selectedRatio, selectedModel, audioOn,
  ]);

  const handleEpisodeSelect = (ep: Episode) => {
    setActiveEpisodeId(ep.id);
    navigate(`/project/${pid}/storyboard/${ep.id}`, { replace: true });
  };

  const handleGenerateAll = async () => {
    if (!eid) return;
    const result = await generateSbs.mutateAsync(eid);
    if (result.length > 0) {
      setExpandedId(result[0].id);
      setActiveVideoSbId(result[0].id);
    }
  };

  const handleCreate = async () => {
    const newSb = await createSb.mutateAsync({
      sequenceNum: storyboards.length + 1,
      sceneNumber: `场景${storyboards.length + 1}`,
      shortDescription: "新镜头",
      detailedDescription: "",
      characters: "",
      dialogues: "",
      cameraDirection: "",
      compositionGuide: "",
      durationSeconds: 5,
      startFramePrompt: "",
      motionScript: "",
      notes: "",
      status: "draft",
    } as unknown as Storyboard);
    setExpandedId(newSb.id);
    setActiveVideoSbId(newSb.id);
  };

  const handleDelete = async (sb: Storyboard) => {
    if (!confirm(`确定删除分镜 #${sb.sequenceNum}？`)) return;
    await deleteSb.mutateAsync(sb.id);
    if (expandedId === sb.id) {
      const fallback = storyboards.find((s) => s.id !== sb.id);
      setExpandedId(fallback?.id ?? null);
      setActiveVideoSbId(fallback?.id ?? null);
    }
  };

  const togglePropKey = (key: string) => {
    setPropKeys((prev) => {
      if (prev.includes(key)) return prev.filter((k) => k !== key);
      if (prev.length >= MAX_PROPS) return prev;
      return [...prev, key];
    });
  };

  const removeCharacter = (key: string) => {
    setCharacterKeys((prev) => prev.filter((k) => k !== key));
  };

  const swapCharacter = (oldKey: string, newKey: string) => {
    if (!newKey || oldKey === newKey) return;
    setCharacterKeys((prev) => {
      if (prev.includes(newKey)) {
        return prev.filter((k) => k !== oldKey);
      }
      return prev.map((k) => (k === oldKey ? newKey : k));
    });
  };

  const promptHasContent = freeformPrompt.trim().length > 0;
  // 至少一张参考图（角色/场景/道具任一）+ 提示词非空
  const hasAnyReferenceImage = characterKeys.length > 0 || !!sceneKey || propKeys.length > 0;
  const canGenerate = !!expanded && hasAnyReferenceImage
    && promptHasContent && !generateStructured.isPending;

  const handleGenerate = async () => {
    if (!expanded || !canGenerate) return;
    setGeneratingVideoId(expanded.id);
    try {
      await generateStructured.mutateAsync({
        episodeId: expanded.episodeId,
        storyboardId: expanded.id,
        characterImageKeys: characterKeys,
        sceneImageKey: sceneKey,
        propImageKeys: propKeys.length > 0 ? propKeys : null,
        styleImageKey: styleKey || null,
        freeformPrompt: freeformPrompt.trim(),
        duration: selectedDuration,
        ratio: selectedRatio,
        model: selectedModel,
        generateAudio: audioOn,
      });
      setActiveVideoSbId(expanded.id);
    } finally {
      setGeneratingVideoId(null);
    }
  };

  // The video shown in the right preview tracks `activeVideoSbId` (independent
  // from accordion expansion) so user can browse other videos via the timeline
  // without losing editor state.
  const previewSb = useMemo(
    () => storyboards.find((s) => s.id === activeVideoSbId) ?? expanded,
    [storyboards, activeVideoSbId, expanded]
  );
  const previewTask = previewSb ? getTaskForSb(previewSb) : null;

  const promptPreviewText = freeformPrompt.trim();
  const sceneRefDisplay = expanded
    ? expanded.sceneRef?.trim() || inferSceneRefFromCharacters(expanded.characters, scenes) || "未标注"
    : "";
  const charactersDisplay = expanded?.characters?.trim() || "未标注";
  const startFrameDisplay = expanded?.startFramePrompt?.trim() || "未生成";
  const motionScriptDisplay = expanded?.motionScript?.trim() || "未生成";
  const videoPromptDisplay = expanded?.videoPrompt?.trim() || "未生成";
  const storyboardInfoFields = expanded ? [
    { label: "出镜角色", value: charactersDisplay },
    { label: "分镜场景", value: sceneRefDisplay },
    { label: "首帧提示词", value: startFrameDisplay },
    { label: "镜头运动", value: motionScriptDisplay },
    { label: "视频提示词", value: videoPromptDisplay, multiline: true },
  ] : [];

  return (
    <div className="h-full flex overflow-hidden bg-[#f4f7fb] text-foreground">

      {/* ─── Far Left: Episode list strip ─── */}
      <div className="flex-shrink-0 border-r border-slate-200/80 bg-white/90 flex flex-col items-center overflow-hidden py-5 shadow-[1px_0_0_rgba(15,23,42,0.02)]" style={{ width: 80 }}>
        <span className="font-semibold mb-5" style={{ color: "#7c8598", fontSize: 16, letterSpacing: 1 }}>
          集数
        </span>
        <div
          className="flex-1 flex flex-col items-center gap-3.5 overflow-y-auto w-full [&::-webkit-scrollbar]:hidden"
          style={{ scrollbarWidth: "none" }}
        >
          {episodesLoading ? (
            <div className="flex items-center justify-center pt-4">
              <Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
            </div>
          ) : episodes.length === 0 ? (
            <div className="px-1 pt-4 text-center">
              <span className="text-[12px] text-muted-foreground leading-tight">暂无集数</span>
            </div>
          ) : (
            episodes.map((ep) => {
              const active = activeEpisodeId === ep.id;
              return (
                <button
                  key={ep.id}
                  onClick={() => handleEpisodeSelect(ep)}
                  title={ep.title ? `第${ep.episodeNumber}集 · ${ep.title}` : `第${ep.episodeNumber}集`}
                  className={`flex items-center justify-center font-bold tabular-nums transition-all duration-200 flex-shrink-0 ${
                    active ? "" : "hover:bg-[#f0f4fb] hover:text-[#475569] hover:shadow-sm"
                  }`}
                  style={{
                    width: 44,
                    height: 50,
                    borderRadius: 14,
                    fontSize: 16,
                    backgroundColor: active ? "#1c64ff" : "#ffffff",
                    color: active ? "#ffffff" : "#8b95a7",
                    boxShadow: active ? "0 12px 24px rgba(28, 100, 255, 0.28)" : "0 1px 2px rgba(15, 23, 42, 0.05)",
                    border: active ? "1px solid rgba(28, 100, 255, 0.35)" : "1px solid rgba(148, 163, 184, 0.22)",
                  }}
                >
                  {ep.episodeNumber}
                </button>
              );
            })
          )}
        </div>
      </div>

      {/* ─── Column 2: Storyboard accordion list (top: prompt summary, then cards) ─── */}
      <div className="flex flex-col flex-shrink-0 border-r border-slate-200/80 overflow-hidden bg-white/95" style={{ width: 380 }}>
        {/* Top: 分镜表 / 备注描述 */}
        <div className="px-4 py-3 border-b border-slate-200/80 bg-white/95 flex-shrink-0 relative shadow-[0_1px_0_rgba(15,23,42,0.03)]">
          <span className="absolute left-0 top-3 bottom-3 w-[3px] bg-[#1c64ff]/55 rounded-r-full" />
          <div className="flex items-center justify-between mb-1.5">
            <span className="text-[11px] font-semibold tracking-[0.12em] uppercase text-muted-foreground">分镜备注</span>
            {(promptPreviewText || expanded) && (
              <button
                onClick={() => setShowPromptDetails(true)}
                className="text-[10px] text-[#1c64ff] hover:text-[#164fd0] flex items-center gap-0.5 font-semibold transition-colors"
                title="查看完整内容"
              >
                <Info className="w-3 h-3" />
                详情
              </button>
            )}
          </div>
          <p className={`text-[11px] leading-relaxed line-clamp-2 ${promptPreviewText ? "text-foreground/85" : "text-muted-foreground italic"}`}>
            {promptPreviewText
              ? (promptPreviewText.length > PROMPT_PREVIEW_LIMIT
                  ? promptPreviewText.slice(0, PROMPT_PREVIEW_LIMIT) + "…"
                  : promptPreviewText)
              : "选中分镜后，这里会显示已填写的视频提示词内容"}
          </p>
        </div>

        {/* Action row: new + AI generate */}
        <div className="h-12 px-3 border-b border-slate-200/80 flex items-center gap-2 flex-shrink-0 bg-white/95">
          <button
            onClick={handleCreate}
            disabled={createSb.isPending || !eid}
            className="flex-1 flex items-center justify-center gap-1.5 py-1.5 rounded-lg bg-[#1c64ff] text-white text-xs font-semibold tracking-wide shadow-[0_6px_14px_rgba(28,100,255,0.22)] transition-all duration-150 hover:bg-[#1857e8] hover:shadow-[0_8px_18px_rgba(28,100,255,0.28)] hover:-translate-y-px disabled:opacity-50 disabled:hover:translate-y-0 disabled:hover:shadow-none"
          >
            {createSb.isPending ? <Loader2 className="w-3 h-3 animate-spin" /> : <Plus className="w-3 h-3" />}
            新建分镜
          </button>
          <button
            onClick={handleGenerateAll}
            disabled={generateSbs.isPending || !eid}
            title="AI批量生成分镜"
            className={`p-2 rounded-lg text-muted-foreground hover:bg-[#f6f9ff] hover:border-[#1c64ff]/30 hover:text-[#1c64ff] transition-colors disabled:opacity-50 ${CONTROL_SURFACE}`}
          >
            {generateSbs.isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Wand2 className="w-4 h-4" />}
          </button>
        </div>

        {/* Cards (accordion) */}
        <div className="flex-1 overflow-y-auto px-2 py-2 bg-[#f7f9fd]">
          {!eid ? (
            <div className="px-3 py-8 text-center">
              <p className="text-xs text-muted-foreground">请从左侧选择集数</p>
            </div>
          ) : isLoading ? (
            <div className="flex items-center justify-center py-10">
              <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
            </div>
          ) : storyboards.length === 0 ? (
            <div className="px-3 py-8 text-center">
              <p className="text-xs text-muted-foreground">点击「AI生成」或「新建分镜」开始</p>
            </div>
          ) : (
            storyboards.map((sb) => (
              <StoryboardCard
                key={sb.id}
                sb={sb}
                isExpanded={expandedId === sb.id}
                dotClass={getDotColor(sb)}
                onToggle={() => {
                  const next = expandedId === sb.id ? null : sb.id;
                  setExpandedId(next);
                  if (next) setActiveVideoSbId(next);
                }}
                onDelete={() => handleDelete(sb)}
                shortDescription={expandedId === sb.id ? shortDescription : sb.shortDescription ?? ""}
                onShortDescriptionChange={setShortDescription}
                characterKeys={expandedId === sb.id ? characterKeys : []}
                onRemoveCharacter={removeCharacter}
                onSwapCharacter={swapCharacter}
                onAddCharacter={() => setShowCharacterPicker(true)}
                keyToCharacter={keyToCharacter}
                characters={characters}
                sceneKey={expandedId === sb.id ? sceneKey : ""}
                onSelectScene={setSceneKey}
                propKeys={expandedId === sb.id ? propKeys : []}
                onTogglePropKey={togglePropKey}
                scenes={scenes}
                sceneByKey={sceneByKey}
              />
            ))
          )}
        </div>
      </div>

      {/* ─── Column 3: Video generation panel (5-section prompt + duration + button) ─── */}
      <div className="flex flex-col flex-shrink-0 border-r border-slate-200/80 overflow-hidden bg-[#f7f9fd]" style={{ width: 340 }}>
        <div className="h-14 px-4 border-b border-slate-200/80 flex items-center flex-shrink-0 bg-white/95 shadow-[0_1px_0_rgba(15,23,42,0.03)]">
          <span className="text-[13px] font-semibold text-foreground tracking-tight">分镜视频生成</span>
          {expanded && (
            <span className="ml-2 text-[11px] text-muted-foreground tabular-nums">
              · 分镜 {String(expanded.sequenceNum).padStart(3, "0")}
            </span>
          )}
        </div>

        <div className="flex-1 overflow-y-auto px-4 py-4 flex flex-col gap-3">
          {!expanded ? (
            <p className="text-xs text-muted-foreground italic mt-4">展开左侧分镜后填写提示词</p>
          ) : (
            <>
              <div className="flex items-start gap-1.5 px-3 py-2.5 rounded-xl bg-white border border-[#1c64ff]/15 shadow-[0_4px_14px_rgba(28,100,255,0.06)]">
                <Sparkles className="w-3.5 h-3.5 text-[#1c64ff] mt-0.5 flex-shrink-0" />
                <p className="text-[11px] text-[#1c64ff] leading-relaxed">
                  角色、场景、道具预设已默认参考，无需再次添加引用
                </p>
              </div>
              <textarea
                value={freeformPrompt}
                onChange={(e) => setFreeformPrompt(e.target.value)}
                placeholder={FREEFORM_PLACEHOLDER}
                className={`flex-1 min-h-[280px] px-3 py-2.5 rounded-xl border border-slate-200/80 bg-white text-sm text-foreground leading-relaxed resize-none font-mono shadow-inner shadow-slate-100/80 ${FOCUS_RING}`}
              />
            </>
          )}
        </div>

        <div className="px-3 py-3 border-t border-slate-200/80 flex-shrink-0 bg-white/95 space-y-2 shadow-[0_-1px_0_rgba(15,23,42,0.03)]">
          <div className="flex items-center gap-2">
            <div className="relative flex-1">
              <select
                value={selectedModel}
                onChange={(e) => setSelectedModel(e.target.value)}
                disabled={!expanded}
                title="视频生成模型"
                className={`w-full appearance-none pl-3 pr-7 py-2 rounded-lg bg-[#f8fafc] text-xs text-foreground cursor-pointer disabled:opacity-50 transition-colors ${CONTROL_SURFACE} ${FOCUS_RING}`}
              >
                {VIDEO_MODEL_OPTIONS.map((m) => (
                  <option key={m.value} value={m.value}>{m.label}</option>
                ))}
              </select>
              <ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-3 h-3 text-muted-foreground pointer-events-none" />
            </div>
            <div className="relative flex-1">
              <select
                value={selectedRatio}
                onChange={(e) => setSelectedRatio(e.target.value)}
                disabled={!expanded}
                className={`w-full appearance-none pl-3 pr-7 py-2 rounded-lg bg-[#f8fafc] text-xs text-foreground cursor-pointer disabled:opacity-50 transition-colors ${CONTROL_SURFACE} ${FOCUS_RING}`}
              >
                {RATIO_OPTIONS.map((r) => (
                  <option key={r.value} value={r.value}>{r.label}</option>
                ))}
              </select>
              <ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-3 h-3 text-muted-foreground pointer-events-none" />
            </div>
            <div className="relative">
              <select
                value={selectedDuration}
                onChange={(e) => setSelectedDuration(Number(e.target.value))}
                disabled={!expanded}
                className={`appearance-none pl-2 pr-6 py-2 rounded-lg bg-[#f8fafc] text-xs text-foreground cursor-pointer w-16 disabled:opacity-50 transition-colors tabular-nums ${CONTROL_SURFACE} ${FOCUS_RING}`}
              >
                {DURATION_OPTIONS.map((d) => (
                  <option key={d} value={d}>{d}</option>
                ))}
              </select>
              <span className="absolute right-2 top-1/2 -translate-y-1/2 text-xs text-muted-foreground pointer-events-none">s</span>
            </div>
            <button
              onClick={() => setAudioOn((v) => !v)}
              disabled={!expanded}
              title={audioOn ? "已开启配音" : "已关闭配音"}
              className={`p-2 rounded-lg border text-xs transition-colors disabled:opacity-50 ${
                audioOn
                  ? "border-[#1c64ff] bg-[#1c64ff]/10 text-[#1c64ff] shadow-[0_4px_12px_rgba(28,100,255,0.12)]"
                  : "border-slate-200/80 bg-[#f8fafc] text-muted-foreground hover:bg-[#eef4ff] hover:text-[#1c64ff]"
              }`}
            >
              {audioOn ? <Volume2 className="w-3.5 h-3.5" /> : <VolumeX className="w-3.5 h-3.5" />}
            </button>
          </div>

          <button
            onClick={handleGenerate}
            disabled={!canGenerate || generatingVideoId === expanded?.id}
            title={!canGenerate ? "需要：至少 1 张参考图（角色/场景/道具任一）+ 提示词" : ""}
            className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-[#1c64ff] text-white text-xs font-semibold tracking-wide shadow-[0_9px_20px_rgba(28,100,255,0.26)] transition-all duration-150 hover:bg-[#1857e8] hover:shadow-[0_12px_24px_rgba(28,100,255,0.32)] hover:-translate-y-px disabled:opacity-50 disabled:hover:translate-y-0 disabled:hover:shadow-none whitespace-nowrap"
          >
            {generatingVideoId === expanded?.id ? (
              <Loader2 className="w-3.5 h-3.5 animate-spin" />
            ) : (
              <Play className="w-3.5 h-3.5" />
            )}
            生成视频{videoCredits != null ? ` (${videoCredits.toLocaleString()}积分)` : ""}
          </button>
        </div>
      </div>

      {/* ─── Column 4: Big video preview + thumbnail timeline ─── */}
      <div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-white/90">
        <div className="h-14 px-5 border-b border-slate-200/80 flex items-center justify-between flex-shrink-0 bg-white/95 shadow-[0_1px_0_rgba(15,23,42,0.03)]">
          <span className="text-[13px] font-semibold text-foreground tracking-tight">
            视频预览
            {previewSb && (
              <span className="ml-2 text-[11px] font-normal text-muted-foreground tabular-nums">
                · 分镜 {String(previewSb.sequenceNum).padStart(3, "0")}
              </span>
            )}
          </span>
          <div className="flex items-center gap-2">
            {assemblyTask?.status === "succeeded" && assemblyTask.downloadUrl && (
              <a
                href={assemblyTask.downloadUrl}
                target="_blank"
                rel="noreferrer"
                className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-emerald-500 text-white text-xs font-medium shadow-[0_6px_14px_rgba(16,185,129,0.22)] hover:bg-emerald-600 hover:shadow-[0_8px_18px_rgba(16,185,129,0.28)] transition-all duration-150"
              >
                <Download className="w-3.5 h-3.5" />
                下载合集
              </a>
            )}
            <button
              onClick={() => startAssembly.mutate()}
              disabled={
                startAssembly.isPending ||
                assemblyTask?.status === "running" ||
                assemblyTask?.status === "pending" ||
                storyboards.length === 0
              }
              className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs text-foreground hover:bg-[#f6f9ff] hover:border-[#1c64ff]/30 hover:text-[#1c64ff] transition-colors disabled:opacity-50 disabled:hover:bg-white disabled:hover:border-slate-200 disabled:hover:text-foreground ${CONTROL_SURFACE}`}
            >
              {startAssembly.isPending || assemblyTask?.status === "running" ? (
                <Loader2 className="w-3.5 h-3.5 animate-spin" />
              ) : (
                <Layers className="w-3.5 h-3.5" />
              )}
              合成本集
            </button>
            <button
              onClick={() => navigate(`/project/${projectId}/video`)}
              className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs text-foreground hover:bg-[#f6f9ff] hover:border-[#1c64ff]/30 hover:text-[#1c64ff] transition-colors ${CONTROL_SURFACE}`}
            >
              视频管理
              <ArrowRight className="w-3.5 h-3.5" />
            </button>
          </div>
        </div>

        <div className="flex-1 overflow-y-auto p-5 min-h-0 bg-[#f4f7fb]">
          {!previewSb ? (
            <div className="h-full flex items-center justify-center">
              <p className="text-sm text-muted-foreground">展开任意分镜后查看视频</p>
            </div>
          ) : previewTask?.status === "succeeded" && previewTask.resultVideoUrl ? (
            <div className="space-y-3">
              <div className="rounded-2xl overflow-hidden bg-black aspect-video shadow-[0_18px_45px_rgba(15,23,42,0.18)] ring-1 ring-black/10">
                <video
                  key={previewTask.resultVideoUrl}
                  src={previewTask.resultVideoUrl}
                  controls
                  autoPlay={false}
                  className="w-full h-full object-contain"
                />
              </div>
              <a
                href={previewTask.resultVideoUrl}
                target="_blank"
                rel="noreferrer"
                className="inline-flex items-center gap-1.5 text-xs text-[#1c64ff] hover:underline font-medium"
              >
                <Download className="w-3.5 h-3.5" />
                下载此镜头视频
              </a>
            </div>
          ) : previewTask?.status === "running" || previewTask?.status === "submitted" || previewTask?.status === "pending" ? (
            <div className={`h-full flex flex-col items-center justify-center gap-4 rounded-2xl ${SOFT_PANEL}`}>
              <div className="relative w-16 h-16 flex items-center justify-center">
                <span className="absolute inset-0 rounded-full bg-[#1c64ff]/10 animate-pulse ring-8 ring-[#1c64ff]/5" />
                <Loader2 className="w-8 h-8 text-[#1c64ff] animate-spin relative" />
              </div>
              <p className="text-sm font-semibold text-foreground tracking-tight">视频生成中…</p>
              <p className="text-xs text-muted-foreground">通常需要 30-60 秒</p>
            </div>
          ) : previewTask?.status === "failed" ? (
            <div className="rounded-2xl border border-red-200 bg-red-50 p-6 shadow-[0_8px_24px_rgba(185,28,28,0.08)]">
              <div className="flex items-center gap-2 mb-2">
                <X className="w-4 h-4 text-red-500" />
                <span className="text-sm font-medium text-red-700">生成失败</span>
              </div>
              <p className="text-xs text-red-600">{previewTask.errorMessage || "未知错误"}</p>
            </div>
          ) : (
            <div className="h-full flex flex-col items-center justify-center gap-4">
              <div className="w-full max-w-sm aspect-video rounded-2xl border-[1.5px] border-dashed border-slate-300/80 bg-white/90 shadow-[0_10px_30px_rgba(15,23,42,0.05)] flex flex-col items-center justify-center gap-3">
                <Play className="w-12 h-12 text-muted-foreground/30" />
                <p className="text-sm text-muted-foreground">尚未生成视频</p>
                <p className="text-xs text-muted-foreground/70">在左侧填好素材+提示词后生成</p>
              </div>
            </div>
          )}
        </div>

        {/* Thumbnail timeline */}
        {storyboards.length > 0 && (
          <div className="border-t border-slate-200/80 bg-white/95 flex-shrink-0 shadow-[0_-1px_0_rgba(15,23,42,0.03)]">
            <div className="px-3 py-3 flex items-center gap-2 overflow-x-auto">
              {storyboards.map((sb) => {
                const t = getTaskForSb(sb);
                const isActive = activeVideoSbId === sb.id;
                const url = t?.resultVideoUrl;
                return (
                  <button
                    key={sb.id}
                    onClick={() => {
                      setActiveVideoSbId(sb.id);
                      setExpandedId(sb.id);
                    }}
                    title={`分镜 ${String(sb.sequenceNum).padStart(3, "0")}`}
                    className={`relative flex-shrink-0 w-[88px] h-[52px] rounded-lg overflow-hidden border transition-all duration-150 ${
                      isActive
                        ? "border-[#1c64ff] ring-2 ring-[#1c64ff]/25 shadow-[0_8px_18px_rgba(28,100,255,0.22)]"
                        : "border-slate-200 hover:border-[#1c64ff]/40 hover:-translate-y-px hover:shadow-sm"
                    }`}
                  >
                    {url ? (
                      <video
                        src={url}
                        muted
                        preload="metadata"
                        className="w-full h-full object-cover pointer-events-none"
                      />
                    ) : (
                      <div className="w-full h-full bg-[#f8fafc] flex items-center justify-center">
                        <ImageIcon className="w-4 h-4 text-muted-foreground/40" />
                      </div>
                    )}
                    <span className="absolute bottom-0 left-0 right-0 bg-black/65 text-white text-[10px] font-medium tabular-nums py-0.5 text-center">
                      {String(sb.sequenceNum).padStart(3, "0")}
                    </span>
                    <span className={`absolute top-1 right-1 w-1.5 h-1.5 rounded-full ring-2 ring-white/80 ${getDotColor(sb)}`} />
                  </button>
                );
              })}
            </div>
          </div>
        )}
      </div>

      {/* ─── Modals ─── */}
      {showCharacterPicker && expanded && (
        <CharacterPickerModal
          projectId={pid}
          characters={characters}
          currentKeys={characterKeys}
          onPick={(k) => {
            if (!characterKeys.includes(k) && characterKeys.length < MAX_CHARACTERS) {
              setCharacterKeys((prev) => [...prev, k]);
            }
          }}
          onClose={() => setShowCharacterPicker(false)}
        />
      )}
      {showPromptDetails && (
        <PromptDetailsModal
          text={freeformPrompt}
          agentInfo={storyboardInfoFields}
          onClose={() => setShowPromptDetails(false)}
        />
      )}
    </div>
  );
}

// ────────────────────────────────────────────────────────────────────────────
// StoryboardCard — accordion item with self-contained editor when expanded
// ────────────────────────────────────────────────────────────────────────────
function StoryboardInfoField({
  label,
  value,
  multiline = false,
  compact = false,
}: {
  label: string;
  value: string;
  multiline?: boolean;
  compact?: boolean;
}) {
  return (
    <div className={compact ? "px-3 py-2" : "px-3 py-2.5"}>
      <div className="flex items-start gap-2">
        <span className="w-16 flex-shrink-0 text-[11px] font-medium text-muted-foreground leading-relaxed">
          {label}
        </span>
        <span
          className={`min-w-0 flex-1 text-[11px] leading-relaxed text-foreground/85 ${
            multiline ? "line-clamp-3 break-words" : "truncate"
          }`}
          title={value}
        >
          {value}
        </span>
      </div>
    </div>
  );
}

interface StoryboardCardProps {
  sb: Storyboard;
  isExpanded: boolean;
  dotClass: string;
  onToggle: () => void;
  onDelete: () => void;
  shortDescription: string;
  onShortDescriptionChange: (v: string) => void;
  characterKeys: string[];
  onRemoveCharacter: (key: string) => void;
  onSwapCharacter: (oldKey: string, newKey: string) => void;
  onAddCharacter: () => void;
  keyToCharacter: Map<string, Character>;
  characters: Character[];
  sceneKey: string;
  onSelectScene: (key: string) => void;
  propKeys: string[];
  onTogglePropKey: (key: string) => void;
  scenes: Scene[];
  sceneByKey: Map<string, Scene>;
}

function StoryboardCard(props: StoryboardCardProps) {
  const { sb, isExpanded, dotClass, onToggle, onDelete } = props;
  const [descCollapsed, setDescCollapsed] = useState(false);

  return (
    <div className={`my-2 rounded-2xl border transition-all duration-200 ${
      isExpanded
        ? "border-[#1c64ff]/35 bg-white shadow-[0_12px_30px_rgba(15,23,42,0.08)]"
        : "border-slate-200/80 bg-white/80 hover:bg-white hover:border-[#1c64ff]/25 hover:shadow-[0_8px_18px_rgba(15,23,42,0.05)]"
    }`}>
      <button
        onClick={onToggle}
        className="w-full px-3.5 pt-3 pb-2.5 flex items-center gap-2 text-left rounded-t-2xl"
      >
        <span className="text-sm font-semibold text-foreground flex-1 truncate">
          分镜信息{String(sb.sequenceNum).padStart(3, "0")}
        </span>
        <span className={`w-2.5 h-2.5 rounded-full flex-shrink-0 ring-4 ring-white ${dotClass}`} />
      </button>

      {isExpanded && (
        <div className="px-3.5 pb-3.5 space-y-3.5 border-t border-slate-100">
          {/* 分镜描述 */}
          <div>
            <button
              onClick={() => setDescCollapsed((v) => !v)}
              className="w-full flex items-center justify-between mb-1.5 rounded-md text-left"
            >
              <span className="text-xs font-medium text-foreground">分镜描述</span>
              {descCollapsed
                ? <ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />
                : <ChevronUp className="w-3.5 h-3.5 text-muted-foreground" />}
            </button>
            {!descCollapsed && (
              <textarea
                rows={4}
                value={props.shortDescription}
                onChange={(e) => props.onShortDescriptionChange(e.target.value)}
                placeholder="简要描述此分镜的剧情或画面"
                className={`w-full px-3 py-2 rounded-xl border border-slate-200/80 bg-[#f8fafc] text-xs text-foreground leading-relaxed resize-none shadow-inner shadow-slate-100/80 ${FOCUS_RING}`}
              />
            )}
          </div>

          {/* 出镜角色 */}
          <CharacterRowList
            characterKeys={props.characterKeys}
            keyToCharacter={props.keyToCharacter}
            characters={props.characters}
            onRemove={props.onRemoveCharacter}
            onSwap={props.onSwapCharacter}
            onAdd={props.onAddCharacter}
          />

          {/* 分镜场景 */}
          <SceneSection
            scenes={props.scenes}
            selectedKey={props.sceneKey}
            onSelect={props.onSelectScene}
          />

          {/* 场景道具 */}
          <PropGrid
            scenes={props.scenes}
            sceneKey={props.sceneKey}
            selectedKeys={props.propKeys}
            onToggle={props.onTogglePropKey}
          />

          {/* 删除按钮 */}
          <div className="pt-1 flex justify-end">
            <button
              onClick={onDelete}
              className="text-[11px] text-destructive hover:text-red-700 flex items-center gap-1 rounded-md px-2 py-1 hover:bg-red-50 transition-colors"
            >
              <Trash2 className="w-3 h-3" />
              删除该分镜
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

// ────────────────────────────────────────────────────────────────────────────
// CharacterRowList — selected character variants as horizontal rows:
//   [avatar | name dropdown | voice dropdown | 删除 | 重绘]
// ────────────────────────────────────────────────────────────────────────────
interface CharacterRowListProps {
  characterKeys: string[];
  keyToCharacter: Map<string, Character>;
  characters: Character[];
  onRemove: (key: string) => void;
  onSwap: (oldKey: string, newKey: string) => void;
  onAdd: () => void;
}

function CharacterRowList({
  characterKeys, keyToCharacter, characters, onRemove, onSwap, onAdd,
}: CharacterRowListProps) {
  return (
    <div className="rounded-xl bg-[#f8fafc] p-3 ring-1 ring-slate-200/70">
      <div className="flex items-center justify-between mb-2">
        <span className="text-xs font-medium text-foreground">出镜角色</span>
        <button
          onClick={onAdd}
          disabled={characterKeys.length >= MAX_CHARACTERS}
          className="flex items-center gap-0.5 text-[11px] text-[#1c64ff] hover:text-[#164fd0] font-medium disabled:opacity-40"
        >
          <Plus className="w-3 h-3" />
          添加
        </button>
      </div>
      {characterKeys.length === 0 ? (
        <p className="text-[11px] text-muted-foreground italic">点击「添加」从角色库挑选或创建变体</p>
      ) : (
        <div className="space-y-2">
          {characterKeys.map((k) => (
            <CharacterRow
              key={k}
              tosKey={k}
              character={keyToCharacter.get(k) ?? null}
              allCharacters={characters}
              currentKeys={characterKeys}
              onRemove={() => onRemove(k)}
              onSwap={(newKey) => onSwap(k, newKey)}
            />
          ))}
        </div>
      )}
    </div>
  );
}

interface CharacterRowProps {
  tosKey: string;
  character: Character | null;
  allCharacters: Character[];
  currentKeys: string[];
  onRemove: () => void;
  onSwap: (newKey: string) => void;
}

function CharacterRow({
  tosKey, character, allCharacters, currentKeys, onRemove, onSwap,
}: CharacterRowProps) {
  const { projectId } = useParams<{ projectId: string }>();
  const regen = useGenerateCharacterImage(projectId ?? "");

  const handleRegen = () => {
    if (!character) return;
    regen.mutate(character.id);
  };

  const url = character ? characterFrontUrl(character) : null;
  const isRegenerating = regen.isPending;

  // Options: this character + any others not already used in the storyboard
  const options = allCharacters.filter((c) => {
    const k = characterFrontKey(c);
    if (!k) return false;
    return k === tosKey || !currentKeys.includes(k);
  });

  return (
    <div className="flex items-center gap-1.5 rounded-xl bg-white p-1.5 ring-1 ring-slate-200/70">
      <div className="w-8 h-8 rounded-lg overflow-hidden border border-slate-200 bg-slate-100 flex-shrink-0">
        {url ? (
          <img src={url} alt={character?.name ?? ""} className="w-full h-full object-cover" />
        ) : (
          <div className="w-full h-full flex items-center justify-center text-[8px] text-muted-foreground">无图</div>
        )}
      </div>
      <div className="relative flex-1 min-w-0">
        <select
          value={tosKey}
          onChange={(e) => onSwap(e.target.value)}
          className={`w-full appearance-none pl-2 pr-6 py-1.5 rounded-lg border border-slate-200/80 bg-white text-xs text-foreground cursor-pointer truncate ${FOCUS_RING}`}
        >
          {!character && <option value={tosKey}>(已失效)</option>}
          {options.map((c) => {
            const k = characterFrontKey(c)!;
            return <option key={k} value={k}>{c.name}</option>;
          })}
        </select>
        <ChevronDown className="absolute right-1.5 top-1/2 -translate-y-1/2 w-3 h-3 text-muted-foreground pointer-events-none" />
      </div>
      <div className="relative flex-1 min-w-0">
        <select
          disabled
          className="w-full appearance-none pl-2 pr-6 py-1.5 rounded-lg border border-slate-200/80 bg-slate-100/80 text-xs text-muted-foreground cursor-not-allowed truncate"
        >
          <option>未配置音色</option>
        </select>
        <ChevronDown className="absolute right-1.5 top-1/2 -translate-y-1/2 w-3 h-3 text-muted-foreground pointer-events-none" />
      </div>
      <button
        onClick={onRemove}
        className="px-2 py-1.5 rounded-lg border border-slate-200/80 text-[11px] text-muted-foreground hover:bg-red-50 hover:text-red-600 transition-colors"
      >
        删除
      </button>
      <button
        onClick={handleRegen}
        disabled={!character || isRegenerating}
        className="px-2 py-1.5 rounded-lg border border-slate-200/80 text-[11px] text-muted-foreground hover:bg-[#f6f9ff] hover:text-[#1c64ff] disabled:opacity-40 flex items-center gap-1 transition-colors"
      >
        {isRegenerating && <Loader2 className="w-3 h-3 animate-spin" />}
        重绘
      </button>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────────────────
// SceneSection — large preview + scene picker dropdown + more menu
// ────────────────────────────────────────────────────────────────────────────
interface SceneSectionProps {
  scenes: Scene[];
  selectedKey: string;
  onSelect: (key: string) => void;
}
function SceneSection({ scenes, selectedKey, onSelect }: SceneSectionProps) {
  const [showPicker, setShowPicker] = useState(false);
  const [menuOpen, setMenuOpen] = useState(false);
  const usable = scenes.filter((s) => !!s.imageTosKey);
  const selected = usable.find((s) => s.imageTosKey === selectedKey) ?? null;

  return (
    <div className="rounded-xl bg-[#f8fafc] p-3 ring-1 ring-slate-200/70">
      <div className="flex items-center justify-between mb-2">
        <span className="text-xs font-medium text-foreground">分镜场景</span>
        {usable.length > 0 && (
          <button
            onClick={() => setShowPicker((v) => !v)}
            className="flex items-center gap-0.5 text-[11px] text-[#1c64ff] hover:text-[#164fd0] font-medium"
          >
            <Plus className="w-3 h-3" />
            {selected ? "更换" : "选择"}
          </button>
        )}
      </div>

      {/* Big preview — clickable to open picker */}
      <button
        type="button"
        onClick={() => usable.length > 0 && setShowPicker((v) => !v)}
        disabled={usable.length === 0}
        className="w-full aspect-video rounded-xl overflow-hidden border border-slate-200/80 bg-slate-100 mb-2 block hover:border-[#1c64ff]/40 hover:shadow-sm transition disabled:cursor-not-allowed"
      >
        {selected?.imageUrl ? (
          <img src={selected.imageUrl} alt={selected.name} className="w-full h-full object-cover" />
        ) : (
          <div className="w-full h-full flex flex-col items-center justify-center gap-1 text-muted-foreground">
            <ImageIcon className="w-5 h-5" />
            <span className="text-[11px]">
              {usable.length === 0 ? "先到「场景」页创建场景" : "点击选择一张场景图"}
            </span>
          </div>
        )}
      </button>

      {/* Bottom row: scene name + role label + actions menu */}
      <div className="flex items-center gap-1.5">
        <div className="flex-1 min-w-0 px-2.5 py-1.5 rounded-lg border border-slate-200/80 bg-white text-xs text-foreground truncate">
          {selected?.name || <span className="text-muted-foreground">请选择场景</span>}
        </div>
        <div className="px-2.5 py-1.5 rounded-lg border border-slate-200/80 bg-white/80 text-xs text-muted-foreground flex-shrink-0">
          主场景图
        </div>
        <div className="relative flex-shrink-0">
          <button
            onClick={() => setMenuOpen((v) => !v)}
            disabled={!selected}
            className="p-1.5 rounded-lg border border-slate-200/80 bg-white text-muted-foreground hover:bg-[#f6f9ff] hover:text-[#1c64ff] disabled:opacity-40 transition-colors"
          >
            <MoreHorizontal className="w-3.5 h-3.5" />
          </button>
          {menuOpen && selected && (
            <>
              <button
                className="fixed inset-0 z-10 cursor-default"
                onClick={() => setMenuOpen(false)}
              />
              <div className="absolute right-0 top-full mt-1 z-20 bg-white border border-slate-200/80 rounded-xl shadow-lg overflow-hidden min-w-[100px]">
                <button
                  onClick={() => { setShowPicker(true); setMenuOpen(false); }}
                  className="w-full px-3 py-1.5 text-left text-xs text-foreground hover:bg-[#f6f9ff]"
                >
                  更换场景
                </button>
                <button
                  onClick={() => { onSelect(""); setMenuOpen(false); }}
                  className="w-full px-3 py-1.5 text-left text-xs text-destructive hover:bg-red-50"
                >
                  清除选择
                </button>
              </div>
            </>
          )}
        </div>
      </div>

      {showPicker && (
        <div className="mt-2 rounded-xl border border-slate-200/80 bg-white/80 p-2">
          {usable.length === 0 ? (
            <p className="text-[11px] text-muted-foreground italic text-center py-2">先到「场景」页创建场景</p>
          ) : (
            <div className="grid grid-cols-3 gap-2">
              {usable.map((s) => {
                const active = selectedKey === s.imageTosKey;
                return (
                  <button
                    key={s.id}
                    onClick={() => {
                      onSelect(active ? "" : s.imageTosKey!);
                      if (!active) setShowPicker(false);
                    }}
                    title={s.name}
                    className={`relative aspect-square rounded-md overflow-hidden border-2 ${
                      active ? "border-[#1c64ff] ring-2 ring-[#1c64ff]/25" : "border-slate-200 hover:border-[#1c64ff]/40"
                    }`}
                  >
                    {s.imageUrl ? (
                      <img src={s.imageUrl} alt={s.name} className="w-full h-full object-cover" />
                    ) : (
                      <div className="w-full h-full flex items-center justify-center text-[9px] text-muted-foreground">无图</div>
                    )}
                    <div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 to-transparent px-1 pt-3 pb-0.5">
                      <span className="text-[10px] text-white truncate block">{s.name}</span>
                    </div>
                    {active && (
                      <div className="absolute top-1 right-1 w-4 h-4 rounded-full bg-[#1c64ff] flex items-center justify-center shadow-sm">
                        <Check className="w-2.5 h-2.5 text-white" />
                      </div>
                    )}
                  </button>
                );
              })}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ────────────────────────────────────────────────────────────────────────────
// PropGrid — selected props as 3-col card grid + inline picker drawer
// ────────────────────────────────────────────────────────────────────────────
interface PropGridProps {
  scenes: Scene[];
  sceneKey: string;
  selectedKeys: string[];
  onToggle: (key: string) => void;
}
function PropGrid({ scenes, sceneKey, selectedKeys, onToggle }: PropGridProps) {
  const [showPicker, setShowPicker] = useState(false);
  const allCandidates = scenes.filter((s) => !!s.imageTosKey && s.imageTosKey !== sceneKey);
  const selected = selectedKeys
    .map((k) => allCandidates.find((s) => s.imageTosKey === k))
    .filter((s): s is Scene => !!s);
  const available = allCandidates.filter((s) => !selectedKeys.includes(s.imageTosKey!));
  const canAddMore = selectedKeys.length < MAX_PROPS;

  return (
    <div className="rounded-xl bg-[#f8fafc] p-3 ring-1 ring-slate-200/70">
      <div className="flex items-center justify-between mb-2">
        <span className="text-xs font-medium text-foreground">场景道具</span>
        <button
          onClick={() => setShowPicker((v) => !v)}
          disabled={!canAddMore && !showPicker}
          className="flex items-center gap-0.5 text-[11px] text-[#1c64ff] hover:text-[#164fd0] font-medium disabled:opacity-40"
        >
          <Plus className="w-3 h-3" />
          添加
        </button>
      </div>

      {selected.length === 0 ? (
        <p className="text-[11px] text-muted-foreground italic mb-2">从场景库中可挑选最多 5 张作为道具</p>
      ) : (
        <div className="grid grid-cols-3 gap-2 mb-2">
          {selected.map((s) => (
            <button
              key={s.id}
              onClick={() => onToggle(s.imageTosKey!)}
              title={`移除 ${s.name}`}
              className="relative aspect-square rounded-xl overflow-hidden border border-slate-200/80 bg-slate-100 group hover:shadow-sm transition-shadow"
            >
              {s.imageUrl ? (
                <img src={s.imageUrl} alt={s.name} className="w-full h-full object-cover" />
              ) : (
                <div className="w-full h-full flex items-center justify-center text-[9px] text-muted-foreground">无图</div>
              )}
              <div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/85 via-black/40 to-transparent px-1.5 pt-3 pb-1 text-left">
                <span className="text-[11px] text-white font-medium truncate block">{s.name}</span>
              </div>
              <div className="absolute top-1 right-1 w-4 h-4 rounded-full bg-black/60 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition">
                <X className="w-2.5 h-2.5" />
              </div>
            </button>
          ))}
        </div>
      )}

      {showPicker && (
        <div className="rounded-xl border border-slate-200/80 bg-white/80 p-2">
          {available.length === 0 ? (
            <p className="text-[11px] text-muted-foreground italic text-center py-2">
              {allCandidates.length === 0 ? "先到「场景」页创建场景" : "已添加全部可选道具"}
            </p>
          ) : (
            <div className="grid grid-cols-4 gap-1.5">
              {available.map((s) => (
                <button
                  key={s.id}
                  onClick={() => {
                    onToggle(s.imageTosKey!);
                    if (selectedKeys.length + 1 >= MAX_PROPS) setShowPicker(false);
                  }}
                  disabled={!canAddMore}
                  title={s.name}
                  className="relative aspect-square rounded-lg overflow-hidden border border-slate-200 bg-white hover:border-[#1c64ff]/40 hover:shadow-sm disabled:opacity-40 transition"
                >
                  {s.imageUrl ? (
                    <img src={s.imageUrl} alt={s.name} className="w-full h-full object-cover" />
                  ) : (
                    <div className="w-full h-full flex items-center justify-center text-[9px] text-muted-foreground">无图</div>
                  )}
                  <div className="absolute inset-x-0 bottom-0 bg-black/60 px-1 py-0.5">
                    <span className="text-[9px] text-white truncate block">{s.name}</span>
                  </div>
                </button>
              ))}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ────────────────────────────────────────────────────────────────────────────
// CharacterPickerModal — pick from existing characters OR create a new variant
// ────────────────────────────────────────────────────────────────────────────
interface CharacterPickerModalProps {
  projectId: string;
  characters: Character[];
  currentKeys: string[];
  onPick: (key: string) => void;
  onClose: () => void;
}
function CharacterPickerModal({
  projectId, characters, currentKeys, onPick, onClose,
}: CharacterPickerModalProps) {
  const [createMode, setCreateMode] = useState(false);

  if (createMode) {
    return (
      <AddCharacterModal
        projectId={projectId}
        onClose={() => setCreateMode(false)}
        onAdded={(c) => {
          const k = characterFrontKey(c);
          if (k) onPick(k);
          setCreateMode(false);
          onClose();
        }}
      />
    );
  }

  const usable = characters.filter((c) => !!characterFrontKey(c));

  return (
    <div className="fixed inset-0 bg-slate-950/55 backdrop-blur-sm z-50 flex items-center justify-center p-4" onClick={onClose}>
      <div
        className="bg-white rounded-2xl shadow-[0_24px_70px_rgba(15,23,42,0.28)] ring-1 ring-white/60 w-full max-w-2xl max-h-[80vh] flex flex-col overflow-hidden"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="flex items-center justify-between px-5 py-3 border-b border-slate-200/80 bg-[#f8fafc]">
          <h3 className="text-sm font-semibold text-foreground">添加出镜角色</h3>
          <button onClick={onClose} className="p-1 hover:bg-white rounded-lg transition-colors">
            <X className="w-4 h-4 text-muted-foreground" />
          </button>
        </div>

        <div className="flex-1 overflow-y-auto p-5">
          <p className="text-xs text-muted-foreground mb-3">从已有角色中挑选，或创建新的角色变体</p>
          {usable.length === 0 ? (
            <p className="text-xs text-muted-foreground italic text-center py-8">
              当前项目还没有角色，请先创建一个新变体
            </p>
          ) : (
            <div className="grid grid-cols-4 gap-3">
              {usable.map((c) => {
                const k = characterFrontKey(c)!;
                const url = characterFrontUrl(c);
                const isAdded = currentKeys.includes(k);
                return (
                  <button
                    key={c.id}
                    disabled={isAdded}
                    onClick={() => { onPick(k); onClose(); }}
                    className={`relative rounded-lg overflow-hidden border-2 transition ${
                      isAdded
                        ? "border-[#1c64ff] opacity-50 cursor-not-allowed"
                        : "border-slate-200 hover:border-[#1c64ff]/60 hover:shadow-sm"
                    }`}
                  >
                    <div className="aspect-square bg-muted">
                      {url ? (
                        <img src={url} alt={c.name} className="w-full h-full object-cover" />
                      ) : (
                        <div className="w-full h-full flex items-center justify-center text-[10px] text-muted-foreground">无图</div>
                      )}
                    </div>
                    <div className="px-2 py-1.5 text-left">
                      <p className="text-xs font-medium text-foreground truncate">{c.name}</p>
                    </div>
                    {isAdded && (
                      <div className="absolute top-1 right-1 w-5 h-5 rounded-full bg-[#1c64ff] flex items-center justify-center shadow-sm">
                        <Check className="w-3 h-3 text-white" />
                      </div>
                    )}
                  </button>
                );
              })}
            </div>
          )}
        </div>

        <div className="px-5 py-3 border-t border-slate-200/80 bg-[#f8fafc] flex justify-between items-center gap-2">
          <button
            onClick={() => setCreateMode(true)}
            className="px-3 py-1.5 rounded-lg border border-[#1c64ff]/35 text-[#1c64ff] text-xs hover:bg-[#1c64ff]/10 flex items-center gap-1.5 transition-colors"
          >
            <Plus className="w-3 h-3" />
            创建新角色变体
          </button>
          <button
            onClick={onClose}
            className="px-4 py-1.5 rounded-lg border border-slate-200/80 bg-white text-xs text-foreground hover:bg-[#f6f9ff] transition-colors"
          >
            完成
          </button>
        </div>
      </div>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────────────────
// AddCharacterModal — create a new character variant (e.g. 陆长生-白衣)
// ────────────────────────────────────────────────────────────────────────────
interface AddCharacterModalProps {
  projectId: string;
  onClose: () => void;
  onAdded: (c: Character) => void;
}
function AddCharacterModal({ projectId, onClose, onAdded }: AddCharacterModalProps) {
  const [name, setName] = useState("");
  const [imagePrompt, setImagePrompt] = useState("");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const saveChar = useSaveCharacter(projectId);
  const genImg = useGenerateCharacterImage(projectId);

  const handleSubmit = async () => {
    if (!name.trim()) {
      setError("请输入角色名称");
      return;
    }
    if (!imagePrompt.trim()) {
      setError("请输入形象提示词");
      return;
    }
    setError(null);
    setBusy(true);
    try {
      const created = await saveChar.mutateAsync({
        name: name.trim(),
        roleType: "supporting",
        imagePrompt: imagePrompt.trim(),
        status: "draft",
      });
      const withImage = await genImg.mutateAsync(created.id);
      onAdded(withImage);
    } catch (e) {
      setError(e instanceof Error ? e.message : "创建失败");
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="fixed inset-0 bg-slate-950/55 backdrop-blur-sm z-50 flex items-center justify-center p-4">
      <div className="bg-white rounded-2xl shadow-[0_24px_70px_rgba(15,23,42,0.28)] ring-1 ring-white/60 w-full max-w-md max-h-[90vh] flex flex-col overflow-hidden">
        <div className="flex items-center justify-between px-5 py-3 border-b border-slate-200/80 bg-[#f8fafc]">
          <h3 className="text-sm font-semibold text-foreground">新增出镜角色变体</h3>
          <button onClick={onClose} className="p-1 hover:bg-white rounded-lg transition-colors">
            <X className="w-4 h-4 text-muted-foreground" />
          </button>
        </div>

        <div className="flex-1 overflow-y-auto p-5 space-y-3">
          <p className="text-[11px] text-muted-foreground">
            创建一个新的角色形象（例如「陆长生 - 白衣」）。系统会立即生成一张参考图。
          </p>
          <div>
            <label className="block text-xs text-foreground mb-1">角色名称 *</label>
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="例如：陆长生 - 白衣"
              className={`w-full px-3 py-2 rounded-xl border border-slate-200/80 bg-white text-sm ${FOCUS_RING}`}
            />
          </div>
          <div>
            <label className="block text-xs text-foreground mb-1">形象提示词 *</label>
            <textarea
              rows={4}
              value={imagePrompt}
              onChange={(e) => setImagePrompt(e.target.value)}
              placeholder="例如：男性，二十岁出头，身穿白色长袍，气质温雅，黑色长发束起"
              className={`w-full px-3 py-2 rounded-xl border border-slate-200/80 bg-white text-sm leading-relaxed resize-none ${FOCUS_RING}`}
            />
          </div>
          {error && <p className="text-xs text-red-500">{error}</p>}
        </div>

        <div className="px-5 py-3 border-t border-slate-200/80 bg-[#f8fafc] flex justify-end gap-2">
          <button
            onClick={onClose}
            disabled={busy}
            className="px-4 py-1.5 rounded-lg border border-slate-200/80 bg-white text-xs text-foreground hover:bg-[#f6f9ff] disabled:opacity-50 transition-colors"
          >
            取消
          </button>
          <button
            onClick={handleSubmit}
            disabled={busy}
            className="px-4 py-1.5 rounded-lg bg-[#1c64ff] text-white text-xs shadow-[0_6px_14px_rgba(28,100,255,0.22)] hover:bg-[#1857e8] hover:shadow-[0_8px_18px_rgba(28,100,255,0.28)] disabled:opacity-50 flex items-center gap-1.5 transition-all"
          >
            {busy && <Loader2 className="w-3 h-3 animate-spin" />}
            创建并生成
          </button>
        </div>
      </div>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────────────────
// PromptDetailsModal — full prompt content
// ────────────────────────────────────────────────────────────────────────────
interface PromptDetailsModalProps {
  text: string;
  agentInfo: Array<{ label: string; value: string; multiline?: boolean }>;
  onClose: () => void;
}
function PromptDetailsModal({ text, agentInfo, onClose }: PromptDetailsModalProps) {
  const trimmed = text.trim();
  return (
    <div className="fixed inset-0 bg-slate-950/55 backdrop-blur-sm z-50 flex items-center justify-center p-4" onClick={onClose}>
      <div
        className="bg-white rounded-2xl shadow-[0_24px_70px_rgba(15,23,42,0.28)] ring-1 ring-white/60 w-full max-w-lg max-h-[80vh] flex flex-col overflow-hidden"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="flex items-center justify-between px-5 py-3 border-b border-slate-200/80 bg-[#f8fafc]">
          <h3 className="text-sm font-semibold text-foreground">分镜提示词全文</h3>
          <button onClick={onClose} className="p-1 hover:bg-white rounded-lg transition-colors">
            <X className="w-4 h-4 text-muted-foreground" />
          </button>
        </div>
        <div className="flex-1 overflow-y-auto p-5 space-y-4">
          {trimmed ? (
            <pre className="text-sm text-foreground leading-relaxed whitespace-pre-wrap font-mono rounded-xl border border-slate-200/80 bg-[#f8fafc] p-4">
              {trimmed}
            </pre>
          ) : (
            <p className="text-sm text-muted-foreground italic">（未填写提示词）</p>
          )}
          {agentInfo.length > 0 && (
            <div className="rounded-xl border border-slate-200/80 bg-white divide-y divide-slate-100 overflow-hidden">
              <div className="px-3 py-2 flex items-center justify-between bg-[#f8fafc]">
                <span className="text-xs font-medium text-foreground">Agent 分镜信息</span>
                <span className="text-[10px] text-muted-foreground">仅作参考</span>
              </div>
              {agentInfo.map((item) => (
                <StoryboardInfoField
                  key={item.label}
                  label={item.label}
                  value={item.value}
                  multiline={item.multiline}
                />
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}
