<template>
  <section class="delivery-workspace">
    <div class="delivery-workspace__tabs">
      <el-tabs v-model="activeTab" class="delivery-workspace__tab-panel">
        <el-tab-pane label="视频复刻" name="remake" />
        <el-tab-pane label="图生视频" name="imageToVideo" />
      </el-tabs>
      <button type="button" class="secret-settings-btn" @click="openSecretDialog">密钥设置</button>
    </div>

    <div class="delivery-grid">
      <div class="delivery-form-area">
        <div class="delivery-column">
          <AiSectionCard v-if="activeTab === 'remake'" step="01" title="参考视频" description="本地视频或抖音链接，作为复刻依据。"
            class="delivery-card">
            <AiAssetDropzone title="参考视频" description="上传后会先转为 OSS 地址。" placeholder="上传本地视频"
              helper="支持 MP4 / MOV，也可在下方粘贴抖音/视频链接" accept="video/*"
              :file-name="currentWorkspace.referenceAsset.fileName"
              :preview-url="currentWorkspace.referenceAsset.previewUrl"
              :preview-kind="currentWorkspace.referenceAsset.previewKind"
              :source-url="currentWorkspace.referenceAsset.sourceUrl"
              :loading="currentWorkspace.referenceAsset.uploading" compact action-label="上传参考视频" selected-label="参考视频"
              @select="(file) => setAsset(activeTab, 'referenceAsset', file)"
              @clear="clearAsset(activeTab, 'referenceAsset')" />
            <div class="script-learning-panel__toolbar">
              <el-input v-model="currentWorkspace.referenceAsset.sourceUrl" class="script-learning-panel__source-input"
                placeholder="粘贴抖音链接或视频链接，作为视频复刻的参考视频" />
              <button type="button" class="script-action-btn" @click="pasteReferenceLink">粘贴</button>
              <button type="button" class="script-primary-action-btn" :disabled="copyLearningLoading"
                @click="rewriteScriptFromSource">
                提取/仿写文案
              </button>
            </div>
          </AiSectionCard>

          <AiSectionCard v-if="activeTab === 'imageToVideo'" step="01" title="视频规格" description="控制成片尺寸、清晰度、时长和整体画面风格。"
            class="delivery-card">
            <div class="delivery-form-grid delivery-form-grid--two">
              <div class="delivery-field">
                <span class="delivery-field__label">画面比例</span>
                <AiChoicePills v-model="currentWorkspace.ratio" :options="ratioOptions" />
              </div>
              <div class="delivery-field">
                <span class="delivery-field__label">输出分辨率</span>
                <AiChoicePills v-model="currentWorkspace.resolution" :options="resolutionOptions" />
              </div>
              <div class="delivery-field">
                <span class="delivery-field__label">模型</span>
                <AiChoicePills v-model="currentWorkspace.model" :options="modelOptions" />
              </div>
              <div class="delivery-field">
                <span class="delivery-field__label">视频时长</span>
                <div class="duration-control">
                  <AiChoicePills v-model="currentWorkspace.duration" :options="durationOptions"
                    class="duration-control__presets" @update:model-value="setDurationPreset" />
                  <div class="duration-control__custom">
                    <span class="duration-control__custom-label">自定义</span>
                    <el-input-number v-model="currentWorkspace.durationSeconds" class="duration-control__input" :min="1"
                      :max="300" :step="1" controls-position="right" @focus="clearDurationPreset"
                      @change="handleCustomDurationChange" />
                    <span class="duration-control__unit">秒</span>
                  </div>
                </div>
              </div>
              <div class="delivery-field">
                <span class="delivery-field__label">视频风格/动作引导</span>
                <el-input v-model="currentWorkspace.videoPrompt" type="textarea" :rows="4" resize="none"
                  placeholder="例如：视频画面保持高清，运镜平稳，整体风格干净明亮。" />
              </div>
            </div>
          </AiSectionCard>

          <AiSectionCard :step="activeTab === 'remake' ? '04' : '03'" title="场景" description="提示词微调或上传背景图替换。"
            class="delivery-card">
            <div class="delivery-field">
              <span class="delivery-field__label">处理方式</span>
              <AiChoicePills v-model="currentWorkspace.sceneMode" :options="sceneModeOptions" />
            </div>
            <div class="delivery-field">
              <span class="delivery-field__label">场景提示词</span>
              <el-input v-model="currentWorkspace.scenePrompt" type="textarea" :rows="4" resize="none"
                placeholder="例如：干净明亮的现代厨房背景，温暖阳光照入。" />
            </div>
            <AiAssetDropzone v-if="currentWorkspace.sceneMode === '上传场景图替换'" title="场景图" description="上传后会先转为 OSS 地址。"
              placeholder="上传场景图" helper="支持 JPG / PNG" accept="image/*"
              :file-name="currentWorkspace.sceneAsset.fileName" :preview-url="currentWorkspace.sceneAsset.previewUrl"
              :preview-kind="currentWorkspace.sceneAsset.previewKind"
              :source-url="currentWorkspace.sceneAsset.sourceUrl" :loading="currentWorkspace.sceneAsset.uploading"
              allow-url compact action-label="上传场景图" selected-label="场景图" url-label="场景图链接" url-placeholder="也可以粘贴场景图链接"
              @select="(file) => setAsset(activeTab, 'sceneAsset', file)"
              @url-change="(url) => setAssetUrl(activeTab, 'sceneAsset', url, 'image/*')" preview-clickable
              @preview="openImagePreview" @clear="clearAsset(activeTab, 'sceneAsset')" />
          </AiSectionCard>
        </div>

        <div class="delivery-column">
          <AiSectionCard v-if="activeTab === 'remake'" step="02" title="视频规格" description="控制成片尺寸、清晰度、时长和整体画面风格。"
            class="delivery-card">
            <div class="delivery-form-grid delivery-form-grid--two">
              <div class="delivery-field">
                <span class="delivery-field__label">画面比例</span>
                <AiChoicePills v-model="currentWorkspace.ratio" :options="ratioOptions" />
              </div>
              <div class="delivery-field">
                <span class="delivery-field__label">输出分辨率</span>
                <AiChoicePills v-model="currentWorkspace.resolution" :options="resolutionOptions" />
              </div>
              <div class="delivery-field">
                <span class="delivery-field__label">模型</span>
                <AiChoicePills v-model="currentWorkspace.model" :options="modelOptions" />
              </div>
              <div class="delivery-field">
                <span class="delivery-field__label">视频时长</span>
                <div class="duration-control">
                  <AiChoicePills v-model="currentWorkspace.duration" :options="durationOptions"
                    class="duration-control__presets" @update:model-value="setDurationPreset" />
                  <div class="duration-control__custom">
                    <span class="duration-control__custom-label">自定义</span>
                    <el-input-number v-model="currentWorkspace.durationSeconds" class="duration-control__input" :min="1"
                      :max="300" :step="1" controls-position="right" @focus="clearDurationPreset"
                      @change="handleCustomDurationChange" />
                    <span class="duration-control__unit">秒</span>
                  </div>
                </div>
              </div>
              <div class="delivery-field">
                <span class="delivery-field__label">视频风格/动作引导</span>
                <el-input v-model="currentWorkspace.videoPrompt" type="textarea" :rows="4" resize="none"
                  placeholder="例如：视频画面保持高清，运镜平稳，整体风格干净明亮。" />
              </div>
            </div>
          </AiSectionCard>

          <AiSectionCard v-if="activeTab === 'imageToVideo'" step="02" title="产品信息" description="填写商品基础信息，上传一张核心产品图。"
            class="delivery-card">
            <div class="delivery-field">
              <span class="delivery-field__label">产品类目</span>
              <el-input v-model="currentWorkspace.categoryName" clearable placeholder="输入产品类目，例如：服饰鞋包 / 男鞋 / 运动鞋" />
            </div>
            <div class="delivery-field">
              <span class="delivery-field__label">产品名称 *</span>
              <el-input v-model="currentWorkspace.productName" clearable aria-required="true" placeholder="例如：新款复古运动鞋" />
            </div>
            <div class="delivery-field">
              <span class="delivery-field__label">核心卖点 / 产品属性</span>
              <el-input v-model="currentWorkspace.productFocus" type="textarea" :rows="4" resize="none"
                placeholder="例如：防滑耐磨、复古网面透气设计。" />
            </div>
            <div class="product-assets">
              <AiAssetDropzone v-for="(asset, index) in currentWorkspace.productAssets" :key="index"
                :title="`产品图 ${index + 1}`" :description="index === 0 ? '最多上传 5 张，提交时按接口数组格式传入。' : ''"
                placeholder="上传产品图" helper="支持 JPG / PNG" accept="image/*" :file-name="asset.fileName"
                :preview-url="asset.previewUrl" :preview-kind="asset.previewKind" :source-url="asset.sourceUrl"
                :loading="asset.uploading" allow-url compact preview-clickable action-label="上传产品图" selected-label="产品图"
                url-label="产品图链接" url-placeholder="也可以粘贴产品图链接"
                @select="(file) => setProductAsset(activeTab, index, file)"
                @url-change="(url) => setProductAssetUrl(activeTab, index, url)" @preview="openImagePreview"
                @clear="clearProductAsset(activeTab, index)" />
              <button v-if="currentWorkspace.productAssets.length < 5" type="button"
                class="script-action-btn product-assets__add" @click="addProductAsset(activeTab)">添加产品图（{{
                  currentWorkspace.productAssets.length }}/5）</button>
            </div>
          </AiSectionCard>

          <AiSectionCard :step="activeTab === 'remake' ? '05' : '04'" title="人脸" description="性别、模特图或人物类型三选一。"
            class="delivery-card">
            <div class="delivery-field">
              <span class="delivery-field__label">输入方式</span>
              <AiChoicePills v-model="currentWorkspace.faceInputType" :options="faceInputTypeOptions" />
            </div>
            <div v-if="currentWorkspace.faceInputType === '性别选项'" class="delivery-field">
              <span class="delivery-field__label">性别</span>
              <AiChoicePills v-model="currentWorkspace.gender" :options="genderOptions" />
            </div>
            <div v-if="currentWorkspace.faceInputType === '人物类型'" class="delivery-field">
              <span class="delivery-field__label">人物类型</span>
              <el-input v-model="currentWorkspace.modelFigure" clearable
                placeholder="输入人物类型，例如：甜妹 / 御姐 / 成熟女性 / 职场女性" />
            </div>
            <AiAssetDropzone v-if="currentWorkspace.faceInputType === '上传模特图'" title="模特图 / 人脸图"
              description="上传后会先转为 OSS 地址。" placeholder="上传清晰正脸图" helper="建议 1080px 以上，单人正面" accept="image/*"
              :file-name="currentWorkspace.modelAsset.fileName" :preview-url="currentWorkspace.modelAsset.previewUrl"
              :preview-kind="currentWorkspace.modelAsset.previewKind"
              :source-url="currentWorkspace.modelAsset.sourceUrl" :loading="currentWorkspace.modelAsset.uploading"
              allow-url compact preview-clickable action-label="上传模特图" selected-label="模特图" url-label="模特图链接"
              url-placeholder="也可以粘贴模特图链接" @select="(file) => setAsset(activeTab, 'modelAsset', file)"
              @url-change="(url) => setAssetUrl(activeTab, 'modelAsset', url, 'image/*')" @preview="openImagePreview"
              @clear="clearAsset(activeTab, 'modelAsset')" />
          </AiSectionCard>

        </div>

        <div class="delivery-column">
          <AiSectionCard v-if="activeTab === 'remake'" step="03" title="产品信息" description="填写商品基础信息，上传一张核心产品图。"
            class="delivery-card">
            <div class="delivery-field">
              <span class="delivery-field__label">产品类目</span>
              <el-input v-model="currentWorkspace.categoryName" clearable placeholder="输入产品类目，例如：服饰鞋包 / 男鞋 / 运动鞋" />
            </div>
            <div class="delivery-field">
              <span class="delivery-field__label">产品名称 *</span>
              <el-input v-model="currentWorkspace.productName" clearable aria-required="true" placeholder="例如：新款复古运动鞋" />
            </div>
            <div class="delivery-field">
              <span class="delivery-field__label">核心卖点 / 产品属性</span>
              <el-input v-model="currentWorkspace.productFocus" type="textarea" :rows="4" resize="none"
                placeholder="例如：防滑耐磨、复古网面透气设计。" />
            </div>
            <div class="product-assets">
              <AiAssetDropzone v-for="(asset, index) in currentWorkspace.productAssets" :key="index"
                :title="`产品图 ${index + 1}`" :description="index === 0 ? '最多上传 5 张，提交时按接口数组格式传入。' : ''"
                placeholder="上传产品图" helper="支持 JPG / PNG" accept="image/*" :file-name="asset.fileName"
                :preview-url="asset.previewUrl" :preview-kind="asset.previewKind" :source-url="asset.sourceUrl"
                :loading="asset.uploading" allow-url compact preview-clickable action-label="上传产品图" selected-label="产品图"
                url-label="产品图链接" url-placeholder="也可以粘贴产品图链接"
                @select="(file) => setProductAsset(activeTab, index, file)"
                @url-change="(url) => setProductAssetUrl(activeTab, index, url)" @preview="openImagePreview"
                @clear="clearProductAsset(activeTab, index)" />
              <button v-if="currentWorkspace.productAssets.length < 5" type="button"
                class="script-action-btn product-assets__add" @click="addProductAsset(activeTab)">添加产品图（{{
                  currentWorkspace.productAssets.length }}/5）</button>
            </div>
          </AiSectionCard>

          <AiSectionCard :step="activeTab === 'remake' ? '06' : '05'" title="话术 / 语音" description="编辑口播台词，选择最终配音方式。"
            class="delivery-card">
            <div class="voice-section">
              <div class="delivery-field">
                <span class="delivery-field__label">语言</span>
                <el-select v-model="currentWorkspace.language" class="delivery-select">
                  <el-option v-for="item in languageOptions" :key="item" :label="item" :value="item" />
                </el-select>
              </div>
              <button type="button" class="script-action-btn voice-manage-btn" @click="openVoiceDialog">音色管理</button>
            </div>

            <div class="delivery-field">
              <span class="delivery-field__label">音频类型</span>
              <AiChoicePills v-model="currentWorkspace.audioType" :options="audioTypeOptions" />
            </div>

            <div class="delivery-field">
              <span class="delivery-field__label">口播台词</span>
              <el-input v-model="currentWorkspace.scriptText" type="textarea" :rows="6" resize="none"
                placeholder="输入指定的口播台词或文案内容。" />
            </div>

            <div v-if="activeTab === 'imageToVideo'" class="delivery-field">
              <span class="delivery-field__label">文案来源链接</span>
              <div class="script-learning-panel__toolbar">
                <el-input v-model="currentWorkspace.scriptSourceUrl" class="script-learning-panel__source-input"
                  placeholder="粘贴抖音链接或视频链接，用于提取/仿写口播文案" />
                <button type="button" class="script-action-btn" @click="pasteScriptSourceLink">粘贴</button>
                <button type="button" class="script-primary-action-btn" :disabled="copyLearningLoading"
                  @click="rewriteScriptFromSource">
                  提取/仿写文案
                </button>
              </div>
            </div>

            <div class="delivery-field">
              <span class="delivery-field__label">配音方式</span>
              <AiChoicePills v-model="currentWorkspace.voiceMode" :options="voiceModeOptions" />
            </div>

            <template v-if="currentWorkspace.voiceMode === 'uploadAudio'">
              <AiAssetDropzone title="背景音乐文件" description="对应 audio_info.bgm_url；仅在背景音乐模式传入。" placeholder="上传背景音乐"
                helper="支持 MP3 / WAV，也可粘贴可访问的音频链接" accept="audio/*" :file-name="currentWorkspace.speechAsset.fileName"
                :preview-url="currentWorkspace.speechAsset.previewUrl"
                :preview-kind="currentWorkspace.speechAsset.previewKind"
                :source-url="currentWorkspace.speechAsset.sourceUrl" :loading="currentWorkspace.speechAsset.uploading"
                allow-url compact action-label="上传背景音乐" selected-label="背景音乐" url-label="背景音乐链接"
                @select="(file) => setAsset(activeTab, 'speechAsset', file)"
                @url-change="(url) => setAssetUrl(activeTab, 'speechAsset', url, 'audio/*')"
                @clear="clearAsset(activeTab, 'speechAsset')" />
            </template>

            <template v-else-if="currentWorkspace.voiceMode === 'synthesize'">
              <div class="delivery-form-grid delivery-form-grid--two">
                <div class="delivery-field">
                  <span class="delivery-field__label">合成音色</span>
                  <el-input v-model="currentWorkspace.voiceId" clearable placeholder="选择音色或手动输入 voice_id" />
                  <div v-if="currentWorkspace.voiceName" class="selected-voice-name">
                    {{ currentWorkspace.voiceName }}
                    <span>{{ currentWorkspace.voiceId }}</span>
                  </div>
                </div>
                <div class="delivery-field">
                  <span class="delivery-field__label">音色名称</span>
                  <el-input v-model="currentWorkspace.voiceName" clearable placeholder="对应 audio_info.voice_name" />
                </div>
                <div class="delivery-field">
                  <span class="delivery-field__label">音色操作</span>
                  <div class="voice-inline-actions">
                    <button type="button" class="script-action-btn" @click="openVoiceDialog">选择音色</button>
                    <button type="button" class="script-primary-action-btn" :disabled="voiceLoading"
                      @click="synthesizeVoice">生成配音</button>
                  </div>
                </div>
                <div class="delivery-field delivery-field--wide">
                  <span class="delivery-field__label">最终配音文件链接</span>
                  <el-input :model-value="currentWorkspace.speechAsset.sourceUrl" readonly
                    placeholder="语音合成成功后会自动写入这里" />
                </div>
              </div>
            </template>

            <div v-if="voiceStatusMessage" class="voice-status" :class="`voice-status--${voiceStatusType}`">
              {{ voiceStatusMessage }}
            </div>
          </AiSectionCard>
        </div>
      </div>

      <AiSectionCard :step="activeTab === 'remake' ? '07' : '06'" title="视频结果" description="提交后在这里等待、预览和下载成片。"
        class="delivery-card delivery-card--preview">
        <el-tabs v-model="currentWorkspace.generationMode" class="generation-mode-tabs">
          <el-tab-pane label="样片模式" name="draft">
            <p>快速生成预览视频，用于验证场景结构、镜头调度、主体动作和提示词意图，消耗更少 token。</p>
          </el-tab-pane>
          <el-tab-pane label="正常生成" name="standard">
            <p>关闭样片模式，按正常规格生成视频。</p>
          </el-tab-pane>
        </el-tabs>
        <div class="preview-panel__meta">
          <div class="preview-row">
            <span class="preview-row__label">模式</span>
            <span class="preview-row__value">{{ activeTab === 'remake' ? '视频复刻' : '图生视频' }}</span>
          </div>
          <div class="preview-row">
            <span class="preview-row__label">规格</span>
            <span class="preview-row__value">{{ currentWorkspace.ratio }} / {{ currentWorkspace.resolution }} / {{
              currentWorkspace.durationSeconds }}秒</span>
          </div>
          <div class="preview-row">
            <span class="preview-row__label">产品</span>
            <span class="preview-row__value">{{ currentWorkspace.productName || '未设置' }}</span>
          </div>
        </div>
        <div class="video-result-actions">
          <el-button class="delivery-primary-btn" type="primary" :loading="assemblyBusy || assemblyDownloading"
            :disabled="assemblyBusy || assemblyDownloading" @click="handleAssemblyPrimaryAction">
            {{ assemblyPrimaryActionText }}
          </el-button>
          <el-button v-if="canResetWorkspace" class="delivery-secondary-btn"
            :disabled="assemblyBusy || assemblyDownloading" @click="resetCurrentWorkspace">
            重置内容
          </el-button>
        </div>
        <div v-if="assemblyBusy || assemblyExecuteId" class="assembly-waiting"
          :class="{ 'assembly-waiting--done': !assemblyBusy && assemblyExecuteId }">
          <div class="assembly-waiting__spinner" aria-hidden="true"></div>
          <div class="assembly-waiting__content">
            <strong>{{ assemblyPolling ? '正在生成视频，请保持页面打开' : '工作流已提交' }}</strong>
            <span v-if="assemblyExecuteId">执行 ID：{{ assemblyExecuteId }}</span>
            <span v-if="assemblyPolling">已查询 {{ assemblyPollCount }} 次，系统会每 5 秒自动刷新结果。</span>
          </div>
        </div>
        <div class="video-result-panel">
          <div v-if="assemblyVideoUrl" class="video-result-panel__player">
            <video :src="assemblyVideoUrl" controls playsinline preload="metadata"></video>
          </div>
          <div v-else class="video-result-panel__empty">
            <span>{{ assemblyPolling ? '视频生成中，完成后会在这里显示结果。' : '提交生成后，这里会显示生成的视频结果。' }}</span>
          </div>
          <div v-if="assemblyVideoUrl" class="video-result-panel__link">
            <span>视频地址</span>
            <a :href="assemblyVideoUrl" target="_blank" rel="noreferrer">{{ assemblyVideoUrl }}</a>
          </div>
          <div v-if="assemblyDebugUrl" class="video-result-panel__link">
            <span>调试链接</span>
            <a :href="assemblyDebugUrl" target="_blank" rel="noreferrer">{{ assemblyDebugUrl }}</a>
          </div>
          <details v-if="assemblyResultText" class="video-result-panel__debug">
            <summary>查看原始返回</summary>
            <el-input v-model="assemblyResultText" type="textarea" :rows="8" resize="none" readonly />
          </details>
        </div>
      </AiSectionCard>
    </div>

    <el-dialog v-model="secretDialogVisible" title="视频密钥设置" width="560px" class="image-video-secret-dialog"
      append-to-body destroy-on-close>
      <div class="secret-status" :class="{ 'secret-status--valid': secretStatus?.valid }">
        <span>{{ secretStatusText }}</span>
        <span v-if="secretExpireText">{{ secretExpireText }}</span>
      </div>

      <el-form label-position="top" class="secret-form">
        <el-form-item label="爬虫密钥">
          <el-input v-model="secretForm.copyApiKey" type="password" show-password placeholder="不填写则保留当前有效值" />
          <div v-if="secretStatus?.copyApiKeyMasked" class="secret-form__hint">当前：{{ secretStatus.copyApiKeyMasked }}
          </div>
        </el-form-item>
        <el-form-item label="T8密钥（海外）">
          <el-input v-model="secretForm.t8Key" type="password" show-password placeholder="不填写则保留当前有效值" />
          <div v-if="secretStatus?.t8KeyMasked" class="secret-form__hint">当前：{{ secretStatus.t8KeyMasked }}</div>
        </el-form-item>
        <el-form-item label="T8密钥（国内）">
          <el-input v-model="secretForm.t8VideoKey" type="password" show-password placeholder="不填写则保留当前有效值" />
          <div v-if="secretStatus?.t8VideoKeyMasked" class="secret-form__hint">当前：{{ secretStatus.t8VideoKeyMasked }}
          </div>
        </el-form-item>
        <el-form-item label="音频密钥">
          <el-input v-model="secretForm.voiceApiKey" type="password" show-password placeholder="不填写则保留当前有效值" />
          <div v-if="secretStatus?.voiceApiKeyMasked" class="secret-form__hint">当前：{{ secretStatus.voiceApiKeyMasked }}
          </div>
        </el-form-item>
        <el-form-item label="音频团队ID">
          <el-input v-model="secretForm.voiceGroupId" type="password" show-password placeholder="不填写则保留当前有效值" />
          <div v-if="secretStatus?.voiceGroupIdMasked" class="secret-form__hint">当前：{{ secretStatus.voiceGroupIdMasked
            }}</div>
        </el-form-item>
        <el-form-item label="有效期">
          <el-select v-model="secretForm.expireDays" class="delivery-select">
            <el-option label="1 天" :value="1" />
            <el-option label="7 天" :value="7" />
            <el-option label="30 天" :value="30" />
            <el-option label="永久" :value="0" />
          </el-select>
        </el-form-item>
      </el-form>

      <template #footer>
        <el-button @click="secretDialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="secretSaving" @click="saveSecretSettings">保存</el-button>
      </template>
    </el-dialog>

    <el-dialog v-model="voiceDialogVisible" title="音色管理" width="920px" class="image-video-secret-dialog voice-dialog"
      append-to-body>
      <el-tabs v-model="voiceDialogTab" class="voice-dialog__tabs">
        <el-tab-pane label="音色列表" name="list">
          <div class="voice-dialog__toolbar voice-dialog__toolbar--list">
            <div class="voice-list-summary">
              <strong>{{ filteredVoiceListItems.length }}</strong>
              <span>/ {{ voiceListItems.length }} 个音色</span>
            </div>
            <el-input v-model="voiceSearchKeyword" class="voice-search-input" clearable placeholder="搜索音色名称或 voice_id"
              @keyup.enter="listVoices" @clear="listVoices" />
            <button type="button" class="script-primary-action-btn" :disabled="voiceLoading" @click="listVoices">
              刷新音色
            </button>
          </div>
          <div v-if="filteredVoiceListItems.length" class="voice-list">
            <div v-for="item in filteredVoiceListItems" :key="item.voiceId" class="voice-list__item">
              <div class="voice-list__meta">
                <strong>{{ item.name || item.voiceId }}</strong>
                <span>{{ item.voiceId }}</span>
                <em v-if="item.description">{{ item.description }}</em>
              </div>
              <span class="voice-list__source">{{ item.sourceLabel }}</span>
              <button type="button" class="script-action-btn" @click="useVoice(item)">使用此音色</button>
              <button v-if="item.sourceKey !== 'system_voice'" type="button"
                class="script-action-btn script-action-btn--danger" :disabled="voiceLoading"
                @click="deleteVoice(item)">删除音色</button>
            </div>
          </div>
          <el-empty v-else description="暂无可展示音色，请确认接口已返回音色数据" />
        </el-tab-pane>

        <el-tab-pane label="克隆音色" name="clone">
          <div class="delivery-field">
            <span class="delivery-field__label">音色名称</span>
            <el-input v-model="voiceCloneName" clearable placeholder="输入克隆后的音色名称" />
          </div>
          <AiAssetDropzone title="克隆素材" description="仅用于音色克隆，不会写入 text_info.file_url。" placeholder="上传音频或视频"
            helper="支持 MP3 / WAV / MP4，或粘贴素材链接" accept="audio/*,video/*" :file-name="voiceCloneAsset.fileName"
            :preview-url="voiceCloneAsset.previewUrl" :preview-kind="voiceCloneAsset.previewKind"
            :source-url="voiceCloneAsset.sourceUrl" :loading="voiceCloneAsset.uploading" allow-url compact
            action-label="上传克隆素材" selected-label="克隆素材" url-label="克隆素材链接" @select="setVoiceCloneAsset"
            @url-change="setVoiceCloneUrl" @clear="clearVoiceCloneAsset" />
          <div class="voice-dialog__toolbar">
            <button type="button" class="script-primary-action-btn" :disabled="voiceLoading" @click="cloneVoice">
              开始克隆
            </button>
          </div>
        </el-tab-pane>

      </el-tabs>
    </el-dialog>

    <el-dialog v-model="imagePreviewVisible" title="图片预览" width="760px" class="image-preview-dialog" append-to-body>
      <img v-if="imagePreviewUrl" :src="imagePreviewUrl" class="image-preview-dialog__image" alt="" />
    </el-dialog>
  </section>
