//! Build script for the `code_graph-mcp` stub crate.
//!
//! Mirrors `crates/code_graph/build.rs` — see that file for the
//! mode/contract documentation. The only difference is which binary
//! is downloaded (`code_graph-mcp` vs `code_graph`), which is driven
//! by `manifest::BINARY_NAME` / `manifest::ARCHIVE_PREFIX`.

use std::env;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};

#[path = "src/manifest.rs"]
#[allow(dead_code)] // MINISIGN_PUBKEY is reserved for future use
mod manifest;

const PLACEHOLDER_PREAMBLE: &[u8] = b"CG_STUB_PLACEHOLDER\n";

fn main() {
    println!("cargo:rerun-if-changed=src/manifest.rs");
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-env-changed=CG_STUB_OFFLINE");
    println!("cargo:rerun-if-env-changed=CG_STUB_BINARY_PATH");

    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
    let embedded = out_dir.join("embedded_binary");

    // 1. Explicit local binary wins (air-gap + pre-publish validation).
    if let Ok(path) = env::var("CG_STUB_BINARY_PATH") {
        println!("cargo:warning=using local binary from CG_STUB_BINARY_PATH={path}");
        let bytes = fs::read(&path)
            .unwrap_or_else(|err| panic!("CG_STUB_BINARY_PATH={path}: read failed: {err}"));
        fs::write(&embedded, &bytes).expect("write embedded_binary failed");
        return;
    }

    // 2. Explicit offline placeholder.
    if env::var("CG_STUB_OFFLINE").as_deref() == Ok("1") {
        write_placeholder(&embedded, "CG_STUB_OFFLINE=1");
        return;
    }

    // 3. Untemplated manifest (workspace dev build).
    if manifest::SHA256_BY_TARGET.is_empty() {
        write_placeholder(&embedded, "SHA256_BY_TARGET is empty (development build)");
        return;
    }

    // 4. Download the archive + verify against the per-target SHA map.
    let bytes = download_archive_and_extract();
    fs::write(&embedded, &bytes).expect("write embedded_binary failed");
}

fn write_placeholder(path: &Path, reason: &str) {
    let mut content = PLACEHOLDER_PREAMBLE.to_vec();
    content.extend_from_slice(format!("reason: {reason}\n").as_bytes());
    content.extend_from_slice(
        b"This build is a development placeholder. Install via:\n  \
          curl -fsSL https://cg.renso.ai/install.sh | sh\n",
    );
    fs::write(path, content).expect("write placeholder failed");
}

fn target_triple() -> String {
    env::var("TARGET").expect("TARGET not set by cargo")
}

fn expected_sha_for(target: &str) -> &'static str {
    for (t, sha) in manifest::SHA256_BY_TARGET {
        if *t == target {
            return sha;
        }
    }
    let known: Vec<&str> = manifest::SHA256_BY_TARGET.iter().map(|(t, _)| *t).collect();
    panic!(
        "no prebuilt binary for target `{target}` in this release.\n\
         Supported targets: {known:?}.\n\
         Workarounds:\n  \
           - Set CG_STUB_OFFLINE=1 and exec the binary yourself.\n  \
           - Set CG_STUB_BINARY_PATH=/path/to/code_graph-mcp and \
             rerun the wrapper build."
    );
}

fn archive_format(target: &str) -> &'static str {
    if target.contains("windows") {
        "zip"
    } else {
        "tar.gz"
    }
}

fn archive_url(target: &str) -> String {
    let version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION not set");
    let ext = archive_format(target);
    format!(
        "{base}/v{version}/{prefix}-{target}.{ext}",
        base = manifest::RELEASE_URL_BASE,
        prefix = manifest::ARCHIVE_PREFIX,
        version = version,
        target = target,
        ext = ext,
    )
}

fn download_archive_and_extract() -> Vec<u8> {
    let target = target_triple();
    let expected_sha = expected_sha_for(&target);
    let url = archive_url(&target);

    println!("cargo:warning=downloading {url}");
    let resp = ureq::get(&url)
        .call()
        .unwrap_or_else(|err| panic!("download failed: {url}: {err}"));
    let mut archive_bytes = Vec::new();
    resp.into_reader()
        .read_to_end(&mut archive_bytes)
        .unwrap_or_else(|err| panic!("download read failed: {url}: {err}"));

    verify_sha256(&archive_bytes, expected_sha, &url);

    let binary_name = if target.contains("windows") {
        format!("{}.exe", manifest::BINARY_NAME)
    } else {
        manifest::BINARY_NAME.to_string()
    };

    if archive_format(&target) == "zip" {
        extract_zip_entry(&archive_bytes, &binary_name)
    } else {
        extract_tar_gz_entry(&archive_bytes, &binary_name)
    }
}

fn extract_tar_gz_entry(bytes: &[u8], name: &str) -> Vec<u8> {
    let gz = flate2::read::GzDecoder::new(bytes);
    let mut ar = tar::Archive::new(gz);
    for entry in ar.entries().expect("tar entries") {
        let mut entry = entry.expect("tar entry");
        let path = entry.path().expect("tar entry path").into_owned();
        if path.file_name().map(|f| f == name).unwrap_or(false) {
            let mut out = Vec::new();
            entry.read_to_end(&mut out).expect("tar entry read");
            return out;
        }
    }
    panic!("binary `{name}` not found in tarball");
}

fn extract_zip_entry(bytes: &[u8], name: &str) -> Vec<u8> {
    use std::io::{Cursor, Read};
    let mut archive = zip::ZipArchive::new(Cursor::new(bytes))
        .unwrap_or_else(|err| panic!("zip parse failed: {err}"));
    for i in 0..archive.len() {
        let mut entry = archive
            .by_index(i)
            .unwrap_or_else(|err| panic!("zip entry {i}: {err}"));
        let entry_path = entry.enclosed_name().map(|p| p.to_path_buf());
        let matches = entry_path
            .as_ref()
            .and_then(|p| p.file_name())
            .map(|f| f == name)
            .unwrap_or(false);
        if matches {
            let mut out = Vec::with_capacity(entry.size() as usize);
            entry
                .read_to_end(&mut out)
                .unwrap_or_else(|err| panic!("zip read: {err}"));
            return out;
        }
    }
    panic!("binary `{name}` not found in zip archive");
}

fn verify_sha256(bytes: &[u8], expected: &str, url: &str) {
    use sha2::{Digest, Sha256};
    let mut hasher = Sha256::new();
    hasher.update(bytes);
    let actual = hex::encode(hasher.finalize());
    if actual != expected {
        panic!(
            "SHA256 mismatch:\n  expected: {expected}\n  actual:   {actual}\n  \
             URL:      {url}\n\nThe published crate references an \
             archive whose hash does not match what was downloaded."
        );
    }
}

// code_graph workspace-bench broad_edit
