/**
 * `installed-packs.json` writer (mmnto-ai/totem#1768, Step 4).
 *
 * Walks two source surfaces — the consumer's `package.json` dependencies
 * for `@mmnto/pack-*` entries, and the consumer's `totem.config.ts`
 * `extends` array — deduplicates, resolves each pack's installed path
 * via npm/pnpm semantics, and writes `.totem/installed-packs.json` for
 * `pack-discovery.ts:loadInstalledPacks()` to consume at boot.
 *
 * Mismatch handling (ADR-097 § 5 Q4 + Open Question 4 disposition):
 * - In both surfaces: union, no warning.
 * - In `extends` only (declared but not resolvable via npm): emit a
 *   warning naming the pack; skip from the manifest. Consumer must
 *   `pnpm add` the pack to make it work.
 * - In `package.json` only (resolvable but not declared in extends):
 *   emit a warning; skip from the manifest. Pack-merge only consumes
 *   `extends`-declared packs; including the dep-only entry would be
 *   misleading.
 *
 * Atomic write semantics: the writer goes through a temp file +
 * rename, mirroring `writeReviewExtensionsFile` (mmnto-ai/totem#1527).
 *
 * Filesystem access happens at the CLI boundary (this module is invoked
 * by `syncCommand`); the resolver itself takes paths + content as
 * inputs so tests can drive it without disk I/O.
 */
import * as fs from 'node:fs';
import { createRequire } from 'node:module';
import * as path from 'node:path';
import { resolveEngineVersion } from './pack-discovery.js';
// ─── Resolver ───────────────────────────────────────
const PACK_NAME_PREFIX = '@mmnto/pack-';
/**
 * Compute the deduplicated union of `package.json` `@mmnto/pack-*` deps
 * and `totem.config.ts` `extends` entries. Returns the manifest payload
 * shape ready for `installed-packs.json` plus per-pack warnings for any
 * surface mismatch.
 *
 * Pure function — no filesystem write. The CLI integration calls
 * `writeInstalledPacksManifest()` to persist.
 */
export function resolveInstalledPacks(input) {
    const deps = input.packageJsonDeps ?? readPackageJsonDeps(input.projectRoot);
    const extendsList = (input.config.extends ?? []).filter((name) => name.startsWith(PACK_NAME_PREFIX));
    const depPackNames = Object.keys(deps).filter((name) => name.startsWith(PACK_NAME_PREFIX)); // totem-context: Object.keys() returns strings; not fileGlobs ast-grep object form
    const extendsSet = new Set(extendsList);
    const depsSet = new Set(depPackNames);
    const union = new Set([...extendsSet, ...depsSet]);
    const warnings = [];
    const resolved = [];
    for (const name of [...union].sort()) {
        if (!extendsSet.has(name)) {
            // Resolvable dep that the consumer hasn't opted into via `extends`.
            // Pack-merge only consumes extends-declared packs, so this pack
            // would never have its rules merged anyway. Skip with a warning.
            warnings.push({ name, reason: 'dep-only' });
            continue;
        }
        if (!depsSet.has(name)) {
            // Declared in extends but not installed via package manager.
            // Cannot resolve a path; skip with a warning.
            warnings.push({ name, reason: 'extends-only' });
            continue;
        }
        const resolver = input.resolvePackPath ?? defaultResolvePackPath;
        const resolvedPath = resolver(name, input.projectRoot);
        if (!resolvedPath) {
            warnings.push({ name, reason: 'extends-only' });
            continue;
        }
        const declaredEngineRange = readEngineRange(resolvedPath);
        if (!declaredEngineRange) {
            warnings.push({ name, reason: 'not-a-pack' });
            continue;
        }
        resolved.push({
            name,
            resolvedPath,
            declaredEngineRange,
        });
    }
    return { resolved, warnings };
}
/**
 * Atomically write `<totemDir>/installed-packs.json` with the given
 * manifest payload. Mirrors `writeReviewExtensionsFile` semantics: temp
 * file + rename so a concurrent boot-time read sees the old or new
 * contents, never a partial write.
 *
 * Stamps the `cohort` field with the running `@mmnto/totem` version
 * via `resolveEngineVersion()` when the caller has not pre-populated
 * one (mmnto-ai/totem#1811). Tests pass an explicit cohort to override
 * the stamp.
 */