</template>

<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'

import AiAssetDropzone from '@/shared/components/ai-workflow/AiAssetDropzone.vue'
import AiChoicePills from '@/shared/components/ai-workflow/AiChoicePills.vue'
import AiSectionCard from '@/shared/components/ai-workflow/AiSectionCard.vue'
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
import {
  cloneImageVideoVoice,
  deleteImageVideoVoice,
  getImageVideoAsyncTask,
  getImageVideoSecretStatus,
  listImageVideoVoices,
  runImageVideoDouyinCopy,
  runImageVideoWorkflow,
  saveImageVideoSecrets,
  synthesizeImageVideoVoice,
  uploadImageVideoMedia,
  type ImageVideoSecretStatusVo,
  type ImageVideoAsyncTaskVo,
  type ImageVideoWorkflowParameters,
} from '@/shared/api/java-modules'

type WorkspaceTab = 'remake' | 'imageToVideo'
type PreviewKind = 'image' | 'video' | 'file'
type MediaType = 'image' | 'video' | 'audio' | 'file'
type AssetKey = 'referenceAsset' | 'sceneAsset' | 'modelAsset' | 'speechAsset'
type VoiceMode = 'textOnly' | 'uploadAudio' | 'synthesize'
type VoiceStatusType = 'info' | 'success' | 'error'

