#!/usr/bin/env node
/**
 * CapCut 成片组装脚本 — 编排器
 *
 * 将图片/视频素材通过 CapCut Mate API 组装为草稿，同步到本地剪映。
 *
 * 模块结构:
 *   capcut_assemble.js       ← 编排器（本文件）+ CLI + 上传 + 同步
 *   lib/capcut-api.js        ← 配置、API 封装、工具函数
 *   lib/capcut-timeline.js   ← 时间线构建 + 视频调整策略
 *   lib/capcut-tracks.js     ← 所有轨道操作（图片/视频/音频/字幕/特效）
 *
 * 用法:
 *   node capcut_assemble.js --input ./output/batch_xxx [选项]
 */

const path = require('path')
const fs = require('fs')

const { US, parseArgs, getResolution, getAudioDurationSec } = require('./lib/capcut-api')
const { buildTimeline, adjustVideoSpeed } = require('./lib/capcut-timeline')
const {
  loadAccountConfig, loadSubtitleStyle,
  loadKenBurns, loadTransitions,
  addImages, addVideos, addSlotsLocally,
  addVoiceover, addBGM,
  addSubtitles,
  consolidateTracks,
  addEffects, addFilter,
} = require('./lib/capcut-tracks')
const { saveManifest } = require('./lib/pipeline-utils')
const { syncDraft, registerDraft } = require('./sync-to-jianying')

// ============================================================================
// OSS 上传
// ============================================================================

const ossUpload = require(path.join(__dirname, 'oss-upload'))

async function uploadToOSS(filePath) {
  const { url } = await ossUpload.uploadFile(filePath)
  return url
}

async function batchUploadToOSS(inputDir, files, concurrency = 3) {
  const urls = {}
  const queue = [...files]
  const workers = Array(Math.min(concurrency, queue.length)).fill(null).map(async () => {
    while (queue.length > 0) {
      const file = queue.shift()
      if (!file) break
      const filePath = path.join(inputDir, file)
      if (!fs.existsSync(filePath)) continue
      try {
        urls[file] = await uploadToOSS(filePath)
        console.log(`   上传: ${file} -> OK`)
      } catch (err) {
        console.error(`   上传失败: ${file} - ${err.message}`)
      }
    }
  })
  await Promise.all(workers)
  return urls
}

async function batchUploadAudio(inputDir, items) {
  const urls = {}
  for (const item of items) {
    // 处理主音频
    if (item.audio && !item.audio.startsWith('http')) {
      if (!urls[item.audio]) {
        const filePath = path.isAbsolute(item.audio)
          ? item.audio
          : path.resolve(inputDir, item.audio)
        if (fs.existsSync(filePath)) {
          try {
            urls[item.audio] = await uploadToOSS(filePath)
            console.log(`   上传: ${path.basename(filePath)} -> OK`)
          } catch (err) {
            console.error(`   上传失败: ${path.basename(filePath)} - ${err.message}`)
          }
        }
      }
    } else if (item.audio) {
      urls[item.audio] = item.audio
    }
    // 处理分段音频
    if (item.segments && item.segments.length > 0) {
      for (const seg of item.segments) {
        if (!seg.audio || seg.error) continue
        if (urls[seg.audio]) continue
        const filePath = path.isAbsolute(seg.audio)
          ? seg.audio
          : path.resolve(inputDir, seg.audio)
        if (!fs.existsSync(filePath)) {
          console.error(`   音频文件不存在: ${filePath}`)
          continue
        }
        try {
          urls[seg.audio] = await uploadToOSS(filePath)
          console.log(`   上传: ${path.basename(filePath)} -> OK`)
        } catch (err) {
          console.error(`   上传失败: ${path.basename(filePath)} - ${err.message}`)
        }
      }
    }
  }
  return urls
}

// ============================================================================
// 同步到本地剪映
// ============================================================================

async function syncToLocalJianying(draftUrl, draftId, totalDurationUs) {
  await syncDraft(draftUrl, { name: draftId })
  registerDraft(draftId, draftId, totalDurationUs)
}

// ============================================================================
// 主流程
// ============================================================================

