//! `ckg install-scip` — download SCIP indexer binaries to `$CKG_HOME/bin/`.
//!
//! Standalone-binary indexers are pulled from their GitHub Releases via
//! `curl + tar`. npm-based indexers (`scip-python`, `scip-typescript`) are
//! installed under `$CKG_HOME/bin/node_modules/` with `--ignore-scripts` so
//! transitive lifecycle scripts (preinstall / postinstall) cannot run as
//! the user.
//!
//! ## Security
//!
//! - **`CKG_OFFLINE=1`** refuses any network spawn (brew, gem, npm).
//! - Missing host runtimes (rust toolchain, node, ruby) are bootstrapped
//!   via Homebrew. On a TTY the user is prompted; pass `--yes` to
//!   auto-confirm. There is no curl-piped shell installer. macOS / Linux
//!   only — `detect_platform` refuses other OSes.
//! - npm runs with `--ignore-scripts` so package lifecycle hooks cannot
//!   execute shell on install. Pinning npm/gem versions to a reviewed
//!   release is left as a follow-up.
//! - `$CKG_HOME` and `$CKG_HOME/bin/` are chmod'd to `0700` after creation
//!   so the installed binaries / model cache are not world-writable.

mod bootstrap;
mod installer;
mod npm;
mod prompt;
mod ruby;
mod rustup;

use std::path::PathBuf;

use anyhow::{Result, bail};
use ckg_fs::create_dir_secure;
use clap::Args as ClapArgs;

use bootstrap::RealEnv;
use installer::{InstallCtx, Installer};
use npm::NpmInstaller;
use ruby::RubyInstaller;
use rustup::RustInstaller;

/// Pinned versions of the SCIP indexers ckg installs. Item #16 in the
/// remaining-issues inventory: `npm install`/`gem install` without a
/// version pin trusts whatever the registry serves at install time —
/// vulnerable to a malicious release of any pinned package between
/// vetting and rollout.
///
/// **Update SOP** (security-sensitive):
/// 1. Audit the upstream release on GitHub
///    (https://github.com/sourcegraph/scip-python,
///    https://github.com/sourcegraph/scip-typescript,
///    https://github.com/sourcegraph/scip-ruby).
/// 2. Verify maintainer identity, signed releases, no suspicious
///    transitive dep changes.
/// 3. Bump the constant. Note the audit date in the PR.
///
/// Setting any of these to `"latest"` bypasses pinning (allowed but
/// flagged via `tracing::warn!` at install time).
pub(super) const SCIP_PYTHON_VERSION: &str = "0.6.6";
pub(super) const SCIP_TYPESCRIPT_VERSION: &str = "0.4.0";
pub(super) const SCIP_RUBY_VERSION: &str = "0.4.7";

#[derive(ClapArgs, Debug)]
pub struct Args {
    /// Indexer(s) to install. Default: all four (rust ruby python typescript).
    #[arg(value_name = "LANG")]
    pub langs: Vec<String>,
    /// Auto-confirm installation of missing host runtimes via the system
    /// package manager (brew / scoop / winget). Skips the interactive prompt.
    #[arg(long, short = 'y')]
    pub yes: bool,
}

/// Refuse a network spawn when `CKG_OFFLINE=1` is set. Returns the value
/// passed in `.context(action)` so the resulting error reads like a
/// normal anyhow message.
pub(super) fn assert_online(action: &str) -> Result<()> {
    if std::env::var_os("CKG_OFFLINE").is_some() {
        bail!(
            "CKG_OFFLINE=1 set; refusing to {action}. Unset CKG_OFFLINE to allow this network call."
        );
    }
    Ok(())
}

pub fn run(home: Option<PathBuf>, args: Args) -> Result<()> {
    let base = crate::ckg_home(home)?;
    let bin_dir = base.join("bin");
    // create_dir_secure: mode 0700 set atomically at directory-create
    // time (no TOCTOU window between mkdir and chmod for fresh dirs).
    create_dir_secure(&base)?;
    create_dir_secure(&bin_dir)?;
    // Default = all four. rust/ruby ship as binaries; python/typescript via npm.
    let langs: Vec<String> = if args.langs.is_empty() {
        vec![
            "rust".into(),
            "ruby".into(),
            "python".into(),
            "typescript".into(),
        ]
    } else {
        args.langs
    };

    let (os, arch) = detect_platform()?;
    eprintln!("platform: {os}-{arch}");
    eprintln!("install dir: {}", bin_dir.display());

    let ctx = InstallCtx {
        bin_dir: &bin_dir,
        yes: args.yes,
    };
    // C2: one shared RealEnv per `run` invocation so PATH augmentations
    // (keg-only brew bin dirs) accumulated during one installer's bootstrap
    // are visible to subsequent spawns within the same session.
    let env = RealEnv::new();
    let mut errs: Vec<String> = Vec::new();
    for lang in &langs {
        match build_installer(lang) {
            Ok(inst) => {
                if let Err(e) = inst.install(&ctx, &env) {
                    errs.push(format!("{}: {e}", inst.name()));
                }
            }
            Err(e) => errs.push(format!("{lang}: {e}")),
        }
    }

    eprintln!("\nDone. `ckg index` will auto-discover these (it checks");
    eprintln!("$CKG_HOME/bin/ before $PATH). To use them outside ckg:");
    eprintln!("  export PATH=\"{}:$PATH\"", bin_dir.display());
    // M8: non-zero exit on partial install failure so callers (CI, setup
    // scripts) can detect incomplete installs. List all failures before
    // returning so the user can fix them all in one pass.
    if !errs.is_empty() {
        eprintln!("\nInstall failures:");
        for e in &errs {
            eprintln!("  - {e}");
        }
        bail!(
            "{} indexer(s) failed to install; see errors above",
            errs.len()
        );
    }
    Ok(())
}

fn build_installer(lang: &str) -> Result<Box<dyn Installer>> {
    Ok(match lang {
        "rust" => Box::new(RustInstaller),
        "ruby" => Box::new(RubyInstaller),
        "python" => Box::new(NpmInstaller {
            lang_token: "python",
            pkg: format!("@sourcegraph/scip-python@{SCIP_PYTHON_VERSION}"),
            binary: "scip-python",
        }),
        "typescript" | "ts" => Box::new(NpmInstaller {
            lang_token: "typescript",
            pkg: format!("@sourcegraph/scip-typescript@{SCIP_TYPESCRIPT_VERSION}"),
            binary: "scip-typescript",
        }),
        other => bail!("unknown indexer {other:?} (supported: rust ruby python typescript)"),
    })
}

fn detect_platform() -> Result<(&'static str, &'static str)> {
    let os = if cfg!(target_os = "macos") {
        "darwin"
    } else if cfg!(target_os = "linux") {
        "linux"
    } else {
        bail!("unsupported OS — scip indexers ship only for macOS / Linux");
    };
    let arch = if cfg!(target_arch = "aarch64") {
        "aarch64"
    } else if cfg!(target_arch = "x86_64") {
        "x86_64"
    } else {
        bail!("unsupported arch");
    };
    Ok((os, arch))
}
