import { Option } from 'commander';
import * as path from 'path';
import * as fs from 'fs';
import { execFileSync, spawnSync } from 'child_process';
import ora from 'ora';
import { success, info } from '../utils/logger.js';
import { outputSuccess, outputError, isPrettyMode } from '../utils/output.js';
import { addEnhancedHelp } from '../utils/helpText.js';
import { getConfigValue } from '../services/config.js';
import { GenerationApiClient, GenerationApiError, validateVideoParams, fileToBase64, downloadResult, resolveOutputPath, } from '../services/generation-api.js';
export function registerVideoCommand(program) {
    const cmd = program
        .command('video')
        .description('生成视频（文生视频 / 图生视频）')
        .argument('[image]', '输入图像路径（默认 dashscope 图生视频必需；纯文生视频暂不可用）')
        .option('-p, --prompt <text>', '视频描述提示词', '')
        .option('-n, --negative <text>', '负向提示词（DashScope）', '')
        .option('-r, --resolution <res>', '分辨率: 720P | 1080P（DashScope）', '1080P')
        .option('-d, --duration <seconds>', '视频时长: 5|10 秒（dashscope / piapi 同样上限）', '5')
        .option('--prompt-extend', '启用智能改写 prompt（DashScope，默认开启）')
        .option('--no-prompt-extend', '禁用智能改写 prompt')
        .option('--audio', '启用自动配音（DashScope，默认开启）')
        .option('--no-audio', '禁用自动配音')
        .option('--audio-url <url>', '自定义音频 URL（DashScope）')
        .option('--shot-type <type>', '镜头类型: single | multi（DashScope）', 'single')
        .option('--watermark', '添加 AI 生成水印（DashScope）')
        .option('--seed <number>', '随机种子')
        // No default — let the backend pick the default provider so this CLI
        // stays version-agnostic. Pass --provider explicitly only to override.
        .addOption(new Option('--provider <name>', '视频生成供应商（不传则由后端选默认）').choices(['kie', 'dashscope', 'piapi', 'heygen']))
        .option('--avatar <id>', 'HeyGen avatar ID')
        .option('--voice <id>', 'HeyGen voice ID')
        .option('--text <text>', 'HeyGen 数字人说话文本')
        .option('--avatar-style <style>', 'HeyGen avatar 样式: normal | circle | closeUp', 'normal')
        .option('--voice-speed <n>', 'HeyGen 语速 (0.5-1.5)', '1.0')
        .option('--voice-emotion <emotion>', 'HeyGen 语音情感: Excited | Friendly | Serious | Soothing | Broadcaster')
        .option('--bg-color <color>', 'HeyGen 背景色 (hex)')
        .option('--bg-image <url>', 'HeyGen 背景图 URL')
        .option('--caption', '启用字幕（HeyGen）')
        .option('--title <title>', '视频标题（HeyGen）')
        .option('--aspect-ratio <ratio>', '宽高比: 16:9 | 9:16 | 1:1 等（PiAPI / DashScope）')
        .option('-o, --output <dir>', '输出目录', '')
        .action(async (image, options) => {
        const pretty = isPrettyMode(options);
        try {
            const duration = parseInt(options.duration);
            const resolution = options.resolution;
            // provider is undefined when --provider not passed — let backend decide.
            const provider = options.provider;
            // HeyGen 数字人视频：独立参数路径
            if (provider === 'heygen') {
                if (!options.avatar) {
                    outputError('MISSING_AVATAR', 'HeyGen 需要 --avatar <id>', options);
                    return;
                }
                if (!options.voice) {
                    outputError('MISSING_VOICE', 'HeyGen 需要 --voice <id>', options);
                    return;
                }
                if (!options.text) {
                    outputError('MISSING_TEXT', 'HeyGen 需要 --text <text>', options);
                    return;
                }
                if (options.text.length > 5000) {
                    outputError('TEXT_TOO_LONG', 'text 不能超过 5000 字符', options);
                    return;
                }
                const speed = parseFloat(options.voiceSpeed);
                if (speed < 0.5 || speed > 1.5) {
                    outputError('INVALID_SPEED', '语速范围 0.5-1.5', options);
                    return;
                }
                const params = {
                    provider: 'heygen',
                    avatar_id: options.avatar,
                    voice_id: options.voice,
                    text: options.text,
                    avatar_style: options.avatarStyle,
                    speed,
                    emotion: options.voiceEmotion,
                    bg_color: options.bgColor,
                    bg_image: options.bgImage,
                    caption: options.caption,
                    title: options.title,
                };
                const client = new GenerationApiClient();
                const defaultDir = getConfigValue('outputDir') || './gen-output';
                const spinner = pretty ? ora('正在提交数字人视频生成任务...').start() : null;
                if (pretty)
                    spinner.text = '正在生成数字人视频（预计 2-5 分钟）...';
                const result = await client.submitAndPoll('video', params, {
                    onProgress: (status) => {
                        if (pretty)
                            spinner.text = `数字人视频生成中 (${status})...`;
                    },
                });
                if (result.status === 'completed' && result.result_url) {
                    const defaultFilename = `avatar_${result.task_id.slice(0, 8)}.mp4`;
                    const { dir, filename } = resolveOutputPath(options.output, defaultDir, defaultFilename);
                    const outputPath = await downloadResult(result.result_url, dir, filename);
                    if (pretty) {
                        spinner.succeed('数字人视频生成完成！');
                        success(`输出文件: ${outputPath}`);
                        info(`任务 ID: ${result.task_id}`);
                    }
                    else {
                        outputSuccess({
                            task_id: result.task_id,
                            status: result.status,
                            output: outputPath,
                            result_url: result.result_url,
                        });
                    }
                }
                else {
                    // task status != completed → 失败。让顶层 success:false 并 exit 1，否则
                    // Agent 会把 failure 当 success（参见 image.ts 同样修复）。
                    const errCode = result.error_code ?? 'GENERATION_FAILED';
                    const errMsg = result.error_message ?? `任务状态: ${result.status}`;
                    if (pretty) {
                        spinner.fail(errMsg);
                        if (result.error_code)
                            info(`错误码: ${result.error_code}${result.retryable ? '（可重试）' : ''}`);
                        info(`使用 'gen task get ${result.task_id}' 查看最新状态`);
                        process.exit(1);
                    }
                    else {
                        outputError(errCode, errMsg, options, {
                            task_id: result.task_id,
                            status: result.status,
                            retryable: result.retryable ?? false,
                            error_class: result.error_class ?? null,
                        });
                    }
                }
                return;
            }
            let imageBase64;
            if (image) {
                const imagePath = path.resolve(image);
                if (!fs.existsSync(imagePath)) {
                    outputError('IMAGE_NOT_FOUND', `图像文件不存在: ${imagePath}`, options);
                    return;
                }
                imageBase64 = await fileToBase64(imagePath);
            }
            // CLI 层快速校验
            if (provider === 'dashscope' && !imageBase64) {
                outputError('MISSING_IMAGE', '该供应商视频生成需要输入图像', options);
                return;
            }
            if (!options.prompt && !imageBase64) {
                outputError('MISSING_INPUT', '请提供 prompt 或输入图像（至少一个）', options);
                return;
            }
            validateVideoParams({ duration, resolution, provider, image: imageBase64 });
            const params = {
                image: imageBase64,
                prompt: options.prompt || undefined,
                negative_prompt: options.negative || undefined,
                resolution: resolution,
                duration,
                prompt_extend: options.promptExtend,
                audio: options.audio,
                audio_url: options.audioUrl,
                shot_type: options.shotType,
                watermark: options.watermark || false,
                seed: options.seed ? parseInt(options.seed) : undefined,
                // Only include provider when explicitly set, so backend default wins.
                ...(provider ? { provider } : {}),
                aspectRatio: options.aspectRatio,
            };
            const client = new GenerationApiClient();
            const defaultDir = getConfigValue('outputDir') || './gen-output';
            const spinner = pretty ? ora('正在提交视频生成任务...').start() : null;
            if (pretty)
                spinner.text = '正在生成视频（预计 1-5 分钟）...';
            const result = await client.submitAndPoll('video', params, {
                onProgress: (status) => {
                    if (pretty)
                        spinner.text = `视频生成中 (${status})...`;
                },
            });
            if (result.status === 'completed' && result.result_url) {
                const defaultFilename = `video_${result.task_id.slice(0, 8)}.mp4`;
                const { dir, filename } = resolveOutputPath(options.output, defaultDir, defaultFilename);
                const outputPath = await downloadResult(result.result_url, dir, filename);
                if (pretty) {
                    spinner.succeed('视频生成完成！');
                    success(`输出文件: ${outputPath}`);
                    info(`任务 ID: ${result.task_id}`);
                }
                else {
                    outputSuccess({
                        task_id: result.task_id,
                        status: result.status,
                        output: outputPath,
                        result_url: result.result_url,
                    });
                }
            }
            else {
                // task status != completed → 失败。让顶层 success:false 并 exit 1，否则
                // Agent 会把 failure 当 success（参见 image.ts 同样修复）。
                const errCode = result.error_code ?? 'GENERATION_FAILED';
                const errMsg = result.error_message ?? `任务状态: ${result.status}`;
                if (pretty) {
                    spinner.fail(errMsg);
                    if (result.error_code)
                        info(`错误码: ${result.error_code}${result.retryable ? '（可重试）' : ''}`);
                    info(`使用 'gen task get ${result.task_id}' 查看最新状态`);
                    process.exit(1);
                }
                else {
                    outputError(errCode, errMsg, options, {
                        task_id: result.task_id,
                        status: result.status,
                        retryable: result.retryable ?? false,
                        error_class: result.error_class ?? null,
                    });
                }
            }
        }
        catch (err) {
            if (err instanceof GenerationApiError) {
                outputError(err.code, err.message, options);
            }
            else {
                outputError('VIDEO_FAILED', `生成失败: ${err}`, options);
            }
        }
    });
    addEnhancedHelp(cmd, {
        examples: [
            '# 图生视频（默认 dashscope，需要输入图像）',
            '$ gen video cat.png --prompt "cat walking"',
            '',
            '# 高清长视频',
            '$ gen video cat.png --duration 10 --resolution 1080P',
            '',
            '# 指定宽高比',
            '$ gen video cat.png --prompt "panning shot" --aspect-ratio 16:9',
            '',
            '# 数字人口播（HeyGen）',
            '$ gen video --provider heygen --avatar <avatar_id> --voice <voice_id> --text "Hello world"',
            '',
            '# 数字人带情感和背景色',
            '$ gen video --provider heygen --avatar <id> --voice <id> --text "Hi" --voice-emotion Friendly --bg-color "#1a1a2e"',
        ],
        outputJson: `{
  "success": true,
  "data": {
    "task_id": "abc-123-def",
    "status": "completed",
    "output": "./gen-output/video_abc123.mp4"
  }
}`,
        relatedCommands: [
            { command: 'task get <id>', description: '查看任务状态' },
            { command: 'image <prompt>', description: '生成图像' },
            { command: 'video stitch', description: '拼接多段 mp4（ffmpeg concat）' },
        ],
        notes: [
            '默认 dashscope（仅图生视频，需要输入图像；自动配音默认开启）',
            '`--provider piapi` 文生 / 图生都支持，但当前账户余额不足暂时不可用，恢复后可切回',
            '`--provider heygen` 生成数字人口播视频（需要 avatar_id + voice_id + text）',
            '需要 Optima 账号（~/.optima/token.json 或 OPTIMA_TOKEN）',
            '视频生成需要 1-5 分钟',
        ],
    });
    registerVideoStitch(cmd);
    registerVideoMotionControl(cmd);
}
/**
 * `gen video motion-control` — pose/motion transfer from reference video onto
 * a still character image (kie.ai kling-2.6/motion-control).
 *
 * Two file inputs (image + reference video). Each accepts a local file path OR
 * an http(s) URL. Local files are base64-encoded; URLs are forwarded as-is for
 * the gen service to rehost on kie's CDN (kie rejects non-kie hosts here).
 *
 * Duration is required for pre-deduct billing ($0.10/s) but we auto-probe the
 * reference video with ffprobe so the caller (agent / user) doesn't have to.
 * --duration overrides the probe if set.
 *
 * Mode is locked to 720p server-side; not exposed here.
 *
 * Returns JSON (success): { task_id, status: "completed", output, result_url }
 * Returns JSON (failed):  { error: { code, message, ... } }
 */
