<!-- 短剧项目详情页，负责剧集导入、项目配置和剧集状态管理。 -->
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, type ComponentPublicInstance } from "vue";
import { Edit, Plus, Setting, Upload } from "@element-plus/icons-vue";
import { ElMessageBox, genFileId, type UploadFile, type UploadInstance, type UploadRawFile } from "element-plus";
import { ArrowLeft, BookOpen, CircleHelp, FilePlus2, FileText, FileUp, LoaderCircle, Pencil, Trash2 } from "@/icons";
import { useRoute, useRouter } from "vue-router";
import CreativeShell from "@/components/creative/CreativeShell.vue";
import LoadingMedia from "@/components/creative/LoadingMedia.vue";
import ModelConfigDialog from "@/components/creative/ModelConfigDialog.vue";
import ProjectDialog from "@/components/creative/ProjectDialog.vue";
import {
  creativeApi,
  type CreativeOptions,
  type DramaImportPreview,
  type DramaParseTask,
  type ProjectDetail,
  type ProjectPayload,
} from "@/api/creative";
import { apiError } from "@/api/http";
import { premiumDramaPromptTypes, useModelPrompts } from "@/composables/useModelPrompts";
import { useToastStore } from "@/stores/toast";
import { firstVideoFrameURL } from "@/utils/media";

const route = useRoute(),
  router = useRouter(),
  toast = useToastStore(),
  projectId = String(route.params.projectId);
const project = ref<ProjectDetail | null>(null),
  options = ref<CreativeOptions>({ styles: [], models: [] }),
  tasks = ref<DramaParseTask[]>([]);
const loading = ref(false),
  saving = ref(false),
  editOpen = ref(false),
  modelsOpen = ref(false),
  creationOpen = ref(false),
  importOpen = ref(false);
const importFile = ref<File>(),
  rawText = ref(""),
  preview = ref<DramaImportPreview>(),
  chaptersPerEpisode = ref(1),
  startEpisodeNo = ref(1),
  importing = ref(false);
const uploadRef = ref<UploadInstance>();
const editingEpisodeId = ref(""),
  editingEpisodeName = ref(""),
  editingEpisodeInput = ref<HTMLInputElement>();
const importMode = ref<"file" | "text">("file"),
  pageDragActive = ref(false);
const { prompts, promptSelections, loadPrompts, selectPrompt, createPrompt, updatePrompt, deletePrompt } =
  useModelPrompts();
let timer: ReturnType<typeof setInterval> | undefined;
let dragDepth = 0;
const modelReady = computed(() =>
  ["text", "image", "video"].every((type) => project.value?.model_configs.some((item) => item.model_type === type)),
);
const defaultModelConfigs = [
  { modelType: "text", purpose: "prompt_reverse", settings: { prompt: "" } },
  { modelType: "image", purpose: "image_generation", settings: { resolution: "1k", prompt: "" } },
  { modelType: "video", purpose: "video_generation", settings: { resolution: "480p", prompt: "" } },
] as const;
const taskByEpisode = computed(() => new Map(tasks.value.map((item) => [item.episode_id, item])));
const activeDramaStatuses = new Set(["queued", "running", "retry_wait", "cancel_requested"]);
const hasActive = computed(() => tasks.value.some((item) => activeDramaStatuses.has(item.status)));
const backTarget = computed(() => ({ name: "drama-projects" }));

function activeTaskForEpisode(episodeId: string) {
  const task = taskByEpisode.value.get(episodeId);
  return task && activeDramaStatuses.has(task.status) ? task : undefined;
}

function parseStatusText(task: DramaParseTask) {
  if (task.status === "running") return "剧本解析中";
  if (task.status === "retry_wait") return "剧本解析重试等待中";
  if (task.status === "cancel_requested") return "剧本解析取消中";
  return "剧本解析排队中";
}

/** 将项目缺失的模型配置按弹窗默认值持久化，不覆盖已经保存的模型选择。 */
async function ensureDefaultModelConfigs(loadedProject: ProjectDetail, loadedOptions: CreativeOptions) {
  const configuredTypes = new Set(loadedProject.model_configs.map((item) => item.model_type));
  const payload: Record<string, { model_id: string; settings: { resolution?: string; prompt: string } }> = {};
  for (const config of defaultModelConfigs) {
    if (configuredTypes.has(config.modelType)) continue;
    const model = loadedOptions.models.find((item) => item.model_type === config.modelType);
    if (model) payload[config.purpose] = { model_id: model.id, settings: { ...config.settings } };
  }
  if (!Object.keys(payload).length) return loadedProject;
  await creativeApi.saveModels(projectId, payload);
  return creativeApi.project(projectId);
}

