"use strict";

/**
 * 口播视频（云端）：RunningHub OpenAPI v2（对齐 Electron VideoGen.runCloudMode）
 * 配置（desktop_configs → AICLIENT_CFG_*）：
 *   VIDEO_GEN_MODE=online
 *   RUNNINGHUB_API_KEY
 *   RUNNINGHUB_WORKFLOW_ID
 *   RUNNINGHUB_BASE_URL（可选，默认 https://www.runninghub.cn）
 *   RUNNINGHUB_INSTANCE_TYPE（可选，default | plus）
 *   RUNNINGHUB_NODES（可选 JSON，节点 field 映射）
 */

const fs = require("fs");
const path = require("path");
const os = require("os");
const http = require("http");
const https = require("https");
const { spawnSync } = require("child_process");
const desktopConfig = require("./desktop_config.js");

const DEFAULT_BASE = "https://www.runninghub.cn";
const POLL_MS = 3000;
const MAX_POLLS = 600;

const DEFAULT_NODES = {
  videoNode: "88",
  audioNode: "15",
  durationNode: "60",
  fpsNode: "63",
  widthNode: "67",
  heightNode: "69",
};

function cfg(key, fallback = "") {
  return desktopConfig.get(key, fallback);
}

function parseNodes() {
  const raw = cfg("RUNNINGHUB_NODES", "");
  if (!raw) return { ...DEFAULT_NODES };
  try {
    const parsed = JSON.parse(raw);
    return { ...DEFAULT_NODES, ...parsed };
  } catch {
    return { ...DEFAULT_NODES };
  }
}

function apiConfig() {
  const apiKey = cfg("RUNNINGHUB_API_KEY");
  const workflowId =
    cfg("RUNNINGHUB_WORKFLOW_ID") || cfg("RUNNINGHUB_WORKFLOW_ID_1") || "";
  const baseUrl = String(cfg("RUNNINGHUB_BASE_URL", DEFAULT_BASE))
    .trim()
    .replace(/\/+$/, "");
  const instanceType = cfg("RUNNINGHUB_INSTANCE_TYPE", "default") || "default";
  return { apiKey, workflowId, baseUrl, instanceType, nodes: parseNodes() };
}

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

function requestBuffer(method, urlStr, body, headers = {}) {
  return new Promise((resolve, reject) => {
    const url = new URL(urlStr);
    const lib = url.protocol === "https:" ? https : http;
    const payload = body ? Buffer.from(body) : null;
    const opts = {
      method,
      hostname: url.hostname,
      port: url.port || (url.protocol === "https:" ? 443 : 80),
      path: url.pathname + url.search,
      headers: {
        Host: url.hostname,
        ...headers,
      },
    };
    if (payload) {
      opts.headers["Content-Length"] = payload.length;
    }
    const req = lib.request(opts, (res) => {
      const chunks = [];
      res.on("data", (c) => chunks.push(c));
      res.on("end", () => {
        const buf = Buffer.concat(chunks);
        let data;
        const text = buf.toString("utf8");
        try {
          data = text ? JSON.parse(text) : {};
        } catch {
          data = { raw: text };
        }
        resolve({ status: res.statusCode || 0, data, text });
      });
    });
    req.on("error", reject);
    if (payload) req.write(payload);
    req.end();
  });
}

function postMultipart(urlStr, filePath, apiKey) {
  return new Promise((resolve, reject) => {
    const boundary = `----RunningHub${Date.now()}`;
    const fileName = path.basename(filePath);
    const fileData = fs.readFileSync(filePath);
    const head = Buffer.from(
      `--${boundary}\r\n` +
        `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n` +
        `Content-Type: application/octet-stream\r\n\r\n`,
    );
    const tail = Buffer.from(`\r\n--${boundary}--\r\n`);
    const body = Buffer.concat([head, fileData, tail]);

    const url = new URL(urlStr);
    const lib = url.protocol === "https:" ? https : http;
    const opts = {
      method: "POST",
      hostname: url.hostname,
      port: url.port || (url.protocol === "https:" ? 443 : 80),
      path: url.pathname + url.search,
      headers: {
        Host: url.hostname,
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": `multipart/form-data; boundary=${boundary}`,
        "Content-Length": body.length,
      },
    };
    const req = lib.request(opts, (res) => {
      const chunks = [];
      res.on("data", (c) => chunks.push(c));
      res.on("end", () => {
        const text = Buffer.concat(chunks).toString("utf8");
        let data;
        try {
          data = text ? JSON.parse(text) : {};
        } catch {
          data = { raw: text };
        }
        if (res.statusCode >= 400) {
          reject(
            new Error(
              data.message || data.msg || data.errorMessage || `HTTP ${res.statusCode}`,
            ),
          );
          return;
        }
        resolve(data);
      });
    });
    req.on("error", reject);
    req.write(body);
    req.end();
  });
}

