"use client"
import { forwardRef, useEffect, useImperativeHandle, useRef, useState, type ReactNode } from "react"
import { createPortal } from "react-dom"
import {
  Link2, Upload, Download, Scissors, Image as ImageIcon,
  Mic, Languages, FileEdit, Sparkles, Film, FileVideo, Loader2, Plus, Check,
  ChevronDown, X, LayoutGrid,
} from "lucide-react"
import { type Job, type KeyFrame, frameUrl, effectiveFrameUrl, videoUrl, generateImage, selectGenerated, generatedImageUrl } from "@/lib/api"
import { type NodeData } from "@/components/nodes"
import { FrameLightbox } from "@/components/lightbox"
import { toast } from "sonner"

type ColType = "input" | "process" | "ai" | "output"
const TYPE_GRAD: Record<ColType, string> = {
  input: "linear-gradient(135deg, #6366f1, #a855f7)",
  process: "linear-gradient(135deg, #f59e0b, #ef4444)",
  ai: "linear-gradient(135deg, #d946ef, #ec4899)",
  output: "linear-gradient(135deg, #10b981, #06b6d4)",
}

type ColState = "pending" | "running" | "done" | "failed"
const STATE_DOT: Record<ColState, string> = {
  pending: "bg-white/25",
  running: "bg-violet-300 shadow-[0_0_8px_rgba(167,139,250,0.8)] animate-pulse",
  done: "bg-emerald-300 shadow-[0_0_8px_rgba(110,231,183,0.7)]",
  failed: "bg-red-400 shadow-[0_0_8px_rgba(248,113,113,0.7)]",
}

function MiniCard({ children, className = "", onClick }: { children: ReactNode; className?: string; onClick?: (e: React.MouseEvent) => void }) {
  return (
    <div
      className={`rounded-lg bg-white/[0.04] dark:bg-white/[0.04] border border-white/10 backdrop-blur-md p-2.5 text-[var(--text-strong)] ${onClick ? "cursor-pointer hover:bg-white/[0.07] hover:border-white/20 transition" : ""} ${className}`}
      onClick={onClick}
    >
      {children}
    </div>
  )
}

type KanbanTone = "violet" | "pink" | "orange" | "blue" | "green" | "cyan" | "rose" | "amber"

function KanbanCard({
  tone = "violet",
  tags,
  title,
  className = "",
  onClick,
  meta,
  children,
}: {
  tone?: KanbanTone
  tags?: string[]
  title?: ReactNode
  className?: string
  onClick?: (e: React.MouseEvent) => void
  meta?: ReactNode
  children?: ReactNode
}) {
  return (
    <div className={`kanban-card kanban-${tone} ${onClick ? "cursor-pointer" : ""} ${className}`} onClick={onClick}>
      {tags && tags.length > 0 && (
        <div className="flex items-center gap-1.5 mb-2 flex-wrap">
          {tags.map((t) => (
            <span key={t} className="kanban-tag">#{t}</span>
          ))}
        </div>
      )}
      {title && <h3 className="text-[13.5px] font-semibold text-[var(--text-strong)] leading-snug mb-1.5">{title}</h3>}
      {children}
      {meta && <div className="kanban-meta">{meta}</div>}
    </div>
  )
}

interface Props {
  data: NodeData
}

export interface DashboardHandle {
  openPanel: (key: string) => void
}

