/**
 * Staleness library entrypoint. Aggregates per-resource checkers into the
 * two operations the rest of the codebase needs:
 *
 *   - `buildManifest(agent, version, cwd)` — snapshot current state.
 *   - `isStale(manifest, agent, version, cwd)` — true when any tracked
 *     resource has drifted from its stored fingerprint, or when an artifact
 *     the last full sync wrote (`writtenTargets`) has been deleted from the
 *     version home (#2398).
 *
 * `loadManifest` / `saveManifest` round-trip the on-disk JSON. The format
 * version stays at 1; new optional fields (workflows, plugins) on old files
 * read as empty maps which forces a single re-sync.
 */
import * as fs from 'fs';
import * as path from 'path';
import { getVersionsDir } from '../state.js';
import { commandsChecker } from './checkers/commands.js';
import { skillsChecker } from './checkers/skills.js';
import { hooksChecker } from './checkers/hooks.js';
import { mcpChecker } from './checkers/mcp.js';
import { subagentsChecker } from './checkers/subagents.js';
import { workflowsChecker } from './checkers/workflows.js';
import { pluginsChecker } from './checkers/plugins.js';
import { buildPermissions, isPermissionsStale } from './checkers/permissions.js';
import { buildRules, isRulesStale } from './checkers/rules.js';
import { MANIFEST_VERSION, } from './types.js';
import { nameSetDiffers } from './fingerprint.js';
export { MANIFEST_VERSION } from './types.js';
/**
 * Standard checkers — uniform contract. Rules and permissions have extra
 * context (agent/version, preset env) so they're wired explicitly below.
 */
const STANDARD_CHECKERS = [
    { checker: commandsChecker, field: 'commands' },
    { checker: skillsChecker, field: 'skills' },
    { checker: hooksChecker, field: 'hooks' },
    { checker: mcpChecker, field: 'mcp' },
    { checker: subagentsChecker, field: 'subagents' },
    { checker: workflowsChecker, field: 'workflows' },
    { checker: pluginsChecker, field: 'plugins' },
];
// ─── Public API ──────────────────────────────────────────────────────────────
function manifestPath(agent, version) {
    return path.join(getVersionsDir(), agent, version, 'home', '.sync-manifest.json');
}
export function loadManifest(agent, version) {
    const p = manifestPath(agent, version);
    try {
        const raw = JSON.parse(fs.readFileSync(p, 'utf-8'));
        if (raw.v !== MANIFEST_VERSION)
            return null;
        return raw;
    }
    catch {
        return null;
    }
}
export function saveManifest(agent, version, manifest) {
    const p = manifestPath(agent, version);
    const tmp = p + '.tmp';
    try {
        fs.mkdirSync(path.dirname(p), { recursive: true });
        fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2));
        fs.renameSync(tmp, p);
    }
    catch {
        try {
            fs.unlinkSync(tmp);
        }
        catch { /* ignore */ }
    }
}
/**
 * Snapshot current resource fingerprints for `agent@version`.
 *
 * When `previous` is supplied (the still-loaded post-guard manifest), any
 * entry whose stored path/mtime/size still match via the checker's `isFresh`
 * is carried forward without re-hashing. Measured: full rebuild ~716 ms →
 * carry-forward near the 5.67 ms `isStale` cost on an unchanged tree
 * (RUSH-2320 #3).
 */
export function buildManifest(agent, version, cwd, previous) {
    const manifest = {
        v: MANIFEST_VERSION,
        syncedAt: new Date().toISOString(),
        commands: {},
        skills: {},
        hooks: {},
        rules: { files: {} },
        mcp: {},
        permissions: { groups: {}, permissionPreset: null },
        subagents: {},
        workflows: {},
        plugins: {},
        // Empty baseline ("nothing recorded to verify"). The full-sync
        // orchestrator (versions.ts) replaces this with the writer-reported
        // artifact paths right before saving; a manifest built outside that path
        // simply verifies nothing. Only an ABSENT field (a pre-upgrade manifest
        // on disk) forces the one-time migration re-sync in isStale.
        writtenTargets: [],
    };
    for (const { checker, field } of STANDARD_CHECKERS) {
        const target = manifest[field];
        const prevMap = (previous?.[field] ?? {});
        for (const name of checker.listNames(cwd)) {
            const prev = prevMap[name];
            if (prev !== undefined && checker.isFresh(name, prev, cwd)) {
                target[name] = prev;
                continue;
            }
            const entry = checker.build(name, cwd);
            if (entry !== null)
                target[name] = entry;
        }
    }
    if (previous?.rules && !isRulesStale(previous.rules, agent, version, cwd)) {
        manifest.rules = previous.rules;
    }
    else {
        manifest.rules = buildRules(agent, version, cwd);
    }
    if (previous?.permissions && !isPermissionsStale(previous.permissions)) {
        manifest.permissions = previous.permissions;
    }
    else {
        manifest.permissions = buildPermissions();
    }
    return manifest;
}
/**
 * True when any tracked resource has drifted from the stored manifest.
 * Walks every resource type in turn and returns true at the first miss —
 * sync detection should be cheap when nothing changed.
 */
export function isStale(manifest, agent, version, cwd) {
    for (const { checker, field } of STANDARD_CHECKERS) {
        const storedMap = (manifest[field] ?? {});
        const currentNames = checker.listNames(cwd);
        if (nameSetDiffers(Object.keys(storedMap), currentNames))
            return true;
        for (const name of currentNames) {
            const entry = storedMap[name];
            if (entry === undefined)
                return true;
            if (!checker.isFresh(name, entry, cwd))
                return true;
        }
    }
    if (isPermissionsStale(manifest.permissions))
        return true;
    if (isRulesStale(manifest.rules, agent, version, cwd))
        return true;
    // Home-side deletion check (#2398): every artifact the last full sync wrote
    // must still exist, or the version home has rotted and needs a re-sync —
    // regardless of source fingerprints. One existsSync per path, no content
    // reads, so the fast-guard budget holds (RUSH-2320). A manifest that
    // predates the field reads as stale once so the next full sync records the
    // baseline (same precedent as the optional workflows/plugins maps).
    if (manifest.writtenTargets === undefined)
        return true;
    for (const target of manifest.writtenTargets) {
        if (!fs.existsSync(target))
            return true;
    }
    return false;
}
