#!/usr/bin/env node

/**
 * Kling Video Generator - 图生视频（官方可灵 API）
 *
 * 使用官方 Kling API (api-beijing.klingai.com) 进行图生视频
 * AK/SK → JWT (HMAC-SHA256) 鉴权
 *
 * 凭证来源（优先级）:
 *   1. ~/.config/kling/.credentials (klingai skill 存储)
 *   2. config.json 的 kelingAccessKeyId + kelingSecretAccessKey
 *      (向下兼容 kelingApiKey 作为 AK)
 *
 * 用法:
 *   node kling-video-generator.js --image <url> --prompt "zoom in" -o ./output
 *   node kling-video-generator.js --image <url> --last-frame <url> --prompt "过渡" -o ./output
 *   node kling-video-generator.js batch ./manifest.json -o ./output
 */

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

// ============================================================================
// 配置
// ============================================================================

function loadConfig() {
  const configPath = path.join(__dirname, '..', '..', 'config.json')
  if (fs.existsSync(configPath)) {
    return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
  }
  return {}
}

const cfg = loadConfig()

const Config = {
  apiBase: (cfg.kelingApiBaseUrl || 'https://api-beijing.klingai.com').replace(/\/+$/, ''),
  model: cfg.kelingModel || 'kling-v3',
  pollInterval: 10000,
  maxPollTime: 600000,
  maxRetries: 3,
}

// ============================================================================
// JWT 鉴权（来自可灵官方 API 规范）
// ============================================================================

function base64url(buf) {
  return Buffer.from(buf).toString('base64')
    .replace(/=/g, '')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
}

function makeJwt(accessKey, secretKey) {
  const header = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))
  const now = Math.floor(Date.now() / 1000)
  const payload = base64url(JSON.stringify({
    iss: accessKey,
    exp: now + 1800,
    nbf: now - 5,
  }))
  const signature = base64url(
    crypto.createHmac('sha256', secretKey).update(`${header}.${payload}`).digest()
  )
  return `${header}.${payload}.${signature}`
}

// ============================================================================
// 凭证加载
// ============================================================================

function getCredentialsDir() {
  const home = process.env.HOME || process.env.USERPROFILE
  return home ? path.join(home, '.config', 'kling') : null
}

function parseCredentialsIni(content) {
  let current = null
  const profiles = {}
  for (const line of content.split('\n')) {
    const t = line.trim()
    if (!t || t.startsWith('#') || t.startsWith(';')) continue
    const m = t.match(/^\[([^\]]+)\]\s*$/)
    if (m) {
      current = m[1].trim()
      if (!profiles[current]) profiles[current] = {}
      continue
    }
    const eqIdx = t.indexOf('=')
    if (eqIdx <= 0 || !current) continue
    const k = t.slice(0, eqIdx).trim()
    let v = t.slice(eqIdx + 1).trim()
    if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
      v = v.slice(1, -1)
    }
    profiles[current][k] = v
  }
  return profiles
}

function readCredentialsFile() {
  const dir = getCredentialsDir()
  if (!dir) return { ak: '', sk: '' }
  const credPath = path.join(dir, '.credentials')
  if (!fs.existsSync(credPath)) return { ak: '', sk: '' }
  try {
    const profiles = parseCredentialsIni(fs.readFileSync(credPath, 'utf-8'))
    const p = profiles['default'] || {}
    return {
      ak: (p.access_key_id || p.access_key || '').trim(),
      sk: (p.secret_access_key || p.secret_key || '').trim(),
    }
  } catch {
    return { ak: '', sk: '' }
  }
}

