//! Thin subprocess wrapper around the `kei-ledger` binary.
//!
//! kei-ledger is a bin-only crate (no lib.rs at the time kei-spawn was
//! introduced). We shell to it rather than replicate SQL — same process
//! model users expect, same DB file, same env contract (`KEI_LEDGER_DB`).
//!
//! Every call surfaces stderr on failure so orchestrator sees the real
//! ledger error (branch too long, duplicate id, etc.), not a wrapped one.

use anyhow::{anyhow, Result};
use std::process::Command;

/// Resolve `kei-ledger` executable. Env override → CARGO env (tests) → PATH.
pub fn ledger_bin() -> String {
    if let Ok(b) = std::env::var("KEI_LEDGER_BIN") {
        return b;
    }
    // CARGO_BIN_EXE_kei-ledger is set for integration tests under workspace.
    if let Ok(b) = std::env::var("CARGO_BIN_EXE_kei-ledger") {
        return b;
    }
    "kei-ledger".into()
}

/// Test / sandbox escape hatch: when set, every ledger call is a no-op.
/// Integration tests use this to avoid needing the real kei-ledger binary
/// on PATH. Production callers MUST NOT set this env var.
fn is_noop() -> bool {
    std::env::var("KEI_SPAWN_LEDGER_NOOP")
        .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
        .unwrap_or(false)
}

/// Run `kei-ledger fork` with DNA + worktree metadata.
pub fn fork(
    id: &str,
    branch: &str,
    parent: Option<&str>,
    spec_sha: &str,
    worktree: Option<&str>,
    dna: Option<&str>,
) -> Result<()> {
    if is_noop() {
        let _ = (id, branch, parent, spec_sha, worktree, dna);
        return Ok(());
    }
    let mut cmd = Command::new(ledger_bin());
    cmd.args(["fork", id, branch, "--spec-sha", spec_sha]);
    if let Some(p) = parent {
        cmd.args(["--parent", p]);
    }
    if let Some(w) = worktree {
        cmd.args(["--worktree", w]);
    }
    if let Some(d) = dna {
        cmd.args(["--dna", d]);
    }
    run(&mut cmd, "fork")
}

/// Run `kei-ledger done <id> --summary <s>`.
pub fn done(id: &str, summary: &str) -> Result<()> {
    if is_noop() {
        let _ = (id, summary);
        return Ok(());
    }
    let mut cmd = Command::new(ledger_bin());
    cmd.args(["done", id, "--summary", summary]);
    run(&mut cmd, "done")
}

/// Run `kei-ledger fail <id> --reason <r>`.
pub fn fail(id: &str, reason: &str) -> Result<()> {
    if is_noop() {
        let _ = (id, reason);
        return Ok(());
    }
    let mut cmd = Command::new(ledger_bin());
    cmd.args(["fail", id, "--reason", reason]);
    run(&mut cmd, "fail")
}

/// Run `kei-ledger list --status running`. Returns raw stdout lines.
pub fn list_running() -> Result<String> {
    if is_noop() {
        return Ok(String::from("(noop: KEI_SPAWN_LEDGER_NOOP=1)\n"));
    }
    let mut cmd = Command::new(ledger_bin());
    cmd.args(["list", "--status", "running"]);
    let out = cmd.output().map_err(|e| anyhow!("spawn kei-ledger: {e}"))?;
    if !out.status.success() {
        let stderr = String::from_utf8_lossy(&out.stderr).to_string();
        return Err(anyhow!("kei-ledger list failed: {stderr}"));
    }
    Ok(String::from_utf8_lossy(&out.stdout).to_string())
}

fn run(cmd: &mut Command, stage: &str) -> Result<()> {
    let out = cmd
        .output()
        .map_err(|e| anyhow!("spawn kei-ledger {stage}: {e}"))?;
    if out.status.success() {
        return Ok(());
    }
    let stderr = String::from_utf8_lossy(&out.stderr).to_string();
    Err(anyhow!("kei-ledger {stage} failed: {stderr}"))
}
