"use strict";

/**
 * Node 流水线宿主能力（对齐 QuickJS __native / Rust js_runtime/native.rs）
 */

const fs = require("fs");
const path = require("path");
const os = require("os");
const crypto = require("crypto");
const { spawn, execFileSync } = require("child_process");
const https = require("https");
const http = require("http");

const TEMP_DIR = path.join(os.tmpdir(), "aiclient-node-pipeline");
fs.mkdirSync(TEMP_DIR, { recursive: true });

/** Tauri 子进程优先 __native.log；独立 node 调试时回退 console */
function log(level, msg, fields) {
  const payload =
    fields == null
      ? null
      : typeof fields === "object" && !Array.isArray(fields)
        ? fields
        : { value: fields };
  if (globalThis.__native?.log) {
    globalThis.__native.log(level, msg, payload);
    return;
  }
  const tag = `[pipeline_native.${level}] ${msg}`;
  if (payload != null) {
    if (level === "error") console.error(tag, payload);
    else if (level === "warn") console.warn(tag, payload);
    else console.log(tag, payload);
  } else if (level === "error") console.error(tag);
  else if (level === "warn") console.warn(tag);
  else console.log(tag);
}

function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

function tempPath(prefix, ext) {
  return path.join(TEMP_DIR, `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext || "tmp"}`);
}

function deleteFile(p) {
  try {
    if (p && fs.existsSync(p)) fs.unlinkSync(p);
  } catch (_) {
    /* ignore */
  }
}

function runCommand(cmd, args, timeoutMs = 300000) {
  return new Promise((resolve, reject) => {
    const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
    let stderr = "";
    child.stderr.on("data", (d) => {
      stderr += d.toString();
    });
    const timer = setTimeout(() => {
      child.kill();
      reject(new Error(`命令超时: ${cmd}`));
    }, timeoutMs);
    child.on("error", (e) => {
      clearTimeout(timer);
      reject(e);
    });
    child.on("close", (code) => {
      clearTimeout(timer);
      if (code === 0) resolve();
      else reject(new Error(`${cmd} 退出码 ${code}: ${stderr.slice(0, 500)}`));
    });
  });
}

function exeName() {
  return process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg";
}

function whichSync(cmd) {
  try {
    if (process.platform === "win32") {
      const out = execFileSync("where", [cmd], { encoding: "utf8", windowsHide: true });
      const line = out.split(/\r?\n/).map((s) => s.trim()).find(Boolean);
      return line && fs.existsSync(line) ? line : null;
    }
    const out = execFileSync("which", [cmd], { encoding: "utf8" });
    const line = out.trim().split(/\r?\n/)[0];
    return line && fs.existsSync(line) ? line : null;
  } catch {
    return null;
  }
}

/** 与 Rust `ffmpeg::locate_ffmpeg` 顺序对齐 */
function locateFfmpeg() {
  const name = exeName();
  const envPath = process.env.AICLIENT_FFMPEG_PATH;
  if (envPath && fs.existsSync(envPath)) {
    return envPath;
  }

  const candidates = [];
  const nodeDir = path.dirname(process.execPath);
  candidates.push(path.join(nodeDir, "binaries", name));
  candidates.push(path.join(nodeDir, name));
  candidates.push(path.join(nodeDir, "..", "binaries", name));
  candidates.push(path.join(nodeDir, "..", "..", "binaries", name));
  candidates.push(path.join(nodeDir, "..", "..", "..", "binaries", name));
  candidates.push(path.join(process.cwd(), "src-tauri", "binaries", name));
  candidates.push(path.join(process.cwd(), "binaries", name));

  for (const p of candidates) {
    try {
      if (fs.existsSync(p)) return p;
    } catch {
      /* ignore */
    }
  }

  return whichSync(name);
}

function requireFfmpeg() {
  const ffmpeg = locateFfmpeg();
  if (!ffmpeg) {
    throw new Error(
      "未找到 ffmpeg。请安装并加入 PATH，或将可执行文件放到 src-tauri/binaries/ffmpeg.exe，" +
        "或在环境变量中设置 AICLIENT_FFMPEG_PATH（桌面端会在启动 Node 时自动注入已解析路径）",
    );
  }
  return ffmpeg;
}

