/**
 * bootstrap-kubernetes.ts — k3s mode of `olam bootstrap`.
 *
 * Wraps the legacy `scripts/dogfood/up.sh` reference flow into a CLI
 * routine so external operators with only the published `@pleri/olam-cli`
 * tarball (no access to the private `pleri/olam` repo) can bring up an
 * end-to-end olam-on-k3s stack with one command.
 *
 * Steps (idempotent):
 *   0. Preflight — detect required tools (k3d, kubectl, helm, jq, curl,
 *      openssl, gh, docker). With --auto-install on macOS, run `brew
 *      install` for missing brew-formulae.
 *   1. Secrets — generate `~/.olam/{auth-secret,kg-bearer-token,
 *      auth-db-secret,mcp-auth-jwt-secret,memory-bearer-secret}` if absent.
 *   2. Colima (macOS only) — ensure running; chmod /var/run/docker.sock
 *      inside the VM (virtiofs mitigation).
 *   3. k3d cluster — create `<cluster>` with the gh-config bind. Skipped
 *      if cluster already exists.
 *   4. Observability — chain bundled scripts in `<install>/host-cp/
 *      observability/` to install Loki + Promtail + Grafana +
 *      kube-prometheus-stack + Kyverno admission policy.
 *   5. Delegate to `olam upgrade -y` — that flow handles namespace + RBAC
 *      + secrets + ghcr-pull + host-side docker-socket-proxy +
 *      manifest apply + rollout + verify.
 *
 * Note: `olam upgrade` already ships the 8-step k8s flow in
 * `upgrade-kubernetes.ts` (Phase 1b C2 + Phase 2 C/D). This module is
 * the prereq + cluster-create + observability wrapper around it.
 */
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readdirSync, writeFileSync, chmodSync, } from "node:fs";
import { join } from "node:path";
import { homedir, platform } from "node:os";
import { randomBytes } from "node:crypto";
import pc from "picocolors";
import { printError, printSuccess, printInfo, printWarning, printHeader, } from "../output.js";
import { installRoot } from "../install-root.js";
import { resolveK8sAssetsRoot } from "./k8s-bootstrap.js";
const OLAM_HOME = join(homedir(), ".olam");
export const DEFAULT_K3S_CLUSTER = "olam-host";
const REQUIRED_TOOLS = [
    { name: "k3d", brew: "k3d" },
    { name: "kubectl", brew: "kubectl" },
    { name: "helm", brew: "helm" },
    { name: "jq", brew: "jq" },
    {
        name: "curl",
        hint: "expected on macOS by default; `apt install curl` on linux",
    },
    {
        name: "openssl",
        hint: "expected on macOS by default; `apt install openssl` on linux",
    },
    { name: "gh", brew: "gh", hint: "after install: `gh auth login`" },
    { name: "docker", hint: "Docker Desktop, or colima on macOS" },
];
const SECRET_FILES = [
    "auth-secret",
    "kg-bearer-token",
    "auth-db-secret",
    "mcp-auth-jwt-secret",
    "memory-bearer-secret",
];
const OBSERVABILITY_SCRIPTS = [
    "loki-ingest.sh",
    "grafana-port-forward.sh",
    "prom-no-double-grafana.sh",
    "kyverno-cardinality-mutate.sh",
];
/** Wraps `command -v <tool>` returning true when the binary is on PATH. */
function hasTool(tool) {
    const result = spawnSync("command", ["-v", tool], {
        shell: true,
        stdio: "pipe",
    });
    return result.status === 0;
}
function detectMissingTools() {
    return REQUIRED_TOOLS.filter((tool) => !hasTool(tool.name));
}
function runOrFail(cmd, args, description) {
    const result = spawnSync(cmd, args, { stdio: "inherit" });
    if (result.status !== 0) {
        printError(`${description} failed (exit ${result.status ?? "signal"})`);
        process.exit(result.status ?? 1);
    }
}
function runCapture(cmd, args) {
    const result = spawnSync(cmd, args, { encoding: "utf8" });
    return {
        ok: result.status === 0,
        stdout: result.stdout ?? "",
        stderr: result.stderr ?? "",
    };
}
function step(label) {
    process.stdout.write("\n" + pc.bold(pc.cyan(`==> ${label}`)) + "\n");
}
function preflight(opts) {
    step("0/6 — preflight (tool detection)");
    const missing = detectMissingTools();
    if (missing.length === 0) {
        printSuccess("all required tools present");
    }
    else if (opts.autoInstall && platform() === "darwin") {
        const brewable = missing.filter((t) => t.brew);
        if (brewable.length > 0) {
            printInfo("auto-install", `${brewable.length} brew formula(s): ${brewable.map((t) => t.brew).join(", ")}`);
            runOrFail("brew", ["install", ...brewable.map((t) => t.brew)], "brew install");
        }
        const stillMissing = detectMissingTools();
        if (stillMissing.length > 0) {
            printError("tools still missing after auto-install:");
            for (const t of stillMissing) {
                process.stderr.write(`  - ${t.name}${t.hint ? ` — ${t.hint}` : ""}\n`);
            }
            process.exit(1);
        }
        printSuccess("all required tools present after auto-install");
    }
    else {
        printError(`${missing.length} required tool(s) missing:`);
        for (const t of missing) {
            const hint = t.brew
                ? `brew install ${t.brew}`
                : (t.hint ?? "(install manually)");
            process.stderr.write(`  - ${pc.bold(t.name)}: ${hint}\n`);
        }
        process.stderr.write("\nRe-run with --auto-install on macOS to install brew-formulae automatically.\n");
        process.exit(1);
    }
    // gh auth — honour env-var auth (GH_TOKEN / GITHUB_TOKEN). `gh auth status`
    // exits non-zero when auth comes from env even though the user IS authenticated
    // (gh prints "✓ Logged in (GH_TOKEN)" but returns exit code 1 because no
    // gh-managed credential is stored). Don't fail bootstrap on that case —
    // exercise the token directly via `gh api user` which actually validates it.
    const authStatus = runCapture("gh", ["auth", "status"]);
    const ghTokenSet = !!(process.env.GH_TOKEN || process.env.GITHUB_TOKEN);
    if (!authStatus.ok && !ghTokenSet) {
        printError("gh is not authenticated — run: gh auth login");
        process.exit(1);
    }
    if (!authStatus.ok && ghTokenSet) {
        // Env-var token present but gh auth status non-zero. Validate by hitting
        // the API. If that fails too, the token is bad/expired.
        if (!runCapture("gh", ["api", "user", "--jq", ".login"]).ok) {
            printError("GH_TOKEN/GITHUB_TOKEN set but invalid — `gh api user` failed");
            process.exit(1);
        }
    }
    printSuccess("gh authenticated");
    // docker reachability
    if (!runCapture("docker", ["info"]).ok) {
        printError("docker daemon unreachable — start Docker Desktop or colima");
        process.exit(1);
    }
    printSuccess("docker daemon reachable");
}
function ensureSecrets() {
    step("1/6 — operator secrets");
    mkdirSync(OLAM_HOME, { recursive: true });
    for (const name of SECRET_FILES) {
        const path = join(OLAM_HOME, name);
        if (existsSync(path)) {
            printInfo("skip", `~/.olam/${name} already exists`);
            continue;
        }
        const hex = randomBytes(32).toString("hex");
        writeFileSync(path, hex + "\n", { encoding: "utf8", mode: 0o600 });
        chmodSync(path, 0o600);
        printSuccess(`generated ~/.olam/${name}`);
    }
}
function ensureColima() {
    if (platform() !== "darwin")
        return;
    step("2/6 — colima (macOS)");
    if (!runCapture("colima", ["status"]).ok) {
        printWarning("colima not running — starting");
        runOrFail("colima", [
            "start",
            "--cpu",
            "4",
            "--memory",
            "8",
            "--vm-type=vz",
            "--mount-type=virtiofs",
        ], "colima start");
    }
    printSuccess("colima running");
    // virtiofs blocks chmod of docker.sock from the macOS side. Idempotent + non-fatal.
    const chmod = runCapture("colima", [
        "ssh",
        "--",
        "sudo",
        "chmod",
        "666",
        "/var/run/docker.sock",
    ]);
    if (chmod.ok) {
        printSuccess("docker.sock chmod applied inside colima VM");
    }
    else {
        printWarning("colima ssh chmod failed — k3d cluster create may still succeed");
    }
}
function ensureCluster(opts) {
    if (opts.skipClusterCreate) {
        step("3/6 — cluster (skipped via --skip-cluster-create)");
        return;
    }
    step(`3/6 — k3d cluster create (${opts.cluster})`);
    if (runCapture("k3d", ["cluster", "list", opts.cluster]).ok) {
        printInfo("cluster", `'${opts.cluster}' already exists`);
        return;
    }
    const ghConfigBind = `${homedir()}/.config/gh:/host/.config/gh`;
    runOrFail("k3d", [
        "cluster",
        "create",
        opts.cluster,
        "--volume",
        ghConfigBind,
        "--wait",
        "--timeout",
        "90s",
    ], `k3d cluster create ${opts.cluster}`);
    if (!runCapture("kubectl", ["cluster-info"]).ok) {
        printError("kubectl cannot reach the cluster — check kubeconfig context");
        process.exit(1);
    }
    printSuccess(`cluster ready (${runCapture("kubectl", ["config", "current-context"]).stdout.trim()})`);
}
/** Resolve the bundled observability scripts dir. */
function resolveObservabilityScriptsDir() {
    // Install-mode: bundle-host-cp.mjs copies scripts/e2e/*.sh to
    // <install-root>/host-cp/observability/.
    const installed = join(installRoot(), "host-cp", "observability");
    if (existsSync(installed))
        return installed;
    // Dev-mode fallback: resolve via the k8s assets root + walk up to repo
    // root then into scripts/e2e/. Works when running against the monorepo.
    const k8sRoot = resolveK8sAssetsRoot();
    if (k8sRoot) {
        const dev = join(k8sRoot, "..", "..", "..", "scripts", "e2e");
        if (existsSync(dev))
            return dev;
    }
    return null;
}
/**
 * Resolve the bundled peripheral-services dir (helm-values + manifests).
 * Returns null when neither install-mode nor dev-mode layout matches.
 */
