import { HEAVY_NPM_DEPS, getDeclaredVersion, withPeerCompanions, satisfiesRange, } from "./heavyDeps.js";
import { readInstalledRecord, } from "./cacheDir.js";
import { ensureRuntimeInstalled, resolveHeavyDepSource, resolveHeavyDepVersion, } from "./loader.js";
import { BROWSER_CHANNELS, ensureBrowserInstalled, } from "./browsers.js";
// Suppress `debug` by default (e.g., the npm install stdout/stderr that
// ensureRuntimeInstalled emits) so API-style callers that don't inject a
// logger don't get flooded. Mirrors the loader.ts pattern: opt back in
// with DOC_DETECTIVE_RUNTIME_DEBUG=1 for diagnostics. The CLI flow wires
// its own logger that respects config.logLevel, so this only governs
// the bare programmatic path.
const RUNTIME_DEBUG = process.env.DOC_DETECTIVE_RUNTIME_DEBUG === "1";
const defaultLogger = (msg, level = "info") => {
    if (level === "debug" && !RUNTIME_DEBUG)
        return;
    if (level === "error")
        console.error(msg);
    else
        console.log(msg);
};
/**
 * Install the heavy npm packages into <cacheDir>/runtime. With no
 * `packages` filter, installs the full HEAVY_NPM_DEPS set. Returns one
 * structured report per package — the action describes whether the install
 * actually fired or whether the package was already current.
 */
export async function installRuntime(options = {}) {
    const { packages, ctx = {}, deps = {}, force = false, dryRun = false, } = options;
    const logger = deps.logger ?? defaultLogger;
    const targets = packages && packages.length > 0
        ? packages
        : [...HEAVY_NPM_DEPS];
    const before = readInstalledRecord(ctx);
    const reports = [];
    if (dryRun) {
        // `installedVersion` describes what is *on disk*; in a dry run nothing
        // has changed on disk yet. Surface the declared package.json range in
        // `notes` instead so consumers don't render a constraint string (e.g.
        // `^7.0.0`) in a column that elsewhere shows a resolved version.
        // Expand with peer companions so the dry-run report matches what the real
        // install actually fetches (e.g. proxy-agent alongside @puppeteer/browsers).
        for (const name of withPeerCompanions(targets)) {
            reports.push({
                assetId: name,
                kind: "npm",
                action: "dry-run",
                notes: [`would install ${name}@${getDeclaredVersion(name)}`],
            });
        }
        return reports;
    }
    // ensureRuntimeInstalled handles the "skip already-present" fast path
    // internally. We call it once with the full target list — single npm
    // invocation, all deps resolved together.
    await ensureRuntimeInstalled(targets, {
        ctx,
        deps: { logger, spawn: deps.spawn },
        force,
    });
    const after = readInstalledRecord(ctx);
    // ensureRuntimeInstalled also installs and records peer companions (e.g.
    // proxy-agent alongside @puppeteer/browsers), so report on the expanded set
    // — otherwise the result omits packages that were actually installed, and
    // diverges from the dry-run report.
    for (const name of withPeerCompanions(targets)) {
        const wasInstalled = before.npmPackages[name];
        const nowInstalled = after.npmPackages[name];
        const installedVersion = nowInstalled?.installedVersion;
        let action;
        if (force)
            action = "forced";
        else if (!wasInstalled && nowInstalled)
            action = "installed";
        else if (wasInstalled &&
            nowInstalled &&
            wasInstalled.installedVersion !== nowInstalled.installedVersion)
            action = "updated";
        else
            action = "already-up-to-date";
        reports.push({ assetId: name, kind: "npm", action, installedVersion });
    }
    return reports;
}
/**
 * Install (or refresh) browser assets into <cacheDir>/browsers. Without
 * `names`, installs every supported asset.
 */
