<!-- 创作资产面板，负责资产选择、编辑、媒体生成和历史输出管理。 -->
<script setup lang="ts">
import { computed, nextTick, reactive, ref, watch } from "vue";
import { ElMessageBox } from "element-plus";
import {
  Back,
  Box,
  Check,
  CircleClose,
  CollectionTag,
  Delete,
  Download,
  FullScreen,
  Headset,
  Loading,
  MagicStick,
  OfficeBuilding,
  Picture,
  Plus,
  Upload,
  UserFilled,
  VideoCamera,
} from "@element-plus/icons-vue";
import { Volume2 } from "@/icons";
import type { GenerationOutput, GenerationTask, ProjectAsset, Storyboard } from "@/api/creative";
import { creativeApi } from "@/api/creative";
import AssetPromptEditor from "@/components/creative/AssetPromptEditor.vue";
import LoadingMedia from "@/components/creative/LoadingMedia.vue";
import VideoPlayer from "@/components/creative/VideoPlayer.vue";
import DurationSelector from "@/components/creative/DurationSelector.vue";
import ZoomableImage from "@/components/creative/ZoomableImage.vue";
import { downloadMedia } from "@/utils/download";
import { taskElapsedLabel } from "@/utils/taskTimeout";
import { formatMediaDuration } from "@/utils/media";
import { useToastStore } from "@/stores/toast";

type StoryboardMediaType = "storyboard_image" | "storyboard_video";
const props = defineProps<{
  projectId: string;
  projectName?: string;
  episodeName?: string;
  assets: ProjectAsset[];
  tasks: GenerationTask[];
  busy?: boolean;
  canGenerate?: boolean;
  now?: number;
  pendingGenerationIds?: string[];
  storyboards?: Storyboard[];
  currentStoryboard?: Storyboard;
  videoGenerationBlockedReason?: string;
  videoDurationRange?: { min: number; max: number };
  mediaFocus?: { type: StoryboardMediaType; storyboardId: string };
  assetFocus?: { assetId: string };
}>();
const toast = useToastStore();
const emit = defineEmits<{
  create: [payload: { asset_type: string; name: string }];
  save: [assetId: string, payload: Partial<ProjectAsset>];
  delete: [asset: ProjectAsset];
  addToStoryboard: [asset: ProjectAsset];
  uploadImage: [assetId: string, file: File];
  removeImage: [assetId: string];
  uploadAudio: [assetId: string, file: File];
  generateImage: [assetId: string, referenceImageDataUrl?: string];
  cancel: [task: GenerationTask];
  generateStoryboardMedia: [
    storyboardId: string,
    taskType: "image_generation" | "video_generation",
    input?: Record<string, unknown>,
  ];
  updateStoryboardDuration: [storyboardId: string, duration: number];
  historyStoryboard: [storyboardId: string];
  historyChanged: [];
}>();

const assetGroups = [
  { type: "character", label: "角色", icon: UserFilled },
  { type: "scene", label: "场景", icon: OfficeBuilding },
  { type: "prop", label: "道具", icon: Box },
  { type: "custom", label: "自定义", icon: CollectionTag },
];
const mediaGroups = [
  { type: "storyboard_image" as const, label: "分镜图", icon: Picture },
  { type: "storyboard_video" as const, label: "视频", icon: VideoCamera },
];
const groups = computed(() =>
  props.storyboards
    ? [
        ...assetGroups.filter((group) => group.type !== "custom"),
        ...mediaGroups,
        assetGroups.find((group) => group.type === "custom")!,
      ]
    : assetGroups,
);
const activeType = ref("character");
const selectedId = ref("");
const detailOpen = ref(false);
const historyOutputs = ref<GenerationOutput[]>([]);
const historyLoading = ref(false);
const historyPreview = ref<GenerationOutput>();
const storyboardVideoPlayer = ref<InstanceType<typeof VideoPlayer>>();
const fullscreenVideoUrl = ref("");
const temporaryReferenceUrl = ref("");
const temporaryReferenceDataUrl = ref("");
const storyboardPrompt = ref("");
const form = reactive({ name: "", image_prompt: "" });
const previewOpen = ref(false);

