// brew-side install wrapper, peer of installAptGroup() in index.ts.
//
// Mirrors the apt path's install + post-check shape: classify each requested
// package against `brew list --versions` first, install the missing subset
// via `brew install <pkgs...>`, then re-probe `brew list --versions` per
// formula and throw loudly if any are still missing. Pure decision functions
// live here so a unit test can exercise every branch without spawning real
// brew — the wrapper accepts injected spawn-result probes.
//
// Why split classify / decide-plan / verify into three exports instead of one
// `installAll(pkgs)`: the call sites in index.ts need fine-grained log lines
// (`[homebrew] <pkg> already installed (v…)` vs `[homebrew] installing <pkg> …`
// vs `[homebrew] <pkg> verified post-install`) and the operator surface for
// each is distinct. Keeping them as separate pure functions also lets the
// pre-flight `which brew` refusal (ensureBrewAvailable) live alongside the
// install/verify primitives without coupling the test grid to spawnSync.
import { spawnSync } from "node:child_process";
import { decideBrewResolution, parseBrewListVersions } from "./brew-resolve.js";
// ---------------------------------------------------------------------------
// ensureBrewAvailable — pre-flight refusal when Homebrew is absent.
// ---------------------------------------------------------------------------
/**
 * Throw the loud refusal documented in the task brief when `which brew`
 * exits non-zero. The message is grep-able and operator-actionable; no silent
 * `curl | bash` fallback (per the task brief's "no auto-installing Homebrew"
 * out-of-scope item).
 */
export function ensureBrewAvailable(spawn) {
    const r = spawn("which", ["brew"]);
    if (r.status !== 0) {
        throw new Error("[create-maxy] Homebrew not found. Install from https://brew.sh and re-run.");
    }
}
// ---------------------------------------------------------------------------
// classifyBrewState — turn a `brew list --versions <pkg>` probe into the
// installed/missing decision the install plan consumes.
// ---------------------------------------------------------------------------
/**
 * Pure: given the spawn result of `brew list --versions <pkg>`, decide whether
 * the formula is installed. A formula counts as installed only when brew
 * exited 0 AND the parsed version list is non-empty — the half-uninstalled
 * cellar state (exit 0 with name-only stdout) is treated as missing so the
 * install pass re-runs and the post-check fails loudly if it still doesn't
 * land. Mirrors apt-resolve's loud-failure contract.
 */
export function classifyBrewState(_pkg, result) {
    if (result.status !== 0)
        return { installed: false, versions: [] };
    const versions = parseBrewListVersions(result.stdout);
    if (versions.length === 0)
        return { installed: false, versions: [] };
    return { installed: true, versions };
}
// ---------------------------------------------------------------------------
// decideBrewInstallPlan — pure: which formulas to install + log lines.
// ---------------------------------------------------------------------------
/**
 * Pure: given each package's resolved formula name + install state, produce
 * the install list (only missing formulas) and the per-pkg log lines. Lines
 * are emitted in input order:
 *   - installed=true  → `  [homebrew] <formula> already installed (v<n>)`
 *   - installed=false → `  [homebrew] installing <formula> …`
 * The "already installed" line uses the first version token (the cellar's
 * current head); subsequent tokens are older retained versions per
 * brew-resolve.ts.
 */
export function decideBrewInstallPlan(input) {
    const toInstall = [];
    const logs = [];
    for (const p of input.pkgs) {
        if (p.installed) {
            const head = p.versions[0] ?? "?";
            logs.push(`  [homebrew] ${p.resolvedFormula} already installed (v${head})`);
            // Uniform-prefix mirror line so the install-log grep recipe in
            // platform/plugins/docs/references/deployment.md (`grep -E '^\[create-maxy\] …'`)
            // covers every brew step alongside platform=, darwin-hostname-mode=, etc.
            logs.push(`  [create-maxy] brew-formula=${p.resolvedFormula} action=present`);
        }
        else {
            logs.push(`  [homebrew] installing ${p.resolvedFormula} …`);
            logs.push(`  [create-maxy] brew-formula=${p.resolvedFormula} action=install`);
            toInstall.push(p.resolvedFormula);
        }
    }
    return { toInstall, logs };
}
// ---------------------------------------------------------------------------
// verifyBrewInstalled — post-install probe + loud-failure throw.
// ---------------------------------------------------------------------------
/**
 * Run `brew list --versions <formula>` (via the injected probe) for every
 * formula and throw if any are still missing. The error message mirrors
 * installAptGroup's "returned 0 but packages are still not installed"
 * contract. Returns the per-formula verified-log lines on success.
 */
