//! Process tree utilities for finding and checking process status.
//!
//! This module provides cross-platform utilities for process management,
//! including checking if processes are alive and walking the process tree
//! to find the Claude Code process.

use std::process::Command;

/// Information about the Claude Code process
#[derive(Debug, Clone)]
pub struct ClaudeProcessInfo {
    /// PID of the Claude Code (node) process
    pub pid: i32,
    /// Command name (e.g., "node", "claude")
    pub comm: String,
}

/// Check if a process with the given PID is still running.
///
/// This uses the `ps` command to check process existence, which works
/// on both macOS and Linux.
///
/// # Examples
///
/// ```
/// use mi6_core::is_process_alive;
///
/// // Our own process should be alive
/// let own_pid = std::process::id() as i32;
/// assert!(is_process_alive(own_pid));
///
/// // A non-existent PID should not be alive
/// assert!(!is_process_alive(999999999));
/// ```
pub fn is_process_alive(pid: i32) -> bool {
    Command::new("ps")
        .args(["-p", &pid.to_string()])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()
        .is_ok_and(|status| status.success())
}

/// Get the parent process ID.
///
/// This is more reliable than the PPID environment variable,
/// which is a shell variable that isn't exported to child processes.
///
/// # Examples
///
/// ```
/// use mi6_core::get_parent_pid;
///
/// // We should always have a parent process
/// let ppid = get_parent_pid();
/// assert!(ppid.is_some());
/// ```
pub fn get_parent_pid() -> Option<i32> {
    let pid = std::process::id() as i32;
    get_process_info(pid).map(|info| info.ppid)
}

/// Walk up the process tree to find the Claude Code process.
///
/// Claude Code runs as a node process, so we look for that in our
/// process ancestry.
///
/// Returns `None` if we can't find a node process in the ancestry.
pub fn find_claude_process() -> Option<ClaudeProcessInfo> {
    find_claude_process_from_pid(get_parent_pid()?)
}

/// Walk up from a given PID to find the Claude Code process.
fn find_claude_process_from_pid(start_pid: i32) -> Option<ClaudeProcessInfo> {
    let mut current_pid = start_pid;
    let mut visited = std::collections::HashSet::new();

    // Walk up the process tree (limit iterations to prevent infinite loops)
    for _ in 0..50 {
        if current_pid <= 1 || visited.contains(&current_pid) {
            break;
        }
        visited.insert(current_pid);

        // Get process info
        let Some(info) = get_process_info(current_pid) else {
            break;
        };

        // Check if this is a framework process (Claude, Codex, Gemini)
        if is_framework_process(&info.comm) {
            return Some(ClaudeProcessInfo {
                pid: current_pid,
                comm: info.comm,
            });
        }

        // Move to parent
        current_pid = info.ppid;
    }

    None
}

struct ProcessInfo {
    ppid: i32,
    comm: String,
}

/// Get process info using ps command (works on macOS and Linux)
fn get_process_info(pid: i32) -> Option<ProcessInfo> {
    // Get PPID and command name in one call
    let output = Command::new("ps")
        .args(["-o", "ppid=,comm=", "-p", &pid.to_string()])
        .output()
        .ok()?;

    if !output.status.success() {
        return None;
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let line = stdout.trim();

    // Parse "  123 node" format
    let mut parts = line.split_whitespace();
    let ppid: i32 = parts.next()?.parse().ok()?;
    let comm = parts.next()?.to_string();

    Some(ProcessInfo { ppid, comm })
}

/// Check if a process name looks like a supported AI coding framework.
///
/// Matches:
/// - Claude Code: runs as "node" or contains "claude"/"electron"
/// - Codex: runs as "codex"
/// - Gemini: runs as "gemini"
/// - Cursor: runs as "Cursor" or "Cursor Helper"
fn is_framework_process(comm: &str) -> bool {
    let comm_lower = comm.to_lowercase();

    comm_lower == "node"
        || comm_lower == "codex"
        || comm_lower == "gemini"
        || comm_lower.contains("claude")
        || comm_lower.contains("electron")
        || comm_lower.contains("cursor")
}

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

    #[test]
    fn test_is_process_alive_self() {
        // Our own process should be alive
        assert!(is_process_alive(std::process::id() as i32));
    }

    #[test]
    fn test_is_process_alive_invalid_pid() {
        // A non-existent PID should not be alive
        assert!(!is_process_alive(999_999_999));
    }

    #[test]
    fn test_get_parent_pid() -> Result<(), String> {
        // We should always have a parent process
        let ppid = get_parent_pid().ok_or("expected parent pid")?;
        assert!(ppid > 0);
        Ok(())
    }

    #[test]
    fn test_get_process_info_self() {
        // We should be able to get info about our own process
        let pid = std::process::id() as i32;
        let info = get_process_info(pid);
        assert!(info.is_some(), "should get info for own process");
    }

    #[test]
    fn test_get_process_info_invalid_pid() {
        // Invalid PID should return None
        let info = get_process_info(999_999_999);
        assert!(info.is_none());
    }
}
