//! Trampoline for the `code_graph-mcp` stub crate.
//!
//! Mirrors `crates/code_graph/src/main.rs`. Only the binary file name
//! changes: this one extracts and execs `code_graph-mcp`.

#![forbid(unsafe_code)]

use std::env;
use std::ffi::OsString;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process;

const EMBEDDED: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/embedded_binary"));
const PLACEHOLDER_PREAMBLE: &[u8] = b"CG_STUB_PLACEHOLDER\n";

const BIN_NAME: &str = "code_graph-mcp";
const CACHE_STAMP_VERSION: &str = "code-graph-cache-v2";

fn main() {
    if EMBEDDED.starts_with(PLACEHOLDER_PREAMBLE) {
        eprintln!(
            "error: this `{BIN_NAME}` build is a development placeholder.\n\
             \n\
             Install a real release with the script installer:\n  \
               curl -fsSL https://cg.renso.ai/install.sh | sh"
        );
        process::exit(126);
    }

    if embedded_binary_looks_like_launcher() {
        eprintln!(
            "error: this `{BIN_NAME}` build embedded another launcher instead of the product binary.\n\
             \n\
             Reinstall from a current release, then retry `{BIN_NAME} --register`."
        );
        process::exit(126);
    }

    let cache_dir = resolve_cache_dir().unwrap_or_else(|err| {
        eprintln!("error: cannot resolve cache directory: {err}");
        process::exit(127);
    });
    let bin_path = cache_dir.join(bin_file_name());

    let exec_path = ensure_extracted(&cache_dir, &bin_path).unwrap_or_else(|err| {
        eprintln!(
            "error: failed to extract {BIN_NAME} to {}: {err}",
            bin_path.display()
        );
        process::exit(127);
    });

    let args: Vec<OsString> = env::args_os().skip(1).collect();
    exec(&exec_path, &args);
}

fn bin_file_name() -> &'static str {
    if cfg!(windows) {
        "code_graph-mcp.exe"
    } else {
        "code_graph-mcp"
    }
}

fn resolve_cache_dir() -> io::Result<PathBuf> {
    let version = env!("CARGO_PKG_VERSION");

    #[cfg(windows)]
    let base = env::var_os("LOCALAPPDATA")
        .map(PathBuf::from)
        .or_else(|| {
            env::var_os("USERPROFILE").map(|p| PathBuf::from(p).join("AppData").join("Local"))
        })
        .ok_or_else(|| io::Error::other("LOCALAPPDATA and USERPROFILE both unset"))?;

    #[cfg(not(windows))]
    let base = env::var_os("XDG_CACHE_HOME")
        .map(PathBuf::from)
        .or_else(|| env::var_os("HOME").map(|p| PathBuf::from(p).join(".cache")))
        .ok_or_else(|| io::Error::other("XDG_CACHE_HOME and HOME both unset"))?;

    Ok(base.join("code-graph").join(version))
}

fn ensure_extracted(cache_dir: &Path, bin_path: &Path) -> io::Result<PathBuf> {
    if cache_is_current(bin_path) && !current_exe_is(bin_path) {
        return Ok(bin_path.to_path_buf());
    }

    match extract(cache_dir, bin_path) {
        Ok(()) => Ok(bin_path.to_path_buf()),
        Err(primary_err) => {
            let fallback = cache_dir.join(fallback_bin_file_name());
            if fallback != bin_path && extract(cache_dir, &fallback).is_ok() {
                return Ok(fallback);
            }
            Err(primary_err)
        }
    }
}

fn extract(cache_dir: &Path, bin_path: &Path) -> io::Result<()> {
    fs::create_dir_all(cache_dir)?;
    let tmp_path = bin_path.with_extension("partial");
    fs::write(&tmp_path, EMBEDDED)?;

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = fs::metadata(&tmp_path)?.permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&tmp_path, perms)?;
    }

    replace_file(&tmp_path, bin_path)?;
    fs::write(cache_stamp_path(bin_path), cache_stamp())?;
    Ok(())
}