function loadCredentials() {
  // 1. ~/.config/kling/.credentials (klingai skill)
  const fileCreds = readCredentialsFile()
  if (fileCreds.ak && fileCreds.sk) {
    console.log(`   凭证来源: ~/.config/kling/.credentials`)
    return { ak: fileCreds.ak, sk: fileCreds.sk }
  }

  // 2. config.json
  const ak = (cfg.kelingAccessKeyId || cfg.kelingApiKey || '').trim()
  const sk = (cfg.kelingSecretAccessKey || '').trim()
  if (ak && sk) {
    console.log(`   凭证来源: config.json`)
    return { ak, sk }
  }

  throw new Error(
    '未配置可灵 API 凭证（需要 AK + SK）。请选择以下方式之一：\n' +
    '  1. 在 config.json 中添加 kelingSecretAccessKey（kelingApiKey 作为 AK）\n' +
    '  2. 运行可灵绑定: node kling.mjs account --bind-url\n' +
    '  3. 在 ~/.config/kling/.credentials 中配置 AK/SK'
  )
}

// ============================================================================
// 提示词优化（失败时自动调整）
// ============================================================================

const PromptOptimizer = {
  optimize(prompt, failReason, attempt) {
    let optimized = prompt

    if (attempt === 1) {
      optimized = simplifyPrompt(prompt)
      console.log(`   重试策略: 简化提示词`)
    }

    if (attempt === 2) {
      optimized = `${simplifyPrompt(prompt)}, smooth motion, high quality`
      console.log(`   重试策略: 简化 + 安全后缀`)
    }

    if (attempt >= 3) {
      optimized = extractCoreSubject(prompt)
      console.log(`   重试策略: 极简提示词`)
    }

    return optimized
  }
}

function simplifyPrompt(prompt) {
  const parts = prompt.split(',').map(s => s.trim())
  return parts.slice(0, 3).join(', ')
}

function extractCoreSubject(prompt) {
  const match = prompt.match(/^([^.!,]+)/)
  return match ? match[1].trim() : 'cinematic motion'
}

// ============================================================================
// 官方可灵 API
// ============================================================================

