import crypto from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";

const ROOT = process.cwd();
const DATA_ROOT = path.resolve(process.env.CMS_DATA_DIR || path.join(ROOT, ".runtime", "cms-data"));
export const CONTENT_LOCK_FILE = path.resolve(process.env.CMS_CONTENT_LOCK || path.join(DATA_ROOT, ".content-write.lock"));
const RECOVERY_LOCK_FILE = `${CONTENT_LOCK_FILE}.recovery`;
const parsedStaleMs = Number(process.env.CMS_CONTENT_LOCK_STALE_MS || 15 * 60 * 1000);
const STALE_MS = Number.isFinite(parsedStaleMs) && parsedStaleMs >= 30_000 ? parsedStaleMs : 15 * 60 * 1000;

interface LockRecord {
  token: string;
  pid: number;
  hostname: string;
  createdAt: string;
  operation: string;
}

export class ContentWriteLockBusyError extends Error {
  status = 409;
  details = { retryAfter: 1 };

  constructor(operation?: string) {
    super(operation ? `另一项内容操作正在进行（${operation}），请稍后重试` : "另一项内容操作正在进行，请稍后重试");
    this.name = "ContentWriteLockBusyError";
  }
}

function processIsAlive(pid: number): boolean {
  if (!Number.isInteger(pid) || pid <= 0) return false;
  try { process.kill(pid, 0); return true; } catch (error) {
    return (error as NodeJS.ErrnoException).code === "EPERM";
  }
}

async function readLock(): Promise<LockRecord | null> {
  try {
    const parsed = JSON.parse(await fs.readFile(CONTENT_LOCK_FILE, "utf8")) as Partial<LockRecord>;
    if (!parsed.token || !parsed.createdAt) return null;
    return parsed as LockRecord;
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
    return null;
  }
}

function isStale(lock: LockRecord | null): boolean {
  if (!lock) return false;
  const age = Date.now() - new Date(lock.createdAt).valueOf();
  if (!Number.isFinite(age) || age > STALE_MS) return true;
  return lock.hostname === os.hostname() && !processIsAlive(Number(lock.pid));
}

async function unreadableLockIsStale(): Promise<boolean> {
  try {
    const stat = await fs.stat(CONTENT_LOCK_FILE);
    return Date.now() - stat.mtimeMs > STALE_MS;
  } catch (error) {
    return (error as NodeJS.ErrnoException).code === "ENOENT";
  }
}

async function recoverStaleLock(): Promise<boolean> {
  let recoveryHandle: fs.FileHandle | null = null;
  try {
    recoveryHandle = await fs.open(RECOVERY_LOCK_FILE, "wx");
    const before = await readLock();
    if (before ? !isStale(before) : !await unreadableLockIsStale()) return false;
    const current = await readLock();
    const currentIsStale = current ? isStale(current) : await unreadableLockIsStale();
    if ((before?.token || null) !== (current?.token || null) || !currentIsStale) return false;
    await fs.unlink(CONTENT_LOCK_FILE).catch((error: NodeJS.ErrnoException) => {
      if (error.code !== "ENOENT") throw error;
    });
    return true;
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "EEXIST") return false;
    throw error;
  } finally {
    if (recoveryHandle) {
      await recoveryHandle.close().catch(() => {});
      await fs.unlink(RECOVERY_LOCK_FILE).catch(() => {});
    }
  }
}

async function acquireContentWriteLock(operation: string): Promise<() => Promise<void>> {
  await fs.mkdir(path.dirname(CONTENT_LOCK_FILE), { recursive: true });
  const record: LockRecord = {
    token: crypto.randomBytes(16).toString("hex"),
    pid: process.pid,
    hostname: os.hostname(),
    createdAt: new Date().toISOString(),
    operation,
  };
  for (let attempt = 0; attempt < 2; attempt += 1) {
    try {
      const handle = await fs.open(CONTENT_LOCK_FILE, "wx");
      await handle.writeFile(`${JSON.stringify(record)}\n`);
      await handle.close();
      return async () => {
        const current = await readLock();
        if (current?.token === record.token) {
          await fs.unlink(CONTENT_LOCK_FILE).catch((error: NodeJS.ErrnoException) => {
            if (error.code !== "ENOENT") throw error;
          });
        }
      };
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
      if (attempt === 0 && await recoverStaleLock()) continue;
      const current = await readLock();
      throw new ContentWriteLockBusyError(current?.operation);
    }
  }
  throw new ContentWriteLockBusyError();
}

export async function withContentWriteLock<T>(operation: string, task: () => Promise<T>): Promise<T> {
  const release = await acquireContentWriteLock(operation);
  try {
    return await task();
  } finally {
    await release();
  }
}