async function ffmpegExtractAudio(videoPath) {
  const dest = tempPath("audio", "wav");
  const ffmpeg = requireFfmpeg();
  await runCommand(ffmpeg, [
    "-y",
    "-i",
    videoPath,
    "-vn",
    "-acodec",
    "pcm_s16le",
    "-ar",
    "16000",
    "-ac",
    "1",
    dest,
  ]);
  return dest;
}

function rfc1123Date() {
  return new Date().toUTCString();
}

function signOss(secret, str) {
  return crypto.createHmac("sha1", secret).update(str).digest("base64");
}

/** 私有桶：ASR 需可公网 GET 的签名 URL（对齐 Electron generateSignedUrl） */
function ossSignedGetUrl(cfg, objectKey, expiresSec = 3600) {
  const expireTime = Math.floor(Date.now() / 1000) + expiresSec;
  const resource = `/${cfg.bucket}/${objectKey}`;
  const stringToSign = `GET\n\n\n${expireTime}\n${resource}`;
  const signature = encodeURIComponent(signOss(cfg.accessKeySecret, stringToSign));
  const host = `${cfg.bucket}.${cfg.region}.aliyuncs.com`;
  return `https://${host}/${objectKey}?OSSAccessKeyId=${encodeURIComponent(cfg.accessKeyId)}&Expires=${expireTime}&Signature=${signature}`;
}

async function aliyunOssUpload(localPath, cfg) {
  const ak = cfg.accessKeyId;
  const sk = cfg.accessKeySecret;
  const bucket = cfg.bucket;
  const region = cfg.region;
  if (!ak || !sk || !bucket || !region) {
    return { success: false, error: "OSS 配置不完整" };
  }
  const objectKey = `audio/${Date.now()}-${Math.random().toString(36).slice(2)}.wav`;
  const host = `${bucket}.${region}.aliyuncs.com`;
  const endpoint = (cfg.endpoint || `https://${host}`).replace(/\/$/, "");
  const url = `${endpoint}/${objectKey}`;
  const body = fs.readFileSync(localPath);
  const date = rfc1123Date();
  const contentType = "audio/wav";
  const canonicalizedResource = `/${bucket}/${objectKey}`;
  const strToSign = `PUT\n\n${contentType}\n${date}\n${canonicalizedResource}`;
  const signature = signOss(sk, strToSign);
  const authorization = `OSS ${ak}:${signature}`;

  return new Promise((resolve) => {
    const u = new URL(url);
    const opts = {
      hostname: u.hostname,
      port: u.port || 443,
      path: u.pathname,
      method: "PUT",
      headers: {
        Date: date,
        "Content-Type": contentType,
        Authorization: authorization,
        "Content-Length": body.length,
      },
    };
    const req = https.request(opts, (res) => {
      const chunks = [];
      res.on("data", (c) => chunks.push(c));
      res.on("end", () => {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          const signedUrl = ossSignedGetUrl(
            { accessKeyId: ak, accessKeySecret: sk, bucket, region },
            objectKey,
            3600,
          );
          resolve({ success: true, url: signedUrl });
        } else {
          resolve({
            success: false,
            error: `OSS HTTP ${res.statusCode}: ${Buffer.concat(chunks).toString().slice(0, 200)}`,
          });
        }
      });
    });
    req.on("error", (e) => resolve({ success: false, error: e.message }));
    req.write(body);
    req.end();
  });
}

/** filetrans: output.result；批量模型: output.results[0] */
function extractTranscriptionUrl(output) {
  if (!output || typeof output !== "object") return null;
  if (output.result?.transcription_url) return output.result.transcription_url;
  const results = output.results;
  if (Array.isArray(results) && results[0]?.transcription_url) {
    return results[0].transcription_url;
  }
  return null;
}

