// Atomic, locked access to calendar.json so concurrent runs (write-daily, publish-tick,
// research refill) can't clobber each other. Every mutation re-reads under an exclusive
// lockfile, mutates, and writes — no long-held in-memory copies.
import { existsSync, readFileSync, writeFileSync, openSync, closeSync, unlinkSync, statSync } from "node:fs";

const LOCK_STALE_MS = 10 * 60 * 1000; // a lock older than this is presumed orphaned
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function acquire(calPath, timeoutMs = 120000) {
  const lp = `${calPath}.lock`;
  const start = Date.now();
  for (;;) {
    try {
      const fd = openSync(lp, "wx"); // exclusive create — fails if it exists
      writeFileSync(lp, `${process.pid} ${new Date().toISOString()}`);
      closeSync(fd);
      return lp;
    } catch {
      try {
        if (Date.now() - statSync(lp).mtimeMs > LOCK_STALE_MS) {
          unlinkSync(lp);
          continue;
        }
      } catch {
        /* lock vanished — retry */
      }
      if (Date.now() - start > timeoutMs) throw new Error(`calendar lock timeout: ${lp}`);
      await sleep(200);
    }
  }
}
const release = (lp) => {
  try {
    unlinkSync(lp);
  } catch {
    /* already gone */
  }
};

export async function withCalendarLock(calPath, fn) {
  const lp = await acquire(calPath);
  try {
    return await fn();
  } finally {
    release(lp);
  }
}

export const readCalendar = (calPath) =>
  existsSync(calPath) ? JSON.parse(readFileSync(calPath, "utf8")) : [];

const writeCalendar = (calPath, cal) => writeFileSync(calPath, JSON.stringify(cal, null, 2));

/** Locked read-modify-write of one entry (Object.assign patch). Returns the updated entry. */
export async function updateEntry(calPath, slug, patch) {
  return withCalendarLock(calPath, () => {
    const cal = readCalendar(calPath);
    const e = cal.find((x) => x.slug === slug);
    if (e) {
      Object.assign(e, patch);
      writeCalendar(calPath, cal);
    }
    return e;
  });
}

/** Locked append if the slug is not already present. */
export async function addEntry(calPath, entry) {
  return withCalendarLock(calPath, () => {
    const cal = readCalendar(calPath);
    if (!cal.some((x) => x.slug === entry.slug)) {
      cal.push(entry);
      writeCalendar(calPath, cal);
    }
    return entry;
  });
}

/** Locked merge: append each new entry whose slug isn't present. Returns count added. */
export async function mergeEntries(calPath, entries) {
  return withCalendarLock(calPath, () => {
    const cal = readCalendar(calPath);
    const have = new Set(cal.map((x) => x.slug));
    let added = 0;
    for (const e of entries) {
      if (!have.has(e.slug)) {
        cal.push(e);
        have.add(e.slug);
        added++;
      }
    }
    writeCalendar(calPath, cal);
    return added;
  });
}