const IMAGE_VIDEO_TASK_POLL_DELAY_MS = 5000
const IMAGE_VIDEO_TASK_MAX_POLLS = 720

type AssetState = {
  fileName: string
  previewUrl: string
  previewKind: PreviewKind
  sourceUrl: string
  mediaType: MediaType
  uploading: boolean
}

type AssemblyState = {
  loading: boolean
  resultText: string
  executeId: string
  polling: boolean
  pollCount: number
  videoUrl: string
  debugUrl: string
  downloading: boolean
  pollTimer: number
}

type WorkspaceState = {
  referenceAsset: AssetState
  productAssets: AssetState[]
  sceneAsset: AssetState
  modelAsset: AssetState
  speechAsset: AssetState
  ratio: string
  resolution: string
  model: string
  generationMode: 'draft' | 'standard'
  duration: string
  durationSeconds: number
  videoPrompt: string
  sceneMode: string
  scenePrompt: string
  faceInputType: string
  gender: string
  modelFigure: string
  language: string
  audioType: string
  voiceMode: VoiceMode
  voiceId: string
  voiceName: string
  scriptText: string
  scriptSourceUrl: string
  categoryName: string
  productName: string
  productFocus: string
  assembly: AssemblyState
}

type VoiceListItem = {
  voiceId: string
  name: string
  groupId: string
  description: string
  sourceKey: string
  sourceLabel: string
  raw: unknown
}

const activeTab = ref<WorkspaceTab>('remake')
const copyLearningLoading = ref(false)
const voiceLoading = ref(false)
const secretDialogVisible = ref(false)
const secretSaving = ref(false)
const secretStatus = ref<ImageVideoSecretStatusVo | null>(null)
const voiceDialogVisible = ref(false)
const voiceDialogTab = ref<'list' | 'clone'>('list')
const voiceStatusMessage = ref('')
const voiceStatusType = ref<VoiceStatusType>('info')
const voiceListItems = ref<VoiceListItem[]>([])
const voiceSearchKeyword = ref('')
const voiceCloneName = ref('')
const imagePreviewVisible = ref(false)
const imagePreviewUrl = ref('')
const currentAssembly = computed(() => currentWorkspace.value.assembly)
const assemblyResultText = computed({
  get: () => currentAssembly.value.resultText,
  set: (value: string) => {
    currentAssembly.value.resultText = value
  },
})
const assemblyExecuteId = computed(() => currentAssembly.value.executeId)
const assemblyPolling = computed(() => currentAssembly.value.polling)
const assemblyPollCount = computed(() => currentAssembly.value.pollCount)
const assemblyVideoUrl = computed(() => currentAssembly.value.videoUrl)
const assemblyDebugUrl = computed(() => currentAssembly.value.debugUrl)
const assemblyDownloading = computed(() => currentAssembly.value.downloading)
const assemblyLoading = computed(() => currentAssembly.value.loading)
const assemblyBusy = computed(() => currentAssembly.value.loading || currentAssembly.value.polling)
const canResetWorkspace = computed(() =>
  Boolean(
    currentAssembly.value.executeId
    || currentAssembly.value.resultText
    || currentAssembly.value.videoUrl
    || currentAssembly.value.debugUrl,
  ),
)
const assemblyPrimaryActionText = computed(() => {
  if (assemblyVideoUrl.value) return assemblyDownloading.value ? '下载中' : '下载视频'
  if (assemblyPolling.value) return '生成中，请等待'
  if (assemblyLoading.value) return '提交中'
  return '开始生成'
})

const secretForm = reactive({
  copyApiKey: '',
  t8Key: '',
  t8VideoKey: '',
  voiceApiKey: '',
  voiceGroupId: '',
  expireDays: 7,
})

const ratioOptions = ['9:16', '16:9', '3:4', '4:3', '1:1']
const resolutionOptions = ['480p', '720p', '1080p']
const modelOptions = [
  { label: 'seedance-2.0-mini', value: 'doubao-seedance-2.0-mini' },
  { label: 'seedance-2.0-fast', value: 'sdols-2.0-fast' },
  { label: 'seedance-2.0', value: 'sdols-2.0' },
]
const durationOptions = ['15', '30', '45', '60']
const sceneModeOptions = ['提示词微调', '上传场景图替换']
const faceInputTypeOptions = ['性别选项', '上传模特图', '人物类型']
const genderOptions = ['女', '男']
const languageOptions = ['中文', '英语', '韩语', '粤语']
const audioTypeOptions = [
  { label: '人物口播', value: '1' },
  { label: '背景旁白', value: '2' },
]
const voiceModeOptions = [
  { label: '视频原声', value: 'textOnly' },
  { label: '背景音乐', value: 'uploadAudio' },
  { label: '参考音色', value: 'synthesize' },
]
const voiceCloneAsset = reactive<AssetState>(createAssetState())

function createAssetState(): AssetState {
  return {
    fileName: '',
    previewUrl: '',
    previewKind: 'file',
    sourceUrl: '',
    mediaType: 'file',
    uploading: false,
  }
}

function createAssemblyState(): AssemblyState {
  return {
    loading: false,
    resultText: '',
    executeId: '',
    polling: false,
    pollCount: 0,
    videoUrl: '',
    debugUrl: '',
    downloading: false,
    pollTimer: 0,
  }
}