function parseTranscriptionText(trans) {
  let fullText = "";
  if (trans?.transcripts?.length) {
    for (const t of trans.transcripts) {
      if (t.sentences?.length) {
        for (const s of t.sentences) {
          if (s.text) {
            if (fullText) fullText += "\n";
            fullText += s.text;
          }
        }
      } else if (t.text) {
        if (fullText) fullText += "\n";
        fullText += t.text;
      }
    }
  } else if (trans?.text) {
    fullText = trans.text;
  }
  return fullText;
}

/** @returns {Array<{ text: string, start: number, end: number }>} start/end 毫秒 */
function normalizeAsrTimeToMs(value) {
  const n = Number(value);
  if (!Number.isFinite(n) || n < 0) return 0;
  // 对齐 Electron generateAssSubtitle：>100 视为毫秒，否则视为秒
  if (n > 100) return Math.round(n);
  return Math.round(n * 1000);
}

function normalizeAsrRecord(record) {
  const text = String(record?.text || record?.sentence || "").trim();
  if (!text) return null;
  let start = normalizeAsrTimeToMs(record.start ?? record.begin ?? record.beginTime ?? 0);
  let end = normalizeAsrTimeToMs(record.end ?? record.endTime ?? start + 2000);
  if (end <= start) end = start + 2000;
  return { text, start, end };
}

/** @returns {Array<{ text: string, start: number, end: number }>} */
function normalizeAsrRecords(records) {
  if (!Array.isArray(records)) return [];
  const normalized = records.map(normalizeAsrRecord).filter(Boolean);
  const MIN_GAP_MS = 50;
  for (let i = 0; i < normalized.length - 1; i++) {
    const current = normalized[i];
    const next = normalized[i + 1];
    if (current.end >= next.start) {
      current.end = Math.max(current.start + MIN_GAP_MS, next.start - MIN_GAP_MS);
    }
    if (current.end <= current.start) {
      current.end = current.start + MIN_GAP_MS;
    }
  }
  return normalized;
}

/** @returns {Array<{ text: string, start: number, end: number }>} start/end 毫秒 */
function parseTranscriptionSentences(trans) {
  const records = [];
  if (trans?.transcripts?.length) {
    for (const t of trans.transcripts) {
      if (t.sentences?.length) {
        for (const s of t.sentences) {
          const text = String(s.text || "").trim();
          if (!text) continue;
          const start = normalizeAsrTimeToMs(s.begin_time ?? s.start_time ?? 0);
          const end = normalizeAsrTimeToMs(
            s.end_time ?? s.end ?? start + 2000,
          );
          records.push({ text, start, end });
        }
      }
    }
  }
  return normalizeAsrRecords(records);
}

async function fetchJson(url, options = {}) {
  const res = await fetch(url, options);
  const text = await res.text();
  let data;
  try {
    data = JSON.parse(text);
  } catch {
    data = { _raw: text };
  }
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
  }
  return data;
}

async function fetchArrayBuffer(url, options = {}) {
  const res = await fetch(url, options);
  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
  }
  return Buffer.from(await res.arrayBuffer());
}

const QWEN_TTS_URL =
  "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation";

/** 对齐 Electron Ka：根据 voiceId 选择 TTS 模型 */
function resolveTtsModel(voiceId) {
  const v = String(voiceId || "").trim();
  if (!v || v === "Cherry") return "qwen3-tts-flash";
  if (v.startsWith("qwen-tts-vc-")) return "qwen3-tts-vc-2026-01-22";
  return "qwen3-tts-flash";
}

function splitTextForTts(text, maxLen = 300) {
  const t = String(text || "").trim();
  if (t.length <= maxLen) return [t];
  const segments = [];
  let remaining = t;
  while (remaining.length > 0) {
    if (remaining.length <= maxLen) {
      segments.push(remaining.trim());
      break;
    }
    let splitIdx = -1;
    for (const sep of ["。", "！", "？", ".", "!", "?"]) {
      const idx = remaining.lastIndexOf(sep, maxLen);
      if (idx > maxLen * 0.3) {
        splitIdx = idx + 1;
        break;
      }
    }
    if (splitIdx === -1) {
      for (const sep of ["，", ",", "、", "：", ":"]) {
        const idx = remaining.lastIndexOf(sep, maxLen);
        if (idx > maxLen * 0.3) {
          splitIdx = idx + 1;
          break;
        }
      }
    }
    if (splitIdx === -1) splitIdx = maxLen;
    segments.push(remaining.slice(0, splitIdx).trim());
    remaining = remaining.slice(splitIdx).trim();
  }
  return segments.filter((s) => s.length > 0);
}

