haj 0.2.5

fast, quiet, beautiful package manager and tui for blahArch.
use owo_colors::OwoColorize;
use std::path::PathBuf;
use std::process::Stdio;
use tokio::io::AsyncReadExt;
use tokio::process::Command;

pub async fn build(pkg_name: &str, is_verbose: bool) -> anyhow::Result<PathBuf> {
    // roooooot user check ahhhhh
    let is_root = unsafe { libc::geteuid() == 0 };
    if is_root {
        anyhow::bail!(
            "{} makepkg cannot run as root. please run haj as a normal user.",
            "".red().bold()
        );
    }

    crate::core::escalate::ensure_sudo().await?;

    let home = std::env::var("HOME").expect("HOME environment variable not set");
    let aur_cache_dir = PathBuf::from(home).join(".cache/haj/aur");
    tokio::fs::create_dir_all(&aur_cache_dir).await?;

    let pkg_dir = aur_cache_dir.join(pkg_name);

    let fetch_spinner =
        crate::ui::progress::spinner(&format!("fetching {} from aur...", pkg_name.bold()));

    if pkg_dir.exists() {
        let status = Command::new("git")
            .arg("-C")
            .arg(&pkg_dir)
            .arg("pull")
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status()
            .await?;

        if !status.success() {
            fetch_spinner.finish_and_clear();
            anyhow::bail!("failed to pull latest changes for {}", pkg_name);
        }
    } else {
        let aur_url = format!("https://aur.archlinux.org/{}.git", pkg_name);
        let status = Command::new("git")
            .arg("clone")
            .arg(&aur_url)
            .arg(&pkg_dir)
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status()
            .await?;

        if !status.success() {
            fetch_spinner.finish_and_clear();
            anyhow::bail!("failed to clone {}. does it exist?", pkg_name);
        }
    }
    fetch_spinner.finish_and_clear();

    let dep_spinner = crate::ui::progress::spinner("resolving dependencies...");

    let srcinfo_out = Command::new("makepkg")
        .arg("--printsrcinfo")
        .current_dir(&pkg_dir)
        .output()
        .await?;

    if !srcinfo_out.status.success() {
        dep_spinner.finish_and_clear();
        anyhow::bail!("failed to read PKGBUILD for {}", pkg_name);
    }

    let srcinfo_str = String::from_utf8_lossy(&srcinfo_out.stdout);
    let mut all_deps = Vec::new();
    for line in srcinfo_str.lines() {
        let line = line.trim();
        if (line.starts_with("depends =") || line.starts_with("makedepends ="))
            && let Some(dep) = line.split(" = ").nth(1)
        {
            all_deps.push(dep.to_string());
        }
    }

    let mut missing_deps = Vec::new();
    if !all_deps.is_empty() {
        let pacman_t = Command::new("pacman")
            .arg("-T")
            .args(&all_deps)
            .output()
            .await?;

        let missing_str = String::from_utf8_lossy(&pacman_t.stdout);
        for dep in missing_str.lines() {
            let dep = dep.trim();
            if !dep.is_empty() {
                missing_deps.push(dep.to_string());
            }
        }
    }
    dep_spinner.finish_and_clear();

    if !missing_deps.is_empty() {
        let install_spinner = crate::ui::progress::spinner(&format!(
            "installing {} missing dependencies...",
            missing_deps.len()
        ));

        let status = Command::new("sudo")
            .arg("pacman")
            .arg("-S")
            .arg("--noconfirm")
            .args(&missing_deps)
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status()
            .await?;

        install_spinner.finish_and_clear();

        if !status.success() {
            anyhow::bail!(
                "failed to install missing build dependencies. (one of them might be an aur package). run 'pacman -S {}' manually to debug.",
                missing_deps.join(" ")
            );
        }
    }

    if is_verbose {
        println!(
            "{} [verbose] building {} with native output...",
            "::".blue(),
            pkg_name.bold()
        );
        let mut child = Command::new("makepkg")
            .current_dir(&pkg_dir)
            .args(["-cf", "--noconfirm", "--nocheck"])
            .stdin(Stdio::inherit())
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .spawn()?;

        let status = child.wait().await?;
        if !status.success() {
            anyhow::bail!("makepkg failed with code {}", status.code().unwrap_or(1));
        }
    } else {
        let build_spinner =
            crate::ui::progress::spinner(&format!("preparing to build {}...", pkg_name.bold()));

        let mut child = Command::new("makepkg")
            .current_dir(&pkg_dir)
            .args(["-cf", "--noconfirm", "--nocheck"])
            .stdin(Stdio::null()) // keyboard disconnect
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()?;

        let mut stdout = child.stdout.take().unwrap();
        let mut stderr = child.stderr.take().unwrap();

        let err_handle = tokio::spawn(async move {
            let mut err_str = String::new();
            let mut buf = [0u8; 1024];
            while let Ok(n) = stderr.read(&mut buf).await {
                if n == 0 {
                    break;
                }
                err_str.push_str(&String::from_utf8_lossy(&buf[..n]));
            }
            err_str
        });

        let mut buf = [0u8; 128];
        let mut current_line = String::new();

        while let Ok(n) = stdout.read(&mut buf).await {
            if n == 0 {
                break;
            }
            let chunk = String::from_utf8_lossy(&buf[..n]);

            for c in chunk.chars() {
                if c == '\n' || c == '\r' {
                    let clean = current_line.trim();
                    if clean.starts_with("==> Making package:") {
                        build_spinner.set_message(format!(
                            "{} initializing build environment...",
                            "::".blue()
                        ));
                    } else if clean.starts_with("==> Retrieving sources") {
                        build_spinner
                            .set_message(format!("{} downloading source code...", "::".blue()));
                    } else if clean.starts_with("==> Starting build()") {
                        build_spinner.set_message(format!(
                            "{} compiling {} (this may take a while)...",
                            "::".blue(),
                            pkg_name.bold()
                        ));
                    } else if clean.starts_with("==> Starting package()") {
                        build_spinner.set_message(format!("{} packaging binary...", "::".blue()));
                    }
                    current_line.clear();
                } else {
                    current_line.push(c);
                }
            }
        }

        let status = child.wait().await?;
        build_spinner.finish_and_clear();

        let err_output = err_handle.await.unwrap_or_default();

        if !status.success() {
            anyhow::bail!(
                "makepkg failed (code {}):\n{}",
                status.code().unwrap_or(1),
                err_output.trim().red()
            );
        }
    }

    let mut built_pkg = None;
    let mut entries = tokio::fs::read_dir(&pkg_dir).await?;
    while let Some(entry) = entries.next_entry().await? {
        let path = entry.path();
        if path.is_file() {
            let file_name = path.file_name().unwrap_or_default().to_string_lossy();
            if file_name.contains(".pkg.tar.") && !file_name.contains("-debug-") {
                built_pkg = Some(path);
                break;
            }
        }
    }

    match built_pkg {
        Some(path) => Ok(path),
        None => anyhow::bail!("build succeeded but could not locate the .pkg.tar archive"),
    }
}