export async function installBrowsers(options = {}) {
    const { names, ctx = {}, deps = {}, force = false, dryRun = false, } = options;
    const logger = deps.logger ?? defaultLogger;
    const targets = names && names.length > 0
        ? names
        : Object.keys(BROWSER_CHANNELS);
    const reports = [];
    if (dryRun) {
        for (const name of targets) {
            reports.push({
                assetId: name,
                kind: "browser",
                action: "dry-run",
                notes: [`channel: ${BROWSER_CHANNELS[name]}`],
            });
        }
        return reports;
    }
    for (const name of targets) {
        const before = readInstalledRecord(ctx).browsers[name];
        const result = await ensureBrowserInstalled(name, {
            ctx,
            deps: { ...deps.browserDeps, logger },
            force,
        });
        let action;
        if (force)
            action = "forced";
        else if (!before)
            action = "installed";
        else if (before.installedVersion !== result.version)
            action = "updated";
        else
            action = "already-up-to-date";
        reports.push({
            assetId: name,
            kind: "browser",
            action,
            installedVersion: result.version,
            notes: result.outdated
                ? [`installed buildId is older than channel "${BROWSER_CHANNELS[name]}"`]
                : undefined,
        });
    }
    return reports;
}
/**
 * Diff what's recorded in <cacheDir>/installed.json against the shim's
 * declared expectations (`package.json#optionalDependencies` for npm deps,
 * `latestKnownVersion` from the cache for browsers). The semver-range
 * check used here lives in `heavyDeps.ts` and is shared with the lazy
 * installer so both code paths see "satisfies the declared constraint"
 * the same way.
 */
export function status(ctx = {}) {
    const record = readInstalledRecord(ctx);
    const rows = [];
    for (const name of HEAVY_NPM_DEPS) {
        const entry = record.npmPackages[name];
        const expected = (() => {
            try {
                return getDeclaredVersion(name);
            }
            catch {
                return undefined;
            }
        })();
        // A package counts as installed when it's recorded in the cache OR
        // resolvable from the shim's node_modules / runtime cache — the same
        // presence check `ensureRuntimeInstalled` uses to report
        // "already-up-to-date". Without the resolvable fallback, `status`
        // showed `installed=—` for pre-installed optionalDependencies (and dev
        // checkouts) that `install all` reports as present — an inconsistency.
        //
        // The package.json range is a constraint (e.g. ^7.0.0), not a target —
        // string equality would flag `7.1.2` against `^7.0.0` as outdated even
        // though npm legitimately resolved it; use a semver-range check, and
        // only where the installer would actually act on it:
        //   - cache record / cache resolution → freshness-checked (the
        //     installer reinstalls a stale cache);
        //   - shim resolution → never flagged outdated (the installer never
        //     overrides a shim-pinned version), matching `install all`.
        // Mirror the runtime resolution order: the shim's node_modules wins
        // over the cache. A shim-resolved package is never "outdated" because
        // ensureRuntimeInstalled never overrides a shim-pinned version — so a
        // stale cache record must NOT flag a shim-resolved dep as outdated.
        // Only genuine cache installs are freshness-checked against the range.
        const source = resolveHeavyDepSource(name, ctx);
        let installed = false;
        let installedVersion;
        let outdated = false;
        if (source === "shim") {
            installed = true;
            installedVersion = resolveHeavyDepVersion(name, ctx) ?? undefined;
        }
        else if (entry) {
            installed = true;
            installedVersion = entry.installedVersion;
            outdated = Boolean(expected && !satisfiesRange(entry.installedVersion, expected));
        }
        else if (source === "cache") {
            // Resolvable in the cache but no record entry (edge case) — still a
            // cache install, so apply the same freshness check.
            installed = true;
            installedVersion = resolveHeavyDepVersion(name, ctx) ?? undefined;
            outdated = Boolean(installedVersion && expected && !satisfiesRange(installedVersion, expected));
        }
        rows.push({
            assetId: name,
            kind: "npm",
            installed,
            installedVersion,
            expectedVersion: expected,
            outdated,
        });
    }
    for (const name of Object.keys(BROWSER_CHANNELS)) {
        const entry = record.browsers[name];
        rows.push({
            assetId: name,
            kind: "browser",
            installed: Boolean(entry),
            installedVersion: entry?.installedVersion,
            latestKnownVersion: entry?.latestKnownVersion,
            outdated: Boolean(entry && entry.latestKnownVersion && entry.installedVersion !== entry.latestKnownVersion),
        });
    }
    return rows;
}
//# sourceMappingURL=installer.js.map