import crypto from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";

const ROOT = process.cwd();
const RUNTIME_DIR = path.join(ROOT, ".runtime");
const DEFAULT_LOCK = process.env.CMS_DATA_DIR
  ? path.join(path.resolve(process.env.CMS_DATA_DIR), ".site-build.lock")
  : path.join(RUNTIME_DIR, "site-build.lock");
const LOCK_FILE = path.resolve(process.env.CMS_BUILD_LOCK || DEFAULT_LOCK);
const RECOVERY_LOCK_FILE = `${LOCK_FILE}.recovery`;
const ASTRO_BIN = path.join(ROOT, "node_modules", "astro", "astro.js");
const DIST_DIR = path.join(ROOT, "dist");
const MAX_LOG_LENGTH = 2 * 1024 * 1024;

export class SiteBuildLockBusyError extends Error {
  constructor() {
    super("网站正在部署或生成静态页面，请稍后重试");
    this.name = "SiteBuildLockBusyError";
  }
}

const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));

async function fileExists(file) {
  try { await fs.access(file); return true; } catch { return false; }
}

function processIsAlive(pid) {
  if (!Number.isInteger(pid) || pid <= 0) return false;
  try { process.kill(pid, 0); return true; } catch { return false; }
}

async function removeStaleLock() {
  let recoveryHandle;
  try {
    recoveryHandle = await fs.open(RECOVERY_LOCK_FILE, "wx");
    const raw = await fs.readFile(LOCK_FILE, "utf8");
    const lock = JSON.parse(raw);
    const sameHost = lock.hostname === os.hostname();
    if (sameHost && !processIsAlive(Number(lock.pid))) {
      await fs.unlink(LOCK_FILE);
      return true;
    }
  } catch (error) {
    if (error.code === "ENOENT") return true;
    if (error.code === "EEXIST") return false;
  } finally {
    if (recoveryHandle) {
      await recoveryHandle.close().catch(() => {});
      await fs.unlink(RECOVERY_LOCK_FILE).catch(() => {});
    }
  }
  return false;
}

async function acquireSiteBuildLock(timeoutMs = 15 * 60 * 1000) {
  await fs.mkdir(path.dirname(LOCK_FILE), { recursive: true });
  const started = Date.now();
  const token = crypto.randomBytes(12).toString("hex");
  while (true) {
    try {
      const handle = await fs.open(LOCK_FILE, "wx");
      await handle.writeFile(JSON.stringify({ token, pid: process.pid, hostname: os.hostname(), startedAt: new Date().toISOString() }));
      await handle.close();
      return async () => {
        try {
          const current = JSON.parse(await fs.readFile(LOCK_FILE, "utf8"));
          if (current.token === token) await fs.unlink(LOCK_FILE);
        } catch (error) {
          if (error.code !== "ENOENT") throw error;
        }
      };
    } catch (error) {
      if (error.code !== "EEXIST") throw error;
      if (await removeStaleLock()) continue;
      if (Date.now() - started >= timeoutMs) throw new SiteBuildLockBusyError();
      await delay(300);
    }
  }
}

export async function tryAcquireSiteBuildLock() {
  try {
    return await acquireSiteBuildLock(0);
  } catch (error) {
    if (error instanceof SiteBuildLockBusyError) return null;
    throw error;
  }
}

export async function withSiteBuildLock(task, options = {}) {
  const release = await acquireSiteBuildLock(options.timeoutMs);
  try { return await task(); } finally { await release(); }
}

function runAstro(command, env, onOutput) {
  return new Promise((resolve, reject) => {
    const child = spawn(process.execPath, [ASTRO_BIN, command], { cwd: ROOT, env: { ...process.env, ...env } });
    let log = "";
    const append = (chunk) => {
      const text = chunk.toString();
      log = `${log}${text}`.slice(-MAX_LOG_LENGTH);
      onOutput?.(text);
    };
    child.stdout.on("data", append);
    child.stderr.on("data", append);
    child.on("error", reject);
    child.on("exit", (code) => code === 0 ? resolve(log) : reject(Object.assign(new Error(`astro ${command} 执行失败`), { log })));
  });
}

export async function buildSiteAtomic(options = {}) {
  await fs.mkdir(RUNTIME_DIR, { recursive: true });
  const id = `${Date.now()}-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
  const temporary = path.join(RUNTIME_DIR, `dist-${id}`);
  const backup = path.join(RUNTIME_DIR, `dist-backup-${id}`);
  let movedCurrent = false;
  let log = "";
  try {
    log += await runAstro("check", {}, options.onOutput);
    log += await runAstro("build", { QIYOUXUE_BUILD_OUT_DIR: temporary }, options.onOutput);
    if (await fileExists(DIST_DIR)) {
      await fs.rename(DIST_DIR, backup);
      movedCurrent = true;
    }
    try {
      await fs.rename(temporary, DIST_DIR);
    } catch (error) {
      if (movedCurrent) await fs.rename(backup, DIST_DIR);
      throw error;
    }
    if (movedCurrent) {
      await fs.rm(backup, { recursive: true, force: true }).catch((error) => {
        options.onOutput?.(`\n旧构建目录清理失败，可稍后手动清理：${error.message}\n`);
      });
    }
    return log;
  } catch (error) {
    await fs.rm(temporary, { recursive: true, force: true });
    if (movedCurrent && !await fileExists(DIST_DIR) && await fileExists(backup)) await fs.rename(backup, DIST_DIR);
    if (error.log) error.log = `${log}${error.log}`.slice(-MAX_LOG_LENGTH);
    else error.log = log;
    throw error;
  }
}
