//! Versioned Claude Code hook adapter. For a Claude agent surface, writes a
//! per-surface CLAUDE_CONFIG_DIR with hooks that call `spacesh notify`, and
//! returns the env to inject. Isolated so hook-format drift is a local fix.
use std::path::PathBuf;
use spacesh_proto::ids::SurfaceId;

/// Is this command a Claude Code agent we should hook? (heuristic)
pub fn is_agent(command: &str, agent_label: Option<&str>) -> bool {
    let base = std::path::Path::new(command)
        .file_name()
        .and_then(|s| s.to_str())
        .unwrap_or(command);
    base == "claude" || agent_label == Some("claude")
}

/// Per-surface config dir under ~/.spacesh/hooks/<surface_id>.
fn dir_for(home: &PathBuf, sid: &SurfaceId) -> PathBuf {
    home.join(".spacesh").join("hooks").join(&sid.0)
}

/// Our Stop/Notification/UserPromptSubmit hooks as a JSON value (the `hooks` object).
fn our_hooks(spacesh_bin: &str) -> serde_json::Value {
    let entry = |state: &str| serde_json::json!({
        "hooks": [{
            "type": "command",
            "command": format!("{spacesh_bin} notify --surface $SPACESH_SURFACE_ID --state {state}")
        }]
    });
    serde_json::json!({
        "Stop": [entry("done")],
        "Notification": [entry("wait")],
        "UserPromptSubmit": [entry("work")],
    })
}

/// Our hooks as a standalone settings JSON string. Passed to `claude` via the
/// `--settings` flag so they layer ON TOP of the user's real config WITHOUT
/// relocating CLAUDE_CONFIG_DIR — which is the whole point: claude only reads the
/// macOS Keychain login (and onboarding state) for its DEFAULT config dir, so any
/// override left the agent "Not logged in". `--settings` keeps the default dir.
pub fn settings_json(spacesh_bin: &str) -> String {
    serde_json::to_string(&serde_json::json!({ "hooks": our_hooks(spacesh_bin) }))
        .unwrap_or_else(|_| "{\"hooks\":{}}".to_string())
}

/// Extra CLI args injecting our notify hooks into a spawned `claude`, leaving its
/// default config dir (Keychain auth + onboarding) untouched.
pub fn claude_settings_args(spacesh_bin: &str) -> Vec<String> {
    vec!["--settings".to_string(), settings_json(spacesh_bin)]
}

/// Remove the legacy per-surface hook dir (best-effort) on close. No longer
/// written, but cleans up dirs left by older builds that used CLAUDE_CONFIG_DIR.
pub fn cleanup(sid: &SurfaceId) {
    if let Some(home) = dirs::home_dir() {
        let _ = std::fs::remove_dir_all(dir_for(&home, sid));
    }
}

/// Absolute path to the `spacesh` CLI binary, sibling of the running daemon.
pub fn spacesh_bin() -> String {
    std::env::current_exe()
        .ok()
        .map(|p| p.with_file_name("spacesh"))
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_else(|| "spacesh".to_string())
}

/// zsh OSC 133 integration script, embedded at build time.
const ZSH_INTEGRATION: &str = include_str!("shell_integration/spacesh.zsh");

/// Is this command a zsh shell we can OSC-133-integrate via ZDOTDIR?
pub fn is_zsh(command: &str) -> bool {
    std::path::Path::new(command).file_name().and_then(|s| s.to_str()) == Some("zsh")
}

/// Prepare a per-surface ZDOTDIR whose .zshrc sources the user's rc then our
/// integration. Returns env pairs (ZDOTDIR, and SPACESH_ZDOTDIR=original) to
/// inject. Best-effort: empty vec on I/O failure.
pub fn shell_env(sid: &SurfaceId) -> Vec<(String, String)> {
    let Some(home) = dirs::home_dir() else { return vec![] };
    let dir = home.join(".spacesh").join("shellint").join(&sid.0);
    if std::fs::create_dir_all(&dir).is_err() {
        return vec![];
    }
    if std::fs::write(dir.join("spacesh.zsh"), ZSH_INTEGRATION).is_err() {
        return vec![];
    }
    let orig_zdotdir = std::env::var("ZDOTDIR").unwrap_or_else(|_| home.to_string_lossy().to_string());
    // .zshrc: source user rc, then our integration.
    let zshrc = format!(
        "[ -f \"{orig}/.zshrc\" ] && source \"{orig}/.zshrc\"\nsource \"{dir}/spacesh.zsh\"\n",
        orig = orig_zdotdir,
        dir = dir.to_string_lossy(),
    );
    if std::fs::write(dir.join(".zshrc"), zshrc).is_err() {
        return vec![];
    }
    vec![("ZDOTDIR".to_string(), dir.to_string_lossy().to_string())]
}

/// Remove the per-surface shellint dir (best-effort).
pub fn cleanup_shell(sid: &SurfaceId) {
    if let Some(home) = dirs::home_dir() {
        let _ = std::fs::remove_dir_all(home.join(".spacesh").join("shellint").join(&sid.0));
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn is_agent_matches_claude_by_command_or_label() {
        assert!(is_agent("claude", None));
        assert!(is_agent("/usr/local/bin/claude", None));
        assert!(is_agent("/bin/sh", Some("claude")));
        assert!(!is_agent("/bin/zsh", None));
        assert!(!is_agent("node", Some("codex")));
    }

    #[test]
    fn settings_json_has_three_events_with_abs_bin_and_env_var() {
        let j = settings_json("/abs/spacesh");
        assert!(j.contains("\"Stop\""));
        assert!(j.contains("\"Notification\""));
        assert!(j.contains("\"UserPromptSubmit\""));
        assert!(j.contains("/abs/spacesh notify --surface $SPACESH_SURFACE_ID --state done"));
        assert!(j.contains("--state wait"));
        assert!(j.contains("--state work"));
        // Valid JSON.
        let _: serde_json::Value = serde_json::from_str(&j).unwrap();
    }

    #[test]
    fn claude_settings_args_pass_hooks_via_settings_flag() {
        let args = claude_settings_args("/abs/spacesh");
        assert_eq!(args.len(), 2);
        assert_eq!(args[0], "--settings");
        // The second arg is valid JSON carrying all three hook events.
        let v: serde_json::Value = serde_json::from_str(&args[1]).unwrap();
        assert!(v["hooks"]["Stop"].is_array());
        assert!(args[1].contains("/abs/spacesh notify --surface $SPACESH_SURFACE_ID --state done"));
        assert!(args[1].contains("--state wait"));
        assert!(args[1].contains("--state work"));
    }

    #[test]
    fn is_zsh_detects_zsh() {
        assert!(is_zsh("/bin/zsh"));
        assert!(is_zsh("zsh"));
        assert!(!is_zsh("/bin/bash"));
    }

    #[test]
    fn shell_env_writes_zdotdir_with_integration() {
        let sid = SurfaceId(format!("s_shtest_{}", std::process::id()));
        let env = shell_env(&sid);
        assert_eq!(env.len(), 1);
        assert_eq!(env[0].0, "ZDOTDIR");
        let dir = std::path::PathBuf::from(&env[0].1);
        assert!(dir.join(".zshrc").exists());
        assert!(dir.join("spacesh.zsh").exists());
        cleanup_shell(&sid);
        assert!(!dir.exists());
    }
}