async function uploadFile(baseUrl, apiKey, filePath) {
  const url = `${baseUrl}/openapi/v2/media/upload/binary`;
  const data = await postMultipart(url, filePath, apiKey);
  if (data.code !== 0 && data.code !== undefined) {
    throw new Error(data.message || data.msg || "上传失败");
  }
  const fileName = data.data?.fileName || data.fileName;
  if (!fileName) {
    throw new Error(`上传未返回 fileName: ${JSON.stringify(data).slice(0, 300)}`);
  }
  return fileName;
}

function locateFfmpegBinary() {
  const fromEnv = process.env.AICLIENT_FFMPEG_PATH;
  if (fromEnv && fs.existsSync(fromEnv)) return fromEnv;
  return process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg";
}

function locateFfprobeBinary() {
  const fromEnv = process.env.AICLIENT_FFPROBE_PATH;
  if (fromEnv && fs.existsSync(fromEnv)) return fromEnv;
  const ffmpeg = locateFfmpegBinary();
  if (ffmpeg && ffmpeg !== "ffmpeg" && ffmpeg !== "ffmpeg.exe") {
    const dir = path.dirname(ffmpeg);
    const sibling = path.join(dir, process.platform === "win32" ? "ffprobe.exe" : "ffprobe");
    if (fs.existsSync(sibling)) return sibling;
  }
  return process.platform === "win32" ? "ffprobe.exe" : "ffprobe";
}

function parseDurationFromFfmpegStderr(text) {
  const m = String(text || "").match(/Duration:\s*(\d+):(\d+):(\d+(?:\.\d+)?)/);
  if (!m) return 0;
  const h = parseFloat(m[1]);
  const min = parseFloat(m[2]);
  const s = parseFloat(m[3]);
  if (!Number.isFinite(h + min + s)) return 0;
  return h * 3600 + min * 60 + s;
}

function probeAudioDuration(audioPath) {
  const ffprobe = locateFfprobeBinary();
  const r = spawnSync(
    ffprobe,
    [
      "-v",
      "error",
      "-show_entries",
      "format=duration",
      "-of",
      "default=noprint_wrappers=1:nokey=1",
      audioPath,
    ],
    { encoding: "utf8", windowsHide: true },
  );
  if (r.status === 0) {
    const n = parseFloat(String(r.stdout || "").trim());
    if (Number.isFinite(n) && n > 0) return n;
  }

  const ffmpeg = locateFfmpegBinary();
  const r2 = spawnSync(ffmpeg, ["-i", audioPath], {
    encoding: "utf8",
    windowsHide: true,
  });
  const fromStderr = parseDurationFromFfmpegStderr(r2.stderr || "");
  if (fromStderr > 0) return fromStderr;
  return 0;
}

async function createTask(config, videoFileName, audioFileName, audioDuration) {
  const { apiKey, workflowId, baseUrl, instanceType, nodes } = config;
  const nodeInfoList = [
    {
      nodeId: nodes.videoNode,
      fieldName: "video",
      fieldValue: videoFileName,
      description: "请导入视频",
    },
    {
      nodeId: nodes.audioNode,
      fieldName: "audio",
      fieldValue: audioFileName,
      description: "请导入新的音频",
    },
  ];
  if (audioDuration > 0 && nodes.durationNode) {
    nodeInfoList.push({
      nodeId: nodes.durationNode,
      fieldName: "value",
      fieldValue: String(Math.ceil(audioDuration)),
      description: "视频时长设置（秒）",
    });
  }
  if (nodes.fpsNode) {
    nodeInfoList.push({
      nodeId: nodes.fpsNode,
      fieldName: "int",
      fieldValue: "30",
      description: "视频帧率",
    });
  }
  if (nodes.widthNode) {
    nodeInfoList.push({
      nodeId: nodes.widthNode,
      fieldName: "int",
      fieldValue: "576",
      description: "视频宽度",
    });
  }
  if (nodes.heightNode) {
    nodeInfoList.push({
      nodeId: nodes.heightNode,
      fieldName: "int",
      fieldValue: "1024",
      description: "视频高度",
    });
  }

  const payload = JSON.stringify({
    randomSeed: true,
    nodeInfoList,
    instanceType,
    retainSeconds: 0,
    usePersonalQueue: false,
  });

  const url = `${baseUrl}/openapi/v2/run/ai-app/${workflowId}`;
  const { status, data } = await requestBuffer("POST", url, payload, {
    "Content-Type": "application/json",
    Authorization: `Bearer ${apiKey}`,
  });
  if (status >= 400) {
    throw new Error(
      data.errorMessage || data.message || data.msg || `创建任务失败 HTTP ${status}`,
    );
  }
  const taskId = data.taskId;
  if (!taskId) {
    throw new Error(data.errorMessage || "创建任务失败：未返回 taskId");
  }
  return taskId;
}