export function verifyBrewInstalled(formulas, probe) {
    const stillMissing = [];
    const logs = [];
    for (const f of formulas) {
        const r = probe(f);
        const state = classifyBrewState(f, r);
        if (!state.installed) {
            stillMissing.push(f);
            continue;
        }
        logs.push(`  [homebrew] ${f} verified post-install`);
    }
    if (stillMissing.length > 0) {
        throw new Error(`[homebrew] installing ${stillMissing.join(", ")} exited 0 but brew list --versions reports missing: ${stillMissing.join(", ")}`);
    }
    return logs;
}
// ---------------------------------------------------------------------------
// installAll — orchestrating wrapper used by index.ts call sites. Production
// path only; tests exercise the pure functions above with injected spawns.
// ---------------------------------------------------------------------------
/**
 * Install every requested package via Homebrew, with the same install +
 * post-check pattern installAptGroup() uses for apt. The flow:
 *   1. ensureBrewAvailable — refuse loud if `which brew` fails.
 *   2. Probe `brew list --versions <original>` → classify per pkg.
 *   3. Resolve each original via decideBrewResolution (so `nodejs` → `node@22`).
 *   4. decideBrewInstallPlan → produces toInstall list + per-pkg log lines.
 *   5. `brew install <toInstall>` if non-empty.
 *   6. verifyBrewInstalled re-probes every resolved formula; throws loud if any
 *      formula is still missing post-install.
 * `log` is the caller's logFile sink (timestamped install log line).
 * Returns the resolved formula names so the caller can emit additional
 * follow-up logging (e.g. `[neo4j] bolt://localhost:<port> reachable…`).
 */
export function installAllBrewPackages(pkgs, log) {
    const spawn = (cmd, args) => {
        const r = spawnSync(cmd, args, {
            stdio: "pipe",
            encoding: "utf-8",
            timeout: 600_000,
        });
        return { status: r.status, stdout: r.stdout ?? "" };
    };
    ensureBrewAvailable(spawn);
    const probed = pkgs.map((original) => {
        // First probe: is the original name installed (e.g. is `node@22` already there)?
        // We probe under the resolved formula because that's the cellar key.
        const provisional = decideBrewResolution({ pkg: original, brewInstalled: false });
        const initialFormula = provisional.resolved;
        const probe = spawn("brew", ["list", "--versions", initialFormula]);
        const state = classifyBrewState(initialFormula, probe);
        // Second resolution pass: brewInstalled=true short-circuits the alias to
        // the original name (matches brew-resolve's contract — concrete-and-installed
        // wins). When the cellar is empty, the alias path takes effect.
        const decision = decideBrewResolution({ pkg: original, brewInstalled: state.installed });
        if (decision.log)
            log(decision.log);
        return {
            original,
            resolvedFormula: state.installed ? initialFormula : decision.resolved,
            installed: state.installed,
            versions: state.versions,
        };
    });
    const plan = decideBrewInstallPlan({ pkgs: probed });
    for (const line of plan.logs)
        log(line);
    if (plan.toInstall.length > 0) {
        const r = spawnSync("brew", ["install", ...plan.toInstall], {
            stdio: "inherit",
            timeout: 1_800_000,
        });
        if (r.status !== 0) {
            throw new Error(`brew install ${plan.toInstall.join(" ")} exited ${r.status}`);
        }
    }
    const verified = verifyBrewInstalled(probed.map((p) => p.resolvedFormula), (formula) => {
        const r = spawnSync("brew", ["list", "--versions", formula], { stdio: "pipe", encoding: "utf-8", timeout: 30_000 });
        return { status: r.status, stdout: r.stdout ?? "" };
    });
    for (const line of verified)
        log(line);
    return probed.map((p) => p.resolvedFormula);
}