export const Dashboard = forwardRef<DashboardHandle, Props>(function Dashboard({ data }, ref) {
  const { job } = data
  const [url, setUrl] = useState("")
  const [videoT, setVideoT] = useState(0)
  const [addingFrame, setAddingFrame] = useState(false)
  const [expanded, setExpanded] = useState<Set<string>>(new Set())
  const [mounted, setMounted] = useState(false)
  useEffect(() => setMounted(true), [])
  useImperativeHandle(ref, () => ({
    openPanel: (key: string) => setExpanded(new Set([key])),
  }), [])

  // drawer 宽度 per panel · 持久化到 localStorage
  const drawerKey = (() => {
    const keys = Array.from(expanded)
    if (keys.length === 0) return ""
    const base = keys[0]
    if (base === "keyframe" && data.expandedFrame !== null) return "keyframe:lightbox"
    return base
  })()
  const isLightboxMode = drawerKey === "keyframe:lightbox"
  const defaultWidth = isLightboxMode ? 760 : 400
  const minDrawerWidth = isLightboxMode ? 480 : 280
  const [drawerWidths, setDrawerWidths] = useState<Record<string, number>>({})
  useEffect(() => {
    if (typeof window === "undefined") return
    try {
      const stored = JSON.parse(localStorage.getItem("skg.drawer.widths") || "{}")
      if (stored && typeof stored === "object") setDrawerWidths(stored)
    } catch { /* ignore */ }
  }, [])
  const drawerWidth = drawerKey ? (drawerWidths[drawerKey] ?? defaultWidth) : defaultWidth
  const setDrawerWidth = (w: number) => {
    if (!drawerKey) return
    const clamped = Math.max(minDrawerWidth, Math.min(1400, w))
    setDrawerWidths((prev) => {
      const next = { ...prev, [drawerKey]: clamped }
      try { localStorage.setItem("skg.drawer.widths", JSON.stringify(next)) } catch { /* ignore */ }
      return next
    })
  }

  // 拖拽
  const dragRef = useRef<{ startX: number; startW: number } | null>(null)
  const onResizeMouseDown = (e: React.MouseEvent) => {
    e.preventDefault()
    e.stopPropagation()
    dragRef.current = { startX: e.clientX, startW: drawerWidth }
    const onMove = (ev: MouseEvent) => {
      if (!dragRef.current) return
      const delta = ev.clientX - dragRef.current.startX
      setDrawerWidth(dragRef.current.startW + delta)
    }
    const onUp = () => {
      dragRef.current = null
      window.removeEventListener("mousemove", onMove)
      window.removeEventListener("mouseup", onUp)
    }
    window.addEventListener("mousemove", onMove)
    window.addEventListener("mouseup", onUp)
  }
  const tileRefs = useRef<Record<string, HTMLButtonElement | null>>({})
  const fileRef = useRef<HTMLInputElement>(null)
  const videoRef = useRef<HTMLVideoElement>(null)

  /* 状态推导 */
  const hasVideo = !!job?.video_url
  const isDownloading = job?.status === "downloading" || job?.status === "created"
  const isSplitting = job?.status === "splitting"
  const isAnalyzing = !!job && ["splitting", "frames_extracted", "transcribing"].includes(job.status)
  const hasFrames = (job?.frames.length ?? 0) > 0
  const hasTranscript = (job?.transcript.length ?? 0) > 0
  const hasZh = job?.transcript.some((s) => s.zh) ?? false
  const isFailed = job?.status === "failed"

  const colState: Record<string, ColState> = {
    // input 状态合并：覆盖原 input + download + split
    input: !job ? "pending"
      : isDownloading ? "running"
      : isSplitting ? "running"
      : hasFrames || hasTranscript ? "done"
      : hasVideo ? "done"
      : isFailed && job.progress < 50 ? "failed"
      : "pending",
    keyframe: !job ? "pending" : (isSplitting && !hasFrames) ? "running" : hasFrames ? "done" : isFailed && job.progress >= 50 && job.progress < 70 ? "failed" : "pending",
    asr: !job ? "pending" : job.status === "transcribing" ? "running" : hasTranscript ? "done" : isFailed && job.progress >= 70 ? "failed" : "pending",
    translate: !job ? "pending" : job.status === "transcribing" ? "running" : hasZh ? "done" : "pending",
    rewrite: "pending",
    imagegen: "pending",
    videogen: "pending",
    compose: "pending",
  }

  /* 每列摘要 = tile 副标题 */
  const colSummary: Record<string, string> = {
    input: !job ? "等待" : isDownloading ? "下载中…" : hasVideo ? `${job.width}×${job.height} · ${job.duration.toFixed(1)}s` : "—",
    keyframe: hasFrames ? `${data.selectedFrames.size}/${job!.frames.length} 选用` : "—",
    asr: hasTranscript ? `${job!.transcript.length} 段` : "—",
    translate: hasZh ? `${job!.transcript.filter((s) => s.zh).length} 段` : "—",
    rewrite: "占位",
    imagegen: data.selectedFrames.size > 0 ? `${data.selectedFrames.size} 帧待编排` : "占位",
    videogen: "占位",
    compose: "占位",
  }

  const TILES: Array<{ key: string; title: string; type: ColType; icon: ReactNode; step: number }> = [
    { key: "input", title: "输入", type: "input", icon: <Link2 className="h-3.5 w-3.5" />, step: 1 },
    { key: "keyframe", title: "关键帧", type: "ai", icon: <ImageIcon className="h-3.5 w-3.5" />, step: 2 },
    { key: "asr", title: "转录", type: "ai", icon: <Mic className="h-3.5 w-3.5" />, step: 3 },
    { key: "translate", title: "翻译", type: "ai", icon: <Languages className="h-3.5 w-3.5" />, step: 4 },
    { key: "rewrite", title: "改写", type: "ai", icon: <FileEdit className="h-3.5 w-3.5" />, step: 5 },
    // imagegen（分镜头编排）已移到顶部 StoryboardBar，不在 sidebar 里
    { key: "videogen", title: "生视频", type: "ai", icon: <Film className="h-3.5 w-3.5" />, step: 7 },
    { key: "compose", title: "合成", type: "output", icon: <FileVideo className="h-3.5 w-3.5" />, step: 8 },
  ]

  // 单选展开：toggle 同一 key = 收起；点其他 key = 切换
  const toggleTile = (key: string) => {
    setExpanded((prev) => (prev.has(key) ? new Set() : new Set([key])))
  }
  const closeTile = (_key: string) => {
    setExpanded(new Set())
    data.onCloseExpandedFrame()
  }

  // 点关键帧缩略图时（onExpandFrame 触发），自动打开 keyframe drawer
  useEffect(() => {
    if (data.expandedFrame !== null && !expanded.has("keyframe")) {
      setExpanded(new Set(["keyframe"]))
    }
  }, [data.expandedFrame])

  const Tile = ({ tkey }: { tkey: string }) => {
    const t = TILES.find((x) => x.key === tkey)!
    const state = colState[t.key]
    const isOpen = expanded.has(t.key)
    return (
      <button
        ref={(el) => { tileRefs.current[t.key] = el }}
        type="button"
        onClick={() => toggleTile(t.key)}
        title={`${t.title} · ${colSummary[t.key]}`}
        className={`group w-full rounded-md overflow-hidden border flex items-center transition relative ${
          isOpen
            ? "bg-white/[0.09] border-white/25 shadow-[0_0_0_1px_rgba(255,255,255,0.08),0_4px_16px_-4px_rgba(0,0,0,0.5)]"
            : "bg-white/[0.035] border-white/8 hover:bg-white/[0.06] hover:border-white/15"
        }`}
        style={{ height: 26 }}
      >
        <span
          className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] rounded-r-full transition-all"
          style={{
            height: isOpen ? "72%" : "44%",
            background: TYPE_GRAD[t.type],
            opacity: isOpen ? 1 : 0.78,
          }}
        />
        <div className="pl-2.5 pr-1.5 flex items-center gap-1.5 flex-1 min-w-0">
          <span className={`shrink-0 transition ${isOpen ? "text-white" : "text-white/70 group-hover:text-white/90"}`}>{t.icon}</span>
          <span className={`text-[10.5px] font-medium truncate transition ${isOpen ? "text-white" : "text-white/85"}`}>{t.title}</span>
        </div>
        <span className={`absolute right-1.5 h-1.5 w-1.5 rounded-full ${STATE_DOT[state]}`} />
      </button>
    )
  }

  return (
    <div className="h-full flex flex-col gap-1 px-1.5 py-2">
      {/* 起点：输入（含下载+拆分） */}
      <Tile tkey="input" />
      {/* 分叉：上路 关键帧 / 生视频（分镜头编排在顶部 bar） */}
      <div className="border-l border-violet-400/25 pl-1 ml-[3px] space-y-1">
        <Tile tkey="keyframe" />
        <Tile tkey="videogen" />
      </div>
      {/* 分叉：下路 转录/翻译/改写 */}
      <div className="border-l border-pink-400/25 pl-1 ml-[3px] space-y-1">
        <Tile tkey="asr" />
        <Tile tkey="translate" />
        <Tile tkey="rewrite" />
      </div>
      {/* 合流 */}
      <Tile tkey="compose" />

      {/* 展开面板 — keyframe 有选中帧时变宽容纳 lightbox · 可拖拽改宽 */}
      {expanded.size > 0 && mounted && createPortal(
        <div
          className="fixed z-[100]"
          style={{
            left: 24,
            top: 16,
            bottom: 16,
            width: drawerWidth,
          }}
        >
          {TILES.filter((t) => expanded.has(t.key)).map((t) => {
            const isKeyframeWithExpand = t.key === "keyframe" && data.expandedFrame !== null
            return (
            <section
              key={t.key}
              className="rounded-2xl border border-white/15 bg-black/60 backdrop-blur-2xl overflow-hidden flex flex-col h-full relative"
              style={{
                animation: "drawer-in 0.24s cubic-bezier(0.32, 0.72, 0, 1)",
                boxShadow: "0 30px 80px -20px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.05)",
              }}
            >
              {/* 右边拖拽手柄 — hover 时显示 */}
              <div
                onMouseDown={onResizeMouseDown}
                title="拖动改变宽度（双击重置）"
                onDoubleClick={() => setDrawerWidth(defaultWidth)}
                className="group/rs absolute top-0 bottom-0 right-0 w-1.5 cursor-col-resize hover:bg-violet-400/30 transition z-[110]"
              >
                <div className="absolute top-1/2 -translate-y-1/2 right-0 w-1 h-12 bg-white/0 group-hover/rs:bg-violet-400/70 rounded-l transition" />
              </div>
              <div className="flex items-center justify-between px-3 py-2" style={{ background: TYPE_GRAD[t.type] }}>
                <div className="flex items-center gap-1.5">
                  <span className="text-white/70 text-[9px] font-mono">{String(t.step).padStart(2, "0")}</span>
                  <span className="text-white">{t.icon}</span>
                  <span className="text-[12.5px] font-semibold text-white">{t.title}</span>
                  <span className="text-[10px] text-white/70 ml-2">{colSummary[t.key]}</span>
                </div>
                <button
                  onClick={() => closeTile(t.key)}
                  className="text-white/70 hover:text-white"
                  title="收起"
                >
                  <X className="h-3.5 w-3.5" />
                </button>
              </div>
              <div className="overflow-y-auto p-3 bg-black/20 flex-1">
                {isKeyframeWithExpand && data.job ? (
                  <FrameLightbox
                    embedded
                    jobId={data.job.id}
                    frames={data.job.frames}
                    activeIndex={data.expandedFrame}
                    selected={data.selectedFrames}
                    onClose={data.onCloseExpandedFrame}
                    onChange={data.onExpandFrame}
                    onToggleSelect={data.onToggleFrame}
                    onJobUpdate={data.onJobUpdate}
                    onSwitchPanel={(key) => {
                      data.onCloseExpandedFrame()
                      setExpanded(new Set([key]))
                    }}
                    onCopyImage={data.onCopyImage}
                  />
                ) : (
                  renderSection(t.key)
                )}
              </div>
            </section>
            )
          })}
        </div>,
        document.body
      )}
    </div>
  )

  function renderSection(key: string): ReactNode {
    return (
      <div className="space-y-3">

            {/* ---- Input — Kanban ---- */}
            {key === "input" && (
              <>
                <KanbanCard tone="violet" tags={["链接", "上传"]} title="输入源">
                  <input
                    value={url}
                    onChange={(e) => setUrl(e.target.value)}
                    placeholder="粘贴 TikTok 链接"
                    disabled={isDownloading || data.submitting}
                    className="w-full text-[12px] px-2.5 py-1.5 rounded-md bg-black/30 border border-white/15 outline-none text-[var(--text-strong)] placeholder:text-[var(--text-faint)] focus:ring-2 focus:ring-[var(--ring)] disabled:opacity-40 mt-1"
                  />
                  <div className="mt-2 flex gap-1.5">
                    <button
                      type="button"
                      disabled={isDownloading || data.submitting || !url.trim()}
                      onClick={() => data.onSubmitUrl(url.trim())}
                      className="flex-1 text-[11.5px] py-1.5 rounded-md bg-white text-black hover:bg-white/90 disabled:opacity-30 inline-flex items-center justify-center gap-1"
                    >
                      {(data.submitting || isDownloading) && <Loader2 className="h-3 w-3 animate-spin" />}
                      {isDownloading ? "下载中" : "提交"}
                    </button>
                    <button
                      type="button"
                      disabled={isDownloading || data.submitting}
                      onClick={() => fileRef.current?.click()}
                      className="text-[11.5px] px-2.5 py-1.5 rounded-md bg-white/[0.06] border border-white/15 hover:bg-white/[0.12] inline-flex items-center gap-1 disabled:opacity-30"
                    >
                      <Upload className="h-3 w-3" /> 上传
                    </button>
                    <input
                      ref={fileRef}
                      type="file"
                      accept="video/mp4,video/quicktime,video/webm,.mp4,.mov,.webm,.mkv,.m4v"
                      className="hidden"
                      onChange={(e) => {
                        const f = e.target.files?.[0]
                        if (f) data.onUploadFile(f)
                        e.target.value = ""
                      }}
                    />
                  </div>
                </KanbanCard>
                {hasVideo && job && (
                  <>
                    {/* 元数据 */}
                    <KanbanCard tone="orange" tags={["视频源", job.url.startsWith("upload://") ? "上传" : "yt-dlp"]} title={`${job.width}×${job.height} · ${job.duration.toFixed(1)}s`}>
                      <div className="text-[10.5px] text-[var(--text-soft)] truncate font-mono">
                        {job.url.startsWith("upload://") ? `📎 ${job.url.slice(9)}` : `🔗 ${job.url}`}
                      </div>
                    </KanbanCard>
                    {/* 拆分流 */}
                    <KanbanCard tone="amber" tags={["ffmpeg"]} title="拆分音视频流">
                      <div className="grid grid-cols-2 gap-2 text-[11px]">
                        <div className="rounded-md bg-violet-500/10 border border-violet-400/20 px-2 py-1.5">
                          <div className="text-[9.5px] uppercase tracking-widest text-violet-300">视频流</div>
                          <div className="text-[var(--text-strong)]">→ 关键帧</div>
                        </div>
                        <div className="rounded-md bg-pink-500/10 border border-pink-400/20 px-2 py-1.5">
                          <div className="text-[9.5px] uppercase tracking-widest text-pink-300">音频流</div>
                          <div className="text-[var(--text-strong)]">→ ASR (wav)</div>
                        </div>
                      </div>
                    </KanbanCard>
                    {/* 解析按钮 */}
                    <KanbanCard tone="green" tags={["下一步"]} title={hasFrames ? "重新解析" : "启动全流程"}>
                      <button
                        type="button"
                        disabled={isAnalyzing || data.analyzing}
                        onClick={data.onAnalyze}
                        className={`w-full text-[12.5px] py-2 rounded-md bg-gradient-to-r from-indigo-500 to-violet-500 text-white hover:opacity-95 disabled:opacity-40 inline-flex items-center justify-center gap-1.5 font-semibold shadow-lg shadow-violet-500/30 mt-1 ${!isAnalyzing && !data.analyzing && !hasFrames ? "animate-pulse" : ""}`}
                      >
                        {(isAnalyzing || data.analyzing) ? <><Loader2 className="h-3.5 w-3.5 animate-spin" /> 解析中…</> : hasFrames ? "↻ 重新解析" : "▶ 开始解析（全流程）"}
                      </button>
                      <div className="kanban-meta">
                        全流程 = 拆分 → 抽帧 → ASR → 翻译
                      </div>
                    </KanbanCard>
                  </>
                )}
              </>
            )}

            {/* ---- Download — 只显示元数据，视频播放器移到 Keyframe section ---- */}
            {key === "download" && (
              !hasVideo ? (
                <KanbanCard tone="orange" tags={["yt-dlp"]} title={isDownloading ? "下载中…" : "等待提交"}>
                  <div className="text-[11.5px] text-[var(--text-soft)]">TikTok / yt-dlp 兼容站点</div>
                </KanbanCard>
              ) : (
                <>
                  <KanbanCard tone="orange" tags={["视频源", job!.url.startsWith("upload://") ? "上传" : "yt-dlp"]} title={job!.url.startsWith("upload://") ? "本地上传" : "TK 下载"}>
                    <div className="text-[11px] text-[var(--text-soft)] truncate font-mono mt-1">
                      {job!.url.startsWith("upload://") ? `📎 ${job!.url.slice(9)}` : `🔗 ${job!.url}`}
                    </div>
                  </KanbanCard>
                  <KanbanCard tone="amber" tags={["元数据"]} title="视频信息">
                    <div className="grid grid-cols-3 gap-3 text-[11px] font-mono">
                      <div><div className="text-[var(--text-faint)] text-[9.5px]">分辨率</div><div className="text-[var(--text-strong)] text-[12.5px] mt-0.5">{job!.width}×{job!.height}</div></div>
                      <div><div className="text-[var(--text-faint)] text-[9.5px]">时长</div><div className="text-[var(--text-strong)] text-[12.5px] mt-0.5">{job!.duration.toFixed(1)}s</div></div>
                      <div><div className="text-[var(--text-faint)] text-[9.5px]">大小</div><div className="text-[var(--text-strong)] text-[12.5px] mt-0.5">~9MB</div></div>
                    </div>
                    <div className="kanban-meta">
                      要看视频 / 选帧请打开「关键帧」
                    </div>
                  </KanbanCard>
                </>
              )
            )}

            {/* ---- Split — Kanban ---- */}
            {key === "split" && (
              <>
                <KanbanCard tone="amber" tags={["ffmpeg", "视频流"]} title="→ 关键帧抽取">
                  <div className="text-[11px] text-[var(--text-soft)]">fast seek + Laplacian 方差评分</div>
                </KanbanCard>
                <KanbanCard tone="amber" tags={["ffmpeg", "音频流"]} title="→ ASR 输入">
                  <div className="text-[11px] text-[var(--text-soft)]">16kHz mono · pcm_s16le wav</div>
                  <div className="kanban-meta">
                    <code className="text-[10px]">-vn -ac 1 -ar 16000</code>
                  </div>
                </KanbanCard>
              </>
            )}

            {/* ---- Keyframe — Kanban 卡片（视频播放器 + 加帧 + 缩略图都在这里） ---- */}
            {key === "keyframe" && (
              <div className="space-y-3">
                {hasVideo && job && (
                  <>
                    <KanbanCard tone="violet" tags={["视频"]} title="拖时间轴选帧">
                      <video
                        ref={videoRef}
                        src={videoUrl(job.id)}
                        controls
                        onTimeUpdate={(e) => setVideoT((e.target as HTMLVideoElement).currentTime)}
                        className="block w-full bg-black rounded-md mt-1"
                      />
                      <div className="kanban-meta">
                        <span className="font-mono">当前 {videoT.toFixed(2)}s</span>
                        <span className="ml-auto">{job.width}×{job.height} · {job.duration.toFixed(1)}s</span>
                      </div>
                    </KanbanCard>
                    <KanbanCard tone="green" tags={["手动加帧"]} title="把当前时间点抽为关键帧">
                      <button
                        type="button"
                        disabled={addingFrame}
                        onClick={async () => {
                          const t = videoRef.current?.currentTime ?? videoT
                          setAddingFrame(true)
                          try { await data.onAddManualFrame(t) } finally { setAddingFrame(false) }
                        }}
                        className="mt-1 w-full text-[12.5px] py-2 rounded-md bg-emerald-500 hover:bg-emerald-400 text-white disabled:opacity-50 inline-flex items-center justify-center gap-1.5 font-medium"
                      >
                        {addingFrame ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />}
                        {addingFrame ? "抽帧中…" : `+ 把 ${videoT.toFixed(1)}s 加为关键帧`}
                      </button>
                    </KanbanCard>
                  </>
                )}
                {!hasFrames ? (
                  <KanbanCard tone="pink" tags={["分镜"]} title="等待解析后抽取">
                    <div className="text-[11.5px] text-[var(--text-soft)]">候选 30 张 → pHash 去重 + 清晰度排序 → 时序分桶 → 5 张代表分镜</div>
                  </KanbanCard>
                ) : (
                  job!.frames.map((f) => {
                    const isSel = data.selectedFrames.has(f.index)
                    const cleaned = !!f.cleaned_url
                    const elCount = f.elements?.length ?? 0
                    const cutCount = f.elements?.filter((e) => e.cutout_id).length ?? 0
                    const tags = [`分镜 ${f.index + 1}`, `${f.timestamp.toFixed(1)}s`]
                    if (cleaned) tags.push("已清洗")
                    if (cutCount > 0) tags.push(`${cutCount}/${elCount} 抠图`)
                    return (
                      <KanbanCard
                        key={f.index}
                        tone={isSel ? "green" : "pink"}
                        tags={tags}
                        className={isSel ? "ring-2 ring-emerald-400/60" : ""}
                        meta={
                          <button
                            type="button"
                            onClick={(e) => { e.stopPropagation(); data.onToggleFrame(f.index) }}
                            className={`ml-auto text-[10.5px] px-2 py-0.5 rounded-full inline-flex items-center gap-1 ${
                              isSel
                                ? "bg-emerald-500 text-white"
                                : "bg-white/10 text-[var(--text-soft)] border border-white/15 hover:bg-white/20"
                            }`}
                          >
                            <Check className="h-2.5 w-2.5" />
                            {isSel ? "已选用" : "选用此帧"}
                          </button>
                        }
                      >
                        <button
                          type="button"
                          onClick={(e) => { e.stopPropagation(); data.onExpandFrame(f.index) }}
                          className="block w-full rounded-md overflow-hidden bg-black"
                          title="点击放大 · 清洗 / 提取元素"
                        >
                          <img
                            src={effectiveFrameUrl(job!.id, f)}
                            alt={`frame ${f.index}`}
                            className="block w-full h-auto"
                            style={{ objectFit: "contain" }}
                          />
                        </button>
                      </KanbanCard>
                    )
                  })
                )}
              </div>
            )}

            {/* ---- ASR / Translate — Kanban 段落卡 ---- */}
            {(key === "asr" || key === "translate") && (
              !hasTranscript ? (
                <KanbanCard tone={key === "asr" ? "blue" : "cyan"} tags={[key === "asr" ? "ASR" : "Translate"]} title="等待数据">
                  <div className="text-[11.5px] text-[var(--text-soft)]">
                    {colState.asr === "running" ? "Gemini 转录中…" : "需要先完成关键帧抽取"}
                  </div>
                </KanbanCard>
              ) : (
                job!.transcript.map((s) => (
                  <KanbanCard
                    key={s.index}
                    tone={key === "asr" ? "blue" : "cyan"}
                    tags={[`段落 ${s.index + 1}`, `${s.start.toFixed(1)}s → ${s.end.toFixed(1)}s`]}
                  >
                    <div className="text-[12.5px] text-[var(--text-strong)] leading-snug mb-1.5">
                      <span className="kanban-tag mr-1.5" style={{ padding: "1px 6px", fontSize: 9.5 }}>EN</span>
                      {s.en}
                    </div>
                    <div className="text-[12.5px] text-[var(--text-strong)] leading-snug">
                      <span className="kanban-tag mr-1.5" style={{ padding: "1px 6px", fontSize: 9.5 }}>ZH</span>
                      {s.zh || <span className="text-[var(--text-faint)] italic">翻译中…</span>}
                    </div>
                  </KanbanCard>
                ))
              )
            )}

            {/* ---- Rewrite — Kanban ---- */}
            {key === "rewrite" && (
              <>
                <KanbanCard tone="green" tags={["产品信息"]} title="SKG 产品卖点">
                  <textarea
                    rows={5}
                    placeholder="粘贴 SKG 产品关键卖点（占位）"
                    disabled
                    className="w-full text-[12px] px-2 py-1.5 rounded-md bg-black/30 border border-dashed border-white/10 placeholder:text-[var(--text-faint)] text-[var(--text-strong)] resize-none opacity-70 mt-1"
                  />
                </KanbanCard>
                <KanbanCard tone="green" tags={["模型"]} title="gemini-2.5-pro">
                  <div className="text-[11px] text-[var(--text-soft)]">按英文转录 + 产品信息 → 输出改写中文文案</div>
                  <div className="kanban-meta">下一冲刺接入</div>
                </KanbanCard>
              </>
            )}

            {/* ---- 分镜头编排 — 基于关键帧 + 元素 + 场景图编排（Phase 2 重写） ---- */}
            {key === "imagegen" && (
              data.selectedFrames.size === 0 ? (
                <KanbanCard tone="pink" tags={["待启动"]} title="未选关键帧">
                  <div className="text-[11px] text-[var(--text-soft)]">在「关键帧」节点勾选 1+ 张后，每张关键帧 → 1 张生成图</div>
                </KanbanCard>
              ) : (
                Array.from(data.selectedFrames).sort((a, b) => a - b).map((frameIdx) => {
                  const f = data.job?.frames.find((x) => x.index === frameIdx)
                  if (!f || !data.job) return null
                  return <ImageGenCard key={frameIdx} job={data.job} frame={f} onJobUpdate={data.onJobUpdate} />
                })
              )
            )}

            {/* ---- VideoGen — Kanban ---- */}
            {key === "videogen" && (
              <>
                <KanbanCard tone="violet" tags={["SKG 网关"]} title="Sora 2">
                  <div className="text-[11px] text-[var(--text-soft)]">/v1/videos 待开通（IT）</div>
                </KanbanCard>
                <KanbanCard tone="violet" tags={["外部"]} title="Seedance">
                  <div className="text-[11px] text-[var(--text-soft)]">字节跳动 · 需独立 API key</div>
                </KanbanCard>
                <KanbanCard tone="violet" tags={["外部"]} title="Kling">
                  <div className="text-[11px] text-[var(--text-soft)]">快手 · 需独立 API key</div>
                </KanbanCard>
                <KanbanCard tone="rose" tags={["占位"]} title="生成视频列表">
                  <div className="text-[11px] text-[var(--text-soft)]">每张生成图 → 1 段 5-10s 视频，按改写文案合成 prompt</div>
                </KanbanCard>
              </>
            )}

            {/* ---- Compose — Kanban ---- */}
            {key === "compose" && (
              <>
                <KanbanCard tone="cyan" tags={["成品"]} title="最终视频">
                  <div className="aspect-video bg-black/40 rounded-md flex items-center justify-center text-[11.5px] text-[var(--text-faint)] mt-1">
                    待合成
                  </div>
                </KanbanCard>
                <KanbanCard tone="cyan" tags={["ffmpeg"]} title="字幕 + TTS 合成">
                  <div className="text-[11px] text-[var(--text-soft)]">视频片段 + 字幕（改写中文）+ TTS 配音 → 最终 mp4</div>
                  <div className="kanban-meta">完全本地 · 零 API 调用</div>
                </KanbanCard>
              </>
            )}
      </div>
    )
  }
})