function concatWavFiles(inputPaths, outputPath) {
  if (inputPaths.length === 0) throw new Error("无输入文件");
  if (inputPaths.length === 1) {
    fs.copyFileSync(inputPaths[0], outputPath);
    return;
  }
  const firstFile = fs.readFileSync(inputPaths[0]);
  const headerSize = 44;
  const header = Buffer.from(firstFile.buffer, 0, headerSize);
  const pcmBuffers = [];
  let totalPcmSize = 0;
  for (const p of inputPaths) {
    const buf = fs.readFileSync(p);
    const pcm = buf.slice(headerSize);
    pcmBuffers.push(pcm);
    totalPcmSize += pcm.length;
  }
  const newHeader = Buffer.from(header);
  newHeader.writeUInt32LE(totalPcmSize + headerSize - 8, 4);
  newHeader.writeUInt32LE(totalPcmSize, 40);
  const outputBuf = Buffer.concat([newHeader, ...pcmBuffers]);
  fs.writeFileSync(outputPath, outputBuf);
}

/**
 * 通义千问 TTS 单段合成（对齐 Electron synthesizeOneSegment）
 */
async function qwenTtsSynthesizeOneSegment(text, config, outputPath) {
  const apiKey = config.apiKey;
  const voice = String(config.voice || "").trim();
  const model = config.model || resolveTtsModel(voice);
  const instruction = String(config.instruction || "").trim();
  const languageType = config.languageType;

  const input = {
    text: String(text || "").trim(),
    voice,
    ...(languageType ? { language_type: languageType } : {}),
    ...(instruction
      ? { instructions: instruction, optimize_instructions: true }
      : {}),
  };

  const data = await fetchJson(QWEN_TTS_URL, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ model, input }),
  });

  const audioUrl = data?.output?.audio?.url;
  if (!audioUrl) {
    return {
      success: false,
      error: data?.message || data?.code || "未获取到音频 URL",
    };
  }

  const buf = await fetchArrayBuffer(audioUrl);
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
  fs.writeFileSync(outputPath, buf);
  return { success: true, audioPath: outputPath };
}

/**
 * 通义千问 TTS 合成（支持长文本分段，对齐 Electron synthesizeSpeech）
 * @param {{ apiKey: string, text: string, voice: string, model?: string, instruction?: string, languageType?: string, outputPath: string }} opts
 */
