import axios from 'axios';
import * as fs from 'fs/promises';
import * as path from 'path';
import { getToken, getGenerationApiUrl } from './auth.js';
// ==================== Error Handling ====================
export class GenerationApiError extends Error {
    code;
    retryAfter;
    httpStatus;
    constructor(code, message, httpStatus, retryAfter) {
        super(message);
        this.name = 'GenerationApiError';
        this.code = code;
        this.httpStatus = httpStatus;
        this.retryAfter = retryAfter;
    }
}
function handleApiError(err) {
    if (err.response?.data?.error) {
        const { code, message, retry_after } = err.response.data.error;
        throw new GenerationApiError(code, message, err.response.status, retry_after);
    }
    if (err.response) {
        throw new GenerationApiError('UNKNOWN_ERROR', `HTTP ${err.response.status}: ${err.response.statusText}`, err.response.status);
    }
    throw new GenerationApiError('NETWORK_ERROR', `网络请求失败: ${err.message}`);
}
// ==================== Client ====================
export class GenerationApiClient {
    client;
    constructor() {
        const token = getToken();
        const baseURL = getGenerationApiUrl();
        this.client = axios.create({
            baseURL,
            headers: {
                'Authorization': `Bearer ${token}`,
                'Content-Type': 'application/json',
            },
            timeout: 30000,
            maxBodyLength: 30 * 1024 * 1024,
            maxContentLength: 30 * 1024 * 1024,
        });
    }
    // ==================== Submit Tasks ====================
    async submitImage(params) {
        return this.submit('/api/image/generate', params);
    }
    async submitVideo(params) {
        return this.submit('/api/video/generate', params);
    }
    async submitVideoMotionControl(params) {
        return this.submit('/api/video-motion-control/generate', params);
    }
    async submitVideoTranslate(params) {
        return this.submit('/api/video-translate/generate', params);
    }
    async submitTTS(params) {
        return this.submit('/api/tts/generate', params);
    }
    async submitASR(params) {
        return this.submit('/api/asr/generate', params);
    }
    async submitTryon(params) {
        return this.submit('/api/tryon/generate', params);
    }
    // Doctor avatar Phase 2 task submissions
    async submitVoiceClone(params) {
        return this.submit('/api/voice-clone/generate', params);
    }
    async submitAvatarTrain(params) {
        return this.submit('/api/avatar-train/generate', params);
    }
    async submitDoctorVideo(params) {
        return this.submit('/api/doctor-video/generate', params);
    }
    async submitVoicePreview(params) {
        return this.submit('/api/voice-preview/generate', params);
    }
    // Convenience facade — fan-out to voice-clone + avatar-train concurrently
    async submitDoctorOnboard(params) {
        try {
            const response = await this.client.post('/api/doctor-avatar/onboard', params);
            return response.data;
        }
        catch (err) {
            handleApiError(err);
        }
    }
    async getDoctorAvatarProfile(externalId) {
        try {
            const response = await this.client.get(`/api/doctor-avatar-profile/${encodeURIComponent(externalId)}`);
            return response.data;
        }
        catch (err) {
            const e = err;
            if (e.response?.status === 404)
                return null;
            handleApiError(e);
        }
    }
    // Multipart upload to /api/heygen-asset/upload — returns asset_id for use in
    // voice-clone / avatar-train submit. Client is responsible for the file's bytes;
    // the generation service stream-forwards the multipart payload to HeyGen /v3/assets.
    async uploadHeyGenAsset(filePath, mimeType) {
        const FormData = (await import('form-data')).default;
        const fsCallbacks = await import('fs');
        const path = await import('path');
        const filename = path.basename(filePath);
        const detectedMime = mimeType || guessMimeFromExt(path.extname(filename));
        const form = new FormData();
        form.append('file', fsCallbacks.createReadStream(filePath), {
            filename,
            contentType: detectedMime,
        });
        try {
            const headers = {
                ...form.getHeaders(),
                Authorization: this.client.defaults.headers.Authorization,
            };
            const response = await axios.post(`${this.client.defaults.baseURL}/api/heygen-asset/upload`, form, {
                headers,
                maxBodyLength: Infinity,
                maxContentLength: Infinity,
                timeout: 300000,
            });
            return response.data;
        }
        catch (err) {
            handleApiError(err);
        }
    }
    // ==================== Task Management ====================
    async getTask(taskId) {
        try {
            const response = await this.client.get(`/api/task/${taskId}`);
            return response.data;
        }
        catch (err) {
            handleApiError(err);
        }
    }
    async listTasks(options) {
        try {
            const params = {};
            if (options?.status)
                params.status = options.status;
            if (options?.type)
                params.type = options.type;
            if (options?.limit)
                params.limit = options.limit;
            const response = await this.client.get('/api/task/list', { params });
            return response.data;
        }
        catch (err) {
            handleApiError(err);
        }
    }
    async cancelTask(taskId) {
        try {
            const response = await this.client.post(`/api/task/${taskId}/cancel`, {});
            return response.data;
        }
        catch (err) {
            handleApiError(err);
        }
    }
    async retryTask(taskId) {
        try {
            const response = await this.client.post(`/api/task/${taskId}/retry`, {});
            return response.data;
        }
        catch (err) {
            handleApiError(err);
        }
    }
    // ==================== Polling ====================
    /**
     * 轮询任务直到完成或超时
     * 超时不视为失败，返回当前状态让调用方决定
     */
    async pollTask(taskId, options) {
        const timeout = options?.timeout || 600000; // 默认 10 分钟
        const interval = options?.interval || 3000; // 默认 3 秒
        const startTime = Date.now();
        while (true) {
            if (Date.now() - startTime > timeout) {
                // 超时时再查一次获取真实状态
                try {
                    const latest = await this.getTask(taskId);
                    latest.error_message = latest.error_message || '轮询超时。使用 gen task get 查看最新状态。';
                    return latest;
                }
                catch {
                    return {
                        task_id: taskId,
                        status: 'processing',
                        error_message: '轮询超时。使用 gen task get 查看最新状态。',
                    };
                }
            }
            const result = await this.getTask(taskId);
            options?.onProgress?.(result.status);
            if (result.status === 'completed' || result.status === 'failed' || result.status === 'cancelled') {
                return result;
            }
            await sleep(interval);
        }
    }
    // ==================== Helpers ====================
    /**
     * 提交任务并轮询等待结果，一站式调用
     */
    async submitAndPoll(type, params, pollOptions) {
        let submitResult;
        switch (type) {
            case 'image':
                submitResult = await this.submitImage(params);
                break;
            case 'video':
                submitResult = await this.submitVideo(params);
                break;
            case 'video-motion-control':
                submitResult = await this.submitVideoMotionControl(params);
                break;
            case 'video-translate':
                submitResult = await this.submitVideoTranslate(params);
                break;
            case 'tts':
                submitResult = await this.submitTTS(params);
                break;
            case 'asr':
                submitResult = await this.submitASR(params);
                break;
            case 'tryon':
                submitResult = await this.submitTryon(params);
                break;
            case 'voice-clone':
                submitResult = await this.submitVoiceClone(params);
                break;
            case 'avatar-train':
                submitResult = await this.submitAvatarTrain(params);
                break;
            case 'doctor-video':
                submitResult = await this.submitDoctorVideo(params);
                break;
            case 'voice-preview':
                submitResult = await this.submitVoicePreview(params);
                break;
        }
        return this.pollTask(submitResult.task_id, pollOptions);
    }
    // ==================== Private ====================
    async submit(endpoint, params) {
        try {
            const response = await this.client.post(endpoint, params);
            return response.data;
        }
        catch (err) {
            handleApiError(err);
        }
    }
}
// ==================== Utilities ====================
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
function guessMimeFromExt(ext) {
    const e = ext.toLowerCase();
    if (e === '.wav')
        return 'audio/wav';
    if (e === '.mp3')
        return 'audio/mpeg';
    if (e === '.m4a')
        return 'audio/mp4';
    if (e === '.aac')
        return 'audio/aac';
    if (e === '.ogg')
        return 'audio/ogg';
    if (e === '.mp4')
        return 'video/mp4';
    if (e === '.mov')
        return 'video/quicktime';
    if (e === '.webm')
        return 'video/webm';
    return 'application/octet-stream';
}
/**
 * 将本地文件读取为 base64 字符串
 */