function resolvePeripheralServicesDir() {
    // Install-mode: bundle-host-cp.mjs copies packages/peripheral-services/
    // {helm-values,manifests} to <install-root>/host-cp/peripheral-services/.
    const installed = join(installRoot(), "host-cp", "peripheral-services");
    if (existsSync(installed))
        return installed;
    // Dev-mode fallback: walk up from k8s assets root to packages/peripheral-services.
    const k8sRoot = resolveK8sAssetsRoot();
    if (k8sRoot) {
        const dev = join(k8sRoot, "..", "..", "peripheral-services");
        if (existsSync(dev))
            return dev;
    }
    return null;
}
/**
 * Resolve OLAM_BUNDLE_ROOT — the dir the observability scripts read to find
 * `peripheral-services/{helm-values,manifests}/...`. In install-mode this is
 * `<install>/host-cp`; in dev-mode it's the monorepo's `packages/` dir.
 */
function resolveBundleRoot() {
    const installed = join(installRoot(), "host-cp");
    if (existsSync(join(installed, "peripheral-services")))
        return installed;
    const k8sRoot = resolveK8sAssetsRoot();
    if (k8sRoot) {
        const dev = join(k8sRoot, "..", "..");
        if (existsSync(join(dev, "peripheral-services")))
            return dev;
    }
    return null;
}
function installObservability(opts) {
    if (opts.skipObservability) {
        step("4/6 — observability (skipped via --skip-observability)");
        return;
    }
    step("4/6 — observability stack (Loki + Promtail + Grafana + Prometheus + Kyverno)");
    const observabilityDir = resolveObservabilityScriptsDir();
    if (!observabilityDir) {
        throw new Error(`Bundled observability assets missing. ` +
            `This indicates a corrupt or incomplete @pleri/olam-cli installation. ` +
            `Remediation: reinstall with 'npm install -g @pleri/olam-cli@latest', ` +
            `or open an issue at https://github.com/pleri/olam/issues with this error.`);
    }
    // Export OLAM_BUNDLE_ROOT so the scripts resolve helm-values/manifests from
    // the published tarball instead of `git rev-parse` (which fails outside the
    // monorepo). resolveBundleRoot returns null only in degenerate dev contexts;
    // the scripts handle absence by falling back to git.
    const bundleRoot = resolveBundleRoot();
    const scriptEnv = { ...process.env };
    if (bundleRoot) {
        scriptEnv.OLAM_BUNDLE_ROOT = bundleRoot;
    }
    for (const script of OBSERVABILITY_SCRIPTS) {
        const path = join(observabilityDir, script);
        if (!existsSync(path)) {
            printWarning(`observability script missing: ${script} — skipping`);
            continue;
        }
        printInfo("run", script);
        const result = spawnSync("bash", [path], {
            stdio: "inherit",
            env: scriptEnv,
        });
        if (result.status !== 0) {
            // loki-ingest's 10s scrub-wait is known to flake on cold-start; the
            // underlying Loki/Promtail install almost always succeeded.
            if (script === "loki-ingest.sh") {
                printWarning("loki-ingest non-zero — likely Promtail scrub-wait flake; continuing");
            }
            else {
                printError(`${script} failed — observability stack not ready`);
                process.exit(1);
            }
        }
        else {
            printSuccess(`${script} passed`);
        }
    }
}
/**
 * Step 5: Apply the peripheral-services manifest tree (IngressRoutes,
 * NetworkPolicies, ServiceMonitors, recording rules, Grafana dashboard
 * ConfigMaps, Kyverno ClusterPolicy). These are NOT covered by `olam
 * upgrade`'s host-cp manifest apply — without this step, the observability
 * stack runs but has no scrape targets and no dashboards.
 */