/** 加载项目详情、创作选项和任务状态，并补齐首次使用时的默认模型配置。 */
async function load(show = true) {
  if (show) loading.value = true;
  try {
    const [loadedProject, loadedOptions, loadedTasks] = await Promise.all([
      creativeApi.project(projectId),
      creativeApi.options(),
      creativeApi.dramaParseTasks(projectId),
    ]);
    options.value = loadedOptions;
    tasks.value = loadedTasks;
    project.value = await ensureDefaultModelConfigs(loadedProject, loadedOptions);
    const max = Math.max(0, ...(project.value?.episodes || []).map((item) => item.episode_no));
    startEpisodeNo.value = max + 1;
  } catch (e) {
    showError(e, "加载失败");
  } finally {
    loading.value = false;
  }
  syncPolling();
}
function syncPolling() {
  if (hasActive.value && !timer) timer = setInterval(() => load(false), 3000);
  else if (!hasActive.value && timer) {
    clearInterval(timer);
    timer = undefined;
  }
}
async function saveProject(payload: ProjectPayload) {
  saving.value = true;
  try {
    await creativeApi.updateProject(projectId, { ...payload, project_type: "premium_drama", localization: null });
    editOpen.value = false;
    toast.success("项目已保存");
    await load(false);
  } catch (e) {
    showError(e, "保存失败");
  } finally {
    saving.value = false;
  }
}
async function saveModels(payload: Record<string, unknown>) {
  saving.value = true;
  try {
    await creativeApi.saveModels(projectId, payload);
    modelsOpen.value = false;
    toast.success("模型配置已保存");
    await load(false);
  } catch (e) {
    showError(e, "保存失败");
  } finally {
    saving.value = false;
  }
}
async function openModels() {
  modelsOpen.value = true;
  await loadPrompts(premiumDramaPromptTypes);
}
function openImportCreation() {
  creationOpen.value = false;
  importOpen.value = true;
}
async function createBlankEpisode() {
  if (saving.value) return;
  saving.value = true;
  try {
    const episode = await creativeApi.createEpisode(projectId);
    creationOpen.value = false;
    toast.success(`第 ${episode.episode_no} 集已创建`);
    await load(false);
  } catch (e) {
    showError(e, "创建失败");
  } finally {
    saving.value = false;
  }
}
function setEditingEpisodeInput(element: Element | ComponentPublicInstance | null) {
  editingEpisodeInput.value = element instanceof HTMLInputElement ? element : undefined;
}
async function startEpisodeRename(episode: ProjectDetail["episodes"][number]) {
  editingEpisodeId.value = episode.id;
  editingEpisodeName.value = episode.name || `第${episode.episode_no}集`;
  await nextTick();
  editingEpisodeInput.value?.focus();
  editingEpisodeInput.value?.select();
}
function cancelEpisodeRename() {
  editingEpisodeId.value = "";
  editingEpisodeName.value = "";
}
function finishEpisodeRename() {
  editingEpisodeInput.value?.blur();
}
async function saveEpisodeName(episode: ProjectDetail["episodes"][number]) {
  if (editingEpisodeId.value !== episode.id) return;
  const name = editingEpisodeName.value.trim();
  cancelEpisodeRename();
  if (!name) {
    toast.warning("剧集名称不能为空");
    return;
  }
  const duplicate = project.value?.episodes.some(
    (item) => item.id !== episode.id && item.name.trim().toLocaleLowerCase() === name.toLocaleLowerCase(),
  );
  if (duplicate) {
    toast.warning("同一项目下不可有同名剧集");
    return;
  }
  if (name === episode.name) return;
  const originalName = episode.name;
  episode.name = name;
  try {
    await creativeApi.updateEpisodeName(projectId, episode.id, name);
    toast.success("剧集名称已保存");
  } catch (e) {
    episode.name = originalName;
    showError(e, "保存剧集名称失败");
  }
}
async function removeEpisode(id: string, no: number) {
  try {
    await ElMessageBox.confirm(`确定删除第 ${no} 集吗？`, `删除剧集`, {
      type: "warning",
      confirmButtonText: "删除",
      cancelButtonText: "取消",
    });
    await creativeApi.deleteEpisode(projectId, id);
    toast.success("剧集已删除");
    await load(false);
  } catch (e) {
    if (e !== "cancel" && e !== "close") showError(e, "删除失败");
  }
}
async function previewImport() {
  if (!importFile.value && !rawText.value.trim()) {
    toast.warning("请选择文件或粘贴小说正文");
    return;
  }
  importing.value = true;
  try {
    preview.value = await creativeApi.previewDramaImport(projectId, {
      file: importFile.value,
      rawText: rawText.value.trim(),
    });
    if (!preview.value.has_chapters) toast.info("未识别到章节标题，请确认是否将全部内容作为一集导入。");
  } catch (e) {
    showError(e, "读取小说失败");
  } finally {
    importing.value = false;
  }
}
async function confirmImport() {
  if (!preview.value) return;
  if (!preview.value.has_chapters) {
    try {
      await ElMessageBox.confirm("未识别到章节标题，是否将全部内容作为一集导入？", "确认单集导入", {
        type: "warning",
        confirmButtonText: "作为一集导入",
        cancelButtonText: "返回",
      });
    } catch {
      return;
    }
  }
  importing.value = true;
  try {
    await creativeApi.confirmDramaImport(projectId, {
      import_token: preview.value.import_token,
      chapters_per_episode: chaptersPerEpisode.value,
      start_episode_no: startEpisodeNo.value,
      treat_as_single_episode: !preview.value.has_chapters,
    });
    toast.success("小说已导入");
    closeImport();
    await load(false);
  } catch (e) {
    showError(e, "导入失败");
  } finally {
    importing.value = false;
  }
}
function validateImportFile(file: File) {
  const name = file.name.trim().toLowerCase();
  if (![".txt", ".doc", ".docx"].some((extension) => name.endsWith(extension))) {
    importFile.value = undefined;
    preview.value = undefined;
    uploadRef.value?.clearFiles();
    toast.warning("仅支持TXT、DOC、DOCX文件");
    return false;
  }
  if (file.size > 3 * 1024 * 1024) {
    importFile.value = undefined;
    preview.value = undefined;
    uploadRef.value?.clearFiles();
    toast.warning("小说文件不能超过3MB");
    return false;
  }
  return true;
}
function selectFile(file: UploadFile) {
  if (!file.raw || !validateImportFile(file.raw)) return;
  importMode.value = "file";
  importFile.value = file.raw;
  rawText.value = "";
  preview.value = undefined;
}
function useRawText() {
  if (!rawText.value) return;
  importFile.value = undefined;
  uploadRef.value?.clearFiles();
  preview.value = undefined;
}
function closeImport() {
  importOpen.value = false;
  importMode.value = "file";
  importFile.value = undefined;
  rawText.value = "";
  preview.value = undefined;
  resetPageDrag();
  uploadRef.value?.clearFiles();
}
function isFileDrag(event: DragEvent) {
  return Array.from(event.dataTransfer?.types || []).includes("Files");
}
function handlePageDragEnter(event: DragEvent) {
  if (!importOpen.value || preview.value || !isFileDrag(event)) return;
  event.preventDefault();
  dragDepth += 1;
  importMode.value = "file";
  pageDragActive.value = true;
}
function handlePageDragOver(event: DragEvent) {
  if (!importOpen.value || preview.value || !isFileDrag(event)) return;
  event.preventDefault();
  if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
  pageDragActive.value = true;
}
function handlePageDragLeave(event: DragEvent) {
  if (!pageDragActive.value) return;
  if (!event.relatedTarget) {
    resetPageDrag();
    return;
  }
  dragDepth = Math.max(0, dragDepth - 1);
  if (dragDepth === 0) pageDragActive.value = false;
}
async function handlePageDrop(event: DragEvent) {
  if (!importOpen.value || preview.value || !isFileDrag(event)) return;
  event.preventDefault();
  event.stopPropagation();
  const file = event.dataTransfer?.files?.[0];
  resetPageDrag();
  if (!file || !validateImportFile(file)) return;
  importMode.value = "file";
  await nextTick();
  const rawFile = file as UploadRawFile;
  rawFile.uid = genFileId();
  uploadRef.value?.clearFiles();
  uploadRef.value?.handleStart(rawFile);
}
function resetPageDrag() {
  dragDepth = 0;
  pageDragActive.value = false;
}
async function openEpisode(id: string) {
  await router.push({
    path: `/drama-projects/${projectId}/episodes/${id}`,
    query: { from: String(route.query.from || "drama-projects") },
  });
}
function showError(error: unknown, title: string) {
  const detail = apiError(error);
  toast.error(detail.message, detail.copyText, title);
}
watch(importMode, (mode) => {
  preview.value = undefined;
  if (mode === "file") rawText.value = "";
  else {
    importFile.value = undefined;
    uploadRef.value?.clearFiles();
  }
});
onMounted(() => {
  load();
  document.addEventListener("dragenter", handlePageDragEnter, true);
  document.addEventListener("dragover", handlePageDragOver, true);
  document.addEventListener("dragleave", handlePageDragLeave, true);
  document.addEventListener("drop", handlePageDrop, true);
});
onBeforeUnmount(() => {
  if (timer) clearInterval(timer);
  document.removeEventListener("dragenter", handlePageDragEnter, true);
  document.removeEventListener("dragover", handlePageDragOver, true);
  document.removeEventListener("dragleave", handlePageDragLeave, true);
  document.removeEventListener("drop", handlePageDrop, true);
});
</script>