/* ============================================================
   ImageGenCard — 单张关键帧的生图卡
   ============================================================ */
function ImageGenCard({ job, frame, onJobUpdate }: {
  job: Job
  frame: KeyFrame
  onJobUpdate: (j: Job) => void
}) {
  const [extra, setExtra] = useState("")
  const [negative, setNegative] = useState("水印, @用户名, TikTok logo, 平台文字, 浮水印")
  const [model, setModel] = useState("gemini-3-pro-image-preview")
  const [mode, setMode] = useState<"edit" | "text">("edit")
  const [generating, setGenerating] = useState(false)
  const basePrompt = frame.description?.suggested_prompt ?? "(尚未识别 · 点关键帧打开 lightbox 先识别)"
  const [editablePrompt, setEditablePrompt] = useState(basePrompt)
  const [showPrompt, setShowPrompt] = useState(false)
  const [previewGenId, setPreviewGenId] = useState<string | null>(null)
  const [previewMounted, setPreviewMounted] = useState(false)
  useEffect(() => setPreviewMounted(true), [])

  // 当 vision 识别完成后更新默认 prompt
  useEffect(() => {
    if (frame.description?.suggested_prompt) {
      setEditablePrompt(frame.description.suggested_prompt)
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [frame.description?.suggested_prompt])

  const handleGenerate = async () => {
    if (!editablePrompt.trim()) {
      toast.error("请先填写 prompt（点上方关键帧识别会自动生成）")
      return
    }
    setGenerating(true)
    try {
      const updated = await generateImage(job.id, frame.index, {
        prompt: editablePrompt,
        extra_prompt: extra,
        negative_prompt: negative,
        model,
        mode,
      })
      onJobUpdate(updated)
      toast.success(`分镜 ${frame.index + 1} 生成完成`)
    } catch (e) {
      toast.error("生图失败：" + (e instanceof Error ? e.message : String(e)))
    } finally {
      setGenerating(false)
    }
  }

  const handleSelectGen = async (genId: string, currentlySelected: boolean) => {
    try {
      const updated = await selectGenerated(job.id, frame.index, genId, !currentlySelected)
      onJobUpdate(updated)
    } catch (e) {
      toast.error("选用失败：" + (e instanceof Error ? e.message : String(e)))
    }
  }

  const gens = frame.generated_images ?? []
  const objects = frame.description?.objects ?? []

  return (
    <KanbanCard tone="rose" tags={[`分镜 ${frame.index + 1}`, `${frame.timestamp.toFixed(1)}s`, "占位 · Phase 2 重写"]} title="单帧编排（旧生图）">
      {/* 参考图 + 识别物体 chips */}
      <div className="flex gap-2 items-start mt-1">
        <img
          src={effectiveFrameUrl(job.id, frame)}
          alt={`frame ${frame.index}`}
          className="rounded-md object-cover flex-shrink-0"
          style={{ width: 96, aspectRatio: `${job.width}/${job.height}` }}
        />
        <div className="flex-1 min-w-0">
          <div className="text-[10px] text-[var(--text-faint)] uppercase tracking-widest mb-1">识别元素</div>
          {objects.length > 0 ? (
            <div className="flex flex-wrap gap-1">
              {objects.slice(0, 6).map((o, i) => (
                <button
                  key={i}
                  onClick={() => setExtra((p) => p ? `${p}, ${o.name}` : o.name)}
                  className="text-[10px] px-1.5 py-0.5 rounded-full bg-white/[0.06] hover:bg-white/[0.12] border border-white/15 text-[var(--text-strong)]"
                  title="点击加入需求"
                >
                  + {o.name}
                </button>
              ))}
            </div>
          ) : (
            <div className="text-[10px] text-[var(--text-faint)] italic">未识别（点上方缩略图打开识别）</div>
          )}
        </div>
      </div>

      {/* 画面描述（AI 自动 · 可展开编辑） */}
      <div className="mt-2.5">
        <button
          type="button"
          onClick={() => setShowPrompt((v) => !v)}
          className="w-full text-[10px] text-[var(--text-faint)] hover:text-[var(--text-strong)] inline-flex items-center justify-between"
        >
          <span>画面描述（AI 自动 · 看图给的英文 prompt） {showPrompt ? "▼" : "▶"}</span>
          <span className="font-mono">{editablePrompt.length} 字符</span>
        </button>
        {showPrompt && (
          <textarea
            value={editablePrompt}
            onChange={(e) => setEditablePrompt(e.target.value)}
            rows={3}
            className="mt-1 w-full text-[11px] px-2 py-1.5 rounded-md bg-black/30 border border-white/10 text-[var(--text-strong)] resize-none focus:ring-1 focus:ring-rose-400/40 outline-none font-mono"
          />
        )}
      </div>

      {/* 正向：我要保留 / 加入 */}
      <div className="mt-2">
        <div className="flex items-center justify-between mb-1">
          <span className="text-[10px] uppercase tracking-widest text-emerald-300">✓ 需要保留 / 加入</span>
        </div>
        <textarea
          value={extra}
          onChange={(e) => setExtra(e.target.value)}
          rows={2}
          placeholder="例：加 SKG logo、保留瓶子、换实验室背景"
          className="w-full text-[11.5px] px-2 py-1.5 rounded-md bg-emerald-500/5 border border-emerald-400/30 text-[var(--text-strong)] placeholder:text-[var(--text-faint)] resize-none focus:ring-2 focus:ring-emerald-400/40 outline-none"
        />
      </div>

      {/* 负向：不需要 / 删除 */}
      <div className="mt-2">
        <div className="flex items-center justify-between mb-1">
          <span className="text-[10px] uppercase tracking-widest text-rose-300">✗ 不要 / 删除</span>
          <button
            onClick={() => setNegative("")}
            className="text-[9px] text-[var(--text-faint)] hover:text-[var(--text-strong)]"
          >
            清空
          </button>
        </div>
        <textarea
          value={negative}
          onChange={(e) => setNegative(e.target.value)}
          rows={2}
          placeholder="例：水印、@用户名、TikTok logo、文字遮罩"
          className="w-full text-[11.5px] px-2 py-1.5 rounded-md bg-rose-500/5 border border-rose-400/30 text-[var(--text-strong)] placeholder:text-[var(--text-faint)] resize-none focus:ring-2 focus:ring-rose-400/40 outline-none"
        />
        <div className="text-[9.5px] text-[var(--text-faint)] mt-0.5">默认已清除水印 / 平台元素 → 生成"纯净图"</div>
      </div>

      {/* 模型 + 模式 + 生成 */}
      <div className="mt-2 flex gap-1.5 items-center">
        <select
          value={model}
          onChange={(e) => setModel(e.target.value)}
          className="flex-1 text-[10.5px] px-2 py-1.5 rounded-md bg-black/40 border border-white/15 text-[var(--text-strong)]"
        >
          <option value="gemini-3-pro-image-preview">nano-banana-pro</option>
          <option value="gemini-3.1-flash-image-preview">gemini-3.1-flash-image</option>
          <option value="gemini-2.5-flash-image">gemini-2.5-flash-image</option>
        </select>
        <select
          value={mode}
          onChange={(e) => setMode(e.target.value as "edit" | "text")}
          className="text-[10.5px] px-2 py-1.5 rounded-md bg-black/40 border border-white/15 text-[var(--text-strong)]"
        >
          <option value="edit">image-to-image</option>
          <option value="text">text-only</option>
        </select>
      </div>
      <button
        onClick={handleGenerate}
        disabled={generating || !editablePrompt.trim()}
        className="mt-2 w-full text-[12px] py-2 rounded-md bg-gradient-to-r from-rose-500 to-pink-500 text-white hover:opacity-95 disabled:opacity-40 inline-flex items-center justify-center gap-1.5 font-semibold"
      >
        {generating ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Sparkles className="h-3.5 w-3.5" />}
        {generating ? "生成中…（约 5-15 秒）" : `⚡ 生成 1 张${gens.length > 0 ? "（再来一张）" : ""}`}
      </button>

      {/* 生成结果网格 — 按视频原比例 + 点击放大 */}
      {gens.length > 0 && (
        <div className="mt-2.5">
          <div className="text-[10px] text-[var(--text-faint)] uppercase tracking-widest mb-1.5">
            生成结果 ({gens.length})
          </div>
          <div className="grid grid-cols-3 gap-1.5">
            {gens.map((g) => (
              <div
                key={g.id}
                className={`relative rounded-md overflow-hidden border-2 transition ${
                  g.selected
                    ? "border-emerald-400 ring-2 ring-emerald-400/40"
                    : "border-white/15 hover:border-white/40"
                }`}
                style={{ aspectRatio: job.height > 0 ? `${job.width}/${job.height}` : "1/1" }}
              >
                <button
                  onClick={() => setPreviewGenId(g.id)}
                  title={`${g.mode} · ${g.model}\n${g.prompt}`}
                  className="absolute inset-0 w-full h-full cursor-zoom-in"
                >
                  <img
                    src={generatedImageUrl(job.id, frame.index, g.id)}
                    alt={`gen ${g.id}`}
                    className="absolute inset-0 w-full h-full object-cover"
                  />
                </button>
                {/* 右上角独立选用按钮（总显示，selected=绿） */}
                <button
                  onClick={(e) => { e.stopPropagation(); handleSelectGen(g.id, g.selected) }}
                  title={g.selected ? "已选用 · 点取消" : "点击选用"}
                  className={`absolute top-1 right-1 h-5 w-5 rounded-full inline-flex items-center justify-center transition shadow-md ${
                    g.selected
                      ? "bg-emerald-500 text-white hover:bg-emerald-400"
                      : "bg-black/60 backdrop-blur text-white/60 hover:bg-emerald-500 hover:text-white"
                  }`}
                >
                  <Check className="h-3 w-3" />
                </button>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* 大图预览 modal */}
      {previewGenId && previewMounted && createPortal(
        (() => {
          const g = gens.find((x) => x.id === previewGenId)
          if (!g) return null
          return (
            <div
              onClick={() => setPreviewGenId(null)}
              className="fixed inset-0 z-[200] bg-black/85 backdrop-blur-xl flex items-center justify-center p-6 cursor-zoom-out"
              style={{ animation: "drawer-in 0.18s cubic-bezier(0.32, 0.72, 0, 1)" }}
            >
              <div
                onClick={(e) => e.stopPropagation()}
                className="relative max-w-full max-h-full flex flex-col gap-2 cursor-default"
              >
                <img
                  src={generatedImageUrl(job.id, frame.index, g.id)}
                  alt={`gen ${g.id}`}
                  className="block rounded-lg object-contain"
                  style={{ maxHeight: "calc(100vh - 140px)", maxWidth: "calc(100vw - 80px)" }}
                />
                <div className="flex items-center justify-between gap-3 px-1">
                  <div className="text-white/70 text-[11px] font-mono truncate">
                    分镜 {frame.index + 1} · {g.mode === "edit" ? "i2i" : "text"} · {g.model}
                  </div>
                  <div className="flex items-center gap-1.5 shrink-0">
                    <button
                      onClick={() => handleSelectGen(g.id, g.selected)}
                      className={`text-[11.5px] px-3 py-1.5 rounded-md inline-flex items-center gap-1.5 transition ${
                        g.selected
                          ? "bg-emerald-500 text-white hover:bg-emerald-400"
                          : "bg-white/10 text-white hover:bg-white/20"
                      }`}
                    >
                      <Check className="h-3.5 w-3.5" />
                      {g.selected ? "已选用" : "选用此图"}
                    </button>
                    <button
                      onClick={() => setPreviewGenId(null)}
                      className="text-[11.5px] px-2.5 py-1.5 rounded-md bg-white/10 text-white hover:bg-white/20 inline-flex items-center gap-1"
                    >
                      <X className="h-3.5 w-3.5" />
                    </button>
                  </div>
                </div>
              </div>
            </div>
          )
        })(),
        document.body
      )}
    </KanbanCard>
  )
}