async function qwenTtsSynthesize(opts) {
  const apiKey = opts.apiKey;
  const text = String(opts.text || "").trim();
  const voice = String(opts.voice || "").trim();
  const outputPath = opts.outputPath;
  const model = opts.model || resolveTtsModel(voice);
  const instruction = String(opts.instruction || "").trim();
  const languageType = opts.languageType;

  if (!apiKey) return { success: false, error: "缺少 DashScope apiKey" };
  if (!text) return { success: false, error: "缺少合成文本" };
  if (!voice) return { success: false, error: "缺少 voice" };
  if (!outputPath) return { success: false, error: "缺少 outputPath" };

  const instrLen = instruction.length;
  const isVcModel = model.includes("-vc") || model.includes("_vc");
  const baseMaxLen = isVcModel ? 200 : 300;
  const effectiveMaxLen = Math.max(100, baseMaxLen - instrLen);
  const segments = splitTextForTts(text, effectiveMaxLen);

  const config = { apiKey, voice, model, instruction, languageType };

  try {
    if (segments.length === 1) {
      const r = await qwenTtsSynthesizeOneSegment(segments[0], config, outputPath);
      if (!r.success) return r;
      log("info", "qwen tts saved", { outputPath, voice, model });
      return { success: true, audioPath: outputPath };
    }

    const parsed = path.parse(outputPath);
    const ext = parsed.ext || ".wav";
    const segmentPaths = [];
    for (let i = 0; i < segments.length; i++) {
      const segPath = path.join(
        parsed.dir,
        `${parsed.name}_seg${i}${ext}`,
      );
      log("info", "qwen tts segment", { i: i + 1, total: segments.length, len: segments[i].length });
      const segResult = await qwenTtsSynthesizeOneSegment(segments[i], config, segPath);
      if (!segResult.success) return segResult;
      segmentPaths.push(segPath);
    }

    const outExt = ext.toLowerCase();
    if (outExt === ".wav") {
      concatWavFiles(segmentPaths, outputPath);
    } else {
      const wavOut = path.join(parsed.dir, `${parsed.name}_merged.wav`);
      concatWavFiles(segmentPaths, wavOut);
      try {
        const ffmpeg = requireFfmpeg();
        await runCommand(ffmpeg, [
          "-y",
          "-i",
          wavOut,
          "-c:a",
          "libmp3lame",
          "-b:a",
          "128k",
          outputPath,
        ]);
        deleteFile(wavOut);
      } catch (e) {
        fs.copyFileSync(wavOut, outputPath.replace(/\.mp3$/i, ".wav"));
        return {
          success: false,
          error: `分段合成成功但转 mp3 失败: ${e.message}（已保存 wav）`,
        };
      }
    }

    for (const p of segmentPaths) deleteFile(p);
    log("info", "qwen tts merged", { outputPath, segments: segments.length });
    return { success: true, audioPath: outputPath };
  } catch (e) {
    return { success: false, error: e.message || String(e) };
  }
}

async function aliyunAsrFiletrans(audioUrl, opts) {
  const apiKey = opts.apiKey || opts.api_key;
  const model = opts.model || "qwen3-asr-flash-filetrans";
  log("info", "apiKey",apiKey);
  log("info", "model",model);
  log("info", "audioUrl",audioUrl);
  if (!apiKey) {
    return { success: false, error: "缺少 DashScope apiKey" };
  }
  const fileUrl = String(audioUrl || "").trim();
  if (!/^https?:\/\//i.test(fileUrl)) {
    return { success: false, error: "ASR 需要可访问的 http(s) 音频 URL（请检查 OSS 上传与签名）" };
  }
  const input =
    model === "qwen3-asr-flash-filetrans"
      ? { file_url: fileUrl }
      : { file_urls: [fileUrl] };
  const submit = await fetchJson(
    "https://dashscope.aliyuncs.com/api/v1/services/audio/asr/transcription",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "X-DashScope-Async": "enable",
      },
      body: JSON.stringify({
        model,
        input,
        parameters: {
          channel_id: [0],
          enable_itn: false,
        },
      }),
    }
  );
  const taskId = submit?.output?.task_id;
  if (!taskId) {
    return { success: false, error: `ASR 提交失败: ${JSON.stringify(submit).slice(0, 200)}` };
  }
  log("info", "voice recognition taskId",taskId);
  for (let i = 0; i < 60; i++) {
    await sleep(5000);
    const poll = await fetchJson(
      `https://dashscope.aliyuncs.com/api/v1/tasks/${taskId}`,
      { headers: { Authorization: `Bearer ${apiKey}` } }
    );
    const status = poll?.output?.task_status;
    log("info", "voice recognition status",status);
    if (status === "SUCCEEDED") {
      const transUrl = extractTranscriptionUrl(poll?.output);
      if (!transUrl) {
        log("error", "voice recognition output", poll?.output);
        return {
          success: false,
          error: "ASR 成功但无 transcription_url（期望 output.result 或 output.results[0]）",
        };
      }
      const trans = await fetchJson(transUrl);
      const fullText = parseTranscriptionText(trans);
      const sentences = parseTranscriptionSentences(trans);
      return { success: true, text: fullText, sentences };
    }
    if (status === "FAILED" || status === "UNKNOWN") {
      const detail =
        poll?.output?.message ||
        poll?.output?.code ||
        poll?.message ||
        (poll?.output ? JSON.stringify(poll.output).slice(0, 300) : null);
      return {
        success: false,
        error: detail || "ASR 任务失败",
      };
    }
  }
  return { success: false, error: "ASR 轮询超时" };
}

