//! npm-based SCIP indexer install path (scip-python, scip-typescript).
//!
//! ## C2: augmented PATH
//!
//! The `npm install` spawn uses `env.augmented_path()` so the keg-only
//! Node.js brew bin dir discovered during `RuntimeBootstrap::ensure` is
//! reachable for this spawn (e.g. `/opt/homebrew/opt/node/bin/npm`).

use std::path::Path;
use std::process::Command;

use anyhow::{Context, Result, bail};
use ckg_fs::symlink;

use super::assert_online;
use super::bootstrap::{BootstrapEnv, RuntimeBootstrap};
use super::installer::{InstallCtx, Installer};

pub(super) struct NpmInstaller {
    pub lang_token: &'static str,
    pub pkg: String,
    pub binary: &'static str,
}

impl Installer for NpmInstaller {
    fn name(&self) -> &'static str {
        self.lang_token
    }
    fn install(&self, ctx: &InstallCtx<'_>, env: &dyn BootstrapEnv) -> Result<()> {
        RuntimeBootstrap::for_node().ensure(ctx)?;
        install_npm_indexer(ctx.bin_dir, &self.pkg, self.binary, env)
    }
}

/// `npm install` the given package into `<bin_dir>/node_modules/` so its
/// `bin/<binary>` becomes available, then symlink it next to `bin_dir`.
///
/// Hardening:
/// - `--ignore-scripts` blocks transitive preinstall/postinstall hooks
///   from running shell as the user. The SCIP indexers themselves don't
///   need lifecycle scripts; this closes the most common npm RCE vector.
/// - C2: `.env("PATH", env.augmented_path())` ensures the npm binary
///   found via keg-only Homebrew is reachable without mutating the global
///   process environment.
/// - TODO(security): pin to a reviewed package version (`@x.y.z`) once
///   the maintainer has done a release-tag review.
fn install_npm_indexer(
    bin_dir: &Path,
    pkg: &str,
    binary: &str,
    env: &dyn BootstrapEnv,
) -> Result<()> {
    assert_online(&format!("npm install {pkg}"))?;
    eprintln!(
        "{binary}: npm install --prefix {} --ignore-scripts {pkg}",
        bin_dir.display()
    );
    let status = Command::new("npm")
        .arg("install")
        .arg("--prefix")
        .arg(bin_dir)
        .arg("--ignore-scripts")
        .arg("--no-fund")
        .arg("--no-audit")
        .arg("--silent")
        .arg(pkg)
        // C2: inject augmented PATH so keg-only npm (e.g. brew node) is found.
        .env("PATH", env.augmented_path())
        .status()
        .with_context(|| "spawning npm")?;
    if !status.success() {
        bail!("npm install exited with {status}");
    }
    let src = bin_dir.join("node_modules").join(".bin").join(binary);
    let dest = bin_dir.join(binary);
    if !src.exists() {
        bail!(
            "npm reported success but no `{binary}` binary at {}",
            src.display()
        );
    }
    if dest.exists() || dest.symlink_metadata().is_ok() {
        let _ = std::fs::remove_file(&dest);
    }
    symlink(&src, &dest)?;
    // CR-fs-M1: don't chmod_exec the dest — `set_permissions` follows
    // symlinks and would flip the +x bit on the npm cache target
    // (foreign tree). Linux resolves symlink-exec through to the
    // target's mode (already set by `npm install`).
    eprintln!("{binary}: installed → {}", dest.display());
    Ok(())
}
