// lock-file.mjs — Exclusive lock with PID + mtime stale-detection.
// Zero dependencies. Uses fs.writeFileSync('wx') for atomic exclusive create.
// Stale-detection is OR-based: stale if PID is dead OR mtime exceeds threshold.
// Either condition alone is enough to break the lock — handles SIGKILL orphans
// (mtime alone) and PID-reuse races (mtime alone) and crashed-then-replaced
// runs (PID alone). Long runs may opt-in to mtime refresh via refreshIntervalMs.

import { writeFileSync, readFileSync, statSync, unlinkSync, utimesSync } from 'node:fs';
import { hostname } from 'node:os';
import { join } from 'node:path';
import { getCacheDir } from './cross-platform-paths.mjs';

const DEFAULT_STALE_THRESHOLD_MS = 60 * 60 * 1000; // 1 hour
const DEFAULT_LOCK_NAME = 'kb-update.lock';

/**
 * Check whether a PID identifies a live process.
 * @param {number} pid — POSIX process id
 * @returns {boolean}
 */
export function isPidAlive(pid) {
  if (typeof pid !== 'number' || !Number.isFinite(pid) || pid <= 0) {
    return false;
  }
  try {
    process.kill(pid, 0);
    return true;
  } catch (err) {
    // EPERM means the process exists but we lack signal permission — still alive.
    return err && err.code === 'EPERM';
  }
}

function safeReadLock(lockPath) {
  try {
    return JSON.parse(readFileSync(lockPath, 'utf8'));
  } catch {
    return null;
  }
}

function lockMtimeMs(lockPath) {
  try {
    return statSync(lockPath).mtimeMs;
  } catch {
    return null;
  }
}

function writeLockFile(lockPath) {
  writeFileSync(
    lockPath,
    JSON.stringify({
      pid: process.pid,
      started: Date.now(),
      host: hostname(),
      version: 1,
    }),
    { flag: 'wx', encoding: 'utf8' }
  );
}

/**
 * Acquire an exclusive lock. Throws ELOCKED if held by a live, fresh holder.
 * Cleans up stale locks (dead PID OR mtime older than staleThresholdMs).
 *
 * @param {string} [lockPath] — absolute lock-file path; defaults to <cache>/kb-update.lock
 * @param {object} [opts]
 * @param {number} [opts.staleThresholdMs] — default 3600000 (1h)
 * @param {number} [opts.refreshIntervalMs] — if > 0, periodically utimes the lock
 * @param {boolean} [opts.registerCleanup] — default true; install exit/signal handlers
 * @returns {{lockPath: string, release: () => void}}
 */
export function acquireLock(lockPath, opts = {}) {
  const staleThresholdMs = opts.staleThresholdMs ?? DEFAULT_STALE_THRESHOLD_MS;
  const refreshIntervalMs = opts.refreshIntervalMs ?? 0;
  const registerCleanup = opts.registerCleanup ?? true;
  const path = lockPath || join(getCacheDir('ms-ai-architect'), DEFAULT_LOCK_NAME);

  try {
    writeLockFile(path);
  } catch (err) {
    if (!err || err.code !== 'EEXIST') throw err;

    const data = safeReadLock(path);
    const mtime = lockMtimeMs(path);
    const holderPid = typeof data?.pid === 'number' ? data.pid : null;
    const pidAlive = holderPid != null ? isPidAlive(holderPid) : false;
    const ageMs = mtime != null ? Date.now() - mtime : Infinity;
    const stale = !pidAlive || ageMs > staleThresholdMs;

    if (!stale) {
      const e = new Error(
        `Lock held by PID ${holderPid} (started ${data?.started ?? 'unknown'})`
      );
      e.code = 'ELOCKED';
      e.holderPid = holderPid;
      throw e;
    }

    try {
      unlinkSync(path);
    } catch {
      // best-effort
    }
    writeLockFile(path); // retry once
  }

  let refreshTimer = null;
  let released = false;

  const release = () => {
    if (released) return;
    released = true;
    if (refreshTimer) {
      clearInterval(refreshTimer);
      refreshTimer = null;
    }
    try {
      const data = safeReadLock(path);
      if (!data || data.pid === process.pid) {
        unlinkSync(path);
      }
    } catch {
      // best-effort
    }
  };

  if (refreshIntervalMs > 0) {
    refreshTimer = setInterval(() => {
      try {
        const now = new Date();
        utimesSync(path, now, now);
      } catch {
        // best-effort
      }
    }, refreshIntervalMs);
    if (typeof refreshTimer.unref === 'function') {
      refreshTimer.unref();
    }
  }

  if (registerCleanup) {
    const onExit = () => release();
    process.once('exit', onExit);
    process.once('SIGINT', () => {
      release();
      process.exit(130);
    });
    process.once('SIGTERM', () => {
      release();
      process.exit(143);
    });
    process.once('SIGHUP', () => {
      release();
      process.exit(129);
    });
    process.once('uncaughtException', (err) => {
      release();
      console.error(err);
      process.exit(1);
    });
  }

  return { lockPath: path, release };
}