async function queryTask(config, taskId) {
  const url = `${config.baseUrl}/openapi/v2/query`;
  const { data } = await requestBuffer(
    "POST",
    url,
    JSON.stringify({ taskId }),
    {
      "Content-Type": "application/json",
      Authorization: `Bearer ${config.apiKey}`,
    },
  );
  return {
    status: data.status,
    errorMessage: data.errorMessage,
    results: data.results,
  };
}

async function waitForTask(config, taskId) {
  for (let i = 0; i < MAX_POLLS; i++) {
    const result = await queryTask(config, taskId);
    if (result.status === "SUCCESS") {
      return result;
    }
    if (result.status === "FAILED") {
      throw new Error(result.errorMessage || "RunningHub 任务失败");
    }
    await sleep(POLL_MS);
  }
  throw new Error(`RunningHub 任务超时（已等待 ${(MAX_POLLS * POLL_MS) / 60000} 分钟）`);
}

function pickResultUrl(results) {
  if (!Array.isArray(results) || !results.length) return "";
  const first = results[0];
  if (typeof first === "string") return first;
  return first.url || first.fileUrl || first.outputUrl || "";
}

function downloadFile(url, destPath) {
  return new Promise((resolve, reject) => {
    const client = url.startsWith("https") ? https : http;
    const file = fs.createWriteStream(destPath);
    client
      .get(url, (res) => {
        if (
          res.statusCode &&
          res.statusCode >= 300 &&
          res.statusCode < 400 &&
          res.headers.location
        ) {
          file.close();
          return downloadFile(res.headers.location, destPath).then(resolve).catch(reject);
        }
        if (res.statusCode !== 200) {
          reject(new Error(`下载视频失败 HTTP ${res.statusCode}`));
          return;
        }
        res.pipe(file);
        file.on("finish", () => file.close(() => resolve(destPath)));
      })
      .on("error", reject);
  });
}

async function ensureLocalVideo(videoUrl) {
  const raw = String(videoUrl || "").trim();
  if (!raw) throw new Error("未获取到视频地址");
  if (!/^https?:\/\//i.test(raw)) {
    if (raw.startsWith("file://")) return raw.replace(/^file:\/\//, "");
    return raw;
  }
  const dir = path.join(os.tmpdir(), "aiclient-runninghub");
  fs.mkdirSync(dir, { recursive: true });
  const dest = path.join(dir, `output_${Date.now()}.mp4`);
  await downloadFile(raw, dest);
  return dest;
}

globalThis.__nodejsMain = async function (params) {
  const p = params || {};
  const mode = String(cfg("VIDEO_GEN_MODE", "")).trim().toLowerCase();
  if (mode && mode !== "online") {
    return {
      success: false,
      error: `runninghub_generate.js 仅用于 VIDEO_GEN_MODE=online，当前为 ${mode}`,
    };
  }

  const config = apiConfig();
  if (!config.apiKey) {
    return {
      success: false,
      error: "缺少 RUNNINGHUB_API_KEY，请在配置管理中设置 RunningHub API 密钥",
    };
  }
  if (!config.workflowId) {
    return {
      success: false,
      error: "缺少 RUNNINGHUB_WORKFLOW_ID，请在配置管理中设置工作流 ID",
    };
  }

  const audioPath = String(p.audioPath || "").trim();
  const videoPath = String(p.avatarVideoPath || "").trim();
  if (!audioPath || !fs.existsSync(audioPath)) {
    return { success: false, error: "缺少或找不到音频文件" };
  }
  if (!videoPath || !fs.existsSync(videoPath)) {
    return { success: false, error: "缺少或找不到形象视频" };
  }

  let audioDuration = Number(p.audioDuration);
  if (!Number.isFinite(audioDuration) || audioDuration <= 0) {
    audioDuration = probeAudioDuration(audioPath);
  }
  if (audioDuration <= 0) {
    return {
      success: false,
      error:
        "无法获取音频时长。云端口播需将时长传给 RunningHub（请安装 ffmpeg/ffprobe，或升级客户端后重试）",
    };
  }
  const durationSec = Math.ceil(audioDuration);

  try {
    const videoFileName = await uploadFile(config.baseUrl, config.apiKey, videoPath);
    const audioFileName = await uploadFile(config.baseUrl, config.apiKey, audioPath);
    const taskId = await createTask(
      config,
      videoFileName,
      audioFileName,
      durationSec,
    );
    const done = await waitForTask(config, taskId);
    const remoteUrl = pickResultUrl(done.results);
    if (!remoteUrl) {
      return {
        success: false,
        error: `任务完成但未返回视频 URL: ${JSON.stringify(done).slice(0, 400)}`,
      };
    }
    const localPath = await ensureLocalVideo(remoteUrl);
    return {
      success: true,
      videoPath: localPath,
      taskId,
      audioDuration: durationSec,
    };
  } catch (e) {
    return { success: false, error: e.message || String(e) };
  }
};
