// storage/spec-lock-store.ts — Persists spec locks (SPEC-301)
// Layout: data/projects/{hash}/locks/{specId}.json
import { readFile, readdir, unlink } from 'node:fs/promises';
import { writeJson, projectDataDir, hashProjectPath, safeJsonParse } from './base-store.js';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function locksDir(projectId) {
    return `${projectDataDir(projectId)}/locks`;
}
function lockFile(projectId, specId) {
    return `${locksDir(projectId)}/${specId}.json`;
}
/** Returns true when the lock's expiration time is in the past. */
export function isExpired(lock) {
    return new Date(lock.expiresAt).getTime() <= Date.now();
}
// ---------------------------------------------------------------------------
// Store functions
// ---------------------------------------------------------------------------
/**
 * Read a lock from disk. Returns null if the file does not exist.
 * Does NOT filter out expired locks — callers must check isExpired().
 */
export async function getLock(projectPath, specId) {
    const projectId = hashProjectPath(projectPath);
    const filePath = lockFile(projectId, specId);
    try {
        const raw = await readFile(filePath, 'utf-8');
        return safeJsonParse(raw, null);
    }
    catch (err) {
        if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
            return null;
        }
        throw err;
    }
}
/** Write a lock to disk, creating parent directories as needed. */
export async function saveLock(projectPath, specId, lock) {
    const projectId = hashProjectPath(projectPath);
    await writeJson(lockFile(projectId, specId), lock);
}
/** Remove a lock file. Silently succeeds if the file does not exist. */
export async function deleteLock(projectPath, specId) {
    const projectId = hashProjectPath(projectPath);
    const filePath = lockFile(projectId, specId);
    try {
        await unlink(filePath);
    }
    catch (err) {
        /* v8 ignore start */
        if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
            return;
        }
        throw err;
        /* v8 ignore stop */
    }
}
/**
 * Return all active (non-expired) locks for a project.
 * Reads all .json files from the locks/ directory and filters out expired ones.
 */
export async function listActiveLocks(projectPath) {
    const projectId = hashProjectPath(projectPath);
    const dir = locksDir(projectId);
    let entries;
    try {
        entries = await readdir(dir);
    }
    catch (err) {
        if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
            return [];
        }
        /* v8 ignore next */
        throw err;
    }
    const jsonFiles = entries.filter((e) => e.endsWith('.json'));
    const locks = [];
    await Promise.all(jsonFiles.map(async (file) => {
        const specId = file.replace(/\.json$/, '');
        const lock = await getLock(projectPath, specId);
        if (lock !== null && !isExpired(lock)) {
            locks.push(lock);
        }
    }));
    return locks;
}
/**
 * Returns true when a non-expired lock exists for the given spec.
 */
export async function isLocked(projectPath, specId) {
    const lock = await getLock(projectPath, specId);
    if (lock === null) {
        return false;
    }
    return !isExpired(lock);
}
//# sourceMappingURL=spec-lock-store.js.map