/** 清理 Windows 不允许出现在文件名中的字符，并保留项目现有中文命名。 */
function safeFilenamePart(value: string | undefined, fallback: string) {
  return (value || fallback).replace(/[\\/:*?"<>|]/g, "-").trim() || fallback;
}

/** 生成当前分镜视频统一使用的下载文件名前缀。 */
function storyboardVideoFilename(suffix: string) {
  const projectName = safeFilenamePart(props.projectName, "项目");
  const episodeName = safeFilenamePart(props.episodeName, "剧集");
  return `${projectName}-${episodeName}-分镜${selectedStoryboard.value?.sequence_no || ""}-${suffix}.mp4`;
}
const filteredAssets = computed(() => props.assets.filter((item) => item.asset_type === activeType.value));
const episodeAssetIds = computed(
  () => new Set((props.storyboards || []).flatMap((item) => (item.asset_refs || []).map((ref) => ref.id))),
);
const currentAssetIds = computed(() => new Set((props.currentStoryboard?.asset_refs || []).map((ref) => ref.id)));
const assetSections = computed(() =>
  props.storyboards === undefined
    ? [{ key: "all", label: "", assets: filteredAssets.value }]
    : [
        {
          key: "used",
          label: "本剧集已使用",
          assets: filteredAssets.value.filter((asset) => episodeAssetIds.value.has(asset.id)),
        },
        {
          key: "unused",
          label: "本剧集未使用",
          assets: filteredAssets.value.filter((asset) => !episodeAssetIds.value.has(asset.id)),
        },
      ],
);
const selected = computed(() => props.assets.find((item) => item.id === selectedId.value));
const activeMediaType = computed(() =>
  mediaGroups.some((group) => group.type === activeType.value) ? (activeType.value as StoryboardMediaType) : undefined,
);
const selectedStoryboard = computed(() =>
  activeMediaType.value ? props.storyboards?.find((item) => item.id === selectedId.value) : undefined,
);
// storyboardPromptAssets 提供当前分镜提示词可 @ 引用的资产集合（仅限分镜已关联的资产）。
const storyboardPromptAssets = computed(() => {
  const referencedIds = new Set((selectedStoryboard.value?.asset_refs || []).map((item) => item.id));
  return props.assets.filter((asset) => referencedIds.has(asset.id));
});
watch(
  selectedStoryboard,
  (storyboard) => {
    storyboardPrompt.value = storyboard?.image_prompt || "";
  },
  { immediate: true },
);
const historyVisible = computed(
  () => detailOpen.value || (activeMediaType.value === "storyboard_video" && Boolean(selectedStoryboard.value)),
);
const panelCount = computed(() => props.assets.length);
const detailTitle = computed(
  () =>
    selected.value?.name || (selectedStoryboard.value ? `分镜 ${selectedStoryboard.value.sequence_no}` : "资产详情"),
);
const activeStatuses = new Set([
  "pending_submission",
  "submitting",
  "submitted",
  "processing",
  "result_ready",
  "downloading",
  "cancel_requested",
]);
function activeStoryboardTask(storyboard: Storyboard, taskType: "image_generation" | "video_generation") {
  return props.tasks.find(
    (item) =>
      item.storyboard_id === storyboard.id &&
      item.task_type === taskType &&
      (taskType !== "image_generation" || !item.asset_id) &&
      activeStatuses.has(item.status),
  );
}
const selectedTask = computed(() =>
  props.tasks.find(
    (item) =>
      item.task_type === "image_generation" && item.asset_id === selectedId.value && activeStatuses.has(item.status),
  ),
);
const selectedPending = computed(() => props.pendingGenerationIds?.includes(selectedId.value) || false);
const taskActive = computed(() => selectedPending.value || Boolean(selectedTask.value));
const selectedTaskStopping = computed(() => selectedTask.value?.status === "cancel_requested");
const historicalOutputs = computed(() =>
  historyOutputs.value.filter((output) => {
    if (activeMediaType.value === "storyboard_image") return !output.active && output.mime_type.startsWith("image/");
    if (activeMediaType.value === "storyboard_video")
      return !Boolean(output.candidate ?? output.active) && output.mime_type.startsWith("video/");
    return !output.active;
  }),
);
const historyLimitReached = computed(() => historicalOutputs.value.length >= 5);
const candidateOutputs = computed(() =>
  historyOutputs.value.filter(
    (output) => output.mime_type.startsWith("video/") && Boolean(output.candidate ?? output.active),
  ),
);
const generationElapsed = computed(() =>
  selectedTask.value ? taskElapsedLabel(selectedTask.value, props.now || Date.now()) : "",
);
const generationProgressLabel = computed(() =>
  selectedPending.value
    ? "正在启动"
    : selectedTaskStopping.value
      ? "取消中"
      : selectedTask.value
        ? `生成中 ${generationElapsed.value}`
        : "生成参考图",
);
const generationActionLabel = computed(() =>
  selectedTask.value && !selectedTaskStopping.value
    ? `取消生成 ${generationElapsed.value}`
    : generationProgressLabel.value,
);
const selectedStoryboardTask = computed(() => {
  if (!selectedStoryboard.value || !activeMediaType.value) return undefined;
  const taskType = activeMediaType.value === "storyboard_image" ? "image_generation" : "video_generation";
  return activeStoryboardTask(selectedStoryboard.value, taskType);
});
const candidateLimitReached = computed(
  () => candidateOutputs.value.length + (selectedStoryboardTask.value ? 1 : 0) >= 3,
);
function assetTaskActive(assetId: string) {
  return Boolean(
    props.pendingGenerationIds?.includes(assetId) ||
    props.tasks.some(
      (item) =>
        item.task_type === "image_generation" &&
        item.asset_id === assetId &&
        item.status !== "cancel_requested" &&
        activeStatuses.has(item.status),
    ),
  );
}
function groupCount(type: string) {
  return props.assets.filter((item) => item.asset_type === type).length;
}
function isMediaGroup(type: string): type is StoryboardMediaType {
  return mediaGroups.some((group) => group.type === type);
}

watch(
  () => props.assets,
  (items) => {
    if (isMediaGroup(activeType.value)) return;
    if (!items.some((item) => item.id === selectedId.value)) selectedId.value = filteredAssets.value[0]?.id || "";
  },
  { immediate: true, deep: true },
);
watch(activeType, (type) => {
  if (isMediaGroup(type)) {
    const currentStoryboardId = props.storyboards?.some((item) => item.id === selectedId.value) ? selectedId.value : "";
    selectedId.value =
      props.currentStoryboard?.id ||
      currentStoryboardId ||
      props.mediaFocus?.storyboardId ||
      props.storyboards?.[0]?.id ||
      "";
    detailOpen.value = type === "storyboard_image" && Boolean(selectedId.value);
  } else {
    selectedId.value = filteredAssets.value[0]?.id || "";
    detailOpen.value = false;
  }
});
watch(
  () => props.mediaFocus,
  (focus) => {
    if (!focus) return;
    activeType.value = focus.type;
    selectedId.value = focus.storyboardId;
    detailOpen.value = focus.type === "storyboard_image";
  },
  { deep: true },
);
watch(
  () => props.assetFocus,
  async (focus) => {
    if (!focus) return;
    const asset = props.assets.find((item) => item.id === focus.assetId);
    if (!asset) return;
    activeType.value = asset.asset_type;
    await nextTick();
    selectedId.value = asset.id;
    detailOpen.value = true;
  },
);
watch(
  () => props.currentStoryboard?.id,
  (storyboardId) => {
    if (storyboardId && activeMediaType.value) selectedId.value = storyboardId;
  },
);
watch(selected, (value) => Object.assign(form, { name: value?.name || "", image_prompt: value?.image_prompt || "" }), {
  immediate: true,
});

async function loadHistory() {
  if (!historyVisible.value || (!selected.value && !selectedStoryboard.value)) {
    historyOutputs.value = [];
    return;
  }
  historyLoading.value = true;
  try {
    historyOutputs.value = selectedStoryboard.value
      ? await creativeApi.outputs(props.projectId, selectedStoryboard.value.id)
      : await creativeApi.assetOutputs(props.projectId, selected.value!.id);
  } finally {
    historyLoading.value = false;
  }
}
async function activateHistory(output: GenerationOutput) {
  if (output.active) return;
  if (selectedStoryboard.value && activeMediaType.value === "storyboard_video")
    await creativeApi.addStoryboardCandidate(props.projectId, selectedStoryboard.value.id, output.id);
  else if (selectedStoryboard.value)
    await creativeApi.activateOutput(props.projectId, selectedStoryboard.value.id, output.id);
  else await creativeApi.activateAssetOutput(props.projectId, selected.value!.id, output.id);
  await loadHistory();
  emit("historyChanged");
}
async function removeCandidate(output: GenerationOutput) {
  if (!selectedStoryboard.value) return;
  try {
    await ElMessageBox.confirm("删除后该视频会移入历史记录，是否继续？", "删除备选视频", {
      type: "warning",
      confirmButtonText: "删除",
      cancelButtonText: "取消",
    });
  } catch (error) {
    if (error === "cancel" || error === "close") return;
    throw error;
  }
  await creativeApi.removeStoryboardCandidate(props.projectId, selectedStoryboard.value.id, output.id);
  await loadHistory();
  emit("historyChanged");
}
async function deleteHistory(output: GenerationOutput) {
  if (output.active) return;
  try {
    await ElMessageBox.confirm("删除后不可恢复，确定删除该历史记录吗？", "删除历史记录", {
      type: "warning",
      confirmButtonText: "删除",
      cancelButtonText: "取消",
    });
  } catch (error) {
    if (error === "cancel" || error === "close") return;
    throw error;
  }
  if (selectedStoryboard.value) await creativeApi.deleteOutput(props.projectId, selectedStoryboard.value.id, output.id);
  else await creativeApi.deleteAssetOutput(props.projectId, selected.value!.id, output.id);
  await loadHistory();
  emit("historyChanged");
}
function previewHistory(output: GenerationOutput) {
  historyPreview.value = output;
  previewOpen.value = true;
}
watch([activeType, selectedId, detailOpen], () => {
  void loadHistory();
});
watch(
  [
    () => selected.value?.image_asset_id,
    () => selectedStoryboard.value?.thumbnail_asset_id,
    () => selectedStoryboard.value?.active_output_id,
  ],
  () => {
    if (historyVisible.value) void loadHistory();
  },
);
watch(selectedId, () => {
  temporaryReferenceUrl.value = "";
  temporaryReferenceDataUrl.value = "";
});

function create() {
  if (isMediaGroup(activeType.value) || props.busy) return;
  const group = assetGroups.find((item) => item.type === activeType.value);
  if (!group) return;
  const names = new Set(props.assets.map((item) => item.name.trim()).filter(Boolean));
  let index = 1;
  while (names.has(`${group.label}${index}`)) index += 1;
  emit("create", { asset_type: group.type, name: `${group.label}${index}` });
}

function selectAsset(asset: ProjectAsset) {
  selectedId.value = asset.id;
  detailOpen.value = true;
}

function removeAsset(asset: ProjectAsset) {
  emit("delete", asset);
}

function save(field: "name" | "image_prompt") {
  if (!selected.value || form[field] === (selected.value[field] || "")) return;
  if (
    field === "name" &&
    props.assets.some(
      (asset) =>
        asset.id !== selected.value?.id &&
        asset.asset_type === selected.value?.asset_type &&
        asset.name.trim().toLocaleLowerCase() === form.name.trim().toLocaleLowerCase(),
    )
  ) {
    toast.error("同类型资产名称已存在");
    form.name = selected.value.name;
    return;
  }
  emit("save", selected.value.id, { [field]: form[field] });
}

function choose(event: Event, type: "image" | "audio") {
  const input = event.target as HTMLInputElement;
  const file = input.files?.[0];
  if (file && (selected.value || selectedStoryboard.value)) {
    if (type === "image") {
      const reader = new FileReader();
      reader.onload = () => {
        temporaryReferenceDataUrl.value = String(reader.result || "");
        temporaryReferenceUrl.value = temporaryReferenceDataUrl.value;
      };
      reader.readAsDataURL(file);
    } else if (selected.value) emit("uploadAudio", selected.value.id, file);
  }
  input.value = "";
}

function chooseAssetImage(event: Event) {
  const input = event.target as HTMLInputElement;
  const file = input.files?.[0];
  if (file && selected.value) emit("uploadImage", selected.value.id, file);
  input.value = "";
}

function clearTemporaryReference() {
  temporaryReferenceUrl.value = "";
  temporaryReferenceDataUrl.value = "";
}

async function downloadImage() {
  if (!selected.value?.image_url) return;
  try {
    await downloadMedia(selected.value.image_asset_id, selected.value.image_url, selected.value.name || "asset");
  } catch (error) {
    toast.error(error instanceof Error ? error.message : "图片下载失败");
  }
}

// downloadStoryboardImage 下载当前分镜的分镜图，文件名使用"分镜N"。
async function downloadStoryboardImage() {
  if (!selectedStoryboard.value?.thumbnail_url) return;
  await downloadMedia(
    selectedStoryboard.value.thumbnail_asset_id,
    selectedStoryboard.value.thumbnail_url,
    `分镜${selectedStoryboard.value.sequence_no}`,
  );
}

// downloadStoryboardVideo 下载当前分镜视频，文件名包含项目、剧集和分镜信息。
async function downloadStoryboardVideo() {
  if (!selectedStoryboard.value?.result_url) return;
  await downloadMedia(
    selectedStoryboard.value.result_media_id,
    selectedStoryboard.value.result_url,
    storyboardVideoFilename("主视频"),
  );
}

/** 将当前分镜图移出主图位置，保留生成结果供历史记录恢复。 */
async function removeStoryboardImage() {
  if (!selectedStoryboard.value?.thumbnail_url) return;
  try {
    await ElMessageBox.confirm("删除后分镜图会移入历史记录，可从历史记录恢复。是否继续？", "删除分镜图", {
      type: "warning",
      confirmButtonText: "删除",
      cancelButtonText: "取消",
    });
  } catch (error) {
    if (error === "cancel" || error === "close") return;
    throw error;
  }
  await creativeApi.removeStoryboardImage(props.projectId, selectedStoryboard.value.id);
  await loadHistory();
  emit("historyChanged");
}

async function removeCurrentImage() {
  if (!selected.value?.image_url) return;
  try {
    await ElMessageBox.confirm("移除后资产图会进入历史记录，可从历史记录恢复。是否继续？", "移除资产图", {
      type: "warning",
      confirmButtonText: "移除",
      cancelButtonText: "取消",
    });
  } catch (error) {
    if (error === "cancel" || error === "close") return;
    throw error;
  }
  emit("removeImage", selected.value.id);
}

function handleGenerationAction() {
  if (!selected.value) return;
  if (selectedTask.value) emit("cancel", selectedTask.value);
  else emit("generateImage", selected.value.id, temporaryReferenceDataUrl.value || undefined);
}

async function handleStoryboardGeneration() {
  if (!selectedStoryboard.value || !activeMediaType.value) return;
  if (selectedStoryboardTask.value) emit("cancel", selectedStoryboardTask.value);
  else if (activeMediaType.value === "storyboard_image") {
    // 生成前先保存分镜图提示词（允许为空，后端会回退到分镜内容），
    // 再把提示词与临时参考图随生成请求提交；后端同时自动携带分镜关联资产的参考图。
    const prompt = storyboardPrompt.value.trim();
    try {
      await creativeApi.updateStoryboard(props.projectId, selectedStoryboard.value.id, { image_prompt: prompt });
    } catch {
      toast.error("分镜图提示词保存失败");
      return;
    }
    emit("generateStoryboardMedia", selectedStoryboard.value.id, "image_generation", {
      prompt,
      image_urls: temporaryReferenceDataUrl.value ? [temporaryReferenceDataUrl.value] : undefined,
    });
  } else {
    emit("generateStoryboardMedia", selectedStoryboard.value.id, "video_generation");
  }
}

function closeStoryboardMedia() {
  detailOpen.value = false;
  activeType.value = "character";
}

async function openCandidateVideo(output: GenerationOutput) {
  fullscreenVideoUrl.value = output.public_url;
  await nextTick();
  storyboardVideoPlayer.value?.openFullscreen();
}
</script>

<template>
  <aside class="asset-panel">
    <header
      v-if="detailOpen"
      class="asset-detail-header"
    >
      <button
        type="button"
        title="返回资产列表"
        @click="activeMediaType ? closeStoryboardMedia() : (detailOpen = false)"
      >
        <el-icon><Back /></el-icon>
      </button>
      <strong>{{ detailTitle }}</strong>
    </header>
    <header v-else>
      <div>
        <strong>资产</strong><span>{{ panelCount }}</span>
      </div>
      <button
        type="button"
        :disabled="Boolean(activeMediaType) || busy"
        :title="
          activeMediaType
            ? '分镜媒体不能手动新建'
            : busy
              ? '正在创建资产'
              : `新建${groups.find((group) => group.type === activeType)?.label || '资产'}`
        "
        @click="create"
      >
        <el-icon><Plus /></el-icon>
      </button>
    </header>
    <nav
      v-if="!detailOpen"
      class="asset-tabs"
      :class="{ 'has-media': storyboards }"
    >
      <button
        v-for="group in groups"
        :key="group.type"
        type="button"
        :class="{ active: activeType === group.type }"
        :title="group.label"
        @click="activeType = group.type"
      >
        <el-icon><component :is="group.icon" /></el-icon
        ><small v-if="!isMediaGroup(group.type)">{{ groupCount(group.type) }}</small>
      </button>
    </nav>
    <div
      v-if="!detailOpen"
      class="asset-list"
      :class="{ 'episode-assets': storyboards !== undefined }"
    >
      <template v-if="!activeMediaType">
        <section
          v-for="section in assetSections"
          :key="section.key"
          class="asset-section"
        >
          <header
            v-if="section.label"
            class="asset-section-header"
          >
            <strong>{{ section.label }}</strong
            ><span>{{ section.assets.length }}</span>
          </header>
          <div class="asset-card-grid">
            <article
              v-for="asset in section.assets"
              :key="asset.id"
              class="asset-card"
            >
              <span
                v-if="currentAssetIds.has(asset.id)"
                class="current-asset-indicator"
                title="当前分镜使用"
              ></span>
              <button
                class="asset-card-main"
                type="button"
                @click="selectAsset(asset)"
              >
                <span class="asset-thumb"
                  ><LoadingMedia
                    v-if="asset.image_url"
                    type="image"
                    :src="asset.image_url"
                    :alt="asset.name" /><i v-else>{{ asset.name.slice(0, 1) }}</i
                  ><span
                    v-if="asset.audio_url"
                    class="asset-audio-badge"
                    title="已上传参考音频"
                    ><Volume2 :size="13" /><small>{{ formatMediaDuration(asset.audio_duration_ms) }}</small></span
                  ><el-icon
                    v-if="assetTaskActive(asset.id)"
                    class="asset-thumb-loading"
                    ><Loading /></el-icon
                ></span>
                <span class="asset-copy"
                  ><b>{{ asset.name }}</b></span
                >
              </button>
              <div class="asset-card-actions">
                <button
                  class="asset-delete"
                  type="button"
                  title="删除资产"
                  :title="`删除资产 ${asset.name}`"
                  @click="removeAsset(asset)"
                >
                  <el-icon><Delete /></el-icon>
                </button>
                <button
                  v-if="!currentAssetIds.has(asset.id)"
                  class="asset-add"
                  type="button"
                  title="添加到当前分镜"
                  :title="`添加资产 ${asset.name} 到当前分镜`"
                  :disabled="busy || !currentStoryboard || currentStoryboard.locked"
                  @click="emit('addToStoryboard', asset)"
                >
                  <el-icon><Plus /></el-icon>
                </button>
              </div>
            </article>
          </div>
          <div
            v-if="!section.assets.length"
            class="asset-section-empty"
          >
            {{ section.key === "used" ? "暂无已使用资产" : section.key === "unused" ? "暂无未使用资产" : "暂无资产" }}
          </div>
        </section>
      </template>
      <section
        v-else-if="activeMediaType === 'storyboard_video' && selectedStoryboard"
        class="storyboard-video-assets"
      >
        <div class="storyboard-video-actions">
          <DurationSelector
            :model-value="selectedStoryboard.duration_seconds"
            :min="videoDurationRange?.min || 5"
            :max="videoDurationRange?.max || 15"
            placement="bottom-start"
            @change="emit('updateStoryboardDuration', selectedStoryboard.id, $event)"
          />
          <el-button
            class="storyboard-video-generate"
            :type="selectedStoryboardTask ? 'danger' : 'primary'"
            :icon="selectedStoryboardTask ? CircleClose : MagicStick"
            :disabled="
              selectedStoryboardTask?.status === 'cancel_requested' ||
              busy ||
              (!selectedStoryboardTask && (candidateLimitReached || Boolean(videoGenerationBlockedReason)))
            "
            @click="handleStoryboardGeneration"
          >
            {{
              selectedStoryboardTask
                ? "取消生成"
                : candidateLimitReached
                  ? "备选视频已满"
                  : videoGenerationBlockedReason || "生成视频"
            }}
          </el-button>
        </div>
        <small
          v-if="!selectedStoryboardTask && videoGenerationBlockedReason"
          class="storyboard-video-limit"
          >{{ videoGenerationBlockedReason }}，请调整 @ 资产后再生成</small
        >
        <div class="storyboard-video-grid">
          <article
            v-for="(output, index) in candidateOutputs"
            :key="output.id"
            class="storyboard-video-card"
            :class="{ active: output.active }"
          >
            <button
              class="storyboard-video-preview"
              type="button"
              :title="`全屏播放备选视频 ${index + 1}`"
              @click="openCandidateVideo(output)"
            >
              <LoadingMedia
                type="video"
                :src="output.public_url"
                muted
                preload="metadata"
                playsinline
                disablepictureinpicture
                disableremoteplayback
                controlslist="nodownload noplaybackrate noremoteplayback"
              />
              <strong>备选 {{ index + 1 }}</strong>
            </button>
            <button
              class="storyboard-video-delete"
              type="button"
              :title="`删除备选视频 ${index + 1}`"
              @click="removeCandidate(output)"
            >
              <el-icon><Delete /></el-icon>
            </button>
            <button
              class="storyboard-video-download"
              type="button"
              :title="`下载备选视频 ${index + 1}`"
              @click="
                downloadMedia(output.media_asset_id, output.public_url, storyboardVideoFilename(`备选${index + 1}`))
              "
            >
              <el-icon><Download /></el-icon>
            </button>
          </article>
          <article
            v-if="selectedStoryboardTask"
            class="storyboard-video-card is-loading"
          >
            <span class="storyboard-video-loading"
              ><el-icon><Loading /></el-icon><small>生成中</small></span
            >
            <strong>新备选视频</strong>
          </article>
          <span
            v-if="!candidateOutputs.length && !selectedStoryboardTask"
            class="storyboard-video-empty"
            >暂无备选视频</span
          >
        </div>
      </section>
      <div
        v-if="activeMediaType && !selectedStoryboard"
        class="asset-empty"
      >
        请先选择一个分镜
      </div>
    </div>
    <Transition name="asset-detail">
      <div
        v-if="selected && detailOpen"
        class="asset-editor"
      >
        <div class="asset-preview">
          <LoadingMedia
            v-if="selected.image_url"
            type="image"
            :src="selected.image_url"
            :alt="`${selected.name} 主图`"
            @click="previewOpen = true"
          /><i v-else>{{ selected.name.slice(0, 1) }}</i>
          <span
            v-if="selected.audio_url"
            class="asset-preview-audio-badge"
            title="已上传参考音频"
            ><Volume2 :size="15" /><small>{{ formatMediaDuration(selected.audio_duration_ms) }}</small></span
          >
          <div
            v-if="taskActive"
            class="asset-image-loading"
          >
            <el-icon><Loading /></el-icon><b>{{ generationProgressLabel }}</b>
          </div>
          <div class="asset-preview-actions">
            <label
              :class="{ disabled: busy }"
              title="上传本地图"
            >
              <el-icon><Upload /></el-icon>
              <input
                type="file"
                accept="image/jpeg,image/png,image/webp"
                :disabled="busy"
                @change="chooseAssetImage"
              />
            </label>
            <button
              v-if="selected.image_url"
              type="button"
              title="查看大图"
              @click="previewOpen = true"
            >
              <el-icon><FullScreen /></el-icon>
            </button>
            <button
              v-if="selected.image_url"
              type="button"
              title="移除图片"
              :disabled="busy"
              @click="removeCurrentImage"
            >
              <el-icon><Delete /></el-icon>
            </button>
            <button
              v-if="selected.image_url"
              type="button"
              title="下载原图"
              @click="downloadImage"
            >
              <el-icon><Download /></el-icon>
            </button>
          </div>
        </div>
        <div class="asset-reference-upload">
          <span>临时参考图</span>
          <span class="asset-reference-box">
            <LoadingMedia
              v-if="temporaryReferenceUrl"
              type="image"
              compact
              :src="temporaryReferenceUrl"
              alt="临时参考图"
            />
            <template v-else
              ><el-icon><Upload /></el-icon><small>上传参考图</small></template
            >
            <input
              type="file"
              accept="image/jpeg,image/png,image/webp"
              @change="choose($event, 'image')"
            />
            <button
              v-if="temporaryReferenceUrl"
              class="asset-reference-remove"
              type="button"
              title="移除临时参考图"
              @click.prevent.stop="clearTemporaryReference"
            >
              <el-icon><CircleClose /></el-icon>
            </button>
          </span>
        </div>
        <div
          class="asset-name-row"
          :class="{ single: selected.asset_type !== 'character' }"
        >
          <label
            ><span>名称</span
            ><el-input
              v-model.trim="form.name"
              maxlength="200"
              @blur="save('name')"
          /></label>
          <label
            v-if="selected.asset_type === 'character'"
            class="audio-field"
            ><span>参考音频</span
            ><span class="audio-upload"
              ><input
                type="file"
                accept="audio/*"
                @change="choose($event, 'audio')"
              /><el-icon><Headset /></el-icon>{{ selected.audio_url ? "重新上传" : "上传" }}</span
            ></label
          >
        </div>
        <label class="prompt-field"
          ><span>生图提示词</span
          ><el-input
            v-model="form.image_prompt"
            type="textarea"
            resize="none"
            @blur="save('image_prompt')"
        /></label>
        <div class="asset-actions">
          <el-button
            :type="selectedTask ? 'danger' : 'primary'"
            :icon="selectedTask ? CircleClose : MagicStick"
            :disabled="
              busy ||
              selectedTaskStopping ||
              historyLimitReached ||
              (!selectedTask && (!canGenerate || !form.image_prompt.trim() || taskActive))
            "
            :loading="!selectedTask && selectedPending"
            @click="handleGenerationAction"
            >{{ generationActionLabel }}</el-button
          >
        </div>
      </div>
    </Transition>
    <Transition name="asset-detail">
      <div
        v-if="selectedStoryboard && activeMediaType && detailOpen"
        class="asset-editor storyboard-media-editor"
      >
        <div class="asset-preview storyboard-media-preview">
          <LoadingMedia
            v-if="activeMediaType === 'storyboard_image' && selectedStoryboard.thumbnail_url"
            type="image"
            :src="selectedStoryboard.thumbnail_url"
            :alt="`分镜 ${selectedStoryboard.sequence_no}`"
          />
          <LoadingMedia
            v-else-if="activeMediaType === 'storyboard_video' && selectedStoryboard.result_url"
            type="video"
            :src="selectedStoryboard.result_url"
            controls
            preload="metadata"
            playsinline
            disablepictureinpicture
            disableremoteplayback
            controlslist="nodownload noplaybackrate noremoteplayback"
          />
          <div
            v-else-if="!selectedStoryboardTask"
            class="storyboard-media-empty"
          >
            <el-icon><Picture v-if="activeMediaType === 'storyboard_image'" /><VideoCamera v-else /></el-icon
            ><span>{{ activeMediaType === "storyboard_image" ? "暂无分镜图" : "暂无视频" }}</span>
          </div>
          <div
            v-if="selectedStoryboardTask"
            class="asset-image-loading"
          >
            <el-icon><Loading /></el-icon
            ><b>{{ selectedStoryboardTask.status === "cancel_requested" ? "取消中" : "生成中" }}</b>
          </div>
          <div
            v-if="
              (activeMediaType === 'storyboard_image' && selectedStoryboard.thumbnail_url) ||
              (activeMediaType === 'storyboard_video' && selectedStoryboard.result_url)
            "
            class="asset-preview-actions"
          >
            <button
              v-if="activeMediaType === 'storyboard_image'"
              type="button"
              title="查看大图"
              @click="previewOpen = true"
            >
              <el-icon><FullScreen /></el-icon>
            </button>
            <button
              v-if="activeMediaType === 'storyboard_image'"
              type="button"
              title="下载分镜图"
              @click="downloadStoryboardImage"
            >
              <el-icon><Download /></el-icon>
            </button>
            <button
              v-if="activeMediaType === 'storyboard_image'"
              type="button"
              title="删除分镜图"
              @click="removeStoryboardImage"
            >
              <el-icon><Delete /></el-icon>
            </button>
            <button
              v-if="activeMediaType === 'storyboard_video'"
              type="button"
              title="下载分镜视频"
              @click="downloadStoryboardVideo"
            >
              <el-icon><Download /></el-icon>
            </button>
          </div>
        </div>
        <div
          v-if="activeMediaType === 'storyboard_image'"
          class="storyboard-reference-upload"
        >
          <span>参考图（可选）</span>
          <span class="asset-reference-box">
            <LoadingMedia
              v-if="temporaryReferenceUrl"
              type="image"
              compact
              :src="temporaryReferenceUrl"
              alt="分镜图参考图"
            />
            <template v-else
              ><el-icon><Upload /></el-icon><small>上传参考图</small></template
            >
            <input
              type="file"
              accept="image/jpeg,image/png,image/webp"
              @change="choose($event, 'image')"
            />
            <button
              v-if="temporaryReferenceUrl"
              class="asset-reference-remove"
              type="button"
              title="移除临时参考图"
              @click.prevent.stop="clearTemporaryReference"
            >
              <el-icon><CircleClose /></el-icon>
            </button>
          </span>
        </div>
        <div
          v-if="activeMediaType === 'storyboard_image'"
          class="storyboard-prompt-field"
        >
          <span>分镜图提示词</span>
          <AssetPromptEditor
            v-model="storyboardPrompt"
            :assets="storyboardPromptAssets"
            variant="drama-storyboard"
            placeholder="请输入分镜图提示词，输入 @ 引用当前分镜素材"
          />
        </div>
        <div class="storyboard-media-summary">
          <strong>分镜 {{ selectedStoryboard.sequence_no }}</strong
          ><span>{{ activeMediaType === "storyboard_image" ? "分镜图处理" : "视频处理" }}</span>
        </div>
        <div class="storyboard-media-actions">
          <el-button
            :type="selectedStoryboardTask ? 'danger' : 'primary'"
            :icon="selectedStoryboardTask ? CircleClose : MagicStick"
            :disabled="selectedStoryboardTask?.status === 'cancel_requested' || busy || historyLimitReached"
            @click="handleStoryboardGeneration"
          >
            {{
              selectedStoryboardTask
                ? "取消生成"
                : activeMediaType === "storyboard_image"
                  ? selectedStoryboard.thumbnail_url
                    ? "重新生成分镜图"
                    : "生成分镜图"
                  : selectedStoryboard.result_url
                    ? "重新生成视频"
                    : "生成视频"
            }}
          </el-button>
        </div>
      </div>
    </Transition>
    <section
      v-if="historyVisible"
      class="asset-history"
      title="历史记录"
    >
      <header>
        <strong>历史记录</strong><small>{{ historicalOutputs.length }} / 5</small>
      </header>
      <div
        v-loading="historyLoading"
        class="asset-history-list"
      >
        <article
          v-for="output in historicalOutputs"
          :key="output.id"
          class="asset-history-item"
        >
          <LoadingMedia
            v-if="output.mime_type.startsWith('image/')"
            type="image"
            :src="output.public_url"
            alt="历史图片"
          />
          <LoadingMedia
            v-else
            type="video"
            :src="output.public_url"
            muted
            playsinline
            preload="metadata"
            disablepictureinpicture
            disableremoteplayback
            controlslist="nodownload noplaybackrate noremoteplayback"
          />
          <span class="asset-history-actions">
            <button
              type="button"
              title="查看"
              @click="previewHistory(output)"
            >
              <el-icon><FullScreen /></el-icon>
            </button>
            <button
              type="button"
              :title="activeMediaType === 'storyboard_video' ? '设为备选' : '设为主图'"
              :disabled="output.active || (activeMediaType === 'storyboard_video' && candidateLimitReached)"
              @click="activateHistory(output)"
            >
              <el-icon><Check /></el-icon>
            </button>
            <button
              type="button"
              title="删除"
              :disabled="output.active"
              @click="deleteHistory(output)"
            >
              <el-icon><Delete /></el-icon>
            </button>
            <button
              type="button"
              title="下载"
              @click="downloadMedia(output.media_asset_id, output.public_url, `history-${output.id}`)"
            >
              <el-icon><Download /></el-icon>
            </button>
          </span>
        </article>
        <span
          v-if="!historicalOutputs.length && !historyLoading"
          class="asset-history-empty"
          >暂无历史记录</span
        >
      </div>
      <small
        v-if="historyLimitReached"
        class="asset-history-limit"
        >历史记录已满，请删除后再生成</small
      >
    </section>

    <el-dialog
      v-model="previewOpen"
      title="资产预览"
      :width="
        historyPreview && !historyPreview.mime_type.startsWith('image/')
          ? 'min(980px, calc(100vw - 28px))'
          : 'min(980px, calc(100vw - 28px))'
      "
      class="asset-preview-dialog"
      append-to-body
      destroy-on-close
      @closed="historyPreview = undefined"
      ><VideoPlayer
        v-if="historyPreview && !historyPreview.mime_type.startsWith('image/')"
        class="asset-history-player"
        :src="historyPreview.public_url" /><ZoomableImage
        v-else-if="historyPreview"
        class="asset-original"
        :src="historyPreview.public_url"
        alt="历史记录预览" /><ZoomableImage
        v-else-if="selected?.image_url"
        class="asset-original"
        :src="selected.image_url"
        :alt="selected.name" /><ZoomableImage
        v-else-if="selectedStoryboard?.thumbnail_url"
        class="asset-original"
        :src="selectedStoryboard.thumbnail_url"
        :alt="`分镜 ${selectedStoryboard.sequence_no}`"
    /></el-dialog>
    <VideoPlayer
      v-if="fullscreenVideoUrl"
      ref="storyboardVideoPlayer"
      class="storyboard-video-player-launcher"
      :src="fullscreenVideoUrl"
    />
  </aside>
</template>

<style scoped lang="scss" src="./AssetPanel.core.scss"></style>
<style scoped src="./AssetPanel.media.css"></style>
<style scoped src="./AssetPanel.preview.css"></style>
<style src="./AssetPanel.dialog.css"></style>