function registerVideoMotionControl(parent) {
    const cmd = parent
        .command('motion-control')
        .description('用参考视频的动作驱动一张人物图（kling motion-control，720p，$0.10/s）')
        .requiredOption('--image <path|url>', '角色参考图（jpeg/png，≥300px，aspect 2:5..5:2，≤10MB）')
        .requiredOption('--reference-video <path|url>', '动作参考视频（mp4/mov/mkv，3-30s，≤100MB）')
        .option('--duration <seconds>', '视频时长（覆盖 ffprobe 自动检测；3-30；character-orientation=image 时 ≤10）')
        .option('--character-orientation <kind>', 'image: 输出 ≤10s | video: 跟参考视频长度（默认 video）')
        .option('-p, --prompt <text>', '可选风格提示词，≤2500 字符', '')
        .option('-o, --output <dir>', '输出目录', '')
        .action(async (options) => {
        const pretty = isPrettyMode(options);
        try {
            // Resolve image (path → base64; URL → passthrough)
            const image = await resolveFileOrUrl(options.image, 'image');
            if (image === null) {
                outputError('IMAGE_NOT_FOUND', `角色参考图不存在: ${options.image}`, options);
                return;
            }
            // Resolve reference video + duration probe
            const refVideo = await resolveFileOrUrl(options.referenceVideo, 'video');
            if (refVideo === null) {
                outputError('REFERENCE_VIDEO_NOT_FOUND', `参考视频不存在: ${options.referenceVideo}`, options);
                return;
            }
            let duration;
            if (options.duration) {
                duration = parseFloat(options.duration);
                if (!Number.isFinite(duration)) {
                    outputError('INVALID_DURATION', '--duration 必须是数字', options);
                    return;
                }
            }
            else {
                // Auto-probe from the local file (skip probe for URLs — caller must pass --duration)
                if (/^https?:\/\//i.test(options.referenceVideo)) {
                    outputError('MISSING_DURATION', '--reference-video 是 URL 时必须显式 --duration <seconds>', options);
                    return;
                }
                const probed = ffprobeDuration(path.resolve(options.referenceVideo));
                if (probed <= 0) {
                    outputError('PROBE_FAILED', `ffprobe 无法读取参考视频时长，请显式传 --duration: ${options.referenceVideo}`, options);
                    return;
                }
                // Round to 1 decimal for cleaner billing dim
                duration = Math.round(probed * 10) / 10;
            }
            if (duration < 3 || duration > 30) {
                outputError('DURATION_OUT_OF_RANGE', `duration 需在 3-30 秒之间（当前 ${duration}）`, options);
                return;
            }
            const charOrient = options.characterOrientation;
            if (charOrient === 'image' && duration > 10) {
                outputError('DURATION_OUT_OF_RANGE', `character-orientation=image 时 duration 上限为 10 秒（参考视频 ${duration}s 超限，请改成 video 或截短参考视频）`, options);
                return;
            }
            if (charOrient !== undefined && !['image', 'video'].includes(charOrient)) {
                outputError('INVALID_CHARACTER_ORIENTATION', '--character-orientation 仅支持 image 或 video', options);
                return;
            }
            const params = {
                image,
                reference_video: refVideo,
                duration,
                ...(charOrient ? { character_orientation: charOrient } : {}),
                ...(options.prompt ? { prompt: options.prompt } : {}),
            };
            const client = new GenerationApiClient();
            const defaultDir = getConfigValue('outputDir') || './gen-output';
            const spinner = pretty ? ora('正在提交 motion-control 任务...').start() : null;
            if (pretty)
                spinner.text = `正在生成动作驱动视频（${duration}s，~$${(duration * 0.1).toFixed(2)}，预计 2-5 分钟）...`;
            const result = await client.submitAndPoll('video-motion-control', params, {
                onProgress: (status) => {
                    if (pretty)
                        spinner.text = `视频生成中 (${status})...`;
                },
            });
            if (result.status === 'completed' && result.result_url) {
                const defaultFilename = `motion_${result.task_id.slice(0, 8)}.mp4`;
                const { dir, filename } = resolveOutputPath(options.output, defaultDir, defaultFilename);
                const outputPath = await downloadResult(result.result_url, dir, filename);
                if (pretty) {
                    spinner.succeed('动作驱动视频生成完成！');
                    success(`输出文件: ${outputPath}`);
                    info(`任务 ID: ${result.task_id}`);
                }
                else {
                    outputSuccess({
                        task_id: result.task_id,
                        status: result.status,
                        output: outputPath,
                        result_url: result.result_url,
                    });
                }
            }
            else {
                const errCode = result.error_code ?? 'GENERATION_FAILED';
                const errMsg = result.error_message ?? `任务状态: ${result.status}`;
                if (pretty) {
                    spinner.fail(errMsg);
                    if (result.error_code)
                        info(`错误码: ${result.error_code}${result.retryable ? '（可重试）' : ''}`);
                    info(`使用 'gen task get ${result.task_id}' 查看最新状态`);
                    process.exit(1);
                }
                else {
                    outputError(errCode, errMsg, options, {
                        task_id: result.task_id,
                        status: result.status,
                        retryable: result.retryable ?? false,
                        error_class: result.error_class ?? null,
                    });
                }
            }
        }
        catch (err) {
            if (err instanceof GenerationApiError) {
                outputError(err.code, err.message, options);
            }
            else {
                outputError('MOTION_CONTROL_FAILED', `生成失败: ${err}`, options);
            }
        }
    });
    addEnhancedHelp(cmd, {
        examples: [
            '# 最常见用法：本地图 + 本地视频，duration 自动 ffprobe',
            '$ gen video motion-control --image character.jpg --reference-video dance.mp4',
            '',
            '# 角色形象保持优先（输出会被截到 ≤10s）',
            '$ gen video motion-control --image character.jpg --reference-video dance.mp4 --character-orientation image',
            '',
            '# 参考视频用 URL 时必须显式传 duration',
            '$ gen video motion-control --image character.jpg --reference-video https://cdn.x.com/y.mp4 --duration 8',
            '',
            '# 附风格提示词',
            '$ gen video motion-control --image character.jpg --reference-video dance.mp4 --prompt "anime style"',
        ],
        outputJson: `{
  "success": true,
  "data": {
    "task_id": "abc-123",
    "status": "completed",
    "output": "./gen-output/motion_abc12345.mp4"
  }
}`,
        relatedCommands: [
            { command: 'video', description: '常规视频生成（文/图生视频）' },
            { command: 'task get <id>', description: '查看任务状态' },
        ],
        notes: [
            '720p only（服务端锁定，CLI 不暴露 --mode）',
            '$0.10/s 用户侧定价；按 duration 预扣',
            '参考视频建议 3-30s；超 30s 服务端会拒；character-orientation=image 时上限 10s',
            '角色图约束：jpeg/png，≥300px，aspect 2:5..5:2，≤10MB',
            '需要 Optima 账号（~/.optima/token.json 或 OPTIMA_TOKEN）',
        ],
    });
}
/**
 * Resolve an input spec into the form gen-api expects:
 *   - http(s) URL  → passthrough (string)
 *   - local file   → base64
 *   - missing path → null (caller should outputError)
 */