fn replace_file(tmp_path: &Path, bin_path: &Path) -> io::Result<()> {
    #[cfg(windows)]
    {
        match fs::rename(tmp_path, bin_path) {
            Ok(()) => Ok(()),
            Err(first_err) if bin_path.exists() => {
                if let Err(remove_err) = fs::remove_file(bin_path) {
                    let _ = fs::remove_file(tmp_path);
                    return Err(remove_err);
                }
                fs::rename(tmp_path, bin_path).map_err(|rename_err| {
                    let _ = fs::remove_file(tmp_path);
                    io::Error::new(
                        rename_err.kind(),
                        format!(
                            "replace {} after initial rename failed ({first_err}): {rename_err}",
                            bin_path.display()
                        ),
                    )
                })
            }
            Err(err) => {
                let _ = fs::remove_file(tmp_path);
                Err(err)
            }
        }
    }

    #[cfg(not(windows))]
    {
        fs::rename(tmp_path, bin_path)
    }
}

fn cache_is_current(bin_path: &Path) -> bool {
    bin_path.is_file()
        && fs::read_to_string(cache_stamp_path(bin_path))
            .map(|stamp| stamp == cache_stamp())
            .unwrap_or(false)
}

fn cache_stamp_path(bin_path: &Path) -> PathBuf {
    let mut path = bin_path.as_os_str().to_os_string();
    path.push(".stamp");
    PathBuf::from(path)
}

fn cache_stamp() -> String {
    format!(
        "{CACHE_STAMP_VERSION}\nbin={BIN_NAME}\nlen={}\n",
        EMBEDDED.len()
    )
}

fn fallback_bin_file_name() -> String {
    if cfg!(windows) {
        format!("{BIN_NAME}.real.exe")
    } else {
        format!("{BIN_NAME}.real")
    }
}

fn current_exe_is(path: &Path) -> bool {
    let Ok(current) = env::current_exe() else {
        return false;
    };
    let Ok(current) = current.canonicalize() else {
        return false;
    };
    let Ok(path) = path.canonicalize() else {
        return false;
    };
    current == path
}

fn embedded_binary_looks_like_launcher() -> bool {
    contains_bytes(EMBEDDED, PLACEHOLDER_PREAMBLE)
}

fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
    !needle.is_empty() && haystack.windows(needle.len()).any(|w| w == needle)
}

#[cfg(unix)]
fn exec(bin_path: &Path, args: &[OsString]) -> ! {
    use std::os::unix::process::CommandExt;
    let err = process::Command::new(bin_path).args(args).exec();
    eprintln!("error: exec {} failed: {err}", bin_path.display());
    process::exit(127);
}

#[cfg(windows)]
fn exec(bin_path: &Path, args: &[OsString]) -> ! {
    let status = match process::Command::new(bin_path).args(args).status() {
        Ok(s) => s,
        Err(err) => {
            eprintln!("error: spawn {} failed: {err}", bin_path.display());
            process::exit(127);
        }
    };
    process::exit(status.code().unwrap_or(127));
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::{SystemTime, UNIX_EPOCH};

    fn temp_dir(name: &str) -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let dir = env::temp_dir().join(format!("{BIN_NAME}-{name}-{}-{nanos}", process::id()));
        fs::create_dir_all(&dir).unwrap();
        dir
    }

    #[test]
    fn refreshes_cache_without_stamp() {
        let dir = temp_dir("refreshes-cache-without-stamp");
        let bin_path = dir.join(bin_file_name());
        fs::write(&bin_path, b"stale launcher").unwrap();

        let exec_path = ensure_extracted(&dir, &bin_path).unwrap();

        assert_eq!(exec_path, bin_path);
        assert_eq!(fs::read(&bin_path).unwrap(), EMBEDDED);
        assert_eq!(
            fs::read_to_string(cache_stamp_path(&bin_path)).unwrap(),
            cache_stamp()
        );
        fs::remove_dir_all(dir).unwrap();
    }

    #[test]
    fn reuses_cache_with_current_stamp() {
        let dir = temp_dir("reuses-cache-with-current-stamp");
        let bin_path = dir.join(bin_file_name());
        fs::write(&bin_path, b"already cached").unwrap();
        fs::write(cache_stamp_path(&bin_path), cache_stamp()).unwrap();

        let exec_path = ensure_extracted(&dir, &bin_path).unwrap();

        assert_eq!(exec_path, bin_path);
        assert_eq!(fs::read(&bin_path).unwrap(), b"already cached");
        fs::remove_dir_all(dir).unwrap();
    }

    #[test]
    fn detects_embedded_launcher_marker() {
        assert!(contains_bytes(
            b"prefix CG_STUB_PLACEHOLDER\n suffix",
            PLACEHOLDER_PREAMBLE
        ));
        assert!(!contains_bytes(b"prefix suffix", PLACEHOLDER_PREAMBLE));
    }
}

// code_graph workspace-bench broad_edit
