#!/usr/bin/env node

/**
 * `rastack update` — update the CLI in place.
 *
 *   rastack update [<version>] [--version <v>] [--check] [--dry-run]
 *
 *   (no args)        update to the latest published version
 *   <version>        pin a target, e.g. `rastack update 0.0.40`
 *   --check          report current vs. latest, install nothing
 *   --dry-run        print the npm command that would run, don't run it
 *
 * `rastack` is a global npm package installed into a self-contained per-user
 * prefix (`~/.rastack`, via the hosted installer). Updating re-runs
 * `npm install -g rastack@<target>` against that SAME prefix — derived from where
 * the running CLI lives — so it never scatters a second copy into npm's default
 * global location. Pure decisions live in `update.ts`; this is the IO wrapper.
 */

import { execFileSync, execSync } from "child_process";
import { readFileSync } from "fs";
import * as path from "path";
import {
  globalPrefixFromPackageDir,
  npmBinary,
  npmInstallArgs,
  npmShellCommand,
  npmSpawnShell,
  parseUpdateFlags,
  updateAvailable,
} from "./update";

/** The npm executable for this platform (`npm.cmd` on Windows, else `npm`). */
const NPM = npmBinary();

/**
 * Whether to launch npm through a shell. Required on Windows, where a bare
 * `.cmd` spawn is rejected with `EINVAL` since the Node CVE-2024-27980 fix;
 * false on POSIX so the direct spawn stays injection-free.
 */
const NPM_SHELL = npmSpawnShell();

/** The installed rastack package directory (…/rastack), one up from dist/. */
const PKG_DIR = path.join(__dirname, "..");
const PKG_JSON = path.join(PKG_DIR, "package.json");

/** Read the version from a package.json on disk, or "unknown" if unreadable. */
function readVersion(file: string): string {
  try {
    return JSON.parse(readFileSync(file, "utf-8")).version ?? "unknown";
  } catch {
    return "unknown";
  }
}

/**
 * Run npm and return stdout, or throw on failure. Windows must launch the
 * `npm.cmd` shim through a shell; we pass one command string to `execSync`
 * rather than an args array with `shell: true` (Node 22+ DEP0190 — args would be
 * concatenated unescaped). The only non-literal token, the version, is already
 * sanitized (`sanitizeVersion`). POSIX keeps the shell-free `execFile`.
 */
function runNpm(
  args: string[],
  opts: { stdio: "inherit" | ["ignore", "pipe", "pipe"]; env?: NodeJS.ProcessEnv },
): Buffer | null {
  // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
  return NPM_SHELL
    ? execSync(npmShellCommand(NPM, args), opts)
    : execFileSync(NPM, args, opts);
}

/** Capture `npm <args>` stdout, or null when npm is absent / the call fails. */
function npmCapture(args: string[]): string | null {
  try {
    const out = runNpm(args, { stdio: ["ignore", "pipe", "pipe"] });
    return (out ?? "").toString().trim();
  } catch {
    return null;
  }
}

function main(argv: string[]): void {
  const flags = parseUpdateFlags(argv);
  const current = readVersion(PKG_JSON);
  const prefix = globalPrefixFromPackageDir(PKG_DIR);

  // The npm prefix must land the update where this CLI actually lives.
  const env = { ...process.env };
  if (prefix) env.npm_config_prefix = prefix;

  if (flags.check) {
    const latest = npmCapture(["view", "rastack@latest", "version"]);
    if (!latest) {
      console.error(
        "  ✗ could not reach the npm registry to check for updates.",
      );
      process.exit(1);
    }
    console.log(`  rastack ${current} (installed) — latest is ${latest}`);
    if (updateAvailable(current, latest)) {
      console.log(`  ▸ run \`rastack update\` to upgrade to ${latest}.`);
    } else {
      console.log("  ✓ you are on the latest version.");
    }
    return;
  }

  const args = npmInstallArgs(flags.version);
  if (flags.dryRun) {
    const pfx = prefix ? `npm_config_prefix=${prefix} ` : "";
    console.log(`\n  ${pfx}npm ${args.join(" ")}\n`);
    return;
  }

  if (!prefix) {
    // Running from a source checkout (not a global install) — nothing to self-update.
    console.error(
      "  ✗ rastack is not running from a global install, so `update` does not apply.\n" +
        "    Reinstall with the hosted installer, or `npm install -g rastack` to manage it.",
    );
    process.exit(1);
  }

  console.log(
    `\n▸ Updating rastack (${current} → ${flags.version}) in ${prefix}\n`,
  );
  try {
    // Re-runs the developer's own npm to update this global package. On Windows
    // the `.cmd` shim is launched through a shell (EINVAL otherwise); the only
    // caller-supplied token — the version — is sanitized in `parseUpdateFlags`,
    // and every other arg is a fixed literal, so nothing injectable reaches it.
    runNpm(args, { stdio: "inherit", env });
  } catch (err) {
    console.error(
      `\n  ✗ npm failed to update rastack. Re-run for detail:\n    npm install -g ${npmInstallArgs(flags.version)[2]}\n    ${(err as Error).message}`,
    );
    process.exit(1);
  }

  // Re-read the (now replaced) package.json to report where we landed.
  const updated = readVersion(PKG_JSON);
  if (updated === current) {
    console.log(`\n  ✓ rastack is up to date (${updated}).\n`);
  } else {
    console.log(`\n  ✓ rastack updated ${current} → ${updated}.\n`);
  }
}

if (require.main === module) {
  main(process.argv.slice(2));
}