<template>
  <CreativeShell page-title="短剧创作">
    <section class="project-context-bar">
      <div class="project-context-main">
        <RouterLink
          class="back-button"
          :to="backTarget"
          title="返回"
        >
          <ArrowLeft :size="18" />
        </RouterLink>
        <strong>{{ project?.name || "短剧创作项目" }}</strong>
      </div>
      <div class="project-context-actions creative-page-actions">
        <el-button
          :icon="Edit"
          @click="editOpen = true"
          >项目设置</el-button
        ><el-button
          :icon="Setting"
          :class="{ 'model-warning': !modelReady }"
          @click="openModels"
          >模型配置</el-button
        ><el-button
          type="primary"
          :icon="Plus"
          @click="creationOpen = true"
          >新建剧集</el-button
        >
      </div>
    </section>
    <div v-loading="loading">
      <section
        v-if="!modelReady && project"
        class="setup-notice"
      >
        <div>
          <strong>解析和生成前需要完成模型配置</strong
          ><span>分别选择文本、图像和视频模型，短剧创作与剧本反推配置相互独立。</span>
        </div>
        <el-button
          type="primary"
          @click="openModels"
          >立即配置</el-button
        >
      </section>
      <section class="episode-list">
        <article
          v-for="episode in project?.episodes || []"
          :key="episode.id"
          class="episode-card"
          @click="openEpisode(episode.id)"
        >
          <div class="episode-cover">
            <LoadingMedia
              v-if="episode.cover_video_url"
              type="video"
              :src="firstVideoFrameURL(episode.cover_video_url)"
              muted
              playsinline
              preload="metadata"
              disablepictureinpicture
              disableremoteplayback
              controlslist="nodownload noplaybackrate noremoteplayback"
            />
            <span
              v-else
              class="episode-cover-placeholder"
              ><BookOpen :size="30"
            /></span>
            <span
              class="episode-status"
              :class="{ active: activeTaskForEpisode(episode.id) }"
            >
              {{
                activeTaskForEpisode(episode.id)
                  ? parseStatusText(activeTaskForEpisode(episode.id)!)
                  : `第 ${episode.episode_no} 集`
              }}
            </span>
          </div>
          <div class="episode-copy">
            <div class="episode-title-row">
              <input
                v-if="editingEpisodeId === episode.id"
                :ref="setEditingEpisodeInput"
                v-model="editingEpisodeName"
                maxlength="160"
                @click.stop
                @blur="saveEpisodeName(episode)"
              />
              <template v-else>
                <h2>{{ episode.name || `第 ${episode.episode_no} 集` }}</h2>
                <button
                  type="button"
                  title="编辑剧集名称"
                  @click.stop="startEpisodeRename(episode)"
                >
                  <Pencil :size="14" />
                </button>
              </template>
            </div>
            <button
              class="episode-delete"
              type="button"
              title="删除"
              @click.stop="removeEpisode(episode.id, episode.episode_no)"
            >
              <Trash2 :size="15" />
            </button>
          </div>
          <div class="episode-meta">
            <span>{{ episode.storyboard_count || 0 }} 个分镜</span>
            <span>{{ episode.analysis_message || (episode.has_text_source ? "原文已就绪" : "待添加原文") }}</span>
          </div>
        </article>
        <div
          v-if="!project?.episodes.length && !loading"
          class="episode-empty"
        >
          <BookOpen :size="36" />
          <strong>暂无剧集</strong>
          <el-button
            type="primary"
            :icon="Plus"
            @click="creationOpen = true"
            >新建剧集</el-button
          >
        </div>
      </section>
    </div>
    <ProjectDialog
      v-model="editOpen"
      :options="options"
      :project="project"
      :saving="saving"
      project-type="premium_drama"
      @save="saveProject"
    />
    <ModelConfigDialog
      v-if="project"
      v-model="modelsOpen"
      :options="options"
      :configs="project.model_configs"
      :prompts="prompts"
      :prompt-selections="promptSelections"
      :saving="saving"
      project-type="premium_drama"
      @save="saveModels"
      @prompt-select="selectPrompt"
      @prompt-create="createPrompt"
      @prompt-update="updatePrompt"
      @prompt-delete="deletePrompt"
    />
    <el-dialog
      v-model="creationOpen"
      title="新建剧集"
      width="min(560px,calc(100vw - 28px))"
      class="creative-dialog"
    >
      <div class="creation-options">
        <button
          type="button"
          @click="openImportCreation"
        >
          <span class="creation-option-icon"><FileUp :size="22" /></span>
          <span><strong>导入剧本</strong><small>上传文件或粘贴正文，按章节批量创建剧集</small></span>
        </button>
        <button
          type="button"
          :disabled="saving"
          @click="createBlankEpisode"
        >
          <span class="creation-option-icon"><FilePlus2 :size="22" /></span>
          <span
            ><strong>{{ saving ? "正在创建" : "创建空白剧集" }}</strong
            ><small>自动创建下一集，稍后填写剧集原文</small></span
          >
        </button>
      </div>
    </el-dialog>
    <el-dialog
      :model-value="importOpen"
      title="导入剧本"
      width="min(700px,calc(100vw - 28px))"
      :class="['creative-dialog', 'import-dialog', { 'page-drag-active': pageDragActive }]"
      @close="closeImport"
    >
      <template #header>
        <span class="import-dialog-title"
          >导入剧本
          <el-tooltip placement="top">
            <template #content
              >系统会按章节标题识别并切分内容，可设置每集包含的章节数。<br />未识别到有效章节标题时，可将全文作为一集导入。</template
            >
            <CircleHelp
              class="import-help"
              :size="16"
              title="查看章节切分说明"
            />
          </el-tooltip>
        </span>
      </template>
      <el-tabs
        v-if="!preview"
        v-model="importMode"
        class="import-tabs"
        stretch
      >
        <el-tab-pane name="file"
          ><template #label
            ><span class="import-tab-label"
              ><el-icon> <Upload /> </el-icon>上传文件</span
            ></template
          >
          <div class="import-form">
            <el-upload
              ref="uploadRef"
              class="import-upload"
              drag
              :auto-upload="false"
              :limit="1"
              accept=".txt,.doc,.docx"
              :on-change="selectFile"
              ><el-icon class="el-icon--upload">
                <Upload />
              </el-icon>
              <div>{{ pageDragActive ? "松开添加文件" : "拖入或选择TXT、DOC、DOCX文件" }}</div>
              <small>单个文件最大3MB</small>
            </el-upload>
          </div>
        </el-tab-pane>
        <el-tab-pane name="text"
          ><template #label
            ><span class="import-tab-label"> <FileText :size="15" />粘贴文本 </span></template
          >
          <div class="import-form">
            <el-input
              v-model="rawText"
              type="textarea"
              :rows="12"
              resize="none"
              maxlength="3145728"
              placeholder="粘贴小说正文"
              @input="useRawText"
            />
          </div>
        </el-tab-pane>
      </el-tabs>
      <div
        v-else
        class="import-preview"
      >
        <strong>{{ preview.has_chapters ? `识别到 ${preview.chapters.length} 个章节` : "未识别到章节标题" }}</strong>
        <p>共 {{ preview.total_characters.toLocaleString() }} 字</p>
        <div
          v-if="preview.has_chapters"
          class="chapter-list"
        >
          <span
            v-for="(chapter, index) in preview.chapters"
            :key="index"
            >{{ chapter.title }}<small>{{ chapter.char_count }} 字</small></span
          >
        </div>
        <el-form label-position="top"
          ><el-form-item
            v-if="preview.has_chapters"
            label="每集包含章节数"
            ><el-input-number
              v-model="chaptersPerEpisode"
              :min="1"
              :max="20" /></el-form-item
          ><el-form-item label="起始集数"
            ><el-input-number
              v-model="startEpisodeNo"
              :min="1"
              :max="9999" /></el-form-item
        ></el-form>
      </div>
      <template #footer
        ><el-button
          v-if="preview"
          @click="preview = undefined"
          >返回</el-button
        ><el-button @click="closeImport">取消</el-button
        ><el-button
          type="primary"
          :loading="importing"
          @click="preview ? confirmImport() : previewImport()"
          >{{ preview ? "确认导入" : "读取并预览" }}</el-button
        ></template
      >
    </el-dialog>
  </CreativeShell>
</template>

<style scoped lang="scss" src="./DramaProjectView.styles.scss"></style>