async function localAsr(audioPath, info) {
  const baseUrl = (info.baseUrl || info.localPath || "").replace(/\/$/, "");
  if (!baseUrl) {
    return { success: false, error: "未配置本地 ASR 服务地址" };
  }
  const asrPath = info.asrPath || "/asr";
  const url = `${baseUrl}${asrPath.startsWith("/") ? asrPath : `/${asrPath}`}`;
  const boundary = `----aiclient${Date.now()}`;
  const fileBuf = fs.readFileSync(audioPath);
  const head = Buffer.from(
    `--${boundary}\r\nContent-Disposition: form-data; name="audio"; filename="audio.wav"\r\nContent-Type: audio/wav\r\n\r\n`,
    "utf8"
  );
  const tail = Buffer.from(`\r\n--${boundary}--\r\n`, "utf8");
  const body = Buffer.concat([head, fileBuf, tail]);
  const u = new URL(url);
  const client = u.protocol === "https:" ? https : http;

  return new Promise((resolve) => {
    const req = client.request(
      {
        hostname: u.hostname,
        port: u.port || (u.protocol === "https:" ? 443 : 80),
        path: u.pathname + u.search,
        method: "POST",
        headers: {
          "Content-Type": `multipart/form-data; boundary=${boundary}`,
          "Content-Length": body.length,
        },
      },
      (res) => {
        const chunks = [];
        res.on("data", (c) => chunks.push(c));
        res.on("end", () => {
          try {
            const raw = Buffer.concat(chunks).toString("utf8");
            const data = JSON.parse(raw);
            if (data.code === 0 || data.success) {
              const records =
                data.data?.data?.records ||
                data.data?.records ||
                data.records ||
                [];
              const sentences = Array.isArray(records)
                ? normalizeAsrRecords(
                    records
                      .map((r) => {
                        const text = String(r.text || "").trim();
                        if (!text) return null;
                        const start = normalizeAsrTimeToMs(
                          r.start ?? r.begin_time ?? 0,
                        );
                        const end = normalizeAsrTimeToMs(
                          r.end ?? r.end_time ?? start + 2000,
                        );
                        return { text, start, end };
                      })
                      .filter(Boolean),
                  )
                : [];
              const text = sentences.length
                ? sentences.map((r) => r.text).join("")
                : data.text || data.data?.text || "";
              resolve({ success: true, text: String(text), sentences });
            } else {
              resolve({ success: false, error: data.msg || data.error || raw.slice(0, 200) });
            }
          } catch (e) {
            resolve({ success: false, error: e.message });
          }
        });
      }
    );
    req.on("error", (e) => resolve({ success: false, error: e.message }));
    req.write(body);
    req.end();
  });
}

const MIN_ENROLLMENT_SAMPLE_RATE = 16000;
const VOICE_ENROLL_URL =
  "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization";

async function ensureEnrollmentAudio(filePath) {
  const ffmpeg = requireFfmpeg();
  const normalized = (p) => (process.platform === "win32" ? p.replace(/\\/g, "/") : p);
  let sampleRate = 0;
  let channels = 0;
  try {
    const ffprobe = locateFfmpeg()?.replace(/ffmpeg(\.exe)?$/i, "ffprobe$1");
    if (ffprobe && fs.existsSync(ffprobe)) {
      const stdout = execFileSync(
        ffprobe,
        [
          "-v",
          "error",
          "-select_streams",
          "a:0",
          "-show_entries",
          "stream=sample_rate,channels",
          "-of",
          "json",
          filePath,
        ],
        { encoding: "utf8", windowsHide: true },
      );
      const probe = JSON.parse(stdout || "{}");
      const stream = (probe.streams && probe.streams[0]) || {};
      sampleRate = parseInt(stream.sample_rate || "0", 10) || 0;
      channels = parseInt(stream.channels || "0", 10) || 0;
    }
  } catch {
    /* 转码兜底 */
  }
  const ext = path.extname(filePath).toLowerCase();
  const needsConvert =
    !sampleRate ||
    sampleRate < MIN_ENROLLMENT_SAMPLE_RATE ||
    channels !== 1 ||
    ext !== ".wav";
  if (!needsConvert) return filePath;

  const parsed = path.parse(filePath);
  const outputPath = path.join(parsed.dir, `${parsed.name}_enroll_16k.wav`);
  await runCommand(ffmpeg, [
    "-y",
    "-i",
    normalized(filePath),
    "-vn",
    "-acodec",
    "pcm_s16le",
    "-ar",
    String(MIN_ENROLLMENT_SAMPLE_RATE),
    "-ac",
    "1",
    normalized(outputPath),
  ]);
  return outputPath;
}

