// sigmashake-ssg — cargo-installable wrapper around the prebuilt ssg binary.
//
// Two installation paths, both supported:
//
//   cargo binstall sigmashake-ssg       (preferred — pulls prebuilt direct)
//   cargo install sigmashake-ssg        (this binary — downloads on first run)
//
// On `cargo install`, the compiled artifact is THIS executable. On first run
// it fetches the matching prebuilt `ssg-<plat>.{tar.gz,zip}` from the
// sigmashakeinc/ssg GitHub release for its own crate version, caches the
// extracted binary under `~/.cache/sigmashake-ssg/<version>/`, and execs
// into it. Subsequent runs hit the cache and exec immediately.
//
// The crate name is `sigmashake-ssg` because the bare `ssg` name on crates.io
// belongs to an unrelated, abandoned project (last release 0.0.8, 2019). We
// don't want to take it over — `cargo binstall sigmashake-ssg` is the path.

use std::env;
use std::fs;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};

#[cfg(unix)]
use std::os::unix::process::CommandExt;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

const CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
const RELEASE_BASE: &str = "https://github.com/sigmashakeinc/ssg/releases/download";

struct Platform {
    asset: &'static str,
    archive: ArchiveKind,
    bin_in_archive: &'static str,
    bin_local: &'static str,
}

#[derive(Clone, Copy)]
enum ArchiveKind {
    TarGz,
    Zip,
}

fn detect_platform() -> Option<Platform> {
    let os = env::consts::OS;
    let arch = env::consts::ARCH;
    match (os, arch) {
        ("linux", "x86_64") => Some(Platform {
            asset: "ssg-linux-x64.tar.gz",
            archive: ArchiveKind::TarGz,
            bin_in_archive: "ssg",
            bin_local: "ssg",
        }),
        ("linux", "aarch64") => Some(Platform {
            asset: "ssg-linux-arm64.tar.gz",
            archive: ArchiveKind::TarGz,
            bin_in_archive: "ssg",
            bin_local: "ssg",
        }),
        ("macos", "x86_64") => Some(Platform {
            asset: "ssg-darwin-x64.tar.gz",
            archive: ArchiveKind::TarGz,
            bin_in_archive: "ssg",
            bin_local: "ssg",
        }),
        ("macos", "aarch64") => Some(Platform {
            asset: "ssg-darwin-arm64.tar.gz",
            archive: ArchiveKind::TarGz,
            bin_in_archive: "ssg",
            bin_local: "ssg",
        }),
        ("windows", "x86_64") => Some(Platform {
            asset: "ssg-win32-x64.zip",
            archive: ArchiveKind::Zip,
            bin_in_archive: "ssg.exe",
            bin_local: "ssg.exe",
        }),
        _ => None,
    }
}

fn cache_root() -> io::Result<PathBuf> {
    let base = if let Some(home) = dirs_cache_dir() {
        home
    } else {
        env::temp_dir()
    };
    let dir = base.join("sigmashake-ssg").join(CRATE_VERSION);
    fs::create_dir_all(&dir)?;
    Ok(dir)
}

fn dirs_cache_dir() -> Option<PathBuf> {
    // Minimal XDG-aware cache lookup without the `dirs` crate dependency.
    #[cfg(unix)]
    {
        if let Ok(xdg) = env::var("XDG_CACHE_HOME") {
            if !xdg.is_empty() {
                return Some(PathBuf::from(xdg));
            }
        }
        if let Ok(home) = env::var("HOME") {
            if !home.is_empty() {
                return Some(PathBuf::from(home).join(".cache"));
            }
        }
        None
    }
    #[cfg(windows)]
    {
        if let Ok(local) = env::var("LOCALAPPDATA") {
            if !local.is_empty() {
                return Some(PathBuf::from(local));
            }
        }
        if let Ok(profile) = env::var("USERPROFILE") {
            if !profile.is_empty() {
                return Some(PathBuf::from(profile).join("AppData").join("Local"));
            }
        }
        None
    }
}

