#!/usr/bin/env node

/**
 * 同时热启动前端、FastAPI 与 Celery。
 * 可使用以下环境变量调整行为：
 * - FRONTEND_CMD：覆盖前端启动命令，默认为 "pnpm dev"
 * - BACKEND_CMD：覆盖 FastAPI 启动命令，默认为 "python -m uvicorn app.main:app --reload --port 8000"
 * - CELERY_CMD：覆盖 Celery 启动命令，默认为 "celery -A app.workers.celery_app worker --loglevel=info"
 * - CELERY_POOL：只在 CELERY_CMD 未覆盖时生效，设置 Celery worker pool；Windows 默认 "solo"，其他平台默认使用 Celery 自身默认值
 * - PYTHON_BIN：只在 BACKEND_CMD 未覆盖时，设置 Python 可执行文件，默认 "python"
 * - CELERY_BIN：只在 CELERY_CMD 未覆盖时，设置 Celery 可执行文件，默认 "celery"
 * - REDIS_URL：仅用于探测 Redis 是否就绪，默认 "redis://localhost:6379/0"
 * - SKIP_CELERY：设为 "1" or "true" 可跳过 Celery。
 */

const { spawn, execSync } = require("child_process");
const path = require("path");
const net = require("net");
const { URL } = require("url");
const fs = require("fs");

const rootDir = path.resolve(__dirname, "..");
const frontendDir = path.join(rootDir, "wolai-frontend");
const backendDir = path.join(rootDir, "wolai-backend");

function resolveBackendExecutable(envName, fallbackName) {
  const fromEnv = (process.env[envName] || "").trim();
  if (fromEnv) return fromEnv;

  const isWin = process.platform === "win32";
  const candidates = isWin
    ? [
        path.join(backendDir, ".venv", "Scripts", `${fallbackName}.exe`),
        path.join(backendDir, ".venv312", "Scripts", `${fallbackName}.exe`),
        path.join(backendDir, "venv", "Scripts", `${fallbackName}.exe`),
      ]
    : [
        path.join(backendDir, ".venv-linux", "bin", fallbackName),
        path.join(backendDir, ".venv", "bin", fallbackName),
        path.join(backendDir, "venv", "bin", fallbackName),
      ];

  for (const candidate of candidates) {
    if (fs.existsSync(candidate)) {
      return candidate;
    }
  }

  return fallbackName;
}

const pythonBin = resolveBackendExecutable("PYTHON_BIN", "python");
const celeryBin = resolveBackendExecutable("CELERY_BIN", "celery");
const celeryPoolFromEnv = (process.env.CELERY_POOL || "").trim();
const skipCelery =
  (process.env.SKIP_CELERY || "").toLowerCase() === "1" ||
  (process.env.SKIP_CELERY || "").toLowerCase() === "true";
const celeryCmdFromEnv = process.env.CELERY_CMD;
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379/0";
const frontendPortFromEnv = Number(process.env.FRONTEND_PORT || 3000);
const backendPortFromEnv = Number(process.env.BACKEND_PORT || 8000);

const tasks = [
  {
    name: "frontend",
    command: process.env.FRONTEND_CMD || "pnpm dev",
    cwd: frontendDir,
  },
  {
    name: "backend",
    command:
      process.env.BACKEND_CMD ||
      `${pythonBin} -m uvicorn app.main:app --reload --port 8000`,
    cwd: backendDir,
  },
];

const children = [];
let shuttingDown = false;

async function isPortFree(host, port, timeoutMs = 400) {
  return await new Promise((resolve) => {
    const socket = net.createConnection({ host, port });
    const timer = setTimeout(() => {
      socket.destroy();
      resolve(true);
    }, timeoutMs);

    socket.once("connect", () => {
      clearTimeout(timer);
      socket.end();
      resolve(false);
    });

    socket.once("error", () => {
      clearTimeout(timer);
      resolve(true);
    });
  });
}

