//! Simulated-merge executor + glob matcher.
//!
//! Schema §Verify execution — worktree short-circuit → simulated merge:
//! orchestrator creates temp worktree off main, applies agent's diff, runs
//! verifies from that vantage to catch integration regressions invisible
//! in agent's isolated worktree.

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

/// Create a temp worktree off `main_repo` at HEAD of `main`, apply the agent's
/// diff, return the temp worktree path. Caller cleans up.
pub fn run_simulated_merge(
    agent_id: &str,
    agent_worktree: &Path,
    main_repo: &Path,
) -> Result<PathBuf> {
    let tmp = std::env::temp_dir().join(format!("kei-test-merge-{agent_id}"));
    let _ = std::fs::remove_dir_all(&tmp);
    run_git(main_repo, &["worktree", "add", "-d", tmp.to_str().unwrap(), "main"])
        .context("git worktree add failed")?;
    let diff = run_git(agent_worktree, &["diff", "main"])
        .context("git diff against main failed")?;
    if !diff.trim().is_empty() {
        apply_diff(&tmp, &diff)?;
    }
    Ok(tmp)
}

/// Apply a unified diff to `dir` via `git apply --index`. Empty diff is a no-op.
pub fn apply_diff(dir: &Path, diff: &str) -> Result<()> {
    use std::io::Write;
    let mut child = Command::new("git")
        .arg("apply")
        .arg("--index")
        .current_dir(dir)
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .context("spawn git apply")?;
    if let Some(mut stdin) = child.stdin.take() {
        stdin.write_all(diff.as_bytes()).context("write diff stdin")?;
    }
    let out = child.wait_with_output().context("git apply wait")?;
    if !out.status.success() {
        anyhow::bail!("git apply failed: {}", String::from_utf8_lossy(&out.stderr));
    }
    Ok(())
}

/// Run `git <args>` in `dir`, return stdout as UTF-8 string.
pub fn run_git(dir: &Path, args: &[&str]) -> Result<String> {
    let out = Command::new("git")
        .args(args)
        .current_dir(dir)
        .output()
        .with_context(|| format!("git {}", args.join(" ")))?;
    if !out.status.success() {
        anyhow::bail!(
            "git {} failed: {}",
            args.join(" "),
            String::from_utf8_lossy(&out.stderr)
        );
    }
    Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}

/// Shell-style glob match. Supports `**` (any directories) and `*` (any chars
/// except `/`). Bracketed classes and `?` not supported — task specs use
/// simple patterns.
pub fn glob_match(pattern: &str, path: &str) -> bool {
    let re = glob_to_regex(pattern);
    match regex::Regex::new(&re) {
        Ok(r) => r.is_match(path),
        Err(_) => false,
    }
}

fn glob_to_regex(pattern: &str) -> String {
    let mut out = String::from("^");
    let bytes = pattern.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        let c = bytes[i] as char;
        if c == '*' && i + 1 < bytes.len() && bytes[i + 1] as char == '*' {
            out.push_str(".*");
            i += 2;
            if i < bytes.len() && bytes[i] as char == '/' {
                i += 1;
            }
        } else if c == '*' {
            out.push_str("[^/]*");
            i += 1;
        } else if "().+?|^$\\[]{}".contains(c) {
            out.push('\\');
            out.push(c);
            i += 1;
        } else {
            out.push(c);
            i += 1;
        }
    }
    out.push('$');
    out
}
