//! `wai package build` and `wai package verify` (the catalogue's `package.build` and
//! `package.verify`): the reference packager, `wai-pack` (extensions/packaging.md), run as
//! itself, so a package made or checked through this door is the packager's own, byte for
//! byte. The packer is found as `wai dev` finds the relay: `--packer BIN`, then
//! `$WAI_PACK_BIN`, then `wai-pack` beside this binary, then on `PATH`. `wai lock`, `wai check`
//! and `wai build` reach it here too: a policy's canonical digest ([`policy`]), and a pack and its
//! verification with the packer's output kept off standard output ([`quiet`]).

use std::path::{Path, PathBuf};
use std::process::Command;

use crate::dx::envelope::OpError;

/// A packaging policy as the packer reads it.
#[derive(Clone, Debug)]
pub struct Policy {
    /// Its `schema`, the version of the policy's format.
    pub schema: String,
    /// `BLAKE3("wai:pack-policy\x01" ‖ canonical JSON)`, in hexadecimal (packaging §3).
    pub blake3: String,
    /// The source each component names, by the component's id, relative to the policy.
    pub sources: Vec<(String, String)>,
}

/// `wai-pack policy <path>`: the policy as the packer reads it, or the packer's refusal.
pub fn policy(bin: &Path, path: &Path) -> Result<Policy, String> {
    let o = Command::new(bin).arg("policy").arg(path).output().map_err(|e| format!("running {}: {e}", bin.display()))?;
    let stdout = String::from_utf8_lossy(&o.stdout);
    if !o.status.success() {
        return Err(String::from_utf8_lossy(&o.stderr).trim().to_string());
    }
    let mut lines = stdout.lines();
    let (Some(json), Some(digest)) = (lines.next(), lines.next()) else {
        return Err(format!("{} policy printed no canonical JSON and digest", bin.display()));
    };
    let v: serde_json::Value = serde_json::from_str(json).map_err(|e| format!("{} policy: {e}", bin.display()))?;
    let sources = v["component"].as_array().map(|c| c.iter().filter_map(|c| Some((c["id"].as_str()?.to_string(), c["source"].as_str()?.to_string()))).collect()).unwrap_or_default();
    Ok(Policy { schema: v["schema"].as_str().unwrap_or_default().to_string(), blake3: digest.trim().to_string(), sources })
}

/// Run the packer with its output on standard error, for a command whose standard output is an
/// envelope or a server's: `Ok(true)` when it exits 0, `Ok(false)` when it reports a refusal.
pub fn quiet(bin: &Path, verb: &str, args: &[String]) -> Result<bool, OpError> {
    let o = Command::new(bin).arg(verb).args(args).output().map_err(|e| OpError::internal(format!("running {}: {e}", bin.display())))?;
    eprint!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr));
    match o.status.code() {
        Some(0) => Ok(true),
        Some(1) => Ok(false),
        c => Err(OpError::internal(format!("{} {verb} exited {c:?}", bin.display()))),
    }
}

/// The packer, and the arguments that are its own.
fn packer(args: &[String]) -> Result<(PathBuf, Vec<String>), OpError> {
    let mut rest = Vec::new();
    let mut given = None;
    let mut i = 0;
    while i < args.len() {
        if args[i] == "--packer" {
            given = Some(args.get(i + 1).ok_or_else(|| OpError::usage("--packer takes the packer's binary", ""))?.clone());
            i += 2;
        } else {
            rest.push(args[i].clone());
            i += 1;
        }
    }
    find(given.as_deref()).map(|p| (p, rest))
}

/// The packer: `given`, then `$WAI_PACK_BIN`, then `wai-pack` beside this binary, then on `PATH`.
pub fn find(given: Option<&str>) -> Result<PathBuf, OpError> {
    if let Some(p) = given.map(PathBuf::from).or_else(|| std::env::var_os("WAI_PACK_BIN").map(PathBuf::from)) {
        return if p.is_file() { Ok(p) } else { Err(OpError::from(format!("wai-pack: {} is not a file", p.display()))) };
    }
    let file = format!("wai-pack{}", std::env::consts::EXE_SUFFIX);
    let mut dirs: Vec<PathBuf> = std::env::current_exe().ok().and_then(|e| e.parent().map(PathBuf::from)).into_iter().collect();
    if let Some(path) = std::env::var_os("PATH") {
        dirs.extend(std::env::split_paths(&path));
    }
    dirs.iter()
        .map(|d| d.join(&file))
        .find(|p| p.is_file())
        .ok_or_else(|| OpError::from("wai-pack is not beside `wai` nor on PATH: install the archive that holds it, or give --packer BIN".to_string()))
}

/// Run the packer: `Ok(true)` when it exits 0, `Ok(false)` when it reports a refusal (exit 1).
fn run(bin: &PathBuf, verb: &str, args: &[String]) -> Result<bool, OpError> {
    let st = Command::new(bin).arg(verb).args(args).status().map_err(|e| OpError::internal(format!("running {}: {e}", bin.display())))?;
    match st.code() {
        Some(0) => Ok(true),
        Some(1) => Ok(false),
        c => Err(OpError::internal(format!("{} {verb} exited {c:?}", bin.display()))),
    }
}

/// `package.build`: `wai-pack pack` with these arguments. A dry run names the directory it
/// would write and runs nothing.
pub fn build(args: &[String]) -> Result<bool, OpError> {
    let (bin, rest) = packer(args)?;
    let out = rest.iter().position(|a| a == "--out").and_then(|i| rest.get(i + 1)).ok_or_else(|| OpError::usage("package build needs --out <dir>", ""))?;
    for f in ["--policy", "--key"] {
        if !rest.iter().any(|a| a == f) {
            return Err(OpError::usage(format!("package build needs {f}"), ""));
        }
    }
    if crate::out::dry_run() {
        crate::out::would_write(out);
        println!("dry run: {} pack would write the package into {out}", bin.display());
        return Ok(true);
    }
    run(&bin, "pack", &rest)
}

/// `package.verify`: `wai-pack verify` with these arguments.
pub fn verify(args: &[String]) -> Result<bool, OpError> {
    let (bin, rest) = packer(args)?;
    if rest.is_empty() {
        return Err(OpError::usage("package verify needs the package's directory", ""));
    }
    run(&bin, "verify", &rest)
}
