import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import axios from 'axios';
import ora from 'ora';
import { spawnSync } from 'child_process';
import { success, info } from '../utils/logger.js';
import { outputSuccess, outputError, isPrettyMode } from '../utils/output.js';
import { addEnhancedHelp } from '../utils/helpText.js';
import { hasFfmpegBinary, ffprobeDuration } from './video.js';
import { GenerationApiClient, GenerationApiError } from '../services/generation-api.js';
/**
 * Concatenate audio segments into a single continuous track for audio-driven
 * doctor-video (spec §4.2). Uses the ffmpeg concat FILTER (decode + re-encode),
 * NOT the concat demuxer with `-c copy`: mp3 frames carry encoder priming/gap,
 * and stream-copy concat bakes those gap-frame timestamps into the track (the
 * same class of seam artifact as video stitch). The filter decodes every input
 * and re-encodes to one sample-continuous stream — CBR libmp3lame for .mp3,
 * lossless PCM for .wav.
 */
export function runFfmpegAudioConcat(inputs, outputPath) {
    const filter = inputs.map((_, i) => `[${i}:a]`).join('') + `concat=n=${inputs.length}:v=0:a=1[out]`;
    const isWav = outputPath.toLowerCase().endsWith('.wav');
    const encoder = isWav ? ['-c:a', 'pcm_s16le'] : ['-c:a', 'libmp3lame', '-b:a', '192k'];
    const inputArgs = inputs.flatMap((p) => ['-i', p]);
    const r = spawnSync('ffmpeg', ['-y', ...inputArgs, '-filter_complex', filter, '-map', '[out]', ...encoder, outputPath], { encoding: 'utf-8' });
    return { ok: r.status === 0, stderr: r.stderr || '' };
}
/**
 * `gen voice clone <audio-file> --external-id <id>` — train a HeyGen instant voice clone
 * from a local audio file. CLI handles the asset upload + submit + poll, then prints voice_id.
 *
 * Phase 2 doctor-avatar feature. See spec §13 in optima-doctor-avatar repo.
 */
