import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import "../../styles/pages/ecommerce-video.css";
import {
  CloseOutlined,
  CopyOutlined,
  DownloadOutlined,
  FolderAddOutlined,
  HistoryOutlined,
  LoadingOutlined,
  ReloadOutlined,
  SendOutlined,
  StopOutlined,
} from "@ant-design/icons";
import { runVideoPlan, renderSceneImage, renderScene, buildSceneTasks, saveVideoHistory } from "./ecommerceVideoService";
import {
  PLAN_STEP_LABELS,
  type EcommerceVideoStage,
  type EcommerceVideoSceneTask,
  type EcommerceVideoPlanProgress,
  type EcommerceVideoPlanResult,
  type PlanStep,
} from "./ecommerceVideoTypes";
import type { AdVideoUserConfig } from "../../api/adVideoPlanClient";
import { ServerRequestError } from "../../api/serverConnection";
import { saveToolResultToLocal, addToolResultToAssetLibrary } from "../workbench/toolResultActions";
import { useAppStore } from "../../stores";
import { useGenerationTasks } from "../../hooks/useGenerationTasks";
import {
  saveEcommerceVideoState,
  loadEcommerceVideoState,
} from "./ecommerceVideoKeepalive";

interface EcommerceVideoWorkspaceProps {
  isAuthenticated: boolean;
  productImageDataUrls: string[];
  productImageFiles?: Array<File | undefined>;
  requirement: string;
  platform: string;
  aspectRatio: string;
  durationSeconds: number;
  resolution: string;
  onRequestLogin?: () => void;
  onOpenHistory?: () => void;
  triggerPlan?: number;
}

const ALL_STEPS: PlanStep[] = [
  "upload", "analyze", "summary", "selling",
  "creative", "storyboard", "prompts", "compliance",
];

function hashString(value: string): string {
  let hash = 2166136261;
  for (let index = 0; index < value.length; index += 1) {
    hash ^= value.charCodeAt(index);
    hash = Math.imul(hash, 16777619);
  }
  return (hash >>> 0).toString(36);
}

function buildInputFingerprint(input: {
  productImageDataUrls: string[];
  requirement: string;
  platform: string;
  aspectRatio: string;
  durationSeconds: number;
  resolution: string;
}): string {
  const imageCount = input.productImageDataUrls.length;
  return hashString([
    String(imageCount),
    input.requirement.trim(),
    input.platform,
    input.aspectRatio,
    input.durationSeconds,
    input.resolution,
  ].join("::"));
}

function mapResolutionToQuality(res: string): "720P" | "1080P" {
  return res.includes("720") ? "720P" : "1080P";
}

function stepCompletedFromProgress(step: PlanStep, p: EcommerceVideoPlanProgress): boolean {
  switch (step) {
    case "upload": return Boolean(p.imageUrls?.length);
    case "analyze": return p.imageDescription !== undefined;
    case "summary": return Boolean(p.summary);
    case "selling": return Boolean(p.selling);
    case "creative": return Boolean(p.creatives?.length);
    case "storyboard": return Boolean(p.storyboard);
    case "prompts": return Boolean(p.videoPrompts);
    case "compliance": return Boolean(p.compliance);
  }
}