async function findFreePort(startPort) {
  let port = Number.isFinite(startPort) ? Math.floor(startPort) : 3000;
  port = Math.max(1, Math.min(65535, port));

  // 最多尝试 20 个端口，避免无限循环。
  for (let i = 0; i < 20; i += 1) {
    // eslint-disable-next-line no-await-in-loop
    const free = await isPortFree("127.0.0.1", port);
    if (free) return port;
    port += 1;
  }

  return startPort;
}

function getListeningPidsByPort(port) {
  const pids = new Set();

  if (process.platform !== "win32") {
    try {
      const out = execSync(`ss -ltnp 'sport = :${port}'`, { encoding: "utf8" });
      for (const match of out.matchAll(/pid=(\d+)/g)) {
        const pid = Number(match[1]);
        if (Number.isFinite(pid) && pid > 0) {
          pids.add(pid);
        }
      }
    } catch {
      // ignore
    }

    if (pids.size > 0) {
      return [...pids];
    }

    try {
      const out = execSync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`, { encoding: "utf8" });
      for (const line of out.split(/\r?\n/)) {
        const pid = Number(line.trim());
        if (Number.isFinite(pid) && pid > 0) {
          pids.add(pid);
        }
      }
    } catch {
      // ignore
    }

    return [...pids];
  }

  try {
    // 说明：netstat 输出示例：
    // TCP    0.0.0.0:3000   0.0.0.0:0   LISTENING   12345
    const out = execSync("netstat -ano -p tcp", { encoding: "utf8" });
    for (const line of out.split(/\r?\n/)) {
      if (!line.includes(`:${port}`)) continue;
      if (!/LISTENING/i.test(line)) continue;
      const parts = line.trim().split(/\s+/);
      const pid = Number(parts[parts.length - 1]);
      if (Number.isFinite(pid) && pid > 0) {
        pids.add(pid);
      }
    }
    return [...pids];
  } catch {
    return [];
  }
}

function getProcessNameByPid(pid) {
  if (process.platform !== "win32") {
    try {
      return execSync(`ps -p ${pid} -o comm=`, { encoding: "utf8" }).trim();
    } catch {
      return "";
    }
  }

  try {
    // 输出为 CSV，示例：
    // "Image Name","PID","Session Name","Session#","Mem Usage"
    // "python.exe","12345","Console","1","12,345 K"
    const out = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, {
      encoding: "utf8",
    }).trim();
    if (!out || /No tasks are running/i.test(out)) return "";
    const firstLine = out.split(/\r?\n/)[0].trim();
    if (!firstLine) return "";
    const cells = firstLine
      .split('","')
      .map((s) => s.replace(/^"/, "").replace(/"$/, ""));
    return (cells[0] || "").trim();
  } catch {
    return "";
  }
}

async function ensurePortFree(port, nameForLog) {
  const free = await isPortFree("127.0.0.1", port);
  if (free) return true;

  const pids = getListeningPidsByPort(port);
  if (pids.length === 0) {
    logPrefix(nameForLog, `检测到端口 ${port} 被占用，但无法定位 PID。`);
    return false;
  }

  let killPids = pids;
  // 安全策略：后端默认只结束 python 进程（避免误杀其他服务）。
  if (nameForLog === "backend") {
    killPids = pids.filter((pid) => {
      const procName = getProcessNameByPid(pid).toLowerCase();
      return procName.includes("python");
    });
    if (killPids.length === 0) {
      logPrefix(
        nameForLog,
        `检测到端口 ${port} 被占用，但没有可安全结束的 python 进程（PIDs=${pids.join(", ")}）。请手动释放端口后重试。`,
      );
      return false;
    }
  }

  logPrefix(nameForLog, `检测到端口 ${port} 被占用，准备重启（结束旧进程）：${killPids.join(", ")}`);
  for (const pid of killPids) {
    try {
      execSync(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
    } catch {
      // ignore
    }
  }

  // 等待端口释放
  for (let i = 0; i < 20; i += 1) {
    // eslint-disable-next-line no-await-in-loop
    const ok = await isPortFree("127.0.0.1", port, 250);
    if (ok) return true;
    // eslint-disable-next-line no-await-in-loop
    await new Promise((r) => setTimeout(r, 150));
  }

  logPrefix(nameForLog, `端口 ${port} 仍未释放，可能有其他程序占用。`);
  return false;
}

function loadEnvFile(filePath) {
  if (!fs.existsSync(filePath)) return {};
  const content = fs.readFileSync(filePath, "utf8");
  return content
    .split(/\r?\n/)
    .filter((line) => line.trim() && !line.trim().startsWith("#"))
    .reduce((acc, line) => {
      const idx = line.indexOf("=");
      if (idx === -1) return acc;
      const key = line.slice(0, idx).trim();
      const value = line.slice(idx + 1).trim();
      acc[key] = value;
      return acc;
    }, {});
}

// 约定：全局仅使用仓库根目录的 .env.all 作为配置来源（生产优先）。
const envAllPath = path.join(rootDir, ".env.all");
if (!fs.existsSync(envAllPath)) {
  console.error("[system] 缺少 .env.all：请在仓库根目录创建全局唯一 env 文件后重试。");
  process.exit(1);
}
const envFromAll = loadEnvFile(envAllPath);
const mergedEnv = {
  ...process.env,
  ...envFromAll,
};

function logPrefix(name, message) {
  console.log(`[${name}] ${message}`);
}

function getDefaultCeleryPool() {
  if (celeryPoolFromEnv) return celeryPoolFromEnv;
  if (process.platform === "win32") return "solo";
  return "";
}

function buildDefaultCeleryCommand() {
  const pool = getDefaultCeleryPool();
  const poolArg = pool ? ` --pool=${pool}` : "";
  return `${celeryBin} -A app.workers.celery_app worker --loglevel=info${poolArg}`;
}

function startTask(task) {
  logPrefix(task.name, `启动命令：${task.command}`);
  const child = spawn(task.command, {
    cwd: task.cwd,
    stdio: "inherit",
    shell: true,
    env: mergedEnv,
  });

  child.on("exit", (code, signal) => {
    if (shuttingDown) {
      return;
    }
    const status =
      signal !== null ? `因信号 ${signal} 退出` : `退出码 ${code ?? "null"}`;
    logPrefix(task.name, `进程结束（${status}），准备清理其它任务。`);
    shutdown(code ?? 0);
  });

  child.on("error", (err) => {
    logPrefix(task.name, `启动失败：${err.message}`);
    shutdown(1);
  });

  children.push(child);
}

function shutdown(code) {
  if (shuttingDown) {
    return;
  }
  shuttingDown = true;
  logPrefix("system", "收到终止信号，正在关闭所有子进程…");

  for (const child of children) {
    if (!child.pid) continue;

    // Windows 下，shell 子进程常常无法可靠传播 SIGINT/SIGTERM 到孙进程（例如 uvicorn --reload）。
    // 这里优先用 taskkill /T /F 确保整个进程树被结束，避免残留占用端口导致下次启动失败。
    if (process.platform === "win32") {
      try {
        execSync(`taskkill /PID ${child.pid} /T /F`, { stdio: "ignore" });
        continue;
      } catch {
        // fallback to signals
      }
    }

    if (!child.killed) {
      child.kill("SIGINT");
      setTimeout(() => {
        if (!child.killed) {
          child.kill("SIGTERM");
        }
      }, 5000);
    }
  }

  setTimeout(() => process.exit(code), 200);
}

process.on("SIGINT", () => shutdown(0));
process.on("SIGTERM", () => shutdown(0));

async function checkRedisReachable(urlString, timeoutMs = 2000) {
  try {
    const url = new URL(urlString);
    const host = url.hostname || "localhost";
    const port = Number(url.port) || 6379;

    return await new Promise((resolve) => {
      const socket = net.createConnection({ host, port });
      const timer = setTimeout(() => {
        socket.destroy();
        resolve(false);
      }, timeoutMs);

      socket.once("connect", () => {
        clearTimeout(timer);
        socket.end();
        resolve(true);
      });

      socket.once("error", () => {
        clearTimeout(timer);
        resolve(false);
      });
    });
  } catch (error) {
    logPrefix("celery", `REDIS_URL (${urlString}) 解析失败：${error.message}，跳过连通性检查。`);
    return true;
  }
}

async function main() {
  // 说明：Next dev 在异常退出时可能残留 `.next/dev/lock`，会导致后续启动直接失败。
  // 这里在启动前做一次“安全清理”，避免重启后仍卡住。
  const nextDevLockPath = path.join(frontendDir, ".next", "dev", "lock");
  try {
    if (fs.existsSync(nextDevLockPath)) {
      fs.rmSync(nextDevLockPath, { force: true });
      logPrefix("frontend", `检测到残留的 Next dev lock，已移除：${nextDevLockPath}`);
    }
  } catch (error) {
    logPrefix("frontend", `尝试移除 Next dev lock 失败：${error.message}`);
  }

  // 说明：你外网绑定了 3000 端口，这里默认强制使用 3000。
  // 如果检测到 3000 被占用，则自动结束旧进程后重启，以保证始终跑在 3000。
  const desiredFrontendPort = frontendPortFromEnv;
  const frontendPortOk = await ensurePortFree(desiredFrontendPort, "frontend");
  if (!frontendPortOk) {
    console.error(`前端端口 ${desiredFrontendPort} 无法释放，已中止启动。`);
    process.exit(1);
  }
  const frontendPort = desiredFrontendPort;
  const frontendUrl = `http://localhost:${frontendPort}`;

  // 说明：在 Windows 的 cmd.exe 下，`pnpm dev -- -p 3000` 会把 `--` 原样传给 next，导致 next 把 `-p` 误当成目录。
  // 用 `pnpm dev -p 3000` 在 PowerShell/cmd.exe 下都能正确传参。
  tasks[0].command = process.env.FRONTEND_CMD || `pnpm dev -p ${frontendPort}`;
  logPrefix("frontend", `前端目录：${frontendDir}`);
  logPrefix("frontend", `前端地址：${frontendUrl}`);

  const desiredBackendPort = backendPortFromEnv;
  if (!process.env.BACKEND_CMD) {
    const backendPortOk = await ensurePortFree(desiredBackendPort, "backend");
    if (!backendPortOk) {
      console.error(`后端端口 ${desiredBackendPort} 无法释放，已中止启动。`);
      process.exit(1);
    }
    tasks[1].command = `${pythonBin} -m uvicorn app.main:app --reload --port ${desiredBackendPort}`;
  }

  if (!skipCelery) {
    const defaultCeleryCommand = buildDefaultCeleryCommand();
    const celeryTask = {
      name: "celery",
      command: celeryCmdFromEnv || defaultCeleryCommand,
      cwd: backendDir,
    };

    if (celeryCmdFromEnv) {
      tasks.push(celeryTask);
    } else if (await checkRedisReachable(redisUrl)) {
      const defaultPool = getDefaultCeleryPool();
      if (defaultPool) {
        logPrefix("celery", `未显式设置 CELERY_CMD，当前平台默认使用 worker pool：${defaultPool}`);
      }
      tasks.push(celeryTask);
    } else {
      // Redis 未就绪时直接跳过 Celery，避免热调试流程整体退出。
      logPrefix(
        "celery",
        `检测到 ${redisUrl} 无法连接，自动跳过 Celery。请先启动 Redis 或设置 SKIP_CELERY=1 显式跳过。`,
      );
    }
  }

  if (tasks.length === 0) {
    console.error("未配置任何可运行的任务，检查环境变量设置。");
    process.exit(1);
  }

  for (const task of tasks) {
    startTask(task);
  }
}

main().catch((error) => {
  logPrefix("system", `启动失败：${error.message}`);
  process.exit(1);
});