export function registerVoiceCommand(program) {
    const voice = program.command('voice').description('医生 voice clone 相关命令(Phase 2)');
    const cloneCmd = voice
        .command('clone')
        .description('用本地音频文件训练 HeyGen voice clone,产出 voice_id')
        .argument('<audio-file>', '音频文件路径(.wav / .mp3 等,30-90s 推荐)')
        .requiredOption('--external-id <id>', '业务方医生标识(写入 doctor_avatar_profiles)')
        .option('--name <name>', 'HeyGen 侧 voice name(默认 doctor-voice)')
        .action(async (audioFile, options) => {
        const pretty = isPrettyMode(options);
        try {
            const audioPath = path.resolve(audioFile);
            if (!fs.existsSync(audioPath)) {
                outputError('AUDIO_NOT_FOUND', `音频文件不存在: ${audioPath}`, options);
                return;
            }
            const client = new GenerationApiClient();
            // Step 1: upload audio asset
            const upSpinner = pretty ? ora('上传音频到 HeyGen 资产存储...').start() : null;
            const asset = await client.uploadHeyGenAsset(audioPath);
            if (pretty)
                upSpinner.succeed(`音频已上传 (asset_id=${asset.asset_id}, ${(asset.size_bytes / 1024).toFixed(1)} KB)`);
            // Step 2: submit voice-clone task
            const trainSpinner = pretty ? ora('提交 voice clone 训练任务...').start() : null;
            const result = await client.submitAndPoll('voice-clone', {
                audio_asset_id: asset.asset_id,
                external_id: options.externalId,
                voice_name: options.name,
            }, {
                timeout: 900000, // 15 min — voice clone usually 30s-2min but allow buffer
                onProgress: (status) => {
                    if (pretty)
                        trainSpinner.text = `voice clone 训练中 (${status})...`;
                },
            });
            if (result.status === 'completed') {
                const voiceId = result.result_meta?.voice_id;
                if (pretty) {
                    trainSpinner.succeed('voice clone 完成!');
                    success(`voice_id: ${voiceId}`);
                    info(`task_id: ${result.task_id}`);
                    info(`external_id: ${options.externalId}`);
                    info(`使用: gen doctor profile ${options.externalId} 查看完整 profile`);
                }
                else {
                    outputSuccess({
                        task_id: result.task_id,
                        status: result.status,
                        voice_id: voiceId,
                        external_id: options.externalId,
                    });
                }
            }
            else {
                if (pretty) {
                    trainSpinner.info(`任务状态: ${result.status}`);
                    if (result.error_message)
                        info(`错误: ${result.error_message}`);
                    info(`使用 'gen task get ${result.task_id}' 查看最新状态`);
                }
                else {
                    outputSuccess({
                        task_id: result.task_id,
                        status: result.status,
                        error_code: result.error_code ?? null,
                        retryable: result.retryable ?? false,
                        error_message: result.error_message,
                    });
                }
            }
        }
        catch (err) {
            if (err instanceof GenerationApiError) {
                outputError(err.code, err.message, options);
            }
            else {
                outputError('VOICE_CLONE_FAILED', `voice clone 失败: ${err}`, options);
            }
        }
    });
    addEnhancedHelp(cloneCmd, {
        examples: [
            '# 训练医生 voice clone',
            '$ gen voice clone ./doctor-audio.wav --external-id doc-123',
            '',
            '# 自定义 voice name',
            '$ gen voice clone ./audio.wav --external-id doc-123 --name "Dr. Wang voice"',
        ],
        outputJson: `{
  "success": true,
  "data": {
    "task_id": "task-uuid",
    "status": "completed",
    "voice_id": "f7a8ea9f...",
    "external_id": "doc-123"
  }
}`,
        relatedCommands: [
            { command: 'avatar train <video-file> --external-id <id>', description: '训练 avatar' },
            { command: 'doctor onboard <audio> <video> --external-id <id>', description: '一次同时训 voice + avatar' },
            { command: 'doctor profile <external-id>', description: '查看医生 profile' },
        ],
        notes: [
            '推荐音频时长 30-90 秒,清晰人声(单声道,降噪后)',
            '训练时长通常 30 秒到 2 分钟',
            '完成后 voice_id 会持久化到 doctor_avatar_profiles 表,用 external_id 查询',
        ],
    });
    const previewCmd = voice
        .command('preview')
        .description('用已克隆的 voice_id 试听一段文本 (audio-only, 不消耗 video credit)')
        .requiredOption('--voice <id>', 'HeyGen voice_id (来自 voice clone)')
        .requiredOption('--text <text>', '要合成的文本')
        .option('--text-type <type>', "text 或 ssml (默认 text)", 'text')
        .option('--output <path>', `音频输出路径 (默认 ~/digital-human/previews/<task_id>.wav)`)
        .action(async (options) => {
        const pretty = isPrettyMode(options);
        try {
            const client = new GenerationApiClient();
            const spinner = pretty ? ora('提交 voice preview 任务...').start() : null;
            const result = await client.submitAndPoll('voice-preview', {
                voice_id: options.voice,
                text: options.text,
                text_type: options.textType,
            }, {
                timeout: 60000, // HeyGen preview 通常 1-2s; 60s buffer 足够
                onProgress: (status) => {
                    if (pretty)
                        spinner.text = `voice preview (${status})...`;
                },
            });
            if (result.status !== 'completed') {
                if (pretty) {
                    spinner.fail(`任务失败: ${result.status}`);
                    if (result.error_message)
                        info(`错误: ${result.error_message}`);
                }
                else {
                    outputSuccess({
                        task_id: result.task_id,
                        status: result.status,
                        error_code: result.error_code ?? null,
                        retryable: result.retryable ?? false,
                        error_message: result.error_message,
                    });
                }
                return;
            }
            const audioUrl = result.result_url;
            if (!audioUrl) {
                outputError('VOICE_PREVIEW_FAILED', '任务完成但未返回 audio_url', options);
                return;
            }
            // Download audio to local (SPEC #120 §6.4.1 option A: CLI-side download,
            // avoids HeyGen CDN TTL risk + worker/CLI cross-filesystem mismatch).
            //
            // Task is already completed + billed at this point. If download fails,
            // surface audio_url + task_id so user can curl-retrieve manually — do
            // NOT throw, because user's only recovery would be re-running this
            // command which re-charges another credit (double-charge bug).
            if (pretty)
                spinner.text = '下载音频到本地...';
            const outputPath = options.output
                ? path.resolve(options.output)
                : path.join(os.homedir(), 'digital-human', 'previews', `${result.task_id}.wav`);
            fs.mkdirSync(path.dirname(outputPath), { recursive: true });
            let downloadOk = false;
            let downloadErr = null;
            for (let attempt = 1; attempt <= 2; attempt++) {
                try {
                    const audioResp = await axios.get(audioUrl, { responseType: 'arraybuffer', timeout: 60000 });
                    fs.writeFileSync(outputPath, Buffer.from(audioResp.data));
                    downloadOk = true;
                    break;
                }
                catch (err) {
                    downloadErr = err;
                    if (attempt < 2)
                        await new Promise((r) => setTimeout(r, 1000));
                }
            }
            const meta = result.result_meta || {};
            if (!downloadOk) {
                // Surface audio_url so user can wget without re-submitting (re-submit = re-bill).
                if (pretty) {
                    spinner.warn('音频下载失败，但任务已完成 — 不要重跑命令（会被双扣费）');
                    info(`audio_url: ${audioUrl}`);
                    info(`task_id: ${result.task_id}`);
                    info(`手动下载: curl -o "${outputPath}" "${audioUrl}"`);
                    info(`错误: ${downloadErr}`);
                }
                else {
                    outputSuccess({
                        task_id: result.task_id,
                        status: result.status,
                        audio_path: null,
                        audio_url: audioUrl,
                        download_failed: true,
                        download_error: String(downloadErr),
                        duration: meta.duration,
                        word_timestamps: meta.word_timestamps,
                        is_pass: meta.is_pass,
                        is_silence: meta.is_silence,
                    });
                }
                return;
            }
            if (pretty) {
                spinner.succeed('voice preview 完成');
                success(`audio: ${outputPath}`);
                info(`duration: ${meta.duration ?? '?'}s`);
                info(`words: ${(meta.word_timestamps ?? []).length}`);
                info(`task_id: ${result.task_id}`);
            }
            else {
                outputSuccess({
                    task_id: result.task_id,
                    status: result.status,
                    audio_path: outputPath,
                    audio_url: audioUrl,
                    duration: meta.duration,
                    word_timestamps: meta.word_timestamps,
                    is_pass: meta.is_pass,
                    is_silence: meta.is_silence,
                });
            }
        }
        catch (err) {
            if (err instanceof GenerationApiError) {
                outputError(err.code, err.message, options);
            }
            else {
                outputError('VOICE_PREVIEW_FAILED', `voice preview 失败: ${err}`, options);
            }
        }
    });
    addEnhancedHelp(previewCmd, {
        examples: [
            '# 试听一段文本',
            '$ gen voice preview --voice abc123 --text "大家好,我是王医生"',
            '',
            '# 指定输出路径',
            '$ gen voice preview --voice abc123 --text "..." --output ./preview.wav',
        ],
        outputJson: `{
  "success": true,
  "data": {
    "task_id": "task-uuid",
    "status": "completed",
    "audio_path": "/Users/zy/digital-human/previews/task-uuid.wav",
    "audio_url": "https://heygen-cdn/.../id=....wav",
    "duration": 3.76,
    "word_timestamps": [{"word": "大", "start": 0.219, "end": 0.359}],
    "is_pass": true,
    "is_silence": false
  }
}`,
        relatedCommands: [
            { command: 'voice clone <audio-file> --external-id <id>', description: '训练 voice clone 拿到 voice_id' },
            { command: 'avatar video --avatar <id> --voice <id> --text <text>', description: '定稿后生成 avatar 视频' },
        ],
        notes: [
            'audio-only 试听,不消耗 HeyGen video credit',
            'HeyGen 同步 API,通常 1-2 秒返回',
            '音频默认下载到 ~/digital-human/previews/<task_id>.wav',
            'word_timestamps 可用于字幕同步',
        ],
    });
    // `gen voice concat` —— 把逐段确认的音频拼成整条,喂给 audio-driven avatar video.
    // 用 concat FILTER 重编码(非 -c copy),避免 mp3 priming/gap 拼缝杂音(spec §4.2 #10)。
    const concatCmd = voice
        .command('concat')
        .description('把多段音频拼成单条连续音轨(concat filter 重编码,非 stream-copy)')
        .option('--workspace-dir <dir>', 'Mode A: 扫描 <dir>/segments/seg-*.{mp3,wav} 按文件名自然排序')
        .option('--inputs <inputs...>', 'Mode B: 显式音频列表(顺序 = 拼接顺序)')
        .requiredOption('--out <path>', '输出音频路径(.mp3 → CBR libmp3lame / .wav → PCM)')
        .action(async (options) => {
        const pretty = isPrettyMode(options);
        try {
            if (!hasFfmpegBinary('ffmpeg')) {
                outputError('CONCAT_FFMPEG_NOT_FOUND', '未找到 ffmpeg 命令。请先安装 ffmpeg 并加入 PATH', options);
                return;
            }
            const hasWorkspace = !!options.workspaceDir;
            const hasInputs = Array.isArray(options.inputs) && options.inputs.length > 0;
            if (hasWorkspace && hasInputs) {
                outputError('CONCAT_USAGE_ERROR', '--workspace-dir 和 --inputs 二选一,不能同时传', options);
                return;
            }
            if (!hasWorkspace && !hasInputs) {
                outputError('CONCAT_USAGE_ERROR', '必须传 --workspace-dir <dir> 或 --inputs <p1> <p2> ...', options);
                return;
            }
            let inputs;
            if (hasWorkspace) {
                const segmentsDir = path.join(path.resolve(options.workspaceDir), 'segments');
                if (!fs.existsSync(segmentsDir)) {
                    outputError('CONCAT_FAILED', `segments 目录不存在: ${segmentsDir}`, options);
                    return;
                }
                inputs = fs
                    .readdirSync(segmentsDir)
                    .filter((f) => /^seg-\d+\.(mp3|wav)$/i.test(f))
                    // natural sort: seg-2 < seg-10 < seg-100 (lexical 会错乱)
                    .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('CONCAT_FAILED', 'inputs 为空,没有音频可拼接', options);
                return;
            }
            for (const input of inputs) {
                if (!fs.existsSync(input)) {
                    outputError('CONCAT_FAILED', `input 文件不存在: ${input}`, options, { failed_input: input });
                    return;
                }
            }
            const outputPath = path.resolve(options.out);
            fs.mkdirSync(path.dirname(outputPath), { recursive: true });
            const spinner = pretty ? ora(`拼接 ${inputs.length} 段音频...`).start() : null;
            const r = runFfmpegAudioConcat(inputs, outputPath);
            if (!r.ok) {
                if (pretty)
                    spinner.fail('音频拼接失败');
                outputError('CONCAT_FAILED', `ffmpeg concat 失败: ${r.stderr.slice(0, 500)}`, options);
                return;
            }
            const duration = ffprobeDuration(outputPath);
            if (pretty) {
                spinner.succeed(`拼接完成: ${outputPath} [${duration.toFixed(2)}s, ${inputs.length} 段]`);
            }
            else {
                outputSuccess({ output_path: outputPath, duration_sec: duration, input_count: inputs.length });
            }
        }
        catch (err) {
            outputError('CONCAT_FAILED', `拼接过程异常: ${err}`, options);
        }
    });
    addEnhancedHelp(concatCmd, {
        examples: [
            '# Mode A: 自动扫描 workspace segments/',
            '$ gen voice concat --workspace-dir ~/digital-human/videos/20260530-1430 --out full-audio.mp3',
            '# Mode B: 显式列音频(顺序 = 拼接顺序)',
            '$ gen voice concat --inputs seg-01.mp3 seg-02.mp3 seg-03.mp3 --out full-audio.mp3',
        ],
        outputJson: `{
  "success": true,
  "data": {
    "output_path": "/abs/path/full-audio.mp3",
    "duration_sec": 69.3,
    "input_count": 14
  }
}`,
        relatedCommands: [
            { command: 'voice preview --voice <id> --text <seg>', description: '逐段生成可试听音频' },
            { command: 'avatar video --avatar <id> --audio <file>', description: '用拼好的音频驱动 avatar 出片' },
        ],
        notes: [
            'concat filter 重编码,非 -c copy:避免 mp3 priming/gap 拼缝杂音',
            '.mp3 输出 = CBR libmp3lame 192k;.wav 输出 = PCM 无损',
            '各段须同源(同一 voice preview),采样率/声道不一致 ffmpeg 会报错',
        ],
    });
}
//# sourceMappingURL=voice.js.map