// SPEC-094 AC-4: File-based mutex for resource locking
import { writeFile, readFile, unlink, mkdir } from 'node:fs/promises';
import { createHash } from 'node:crypto';
const LOCK_DIR = 'data/.locks';
const STALE_THRESHOLD_MS = 30_000;
const DEFAULT_TIMEOUT_MS = 10_000;
const POLL_INTERVAL_MS = 100;
function hashResource(resource) {
    return createHash('sha256').update(resource).digest('hex').slice(0, 16);
}
/* v8 ignore next 8 -- process.kill(pid, 0) is OS-dependent, not unit-testable */
function isProcessRunning(pid) {
    try {
        process.kill(pid, 0);
        return true;
    }
    catch {
        return false;
    }
}
/* v8 ignore start -- depends on isProcessRunning which is OS-dependent */
function isStale(content) {
    const lockAge = Date.now() - new Date(content.timestamp).getTime();
    if (lockAge > STALE_THRESHOLD_MS) {
        return true;
    }
    if (!isProcessRunning(content.pid)) {
        return true;
    }
    return false;
}
/* v8 ignore stop */
async function tryReadLock(lockPath) {
    try {
        const raw = await readFile(lockPath, 'utf-8');
        return JSON.parse(raw);
    }
    catch {
        return null;
    }
}
async function ensureLockDir() {
    await mkdir(LOCK_DIR, { recursive: true });
}
/**
 * Acquires a file-based lock for a named resource.
 * Polls until the lock is available or timeout expires.
 * Stale locks (old timestamp or dead PID) are auto-released.
 */
export async function acquireLock(resource, timeoutMs = DEFAULT_TIMEOUT_MS) {
    await ensureLockDir();
    const hash = hashResource(resource);
    const lockPath = `${LOCK_DIR}/${hash}.lock`;
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        const existing = await tryReadLock(lockPath);
        if (existing) {
            if (isStale(existing)) {
                try {
                    await unlink(lockPath);
                }
                catch {
                    // Another process may have already removed it
                }
            }
            else {
                await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
                continue;
            }
        }
        const content = {
            pid: process.pid,
            timestamp: new Date().toISOString(),
            resource,
        };
        try {
            await writeFile(lockPath, JSON.stringify(content, null, 2), {
                flag: 'wx',
            });
        }
        catch /* v8 ignore start */ {
            // File was created between our check and write — retry
            await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
            continue;
        } /* v8 ignore stop */
        return {
            resource,
            lockPath,
            pid: process.pid,
            acquiredAt: content.timestamp,
        };
    }
    const error = new Error(`Lock timeout after ${timeoutMs}ms for resource "${resource}". Another process holds the lock.`);
    error.code = 'LOCK_TIMEOUT';
    throw error;
}
/**
 * Releases a previously acquired lock.
 * No-op if the lock file was already removed.
 */
export async function releaseLock(handle) {
    try {
        await unlink(handle.lockPath);
    }
    catch (err) {
        /* v8 ignore start -- defensive: non-ENOENT errors from unlink */
        if (err.code !== 'ENOENT') {
            throw err;
        }
        /* v8 ignore stop */
    }
}
//# sourceMappingURL=file-mutex.js.map