function createWorkspaceState(): WorkspaceState {
  return {
    referenceAsset: createAssetState(),
    productAssets: [createAssetState()],
    sceneAsset: createAssetState(),
    modelAsset: createAssetState(),
    speechAsset: createAssetState(),
    ratio: '9:16',
    resolution: '720p',
    model: 'sdols-2.0-fast',
    generationMode: 'draft',
    duration: '15',
    durationSeconds: 15,
    videoPrompt: '',
    sceneMode: '提示词微调',
    scenePrompt: '',
    faceInputType: '性别选项',
    gender: '女',
    modelFigure: '御姐',
    language: '中文',
    audioType: '1',
    voiceMode: 'textOnly',
    voiceId: '',
    voiceName: '',
    scriptText: '',
    scriptSourceUrl: '',
    categoryName: '',
    productName: '',
    productFocus: '',
    assembly: createAssemblyState(),
  }
}

const workspaces = reactive<Record<WorkspaceTab, WorkspaceState>>({
  remake: createWorkspaceState(),
  imageToVideo: createWorkspaceState(),
})

const currentWorkspace = computed(() => workspaces[activeTab.value])
const filteredVoiceListItems = computed(() => {
  const keyword = voiceSearchKeyword.value.trim().toLowerCase()
  if (!keyword) return voiceListItems.value
  return voiceListItems.value.filter((item) =>
    [item.voiceId, item.name, item.description, item.sourceLabel].some((text) => text.toLowerCase().includes(keyword)),
  )
})
const secretStatusText = computed(() => {
  if (!secretStatus.value?.configured) return '未配置完整密钥'
  if (secretStatus.value.valid) return '密钥已配置且有效'
  return '密钥已过期或不可用'
})
const secretExpireText = computed(() => {
  if (!secretStatus.value?.expiresAt) return ''
  if (secretStatus.value.expireDays === 0) return '有效期：永久'
  return `过期时间：${formatSecretTime(secretStatus.value.expiresAt)}`
})

function normalizeDuration(value: number | string) {
  const nextValue = Number(value)
  if (Number.isFinite(nextValue) && nextValue > 0) return Math.round(nextValue)
  return 15
}

function setDurationPreset(value: string | string[]) {
  const rawValue = Array.isArray(value) ? value[0] : value
  const nextValue = Number(rawValue)
  if (Number.isFinite(nextValue) && nextValue > 0) {
    currentWorkspace.value.duration = rawValue
    currentWorkspace.value.durationSeconds = nextValue
  }
}

function clearDurationPreset() {
  currentWorkspace.value.duration = ''
}

function handleCustomDurationChange(value: number | undefined) {
  clearDurationPreset()
  const nextValue = Number(value)
  if (Number.isFinite(nextValue) && nextValue > 0) {
    currentWorkspace.value.durationSeconds = Math.round(nextValue)
  }
}

function scheduleDeliveryGridLayout() {
  // CSS grid now owns layout. Keep this as a harmless compatibility hook for
  // existing upload/state handlers that used to request masonry reflow.
}

function resolvePreviewKindFromMediaType(mediaType: string): PreviewKind {
  if (mediaType === 'image') return 'image'
  if (mediaType === 'video') return 'video'
  return 'file'
}

function resolveMediaTypeFromUrl(url: string, accept = ''): MediaType {
  const cleanUrl = url.split('?')[0]?.toLowerCase() || ''
  if (/\.(png|jpe?g|webp|gif|bmp|svg)$/.test(cleanUrl)) return 'image'
  if (/\.(mp4|mov|webm|m4v|ogg|avi|mkv)$/.test(cleanUrl)) return 'video'
  if (/\.(mp3|wav|m4a|aac|flac)$/.test(cleanUrl)) return 'audio'
  return 'file'
}

function setAssetUrl(tab: WorkspaceTab, assetKey: AssetKey, url: string, accept = '') {
  const target = workspaces[tab][assetKey]
  const normalizedUrl = (url || '').trim()
  const mediaType = resolveMediaTypeFromUrl(normalizedUrl, accept)
  target.sourceUrl = normalizedUrl
  target.previewUrl = normalizedUrl
  target.fileName = normalizedUrl ? '在线素材' : ''
  target.mediaType = mediaType
  target.previewKind = resolvePreviewKindFromMediaType(mediaType)
  scheduleDeliveryGridLayout()
}

function setReferenceLink(tab: WorkspaceTab, url: string) {
  const target = workspaces[tab].referenceAsset
  const normalizedUrl = (url || '').trim()
  const mediaType = resolveMediaTypeFromUrl(normalizedUrl)
  target.sourceUrl = normalizedUrl
  target.mediaType = mediaType === 'video' ? 'video' : 'file'
  target.previewKind = mediaType === 'video' ? 'video' : 'file'
  target.previewUrl = mediaType === 'video' ? normalizedUrl : ''
  target.fileName = normalizedUrl ? (mediaType === 'video' ? '在线视频' : '在线链接') : ''
  scheduleDeliveryGridLayout()
}

async function setAsset(tab: WorkspaceTab, assetKey: AssetKey, file: File) {
  const target = workspaces[tab][assetKey]
  target.uploading = true
  try {
    const result = await uploadImageVideoMedia(file)
    target.fileName = result.originalFilename || file.name
    target.sourceUrl = result.url
    target.previewUrl = result.url
    target.mediaType = (result.mediaType || 'file') as MediaType
    target.previewKind = resolvePreviewKindFromMediaType(target.mediaType)
    ElMessage.success('素材上传成功')
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '素材上传失败')
  } finally {
    target.uploading = false
    scheduleDeliveryGridLayout()
  }
}

function clearAsset(tab: WorkspaceTab, assetKey: AssetKey) {
  Object.assign(workspaces[tab][assetKey], createAssetState())
  scheduleDeliveryGridLayout()
}

function addProductAsset(tab: WorkspaceTab) {
  const productAssets = workspaces[tab].productAssets
  if (productAssets.length < 5) productAssets.push(createAssetState())
  scheduleDeliveryGridLayout()
}

function setProductAssetUrl(tab: WorkspaceTab, index: number, url: string) {
  const target = workspaces[tab].productAssets[index]
  if (!target) return
  const normalizedUrl = (url || '').trim()
  target.sourceUrl = normalizedUrl
  target.previewUrl = normalizedUrl
  target.fileName = normalizedUrl ? '在线素材' : ''
  target.mediaType = resolveMediaTypeFromUrl(normalizedUrl, 'image/*')
  target.previewKind = resolvePreviewKindFromMediaType(target.mediaType)
  scheduleDeliveryGridLayout()
}

async function setProductAsset(tab: WorkspaceTab, index: number, file: File) {
  const target = workspaces[tab].productAssets[index]
  if (!target) return
  target.uploading = true
  try {
    const result = await uploadImageVideoMedia(file)
    target.fileName = result.originalFilename || file.name
    target.sourceUrl = result.url
    target.previewUrl = result.url
    target.mediaType = (result.mediaType || 'file') as MediaType
    target.previewKind = resolvePreviewKindFromMediaType(target.mediaType)
    ElMessage.success('产品图上传成功')
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '产品图上传失败')
  } finally {
    target.uploading = false
    scheduleDeliveryGridLayout()
  }
}

function clearProductAsset(tab: WorkspaceTab, index: number) {
  const productAssets = workspaces[tab].productAssets
  if (productAssets.length === 1) {
    Object.assign(productAssets[0], createAssetState())
  } else {
    productAssets.splice(index, 1)
  }
  scheduleDeliveryGridLayout()
}

function openImagePreview(url: string) {
  const normalizedUrl = (url || '').trim()
  if (!normalizedUrl) return
  imagePreviewUrl.value = normalizedUrl
  imagePreviewVisible.value = true
}

function resetCurrentWorkspace() {
  const tab = activeTab.value
  stopAssemblyPolling(tab)
  workspaces[tab] = createWorkspaceState()
  voiceStatusMessage.value = ''
  voiceStatusType.value = 'info'
  scheduleDeliveryGridLayout()
  ElMessage.success('已重置当前模块内容')
}

async function pasteReferenceLink() {
  try {
    const text = await navigator.clipboard.readText()
    setReferenceLink(activeTab.value, text)
    if (activeTab.value === 'remake') currentWorkspace.value.scriptSourceUrl = text.trim()
  } catch {
    ElMessage.warning('无法读取剪贴板，请手动粘贴')
  }
}

async function pasteScriptSourceLink() {
  try {
    currentWorkspace.value.scriptSourceUrl = (await navigator.clipboard.readText()).trim()
    scheduleDeliveryGridLayout()
  } catch {
    ElMessage.warning('无法读取剪贴板，请手动粘贴')
  }
}

async function refreshSecretStatus(showMessage = false) {
  try {
    secretStatus.value = await getImageVideoSecretStatus()
    if (showMessage) ElMessage.success('密钥状态已刷新')
  } catch (error) {
    if (showMessage) ElMessage.error(error instanceof Error ? error.message : '密钥状态查询失败')
  }
}

function openSecretDialog() {
  resetSecretForm()
  secretDialogVisible.value = true
  void refreshSecretStatus(false)
}

function resetSecretForm() {
  secretForm.copyApiKey = ''
  secretForm.t8Key = ''
  secretForm.t8VideoKey = ''
  secretForm.voiceApiKey = ''
  secretForm.voiceGroupId = ''
  secretForm.expireDays = secretStatus.value?.expireDays ?? 7
}

async function saveSecretSettings() {
  secretSaving.value = true
  try {
    secretStatus.value = await saveImageVideoSecrets({
      copyApiKey: secretForm.copyApiKey.trim(),
      t8Key: secretForm.t8Key.trim(),
      t8VideoKey: secretForm.t8VideoKey.trim(),
      voiceApiKey: secretForm.voiceApiKey.trim(),
      voiceGroupId: secretForm.voiceGroupId.trim(),
      expireDays: secretForm.expireDays,
    })
    ElMessage.success('密钥已保存')
    secretDialogVisible.value = false
    resetSecretForm()
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '密钥保存失败')
  } finally {
    secretSaving.value = false
  }
}

function formatSecretTime(value: string) {
  if (!value) return ''
  return value.replace('T', ' ').slice(0, 19)
}

function sleep(milliseconds: number) {
  return new Promise<void>((resolve) => window.setTimeout(resolve, milliseconds))
}

function normalizeTaskStatus(value: string | undefined) {
  return (value || '').trim().toUpperCase()
}

function isTerminalCozeStatus(value: string | undefined) {
  return ['SUCCESS', 'SUCCEEDED', 'COMPLETED', 'DONE', 'FINISHED', 'FAILED', 'FAIL', 'ERROR', 'CANCELED', 'CANCELLED']
    .includes(normalizeTaskStatus(value))
}

function isRecoverableFailedImageVideoTask(task: ImageVideoAsyncTaskVo) {
  return task.status === 'FAILED'
    && Boolean(task.cozeExecuteId)
    && !isTerminalCozeStatus(task.cozeStatus)
}

function isTerminalImageVideoTask(task: ImageVideoAsyncTaskVo) {
  return task.status === 'SUCCESS' || (task.status === 'FAILED' && !isRecoverableFailedImageVideoTask(task))
}

