/**
 * install-manifest — records every path + registration Nexus creates, so that
 * `nexus uninstall` can reverse them without relying on heuristics alone.
 *
 * The manifest is additive and idempotent: repeat records collapse, and a
 * missing manifest is treated as empty (uninstall falls back to enumerate +
 * heuristic scan in that case).
 */
import * as fs from 'fs';
import * as path from 'path';
import { getInstallManifestPath } from './state-locator.js';
export const MANIFEST_VERSION = 1;
/**
 * Architecture generation marker for the installed runtime. Bump this when
 * the install layout changes (new dashboard, new MCP registrations, new
 * hook surfaces, new state directories) so the install-wizard can detect
 * older shapes and run migrations rather than silently leaving stale
 * registrations alongside the new ones.
 *
 * Manifests written before this field was introduced have `undefined` here
 * and are treated as the inferred legacy generation `v6` (matching the
 * setup-marker-v6.json filename Nexus has used since the v6 release).
 */
export const INSTALL_ARCH_GENERATION = 'v7';
function emptyManifest() {
    const now = Date.now();
    return {
        version: MANIFEST_VERSION,
        createdAt: now,
        updatedAt: now,
        paths: [],
        registrations: [],
        setupMarkers: [],
        architectureGeneration: INSTALL_ARCH_GENERATION,
    };
}
export function loadManifest(filePath = getInstallManifestPath()) {
    try {
        const raw = fs.readFileSync(filePath, 'utf8');
        const parsed = JSON.parse(raw);
        if (!parsed || typeof parsed !== 'object')
            return emptyManifest();
        const m = parsed;
        if (m.version !== MANIFEST_VERSION)
            return emptyManifest();
        return {
            version: MANIFEST_VERSION,
            createdAt: Number(m.createdAt) || Date.now(),
            updatedAt: Number(m.updatedAt) || Date.now(),
            paths: Array.isArray(m.paths) ? m.paths.filter(isPathEntry) : [],
            registrations: Array.isArray(m.registrations) ? m.registrations.filter(isRegistrationEntry) : [],
            setupMarkers: Array.isArray(m.setupMarkers) ? m.setupMarkers.filter((v) => typeof v === 'string') : [],
            architectureGeneration: typeof m.architectureGeneration === 'string'
                ? m.architectureGeneration
                : undefined,
        };
    }
    catch {
        return emptyManifest();
    }
}
/** Stamp the manifest with the current architecture generation. Idempotent. */
export function recordArchitectureGeneration(manifest, generation = INSTALL_ARCH_GENERATION) {
    if (manifest.architectureGeneration === generation)
        return manifest;
    return { ...manifest, architectureGeneration: generation };
}
/**
 * Inspect the manifest's recorded generation against the current build.
 * `previousGeneration` is undefined for fresh installs, 'v6' for legacy
 * manifests written before the field existed. `requiresMigration` is true
 * when the manifest is older than INSTALL_ARCH_GENERATION.
 */
export function detectArchitectureUpgrade(manifest) {
    const previousGeneration = manifest.architectureGeneration;
    const requiresMigration = previousGeneration !== INSTALL_ARCH_GENERATION
        && (manifest.paths.length > 0 || manifest.registrations.length > 0 || manifest.setupMarkers.length > 0);
    return {
        previousGeneration,
        currentGeneration: INSTALL_ARCH_GENERATION,
        requiresMigration,
    };
}
function isPathEntry(value) {
    if (!value || typeof value !== 'object')
        return false;
    const v = value;
    return typeof v.path === 'string' && (v.kind === 'file' || v.kind === 'dir') && typeof v.scope === 'string';
}
function isRegistrationEntry(value) {
    if (!value || typeof value !== 'object')
        return false;
    const v = value;
    return typeof v.filePath === 'string' && Array.isArray(v.jsonPath) && typeof v.target === 'string';
}
export function saveManifest(manifest, filePath = getInstallManifestPath()) {
    const dir = path.dirname(filePath);
    fs.mkdirSync(dir, { recursive: true });
    const next = {
        ...manifest,
        version: MANIFEST_VERSION,
        updatedAt: Date.now(),
        architectureGeneration: manifest.architectureGeneration ?? INSTALL_ARCH_GENERATION,
    };
    fs.writeFileSync(filePath, JSON.stringify(next, null, 2), 'utf8');
}
export function recordPath(manifest, entry) {
    const existing = manifest.paths.findIndex((e) => e.path === entry.path);
    if (existing >= 0)
        return manifest;
    return {
        ...manifest,
        paths: [...manifest.paths, { ...entry, addedAt: Date.now() }],
    };
}
export function recordRegistration(manifest, entry) {
    const jsonKey = entry.jsonPath.join('.');
    const dupIndex = manifest.registrations.findIndex((e) => e.filePath === entry.filePath && e.jsonPath.join('.') === jsonKey && e.entryMarker === entry.entryMarker);
    if (dupIndex >= 0)
        return manifest;
    return {
        ...manifest,
        registrations: [
            ...manifest.registrations,
            { kind: entry.kind ?? 'ide-setting', target: entry.target, filePath: entry.filePath, jsonPath: entry.jsonPath, entryMarker: entry.entryMarker, addedAt: Date.now() },
        ],
    };
}
export function recordSetupMarker(manifest, markerPath) {
    if (manifest.setupMarkers.includes(markerPath))
        return manifest;
    return { ...manifest, setupMarkers: [...manifest.setupMarkers, markerPath] };
}
export function clearManifest(filePath = getInstallManifestPath()) {
    try {
        fs.unlinkSync(filePath);
    }
    catch {
        // missing is fine
    }
}
/** Append-or-update helper used by install-wizard. Returns the written manifest. */
export function appendToManifest(mutator, filePath = getInstallManifestPath()) {
    const current = loadManifest(filePath);
    const next = mutator(current);
    if (next === current)
        return current;
    saveManifest(next, filePath);
    return next;
}