export async function fileToBase64(filePath) {
    const absolutePath = path.resolve(filePath);
    const buffer = await fs.readFile(absolutePath);
    return buffer.toString('base64');
}
/**
 * 将本地图像读取为 base64 data URL
 */
export async function imageToBase64DataUrl(filePath) {
    const absolutePath = path.resolve(filePath);
    const buffer = await fs.readFile(absolutePath);
    const base64 = buffer.toString('base64');
    const ext = path.extname(filePath).toLowerCase();
    const mimeType = ext === '.png' ? 'image/png' : 'image/jpeg';
    return `data:${mimeType};base64,${base64}`;
}
/**
 * 解析输出路径：如果用户传的是文件路径（有扩展名），直接用；否则当目录，拼上默认文件名。
 */
export function resolveOutputPath(userOutput, defaultDir, defaultFilename) {
    if (!userOutput) {
        return { dir: defaultDir, filename: defaultFilename };
    }
    const ext = path.extname(userOutput);
    if (ext) {
        // 用户传的是文件路径，如 /home/user/result.png
        return { dir: path.dirname(userOutput), filename: path.basename(userOutput) };
    }
    // 用户传的是目录，如 /home/user/output/
    return { dir: userOutput, filename: defaultFilename };
}
/**
 * 下载结果文件到本地
 */