function audioFileToDataUri(filePath) {
  const ext = path.extname(filePath).toLowerCase();
  const mimeTypes = {
    ".wav": "audio/wav",
    ".mp3": "audio/mpeg",
    ".m4a": "audio/m4a",
    ".flac": "audio/flac",
    ".ogg": "audio/ogg",
    ".aac": "audio/aac",
    ".webm": "audio/webm",
  };
  const mime = mimeTypes[ext] || "audio/wav";
  const buf = fs.readFileSync(filePath);
  return `data:${mime};base64,${buf.toString("base64")}`;
}

/**
 * 阿里云声音复刻（对齐 Electron aliyun:cosyvoice:enroll）
 * @param {{ apiKey: string, audioPath: string, name: string }} opts
 */
async function qwenVoiceEnroll(opts) {
  const apiKey = String(opts.apiKey || "").trim();
  const audioPath = String(opts.audioPath || "").trim();
  const name = String(opts.name || "").trim();
  if (!apiKey) return { success: false, error: "缺少 DashScope apiKey" };
  if (!audioPath || !fs.existsSync(audioPath)) {
    return { success: false, error: "参考音频文件不存在" };
  }
  if (!name) return { success: false, error: "请先输入音色名称" };

  try {
    const prepared = await ensureEnrollmentAudio(audioPath);
    const dataUri = audioFileToDataUri(prepared);
    let prefix = name.replace(/[^a-zA-Z0-9_]/g, "");
    if (!prefix) prefix = "custom";
    else if (prefix.length > 10) prefix = prefix.substring(0, 10);

    const payload = {
      model: "qwen-voice-enrollment",
      input: {
        action: "create",
        target_model: "qwen3-tts-vc-2026-01-22",
        preferred_name: prefix,
        audio: { data: dataUri },
      },
    };

    const data = await fetchJson(VOICE_ENROLL_URL, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });

    const voiceId = data?.output?.voice || data?.output?.voice_id;
    if (!voiceId) {
      return {
        success: false,
        error: data?.message || data?.code || "复刻响应格式错误",
      };
    }
    return { success: true, voiceId: String(voiceId) };
  } catch (e) {
    return { success: false, error: e.message || String(e) };
  }
}

async function openaiChat(req) {
  let apiUrl = (req.apiUrl || "").replace(/\/$/, "");
  if (!apiUrl.includes("/chat/completions")) {
    apiUrl = apiUrl.endsWith("/v1")
      ? `${apiUrl}/chat/completions`
      : `${apiUrl}/v1/chat/completions`;
  }
  const body = {
    model: req.model,
    messages: req.messages,
    stream: false,
    temperature: typeof req.temperature === "number" ? req.temperature : 0.8,
    max_tokens: req.max_tokens || req.maxTokens || 2000,
  };
  try {
    const data = await fetchJson(apiUrl, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${req.apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });
    const content = data?.choices?.[0]?.message?.content;
    if (!content) {
      return { success: false, error: "模型返回为空" };
    }
    return { success: true, content: String(content).trim() };
  } catch (e) {
    return { success: false, error: e.message };
  }
}

module.exports = {
  tempPath,
  deleteFile,
  ffmpegExtractAudio,
  aliyunOssUpload,
  aliyunAsrFiletrans,
  localAsr,
  parseTranscriptionSentences,
  openaiChat,
  resolveTtsModel,
  splitTextForTts,
  qwenTtsSynthesize,
  qwenVoiceEnroll,
  locateFfmpeg,
};