fn fetch(url: &str) -> io::Result<Vec<u8>> {
    let agent = ureq::AgentBuilder::new()
        .user_agent(&format!("sigmashake-ssg/{}", CRATE_VERSION))
        .build();
    let resp = agent
        .get(url)
        .call()
        .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{e}")))?;
    if resp.status() < 200 || resp.status() >= 300 {
        return Err(io::Error::new(
            io::ErrorKind::Other,
            format!("HTTP {} fetching {}", resp.status(), url),
        ));
    }
    let mut buf = Vec::new();
    resp.into_reader().read_to_end(&mut buf)?;
    Ok(buf)
}

fn extract(plat: &Platform, body: &[u8], out_dir: &Path) -> io::Result<PathBuf> {
    let target = out_dir.join(plat.bin_local);
    match plat.archive {
        ArchiveKind::TarGz => {
            let gz = flate2::read::GzDecoder::new(body);
            let mut tar = tar::Archive::new(gz);
            for entry in tar.entries()? {
                let mut entry = entry?;
                let path = entry.path()?.into_owned();
                let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
                if name == plat.bin_in_archive {
                    let mut out = fs::File::create(&target)?;
                    io::copy(&mut entry, &mut out)?;
                    out.flush()?;
                    drop(out);
                    set_executable(&target)?;
                    return Ok(target);
                }
            }
            Err(io::Error::new(
                io::ErrorKind::NotFound,
                format!("`{}` not found inside archive", plat.bin_in_archive),
            ))
        }
        ArchiveKind::Zip => {
            let cursor = io::Cursor::new(body);
            let mut zip = zip::ZipArchive::new(cursor)
                .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{e}")))?;
            for i in 0..zip.len() {
                let mut entry = zip
                    .by_index(i)
                    .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{e}")))?;
                let entry_name = entry.name().to_string();
                // Windows zip uses backslashes; entries are like `ssg.exe`
                // at top level so a basename match is sufficient.
                let basename = entry_name
                    .rsplit_once(|c| c == '/' || c == '\\')
                    .map(|(_, b)| b)
                    .unwrap_or(&entry_name);
                if basename == plat.bin_in_archive {
                    let mut out = fs::File::create(&target)?;
                    io::copy(&mut entry, &mut out)?;
                    out.flush()?;
                    drop(out);
                    set_executable(&target)?;
                    return Ok(target);
                }
            }
            Err(io::Error::new(
                io::ErrorKind::NotFound,
                format!("`{}` not found inside zip", plat.bin_in_archive),
            ))
        }
    }
}

fn set_executable(path: &Path) -> io::Result<()> {
    #[cfg(unix)]
    {
        let mut perms = fs::metadata(path)?.permissions();
        perms.set_mode(0o755);
        fs::set_permissions(path, perms)?;
    }
    #[cfg(not(unix))]
    {
        let _ = path;
    }
    Ok(())
}

fn ensure_binary() -> io::Result<PathBuf> {
    let plat = detect_platform().ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::Unsupported,
            format!(
                "unsupported platform: {}-{} — see https://docs.sigmashake.com/cli",
                env::consts::OS,
                env::consts::ARCH,
            ),
        )
    })?;
    let cache_dir = cache_root()?;
    let bin = cache_dir.join(plat.bin_local);
    if bin.exists() {
        return Ok(bin);
    }
    let url = format!("{}/v{}/{}", RELEASE_BASE, CRATE_VERSION, plat.asset);
    eprintln!("sigmashake-ssg: fetching {}", url);
    let body = fetch(&url)?;
    extract(&plat, &body, &cache_dir)
}

fn run() -> io::Result<i32> {
    let bin = ensure_binary()?;
    let args: Vec<String> = env::args().skip(1).collect();
    #[cfg(unix)]
    {
        // exec replaces the current process — never returns on success.
        let err = Command::new(&bin).args(&args).exec();
        return Err(err);
    }
    #[cfg(not(unix))]
    {
        let status = Command::new(&bin).args(&args).status()?;
        Ok(status.code().unwrap_or(1))
    }
}

fn main() -> ExitCode {
    match run() {
        Ok(code) => ExitCode::from(code.clamp(0, 255) as u8),
        Err(e) => {
            let _ = writeln!(io::stderr(), "sigmashake-ssg: {e}");
            ExitCode::from(1)
        }
    }
}