async function waitForImageVideoTask(ticket: ImageVideoAsyncTaskVo): Promise<unknown> {
  let task = ticket
  for (let pollCount = 0; pollCount < IMAGE_VIDEO_TASK_MAX_POLLS; pollCount += 1) {
    if (task.status === 'SUCCESS') return task.result
    if (isTerminalImageVideoTask(task)) {
      throw new Error(task.errorMessage || 'Coze task failed')
    }
    await sleep(IMAGE_VIDEO_TASK_POLL_DELAY_MS)
    task = await getImageVideoAsyncTask(task.taskId)
  }
  throw new Error('Coze task polling timed out')
}

async function rewriteScriptFromSource() {
  const sourceUrl = currentWorkspace.value.scriptSourceUrl.trim()
  const referenceUrl = currentWorkspace.value.referenceAsset.sourceUrl.trim()
  const url = activeTab.value === 'remake' ? referenceUrl || sourceUrl : sourceUrl
  if (!url) {
    ElMessage.warning('请先粘贴文案来源链接')
    return
  }
  if (looksLikeJsonObject(url)) {
    ElMessage.warning('请粘贴视频链接或文案来源链接，不要粘贴接口返回内容')
    return
  }
  const productName = currentWorkspace.value.productName.trim()
  if (!productName) {
    ElMessage.warning('请输入产品名称')
    return
  }
  if (activeTab.value === 'remake') currentWorkspace.value.scriptSourceUrl = url
  copyLearningLoading.value = true
  try {
    const ticket = await runImageVideoDouyinCopy({
      url,
      duration: Math.max(0, Math.round(Number(currentWorkspace.value.durationSeconds) || 0)),
      proc_info: {
        type: currentWorkspace.value.categoryName.trim(),
        name: productName,
        proc_image: resolveProductImageUrls(currentWorkspace.value),
        properties: currentWorkspace.value.productFocus.trim(),
      },
    })
    const result = await waitForImageVideoTask(ticket)
    const text = cleanCopyText(findTextByKeys(result, ['scriptDraft', 'script_draft', 'recognizedContent', 'recognized_content']))
    currentWorkspace.value.scriptText = text
    ElMessage.success('文案已提取')
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '文案提取失败')
  } finally {
    copyLearningLoading.value = false
  }
}

function resolveAssetUrl(asset: AssetState) {
  return asset.sourceUrl.trim()
}

function looksLikeJsonObject(value: string) {
  const text = value.trim()
  return text.startsWith('{') && text.endsWith('}')
}

function cleanCopyText(value: string) {
  return value
    .replace(/^\s*https?:\/\/\S+\s*$/gm, '')
    .replace(/https?:\/\/\S+/g, '')
    .replace(/^[ \t]+|[ \t]+$/gm, '')
    .replace(/\n{3,}/g, '\n\n')
    .trim()
}

function resolveProductImageUrls(workspace: WorkspaceState) {
  return workspace.productAssets.map(resolveAssetUrl).filter(Boolean).slice(0, 5)
}

function resolveVoiceAudioUrl(workspace: WorkspaceState) {
  if (workspace.voiceMode !== 'synthesize') return ''
  const url = resolveAssetUrl(workspace.speechAsset)
  return isCozeWorkflowDebugUrl(url) ? '' : url
}

function resolveBgmUrl(workspace: WorkspaceState) {
  if (workspace.voiceMode !== 'uploadAudio') return ''
  const url = resolveAssetUrl(workspace.speechAsset)
  return isCozeWorkflowDebugUrl(url) ? '' : url
}

function resolveAudioMode(workspace: WorkspaceState) {
  if (workspace.voiceMode === 'uploadAudio') return 2
  return 1
}

function resolveAudioInfo(workspace: WorkspaceState): ImageVideoWorkflowParameters['audio_info'] {
  const mode = resolveAudioMode(workspace)
  const audioUrl = mode === 1 ? resolveVoiceAudioUrl(workspace) : ''
  const bgmUrl = mode === 2 ? resolveBgmUrl(workspace) : ''
  return {
    audio_url: audioUrl,
    type: Number(workspace.audioType) || 1,
    bgm_url: bgmUrl,
    mode,
    voice_name: mode === 1 ? workspace.voiceName.trim() : '',
  }
}

function resolveReferenceVideoInfo(workspace: WorkspaceState) {
  if (activeTab.value !== 'remake') {
    return { videoUrl: '', shareUrl: '', refVideoMode: '1' }
  }
  const referenceUrl = resolveAssetUrl(workspace.referenceAsset)
  if (!referenceUrl) {
    return { videoUrl: '', shareUrl: '', refVideoMode: '1' }
  }
  const mediaType = workspace.referenceAsset.mediaType
  const refVideoMode = mediaType === 'video' ? '1' : '2'
  return {
    videoUrl: refVideoMode === '1' ? referenceUrl : '',
    shareUrl: refVideoMode === '2' ? referenceUrl : '',
    refVideoMode,
  }
}

function resolveFaceInfo(workspace: WorkspaceState): ImageVideoWorkflowParameters['face_info'] {
  if (workspace.faceInputType === '上传模特图') {
    const modelImageUrl = resolveAssetUrl(workspace.modelAsset)
    return { type: 2, model_figure: '', model_image: modelImageUrl ? [modelImageUrl] : [] }
  }
  if (workspace.faceInputType === '人物类型') {
    return { type: 3, model_figure: workspace.modelFigure, model_image: [] }
  }
  return { type: 1, model_figure: workspace.gender, model_image: [] }
}

function buildImageVideoWorkflowParameters(): ImageVideoWorkflowParameters | null {
  const workspace = currentWorkspace.value
  const referenceVideoInfo = resolveReferenceVideoInfo(workspace)
  const productImageUrls = resolveProductImageUrls(workspace)
  return {
    api_key_info: {
      t8star_key: '',
      t8_video_key: '',
      ai_conductor_key: '',
    },
    bg_info: {
      type: workspace.sceneMode === '上传场景图替换' ? 2 : 1,
      prompt: workspace.scenePrompt.trim(),
      bg_image: resolveAssetUrl(workspace.sceneAsset),
    },
    face_info: resolveFaceInfo(workspace),
    proc_info: {
      type: workspace.categoryName.trim(),
      name: workspace.productName.trim(),
      proc_image: productImageUrls,
      properties: workspace.productFocus.trim(),
    },
    text_info: {
      type: 1,
      language: workspace.language,
      text: workspace.scriptText.trim(),
      file_url: resolveVoiceAudioUrl(workspace),
    },
    audio_info: resolveAudioInfo(workspace),
    video_info: {
      video_url: referenceVideoInfo.videoUrl,
      share_url: referenceVideoInfo.shareUrl,
      ref_video_mode: referenceVideoInfo.refVideoMode,
      mode: activeTab.value === 'imageToVideo' ? '1' : '2',
      draft: workspace.generationMode === 'draft',
      model: workspace.model,
      prompt: workspace.videoPrompt.trim(),
      ratio: workspace.ratio,
      resolution: workspace.resolution,
      duration: normalizeDuration(workspace.durationSeconds),
    },
  }
}

async function startAssembly() {
  if (assemblyLoading.value) return
  const tab = activeTab.value
  const assembly = workspaces[tab].assembly
  const parameters = buildImageVideoWorkflowParameters()
  if (!parameters) return
  stopAssemblyPolling(tab)
  assembly.loading = true
  assembly.resultText = ''
  assembly.executeId = ''
  assembly.videoUrl = ''
  assembly.debugUrl = ''
  try {
    const ticket = await runImageVideoWorkflow(parameters)
    assembly.resultText = JSON.stringify(ticket, null, 2)
    startAssemblyPolling(tab, ticket.taskId)
    ElMessage.success('工作流已提交')
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '工作流提交失败')
  } finally {
    assembly.loading = false
  }
}

function handleAssemblyPrimaryAction() {
  if (assemblyVideoUrl.value) {
    void downloadAssemblyVideo()
    return
  }
  void startAssembly()
}

function buildAssemblyVideoFilename() {
  const modeName = activeTab.value === 'remake' ? '视频复刻' : '图生视频'
  const productName = currentWorkspace.value.productName.trim() || '带货视频'
  const executeId = assemblyExecuteId.value.trim()
  const extensionMatch = assemblyVideoUrl.value.split('?')[0]?.match(/\.(mp4|mov|webm|m4v)$/i)
  const extension = extensionMatch?.[1]?.toLowerCase() || 'mp4'
  const baseName = [modeName, productName, executeId].filter(Boolean).join('-')
  return `${baseName.replace(/[\\/:*?"<>|\s]+/g, '-')}.${extension}`
}

async function downloadAssemblyVideo() {
  const url = assemblyVideoUrl.value.trim()
  if (!url) {
    ElMessage.warning('当前没有可下载的视频')
    return
  }
  const assembly = currentAssembly.value
  assembly.downloading = true
  try {
    const filename = buildAssemblyVideoFilename()
    const downloadId = `image-video:${assemblyExecuteId.value || Date.now()}`
    const result = await saveUrlWithProgress(url, filename, downloadId)
    if (result.success) {
      ElMessage.success(`已保存: ${result.path || filename}`)
    } else if (result.error && result.error !== '用户取消') {
      ElMessage.error(result.error)
    }
  } finally {
    assembly.downloading = false
  }
}

function startAssemblyPolling(tab: WorkspaceTab, taskId: number) {
  stopAssemblyPolling(tab)
  const assembly = workspaces[tab].assembly
  assembly.polling = true
  assembly.pollCount = 0
  void pollAssemblyResult(tab, taskId)
}

function stopAssemblyPolling(tab: WorkspaceTab) {
  const assembly = workspaces[tab].assembly
  if (assembly.pollTimer) {
    window.clearTimeout(assembly.pollTimer)
    assembly.pollTimer = 0
  }
  assembly.polling = false
  assembly.pollCount = 0
}

async function pollAssemblyResult(tab: WorkspaceTab, taskId: number) {
  if (!taskId) return
  const assembly = workspaces[tab].assembly
  assembly.pollCount += 1
  try {
    const task = await getImageVideoAsyncTask(taskId)
    const result = task.result
    const status = task.cozeStatus || task.status
    const videoUrl = findVideoResultUrl(result)
    const debugUrl = findTextByKeys(result, ['debug_url', 'debugUrl'])
    if (task.cozeExecuteId) assembly.executeId = task.cozeExecuteId
    if (videoUrl) assembly.videoUrl = videoUrl
    if (debugUrl) assembly.debugUrl = debugUrl
    assembly.resultText = JSON.stringify({
      task_id: taskId,
      execute_id: task.cozeExecuteId || assembly.executeId,
      polling: !isTerminalImageVideoTask(task),
      poll_count: assembly.pollCount,
      status,
      video_url: videoUrl || assembly.videoUrl,
      result,
    }, null, 2)
    if (isTerminalImageVideoTask(task)) {
      assembly.polling = false
      if (task.status === 'FAILED') {
        ElMessage.error('Coze 工作流执行失败')
      } else {
        ElMessage.success(videoUrl || assembly.videoUrl ? '视频生成完成' : 'Coze 工作流执行完成，未解析到视频地址')
      }
      return
    }
  } catch (error) {
    assembly.resultText = JSON.stringify({
      task_id: taskId,
      polling: false,
      error: error instanceof Error ? error.message : '查询 Coze 执行结果失败',
    }, null, 2)
    assembly.polling = false
    ElMessage.error(error instanceof Error ? error.message : '查询 Coze 执行结果失败')
    return
  }

  if (assembly.pollCount >= 720) {
    assembly.polling = false
    ElMessage.warning('Coze 执行结果查询超时，请稍后通过 execute_id 查看')
    return
  }
  assembly.pollTimer = window.setTimeout(() => {
    void pollAssemblyResult(tab, taskId)
  }, IMAGE_VIDEO_TASK_POLL_DELAY_MS)
}

