/**
 * Cron job store — persistent JSON file with atomic writes.
 *
 * Jobs live at ~/.shortcut/agent/cron/jobs.json.
 * Writes use temp-file + rename for crash safety, with a .bak backup.
 */
import { randomBytes } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { getAgentDir } from '../../config.js';
/** Lazy getter — avoids caching getAgentDir() at module load time. */
export function getCronDir() {
    return path.join(getAgentDir(), 'cron');
}
/** Lazy getter — avoids caching getAgentDir() at module load time. */
export function getCronStorePath() {
    return path.join(getCronDir(), 'jobs.json');
}
/** Cache to skip writes when content hasn't changed. */
const serializedCache = new Map();
export async function loadCronStore(storePath = getCronStorePath()) {
    try {
        const raw = await fs.promises.readFile(storePath, 'utf-8');
        const parsed = JSON.parse(raw);
        if (typeof parsed.version === 'number' && parsed.version !== 1) {
            throw new Error(`Unsupported cron store version: ${parsed.version}`);
        }
        if ('jobs' in parsed && !Array.isArray(parsed.jobs)) {
            throw new Error('Corrupt cron store: jobs is not an array');
        }
        const jobs = Array.isArray(parsed.jobs)
            ? parsed.jobs.filter((j) => j != null &&
                typeof j === 'object' &&
                typeof j.id === 'string')
            : [];
        const store = { version: 1, jobs: jobs };
        serializedCache.set(storePath, JSON.stringify(store, null, 2));
        return store;
    }
    catch (err) {
        if (err.code === 'ENOENT') {
            serializedCache.delete(storePath);
            return { version: 1, jobs: [] };
        }
        throw err;
    }
}
export async function saveCronStore(storePath, store, opts) {
    const json = JSON.stringify(store, null, 2);
    if (serializedCache.get(storePath) === json)
        return;
    const dir = path.dirname(storePath);
    await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
    // Backup existing file
    if (!opts?.skipBackup) {
        try {
            await fs.promises.copyFile(storePath, `${storePath}.bak`);
        }
        catch {
            // best-effort — file may not exist yet
        }
    }
    // Atomic write: temp file → rename
    const tmp = `${storePath}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
    await fs.promises.writeFile(tmp, json, { encoding: 'utf-8', mode: 0o600 });
    try {
        await fs.promises.rename(tmp, storePath);
    }
    catch (err) {
        // Windows fallback: rename can fail with EPERM/EEXIST when dest exists
        const code = err.code;
        if (code === 'EPERM' || code === 'EEXIST') {
            await fs.promises.copyFile(tmp, storePath);
            await fs.promises.unlink(tmp).catch(() => { });
        }
        else {
            await fs.promises.unlink(tmp).catch(() => { });
            throw err;
        }
    }
    serializedCache.set(storePath, json);
}
/** Clear the serialization cache (for testing). */
export function clearStoreCache() {
    serializedCache.clear();
}
//# sourceMappingURL=store.js.map