export function writeInstalledPacksManifest(totemDirAbs, manifest) {
    if (!fs.existsSync(totemDirAbs)) {
        fs.mkdirSync(totemDirAbs, { recursive: true });
    }
    const stamped = {
        ...manifest,
        cohort: manifest.cohort ?? resolveEngineVersion(),
    };
    const finalPath = path.join(totemDirAbs, 'installed-packs.json');
    const tmpPath = finalPath + '.tmp';
    // totem-context: writing the totem pack manifest under <totemDir>/, not a git hook
    fs.writeFileSync(tmpPath, JSON.stringify(stamped, null, 2) + '\n', 'utf-8');
    fs.renameSync(tmpPath, finalPath);
    return finalPath;
}
// ─── Internal helpers ───────────────────────────────
function readPackageJsonDeps(projectRoot) {
    const pkgPath = path.join(projectRoot, 'package.json');
    if (!fs.existsSync(pkgPath))
        return {};
    let parsed;
    // totem-context: intentional cleanup — package.json missing/corrupt is non-fatal to sync; treat as no deps
    try {
        parsed = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); // totem-context: sync read at boot; sync command is itself synchronous CLI top-level
    }
    catch {
        return {};
    }
    if (typeof parsed !== 'object' || parsed === null)
        return {};
    const obj = parsed;
    const out = {};
    for (const field of ['dependencies', 'devDependencies']) {
        const section = obj[field];
        if (typeof section !== 'object' || section === null)
            continue;
        for (const [name, value] of Object.entries(section)) {
            if (typeof value === 'string')
                out[name] = value;
        }
    }
    return out;
}
function defaultResolvePackPath(name, fromDir) {
    // We resolve from the project root rather than from this module — packs
    // live in the consumer's node_modules, not core's.
    const require = createRequire(path.join(fromDir, 'package.json'));
    try {
        // Resolve to the package's package.json so the directory containing it
        // is the pack's package root. require.resolve(name) would land on the
        // package's `main` entry, which isn't what pack-discovery wants.
        const pkgJsonPath = require.resolve(`${name}/package.json`);
        return path.dirname(pkgJsonPath); // totem-context: intentional cleanup — unresolvable pack returns undefined to flow into the `extends-only` warning path
    }
    catch {
        return undefined;
    }
}
function readEngineRange(packResolvedPath) {
    // ADR-097 § Q6 (amended 2026-05-03 via mmnto-ai/totem#1803): pack engine
    // version constraint lives in `engines['@mmnto/totem']`, NOT
    // `peerDependencies`. The `peerDependencies` slot is reserved for actual
    // peer packages the consumer must install (e.g., `@ast-grep/napi`).
    // Rationale: changesets `fixed` group siblings cannot peer-dep one
    // another (mmnto-ai/totem#1776 / #1777 wiggle); engines field is
    // architecturally correct + immune to fixed-group auto-bump.
    const pkgPath = path.join(packResolvedPath, 'package.json');
    if (!fs.existsSync(pkgPath))
        return undefined;
    let parsed;
    // totem-context: intentional cleanup — corrupted pack package.json returns undefined to flow into the `not-a-pack` warning path
    try {
        parsed = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); // totem-context: sync read at boot; sync command is itself synchronous CLI top-level
    }
    catch {
        return undefined;
    }
    if (typeof parsed !== 'object' || parsed === null)
        return undefined;
    const engines = parsed.engines; // totem-context: SAFETY INVARIANT — `parsed` confirmed non-null object by the `typeof !== 'object' || === null` guard immediately above. Cast is for index access into runtime-narrowed JSON; no formal schema validation, but `engines` is itself runtime-checked on the next line before use.
    if (typeof engines !== 'object' || engines === null)
        return undefined;
    const range = engines['@mmnto/totem']; // totem-context: engines was narrowed via `typeof !== 'object' || === null` guard above; the cast is for index access; result is runtime-checked via `typeof range === 'string'` below
    return typeof range === 'string' ? range : undefined;
}
//# sourceMappingURL=pack-manifest-writer.js.map