import * as path from 'path';
import * as fs from 'fs';
import ora from 'ora';
import { success, info, warning } from '../utils/logger.js';
import { outputSuccess, outputError, isPrettyMode } from '../utils/output.js';
import { addEnhancedHelp } from '../utils/helpText.js';
import { GenerationApiClient, GenerationApiError, resolveOutputPath, downloadResult, } from '../services/generation-api.js';
import { getConfigValue } from '../services/config.js';
import { hasFfmpegBinary, ffprobeDuration } from './video.js';
/**
 * Decide audio-driven vs text-driven for `gen doctor video` from CLI options,
 * enforcing the 二选一 contract (spec §4.2). Throws on invalid combos so the
 * action surfaces a clean validation error before any upload/submit.
 */
export function selectDoctorVideoMode(opts) {
    const hasAudio = !!opts.audio;
    const hasVoiceOrText = !!opts.voice || !!opts.text;
    if (hasAudio && hasVoiceOrText) {
        throw new Error('--audio 与 --voice/--text 互斥(二选一)');
    }
    if (hasAudio)
        return { mode: 'audio' };
    if (!opts.voice || !opts.text) {
        throw new Error('文本驱动需要 --voice 和 --text(或改用 --audio 音频驱动)');
    }
    return { mode: 'text' };
}
/**
 * `gen doctor` namespace — historical naming (Phase 2 POC originated for medical
 * scenarios). Functionally identical to `gen avatar *` and kept as a
 * backward-compatible alias.
 *
 * Subcommands (registered via registerDoctorSubcommands, also reused under
 * `gen avatar` in avatar.ts):
 *   onboard <audio> <video> --external-id <id>
 *   video --avatar <id> --voice <id> --text <...>
 *   profile <external-id>
 *
 * All user-facing help text refers to `gen avatar *` (recommended name),
 * not `gen doctor *`. `--help` under either namespace guides users toward
 * the unified command set.
 */