async function assemble(args) {
  const {
    input,
    manifest: manifestPath,
    mode = 'images',
    subtitles = 'true',
    splitCaptions = 'true',
    voiceover = 'true',
    bgm,
    effects: effectsStr,
    filter: filterStr,
    format = '9:16',
    apiKey = '',
    animation = '轻微放大',
  } = args

  if (!input) throw new Error('缺少 --input 参数')

  const inputDir = path.resolve(input)
  const manifestFile = manifestPath
    ? path.resolve(manifestPath)
    : path.join(inputDir, 'manifest.json')

  if (!fs.existsSync(manifestFile)) {
    throw new Error(`找不到 manifest.json: ${manifestFile}`)
  }

  const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'))

  // 从 account.json 自动继承 effects / filter
  let finalEffects = effectsStr
  let finalFilter = filterStr
  if (!finalEffects || !finalFilter) {
    const accountData = loadAccountConfig(manifest)
    if (!finalEffects && accountData.capcut?.effects?.length) {
      finalEffects = accountData.capcut.effects.join(',')
    }
    if (!finalFilter && accountData.capcut?.filter) {
      finalFilter = accountData.capcut.filter
    }
  }

  const { width, height } = getResolution(format)

  const items = manifest.items.filter(item => {
    if (item.url) return true
    if (item.video) return true
    const filePath = path.join(inputDir, item.file)
    return fs.existsSync(filePath)
  })

  if (items.length === 0) throw new Error('没有可用的素材文件')

  // 测量实际时长
  let audioMeasured = 0, videoMeasured = 0
  for (const item of items) {
    if (item.audio && !item.audio.startsWith('http')) {
      const audioPath = path.isAbsolute(item.audio)
        ? item.audio
        : path.resolve(inputDir, item.audio)
      if (fs.existsSync(audioPath)) {
        const actualDur = await getAudioDurationSec(audioPath)
        if (actualDur != null) { item.audioDuration = actualDur; audioMeasured++ }
      }
    }
    if (item.video) {
      const videoPath = path.isAbsolute(item.video)
        ? item.video
        : path.resolve(inputDir, item.video)
      if (fs.existsSync(videoPath)) {
        const actualDur = await getAudioDurationSec(videoPath)
        if (actualDur != null) { item.videoDuration = actualDur; videoMeasured++ }
      }
    }
  }
  if (audioMeasured > 0 || videoMeasured > 0) {
    console.log(`  实际时长测量: 音频 ${audioMeasured} 个, 视频 ${videoMeasured} 个`)
  }

  const timeline = buildTimeline(items)
  const totalDurationUs = timeline.length > 0 ? timeline[timeline.length - 1].end : 0
  const hasTTS = items.some(item => item.audio && item.audioDuration != null)

  // 时间轴诊断
  for (let i = 0; i < items.length; i++) {
    const item = items[i]
    const tl = timeline[i]
    if (tl.skip) { console.log(`  [${i + 1}] 跳过（无音频）`); continue }
    const audioDur = item.audioDuration || 0
    const slotDur = tl.duration / US
    const diff = slotDur - audioDur
    const videoDur = (item.videoDuration || 0)
    const stratInfo = tl.strategy && tl.strategy !== 'none' ? ` 策略=${tl.strategy}` : ''
    const marker = Math.abs(diff) > 0.05 ? ' ⚠️ 不对齐' : ''
    console.log(`  [${i + 1}] 画面=${slotDur.toFixed(2)}s 音频=${audioDur.toFixed(2)}s 视频=${videoDur.toFixed(2)}s${stratInfo}${marker}`)
  }

  const transitionConfig = loadTransitions(manifest)

  console.log(`\nCapCut 成片组装`)
  console.log(`  模式: ${mode}  画幅: ${format} (${width}x${height})`)
  console.log(`  时间线: ${hasTTS ? 'TTS音频驱动' : '视频原始时长'}  总时长: ${(totalDurationUs / US).toFixed(1)}s`)
  console.log(`  字幕: ${subtitles}  配音: ${voiceover}  动画: ${animation}`)
  if (finalEffects) console.log(`  特效: ${finalEffects}`)
  if (finalFilter) console.log(`  滤镜: ${finalFilter}`)
  console.log(`  素材: ${items.length} 个可用\n`)

  const steps = []
  if (mode === 'images') steps.push('upload')
  steps.push('draft', 'materials', 'kenburns', 'audio_oss', 'voiceover', 'audio', 'subtitles', 'effects', 'filter', 'save', 'sync', 'consolidate')
  const totalSteps = steps.length
  let step = 0

  // -- 上传图片到 OSS --
  let imgUrls = {}
  if (mode === 'images') {
    const needUpload = []
    for (const item of items) {
      if (item.url && item.url.startsWith('http')) {
        imgUrls[item.file] = item.url
      } else {
        needUpload.push(item.file)
      }
    }
    if (needUpload.length > 0) {
      step++; console.log(`[${step}/${totalSteps}] 上传图片到 OSS (${needUpload.length} 张需上传, ${Object.keys(imgUrls).length} 张已有URL)...`)
      const uploaded = await batchUploadToOSS(inputDir, needUpload)
      imgUrls = { ...imgUrls, ...uploaded }
    } else {
      step++; console.log(`[${step}/${totalSteps}] 所有图片已有 URL，跳过上传`)
    }
    if (Object.keys(imgUrls).length === 0) throw new Error('所有图片上传失败')
    console.log(`   成功: ${Object.keys(imgUrls).length}/${items.length}\n`)
  }

  // -- 创建草稿 --
  step++; console.log(`[${step}/${totalSteps}] 创建草稿...`)
  const draftRes = await require('./lib/capcut-api').api('create_draft', { width, height })
  const draftUrl = draftRes.draft_url
  const draftId = new URL(draftUrl).searchParams.get('draft_id')
  console.log(`   draft_id: ${draftId}\n`)

  // -- 导入素材 --
  step++; console.log(`[${step}/${totalSteps}] 导入素材...`)
  let imageSegmentIds = []
  if (mode === 'images') {
    imageSegmentIds = await addImages(draftUrl, items, imgUrls, timeline, width, height, animation, transitionConfig)
  } else {
    // 视频模式：调整 → 上传 → 添加
    let adjustedCount = 0
    for (let i = 0; i < items.length; i++) {
      const item = items[i]
      const tl = timeline[i]
      if (tl.strategy && tl.strategy !== 'none' && item.video) {
        const videoPath = path.resolve(inputDir, item.video)
        const audioDur = tl.duration / US
        const adjustedPath = await adjustVideoSpeed(videoPath, audioDur, tl.strategy, tl.speed, tl.freezeExtra || 0)
        if (adjustedPath !== videoPath) {
          item.video = path.relative(inputDir, adjustedPath)
          item.videoDuration = audioDur
          adjustedCount++
        }
      }
    }
    if (adjustedCount > 0) {
      console.log(`  视频调整: ${adjustedCount}/${items.length} 个`)
    }

    const missingUrl = items.filter(it => it.video && !it.videoUrl)
    if (missingUrl.length > 0) {
      console.log(`  上传 ${missingUrl.length} 个视频到 OSS...`)
      for (const item of missingUrl) {
        const videoPath = path.resolve(inputDir, item.video)
        try {
          const url = await uploadToOSS(videoPath)
          item.videoUrl = url
          if (manifestFile) {
            try {
              const m = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'))
              const mi = m.items.find(i => i.id === item.id)
              if (mi) { mi.videoUrl = url; saveManifest(manifestFile, m) }
            } catch (_) {}
          }
        } catch (err) {
          console.log(`  视频上传失败: ${err.message}`)
        }
      }
    }
    const segmentIds = await addVideos(draftUrl, inputDir, items, timeline, width, height, transitionConfig)
    // 将 segment_ids 附加到 items，供后续 addSlotsLocally 使用
    if (segmentIds && segmentIds.length > 0) {
      items.forEach((item, i) => { item._segmentId = segmentIds[i] || null })
    }
  }

  // -- Ken Burns --
  if (mode === 'images' && imageSegmentIds.length > 0) {
    step++; console.log(`[${step}/${totalSteps}] 添加 Ken Burns 镜头动画...`)
    await addKenBurns(draftUrl, imageSegmentIds, items, timeline, manifest)
  }

  // -- 上传 TTS 音频到 OSS --
  let audioUrls = {}
  if (voiceover === 'true' && hasTTS) {
    step++; console.log(`[${step}/${totalSteps}] 上传 TTS 音频到 OSS...`)
    try {
      audioUrls = await batchUploadAudio(inputDir, items)
      console.log(`   成功: ${Object.keys(audioUrls).length} 段音频\n`)
      if (Object.keys(audioUrls).length > 0 && manifestFile) {
        let changed = false
        for (const item of manifest.items) {
          if (item.audio && audioUrls[item.audio]) {
            item.audio = audioUrls[item.audio]
            changed = true
          }
        }
        if (changed) saveManifest(manifestFile, manifest)
      }
    } catch (err) {
      console.log(`   OSS 上传失败，将尝试本地路径: ${err.message}\n`)
    }
  }

  // -- 添加 TTS 配音 --
  step++; console.log(`[${step}/${totalSteps}] 添加 TTS 配音...`)
  if (voiceover === 'true' && hasTTS) {
    await addVoiceover(draftUrl, inputDir, items, timeline, audioUrls)
  } else {
    console.log('   跳过（无 TTS 音频或未启用）')
  }

  // -- 添加 BGM --
  step++; console.log(`[${step}/${totalSteps}] 添加背景音乐...`)
  if (bgm) {
    await addBGM(draftUrl, bgm, totalDurationUs)
  } else {
    console.log('   跳过（未指定 --bgm）')
  }

  // -- 字幕风格 --
  const subtitleStyle = loadSubtitleStyle(manifest)
  if (Object.keys(subtitleStyle).length > 0) {
    console.log(`  字幕风格: ${subtitleStyle.font || '默认'} ${subtitleStyle.inAnimation ? subtitleStyle.inAnimation + '→' + subtitleStyle.outAnimation : ''}`)
  }

  // -- 添加字幕 --
  step++; console.log(`[${step}/${totalSteps}] 添加字幕...`)
  if (subtitles === 'true' && items.some(i => i.script || i.text)) {
    await addSubtitles(draftUrl, items, timeline, subtitleStyle, splitCaptions === 'true')
  } else {
    console.log('   跳过')
  }

  // -- 特效 --  // -- 特效 --
  step++; console.log(`[${step}/${totalSteps}] 添加特效...`)
  if (finalEffects) {
    try {
      await addEffects(draftUrl, finalEffects, totalDurationUs)
    } catch (e) {
      console.log(`   特效跳过: ${e.message}`)
    }
  } else {
    console.log('   跳过（未配置特效）')
  }

  // -- 滤镜 --
  step++; console.log(`[${step}/${totalSteps}] 添加滤镜...`)
  if (finalFilter) {
    try {
      await addFilter(draftUrl, finalFilter, totalDurationUs)
    } catch (e) {
      console.log(`   滤镜跳过: ${e.message}`)
    }
  } else {
    console.log('   跳过（未配置滤镜）')
  }

  // -- 保存草稿 --
  step++; console.log(`[${step}/${totalSteps}] 保存草稿...`)
  await require('./lib/capcut-api').api('save_draft', { draft_url: draftUrl })
  console.log('   已保存\n')

  // -- 同步到本地剪映 --
  step++; console.log(`[${step}/${totalSteps}] 同步到本地剪映...`)
  await syncToLocalJianying(draftUrl, draftId, totalDurationUs)
  console.log('   同步完成\n')

  // -- 合并同类型轨道（TTS 逐条降级时每条独占一个轨道）--
  step++; console.log(`[${step}/${totalSteps}] 合并同类型轨道...`)
  consolidateTracks(draftId)

  // -- 视频轨道 slot 写入（在 syncToLocalJianying 之后执行，此时本地草稿文件已存在）--
  if (mode !== 'images') {
    step++; console.log(`[${step}/${totalSteps}] 写入视频轨道时间线...`)
    await addSlotsLocally(draftUrl, items, timeline, null, { draftId })
    console.log('   视频轨道写入完成\n')
  }

  // -- 云渲染（可选）--
  if (apiKey) {
    console.log('提交云渲染...')
    await require('./lib/capcut-api').api('gen_video', { draft_url: draftUrl, apiKey })
    console.log('渲染已提交，使用 gen_video_status 查询进度')
  }

  console.log(`\n成片组装完成`)
  console.log(`  草稿ID: ${draftId}`)
  console.log(`  总时长: ${(totalDurationUs / US).toFixed(1)}s`)
  console.log(`  素材数: ${items.length}`)
  console.log(`  时间线: ${hasTTS ? 'TTS音频驱动' : '视频原始时长'}`)
  if (mode === 'videos' && subtitles === 'false') {
    console.log(`\n  >> 视频模式未加字幕，请在剪映中打开草稿 → 识别字幕 → 语音识别生成\n`)
  }
}

