//! `ckg processes` — traced execution flows from entry points.
//!
//! Entry points: `main`, `test_*`, names matching `--entry <pattern>`, and
//! route handlers (Symbol where qname appears as a Route.handler doc).
//! For each entry we run a depth-bounded forward BFS over `Calls` and print
//! the indented tree.

use std::collections::{HashMap, HashSet};
use std::path::PathBuf;

use anyhow::Result;
use clap::Args as ClapArgs;
use cozo::DataValue;

#[derive(ClapArgs, Debug)]
pub struct Args {
    /// Substring filter: only entries whose qname contains this run.
    #[arg(long)]
    pub entry: Option<String>,
    /// Maximum traversal depth.
    #[arg(long, default_value_t = 4)]
    pub depth: u32,
    /// Maximum entries to show.
    #[arg(long, default_value_t = 20)]
    pub limit: u32,
}

pub fn run(home: Option<PathBuf>, args: Args) -> Result<()> {
    let (_base, storage) = super::repo_lookup::open_for(home, None)?;

    // CR-I6: cap full-table scans so a million-symbol monorepo can't OOM
    // the CLI. Mirrors the MCP-side `MAX_SCAN_ROWS` ceiling.
    // CR-I4: read-only — use the immutable engine.
    const MAX_SCAN_ROWS: usize = 200_000;
    let symbols = storage.run_immutable(&format!(
        "?[id, qname, name, kind] := *Symbol{{id, qname, name, kind}} :limit {MAX_SCAN_ROWS}"
    ))?;
    let mut id_to_q: HashMap<String, (String, String, String)> = HashMap::new();
    let mut entries: Vec<(String, String)> = Vec::new();
    // CLI-H5: warn when the scan cap is reached so the user knows
    // results may be incomplete.
    if symbols.rows.len() >= MAX_SCAN_ROWS {
        eprintln!(
            "(warning: symbol scan capped at {MAX_SCAN_ROWS}; \
             process traces may be incomplete — narrow with --entry)"
        );
    }
    for r in &symbols.rows {
        let id = match &r[0] {
            DataValue::Str(s) => s.to_string(),
            _ => continue,
        };
        let qname = match &r[1] {
            DataValue::Str(s) => s.to_string(),
            _ => String::new(),
        };
        let name = match &r[2] {
            DataValue::Str(s) => s.to_string(),
            _ => String::new(),
        };
        let kind = match &r[3] {
            DataValue::Str(s) => s.to_string(),
            _ => String::new(),
        };
        id_to_q.insert(id.clone(), (qname.clone(), name.clone(), kind.clone()));
        if is_entry_point(&name, &kind, args.entry.as_deref()) {
            entries.push((id, qname));
        }
    }
    if entries.is_empty() {
        println!("(no entry points found — try --entry <name-substring>)");
        return Ok(());
    }
    entries.truncate((args.limit.max(1)) as usize); // #15: --limit 0 → 1

    // Edges: id → Vec<id>. Same CR-I6 cap; CR-I4 immutable.
    let calls = storage.run_immutable(&format!(
        "?[src, dst] := *Calls{{src, dst}} :limit {MAX_SCAN_ROWS}"
    ))?;
    let mut adj: HashMap<String, Vec<String>> = HashMap::new();
    for r in calls.rows {
        if let (DataValue::Str(s), DataValue::Str(d)) = (&r[0], &r[1]) {
            adj.entry(s.to_string()).or_default().push(d.to_string());
        }
    }

    // CR2 fix: clamp `args.depth` so a malicious / accidental
    // `--depth 1000000` can't overflow the recursion stack. 16 levels
    // is plenty for any realistic process trace; mirrors the MCP-side
    // `processes_tool` clamp.
    const MAX_DEPTH: u32 = 16;
    let depth = args.depth.min(MAX_DEPTH);
    for (entry_id, entry_q) in entries {
        println!("\n● {entry_q}");
        let path_visited: HashSet<String> = std::iter::once(entry_id.clone()).collect();
        bfs_print(&adj, &id_to_q, &entry_id, 1, depth, &path_visited);
    }
    Ok(())
}

fn is_entry_point(name: &str, kind: &str, filter: Option<&str>) -> bool {
    if let Some(f) = filter {
        return name.contains(f);
    }
    if !matches!(kind, "function" | "method") {
        return false;
    }
    name == "main" || name.starts_with("test_")
}

/// M10: Iterative tree print using explicit stack. Prevents stack
/// overflow on deeply-nested call graphs (same fix family as PARSE-C1).
fn bfs_print(
    adj: &HashMap<String, Vec<String>>,
    id_to_q: &HashMap<String, (String, String, String)>,
    root: &str,
    start_depth: u32,
    max_depth: u32,
    root_visited: &HashSet<String>,
) {
    // Stack entries: (node_id, depth, path_visited)
    let mut stack: Vec<(String, u32, HashSet<String>)> = Vec::new();
    stack.push((root.to_string(), start_depth, root_visited.clone()));

    while let Some((from, depth, path_visited)) = stack.pop() {
        if depth > max_depth {
            continue;
        }
        let Some(children) = adj.get(&from) else {
            continue;
        };
        let indent = "  ".repeat(depth as usize);
        // Push children in reverse so first child is processed first.
        for child in children.iter().rev() {
            if path_visited.contains(child) {
                continue;
            }
            let label = id_to_q
                .get(child)
                .map(|(q, _, _)| q.clone())
                .unwrap_or_else(|| child.clone());
            println!("{indent}└─ {label}");
            let mut branch_visited = path_visited.clone();
            branch_visited.insert(child.clone());
            stack.push((child.clone(), depth + 1, branch_visited));
        }
    }
}