export async function downloadResult(url, outputDir, filename) {
    await fs.mkdir(outputDir, { recursive: true });
    const response = await axios.get(url, {
        responseType: 'arraybuffer',
        timeout: 300000, // 5 分钟，视频文件可能较大
    });
    const outputPath = path.join(outputDir, filename);
    await fs.writeFile(outputPath, response.data);
    return outputPath;
}
/**
 * CLI 层参数校验 - 快速失败
 */
export function validateImageParams(params) {
    if (params.width && params.width > 2048)
        throw new Error('图片宽度不能超过 2048');
    if (params.height && params.height > 2048)
        throw new Error('图片高度不能超过 2048');
    if (params.prompt.length > 10000)
        throw new Error('prompt 不能超过 10000 字符');
}
export function validateVideoParams(params) {
    if (params.provider === 'heygen') {
        const p = params;
        if (!p.avatar_id)
            throw new Error('HeyGen 需要 avatar_id');
        if (!p.voice_id)
            throw new Error('HeyGen 需要 voice_id');
        if (!p.text)
            throw new Error('HeyGen 需要 text');
        if (p.text.length > 5000)
            throw new Error('text 不能超过 5000 字符');
        if (p.speed !== undefined && (p.speed < 0.5 || p.speed > 1.5))
            throw new Error('语速范围 0.5-1.5');
        return;
    }
    if (params.duration) {
        // Different providers allow different duration sets. When provider is
        // unspecified, defer to the backend (it knows its own default's range).
        if (params.provider === 'dashscope') {
            if (![5, 10].includes(params.duration))
                throw new Error('视频时长仅支持 5 或 10 秒');
        }
        else if (params.provider === 'piapi') {
            if (![10, 15].includes(params.duration))
                throw new Error('视频时长仅支持 10 或 15 秒');
        }
        else if (params.provider === 'kie' || params.provider === undefined) {
            if (params.duration < 4 || params.duration > 15)
                throw new Error('视频时长需在 4-15 秒之间');
        }
    }
    if (params.resolution) {
        // Lowercase 480p/720p is the kie convention; uppercase 720P/1080P is dashscope.
        // Accept both so the CLI doesn't reject either provider's valid input.
        const ok = ['480p', '720p', '720P', '1080P'];
        if (!ok.includes(params.resolution)) {
            throw new Error('分辨率仅支持 480p / 720p / 720P / 1080P');
        }
    }
    if (params.provider && !['kie', 'dashscope', 'piapi', 'heygen'].includes(params.provider)) {
        throw new Error(`未知 provider: ${params.provider}（可选: kie, dashscope, piapi, heygen）`);
    }
}
export function validateTTSParams(params) {
    if (params.text.length > 5000)
        throw new Error('文本长度不能超过 5000 字符');
}
export function validateASRParams(params) {
    if (params.fileSize > 25 * 1024 * 1024)
        throw new Error('音频文件不能超过 25MB');
}
//# sourceMappingURL=generation-api.js.map