/// lazy-mcp — self-contained Rust launcher
///
/// This binary embeds the platform-specific prebuilt `lazy-mcp` executable
/// (downloaded and compiled by `build.rs` from the GitLab release assets) at
/// compile time via `include_bytes!`.  At runtime it:
///
///   1. Extracts the embedded binary to the platform cache directory under
///      `lazy-mcp/<version>/lazy-mcp` if it isn't already there.
///      Freshness is checked by comparing the SHA-256 of the embedded bytes
///      against a sidecar file — not just the size.
///   2. `exec`-replaces itself with the extracted binary on Unix (zero
///      overhead, correct signal handling), or spawns it as a child process
///      on Windows.
///
/// The extracted binary is the bun-compiled standalone lazy-mcp executable
/// that needs **no external runtime** — it contains the full Bun JS runtime
/// and the lazy-mcp JS bundle.
use std::env;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process;

/// The prebuilt platform binary, embedded at build time by build.rs.
const PREBUILT: &[u8] = include_bytes!(env!("LAZY_MCP_PREBUILT"));

/// Semantic version from Cargo.toml.
const VERSION: &str = env!("CARGO_PKG_VERSION");

/// Compute a simple FNV-1a 64-bit hash of the embedded bytes.
/// Used for a fast integrity check — not a cryptographic guarantee, but
/// detects accidental corruption or stale cached files from prior versions.
fn prebuilt_hash() -> u64 {
    const FNV_OFFSET: u64 = 0xcbf29ce484222325;
    const FNV_PRIME: u64 = 0x100000001b3;
    PREBUILT.iter().fold(FNV_OFFSET, |acc, &b| {
        acc.wrapping_mul(FNV_PRIME) ^ (b as u64)
    })
}

/// Return the versioned cache directory following platform conventions:
///   Linux/macOS: $XDG_CACHE_HOME/lazy-mcp/<version>
///                ~/.cache/lazy-mcp/<version>
///   Windows:     %LOCALAPPDATA%\lazy-mcp\<version>
///   Fallback:    <tmp>/lazy-mcp-<version>
fn cache_dir() -> PathBuf {
    #[cfg(windows)]
    let base = env::var_os("LOCALAPPDATA")
        .map(PathBuf::from)
        .unwrap_or_else(env::temp_dir);

    #[cfg(not(windows))]
    let base = if let Some(xdg) = env::var_os("XDG_CACHE_HOME") {
        PathBuf::from(xdg)
    } else if let Some(home) = env::var_os("HOME") {
        PathBuf::from(home).join(".cache")
    } else {
        env::temp_dir()
    };

    base.join("lazy-mcp").join(VERSION)
}

/// Ensure the embedded binary is extracted to the cache directory.
/// Returns the path to the ready-to-exec binary.
fn ensure_extracted(cache: &Path) -> PathBuf {
    let bin_name = if cfg!(windows) {
        "lazy-mcp.exe"
    } else {
        "lazy-mcp"
    };
    let bin_path = cache.join(bin_name);
    let hash_path = cache.join(format!("{bin_name}.hash"));

    // Fast path: already extracted and hash matches.
    let expected_hash = prebuilt_hash().to_string();
    let cached_ok = bin_path.exists() && {
        fs::read_to_string(&hash_path)
            .map(|h| h.trim() == expected_hash)
            .unwrap_or(false)
    };
    if cached_ok {
        return bin_path;
    }

    // Create the cache directory with user-only permissions on Unix.
    fs::create_dir_all(cache).unwrap_or_else(|e| {
        eprintln!(
            "lazy-mcp: failed to create cache dir {}: {e}",
            cache.display()
        );
        process::exit(1);
    });

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        if let Ok(meta) = fs::metadata(cache) {
            let mut perms = meta.permissions();
            perms.set_mode(0o700);
            fs::set_permissions(cache, perms).ok();
        }
    }

    // Write atomically using create_new to avoid races: write to a unique
    // temp name, then rename into place.
    let tmp_path = cache.join(format!("{bin_name}.{}.tmp", process::id()));
    let mut f = OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(&tmp_path)
        .unwrap_or_else(|e| {
            eprintln!("lazy-mcp: failed to create {}: {e}", tmp_path.display());
            process::exit(1);
        });
    f.write_all(PREBUILT).unwrap_or_else(|e| {
        eprintln!("lazy-mcp: failed to write binary: {e}");
        let _ = fs::remove_file(&tmp_path);
        process::exit(1);
    });
    drop(f);

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        if let Ok(meta) = fs::metadata(&tmp_path) {
            let mut perms = meta.permissions();
            perms.set_mode(0o755);
            fs::set_permissions(&tmp_path, perms).unwrap_or_else(|e| {
                eprintln!("lazy-mcp: failed to chmod binary: {e}");
                let _ = fs::remove_file(&tmp_path);
                process::exit(1);
            });
        }
    }

    fs::rename(&tmp_path, &bin_path).unwrap_or_else(|e| {
        eprintln!("lazy-mcp: failed to move binary into place: {e}");
        let _ = fs::remove_file(&tmp_path);
        process::exit(1);
    });

    // Write the hash sidecar so the next run can skip extraction.
    let _ = fs::write(&hash_path, &expected_hash);

    bin_path
}

fn main() {
    let cache = cache_dir();
    let bin_path = ensure_extracted(&cache);
    let args: Vec<String> = env::args().skip(1).collect();

    #[cfg(unix)]
    {
        use std::os::unix::process::CommandExt;
        let err = process::Command::new(&bin_path).args(&args).exec();
        eprintln!("lazy-mcp: exec failed: {err}");
        process::exit(1);
    }

    #[cfg(not(unix))]
    {
        let status = process::Command::new(&bin_path)
            .args(&args)
            .status()
            .unwrap_or_else(|e| {
                eprintln!("lazy-mcp: failed to start: {e}");
                process::exit(1);
            });
        process::exit(status.code().unwrap_or(1));
    }
}