function applyPeripheralServicesManifests() {
    step("5/6 — peripheral-services manifests (IngressRoutes + ServiceMonitors + recording rules + dashboards + Kyverno)");
    const dir = resolvePeripheralServicesDir();
    if (!dir) {
        printWarning("bundled peripheral-services manifests not found — skipping");
        return;
    }
    const manifestsDir = join(dir, "manifests");
    if (!existsSync(manifestsDir)) {
        printWarning(`peripheral-services/manifests not found at ${manifestsDir} — skipping`);
        return;
    }
    // Skip deployment manifests (files matching *deploy*.yaml): these overlap with the
    // canonical deployment specs applied by `olam upgrade` step 3 (k8s/manifests/<svc>/50-deployment.yaml).
    // Applying them here causes a rolling update when upgrade re-applies with a different image
    // digest, making `kubectl rollout status` fail while the old pod is Terminating.
    // Standalone operators can still apply deployment manifests manually if needed.
    const manifestFiles = readdirSync(manifestsDir)
        .filter((f) => (f.endsWith(".yaml") || f.endsWith(".yml")) && !/deploy/i.test(f))
        .map((f) => join(manifestsDir, f));
    if (manifestFiles.length === 0) {
        printWarning("no non-deployment peripheral-services manifests found — skipping");
        return;
    }
    for (const manifest of manifestFiles) {
        printInfo("apply", manifest.replace(manifestsDir + "/", ""));
        const r = spawnSync("kubectl", ["apply", "-f", manifest], {
            stdio: "inherit",
        });
        if (r.status !== 0) {
            printError(`kubectl apply failed for ${manifest}`);
            process.exit(r.status ?? 1);
        }
    }
    printSuccess("peripheral-services manifests applied");
}
function delegateToUpgrade() {
    step("6/6 — apply host-cp + peripherals + rollout (olam upgrade)");
    // Re-invoke `olam upgrade -y` via the same process binary. The upgrade
    // flow's 8-step k8s path (upgrade-kubernetes.ts) handles namespace +
    // RBAC + Secrets + ghcr-pull + host-side docker-socket-proxy + manifest
    // apply + rollout status + port-forward + /health verify + audit log.
    const olamBin = process.argv[1] ?? "olam";
    const result = spawnSync(process.execPath, [olamBin, "upgrade", "-y"], {
        stdio: "inherit",
    });
    if (result.status !== 0) {
        printError("olam upgrade failed — see output above");
        process.exit(result.status ?? 1);
    }
}
function summary(opts) {
    step("done");
    process.stdout.write(`
  Cluster:    ${opts.cluster}
  Next steps:
    olam doctor                       # verify the stack
    olam services status              # peripherals 1/1
    kubectl get pods -n olam

  Grafana:
    kubectl port-forward -n monitoring svc/olam-grafana 3000:80
    open http://localhost:3000
    # admin password:
    kubectl get secret olam-grafana-admin -n monitoring \\
      -o jsonpath='{.data.admin-password}' | base64 -d
`);
}
export async function runBootstrapKubernetes(rawOpts) {
    // Default auto-install ON for macOS. The wizard contract is "fresh
    // machine → working cluster in one command", which requires installing
    // missing brew formulae without operator intervention. Linux operators
    // see a per-tool install hint (apt/dnf auto-install is queued as a
    // follow-up). Pass --no-auto-install or set autoInstall=false to
    // restore the earlier opt-in behaviour.
    const defaultAutoInstall = platform() === "darwin";
    const opts = {
        cluster: rawOpts.cluster ?? DEFAULT_K3S_CLUSTER,
        autoInstall: rawOpts.autoInstall ?? defaultAutoInstall,
        skipObservability: rawOpts.skipObservability ?? false,
        skipClusterCreate: rawOpts.skipClusterCreate ?? false,
    };
    printHeader("olam setup — k3s mode");
    printInfo("mode", "one-command bring-up of olam peripherals + observability on a local k3d cluster");
    preflight(opts);
    ensureSecrets();
    ensureColima();
    ensureCluster(opts);
    installObservability(opts);
    applyPeripheralServicesManifests();
    delegateToUpgrade();
    summary(opts);
    printSuccess("setup complete");
    return { exitCode: 0, summary: `setup complete on cluster ${opts.cluster}` };
}
//# sourceMappingURL=bootstrap-kubernetes.js.map