function setVoiceStatus(message: string, type: VoiceStatusType = 'info') {
  voiceStatusMessage.value = message
  voiceStatusType.value = type
  scheduleDeliveryGridLayout()
}

function openVoiceDialog() {
  voiceDialogVisible.value = true
  if (!voiceListItems.value.length) {
    void listVoices()
  }
}

function parseJsonString(value: string): unknown {
  const text = value.trim()
  if (!text) return null
  const candidates = [text]
  if (text.startsWith('"') && text.endsWith('"')) {
    candidates.push(text.slice(1, -1))
  }
  for (const candidate of candidates) {
    const nextText = candidate.trim()
    if (!['{', '['].includes(nextText[0])) continue
    try {
      return JSON.parse(nextText)
    } catch {
      try {
        return JSON.parse(nextText.replace(/\\"/g, '"').replace(/\\\\/g, '\\'))
      } catch {
        // Continue trying other candidates.
      }
    }
  }
  return null
}

function expandJsonStrings(value: unknown, depth = 0): unknown {
  if (depth > 5 || value == null) return value
  if (typeof value === 'string') {
    const parsed = parseJsonString(value)
    return parsed == null ? value : expandJsonStrings(parsed, depth + 1)
  }
  if (Array.isArray(value)) {
    return value.map((item) => expandJsonStrings(item, depth + 1))
  }
  if (typeof value === 'object') {
    const record = value as Record<string, unknown>
    return Object.fromEntries(Object.entries(record).map(([key, item]) => [key, expandJsonStrings(item, depth + 1)]))
  }
  return value
}

function collectVoiceArrays(value: unknown): unknown[] {
  const arrays: unknown[] = []
  const expanded = expandJsonStrings(value)

  function collect(node: unknown) {
    if (!node || typeof node !== 'object') return
    if (Array.isArray(node)) {
      node.forEach(collect)
      return
    }
    const record = node as Record<string, unknown>
      ;['system_voice', 'voice_cloning', 'voice_generation', 'music_generation'].forEach((key) => {
        const item = record[key]
        if (Array.isArray(item)) arrays.push(...item)
      })
    Object.values(record).forEach(collect)
  }

  collect(expanded)
  return arrays.length ? arrays : Array.isArray(expanded) ? expanded : [expanded]
}

function getRecordValue(record: Record<string, unknown>, path: string[]) {
  let current: unknown = record
  for (const key of path) {
    if (!current || typeof current !== 'object' || Array.isArray(current)) return undefined
    current = (current as Record<string, unknown>)[key]
  }
  return current
}

function findUrl(value: unknown): string {
  if (!value) return ''
  if (typeof value === 'string') {
    const match = value.match(/https?:\/\/[^\s"'<>]+/)
    if (match?.[0]) return match[0]
    const parsed = parseJsonString(value)
    return parsed ? findUrl(parsed) : ''
  }
  if (Array.isArray(value)) {
    for (const item of value) {
      const found = findUrl(item)
      if (found) return found
    }
  }
  if (typeof value === 'object') {
    for (const item of Object.values(value as Record<string, unknown>)) {
      const found = findUrl(item)
      if (found) return found
    }
  }
  return ''
}

function findUrlByKeys(value: unknown, keys: string[]): string {
  if (!value) return ''
  const normalizedKeys = keys.map((key) => key.toLowerCase())
  if (typeof value === 'string') {
    const parsed = parseJsonString(value)
    return parsed ? findUrlByKeys(parsed, keys) : ''
  }
  if (Array.isArray(value)) {
    for (const item of value) {
      const found = findUrlByKeys(item, keys)
      if (found) return found
    }
    return ''
  }
  if (typeof value === 'object') {
    const record = value as Record<string, unknown>
    for (const [key, rawValue] of Object.entries(record)) {
      if (!normalizedKeys.includes(key.toLowerCase())) continue
      const url = typeof rawValue === 'string'
        ? findUrl(rawValue)
        : findUrlMatching(rawValue, (item) => !isCozeWorkflowDebugUrl(item))
      if (url && !isCozeWorkflowDebugUrl(url)) return url
    }
    for (const rawValue of Object.values(record)) {
      const found = findUrlByKeys(rawValue, keys)
      if (found) return found
    }
  }
  return ''
}

function findUrlMatching(value: unknown, predicate: (url: string) => boolean): string {
  if (!value) return ''
  if (typeof value === 'string') {
    const matches = value.match(/https?:\/\/[^\s"'<>]+/g) || []
    const found = matches.find(predicate)
    if (found) return found
    const parsed = parseJsonString(value)
    return parsed ? findUrlMatching(parsed, predicate) : ''
  }
  if (Array.isArray(value)) {
    for (const item of value) {
      const found = findUrlMatching(item, predicate)
      if (found) return found
    }
  }
  if (typeof value === 'object') {
    for (const item of Object.values(value as Record<string, unknown>)) {
      const found = findUrlMatching(item, predicate)
      if (found) return found
    }
  }
  return ''
}

function isCozeWorkflowDebugUrl(url: string) {
  const text = (url || '').trim().toLowerCase()
  return text.includes('coze.cn/work_flow') || (text.includes('execute_id=') && text.includes('workflow_id='))
}

function isMediaFileUrl(url: string) {
  const text = (url || '').trim()
  if (!/^https?:\/\//i.test(text) || isCozeWorkflowDebugUrl(text)) return false
  return /\.(mp3|wav|m4a|aac|flac|ogg|mp4|mov|webm|m4v)(\?|#|$)/i.test(text)
}

function isVideoFileUrl(url: string) {
  const text = (url || '').trim()
  if (!/^https?:\/\//i.test(text) || isCozeWorkflowDebugUrl(text)) return false
  return /\.(mp4|mov|webm|m4v)(\?|#|$)/i.test(text)
}

function findVideoResultUrl(value: unknown) {
  const expanded = expandJsonStrings(value)
  return findUrlByKeys(expanded, [
    'video_url',
    'videoUrl',
    'video',
    'output_video_url',
    'outputVideoUrl',
    'result_video_url',
    'resultVideoUrl',
  ]) || findUrlMatching(expanded, isVideoFileUrl)
}

function findMediaFileUrl(value: unknown) {
  return findUrlByKeys(value, [
    'audio_url',
    'audioUrl',
    'file_url',
    'fileUrl',
    'media_url',
    'mediaUrl',
    'output_url',
    'outputUrl',
    'url',
  ]) || findUrlMatching(value, isMediaFileUrl)
}

function resolveWorkflowStatus(value: unknown) {
  return (findTextByKeys(value, [
    'execute_status',
    'executeStatus',
    'workflow_status',
    'workflowStatus',
    'status',
  ]) || '').toUpperCase()
}

function isTerminalWorkflowStatus(status: string) {
  const normalized = (status || '').toUpperCase()
  return ['SUCCESS', 'SUCCEEDED', 'COMPLETED', 'DONE', 'FINISHED', 'FAILED', 'FAIL', 'ERROR', 'CANCELED', 'CANCELLED'].includes(normalized)
}

function isFailedWorkflowStatus(status: string) {
  const normalized = (status || '').toUpperCase()
  return ['FAILED', 'FAIL', 'ERROR', 'CANCELED', 'CANCELLED'].includes(normalized)
}

function findTextByKeys(value: unknown, keys: string[]): string {
  if (!value) return ''
  const normalizedKeys = keys.map((key) => key.toLowerCase())
  if (typeof value === 'string') {
    const parsed = parseJsonString(value)
    return parsed ? findTextByKeys(parsed, keys) : ''
  }
  if (typeof value === 'object') {
    if (Array.isArray(value)) {
      for (const item of value) {
        const found = findTextByKeys(item, keys)
        if (found) return found
      }
      return ''
    }
    const record = value as Record<string, unknown>
    for (const [key, rawValue] of Object.entries(record)) {
      if (normalizedKeys.includes(key.toLowerCase()) && rawValue != null) {
        const text = String(rawValue).trim()
        if (text) return text
      }
    }
    for (const rawValue of Object.values(record)) {
      const found = findTextByKeys(rawValue, keys)
      if (found) return found
    }
  }
  return ''
}

function extractVoiceItems(value: unknown): VoiceListItem[] {
  const items: VoiceListItem[] = []
  const seen = new Set<string>()
  const expanded = expandJsonStrings(value)
  const sourceConfigs = [
    { key: 'system_voice', label: '系统音色' },
    { key: 'voice_cloning', label: '克隆音色' },
    { key: 'voice_generation', label: '生成音色' },
    { key: 'music_generation', label: '音乐生成' },
  ]

  function addVoice(node: unknown, sourceKey = 'unknown', sourceLabel = '音色') {
    if (!node) return
    if (typeof node === 'string') {
      const parsed = parseJsonString(node)
      if (parsed) addVoice(parsed, sourceKey, sourceLabel)
      return
    }
    if (typeof node !== 'object') return
    if (Array.isArray(node)) {
      node.forEach((item) => addVoice(item, sourceKey, sourceLabel))
      return
    }
    const record = node as Record<string, unknown>
    const voiceId = firstString(record.voice_id, record.voiceId, record.voiceID)
    if (voiceId && !seen.has(voiceId)) {
      seen.add(voiceId)
      items.push({
        voiceId,
        name: firstString(record.name, record.voice_name, record.voiceName, record.title, record.nickname),
        groupId: firstString(record.group_id, record.groupId),
        description: Array.isArray(record.description)
          ? record.description.map((item) => String(item).trim()).filter(Boolean).join('；')
          : firstString(record.description),
        sourceKey,
        sourceLabel,
        raw: node,
      })
    }
  }

  function collectFromKnownArrays(node: unknown) {
    if (!node || typeof node !== 'object') return
    if (Array.isArray(node)) {
      node.forEach(collectFromKnownArrays)
      return
    }
    const record = node as Record<string, unknown>
    sourceConfigs.forEach((config) => {
      const direct = record[config.key]
      const nested = getRecordValue(record, ['data', config.key])
        ;[direct, nested].forEach((source) => {
          if (Array.isArray(source)) source.forEach((item) => addVoice(item, config.key, config.label))
        })
    })
    Object.values(record).forEach(collectFromKnownArrays)
  }

  collectFromKnownArrays(expanded)
  if (!items.length) {
    collectVoiceArrays(expanded).forEach((item) => addVoice(item))
  }
  return items
}

function firstString(...values: unknown[]) {
  for (const value of values) {
    if (value == null) continue
    const text = String(value).trim()
    if (text) return text
  }
  return ''
}

async function listVoices() {
  voiceLoading.value = true
  try {
    const ticket = await listImageVideoVoices(voiceSearchKeyword.value.trim())
    const result = await waitForImageVideoTask(ticket)
    voiceListItems.value = extractVoiceItems(result)
    setVoiceStatus(
      voiceListItems.value.length ? `已获取 ${voiceListItems.value.length} 个音色` : '音色接口已返回，未解析到列表项',
      voiceListItems.value.length ? 'success' : 'info',
    )
  } catch (error) {
    const message = error instanceof Error ? error.message : '查看音色失败'
    setVoiceStatus(message, 'error')
    ElMessage.error(message)
  } finally {
    voiceLoading.value = false
  }
}

async function deleteVoice(item: VoiceListItem) {
  if (item.sourceKey === 'system_voice') {
    ElMessage.warning('系统音色不支持删除')
    return
  }
  const voiceId = item.voiceId
  try {
    await ElMessageBox.confirm(`确定删除音色 ${voiceId} 吗？`, '删除音色', {
      confirmButtonText: '删除',
      cancelButtonText: '取消',
      type: 'warning',
    })
  } catch {
    return
  }
  voiceLoading.value = true
  try {
    const ticket = await deleteImageVideoVoice(voiceId)
    await waitForImageVideoTask(ticket)
    voiceListItems.value = voiceListItems.value.filter((item) => item.voiceId !== voiceId)
    setVoiceStatus(`删除音色请求已提交：${voiceId}`, 'success')
    ElMessage.success('删除音色请求已提交')
  } catch (error) {
    const message = error instanceof Error ? error.message : '删除音色失败'
    setVoiceStatus(message, 'error')
    ElMessage.error(message)
  } finally {
    voiceLoading.value = false
  }
}

async function cloneVoice() {
  const name = voiceCloneName.value.trim()
  if (!name) {
    ElMessage.warning('请输入音色名称')
    return
  }
  const sourceUrl = resolveAssetUrl(voiceCloneAsset)
  if (!sourceUrl) {
    ElMessage.warning('请先上传或填写音频/视频链接')
    return
  }
  voiceLoading.value = true
  try {
    const clonePayload = {
      audioUrl: sourceUrl,
      videoUrl: voiceCloneAsset.mediaType === 'video' ? sourceUrl : '',
    }
    const ticket = await cloneImageVideoVoice({
      name,
      audioUrl: clonePayload.audioUrl,
      videoUrl: clonePayload.videoUrl,
    })
    const result = await waitForImageVideoTask(ticket)
    const voiceId = findTextByKeys(result, ['voice_id', 'voiceId', 'voiceID'])
    if (voiceId) {
      currentWorkspace.value.voiceId = voiceId
      currentWorkspace.value.voiceName = name
      setVoiceStatus(`音色克隆完成，已回填 voice_id：${voiceId}`, 'success')
      ElMessage.success('音色克隆完成，已回填 voice_id')
    } else {
      setVoiceStatus('音色克隆请求已提交，返回中未解析到 voice_id', 'success')
      ElMessage.success('音色克隆请求已提交')
    }
  } catch (error) {
    const message = error instanceof Error ? error.message : '音色克隆失败'
    setVoiceStatus(message, 'error')
    ElMessage.error(message)
  } finally {
    voiceLoading.value = false
  }
}

async function synthesizeVoice() {
  const workspace = currentWorkspace.value
  const text = workspace.scriptText.trim()
  const voiceId = workspace.voiceId.trim()
  if (!text) {
    ElMessage.warning('请先输入口播台词')
    return
  }
  if (!voiceId) {
    ElMessage.warning('请先输入 voice_id')
    return
  }
  voiceLoading.value = true
  try {
    const ticket = await synthesizeImageVideoVoice({ text, voiceId })
    const result = await waitForImageVideoTask(ticket)
    const audioUrl = findMediaFileUrl(result)
    if (audioUrl) {
      setAssetUrl(activeTab.value, 'speechAsset', audioUrl, 'audio/*')
      setVoiceStatus('语音合成完成，已写入最终配音文件', 'success')
      ElMessage.success('语音合成完成，已写入口播音频链接')
    } else {
      setVoiceStatus('语音合成请求已提交，未在返回中解析到音频 URL', 'info')
      ElMessage.success('语音合成请求已提交')
    }
  } catch (error) {
    const message = error instanceof Error ? error.message : '语音合成失败'
    setVoiceStatus(message, 'error')
    ElMessage.error(message)
  } finally {
    voiceLoading.value = false
  }
}

function useVoice(item: VoiceListItem) {
  currentWorkspace.value.voiceId = item.voiceId
  currentWorkspace.value.voiceName = item.name || item.voiceId
  currentWorkspace.value.voiceMode = 'synthesize'
  setVoiceStatus(`已选择音色：${currentWorkspace.value.voiceName}（${item.voiceId}）`, 'success')
  voiceDialogVisible.value = false
}

async function setVoiceCloneAsset(file: File) {
  voiceCloneAsset.uploading = true
  try {
    const result = await uploadImageVideoMedia(file)
    voiceCloneAsset.fileName = result.originalFilename || file.name
    voiceCloneAsset.sourceUrl = result.url
    voiceCloneAsset.previewUrl = result.url
    voiceCloneAsset.mediaType = (result.mediaType || 'file') as MediaType
    voiceCloneAsset.previewKind = resolvePreviewKindFromMediaType(voiceCloneAsset.mediaType)
    ElMessage.success('克隆素材上传成功')
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '克隆素材上传失败')
  } finally {
    voiceCloneAsset.uploading = false
  }
}

function setVoiceCloneUrl(url: string) {
  const normalizedUrl = (url || '').trim()
  const mediaType = resolveMediaTypeFromUrl(normalizedUrl, 'audio/*,video/*')
  voiceCloneAsset.sourceUrl = normalizedUrl
  voiceCloneAsset.previewUrl = normalizedUrl
  voiceCloneAsset.fileName = normalizedUrl ? '在线克隆素材' : ''
  voiceCloneAsset.mediaType = mediaType
  voiceCloneAsset.previewKind = resolvePreviewKindFromMediaType(mediaType)
}

function clearVoiceCloneAsset() {
  Object.assign(voiceCloneAsset, createAssetState())
}

onMounted(() => {
  void refreshSecretStatus(false)
})

onBeforeUnmount(() => {
  stopAssemblyPolling('remake')
  stopAssemblyPolling('imageToVideo')
})
</script>

<style scoped>
.delivery-workspace {
  --delivery-bg: #101010;
  --delivery-surface: #181818;
  --delivery-surface-2: #202020;
  --delivery-border: #303030;
  --delivery-text: #f3f3f3;
  --delivery-muted: #9b9b9b;
  --delivery-gold: #b58c51;
  --delivery-result-width: clamp(340px, 24vw, 420px);
  min-height: 100%;
  padding: 18px 20px 32px;
  background: var(--delivery-bg);
  color: var(--delivery-text);
}

.delivery-workspace__tabs {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 18px;
  border-bottom: 1px solid #2f2f2f;
}

.delivery-workspace__tab-panel {
  flex: 1;
  min-width: 0;
}

.delivery-workspace__tab-panel :deep(.el-tabs__nav-wrap::after) {
  background: #d8d8d8;
}

.delivery-workspace__tab-panel :deep(.el-tabs__item) {
  color: #ffffff;
  font-size: 14px;
  font-weight: 700;
  opacity: 1;
}

.delivery-workspace__tab-panel :deep(.el-tabs__item.is-active) {
  color: #409eff;
}

.delivery-workspace__tab-panel :deep(.el-tabs__active-bar) {
  background-color: #409eff;
}

.secret-settings-btn {
  height: 36px;
  padding: 0 18px;
  border: 1px solid #3e3e3e;
  border-radius: 8px;
  background: #161616;
  color: #f4d392;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}

.secret-settings-btn:hover {
  border-color: #a9824e;
  color: #fff1cf;
}

.delivery-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--delivery-result-width);
  gap: 14px;
  align-items: start;
}

.delivery-form-area {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
  align-items: start;
  min-width: 0;
}

.delivery-column {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 14px;
}

.delivery-card {
  width: 100%;
  min-width: 0;
  align-self: stretch;
}

.delivery-card--wide {
  grid-column: auto;
}

.delivery-card--preview {
  position: sticky;
  top: 72px;
  grid-column: 2;
  grid-row: 1;
  align-self: start;
  max-height: calc(100vh - 88px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.delivery-form-grid {
  display: grid;
  gap: 14px;
}

.delivery-form-grid--two {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.delivery-field {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}

.delivery-field__label {
  color: #f6f6f6;
  font-size: 13px;
  font-weight: 700;
}

.delivery-field--wide {
  grid-column: 1 / -1;
}

.duration-control {
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: flex-start;
}

.duration-control__presets {
  width: 100%;
}

.duration-control__custom {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 40px;
}

.duration-control__custom-label,
.duration-control__unit {
  color: #b8b8b8;
  font-size: 12px;
  white-space: nowrap;
}

.duration-control__input {
  width: 112px;
}

.script-learning-panel__toolbar {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  gap: 8px;
  align-items: center;
}

.script-learning-panel__source-input {
  min-width: 0;
}

.script-action-btn,
.script-primary-action-btn {
  min-height: 36px;
  border: 1px solid #484848;
  border-radius: 8px;
  background: #171717;
  color: #f7f7ff;
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  white-space: nowrap;
}

.script-primary-action-btn,
.delivery-primary-btn {
  background: linear-gradient(135deg, #a57d45 0%, #c9a260 100%);
  border-color: transparent;
  color: #fff;
}

.script-action-btn--danger {
  color: #ff9f9f;
}

.script-action-btn:disabled,
.script-primary-action-btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.voice-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.product-assets {
  display: grid;
  gap: 12px;
}

.product-assets__add {
  justify-self: start;
}

.scene-face-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
}

.scene-face-panel {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 12px;
  padding: 12px;
  border: 1px solid #303030;
  border-radius: 10px;
  background: #141414;
}

.scene-face-panel__head {
  display: grid;
  gap: 4px;
  padding-bottom: 8px;
  border-bottom: 1px solid #2b2b2b;
}

.scene-face-panel__head strong {
  color: #f3f3f3;
  font-size: 14px;
}

.scene-face-panel__head span {
  color: #8e8e8e;
  font-size: 12px;
}

.voice-section {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px;
  align-items: end;
}

.voice-manage-btn {
  min-width: 104px;
}

.voice-inline-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  min-height: 36px;
}

.voice-status {
  padding: 9px 11px;
  border: 1px solid #393939;
  border-radius: 8px;
  background: #1a1a1a;
  color: #d6d6d6;
  font-size: 12px;
  line-height: 1.5;
}

.voice-status--success {
  border-color: rgba(74, 170, 118, 0.45);
  background: rgba(74, 170, 118, 0.12);
  color: #b8f0cf;
}

.voice-status--error {
  border-color: rgba(218, 84, 84, 0.55);
  background: rgba(218, 84, 84, 0.12);
  color: #ffb6b6;
}

.selected-voice-name {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 8px 10px;
  border: 1px solid rgba(74, 170, 118, 0.45);
  border-radius: 8px;
  background: rgba(74, 170, 118, 0.12);
  color: #d8ffe6;
  font-size: 13px;
  font-weight: 700;
}

.selected-voice-name span {
  color: #a9d9bd;
  font-size: 12px;
  font-weight: 500;
  word-break: break-all;
}

.voice-dialog__tabs {
  max-height: 62vh;
  overflow-y: auto;
  padding-right: 4px;
}

.voice-dialog__tabs :deep(.el-tabs__item:not(.is-active)) {
  color: #fff;
}

.voice-dialog__toolbar {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin: 10px 0 0;
}

.voice-dialog__toolbar--list {
  align-items: center;
  justify-content: space-between;
  margin: 12px 0;
}

.voice-list-summary {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  min-width: 110px;
  color: #6f6f6f;
  font-size: 12px;
}

.voice-list-summary strong {
  color: #b58c51;
  font-size: 18px;
}

.voice-search-input {
  flex: 1;
  min-width: 180px;
}

.voice-list {
  display: grid;
  gap: 8px;
  max-height: 420px;
  overflow-y: auto;
  padding-right: 4px;
}

.voice-list__item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto auto;
  gap: 10px;
  align-items: center;
  padding: 10px 12px;
  border: 1px solid #333;
  border-radius: 8px;
  background: #171717;
}

.voice-list__meta {
  display: grid;
  gap: 4px;
  min-width: 0;
}

.voice-list__meta strong {
  color: #f4f4f4;
  font-size: 13px;
}

.voice-list__meta span {
  color: #9d9d9d;
  font-size: 12px;
  word-break: break-all;
}

.voice-list__meta em {
  color: #777;
  font-size: 12px;
  font-style: normal;
  line-height: 1.4;
}

.voice-list__source {
  padding: 3px 8px;
  border: 1px solid rgba(181, 140, 81, 0.34);
  border-radius: 999px;
  color: #b58c51;
  font-size: 12px;
  white-space: nowrap;
}

.preview-panel__meta {
  display: grid;
  gap: 8px;
  padding: 12px;
  border: 1px solid #363636;
  border-radius: 8px;
  background: #141414;
}

.generation-mode-tabs :deep(.el-tabs__header) {
  margin-bottom: 10px;
}

.generation-mode-tabs :deep(.el-tabs__item) {
  color: #f2f2f2;
  font-weight: 700;
}

.generation-mode-tabs :deep(.el-tabs__active-bar) {
  background-color: #b99455;
}

.generation-mode-tabs :deep(.el-tabs__content) {
  color: #a8a8a8;
  font-size: 12px;
  line-height: 1.5;
}

.generation-mode-tabs p {
  margin: 0;
}

.preview-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  color: #c8c8c8;
  font-size: 13px;
}

.preview-row__value {
  color: #f3f3f3;
  font-weight: 700;
  text-align: right;
}

.video-result-actions {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px;
  align-items: center;
}

.video-result-actions .delivery-primary-btn {
  width: 100%;
}

.delivery-secondary-btn {
  min-height: 36px;
  border-color: #474747;
  background: #1a1a1a;
  color: #f2f2f2;
}

.assembly-waiting {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 10px;
  align-items: center;
  margin: -2px 0 12px;
  padding: 10px 12px;
  border: 1px solid rgba(181, 140, 81, 0.42);
  border-radius: 8px;
  background: rgba(181, 140, 81, 0.12);
  color: #f4d392;
}

.assembly-waiting--done {
  border-color: rgba(74, 170, 118, 0.45);
  background: rgba(74, 170, 118, 0.12);
  color: #b8f0cf;
}

.assembly-waiting__spinner {
  width: 18px;
  height: 18px;
  border: 2px solid rgba(244, 211, 146, 0.28);
  border-top-color: #f4d392;
  border-radius: 50%;
  animation: assembly-spin 0.8s linear infinite;
}

.assembly-waiting--done .assembly-waiting__spinner {
  border-color: rgba(184, 240, 207, 0.45);
  animation: none;
}

.assembly-waiting__content {
  display: grid;
  gap: 3px;
  min-width: 0;
}

.assembly-waiting__content strong {
  font-size: 13px;
  line-height: 1.4;
}

.assembly-waiting__content span {
  color: #cfcfcf;
  font-size: 12px;
  line-height: 1.45;
  word-break: break-all;
}

.video-result-panel {
  display: grid;
  gap: 10px;
}

.video-result-panel__player,
.video-result-panel__empty {
  width: 100%;
  min-height: 220px;
  border: 1px solid #303030;
  border-radius: 8px;
  background: #0f0f0f;
  overflow: hidden;
}

.video-result-panel__player video {
  display: block;
  width: 100%;
  max-height: min(48vh, 520px);
  background: #000;
}

.video-result-panel__empty {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  color: #858585;
  font-size: 13px;
  text-align: center;
}

.video-result-panel__link {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 10px;
  align-items: center;
  padding: 9px 10px;
  border: 1px solid #333;
  border-radius: 8px;
  background: #151515;
  font-size: 12px;
}

.video-result-panel__link span {
  color: #9f9f9f;
  white-space: nowrap;
}

.video-result-panel__link a {
  color: #7eb7ff;
  text-decoration: none;
  word-break: break-all;
}

.video-result-panel__debug {
  border: 1px solid #333;
  border-radius: 8px;
  background: #121212;
  overflow: hidden;
}

.video-result-panel__debug summary {
  padding: 10px 12px;
  color: #b8b8b8;
  font-size: 12px;
  cursor: pointer;
}

.video-result-panel__debug :deep(.el-textarea__inner) {
  border-width: 1px 0 0;
  border-radius: 0;
}

@keyframes assembly-spin {
  to {
    transform: rotate(360deg);
  }
}

.secret-status {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 16px;
  padding: 10px 12px;
  border: 1px solid #4a3620;
  border-radius: 8px;
  background: rgba(181, 140, 81, 0.12);
  color: #f4d392;
  font-size: 13px;
}

.secret-status--valid {
  border-color: rgba(74, 170, 118, 0.45);
  background: rgba(74, 170, 118, 0.12);
  color: #b8f0cf;
}

.secret-form {
  max-height: 56vh;
  overflow-y: auto;
  padding-right: 4px;
}

.secret-form__hint {
  margin-top: 6px;
  color: #a8a8a8;
  font-size: 12px;
}

.image-preview-dialog__image {
  display: block;
  width: 100%;
  max-height: 72vh;
  object-fit: contain;
  border-radius: 8px;
  background: #101010;
}

:deep(.ai-section-card) {
  border-color: var(--delivery-border);
  border-radius: 10px;
  background: var(--delivery-surface);
  overflow: hidden;
}

:deep(.ai-section-card__header) {
  padding: 10px 12px;
  border-bottom-color: #333;
}

:deep(.ai-section-card__body) {
  gap: 11px;
  padding: 12px 14px 14px;
}

:deep(.ai-section-card__badge) {
  width: 22px;
  height: 22px;
  background: linear-gradient(135deg, #7d6744 0%, #b18a54 100%);
  font-size: 11px;
}

:deep(.ai-section-card__title) {
  font-size: 14px;
}

:deep(.ai-section-card__description) {
  margin-top: 4px;
  font-size: 11px;
  line-height: 1.35;
}

:deep(.ai-choice-pills) {
  gap: 6px;
}

:deep(.ai-choice-pills__item) {
  min-height: 34px;
  padding: 0 12px;
  border-color: #3f3f3f;
  background: #171717;
  color: #f2f2f2;
}

:deep(.ai-choice-pills__item--active) {
  border-color: #b99455;
  background: rgba(181, 140, 81, 0.2);
  color: #fff;
}

:deep(.el-input__wrapper),
:deep(.el-textarea__inner),
:deep(.el-select__wrapper) {
  background: #111111;
  border: 1px solid #383838;
  box-shadow: none;
}

:deep(.el-input__inner),
:deep(.el-textarea__inner),
:deep(.el-select__placeholder),
:deep(.el-select__selected-item) {
  color: #f4f4f4;
}

:deep(.el-textarea__inner::placeholder),
:deep(.el-input__inner::placeholder) {
  color: #838383;
}

:deep(.el-input__wrapper.is-focus),
:deep(.el-textarea__inner:focus),
:deep(.el-select__wrapper.is-focused) {
  border-color: #a9824e;
}

:deep(.el-input-number .el-input__wrapper) {
  padding-left: 8px;
  padding-right: 32px;
}

:deep(.el-button.delivery-primary-btn) {
  width: 100%;
  min-height: 40px;
  margin-bottom: 12px;
  font-weight: 800;
}

:global(.el-dialog.image-video-secret-dialog) {
  border: 1px solid #393939;
  border-radius: 12px;
  background: #151515;
  color: #f2f2f2;
}

:global(.el-dialog.image-video-secret-dialog .el-dialog__header) {
  margin: 0;
  padding: 18px 20px 12px;
  border-bottom: 1px solid #303030;
  background: #151515;
}

:global(.el-dialog.image-video-secret-dialog .el-dialog__body) {
  padding: 16px 20px;
  background: #151515;
  color: #f2f2f2;
}

:global(.el-dialog.image-video-secret-dialog .el-dialog__footer) {
  padding: 12px 20px 18px;
  border-top: 1px solid #303030;
  background: #151515;
}

:global(.el-dialog.image-video-secret-dialog .el-dialog__close) {
  color: #e8e8e8;
}

:deep(.el-dialog__title),
:deep(.el-form-item__label) {
  color: #f5f5f5;
}

:global(.el-dialog.image-video-secret-dialog .el-dialog__title) {
  color: #ffffff;
  font-weight: 700;
}

:global(.el-dialog.image-video-secret-dialog .el-form-item__label) {
  color: #ffffff;
  font-weight: 700;
}

:global(.el-dialog.image-video-secret-dialog .el-input__wrapper),
:global(.el-dialog.image-video-secret-dialog .el-select__wrapper),
:global(.el-dialog.image-video-secret-dialog .el-textarea__inner) {
  background: #101010;
  border-color: #414141;
  color: #f4f4f4;
}

:global(.el-dialog.image-video-secret-dialog .el-input__inner),
:global(.el-dialog.image-video-secret-dialog .el-select__selected-item),
:global(.el-dialog.image-video-secret-dialog .el-select__placeholder) {
  color: #f4f4f4;
}

:global(.el-dialog.image-video-secret-dialog .el-input__inner::placeholder),
:global(.el-dialog.image-video-secret-dialog .el-textarea__inner::placeholder) {
  color: #9d9d9d;
}

:global(.el-dialog.image-video-secret-dialog .secret-form__hint) {
  color: #b8b8b8;
}

:global(.el-dialog.image-video-secret-dialog .secret-status) {
  color: #f4d392;
}

:global(.el-dialog.image-video-secret-dialog .secret-status--valid) {
  color: #b8f0cf;
}

@media (max-width: 1320px) {
  .delivery-grid {
    --delivery-result-width: 360px;
    grid-template-columns: minmax(0, 1fr) var(--delivery-result-width);
  }

  .delivery-form-area {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .delivery-card--preview {
    grid-column: 2;
    grid-row: 1;
  }
}

@media (max-width: 980px) {
  .delivery-workspace {
    padding: 16px;
  }

  .delivery-grid {
    grid-template-columns: 1fr;
  }

  .delivery-form-area {
    grid-template-columns: 1fr;
  }

  .delivery-form-grid--two {
    grid-template-columns: 1fr;
  }

  .scene-face-grid {
    grid-template-columns: 1fr;
  }

  .delivery-card,
  .delivery-card--wide,
  .delivery-card--preview {
    grid-column: 1;
  }

  .delivery-card--preview {
    position: static;
    max-height: none;
    overflow: visible;
  }

  .script-learning-panel__toolbar {
    grid-template-columns: 1fr;
  }

  .script-action-btn,
  .script-primary-action-btn,
  .duration-control__custom,
  .duration-control__input {
    width: 100%;
  }

  .voice-section,
  .voice-list__item {
    grid-template-columns: 1fr;
  }

  .voice-dialog__toolbar--list {
    align-items: stretch;
    flex-direction: column;
  }

  .voice-inline-actions {
    flex-direction: column;
    align-items: stretch;
  }
}
</style>