export default function EcommerceVideoWorkspace({
  isAuthenticated,
  productImageDataUrls,
  productImageFiles = [],
  requirement,
  platform,
  aspectRatio,
  durationSeconds,
  resolution,
  onRequestLogin,
  onOpenHistory,
  triggerPlan,
}: EcommerceVideoWorkspaceProps) {
  const [stage, setStage] = useState<EcommerceVideoStage>("idle");
  const [planResult, setPlanResult] = useState<EcommerceVideoPlanResult | null>(null);
  const [planProgress, setPlanProgress] = useState<EcommerceVideoPlanProgress | null>(null);
  const [scenes, setScenes] = useState<EcommerceVideoSceneTask[]>([]);
  const [completedSteps, setCompletedSteps] = useState<PlanStep[]>([]);
  const [sourceImageUrls, setSourceImageUrls] = useState<string[]>([]);
  const [currentStep, setCurrentStep] = useState<PlanStep | null>(null);
  const [failedStep, setFailedStep] = useState<PlanStep | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [actionNotice, setActionNotice] = useState<string | null>(null);
  const [previewMedia, setPreviewMedia] = useState<{ url: string; type: "image" | "video" } | null>(null);
  const [flowZoom, setFlowZoom] = useState(1);
  const abortControllerRef = useRef<AbortController | null>(null);
  const renderAbortRef = useRef({ current: false });
  const actionNoticeTimerRef = useRef<number | null>(null);
  const setView = useAppStore((s) => s.setView);
  const keepaliveRestoredFingerprintRef = useRef<string | null>(null);
  const keepalivePollingStartedRef = useRef(false);
  const generation = useGenerationTasks({ sourceView: "ecommerce" });
  const sceneStoreIdMap = useRef<Map<number, string>>(new Map());
  const inputFingerprint = useMemo(
    () => buildInputFingerprint({ productImageDataUrls, requirement, platform, aspectRatio, durationSeconds, resolution }),
    [productImageDataUrls, requirement, platform, aspectRatio, durationSeconds, resolution],
  );

  // ── Keep-alive: restore saved state on mount ─────────────
  useEffect(() => {
    if (keepaliveRestoredFingerprintRef.current === inputFingerprint) return;
    keepaliveRestoredFingerprintRef.current = inputFingerprint;
    const saved = loadEcommerceVideoState(inputFingerprint);
    if (!saved) return;
    if (saved.stage === "idle" || saved.stage === "cancelled") return;
    // Restore completed / in-progress states — results persist across page switches
    setStage(saved.stage);
    setCompletedSteps(saved.completedSteps || []);
    setPlanResult(saved.planResult);
    setPlanProgress((saved as { planProgress?: EcommerceVideoPlanProgress | null }).planProgress || null);
    setScenes(saved.scenes || []);
    setSourceImageUrls(saved.sourceImageUrls || saved.planResult?.imageUrls || []);
  }, [inputFingerprint]);

  // ── Keep-alive: save state on changes ───────────────────
  useEffect(() => {
    if (stage === "idle" || stage === "cancelled") return;
    saveEcommerceVideoState({ inputFingerprint, stage, completedSteps, planResult, planProgress, scenes, sourceImageUrls });
  }, [inputFingerprint, stage, completedSteps, planResult, planProgress, scenes, sourceImageUrls]);

  // ── Auto-advance: automatically run the full pipeline ─────────
  useEffect(() => {
    const delay = 600;
    if (stage === "planned" && planResult && scenes.length > 0) {
      const timer = setTimeout(() => { void handleGenerateImages(); }, delay);
      return () => clearTimeout(timer);
    }
    if (stage === "imaged" && scenes.every((s) => s.imageUrl)) {
      const timer = setTimeout(() => { void handleRenderVideos(); }, delay);
      return () => clearTimeout(timer);
    }
  }, [stage, scenes, planResult]);

  // ── External trigger: start plan from parent ────────────────
  const triggerPlanPrevRef = useRef(triggerPlan);
  useEffect(() => {
    if (triggerPlan != null && triggerPlan !== triggerPlanPrevRef.current) {
      triggerPlanPrevRef.current = triggerPlan;
      void handlePlan();
    }
  }, [triggerPlan]);

  // ── Auto-save: persist completed results to server ──────────
  const historySavedRef = useRef(false);
  useEffect(() => {
    if (stage !== "completed") { historySavedRef.current = false; return; }
    if (historySavedRef.current) return;
    if (!planResult || !scenes.length) return;
    historySavedRef.current = true;
    const title = planResult.storyboard?.video_title || planResult.summary?.product_name || "电商广告视频";
    saveVideoHistory({
      title,
      config: { platform, aspectRatio, durationSeconds, resolution },
      plan: planResult as unknown as Record<string, unknown>,
      scenes: scenes.map((s) => ({ sceneId: s.sceneId, prompt: s.prompt, imageUrl: s.imageUrl, videoUrl: s.resultUrl })),
      sourceImageUrls,
    }).catch(() => {});
  }, [stage, planResult, scenes, sourceImageUrls, platform, aspectRatio, durationSeconds, resolution]);

  // ── Keep-alive: resume polling for running tasks ──────────
  useEffect(() => {
    if (keepalivePollingStartedRef.current) return;
    if (!scenes.length || stage === "idle" || stage === "cancelled" || stage === "completed") return;

    const hasRunningScenes = scenes.some((s) => s.status === "running" || s.status === "pending");
    if (!hasRunningScenes) return;
    keepalivePollingStartedRef.current = true;

    // Resume polling for image generation tasks
    if (stage === "imaging") {
      renderAbortRef.current = { current: false };
      void (async () => {
        for (const scene of scenes) {
          if (renderAbortRef.current.current) break;
          if (scene.status !== "running" && scene.status !== "pending") continue;
          if (!scene.imageTaskId) continue;
          try {
            const { waitForTask } = await import("../../api/taskSubscription");
            const resultUrl = await waitForTask(scene.imageTaskId, {
              abortRef: renderAbortRef.current,
              onProgress: (e) =>
                setScenes((prev) => prev.map((s) => (s.sceneId === scene.sceneId ? { ...s, progress: e.progress } : s))),
            });
            if (resultUrl) {
              setScenes((prev) =>
                prev.map((s) => (s.sceneId === scene.sceneId ? { ...s, status: "idle", progress: 100, imageUrl: resultUrl } : s)),
              );
            }
          } catch {
            setScenes((prev) =>
              prev.map((s) => (s.sceneId === scene.sceneId ? { ...s, status: "idle", error: "恢复任务失败" } : s)),
            );
          }
        }
        setScenes((current) => {
          const allImaged = current.every((s) => s.imageUrl);
          if (allImaged) setStage("imaged");
          return current;
        });
      })();
    }

    // Resume polling for video rendering tasks
    if (stage === "rendering") {
      renderAbortRef.current = { current: false };
      void (async () => {
        for (const scene of scenes) {
          if (renderAbortRef.current.current) break;
          if (scene.status !== "running" && scene.status !== "pending") continue;
          if (!scene.taskId) continue;
          try {
            const { waitForTask } = await import("../../api/taskSubscription");
            const resultUrl = await waitForTask(scene.taskId, {
              abortRef: renderAbortRef.current,
              onProgress: (e) =>
                setScenes((prev) => prev.map((s) => (s.sceneId === scene.sceneId ? { ...s, progress: e.progress } : s))),
            });
            if (resultUrl) {
              setScenes((prev) =>
                prev.map((s) =>
                  s.sceneId === scene.sceneId ? { ...s, status: "completed", progress: 100, resultUrl: resultUrl } : s,
                ),
              );
            }
          } catch {
            setScenes((prev) =>
              prev.map((s) => (s.sceneId === scene.sceneId ? { ...s, status: "failed", error: "恢复任务失败" } : s)),
            );
          }
        }
        setScenes((current) => {
          const hasFailed = current.some((s) => s.status === "failed");
          const allDone = current.every((s) => s.status === "completed" || s.status === "failed");
          if (allDone) setStage(hasFailed ? "partial_failed" : "completed");
          return current;
        });
      })();
    }
  }, [scenes, stage]);

  // Note: keep-alive is NOT cleared on completion — results persist across page switches.
  // Only cleared when user explicitly starts a new plan via handlePlan.

  useEffect(() => {
    return () => {
      if (actionNoticeTimerRef.current !== null) {
        window.clearTimeout(actionNoticeTimerRef.current);
      }
    };
  }, []);

  const showNotice = (msg: string) => {
    setActionNotice(msg);
    if (actionNoticeTimerRef.current !== null) {
      window.clearTimeout(actionNoticeTimerRef.current);
    }
    actionNoticeTimerRef.current = window.setTimeout(() => {
      actionNoticeTimerRef.current = null;
      setActionNotice(null);
    }, 3000);
  };

  const _handleDownload = async (url: string) => {
    try {
      await saveToolResultToLocal({
        url, name: `ecommerce-video-${Date.now()}`, type: "video",
        isVideo: true, tags: ["电商", "短视频", "生成视频"],
      });
      showNotice("下载完成");
    } catch {
      const a = document.createElement("a"); a.href = url; a.download = "ecommerce-video.mp4"; a.click();
    }
  };

  const _handleSaveAsset = async (url: string) => {
    try {
      const result = await addToolResultToAssetLibrary({
        url, name: `电商短视频-${Date.now()}.mp4`, description: "电商广告视频生成结果",
        type: "video", isVideo: true, tags: ["电商", "短视频", "广告视频"],
        metadata: { source: "ecommerce-video", platform },
      });
      showNotice(result === "server" ? "已保存到资产库" : "已保存到本地资产库");
    } catch { showNotice("保存失败"); }
  };

  const handleSaveAllAssets = async () => {
    if (!completedScenes.length) return;
    let saved = 0;
    for (const scene of completedScenes) {
      try {
        await addToolResultToAssetLibrary({
          url: scene.resultUrl!, name: `电商短视频-镜头${scene.sceneId}-${Date.now()}.mp4`,
          description: `电商广告视频 - 镜头${scene.sceneId}`,
          type: "video", isVideo: true, tags: ["电商", "短视频", "广告视频"],
          metadata: { source: "ecommerce-video", platform, sceneId: scene.sceneId },
        });
        saved++;
      } catch { /* continue */ }
    }
    showNotice(saved > 0 ? `已保存 ${saved}/${completedScenes.length} 个视频到资产库` : "保存失败");
  };

  const handleDownloadAll = async () => {
    for (const scene of completedScenes) {
      await new Promise((r) => setTimeout(r, 300));
      const a = document.createElement("a");
      a.href = scene.resultUrl!;
      a.download = `ecommerce-video-scene-${scene.sceneId}.mp4`;
      a.click();
    }
    showNotice(`正在下载 ${completedScenes.length} 个视频`);
  };

  const handleImportToCanvas = async (url: string) => {
    try {
      await addToolResultToAssetLibrary({
        url, name: `电商短视频-${Date.now()}.mp4`, description: "电商广告视频 - 导入画布",
        type: "video", isVideo: true, tags: ["电商", "短视频", "画布导入"],
        metadata: { source: "ecommerce-video", platform },
      });
      setView("canvas");
      showNotice("已保存资产并跳转画布");
    } catch { showNotice("导入失败"); }
  };

  const buildConfig = useCallback((): AdVideoUserConfig => ({
    platform, aspectRatio, durationSeconds,
    style: "痛点解决", language: "中文", market: "中国",
    needVoiceover: true, needSubtitle: true, conversionFocus: "conversion",
  }), [platform, aspectRatio, durationSeconds]);

  // ── Phase 1: Planning ──────────────────────────────────────

  const runPlanFlow = async (resume: EcommerceVideoPlanProgress | null) => {
    abortControllerRef.current?.abort();
    const controller = new AbortController();
    abortControllerRef.current = controller;
    setStage("planning"); setError(null); setFailedStep(null);
    if (!resume) {
      setCompletedSteps([]); setPlanResult(null); setScenes([]); setSourceImageUrls([]); setPlanProgress(null);
    }
    setCurrentStep(null);
    // Mutable snapshot — async handlers must persist to localStorage directly since the component may unmount
    let livePlanProgress: EcommerceVideoPlanProgress = resume ? { ...resume } : {};
    let liveCompletedSteps: PlanStep[] = resume
      ? ALL_STEPS.filter((s) => stepCompletedFromProgress(s, resume))
      : [];
    const persist = (stageNow: EcommerceVideoStage) => {
      saveEcommerceVideoState({
        inputFingerprint,
        stage: stageNow,
        completedSteps: liveCompletedSteps,
        planResult: null,
        planProgress: livePlanProgress,
        scenes: [],
        sourceImageUrls: livePlanProgress.imageUrls || [],
      });
    };
    try {
      const productImageSources = productImageDataUrls.map((url, index) => productImageFiles[index] ?? url);
      const result = await runVideoPlan(
        productImageSources, requirement, buildConfig(),
        {
          onStepStart: (step) => setCurrentStep(step),
          onStepDone: (step) => {
            liveCompletedSteps = [...liveCompletedSteps, step];
            setCompletedSteps((prev) => [...prev, step]);
          },
          onImagesUploaded: (urls) => {
            setSourceImageUrls(urls);
            livePlanProgress = { ...livePlanProgress, imageUrls: urls };
            persist("planning");
          },
          onUploadRejected: (messages) => {
            if (messages.length) showNotice(`已跳过 ${messages.length} 张上传失败的图片`);
          },
          onPartialProgress: (progress) => {
            livePlanProgress = progress;
            setPlanProgress(progress);
            persist("planning");
          },
          resumeFrom: resume || undefined,
          signal: controller.signal,
        },
      );
      const builtScenes = buildSceneTasks(result);
      setPlanResult(result);
      setPlanProgress(null);
      setScenes(builtScenes);
      setStage("planned");
      saveEcommerceVideoState({ inputFingerprint, stage: "planned", completedSteps: [...ALL_STEPS], planResult: result, planProgress: null, scenes: builtScenes, sourceImageUrls: result.imageUrls });
    } catch (err) {
      if ((err as Error).name === "AbortError" && controller.signal.aborted) return;
      const message = err instanceof Error ? err.message : "策划失败";
      setError(message);
      // Mark the step that was in-progress as failed so user can resume
      setFailedStep((prev) => prev || currentStep);
      setStage("idle");
      // Persist partial progress so the user can resume after a page switch
      persist("idle");
    } finally { setCurrentStep(null); }
  };

  const handlePlan = async () => {
    if (!isAuthenticated) { onRequestLogin?.(); return; }
    if (!productImageDataUrls.length && !requirement.trim()) {
      setError("请先上传产品图片或填写商品说明"); return;
    }
    await runPlanFlow(null);
  };

  const handleResumePlan = async () => {
    if (!isAuthenticated) { onRequestLogin?.(); return; }
    if (!planProgress) { void handlePlan(); return; }
    await runPlanFlow(planProgress);
  };

  // ── Phase 2: Image generation per scene ──────────────────────

  const handleGenerateImages = async () => {
    if (!planResult || !scenes.length) return;
    setStage("imaging"); setError(null);
    renderAbortRef.current = { current: false };
    const ratio = aspectRatio.includes("9:16") || aspectRatio.includes("9：16") ? "9:16"
      : aspectRatio.includes("16:9") || aspectRatio.includes("16：9") ? "16:9"
      : "1:1";
    let currentScenes = [...scenes];
    const persistScenes = (next: EcommerceVideoSceneTask[]) => {
      currentScenes = next;
      setScenes(next);
      saveEcommerceVideoState({ inputFingerprint, stage: "imaging", completedSteps, planResult, scenes: next, sourceImageUrls });
    };
    // Only redo scenes missing imageUrl — preserves successfully generated images on partial retry
    const scenesToProcess = currentScenes.filter((s) => !s.imageUrl);
    if (!scenesToProcess.length) { setStage("imaged"); return; }
    for (const scene of scenesToProcess) {
      if (renderAbortRef.current.current) break;
      persistScenes(currentScenes.map((s) => s.sceneId === scene.sceneId ? { ...s, status: "pending", error: undefined } : s));
      try {
        await renderSceneImage(
          { sceneId: scene.sceneId, prompt: scene.prompt, aspectRatio: ratio, productImageUrls: sourceImageUrls },
          {
            onSceneImageSubmitted: (id, taskId) => {
            persistScenes(currentScenes.map((s) => s.sceneId === id ? { ...s, imageTaskId: taskId, status: "running" } : s));
            const storeId = generation.submitTask({ title: `分镜${id}图片`, type: "image", status: "running", progress: 0, prompt: scene.prompt, sourceView: "ecommerce", taskId, params: { sceneId: id, phase: "imaging" } });
            sceneStoreIdMap.current.set(id, storeId);
          },
            onSceneImageProgress: (id, progress) => persistScenes(currentScenes.map((s) => s.sceneId === id ? { ...s, progress } : s)),
          onSceneImageCompleted: (id, url) => {
            persistScenes(currentScenes.map((s) => s.sceneId === id ? { ...s, status: "idle", progress: 100, imageUrl: url } : s));
            const sid = sceneStoreIdMap.current.get(id);
            if (sid) generation.markCompleted(sid, url);
          },
          onSceneImageFailed: (id, err2) => {
            persistScenes(currentScenes.map((s) => s.sceneId === id ? { ...s, status: "idle", error: err2 } : s));
            const sid = sceneStoreIdMap.current.get(id);
            if (sid) generation.markFailed(sid, err2);
          },
          },
          renderAbortRef.current,
        );
      } catch (err) {
        const message = err instanceof Error ? err.message : "图片生成失败";
        persistScenes(currentScenes.map((s) => s.sceneId === scene.sceneId ? { ...s, status: "idle", error: message } : s));
      }
    }
    const allHaveImages = currentScenes.every((s) => s.imageUrl);
    const finalStage = allHaveImages ? "imaged" as const : "partial_failed" as const;
    setStage(finalStage);
    saveEcommerceVideoState({ inputFingerprint, stage: finalStage, completedSteps, planResult, scenes: currentScenes, sourceImageUrls });
  };

  // ── Phase 3: Video rendering from generated images ──────────

  const handleRenderVideos = async () => {
    if (!scenes.length) return;
    if (!scenes.some((s) => s.imageUrl)) { setError("请先生成分镜图片"); return; }
    setStage("rendering"); setError(null);
    renderAbortRef.current = { current: false };
    const quality = mapResolutionToQuality(resolution);
    let currentScenes = [...scenes];
    const persistScenes = (next: EcommerceVideoSceneTask[]) => {
      currentScenes = next;
      setScenes(next);
      saveEcommerceVideoState({ inputFingerprint, stage: "rendering", completedSteps, planResult, scenes: next, sourceImageUrls });
    };
    // Only render scenes that haven't completed yet — preserves successful videos on partial retry
    const scenesToProcess = currentScenes.filter((s) => s.imageUrl && s.status !== "completed");
    if (!scenesToProcess.length) { setStage(currentScenes.every((s) => s.status === "completed") ? "completed" : "partial_failed"); return; }
    for (const scene of scenesToProcess) {
      if (renderAbortRef.current.current) break;
      if (!scene.imageUrl) continue;
      persistScenes(currentScenes.map((s) => s.sceneId === scene.sceneId ? { ...s, status: "pending", error: undefined } : s));
      try {
        await renderScene(
          { sceneId: scene.sceneId, prompt: scene.prompt, durationSeconds: scene.durationSeconds, imageUrl: scene.imageUrl, productImageUrls: sourceImageUrls, aspectRatio, resolution: quality },
          {
            onSceneSubmitted: (id, taskId) => {
              persistScenes(currentScenes.map((s) => s.sceneId === id ? { ...s, taskId, status: "running" } : s));
              const storeId = generation.submitTask({ title: `分镜${id}视频`, type: "video", status: "running", progress: 0, prompt: scene.prompt, sourceView: "ecommerce", taskId, params: { sceneId: id, phase: "rendering" } });
              sceneStoreIdMap.current.set(id, storeId);
            },
            onSceneProgress: (id, progress) => persistScenes(currentScenes.map((s) => s.sceneId === id ? { ...s, progress } : s)),
            onSceneCompleted: (id, url) => {
              persistScenes(currentScenes.map((s) => s.sceneId === id ? { ...s, status: "completed", progress: 100, resultUrl: url } : s));
              const sid = sceneStoreIdMap.current.get(id);
              if (sid) generation.markCompleted(sid, url);
            },
            onSceneFailed: (id, err2) => {
              persistScenes(currentScenes.map((s) => s.sceneId === id ? { ...s, status: "failed", error: err2 } : s));
              const sid = sceneStoreIdMap.current.get(id);
              if (sid) generation.markFailed(sid, err2);
            },
          },
          renderAbortRef.current,
        );
      } catch (err) {
        const msg = err instanceof Error ? err.message : "生成失败";
        const isPayment = err instanceof ServerRequestError && err.status === 402;
        persistScenes(currentScenes.map((s) => s.sceneId === scene.sceneId ? { ...s, status: "failed", error: isPayment ? "余额不足，请充值后继续" : msg } : s));
        if (isPayment) { setError("余额不足，请充值后再生成视频"); renderAbortRef.current.current = true; break; }
      }
    }
    const hasFailed = currentScenes.some((s) => s.status === "failed");
    const allDone = currentScenes.every((s) => s.status === "completed" || s.status === "failed");
    const finalStage = allDone ? (hasFailed ? "partial_failed" as const : "completed" as const) : "rendering" as const;
    setScenes(currentScenes);
    setStage(finalStage);
    saveEcommerceVideoState({ inputFingerprint, stage: finalStage, completedSteps, planResult, scenes: currentScenes, sourceImageUrls });
  };

  const handleCancel = () => { abortControllerRef.current?.abort(); renderAbortRef.current.current = true; setStage("cancelled"); };

  const handleRetryScene = async (scene: EcommerceVideoSceneTask) => {
    if (!scene.imageUrl) return;
    setScenes((prev) => prev.map((s) => s.sceneId === scene.sceneId ? { ...s, status: "pending", error: undefined } : s));
    try {
      await renderScene(
        { sceneId: scene.sceneId, prompt: scene.prompt, durationSeconds: scene.durationSeconds, imageUrl: scene.imageUrl!, productImageUrls: sourceImageUrls, aspectRatio, resolution: mapResolutionToQuality(resolution) },
        {
          onSceneSubmitted: (id, taskId) => setScenes((prev) => prev.map((s) => s.sceneId === id ? { ...s, taskId, status: "running" } : s)),
          onSceneProgress: (id, progress) => setScenes((prev) => prev.map((s) => s.sceneId === id ? { ...s, progress } : s)),
          onSceneCompleted: (id, url) => setScenes((prev) => prev.map((s) => s.sceneId === id ? { ...s, status: "completed", progress: 100, resultUrl: url } : s)),
          onSceneFailed: (id, err2) => setScenes((prev) => prev.map((s) => s.sceneId === id ? { ...s, status: "failed", error: err2 } : s)),
        },
        renderAbortRef.current,
      );
    } catch (err) {
      setScenes((prev) => prev.map((s) => s.sceneId === scene.sceneId ? { ...s, status: "failed", error: (err as Error).message } : s));
    }
  };

  // ── Derived state ───────────────────────────────────────────

  const completedScenes = scenes.filter((s) => s.status === "completed" && s.resultUrl);
  const imagedScenes = scenes.filter((s) => s.imageUrl);
  const primaryVideo = completedScenes[0]?.resultUrl;
  const sourceImage = sourceImageUrls[0] || planResult?.imageUrls[0] || productImageDataUrls[0] || "";
  const flowHasStarted = stage !== "idle" || completedSteps.length > 0;
  const flowMeta = `${platform} / ${aspectRatio} / ${durationSeconds}s / ${resolution}`;

  return (
    <div className="ecom-video-workspace" data-stage={stage}>
      {/* ── Flow bar ──────────────────────────────────── */}
      <header className="ecom-video-flowbar">
        <div className="ecom-video-flowbar__title" aria-label={`短视频分镜流，${flowMeta}`} title={flowMeta}>
          <span className={`ecom-video-flowbar__pulse${flowHasStarted ? " is-active" : ""}`} aria-hidden="true" />
          <span className="ecom-video-flowbar__wave" aria-hidden="true"><i /><i /><i /></span>
        </div>

        <div className="ecom-video-step-dots" aria-label="策划进度">
          {ALL_STEPS.map((step) => {
            const isDone = completedSteps.includes(step);
            const isActive = currentStep === step;
            return <span key={step} className={`ecom-video-step-dot ${isDone ? "is-done" : isActive ? "is-active" : ""}`} title={PLAN_STEP_LABELS[step]} />;
          })}
        </div>

        <div className="ecom-video-flowbar__zoom">
          <button type="button" onClick={() => setFlowZoom((z) => Math.max(0.25, z - 0.1))} disabled={flowZoom <= 0.25} aria-label="缩小">−</button>
          <span>{Math.round(flowZoom * 100)}%</span>
          <button type="button" onClick={() => setFlowZoom((z) => Math.min(2, z + 0.1))} disabled={flowZoom >= 2} aria-label="放大">+</button>
        </div>

        <div className="ecom-video-flowbar__actions">
          {onOpenHistory ? (
            <button type="button" className="ecom-video-flow-action ecom-video-flow-action--ghost" onClick={onOpenHistory} title="生成记录">
              <HistoryOutlined />
            </button>
          ) : null}
          {error ? <span className="ecom-video-flowbar__error" role="alert">{error}</span> : null}
          {stage === "idle" && planProgress && (planProgress.summary || planProgress.creatives || planProgress.storyboard) ? (
            <button type="button" className="ecom-video-flow-action ecom-video-flow-action--ghost"
              onClick={() => void handleResumePlan()} title={`从「${failedStep ? PLAN_STEP_LABELS[failedStep] : "已中断处"}」继续策划`}>
              <ReloadOutlined /> 继续
            </button>
          ) : null}
          {stage === "planned" || stage === "imaged" ? (
            <button type="button" className="ecom-video-flow-action ecom-video-flow-action--ghost"
              onClick={() => void handleGenerateImages()} title={stage === "imaged" ? "重新生成分镜图" : "生成图片"}>
              {stage === "imaged" ? <ReloadOutlined /> : <SendOutlined />}
            </button>
          ) : null}
          {stage === "imaged" || (stage === "partial_failed" && imagedScenes.length > 0) ? (
            <button type="button" className="ecom-video-flow-action ecom-video-flow-action--ghost"
              onClick={() => void handleRenderVideos()} title={stage === "partial_failed" ? "重新生成失败的视频" : "生成视频"}>
              <SendOutlined />
            </button>
          ) : null}
          {stage === "planning" ? (
            <span className="ecom-video-flowbar__stage-label"><LoadingOutlined /> {currentStep ? PLAN_STEP_LABELS[currentStep] : "策划中"}</span>
          ) : null}
          {stage === "imaging" ? (
            <span className="ecom-video-flowbar__stage-label"><LoadingOutlined /> 生成图片中</span>
          ) : null}
          {stage === "rendering" ? (
            <span className="ecom-video-flowbar__stage-label"><LoadingOutlined /> 生成视频中</span>
          ) : null}
          {stage === "planning" || stage === "imaging" || stage === "rendering" ? (
            <button type="button" className="ecom-video-flow-action ecom-video-flow-action--danger" onClick={handleCancel} title="终止">
              <StopOutlined />
            </button>
          ) : null}
        </div>
      </header>

      {/* ── Flow canvas ──────────────────────────────────── */}
      <section className="ecom-video-flow-canvas" aria-label="视频分镜流程图">
        <div style={{ zoom: flowZoom, flexShrink: 0, display: "flex", alignItems: "flex-start", justifyContent: "center", minWidth: "max-content" }}>
        {!sourceImage ? (
          <div className="ecom-video-empty">
            <span>上传商品图并点击"一键策划"开始</span>
          </div>
        ) : (
          <div className="ecom-video-tree">
            {/* Source Node — 附件原图 */}
            <div className="ecom-video-tree__source">
              <article className="ecom-video-tree-node ecom-video-tree-node--source">
                <img src={sourceImage} alt="商品原图" />
              </article>
              <span className="ecom-video-tree-node__label">附件原图</span>
            </div>
      
            {/* Branch Connector — 分支连接线 */}
            <div className="ecom-video-tree__trunk" aria-hidden="true">
              <div className="ecom-video-tree__trunk-line" />
              <div className="ecom-video-tree__branches-line">
                {scenes.length > 0 ? scenes.map((s) => (
                  <div key={`trunk-${s.sceneId}`} className="ecom-video-tree__branch-tap" />
                )) : (
                  <div className="ecom-video-tree__branch-tap" />
                )}
              </div>
            </div>
      
            {/* Branches — 每个场景一条分支 */}
            <div className="ecom-video-tree__rows">
              {scenes.length > 0 ? scenes.map((scene, idx) => {
                const planDone = completedSteps.length >= ALL_STEPS.length;
                const imgReady = !!scene.imageUrl;
                const imgRunning = stage === "imaging" && (scene.status === "running" || scene.status === "pending") && !scene.imageUrl;
                const vidReady = scene.status === "completed" && scene.resultUrl;
                const vidRunning = stage === "rendering" && (scene.status === "running" || scene.status === "pending");
                const vidFailed = scene.status === "failed";
      
                return (
                  <div key={scene.sceneId} className="ecom-video-tree__row" style={{ animationDelay: `${idx * 120}ms` }}>
                    <article className={`ecom-video-tree-node ecom-video-tree-node--text${planDone ? " is-completed" : currentStep ? " is-active" : ""}`}>
                      <div className="ecom-video-tree-node__inner">
                        <span className="ecom-video-tree-node__title">分镜文本{scene.sceneId}</span>
                        <span className="ecom-video-tree-node__desc">
                          {planDone ? "已完成" : stage === "planning" ? "策划中..." : "等待策划"}
                        </span>
                      </div>
                    </article>
      
                    <div className="ecom-video-tree__arrow" aria-hidden="true">
                      <svg viewBox="0 0 40 20" fill="none"><path d="M0 10 H28 M22 4 L30 10 L22 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
                    </div>
      
                    <article className={`ecom-video-tree-node ecom-video-tree-node--image${imgReady ? " is-completed" : imgRunning ? " is-active" : ""}`} onClick={imgReady ? () => setPreviewMedia({ url: scene.imageUrl!, type: "image" }) : undefined} style={imgReady ? { cursor: "pointer" } : undefined}>
                      {imgReady ? (
                        <img src={scene.imageUrl!} alt={`分镜${scene.sceneId}`} />
                      ) : (
                        <div className="ecom-video-tree-node__placeholder">
                          {imgRunning ? <LoadingOutlined /> : <span>待生成</span>}
                        </div>
                      )}
                      {imgRunning ? <span className="ecom-video-tree-node__progress">{scene.progress || 0}%</span> : null}
                      <span className="ecom-video-tree-node__tag">分镜图{scene.sceneId}</span>
                    </article>
      
                    <div className="ecom-video-tree__arrow" aria-hidden="true">
                      <svg viewBox="0 0 40 20" fill="none"><path d="M0 10 H28 M22 4 L30 10 L22 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
                    </div>
      
                    <article className={`ecom-video-tree-node ecom-video-tree-node--video${vidReady ? " is-completed" : vidRunning ? " is-active" : vidFailed ? " is-failed" : ""}`} onClick={vidReady ? () => setPreviewMedia({ url: scene.resultUrl!, type: "video" }) : undefined} style={vidReady ? { cursor: "pointer" } : undefined}>
                      {vidReady ? (
                        <video src={scene.resultUrl!} muted playsInline loop autoPlay />
                      ) : (
                        <div className="ecom-video-tree-node__placeholder">
                          {vidRunning ? <LoadingOutlined /> : vidFailed ? <span>失败</span> : <span>待生成</span>}
                        </div>
                      )}
                      {vidRunning ? <span className="ecom-video-tree-node__progress">{scene.progress || 0}%</span> : null}
                      <span className="ecom-video-tree-node__tag">分镜视频{scene.sceneId}</span>
                      {vidFailed ? (
                        <button type="button" className="ecom-video-tree-node__retry"
                          onClick={(e) => { e.stopPropagation(); void handleRetryScene(scene); }}
                          title="重试此镜头">
                          <ReloadOutlined />
                        </button>
                      ) : null}
                    </article>
                  </div>
                );
              }) : (
                <div className={`ecom-video-tree__row ecom-video-tree__row--empty${stage === "planning" ? " is-planning" : ""}`}>
                  <article className="ecom-video-tree-node ecom-video-tree-node--text">
                    <div className="ecom-video-tree-node__inner">
                      <span className="ecom-video-tree-node__title">分镜策划</span>
                      <span className="ecom-video-tree-node__desc">{stage === "planning" ? "策划中..." : "点击一键策划开始"}</span>
                    </div>
                  </article>
                  <div className="ecom-video-tree__arrow" aria-hidden="true">
                    <svg viewBox="0 0 40 20" fill="none"><path d="M0 10 H28 M22 4 L30 10 L22 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
                  </div>
                  <article className="ecom-video-tree-node ecom-video-tree-node--image">
                    <div className="ecom-video-tree-node__placeholder">
                      {stage === "planning" ? <LoadingOutlined /> : <span>待生成</span>}
                    </div>
                    <span className="ecom-video-tree-node__tag">分镜图</span>
                  </article>
                  <div className="ecom-video-tree__arrow" aria-hidden="true">
                    <svg viewBox="0 0 40 20" fill="none"><path d="M0 10 H28 M22 4 L30 10 L22 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
                  </div>
                  <article className="ecom-video-tree-node ecom-video-tree-node--video">
                    <div className="ecom-video-tree-node__placeholder">
                      {stage === "planning" ? <LoadingOutlined /> : <span>待生成</span>}
                    </div>
                    <span className="ecom-video-tree-node__tag">分镜视频</span>
                  </article>
                </div>
              )}
            </div>
          </div>
        )}
        </div>

        {/* ── Delivery dock ────────────────────────────── */}
        {primaryVideo ? (
          <div className="ecom-video-flow-dock" aria-label="视频交付操作">
            <button type="button" onClick={() => void handleDownloadAll()} title={`下载全部 ${completedScenes.length} 个视频`}><DownloadOutlined /></button>
            <button type="button" onClick={() => void handleSaveAllAssets()} title={`保存全部 ${completedScenes.length} 个视频到资产库`}><FolderAddOutlined /></button>
            {primaryVideo ? <button type="button" onClick={() => void handleImportToCanvas(primaryVideo)} title="导入画布"><SendOutlined /></button> : null}
            {primaryVideo ? <button type="button" onClick={() => void navigator.clipboard.writeText(primaryVideo)} title="复制链接"><CopyOutlined /></button> : null}
          </div>
        ) : null}
        {actionNotice ? <div className="ecom-video-flow-notice">{actionNotice}</div> : null}
      </section>

      {previewMedia ? (
        <div className="ecom-video-preview-overlay" onClick={() => setPreviewMedia(null)}>
          <button type="button" className="ecom-video-preview-overlay__close" onClick={() => setPreviewMedia(null)}>
            <CloseOutlined />
          </button>
          {previewMedia.type === "image" ? (
            <img src={previewMedia.url} alt="预览" onClick={(e) => e.stopPropagation()} />
          ) : (
            <video src={previewMedia.url} controls autoPlay onClick={(e) => e.stopPropagation()} />
          )}
        </div>
      ) : null}
    </div>
  );
}