const KlingApi = {
  async create(imageUrl, prompt, options = {}) {
    const {
      model = Config.model,
      duration = 5,
      mode = 'std',
      lastFrameUrl = '',
    } = options

    const creds = loadCredentials()
    const token = makeJwt(creds.ak, creds.sk)

    const body = {
      model_name: model,
      image: imageUrl,
      prompt,
      duration: String(duration),
      mode,
      multi_shot: false,
    }

    if (lastFrameUrl) {
      body.image_tail = lastFrameUrl
    }

    const modeLabel = lastFrameUrl ? '首尾帧' : '单图'

    console.log(`\n📡 提交可灵视频任务 [${modeLabel}]`)
    console.log(`   API: ${Config.apiBase}`)
    console.log(`   模型: ${model}`)
    console.log(`   提示词: ${prompt.substring(0, 80)}...`)
    if (lastFrameUrl) {
      console.log(`   起始帧: ${imageUrl.substring(0, 60)}...`)
      console.log(`   结束帧: ${lastFrameUrl.substring(0, 60)}...`)
    } else {
      console.log(`   参考图: ${imageUrl ? imageUrl.substring(0, 60) + '...' : '无'}`)
    }
    console.log(`   时长: ${duration}s | 画质: ${mode}`)

    const res = await fetch(`${Config.apiBase}/v1/videos/image2video`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`,
      },
      body: JSON.stringify(body),
    })

    const json = await res.json()

    if (json.code !== undefined && json.code !== 0 && json.code !== 200) {
      throw new Error(`可灵 API 错误 (code=${json.code}): ${json.message || JSON.stringify(json)}`)
    }

    const data = json.data || {}
    const taskId = data.task_id

    if (!taskId) {
      throw new Error(`可灵提交失败: ${JSON.stringify(json)}`)
    }

    console.log(`   任务 ID: ${taskId}`)
    return taskId
  },

  async query(taskId) {
    const creds = loadCredentials()
    const token = makeJwt(creds.ak, creds.sk)

    const res = await fetch(`${Config.apiBase}/v1/videos/image2video/${taskId}`, {
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
    })

    const json = await res.json()

    if (json.code !== undefined && json.code !== 0 && json.code !== 200) {
      throw new Error(`可灵查询错误 (code=${json.code}): ${json.message}`)
    }

    return json.data || {}
  },

  async poll(taskId) {
    const startTime = Date.now()
    let lastStatus = ''

    console.log(`\n⏳ 等待可灵视频生成（预计 60-300 秒）...`)

    while (Date.now() - startTime < Config.maxPollTime) {
      const data = await KlingApi.query(taskId)
      const status = data.task_status || ''

      if (status === 'succeed') {
        const videos = data.task_result?.videos || []
        if (videos.length === 0) throw new Error('可灵生成成功但未返回视频')
        console.log(`\n✅ 视频生成完成!`)
        console.log(`   视频: ${videos[0].url}`)
        return { success: true, videoUrl: videos[0].url }
      }

      if (status === 'failed') {
        throw new Error(data.task_status_msg || '可灵生成失败')
      }

      if (status !== lastStatus) {
        lastStatus = status
        const elapsed = Math.round((Date.now() - startTime) / 1000)
        console.log(`   状态: ${status}   已等待: ${elapsed}s`)
      }

      await new Promise(r => setTimeout(r, Config.pollInterval))
    }

    throw new Error(`可灵生成超时 (${Config.maxPollTime / 1000}s)`)
  },
}

// ============================================================================
// 图片下载
// ============================================================================

const https = require('https')
const http = require('http')

async function download(url, outputPath) {
  const protocol = url.startsWith('https') ? https : http
  return new Promise((resolve, reject) => {
    const file = fs.createWriteStream(outputPath)
    protocol.get(url, (response) => {
      if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
        file.close()
        fs.unlinkSync(outputPath)
        return download(response.headers.location, outputPath).then(resolve).catch(reject)
      }
      response.pipe(file)
      file.on('finish', () => { file.close(); resolve(outputPath) })
    }).on('error', (err) => {
      file.close()
      if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath)
      reject(err)
    })
  })
}

// ============================================================================
// 核心流程（单任务带重试）
// ============================================================================

async function generate(imageUrl, prompt, options = {}) {
  const { outputDir = './output', duration = 5, mode = 'std' } = options

  fs.mkdirSync(outputDir, { recursive: true })

  let currentPrompt = prompt
  let lastError = null

  for (let attempt = 0; attempt <= Config.maxRetries; attempt++) {
    try {
      if (attempt > 0) {
        currentPrompt = PromptOptimizer.optimize(prompt, lastError, attempt)
        console.log(`\n🔄 第 ${attempt} 次重试`)
        console.log(`   新提示词: ${currentPrompt}`)
      }

      const taskId = await KlingApi.create(imageUrl, currentPrompt, {
        duration, mode,
        lastFrameUrl: options.lastFrameUrl,
      })
      const result = await KlingApi.poll(taskId)

      const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
      const videoFile = path.join(outputDir, `${timestamp}_kling.mp4`)
      await download(result.videoUrl, videoFile)
      console.log(`   下载完成: ${videoFile}`)

      return {
        success: true,
        taskId,
        prompt: currentPrompt,
        originalPrompt: prompt,
        attempts: attempt + 1,
        files: [videoFile],
      }

    } catch (err) {
      lastError = err.message
      console.error(`   ❌ 第 ${attempt + 1} 次失败: ${err.message}`)

      if (attempt < Config.maxRetries) {
        console.log(`   等待 5 秒后重试...`)
        await new Promise(r => setTimeout(r, 5000))
      }
    }
  }

  throw new Error(`可灵视频生成失败（已重试 ${Config.maxRetries} 次）: ${lastError}`)
}

// ============================================================================
// 批量并行生成（支持 manifest.json）
// ============================================================================

async function batchGenerate(tasks, options = {}) {
  const { outputDir = './output', concurrency = 2, duration = 5, mode = 'std' } = options

  fs.mkdirSync(outputDir, { recursive: true })

  // 支持 manifest 格式
  if (tasks.items && Array.isArray(tasks.items)) {
    tasks = tasks.items.map(item => ({
      image: item.url || item.image || '',
      prompt: item.videoPrompt || item.prompt || 'cinematic motion',
      text: item.text || item.caption || '',
      keyword: item.keyword || '',
      keywordColor: item.keywordColor || '',
      file: item.file || '',
      lastFrameUrl: item.lastFrameUrl || '',
    }))
  }

  // Phase 1: 并行提交
  const modeLabel = tasks.some(t => t.lastFrameUrl) ? '首尾帧' : '单图'
  console.log(`\n📡 并行提交 ${tasks.length} 个可灵视频任务（并发: ${concurrency}，模式: ${modeLabel}）...`)

  const submitted = []
  for (let i = 0; i < tasks.length; i += concurrency) {
    const batch = tasks.slice(i, i + concurrency)
    const batchResults = await Promise.allSettled(
      batch.map(async (task, j) => {
        const idx = i + j
        const prompt = task.videoPrompt || task.prompt
        console.log(`   [${idx + 1}/${tasks.length}] 提交: ${prompt.substring(0, 50)}...`)
        try {
          const taskId = await KlingApi.create(task.image, prompt, {
            duration, mode, lastFrameUrl: task.lastFrameUrl,
          })
          return { idx, taskId, task, error: null }
        } catch (err) {
          console.error(`   [${idx + 1}] 提交失败: ${err.message}`)
          return { idx, taskId: null, task, error: err.message }
        }
      })
    )
    submitted.push(...batchResults.map(r => r.value || r.reason))
  }

  const pendingTasks = submitted.filter(s => s.taskId)

  if (pendingTasks.length === 0) {
    console.error('\n❌ 所有任务提交失败')
    return tasks.map((task, idx) => ({
      success: false, ...task,
      error: (submitted.find(s => s.idx === idx) || {}).error || '提交失败',
    }))
  }

  // Phase 2: 并行轮询
  console.log(`\n⏳ 并行等待 ${pendingTasks.length} 个视频生成...`)

  const pollResults = await Promise.allSettled(
    pendingTasks.map(async ({ idx, taskId, task }) => {
      const prompt = task.videoPrompt || task.prompt
      const result = await pollWithRetry(taskId, prompt, {
        outputDir, duration, mode,
        imageUrl: task.image, lastFrameUrl: task.lastFrameUrl,
      })
      return { idx, ...result, task }
    })
  )

  // 合并结果
  const results = []
  for (let i = 0; i < tasks.length; i++) {
    const submittedInfo = submitted.find(s => s.idx === i)
    if (!submittedInfo || !submittedInfo.taskId) {
      results.push({ success: false, ...tasks[i], error: submittedInfo?.error || '提交失败' })
      continue
    }
    const pollResult = pollResults.find(r => {
      if (r.status === 'fulfilled') return r.value.idx === i
      return false
    })
    if (pollResult && pollResult.status === 'fulfilled') {
      results.push({ success: true, ...tasks[i], ...pollResult.value })
    } else {
      const reason = pollResult?.reason?.message || '生成失败'
      results.push({ success: false, ...tasks[i], error: reason })
    }
  }

  const ok = results.filter(r => r.success).length
  console.log(`\n✨ 批量完成: ${ok}/${tasks.length} 成功`)

  // 输出 manifest.json
  const manifestItems = results
    .filter(r => r.success && r.files && r.files.length > 0)
    .map(r => {
      const item = {
        file: path.basename(r.files[0]),
        duration: 6,
      }
      if (r.text) item.text = r.text
      if (r.caption) item.caption = r.caption
      if (r.keyword) item.keyword = r.keyword
      if (r.keywordColor) item.keywordColor = r.keywordColor
      return item
    })

  if (manifestItems.length > 0 && !options.skipManifestWrite) {
    const manifestPath = path.join(outputDir, 'manifest.json')
    fs.writeFileSync(manifestPath, JSON.stringify({ items: manifestItems }, null, 2))
    console.log(`   已生成 manifest.json（${manifestItems.length} 条）`)
  }

  return results
}

/**
 * 轮询 + 失败重试（单任务）
 */
async function pollWithRetry(taskId, prompt, options = {}) {
  let currentTaskId = taskId
  let currentPrompt = prompt
  let lastError = null

  for (let attempt = 0; attempt <= Config.maxRetries; attempt++) {
    try {
      if (attempt > 0) {
        currentPrompt = PromptOptimizer.optimize(prompt, lastError, attempt)
        console.log(`\n   🔄 重试 (任务 ${currentTaskId.substring(0, 8)}...): ${currentPrompt.substring(0, 50)}`)
        currentTaskId = await KlingApi.create(
          options.imageUrl || '',
          currentPrompt,
          { duration: options.duration, mode: options.mode, lastFrameUrl: options.lastFrameUrl || '' }
        )
      }

      const result = await KlingApi.poll(currentTaskId)

      const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
      const videoFile = path.join(options.outputDir || './output', `${timestamp}_kling.mp4`)
      await download(result.videoUrl, videoFile)

      return {
        taskId: currentTaskId,
        prompt: currentPrompt,
        originalPrompt: prompt,
        attempts: attempt + 1,
        file: videoFile,
        files: [videoFile],
        duration: 6,
      }
    } catch (err) {
      lastError = err.message
      if (attempt < Config.maxRetries) {
        await new Promise(r => setTimeout(r, 5000))
      }
    }
  }

  throw new Error(`重试 ${Config.maxRetries} 次后仍失败: ${lastError}`)
}

// ============================================================================
// CLI
// ============================================================================

function showHelp() {
  console.log(`
🎬 Kling Video Generator - 图生视频（官方可灵 API）

用法:
  node kling-video-generator.js --image <url> --prompt "指令" [options]
  node kling-video-generator.js --image <url> --last-frame <url> --prompt "过渡" [options]
  node kling-video-generator.js batch <manifest.json> [options]

选项:
  -o, --output <dir>        输出目录 (默认: ./output)
  --duration <s>            视频时长 5 或 10 (默认: 5)
  --mode <mode>             画质 std/pro (默认: std)
  --model <model>           模型名称 (默认: ${Config.model})
  --last-frame <url>        结束帧 URL（首尾帧模式）
  --retries <n>             失败重试次数 (默认: 3)
  -h, --help                帮助

凭证:
  需要在 config.json 中配置:
    kelingApiBaseUrl: "https://api-beijing.klingai.com"
    kelingApiKey: "<你的 Access Key ID>"
    kelingSecretAccessKey: "<你的 Secret Access Key>"
    kelingModel: "kling-v3"

  或使用 klingai skill 的 ~/.config/kling/.credentials
`)
}

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

  if (args.includes('-h') || args.includes('--help') || args.length === 0) {
    showHelp()
    return
  }

  let command = 'single'
  const options = {
    outputDir: './output',
    imageUrl: '',
    lastFrameUrl: '',
    prompt: '',
    duration: 5,
    mode: 'std',
  }

  let i = 0
  if (args[0] === 'batch') {
    command = 'batch'
    i = 1
  }

  const params = []

  while (i < args.length) {
    const arg = args[i]
    if (arg === '-o' || arg === '--output') {
      options.outputDir = args[++i]
    } else if (arg === '--duration') {
      options.duration = parseInt(args[++i], 10)
    } else if (arg === '--mode') {
      options.mode = args[++i]
    } else if (arg === '--model') {
      Config.model = args[++i]
    } else if (arg === '--image') {
      options.imageUrl = args[++i]
    } else if (arg === '--last-frame') {
      options.lastFrameUrl = args[++i]
    } else if (arg === '--prompt') {
      options.prompt = args[++i]
    } else if (arg === '--retries') {
      Config.maxRetries = parseInt(args[++i], 10)
    } else {
      params.push(arg)
    }
    i++
  }

  if (command === 'batch') {
    const filePath = params[0]
    if (!filePath || !fs.existsSync(filePath)) {
      console.error('请提供 manifest.json 路径')
      process.exit(1)
    }
    const tasks = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
    await batchGenerate(tasks, options)
  } else {
    if (!options.imageUrl) {
      console.error('请提供 --image 参数（图片 URL）')
      process.exit(1)
    }
    if (!options.prompt) {
      console.error('请提供 --prompt 参数')
      process.exit(1)
    }
    await generate(options.imageUrl, options.prompt, options)
  }
}

// ============================================================================
// 导出
// ============================================================================

module.exports = { generate, batchGenerate, pollWithRetry, KlingApi, PromptOptimizer }

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