async function resolveFileOrUrl(spec, _kind) {
    if (/^https?:\/\//i.test(spec))
        return spec;
    const resolved = path.resolve(spec);
    if (!fs.existsSync(resolved))
        return null;
    return fileToBase64(resolved);
}
/**
 * `gen video stitch` —— 多段 mp4 拼接 (ffmpeg concat) primitive.
 *
 * 实施 SPEC: docs/superpowers/specs/2026-05-16-gen-video-stitch-primitive.md (optima-agent)
 *
 * 关键设计:
 * - Mode A (--workspace-dir): glob <dir>/segments/seg-*.mp4 按文件名 lexical sort
 * - Mode B (--inputs): 显式 mp4 列表,用户给的顺序
 * - --codec auto (默认): 试 -c copy → fail 回退 libx264+aac
 * - 失败 failed_input 探测 3 层: stderr parse → ffprobe scan → null + codec mismatch
 * - concat list 用固定路径 <workspace>/.stitch-concat.txt,不用 mktemp
 *
 * 返回 JSON (成功): { output_path, duration_sec, concat_mode, input_count }
 * 返回 JSON (失败): { error: { code, message, failed_input } }
 */
function registerVideoStitch(parent) {
    const cmd = parent
        .command('stitch')
        .description('拼接多段 mp4 为单个视频（ffmpeg concat）')
        .option('--workspace-dir <dir>', 'Mode A: 扫描 <dir>/segments/seg-*.mp4 按文件名排序')
        .option('--inputs <inputs...>', 'Mode B: 显式 mp4 列表 (顺序 = concat 顺序)')
        // -o 短 flag 跟父命令 gen video -o <dir> 冲突 (commander 限制),subcommand 用长 flag --out
        // SPEC §4.1 用 -o 是设计期写法,实施时改 --out 不影响契约 (wrapper 调时跟 SPEC 实施版本一致即可)
        .requiredOption('--out <path>', '输出 mp4 路径')
        .addOption(new Option('--codec <mode>', 'codec 模式: auto 试 copy 失败回退 reencode')
        .choices(['auto', 'copy', 'reencode'])
        .default('auto'))
        .action(async (options) => {
        const pretty = isPrettyMode(options);
        try {
            // P2-3: ffmpeg / ffprobe PATH 预检
            if (!hasFfmpegBinary('ffmpeg')) {
                outputError('STITCH_FFMPEG_NOT_FOUND', '未找到 ffmpeg 命令。请先安装 ffmpeg (https://ffmpeg.org/) 并加入 PATH', options);
                return;
            }
            if (!hasFfmpegBinary('ffprobe')) {
                outputError('STITCH_FFPROBE_NOT_FOUND', '未找到 ffprobe 命令 (ffmpeg 套件)。请确认 ffmpeg 完整安装并加入 PATH', options);
                return;
            }
            // Mode A vs Mode B 互斥校验
            const hasWorkspace = !!options.workspaceDir;
            const hasInputs = Array.isArray(options.inputs) && options.inputs.length > 0;
            if (hasWorkspace && hasInputs) {
                outputError('STITCH_USAGE_ERROR', '--workspace-dir 和 --inputs 二选一,不能同时传', options);
                return;
            }
            if (!hasWorkspace && !hasInputs) {
                outputError('STITCH_USAGE_ERROR', '必须传 --workspace-dir <dir> 或 --inputs <p1> <p2> ...', options);
                return;
            }
            // --codec 校验由 commander .choices() 处理,这里直接读 (无效值 commander 已 reject)
            const codec = options.codec;
            // 解析 inputs (Mode A glob 或 Mode B 直接用)
            let inputs;
            if (hasWorkspace) {
                const segmentsDir = path.join(path.resolve(options.workspaceDir), 'segments');
                if (!fs.existsSync(segmentsDir)) {
                    outputError('STITCH_FAILED', `segments 目录不存在: ${segmentsDir}`, options);
                    return;
                }
                inputs = fs
                    .readdirSync(segmentsDir)
                    .filter((f) => /^seg-\d+\.mp4$/.test(f))
                    // natural sort: 不依赖填充宽度,seg-2 < seg-10 < seg-100 都对
                    // (lexical sort 在混 1/2/3 位时会错乱:seg-1 < seg-10 < seg-100 < seg-2)
                    .sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
                    .map((f) => path.join(segmentsDir, f));
            }
            else {
                inputs = options.inputs.map((p) => path.resolve(p));
            }
            if (inputs.length === 0) {
                outputError('STITCH_FAILED', 'inputs 为空,没有 mp4 可拼接', options);
                return;
            }
            // 验证 inputs 存在
            for (const input of inputs) {
                if (!fs.existsSync(input)) {
                    outputError('STITCH_FAILED', `input 文件不存在: ${input}`, options, { failed_input: input });
                    return;
                }
            }
            const outputPath = path.resolve(options.out);
            // concat list 临时文件位置 (SPEC 决策 #11):
            // - Mode A: <workspace>/.stitch-concat.txt
            // - Mode B 无 workspace 概念,落 output dir 同卷,避免跨 fs 问题
            // - 假设单作业,并发场景需各自 workspace 或换 mktemp (本 v1 不做)
            const concatListPath = path.join(hasWorkspace ? path.resolve(options.workspaceDir) : path.dirname(outputPath), '.stitch-concat.txt');
            const spinner = pretty ? ora(`拼接 ${inputs.length} 段 mp4...`).start() : null;
            // 生成 concat list (ffmpeg concat demuxer 格式)
            const concatContent = inputs.map((p) => `file '${p.replace(/'/g, "'\\''")}'`).join('\n') + '\n';
            fs.writeFileSync(concatListPath, concatContent);
            try {
                // 执行 ffmpeg,根据 --codec 决定路径
                let concatMode;
                let ffmpegError = null;
                if (codec === 'copy') {
                    // 只试 copy,失败直接报错
                    const r = runFfmpegCopy(concatListPath, outputPath);
                    if (r.ok) {
                        concatMode = 'copy';
                    }
                    else {
                        ffmpegError = r.stderr;
                        concatMode = 'copy'; // 标记,但下面会进 error path
                    }
                }
                else if (codec === 'reencode') {
                    // 直接 reencode,跳过 copy 试探
                    const r = runFfmpegReencode(concatListPath, outputPath);
                    if (r.ok) {
                        concatMode = 'reencode';
                    }
                    else {
                        ffmpegError = r.stderr;
                        concatMode = 'reencode';
                    }
                }
                else {
                    // auto: 试 copy → fail 回退 reencode
                    const r1 = runFfmpegCopy(concatListPath, outputPath);
                    if (r1.ok) {
                        concatMode = 'copy';
                    }
                    else {
                        const r2 = runFfmpegReencode(concatListPath, outputPath);
                        if (r2.ok) {
                            concatMode = 'reencode';
                        }
                        else {
                            ffmpegError = r2.stderr;
                            concatMode = 'reencode';
                        }
                    }
                }
                if (ffmpegError) {
                    // 3 层 failed_input 探测
                    const failedInput = detectFailedInput(ffmpegError, inputs);
                    if (pretty)
                        spinner.fail('ffmpeg concat 失败');
                    outputError('STITCH_FAILED', failedInput
                        ? `ffmpeg concat 失败 (failing input: ${failedInput})`
                        : 'ffmpeg concat 失败:所有 input 单独 ffprobe OK,但 concat 失败 (likely codec mismatch)', options, { failed_input: failedInput });
                    return;
                }
                // 成功:ffprobe 取 duration
                const duration = ffprobeDuration(outputPath);
                if (pretty) {
                    spinner.succeed(`拼接完成 (${concatMode}): ${outputPath} [${duration.toFixed(2)}s, ${inputs.length} 段]`);
                }
                else {
                    outputSuccess({
                        output_path: outputPath,
                        duration_sec: duration,
                        concat_mode: concatMode,
                        input_count: inputs.length,
                    });
                }
            }
            finally {
                // 清理临时 concat list
                if (fs.existsSync(concatListPath)) {
                    try {
                        fs.unlinkSync(concatListPath);
                    }
                    catch {
                        /* ignore */
                    }
                }
            }
        }
        catch (err) {
            outputError('STITCH_FAILED', `拼接过程异常: ${err}`, options);
        }
    });
    addEnhancedHelp(cmd, {
        examples: [
            '# Mode A: 自动扫描 workspace segments/',
            '$ gen video stitch --workspace-dir ~/digital-human/videos/20260516-1430 --out final.mp4',
            '# Mode B: 显式列 mp4 (任意顺序)',
            '$ gen video stitch --inputs intro.mp4 seg-1.mp4 seg-2.mp4 outro.mp4 --out final.mp4',
            '# 强制重编码 (保证 codec 兼容)',
            '$ gen video stitch --workspace-dir . --out final.mp4 --codec reencode',
        ],
        outputJson: `{
  "success": true,
  "data": {
    "output_path": "/abs/path/final.mp4",
    "duration_sec": 87.3,
    "concat_mode": "copy",
    "input_count": 4
  }
}`,
        relatedCommands: [
            { command: 'video', description: '生成单段视频' },
        ],
        notes: [
            'Mode A / Mode B 二选一,不能同时传',
            '--codec auto (默认): 试 -c copy → fail 回退 libx264+aac,JSON 返回实际 concat_mode',
            '失败时 failed_input 字段指出 broken mp4 (若可定位)',
            'concat list 临时文件用完即删,无残留',
        ],
    });
}
// ===== Helpers (exported for unit tests) =====
export function hasFfmpegBinary(name) {
    const r = spawnSync(name, ['-version'], { encoding: 'utf-8' });
    return r.status === 0;
}
export function runFfmpegCopy(concatListPath, outputPath) {
    const r = spawnSync('ffmpeg', ['-y', '-f', 'concat', '-safe', '0', '-i', concatListPath, '-c', 'copy', outputPath], { encoding: 'utf-8' });
    return { ok: r.status === 0, stderr: r.stderr || '' };
}
export function runFfmpegReencode(concatListPath, outputPath) {
    const r = spawnSync('ffmpeg', [
        '-y',
        '-f',
        'concat',
        '-safe',
        '0',
        '-i',
        concatListPath,
        '-c:v',
        'libx264',
        '-preset',
        'fast',
        '-c:a',
        'aac',
        outputPath,
    ], { encoding: 'utf-8' });
    return { ok: r.status === 0, stderr: r.stderr || '' };
}
export function ffprobeDuration(filePath) {
    try {
        const out = execFileSync('ffprobe', ['-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', filePath], { encoding: 'utf-8' });
        const d = parseFloat(out.trim());
        return Number.isFinite(d) ? d : 0;
    }
    catch {
        return 0;
    }
}
/**
 * 3 层 failed_input 探测(SPEC 决策 #9):
 * 1) ffmpeg stderr 含 input 路径字面 → 再 ffprobe verify 一次,验证 fail 才返回
 *    (P2-2 修正:ffmpeg `Input #N` 默认会把所有 input 名字打 stderr,
 *    Layer 1 命中不等于该 input 真坏;必须 ffprobe verify 避免 false positive)
 * 2) 否则 ffprobe scan 全部 input,首个 exit≠0 = failed_input
 * 3) 都 ffprobe OK 但 concat 仍失败 → null (codec mismatch,非单个 input 问题)
 */
export function detectFailedInput(stderr, inputs) {
    // Layer 1: stderr 含具体 input 路径 + ffprobe 验证
    for (const input of inputs) {
        if (stderr.includes(input) || stderr.includes(path.basename(input))) {
            const r = spawnSync('ffprobe', ['-v', 'error', input], { encoding: 'utf-8' });
            if (r.status !== 0 || (r.stderr && r.stderr.trim() !== '')) {
                return input; // 验证确认坏
            }
            // stderr 提了 input 但 ffprobe OK → 可能只是 Input #N 标识,降级 Layer 2
        }
    }
    // Layer 2: ffprobe scan 每个 input
    for (const input of inputs) {
        const r = spawnSync('ffprobe', ['-v', 'error', input], { encoding: 'utf-8' });
        if (r.status !== 0 || (r.stderr && r.stderr.trim() !== '')) {
            return input;
        }
    }
    // Layer 3: 都 OK 但 concat fail → 不是单个 input 问题
    return null;
}
//# sourceMappingURL=video.js.map