/**
 * Lock helper — thin wrapper around `proper-lockfile` providing a
 * single `withLock(filePath, fn)` entry point used by every shared-state
 * write in the framework.
 *
 * Guarantees:
 * - Mutual exclusion across processes, including across git worktrees that
 *   share the same `.aperant/` directory (lock is taken on the target
 *   file path itself).
 * - Stale-lock recovery via `proper-lockfile`'s `stale` option (default 5s).
 * - Structured timeout error emitted to `.aperant/events/` so Pre-mortem #1
 *   monitoring can catch sustained contention.
 *
 * Callers pass the path to the shared JSON/Markdown file (NOT a `.lock`
 * sidecar). `proper-lockfile` manages the sidecar internally.
 */
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import properLockfile from 'proper-lockfile';
/**
 * Acquire an exclusive lock on `filePath`, run `fn`, then release.
 *
 * The target file does not need to exist — `proper-lockfile` is configured
 * with `realpath: false` by default so we can lock paths the caller is
 * about to create. `fn` is awaited if it returns a promise.
 */
export async function withLock(filePath, fn, options = {}) {
    const resolvedPath = resolve(filePath);
    // Ensure the parent directory exists so proper-lockfile can place its sidecar.
    mkdirSync(dirname(resolvedPath), { recursive: true });
    // Ensure the target file itself exists — proper-lockfile refuses to lock
    // non-existent paths even with realpath:false on some platforms. Creating
    // an empty placeholder is harmless; downstream atomic-write paths will
    // overwrite.
    if (!existsSync(resolvedPath)) {
        writeFileSync(resolvedPath, '', { flag: 'a' });
    }
    const stale = options.stale ?? 5000;
    const retries = options.retries ?? 20;
    const minTimeout = options.retryDelayMs ?? 50;
    const maxTimeout = options.maxRetryDelayMs ?? 500;
    const realpath = options.realpath ?? false;
    let release = null;
    try {
        release = await properLockfile.lock(resolvedPath, {
            stale,
            realpath,
            retries: {
                retries,
                minTimeout,
                maxTimeout,
                factor: 1.5,
                randomize: true,
            },
        });
    }
    catch (err) {
        // Structured event so operators can spot contention hot-spots.
        recordLockTimeout({
            filePath: resolvedPath,
            projectRoot: options.projectRoot,
            reason: err instanceof Error ? err.message : String(err),
        });
        throw new LockTimeoutError(resolvedPath, err instanceof Error ? err : new Error(String(err)));
    }
    try {
        return await fn();
    }
    finally {
        if (release) {
            try {
                await release();
            }
            catch {
                // release errors are non-fatal — proper-lockfile handles stale cleanup.
            }
        }
    }
}
export class LockTimeoutError extends Error {
    code = 'E_LOCK_TIMEOUT';
    target;
    cause;
    constructor(target, cause) {
        super(`Could not acquire lock on ${target}: ${cause.message}`);
        this.name = 'LockTimeoutError';
        this.target = target;
        this.cause = cause;
    }
}
function recordLockTimeout(info) {
    if (!info.projectRoot)
        return;
    try {
        const eventsDir = join(info.projectRoot, '.aperant', 'events');
        mkdirSync(eventsDir, { recursive: true });
        const ts = new Date().toISOString().replace(/[:.]/g, '-');
        const file = join(eventsDir, `lock_timeout_${ts}.json`);
        writeFileSync(file, JSON.stringify({
            type: 'lock_timeout',
            target: info.filePath,
            reason: info.reason,
            pid: process.pid,
            at: new Date().toISOString(),
        }, null, 2));
    }
    catch {
        // best-effort; failure to log must not mask the original error.
    }
}
//# sourceMappingURL=lock.js.map