export function registerDoctorCommand(program) {
    const doctor = program.command('doctor').description('数字人分身相关命令(兼容性别名,推荐用 gen avatar *)');
    registerOnboard(doctor);
    registerVideo(doctor);
    registerProfile(doctor);
}
// Exported so gen avatar (commands/avatar.ts) can register the same subcommands
// under the recommended `gen avatar` namespace. `gen doctor *` stays as a
// backward-compatible alias.
export function registerDoctorSubcommands(parent) {
    registerOnboard(parent);
    registerVideo(parent);
    registerProfile(parent);
}
function registerOnboard(doctor) {
    const cmd = doctor
        .command('onboard')
        .description('一次性训练 voice + avatar 数字分身(并发 2 个底层 task)')
        .argument('<audio-file>', '音频文件路径(.wav / .mp3 等)')
        .argument('<video-file>', '视频文件路径(.mp4 / .mov 等)')
        .requiredOption('--external-id <id>', '业务方分身唯一标识')
        .option('--clean-bg', '声明视频背景已干净(白墙/医院/棚拍/绿幕)', false)
        .option('--accept-baked-bg', '明确接受脏背景被 baked 进 avatar look(spec §13.4 — 仅用于探针/非上线)', false)
        .action(async (audioFile, videoFile, options) => {
        const pretty = isPrettyMode(options);
        try {
            if (!options.cleanBg && !options.acceptBakedBg) {
                outputError('BG_DECISION_REQUIRED', '请二选一(spec §13.4):\n  --clean-bg          视频背景已干净\n  --accept-baked-bg   接受脏背景成为 avatar 永久背景', options);
                return;
            }
            if (options.cleanBg && options.acceptBakedBg) {
                outputError('BG_DECISION_CONFLICT', '--clean-bg 和 --accept-baked-bg 二选一', options);
                return;
            }
            const audioPath = path.resolve(audioFile);
            const videoPath = path.resolve(videoFile);
            if (!fs.existsSync(audioPath)) {
                outputError('AUDIO_NOT_FOUND', `音频文件不存在: ${audioPath}`, options);
                return;
            }
            if (!fs.existsSync(videoPath)) {
                outputError('VIDEO_NOT_FOUND', `视频文件不存在: ${videoPath}`, options);
                return;
            }
            if (pretty && options.acceptBakedBg) {
                warning('已显式接受脏背景被 baked,生成视频时无法替换 avatar 内置背景');
            }
            const client = new GenerationApiClient();
            // Step 1+2: upload both assets concurrently
            const upSpinner = pretty ? ora('并发上传音频 + 视频到 HeyGen 资产存储...').start() : null;
            const [audioAsset, videoAsset] = await Promise.all([
                client.uploadHeyGenAsset(audioPath),
                client.uploadHeyGenAsset(videoPath),
            ]);
            if (pretty)
                upSpinner.succeed(`两个资产已上传(audio=${audioAsset.asset_id}, video=${videoAsset.asset_id})`);
            // Step 3: submit onboard facade
            const onboardSpinner = pretty ? ora('提交分身 onboard(并发触发 voice + avatar 训练)...').start() : null;
            const onboard = await client.submitDoctorOnboard({
                audio_asset_id: audioAsset.asset_id,
                video_asset_id: videoAsset.asset_id,
                external_id: options.externalId,
                training_video_clean_bg: options.cleanBg,
            });
            if (pretty)
                onboardSpinner.succeed(`onboard 提交成功(voice_task=${onboard.voice_task_id}, avatar_task=${onboard.avatar_task_id})`);
            if (pretty) {
                info('两个底层 task 异步训练中,使用以下命令各自查状态:');
                info(`  gen task get ${onboard.voice_task_id}`);
                info(`  gen task get ${onboard.avatar_task_id}`);
                info(`完成后用: gen avatar profile ${options.externalId} 查看 voice_id + look_id`);
            }
            else {
                outputSuccess({
                    external_id: onboard.external_id,
                    voice_task_id: onboard.voice_task_id,
                    avatar_task_id: onboard.avatar_task_id,
                    status: 'pending',
                });
            }
        }
        catch (err) {
            if (err instanceof GenerationApiError) {
                outputError(err.code, err.message, options);
            }
            else {
                outputError('ONBOARD_FAILED', `onboard 失败: ${err}`, options);
            }
        }
    });
    addEnhancedHelp(cmd, {
        examples: [
            '# 一次性训练数字分身的 voice + avatar',
            '$ gen avatar onboard ./audio.wav ./video.mp4 --external-id dr-zhang --clean-bg',
        ],
        outputJson: `{
  "success": true,
  "data": {
    "external_id": "doc-123",
    "voice_task_id": "task-uuid-1",
    "avatar_task_id": "task-uuid-2",
    "status": "pending"
  }
}`,
        relatedCommands: [
            { command: 'task get <id>', description: '查 voice/avatar 任务状态' },
            { command: 'avatar profile <external-id>', description: '查 profile(含 voice_id + look_id)' },
            { command: 'avatar video --avatar --voice --text', description: '用训练好的 ID 合成视频' },
        ],
        notes: [
            'Onboard 内部并发触发 voice-clone + avatar-train 两个独立 task(失败 isolation)',
            '想细控就分别用 gen voice clone + gen avatar train',
        ],
    });
}
function registerVideo(doctor) {
    const cmd = doctor
        .command('video')
        .description('用训练好的 avatar 合成数字分身视频(文本驱动或音频驱动;自动 retry-on-still-processing + DELETE cleanup)')
        .requiredOption('--avatar <id>', 'avatar look_id(从 gen avatar train 或 gen avatar profile 拿到)')
        .option('--voice <id>', '文本驱动:voice_id(从 gen voice clone 或 gen avatar profile 拿到)')
        .option('--text <text>', '文本驱动:分身说话文本(≤ 50 字, spec §13.3)')
        .option('--audio <file>', '音频驱动:用拼好的整条音频驱动 avatar(与 --voice/--text 互斥,无 50 字限制)')
        .option('--title <title>', '视频标题')
        .option('-o, --output <path>', '输出文件 / 目录')
        .action(async (options) => {
        const pretty = isPrettyMode(options);
        try {
            // 二选一:音频驱动 OR 文本驱动(spec §4.2)。无效组合先报错,避免空跑上传/提交。
            let mode;
            try {
                mode = selectDoctorVideoMode({ voice: options.voice, text: options.text, audio: options.audio });
            }
            catch (e) {
                outputError('USAGE_ERROR', e.message, options);
                return;
            }
            const client = new GenerationApiClient();
            const defaultDir = getConfigValue('outputDir') || './gen-output';
            let submitParams;
            if (mode.mode === 'audio') {
                const audioPath = path.resolve(options.audio);
                if (!fs.existsSync(audioPath)) {
                    outputError('AUDIO_NOT_FOUND', `音频文件不存在: ${audioPath}`, options);
                    return;
                }
                if (!hasFfmpegBinary('ffprobe')) {
                    outputError('FFPROBE_NOT_FOUND', '未找到 ffprobe(ffmpeg 套件)。请安装 ffmpeg 并加入 PATH', options);
                    return;
                }
                // ffprobe 本地音频时长 → 作为预扣 hold 传 `duration`(字段名逐字一致, spec §4.2)。
                // 仅预扣占位,最终扣费以服务端权威 data.duration settle 为准。
                const duration = ffprobeDuration(audioPath);
                // ffprobe 已 gate 存在,duration<=0 基本=文件损坏/格式不支持。提前挡,
                // 别空跑一次 upload + 提交才在 HeyGen 侧失败。
                if (!(duration > 0)) {
                    outputError('AUDIO_UNREADABLE', `无法读取音频时长(ffprobe 返回 ${duration});文件可能损坏或格式不支持: ${audioPath}`, options);
                    return;
                }
                const upSpinner = pretty ? ora('上传音频到 HeyGen 资产存储...').start() : null;
                const asset = await client.uploadHeyGenAsset(audioPath);
                if (pretty)
                    upSpinner.succeed(`音频已上传 (asset_id=${asset.asset_id}, ${duration.toFixed(1)}s)`);
                submitParams = {
                    avatar_id: options.avatar,
                    audio_asset_id: asset.asset_id,
                    duration,
                    title: options.title,
                };
            }
            else {
                if (options.text.length > 50) {
                    outputError('TEXT_TOO_LONG', 'text 不能超过 50 字符(spec §13.3:trained avatar 长 prompt 挂死,client 端拆段)', options);
                    return;
                }
                submitParams = {
                    avatar_id: options.avatar,
                    voice_id: options.voice,
                    text: options.text,
                    title: options.title,
                };
            }
            const spinner = pretty ? ora('提交分身视频生成任务...').start() : null;
            const result = await client.submitAndPoll('doctor-video', submitParams, {
                timeout: 1800000, // 30 min
                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(`task_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('DOCTOR_VIDEO_FAILED', `生成失败: ${err}`, options);
            }
        }
    });
    addEnhancedHelp(cmd, {
        examples: [
            '# 用训练好的数字分身 avatar + voice 合成 ≤ 50 字短视频',
            '$ gen avatar video --avatar look_xxx --voice voice_xxx --text "你好,我是张医生。"',
        ],
        outputJson: `{
  "success": true,
  "data": {
    "task_id": "task-uuid",
    "status": "completed",
    "output": "./gen-output/avatar_abc123.mp4",
    "result_url": "https://heygen/..."
  }
}`,
        relatedCommands: [
            { command: 'avatar profile <external-id>', description: '查 avatar look_id + voice_id' },
            { command: 'video --provider heygen', description: '用任意 HeyGen avatar/voice 合成(无 retry/cleanup wrapper)' },
        ],
        notes: [
            '**text 上限 50 字**(spec §13.3:trained avatar 长 prompt 挂死)',
            '内部封装:retry-on-"still-processing"(初始 1min,30min 上限)+ DELETE cleanup 卡死任务',
            '生成时长几十秒到几分钟',
        ],
    });
}
function registerProfile(doctor) {
    const cmd = doctor
        .command('profile')
        .description('查询数字分身 profile(voice_id + look_id + 状态)')
        .argument('<external-id>', '业务方分身唯一标识')
        .action(async (externalId, options) => {
        const pretty = isPrettyMode(options);
        try {
            const client = new GenerationApiClient();
            const profile = await client.getDoctorAvatarProfile(externalId);
            if (!profile) {
                if (pretty) {
                    warning(`profile 不存在: external_id=${externalId}`);
                    info('先训练:gen voice clone / gen avatar train / gen avatar onboard');
                }
                else {
                    outputError('NOT_FOUND', `profile not found: ${externalId}`, options);
                }
                return;
            }
            if (pretty) {
                success(`Digital Avatar Profile: ${profile.external_id}`);
                info(`voice_id:        ${profile.heygen_voice_id ?? '(not trained)'}`);
                info(`voice_status:    ${profile.voice_status}${profile.voice_error ? ` (${profile.voice_error})` : ''}`);
                info(`avatar_group_id: ${profile.heygen_avatar_group_id ?? '(not trained)'}`);
                info(`avatar look_ids: ${JSON.stringify(profile.heygen_avatar_look_ids)}`);
                info(`avatar_status:   ${profile.avatar_status}${profile.avatar_error ? ` (${profile.avatar_error})` : ''}`);
                info(`preview_image:   ${profile.preview_image_url ?? '(none)'}`);
                info(`clean_bg:        ${profile.training_video_clean_bg}`);
                info(`consent_status:  ${profile.consent_status ?? '(unknown)'}`);
                info(`created_at:      ${profile.created_at}`);
                info(`updated_at:      ${profile.updated_at}`);
            }
            else {
                outputSuccess(profile);
            }
        }
        catch (err) {
            if (err instanceof GenerationApiError) {
                outputError(err.code, err.message, options);
            }
            else {
                outputError('PROFILE_QUERY_FAILED', `查询 profile 失败: ${err}`, options);
            }
        }
    });
    addEnhancedHelp(cmd, {
        examples: ['# 查看数字分身训练状态 + ID', '$ gen avatar profile dr-zhang'],
        outputJson: `{
  "success": true,
  "data": {
    "external_id": "dr-zhang",
    "heygen_voice_id": "f7a8ea9f...",
    "voice_status": "complete",
    "heygen_avatar_group_id": "174fa88e...",
    "heygen_avatar_look_ids": ["04182555..."],
    "avatar_status": "completed",
    "preview_image_url": "https://heygen/preview.jpg",
    "training_video_clean_bg": true,
    "consent_status": "skipped",
    "created_at": "2026-05-08T12:00:00Z",
    "updated_at": "2026-05-08T12:10:00Z"
  }
}`,
        relatedCommands: [
            { command: 'avatar video --avatar --voice --text', description: '用 profile 里的 ID 合成视频' },
        ],
        notes: ['voice_status / avatar_status 独立(允许只训其中一个)'],
    });
}
//# sourceMappingURL=doctor.js.map