// ============================================================================
// CLI 入口
// ============================================================================

async function main() {
  const args = parseArgs(process.argv.slice(2))

  if (!args.input) {
    console.log('用法: node capcut_assemble.js --input <目录> [选项]')
    console.log('')
    console.log('必填:')
    console.log('  --input <dir>            素材目录（含 manifest.json）')
    console.log('')
    console.log('选项:')
    console.log('  --mode images|videos     素材类型（默认 images）')
    console.log('  --format 9:16            画幅比例')
    console.log('  --voiceover true|false   是否添加TTS配音轨道（默认 true）')
    console.log('  --subtitles true|false   是否添加字幕（默认 true）')
    console.log('  --split-captions true|false  分句字幕模式（默认 true，按标点切分）')
    console.log('  --bgm <url>              背景音乐 URL')
    console.log('  --effects "名称1,名称2"  特效名称（逗号分隔）')
    console.log('  --filter "名称:强度"     滤镜（强度 0-100）')
    console.log('  --apiKey <key>           云渲染 API Key（可选）')
    console.log('  --manifest <path>        manifest.json 路径')
    console.log('')
    console.log('时间线规则:')
    console.log('  图片模式: TTS 音频时长 = 画面时长，无音频则跳过')
    console.log('  视频模式: TTS 为主轴，视频通过以下策略适配:')
    console.log('    视频比音频长 → 加速(≤2x) 或 裁剪(>2x)')
    console.log('    视频比音频短 → 放缓(≥0.5x) 或 画面停顿(<0.5x)')
    console.log('    所有策略失败 → 兜底截断')
    console.log('')
    console.log('配置:')
    console.log('  请运行 node setup.js 生成配置')
    process.exit(0)
  }

  await assemble(args)
}

if (require.main === module) {
  main().catch(err => {
    console.error(`\n错误: ${err.message}`)
    process.exit(1)
  })
}

module.exports = { assemble }
